From 926a58bad65861a0e9d77fcf711de98e379b39a1 Mon Sep 17 00:00:00 2001 From: christoph Date: Thu, 8 Feb 2024 16:31:41 +0100 Subject: [PATCH] feat(ui): add package detail page #172 --- api/v1alpha1/package_manifest.go | 13 +- api/v1alpha1/zz_generated.deepcopy.go | 20 ++ cmd/glasskube/cmd/serve.go | 5 +- .../packages.glasskube.dev_packageinfos.yaml | 14 + go.mod | 1 + go.sum | 6 +- internal/controller/packageinfo_controller.go | 2 +- internal/repo/repo.go | 21 +- internal/web/components/installbutton.go | 46 ---- .../components/pkg_detail_btns/controller.go | 55 ++++ .../components/pkg_overview_btn/controller.go | 52 ++++ internal/web/server.go | 257 ++++++++++-------- internal/web/templates.go | 47 ++++ .../templates/components/installbutton.html | 55 ---- .../templates/components/pkg-detail-btns.html | 36 +++ .../components/pkg-overview-btn.html | 29 ++ internal/web/templates/layout/base.html | 19 +- internal/web/templates/pages/package.html | 73 +++++ internal/web/templates/pages/packages.html | 40 +-- pkg/client/package_status.go | 12 + pkg/describe/describe.go | 40 +++ pkg/list/list.go | 6 +- pkg/manifest/manifest.go | 20 ++ schema/package-manifest/schema.json | 25 ++ 24 files changed, 640 insertions(+), 254 deletions(-) delete mode 100644 internal/web/components/installbutton.go create mode 100644 internal/web/components/pkg_detail_btns/controller.go create mode 100644 internal/web/components/pkg_overview_btn/controller.go create mode 100644 internal/web/templates.go delete mode 100644 internal/web/templates/components/installbutton.html create mode 100644 internal/web/templates/components/pkg-detail-btns.html create mode 100644 internal/web/templates/components/pkg-overview-btn.html create mode 100644 internal/web/templates/pages/package.html create mode 100644 pkg/describe/describe.go create mode 100644 pkg/manifest/manifest.go diff --git a/api/v1alpha1/package_manifest.go b/api/v1alpha1/package_manifest.go index e04787d7a..175b25ea8 100644 --- a/api/v1alpha1/package_manifest.go +++ b/api/v1alpha1/package_manifest.go @@ -49,10 +49,17 @@ type PlainManifest struct { Url string `json:"url" jsonschema:"required"` } +type PackageReference struct { + Label string `json:"label" jsonschema:"required"` + Url string `json:"url" jsonschema:"required"` +} + type PackageManifest struct { - Name string `json:"name" jsonschema:"required"` - ShortDescription string `json:"shortDescription,omitempty"` - IconUrl string `json:"iconUrl,omitempty" jsonschema:"format=uri"` + Name string `json:"name" jsonschema:"required"` + ShortDescription string `json:"shortDescription,omitempty"` + LongDescription string `json:"longDescription,omitempty"` + References []PackageReference `json:"references,omitempty"` + IconUrl string `json:"iconUrl,omitempty" jsonschema:"format=uri"` // Helm instructs the controller to create a helm release when installing this package. Helm *HelmManifest `json:"helm,omitempty"` // Kustomize instructs the controller to apply a kustomization when installing this package [PLACEHOLDER]. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 156e50e44..bcddd2cea 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -293,6 +293,11 @@ func (in *PackageList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PackageManifest) DeepCopyInto(out *PackageManifest) { *out = *in + if in.References != nil { + in, out := &in.References, &out.References + *out = make([]PackageReference, len(*in)) + copy(*out, *in) + } if in.Helm != nil { in, out := &in.Helm, &out.Helm *out = new(HelmManifest) @@ -325,6 +330,21 @@ func (in *PackageManifest) DeepCopy() *PackageManifest { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackageReference) DeepCopyInto(out *PackageReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageReference. +func (in *PackageReference) DeepCopy() *PackageReference { + if in == nil { + return nil + } + out := new(PackageReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PackageSpec) DeepCopyInto(out *PackageSpec) { *out = *in diff --git a/cmd/glasskube/cmd/serve.go b/cmd/glasskube/cmd/serve.go index 6490ac627..c90f41999 100644 --- a/cmd/glasskube/cmd/serve.go +++ b/cmd/glasskube/cmd/serve.go @@ -1,7 +1,6 @@ package cmd import ( - "context" "fmt" "os" @@ -46,9 +45,9 @@ var serveCmd = &cobra.Command{ } } - var ctx context.Context + var ctx = cmd.Context() if cfg != nil { - ctx, err = client.SetupContext(cmd.Context(), cfg) + ctx, err = client.SetupContext(ctx, cfg) if err != nil { fmt.Fprintf(os.Stderr, "An error occurred starting the webserver:\n\n%v\n", err) os.Exit(1) diff --git a/config/crd/bases/packages.glasskube.dev_packageinfos.yaml b/config/crd/bases/packages.glasskube.dev_packageinfos.yaml index 8d2ff420f..3c9eb4b9b 100644 --- a/config/crd/bases/packages.glasskube.dev_packageinfos.yaml +++ b/config/crd/bases/packages.glasskube.dev_packageinfos.yaml @@ -180,6 +180,8 @@ spec: description: Kustomize instructs the controller to apply a kustomization when installing this package [PLACEHOLDER]. type: object + longDescription: + type: string manifests: items: properties: @@ -191,6 +193,18 @@ spec: type: array name: type: string + references: + items: + properties: + label: + type: string + url: + type: string + required: + - label + - url + type: object + type: array shortDescription: type: string required: diff --git a/go.mod b/go.mod index e4d6f3030..cf4640e8c 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/fatih/color v1.16.0 github.com/fluxcd/helm-controller/api v0.37.4 github.com/fluxcd/source-controller/api v1.2.4 + github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.1 github.com/invopop/jsonschema v0.12.0 github.com/onsi/ginkgo/v2 v2.15.0 diff --git a/go.sum b/go.sum index 16ad0bc80..f02dfee0b 100644 --- a/go.sum +++ b/go.sum @@ -65,9 +65,9 @@ github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2Xc github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= @@ -246,8 +246,6 @@ k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910 h1:1Rp/XEKP5uxPs6QrsngEHA k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.17.0 h1:fjJQf8Ukya+VjogLO6/bNX9HE6Y2xpsO5+fyS26ur/s= -sigs.k8s.io/controller-runtime v0.17.0/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= sigs.k8s.io/controller-runtime v0.17.1 h1:V1dQELMGVk46YVXXQUbTFujU7u4DQj6YUj9Rb6cuzz8= sigs.k8s.io/controller-runtime v0.17.1/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/internal/controller/packageinfo_controller.go b/internal/controller/packageinfo_controller.go index ee53cc1f6..9844c7c9c 100644 --- a/internal/controller/packageinfo_controller.go +++ b/internal/controller/packageinfo_controller.go @@ -80,7 +80,7 @@ func (r *PackageInfoReconciler) Reconcile(ctx context.Context, req ctrl.Request) } if shouldSyncFromRepo(packageInfo) { - if err := repo.FetchPackageManifest(ctx, &packageInfo); err != nil { + if err := repo.LoadPackageManifest(ctx, &packageInfo); err != nil { err1 := conditions.SetFailedAndUpdate(ctx, r.Client, r.EventRecorder, &packageInfo, &packageInfo.Status.Conditions, condition.SyncFailed, err.Error()) return requeue.Always(ctx, multierr.Append(err, err1)) } else { diff --git a/internal/repo/repo.go b/internal/repo/repo.go index b66ed932b..985ced39f 100644 --- a/internal/repo/repo.go +++ b/internal/repo/repo.go @@ -26,7 +26,26 @@ type PackageTeaser struct { IconUrl string `json:"iconUrl,omitempty"` } -func FetchPackageManifest(ctx context.Context, pi *packagesv1alpha1.PackageInfo) error { +func GetPackageManifest(repoUrl string, pkgName string) (*packagesv1alpha1.PackageManifest, error) { + if len(repoUrl) == 0 { + repoUrl = defaultRepositoryUrl + } + url, err := url.JoinPath(repoUrl, pkgName, "package.yaml") + if err != nil { + return nil, err + } + body, err := doFetch(url) + if err != nil { + return nil, err + } + var manifest packagesv1alpha1.PackageManifest + if err = yaml.Unmarshal(body, &manifest); err != nil { + return nil, err + } + return &manifest, nil +} + +func LoadPackageManifest(ctx context.Context, pi *packagesv1alpha1.PackageInfo) error { log := log.FromContext(ctx) url, err := getPackageManifestUrl(*pi) if err != nil { diff --git a/internal/web/components/installbutton.go b/internal/web/components/installbutton.go deleted file mode 100644 index 74ad20ec4..000000000 --- a/internal/web/components/installbutton.go +++ /dev/null @@ -1,46 +0,0 @@ -package components - -import ( - "fmt" - "html/template" - "io" - "os" - - "github.com/glasskube/glasskube/api/v1alpha1" - "github.com/glasskube/glasskube/pkg/client" - "github.com/glasskube/glasskube/pkg/list" -) - -func GetButtonId(pkgName string) string { - return fmt.Sprintf("install-%v", pkgName) -} - -func GetSwap(buttonId string) string { - return fmt.Sprintf("outerHTML:#%s", buttonId) -} - -func RenderInstallButton(w io.Writer, tmpl *template.Template, pkgName string, status *client.PackageStatus, manifest *v1alpha1.PackageManifest) { - buttonId := GetButtonId(pkgName) - err := tmpl.ExecuteTemplate(w, "installbutton", &map[string]any{ - "ButtonId": buttonId, - "Swap": GetSwap(buttonId), - "PackageName": pkgName, - "Status": status, - "Manifest": manifest, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "An error occurred rendering the install button for %v: \n%v\n"+ - "This is most likely a BUG!", pkgName, err) - } -} - -func ToInstallButtonInput(pkgTeaser list.PackageTeaserWithStatus) map[string]any { - buttonId := GetButtonId(pkgTeaser.PackageName) - return map[string]any{ - "ButtonId": buttonId, - "Swap": GetSwap(buttonId), - "PackageName": pkgTeaser.PackageName, - "Status": pkgTeaser.Status, - "Manifest": pkgTeaser.InstalledManifest, - } -} diff --git a/internal/web/components/pkg_detail_btns/controller.go b/internal/web/components/pkg_detail_btns/controller.go new file mode 100644 index 000000000..c99faf566 --- /dev/null +++ b/internal/web/components/pkg_detail_btns/controller.go @@ -0,0 +1,55 @@ +package pkg_detail_btns + +import ( + "fmt" + "html/template" + "io" + "os" + + "github.com/glasskube/glasskube/api/v1alpha1" + "github.com/glasskube/glasskube/pkg/client" +) + +const TemplateId = "pkg-detail-btns" + +type pkgDetailBtnsInput struct { + ContainerId string + Swap string + PackageName string + Status *client.PackageStatus + Manifest *v1alpha1.PackageManifest +} + +func getId(pkgName string) string { + return fmt.Sprintf("%v-%v", TemplateId, pkgName) +} + +func getSwap(id string) string { + return fmt.Sprintf("outerHTML:#%s", id) +} + +func Render(w io.Writer, tmpl *template.Template, pkgName string, status *client.PackageStatus, manifest *v1alpha1.PackageManifest) { + id := getId(pkgName) + err := tmpl.ExecuteTemplate(w, TemplateId, &pkgDetailBtnsInput{ + ContainerId: id, + Swap: getSwap(id), + PackageName: pkgName, + Status: status, + Manifest: manifest, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "An error occurred rendering %v for %v: \n%v\n"+ + "This is most likely a BUG!", TemplateId, pkgName, err) + } +} + +func ForPkgDetailBtns(pkgName string, status *client.PackageStatus, manifest *v1alpha1.PackageManifest) *pkgDetailBtnsInput { + id := getId(pkgName) + return &pkgDetailBtnsInput{ + ContainerId: id, + Swap: "", + PackageName: pkgName, + Status: status, + Manifest: manifest, + } +} diff --git a/internal/web/components/pkg_overview_btn/controller.go b/internal/web/components/pkg_overview_btn/controller.go new file mode 100644 index 000000000..aa6f866e6 --- /dev/null +++ b/internal/web/components/pkg_overview_btn/controller.go @@ -0,0 +1,52 @@ +package pkg_overview_btn + +import ( + "fmt" + "html/template" + "io" + "os" + + "github.com/glasskube/glasskube/api/v1alpha1" + "github.com/glasskube/glasskube/pkg/client" + "github.com/glasskube/glasskube/pkg/list" +) + +const TemplateId = "pkg-overview-btn" + +type pkgOverviewBtnInput struct { + ButtonId string + Swap string + PackageName string + Status *client.PackageStatus + Manifest *v1alpha1.PackageManifest +} + +func getButtonId(pkgName string) string { + return fmt.Sprintf("%v-%v", TemplateId, pkgName) +} + +func Render(w io.Writer, tmpl *template.Template, pkgName string, status *client.PackageStatus, manifest *v1alpha1.PackageManifest) { + buttonId := getButtonId(pkgName) + err := tmpl.ExecuteTemplate(w, TemplateId, &pkgOverviewBtnInput{ + ButtonId: buttonId, + Swap: fmt.Sprintf("outerHTML:#%s", buttonId), + PackageName: pkgName, + Status: status, + Manifest: manifest, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "An error occurred rendering %v for %v: \n%v\n"+ + "This is most likely a BUG!", TemplateId, pkgName, err) + } +} + +func ForPkgOverviewBtn(pkgTeaser *list.PackageTeaserWithStatus) *pkgOverviewBtnInput { + buttonId := getButtonId(pkgTeaser.PackageName) + return &pkgOverviewBtnInput{ + ButtonId: buttonId, + Swap: "", + PackageName: pkgTeaser.PackageName, + Status: pkgTeaser.Status, + Manifest: pkgTeaser.InstalledManifest, + } +} diff --git a/internal/web/server.go b/internal/web/server.go index 778d80e98..c87044b57 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -6,17 +6,21 @@ import ( "embed" "errors" "fmt" - "html/template" "io/fs" "net" "net/http" "os" "syscall" - "github.com/glasskube/glasskube/api/v1alpha1" - "github.com/glasskube/glasskube/internal/web/components" + "github.com/glasskube/glasskube/pkg/manifest" + + "github.com/glasskube/glasskube/pkg/describe" + "github.com/glasskube/glasskube/api/v1alpha1" "github.com/glasskube/glasskube/internal/cliutils" + "github.com/glasskube/glasskube/internal/web/components/pkg_detail_btns" + "github.com/glasskube/glasskube/internal/web/components/pkg_overview_btn" + "github.com/gorilla/mux" "github.com/glasskube/glasskube/pkg/client" "github.com/glasskube/glasskube/pkg/install" @@ -26,16 +30,9 @@ import ( "github.com/glasskube/glasskube/pkg/uninstall" ) -var ( - baseTemplate *template.Template - pkgsPageTmpl *template.Template - supportPageTmpl *template.Template - installBtnTmpl *template.Template - - //go:embed root - //go:embed templates - embededFs embed.FS -) +//go:embed root +//go:embed templates +var embededFs embed.FS type ServerConfigSupport struct { KubeconfigMissing bool @@ -49,24 +46,12 @@ func init() { loadTemplates() } -func loadTemplates() { - templateFuncs := template.FuncMap{ - "ToInstallButtonInput": components.ToInstallButtonInput, - } - baseTemplate = template.Must( - template.New("base.html").Funcs(templateFuncs).ParseFS(embededFs, "templates/layout/base.html")) - pkgsPageTmpl = template.Must(template.Must(baseTemplate.Clone()). - ParseFS(embededFs, "templates/pages/packages.html", "templates/components/*.html")) - supportPageTmpl = template.Must(template.Must(baseTemplate.Clone()). - ParseFS(embededFs, "templates/pages/support.html", "templates/components/*.html")) - installBtnTmpl = template.Must(template.New("installbutton"). - ParseFS(embededFs, "templates/components/installbutton.html")) -} - type server struct { host string port int32 listener net.Listener + ctx context.Context + support *ServerConfigSupport pkgClient *client.PackageV1Alpha1Client wsHub *WsHub forwarders map[string]*open.OpenResult @@ -80,11 +65,30 @@ func NewServer(host string, port int32) *server { } } +func (s *server) broadcastPkgStatusUpdate( + pkgName string, + status *client.PackageStatus, + manifest *v1alpha1.PackageManifest, +) { + go func() { + var bf bytes.Buffer + pkg_overview_btn.Render(&bf, pkgOverviewBtnTmpl, pkgName, status, manifest) + s.wsHub.Broadcast <- bf.Bytes() + }() + go func() { + var bf bytes.Buffer + pkg_detail_btns.Render(&bf, pkgDetailBtnsTmpl, pkgName, status, manifest) + s.wsHub.Broadcast <- bf.Bytes() + }() +} + func (s *server) Start(ctx context.Context, support *ServerConfigSupport) error { if s.listener != nil { return errors.New("server is already listening") } + s.support = support + s.ctx = ctx s.pkgClient = client.FromContext(ctx) root, err := fs.Sub(embededFs, "root") @@ -93,92 +97,22 @@ func (s *server) Start(ctx context.Context, support *ServerConfigSupport) error } s.wsHub = NewHub() - fileServer := http.FileServer(http.FS(root)) - http.Handle("/static/", fileServer) - http.Handle("/favicon.ico", fileServer) - http.HandleFunc("/ws", s.wsHub.handler) - http.HandleFunc("/install", func(w http.ResponseWriter, r *http.Request) { - pkgName := r.FormValue("packageName") - go func() { - status, err := install.NewInstaller(s.pkgClient). - WithStatusWriter(statuswriter.Stderr()). - InstallBlocking(ctx, pkgName) - if err != nil { - fmt.Fprintf(os.Stderr, "An error occurred installing %v: \n%v\n", pkgName, err) - } - - var packageInfo v1alpha1.PackageInfo - var manifest *v1alpha1.PackageManifest - // TODO: Change this to use the actual package info name instead of the package name - if err := s.pkgClient.PackageInfos().Get(ctx, pkgName, &packageInfo); err != nil { - fmt.Printf("could not fetch PackageInfo %v: %v\n", pkgName, err) - } else { - manifest = packageInfo.Status.Manifest - } - - // broadcast the status update to all clients - var bf bytes.Buffer - components.RenderInstallButton(&bf, installBtnTmpl, pkgName, status, manifest) - s.wsHub.Broadcast <- bf.Bytes() - }() - - // broadcast the pending button to all clients (note that we do not return any html from the install endpoint) - var bf bytes.Buffer - components.RenderInstallButton(&bf, installBtnTmpl, pkgName, &client.PackageStatus{Status: "Pending"}, nil) - s.wsHub.Broadcast <- bf.Bytes() - }) - http.HandleFunc("/uninstall", func(w http.ResponseWriter, r *http.Request) { - pkgName := r.FormValue("packageName") - pkg, err := list.Get(s.pkgClient, ctx, pkgName) - if err != nil { - fmt.Fprintf(os.Stderr, "An error occurred uninstalling %v: \n%v\n", pkgName, err) - return - } - - err = uninstall.Uninstall(s.pkgClient, ctx, pkg) - if err != nil { - fmt.Fprintf(os.Stderr, "An error occurred uninstalling %v: \n%v\n", pkgName, err) - } - - // broadcast the button depending on status to all clients - var bf bytes.Buffer - components.RenderInstallButton(&bf, installBtnTmpl, pkgName, nil, nil) - s.wsHub.Broadcast <- bf.Bytes() - }) - http.HandleFunc("/open", func(w http.ResponseWriter, r *http.Request) { - pkgName := r.FormValue("packageName") - if result, ok := s.forwarders[pkgName]; ok { - result.WaitReady() - _ = cliutils.OpenInBrowser(result.Url) - return - } - - result, err := open.NewOpener().Open(ctx, pkgName, "") - if err != nil { - fmt.Fprintf(os.Stderr, "could not open %v: %v\n", pkgName, err) - } else { - s.forwarders[pkgName] = result - result.WaitReady() - _ = cliutils.OpenInBrowser(result.Url) - w.WriteHeader(http.StatusAccepted) - } - }) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if support != nil { - err := supportPageTmpl.Execute(w, support) - if err != nil { - fmt.Fprintf(os.Stderr, "An error occurred rendering the response: \n%v\n", err) - } - return - } - packages, _ := list.GetPackagesWithStatus(s.pkgClient, ctx, list.IncludePackageInfos) - err := pkgsPageTmpl.Execute(w, packages) - if err != nil { - fmt.Fprintf(os.Stderr, "An error occurred rendering the response: \n%v\n", err) - } + router := mux.NewRouter() + router.PathPrefix("/static/").Handler(fileServer) + router.Handle("/favicon.ico", fileServer) + router.HandleFunc("/ws", s.wsHub.handler) + router.HandleFunc("/support", s.supportPage) + router.HandleFunc("/packages", s.packages) + router.HandleFunc("/packages/install", s.install) + router.HandleFunc("/packages/uninstall", s.uninstall) + router.HandleFunc("/packages/open", s.open) + router.HandleFunc("/packages/{pkgName}", s.packageDetail) + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/packages", http.StatusFound) }) + http.Handle("/", router) bindAddr := fmt.Sprintf("%v:%d", s.host, s.port) @@ -216,6 +150,109 @@ func (s *server) Start(ctx context.Context, support *ServerConfigSupport) error return nil } +func (s *server) install(w http.ResponseWriter, r *http.Request) { + pkgName := r.FormValue("packageName") + go func() { + status, err := install.NewInstaller(s.pkgClient). + WithStatusWriter(statuswriter.Stderr()). + InstallBlocking(s.ctx, pkgName) + if err != nil { + fmt.Fprintf(os.Stderr, "An error occurred installing %v: \n%v\n", pkgName, err) + return + } + manifest, err := manifest.GetInstalledManifest(s.ctx, pkgName) + if err != nil { + fmt.Fprintf(os.Stderr, "could not fetch manifest of %v: %v\n", pkgName, err) + return + } + s.broadcastPkgStatusUpdate(pkgName, status, manifest) + }() + s.broadcastPkgStatusUpdate(pkgName, client.NewPendingStatus(), nil) +} + +func (s *server) uninstall(w http.ResponseWriter, r *http.Request) { + pkgName := r.FormValue("packageName") + pkg, err := list.Get(s.pkgClient, s.ctx, pkgName) + if err != nil { + fmt.Fprintf(os.Stderr, "An error occurred uninstalling %v: \n%v\n", pkgName, err) + return + } + // once we have blocking uninstall available, this should be changed to also broadcast the pending update first + err = uninstall.Uninstall(s.pkgClient, s.ctx, pkg) + if err != nil { + fmt.Fprintf(os.Stderr, "An error occurred uninstalling %v: \n%v\n", pkgName, err) + } + s.broadcastPkgStatusUpdate(pkgName, nil, nil) +} + +func (s *server) open(w http.ResponseWriter, r *http.Request) { + pkgName := r.FormValue("packageName") + if result, ok := s.forwarders[pkgName]; ok { + result.WaitReady() + _ = cliutils.OpenInBrowser(result.Url) + return + } + + result, err := open.NewOpener().Open(s.ctx, pkgName, "") + if err != nil { + fmt.Fprintf(os.Stderr, "could not open %v: %v\n", pkgName, err) + } else { + s.forwarders[pkgName] = result + result.WaitReady() + _ = cliutils.OpenInBrowser(result.Url) + w.WriteHeader(http.StatusAccepted) + } +} + +func (s *server) packages(w http.ResponseWriter, r *http.Request) { + if s.support != nil { + http.Redirect(w, r, "/support", http.StatusFound) + return + } + + packages, err := list.GetPackagesWithStatus(s.pkgClient, s.ctx, list.IncludePackageInfos) + if err != nil { + fmt.Fprintf(os.Stderr, "could not load packages: %v\n", err) + return + } + err = pkgsPageTmpl.Execute(w, packages) + if err != nil { + fmt.Fprintf(os.Stderr, "An error occurred rendering the response: \n%v\n", err) + } +} + +func (s *server) packageDetail(w http.ResponseWriter, r *http.Request) { + if s.support != nil { + http.Redirect(w, r, "/support", http.StatusFound) + return + } + pkgName := mux.Vars(r)["pkgName"] + pkg, status, manifest, err := describe.DescribePackage(s.ctx, pkgName) + if err != nil { + fmt.Fprintf(os.Stderr, "An error occurred fetching package details of %v: \n%v\n", pkgName, err) + return + } + err = pkgPageTmpl.Execute(w, &map[string]any{ + "Package": pkg, + "Status": status, + "Manifest": manifest, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "An error occurred rendering the package detail page of %v: \n%v\n", pkgName, err) + } +} + +func (s *server) supportPage(w http.ResponseWriter, r *http.Request) { + if s.support != nil { + err := supportPageTmpl.Execute(w, s.support) + if err != nil { + fmt.Fprintf(os.Stderr, "An error occurred rendering the support page: \n%v\n", err) + } + } else { + http.Redirect(w, r, "/", http.StatusFound) + } +} + func isPortConflictError(err error) bool { if opErr, ok := err.(*net.OpError); ok { if osErr, ok := opErr.Err.(*os.SyscallError); ok { diff --git a/internal/web/templates.go b/internal/web/templates.go new file mode 100644 index 000000000..6a806483e --- /dev/null +++ b/internal/web/templates.go @@ -0,0 +1,47 @@ +package web + +import ( + "fmt" + "html/template" + "path" + + "github.com/glasskube/glasskube/internal/web/components/pkg_detail_btns" + "github.com/glasskube/glasskube/internal/web/components/pkg_overview_btn" +) + +var ( + baseTemplate *template.Template + pkgsPageTmpl *template.Template + pkgPageTmpl *template.Template + supportPageTmpl *template.Template + pkgOverviewBtnTmpl *template.Template + pkgDetailBtnsTmpl *template.Template + templatesDir = "templates" + componentsDir = path.Join(templatesDir, "components") + pagesDir = path.Join(templatesDir, "pages") +) + +func loadTemplates() { + templateFuncs := template.FuncMap{ + "ForPkgOverviewBtn": pkg_overview_btn.ForPkgOverviewBtn, + "ForPkgDetailBtns": pkg_detail_btns.ForPkgDetailBtns, + "PackageManifestUrl": func(pkgName string) string { + // TODO get configured repository URL instead + return fmt.Sprintf("https://github.com/glasskube/packages/blob/main/packages/%s/package.yaml", pkgName) + }, + } + baseTemplate = template.Must(template.New("base.html"). + Funcs(templateFuncs). + ParseFS(embededFs, path.Join(templatesDir, "layout", "base.html"))) + pkgsPageTmpl = template.Must( + template.Must(baseTemplate.Clone()). + ParseFS(embededFs, path.Join(pagesDir, "packages.html"), path.Join(componentsDir, "*.html"))) + pkgPageTmpl = template.Must(template.Must(baseTemplate.Clone()). + ParseFS(embededFs, path.Join(pagesDir, "package.html"), path.Join(componentsDir, "*.html"))) + supportPageTmpl = template.Must(template.Must(baseTemplate.Clone()). + ParseFS(embededFs, path.Join(pagesDir, "support.html"), path.Join(componentsDir, "*.html"))) + pkgOverviewBtnTmpl = template.Must(template.New(pkg_overview_btn.TemplateId). + ParseFS(embededFs, path.Join(componentsDir, "pkg-overview-btn.html"))) + pkgDetailBtnsTmpl = template.Must(template.New(pkg_detail_btns.TemplateId). + ParseFS(embededFs, path.Join(componentsDir, "pkg-detail-btns.html"))) +} diff --git a/internal/web/templates/components/installbutton.html b/internal/web/templates/components/installbutton.html deleted file mode 100644 index da5009f39..000000000 --- a/internal/web/templates/components/installbutton.html +++ /dev/null @@ -1,55 +0,0 @@ -{{define "installbutton"}} - -{{if eq .Status nil}} - -{{else if eq .Status.Status "Pending"}} - -{{else if eq .Status.Status "Failed"}} -
- - -
-{{else if and .Manifest .Manifest.Entrypoints }} -
- - - -
-{{else}} -
- - -
-{{end}} - - -{{end}} \ No newline at end of file diff --git a/internal/web/templates/components/pkg-detail-btns.html b/internal/web/templates/components/pkg-detail-btns.html new file mode 100644 index 000000000..46ff301b1 --- /dev/null +++ b/internal/web/templates/components/pkg-detail-btns.html @@ -0,0 +1,36 @@ +{{define "pkg-detail-uninstall"}} + +{{end}} + +{{define "pkg-detail-btns"}} + + + {{if eq .Status nil}} + + {{else if eq .Status.Status "Pending"}} + + {{else if eq .Status.Status "Failed"}} + + Installation Failed + + {{template "pkg-detail-uninstall" .}} + {{else if and .Manifest .Manifest.Entrypoints }} + + {{template "pkg-detail-uninstall" .}} + {{else}} + + + Installed + + {{template "pkg-detail-uninstall" .}} + {{end}} + + +{{end}} diff --git a/internal/web/templates/components/pkg-overview-btn.html b/internal/web/templates/components/pkg-overview-btn.html new file mode 100644 index 000000000..9b8a8ffdd --- /dev/null +++ b/internal/web/templates/components/pkg-overview-btn.html @@ -0,0 +1,29 @@ +{{define "pkg-overview-btn"}} + + + {{if eq .Status nil}} + + {{else if eq .Status.Status "Pending"}} + + {{else if eq .Status.Status "Failed"}} + + Installation Failed + + {{else if and .Manifest .Manifest.Entrypoints }} + + {{else}} + + + Installed + + {{end}} + +{{end}} diff --git a/internal/web/templates/layout/base.html b/internal/web/templates/layout/base.html index 40a104421..6b83005c9 100644 --- a/internal/web/templates/layout/base.html +++ b/internal/web/templates/layout/base.html @@ -3,21 +3,29 @@ Glasskube - + + + + + + + -