AppManager: monitor installed HelmRelease resources
Change-Id: Ia036e7dda8136ad696d8222e799c4d1b6a9018a9
diff --git a/core/installer/welcome/appmanager-tmpl/app.html b/core/installer/welcome/appmanager-tmpl/app.html
index cab78bf..64c1458 100644
--- a/core/installer/welcome/appmanager-tmpl/app.html
+++ b/core/installer/welcome/appmanager-tmpl/app.html
@@ -1,3 +1,16 @@
+{{ define "task" }}
+{{ range . }}
+<li aria-busy="{{ eq .Status 1 }}">
+ {{ if eq .Status 3 }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none"><circle cx="12" cy="12" r="8" fill="green" fill-opacity="0.25"/><path stroke="green" stroke-width="1.2" d="m8.5 11l2.894 2.894a.15.15 0 0 0 .212 0L19.5 6"/><path stroke="green" stroke-linecap="round" d="M19.358 10.547a7.5 7.5 0 1 1-3.608-5.042"/></g></svg>{{ end }}{{ .Title }}{{ if .Err }} - {{ .Err.Error }} {{ end }}
+ {{ if .Subtasks }}
+ <ul>
+ {{ template "task" .Subtasks }}
+ </ul>
+ {{ end }}
+</li>
+{{ end }}
+{{ end }}
+
{{ define "schema-form" }}
{{ $readonly := .ReadOnly }}
{{ $networks := .AvailableNetworks }}
@@ -75,6 +88,20 @@
{{ $schema := .App.Schema }}
{{ $networks := .AvailableNetworks }}
+{{ $renderForm := true }}
+
+{{ if .Task }}
+ {{if or (eq .Task.Status 0) (eq .Task.Status 1) }}
+ {{ $renderForm = false }}
+ Waiting for resources:
+ <ul class="progress">
+ {{ template "task" .Task.Subtasks }}
+ </ul>
+ <script>setTimeout(() => location.reload(), 3000);</script>
+ {{ end }}
+{{ end }}
+
+{{ if $renderForm }}
<form id="config-form">
{{ if $instance }}
{{ template "schema-form" (dict "Schema" $schema "AvailableNetworks" $networks "ReadOnly" false "Data" ($instance.InputToValues $schema)) }}
@@ -92,7 +119,13 @@
</form>
{{ range .Instances }}
- {{ if or (not $instance) (ne $instance.Id .Id)}}
+ {{ $r := true}}
+ {{ if $instance }}
+ {{ if eq $instance.Id .Id }}
+ {{ $r = false}}
+ {{ end }}
+ {{ end }}
+ {{ if $r }}
<details>
<summary>{{ .Id }}</summary>
{{ template "schema-form" (dict "Schema" $schema "AvailableNetworks" $networks "ReadOnly" true "Data" (.InputToValues $schema)) }}
@@ -100,20 +133,12 @@
</details>
{{ end }}
{{ end }}
-
-
-<div id="toast-success" class="toast hidden">
- <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36"><path fill="currentColor" d="M18 2a16 16 0 1 0 16 16A16 16 0 0 0 18 2Zm0 30a14 14 0 1 1 14-14a14 14 0 0 1-14 14Z" class="clr-i-outline clr-i-outline-path-1"/><path fill="currentColor" d="M28 12.1a1 1 0 0 0-1.41 0l-11.1 11.05l-6-6A1 1 0 0 0 8 18.53L15.49 26L28 13.52a1 1 0 0 0 0-1.42Z" class="clr-i-outline clr-i-outline-path-2"/><path fill="none" d="M0 0h36v36H0z"/></svg> {{ if $instance }}Update succeeded{{ else }}Install succeeded{{ end}}
-</div>
+{{ end }}
<div id="toast-failure" class="toast hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10Zm3-6L9 8m0 8l6-8"/></svg> {{ if $instance }}Update failed{{ else}}Install failed{{ end }}
</div>
-<div id="toast-uninstall-success" class="toast hidden">
- <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36"><path fill="currentColor" d="M18 2a16 16 0 1 0 16 16A16 16 0 0 0 18 2Zm0 30a14 14 0 1 1 14-14a14 14 0 0 1-14 14Z" class="clr-i-outline clr-i-outline-path-1"/><path fill="currentColor" d="M28 12.1a1 1 0 0 0-1.41 0l-11.1 11.05l-6-6A1 1 0 0 0 8 18.53L15.49 26L28 13.52a1 1 0 0 0 0-1.42Z" class="clr-i-outline clr-i-outline-path-2"/><path fill="none" d="M0 0h36v36H0z"/></svg> Uninstalled application
-</div>
-
<div id="toast-uninstall-failure" class="toast hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10Zm3-6L9 8m0 8l6-8"/></svg> Failed to uninstall application
</div>
@@ -192,18 +217,10 @@
);
}
- function installSucceeded() {
- actionFinished(document.getElementById("toast-success"));
- }
-
function installFailed() {
actionFinished(document.getElementById("toast-failure"));
}
- function uninstallSucceeded() {
- actionFinished(document.getElementById("toast-uninstall-success"));
- }
-
function uninstallFailed() {
actionFinished(document.getElementById("toast-uninstall-failure"));
}
@@ -213,16 +230,16 @@
async function install() {
installStarted();
const resp = await fetch(submitAddr, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Accept": "application/json",
- },
- body: JSON.stringify(config),
- });
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ },
+ body: JSON.stringify(config),
+ });
if (resp.status === 200) {
- installSucceeded();
- } else {
+ window.location = await resp.text();
+ } else {
installFailed();
}
}
@@ -234,7 +251,7 @@
method: "POST",
});
if (resp.status === 200) {
- uninstallSucceeded();
+ window.location = await resp.text();
} else {
uninstallFailed();
}
diff --git a/core/installer/welcome/appmanager.go b/core/installer/welcome/appmanager.go
index a6eb243..e7322ab 100644
--- a/core/installer/welcome/appmanager.go
+++ b/core/installer/welcome/appmanager.go
@@ -32,6 +32,8 @@
m *installer.AppManager
r installer.AppRepository
reconciler tasks.Reconciler
+ h installer.HelmReleaseMonitor
+ tasks map[string]tasks.Task
}
func NewAppManagerServer(
@@ -39,12 +41,15 @@
m *installer.AppManager,
r installer.AppRepository,
reconciler tasks.Reconciler,
+ h installer.HelmReleaseMonitor,
) *AppManagerServer {
return &AppManagerServer{
port,
m,
r,
reconciler,
+ h,
+ map[string]tasks.Task{},
}
}
@@ -107,7 +112,7 @@
if err != nil {
return err
}
- return c.JSON(http.StatusOK, app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{instance}})
+ return c.JSON(http.StatusOK, app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}})
}
func (s *AppManagerServer) handleAppInstall(c echo.Context) error {
@@ -139,13 +144,22 @@
instanceId := a.Slug() + suffix
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, a.Namespace(), suffix)
- if err := s.m.Install(a, instanceId, appDir, namespace, values); err != nil {
- log.Printf("%s\n", err.Error())
+ rr, err := s.m.Install(a, instanceId, appDir, namespace, values)
+ if err != nil {
return err
}
ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
go s.reconciler.Reconcile(ctx)
- return c.String(http.StatusOK, "Installed")
+ if _, ok := s.tasks[instanceId]; ok {
+ panic("MUST NOT REACH!")
+ }
+ t := tasks.NewMonitorRelease(s.h, rr)
+ t.OnDone(func(err error) {
+ delete(s.tasks, instanceId)
+ })
+ s.tasks[instanceId] = t
+ go t.Start()
+ return c.String(http.StatusOK, fmt.Sprintf("/instance/%s", instanceId))
}
func (s *AppManagerServer) handleAppUpdate(c echo.Context) error {
@@ -166,13 +180,22 @@
if err != nil {
return err
}
- if err := s.m.Update(a, slug, values); err != nil {
- fmt.Println(err)
+ if _, ok := s.tasks[slug]; ok {
+ return fmt.Errorf("Update already in progress")
+ }
+ rr, err := s.m.Update(a, slug, values)
+ if err != nil {
return err
}
ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
go s.reconciler.Reconcile(ctx)
- return c.String(http.StatusOK, "Installed")
+ t := tasks.NewMonitorRelease(s.h, rr)
+ t.OnDone(func(err error) {
+ delete(s.tasks, slug)
+ })
+ s.tasks[slug] = t
+ go t.Start()
+ return c.String(http.StatusOK, fmt.Sprintf("/instance/%s", slug))
}
func (s *AppManagerServer) handleAppRemove(c echo.Context) error {
@@ -182,7 +205,7 @@
}
ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
go s.reconciler.Reconcile(ctx)
- return c.String(http.StatusOK, "Installed")
+ return c.String(http.StatusOK, "/")
}
func (s *AppManagerServer) handleIndex(c echo.Context) error {
@@ -206,6 +229,7 @@
Instance *installer.AppInstanceConfig
Instances []installer.AppInstanceConfig
AvailableNetworks []installer.Network
+ Task tasks.Task
}
func (s *AppManagerServer) handleAppUI(c echo.Context) error {
@@ -265,11 +289,13 @@
if err != nil {
return err
}
+ t := s.tasks[slug]
err = appTmpl.Execute(c.Response(), appContext{
App: a,
- Instance: &instance,
+ Instance: instance,
Instances: instances,
AvailableNetworks: installer.CreateNetworks(global),
+ Task: t,
})
return err
}
diff --git a/core/installer/welcome/env_test.go b/core/installer/welcome/env_test.go
index a689f54..ed7a2d3 100644
--- a/core/installer/welcome/env_test.go
+++ b/core/installer/welcome/env_test.go
@@ -205,7 +205,7 @@
if err != nil {
t.Fatal(err)
}
- if err := infraMgr.Install(app, "/infrastructure/dns-gateway", "dns-gateway", map[string]any{
+ if _, err := infraMgr.Install(app, "/infrastructure/dns-gateway", "dns-gateway", map[string]any{
"servers": []installer.EnvDNS{},
}); err != nil {
t.Fatal(err)
diff --git a/core/installer/welcome/welcome.go b/core/installer/welcome/welcome.go
index d7bccdd..1592a5c 100644
--- a/core/installer/welcome/welcome.go
+++ b/core/installer/welcome/welcome.go
@@ -226,7 +226,7 @@
instanceId := fmt.Sprintf("%s-%s", app.Slug(), req.Username)
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"username": req.Username,
"preAuthKey": map[string]any{
"enabled": false,