diff --git a/Dockerfile b/Dockerfile index 07d9826f9bcab938f729ec20c2427a4f70bb00f3..a2d942beb4a90171676ee5bab74f1afa5457f227 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.19 as build +FROM golang:1.23 as build ADD . /src RUN cd /src && go build -tags netgo -o gitlab-review-float-env-dashboard ./main.go && strip gitlab-review-float-env-dashboard diff --git a/debian/changelog b/debian/changelog index d2792618ee127f550f0c313bae5529aefbcff149..d0ac3557b2a19cd506dc416851e0a2cf75e62215 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +gitlab-review-float-env-dashboard (0.2) unstable; urgency=medium + + * Refactor template handling. + + -- ale <ale@incal.net> Thu, 09 Jan 2025 08:59:46 +0100 + gitlab-review-float-env-dashboard (0.1) unstable; urgency=medium * First release. diff --git a/main.go b/main.go index c50238a71b207bb9361aa6128bfc31008ceac103..c6f981abf075550397a0f718d1eb8585f3020dc3 100644 --- a/main.go +++ b/main.go @@ -10,21 +10,64 @@ import ( "io" "log" "net/http" + "os" ) var ( - addr = flag.String("addr", ":3499", "address to listen on") - jumpHost = flag.String("jump-host", "localhost", "SSH jump host") - hostname = flag.String("hostname", "", "(deprecated) see --jump-host") - hostConnFmt = flag.String( - "host-ssh-format", - "ssh -o UserKnownHostsFile=/dev/null -i ~/.vagrant.d/insecure_private_key -J {{.JumpHost}} root@{{.Host.IP}}", - "format string for host-specific SSH connections") - enableSOCKS = flag.Bool("enable-socks", false, "show SOCKS5 connection params") - title = flag.String("title", "VMine test cluster", "title string") + addr = flag.String("addr", ":3499", "address to listen on") + gitlabURL = flag.String("gitlab-url", "https://git.autistici.org", "Gitlab public URL") + title = flag.String("title", "VMine test cluster", "title string") + contentTplFile = flag.String("template", "", "custom content template file, use default if empty") ) var ( + defaultContentTplSrc = ` +{{$jumpHost := "miscredenza.investici.org"}} +{{$socksIP := (idx .Inventory 0).IP}} + +<h4>Hosts</h4> + +<p> + You can connect to individual hosts over SSH via the jump host <i>{{$jumpHost}}</i>.<br> + Download the required SSH private key here: + <a href="{{.GitlabURL}}/{{.ProjectName}}/-/jobs/{{.JobID}}/artifacts/browse/build-{{.JobID}}/ssh/vmkey">vmkey</a>. + Remember to <code>chmod 600</code> the private key file after you download it. +</p> + +<table class="table"> +<tbody> +{{range $index, $host := .Inventory}} +<tr> + <td> + <div class="host"> + <b class="name">{{$host.Name}}</b> {{$host.IP}}<br> + <span class="attrs"> + {{$host.CPU}} cores, {{$host.RAM}}M ram + </span> + </div> + </td> + <td> + <pre>ssh -o UserKnownHostsFile=/dev/null -i vmkey -J {{$jumpHost}} root@{{$host.IP}}</pre> + </td> +</tr> +{{end}} +</tbody> +</table> + +<h4>HTTP connection (via SOCKS5 proxy)</h4> + +<p> + Forward a local port to the SOCKS5 proxy running on {{$socksIP}}, and run a + private browser that uses it: +</p> + +<pre>ssh -nN -L 9051:{{$socksIP}}:9051 {{$jumpHost}} & +chromium --user-data-dir=$(mktemp -d) --no-first-run --no-default-browser-check \ + --proxy-server=socks5://localhost:9051 https://... +</pre> +` + defaultContentTpl = template.Must(template.New("").Parse(defaultContentTplSrc)) + dashTplSrc = `<!doctype html> <html lang="en"> <head> @@ -44,64 +87,28 @@ body { background: white; } <h1>{{.Title}}</h1> - <h4>Hosts</h4> - - <p> - You can connect to individual hosts over SSH via the jump host <i>{{.JumpHost}}</i>.<br> - Download the required SSH private key here: - <a href="https://github.com/hashicorp/vagrant/blob/main/keys/vagrant">Vagrant default insecure private key</a>, - if you do not already have it, and save it to <i>~/.vagrant.d/insecure_private_key</i>. - </p> - - <table class="table"> - <tbody> - {{range $index, $host := .Inventory}} - <tr> - <td> - <div class="host"> - <b class="name">{{$host.Name}}</b> {{$host.IP}}<br> - <span class="attrs"> - {{$host.CPU}} cores, {{$host.RAM}}M ram - </span> - </div> - </td> - <td> - <pre>{{hostConn $host}}</pre> - </td> - </tr> - {{end}} - </tbody> - </table> - - {{if .EnableSOCKS}} - <h4>HTTP connection (via SOCKS5 proxy)</h4> - - <p> - Forward a local port to the SOCKS5 proxy running on {{.SocksIP}}, and run a - private browser that uses it: - </p> - - <pre>ssh -nN -L 9051:{{.SocksIP}}:9051 {{.JumpHost}} & -chromium --user-data-dir=$(mktemp -d) --no-first-run --no-default-browser-check \ - --proxy-server=socks5://localhost:9051 https://... -</pre> - {{end}} + {{.Content}} + </body> </html>` - dashTpl = template.Must(template.New("").Funcs(map[string]interface{}{ - "hostConn": hostConnString, - }).Parse(dashTplSrc)) - hostConnTpl *template.Template + dashTpl = template.Must(template.New("").Parse(dashTplSrc)) + + contentTpl *template.Template ) -func hostConnString(host *hostinfo) string { - var buf bytes.Buffer - hostConnTpl.Execute(&buf, map[string]interface{}{ - "JumpHost": *jumpHost, - "Host": host, - }) - return buf.String() +func parseTemplate() error { + if *contentTplFile == "" { + contentTpl = defaultContentTpl + return nil + } + + data, err := os.ReadFile(*contentTplFile) + if err != nil { + return err + } + contentTpl, err = template.New("").Parse(string(data)) + return err } type hostinfo struct { @@ -112,6 +119,12 @@ type hostinfo struct { IP string `json:"ip"` } +type payload struct { + Inventory []*hostinfo `json:"inv"` + JobID string `json:"job"` + ProjectName string `json:"proj"` +} + func decompress(input []byte) ([]byte, error) { var out bytes.Buffer r := flate.NewReader(bytes.NewReader(input)) @@ -120,7 +133,7 @@ func decompress(input []byte) ([]byte, error) { return out.Bytes(), err } -func decodePayload(encoded string) ([]*hostinfo, error) { +func decodePayload(encoded string) (*payload, error) { compressed, err := base64.URLEncoding.DecodeString(encoded) if err != nil { return nil, err @@ -131,15 +144,18 @@ func decodePayload(encoded string) ([]*hostinfo, error) { return nil, err } - var inventory []*hostinfo - err = json.Unmarshal(decompressed, &inventory) - return inventory, err + var p payload + if err := json.Unmarshal(decompressed, &p); err != nil { + return nil, err + } + + return &p, err } func handleRequest(w http.ResponseWriter, req *http.Request) { - inventory, err := decodePayload(req.URL.Path) + payload, err := decodePayload(req.URL.Path) if err != nil { - log.Printf("error decoding payload: %s", err) + log.Printf("error decoding payload: %v", err) http.Error(w, "Bad Request", http.StatusBadRequest) return } @@ -147,19 +163,33 @@ func handleRequest(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") - // nolint: errcheck - dashTpl.Execute(w, struct { + // Render internal template. + var buf bytes.Buffer + if err := contentTpl.Execute(&buf, struct { Inventory []*hostinfo - SocksIP string - JumpHost string - EnableSOCKS bool + ProjectName string + JobID string Title string + GitlabURL string }{ - Inventory: inventory, - SocksIP: inventory[0].IP, - JumpHost: *jumpHost, - EnableSOCKS: *enableSOCKS, + Inventory: payload.Inventory, + ProjectName: payload.ProjectName, + JobID: payload.JobID, Title: *title, + GitlabURL: *gitlabURL, + }); err != nil { + log.Printf("template error: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // nolint: errcheck + dashTpl.Execute(w, struct { + Title string + Content template.HTML + }{ + Title: *title, + Content: template.HTML(buf.String()), }) } @@ -167,14 +197,8 @@ func main() { log.SetFlags(0) flag.Parse() - if *hostname != "" { - *jumpHost = *hostname - } - - var err error - hostConnTpl, err = template.New("hostconn").Parse(*hostConnFmt) - if err != nil { - log.Fatalf("error in host connection template: %v", err) + if err := parseTemplate(); err != nil { + log.Fatalf("template error: %v", err) } http.Handle("/dash/", http.StripPrefix("/dash/", http.HandlerFunc(handleRequest)))