diff --git a/ui/assets/v1/footer b/ui/assets/v1/footer new file mode 100644 index 0000000000..9662f4740b --- /dev/null +++ b/ui/assets/v1/footer @@ -0,0 +1,3 @@ + + + diff --git a/ui/assets/v1/header b/ui/assets/v1/header new file mode 100644 index 0000000000..00b24fd786 --- /dev/null +++ b/ui/assets/v1/header @@ -0,0 +1,7 @@ + + + + +{{.Title}} + + diff --git a/ui/assets/v1/index.tmpl b/ui/assets/v1/index.tmpl new file mode 100644 index 0000000000..a203cdc048 --- /dev/null +++ b/ui/assets/v1/index.tmpl @@ -0,0 +1,7 @@ +{{- template "header" . -}} +
+ + + +
+{{ template "footer" . -}} diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000000..9d902da177 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,99 @@ +// Package ui is a barebones HTML UI for clair. +package ui + +import ( + "embed" + "fmt" + "html/template" + "io" + "io/fs" + "net/http" + "path" + "strings" + + "github.com/quay/clair/v4/cmd" +) + +type V1 struct { + mux *http.ServeMux + tmpl *template.Template + sys fs.FS + ctx map[string]*PageContext +} + +type PageContext struct { + Title string + Aux map[string]interface{} +} + +var ( + _ http.Handler = (*V1)(nil) + + //go:embed assets/v1 + v1assets embed.FS +) + +func New() (*V1, error) { + var err error + h := V1{ + mux: http.NewServeMux(), + } + h.sys, err = fs.Sub(v1assets, "assets/v1") + if err != nil { + return nil, err + } + t := template.New("") + t.Funcs(template.FuncMap(map[string]interface{}{ + "Version": func() string { + return cmd.Version + }, + })) + h.tmpl, err = t.ParseFS(h.sys, "*.tmpl", "header", "footer") + if err != nil { + return nil, err + } + + h.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + p := strings.TrimLeft(r.URL.Path, "/") + try := []string{ + p + ".tmpl", + path.Join(p, "index.tmpl"), + } + for _, n := range try { + t := h.tmpl.Lookup(n) + if t == nil { + continue + } + w.Header().Set("content-type", "text/html") + if err := t.Execute(w, h.ctx[n]); err != nil { + srv := r.Context().Value(http.ServerContextKey).(*http.Server) + srv.ErrorLog.Printf("error executing template %q: %v", n, err) + } + return + } + if f, err := h.sys.Open(p); err == nil { + defer f.Close() + io.Copy(w, f) + return + } + w.WriteHeader(http.StatusNotFound) + }) + + h.ctx = map[string]*PageContext{ + "index.tmpl": { + Title: "Clair", + }, + } + + return &h, nil +} + +func (h *V1) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + case http.MethodPost: + default: + http.Error(w, fmt.Sprintf("disallowed method: %q", r.Method), http.StatusMethodNotAllowed) + } + h.mux.ServeHTTP(w, r) +} diff --git a/ui/ui_test.go b/ui/ui_test.go new file mode 100644 index 0000000000..0aca4e1d9e --- /dev/null +++ b/ui/ui_test.go @@ -0,0 +1,30 @@ +package ui_test + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/quay/clair/v4/ui" +) + +func TestIndex(t *testing.T) { + h, err := ui.New() + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + rec.Body = new(bytes.Buffer) + + h.ServeHTTP(rec, req) + res := rec.Result() + + t.Logf("%+#q", rec.Body.String()) + + if res.StatusCode != http.StatusOK { + t.Fail() + } +}