AppManager: monitor installed HelmRelease resources
Change-Id: Ia036e7dda8136ad696d8222e799c4d1b6a9018a9
diff --git a/charts/appmanager/templates/install.yaml b/charts/appmanager/templates/install.yaml
index c8d6b04..f84e7ba 100644
--- a/charts/appmanager/templates/install.yaml
+++ b/charts/appmanager/templates/install.yaml
@@ -9,6 +9,12 @@
- namespaces
verbs:
- create
+- apiGroups:
+ - "helm.toolkit.fluxcd.io"
+ resources:
+ - helmreleases
+ verbs:
+ - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 871dae5..8ca42f2 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -12,11 +12,15 @@
"github.com/giolekva/pcloud/core/installer/io"
"github.com/giolekva/pcloud/core/installer/soft"
+
+ "sigs.k8s.io/yaml"
)
const configFileName = "config.yaml"
const kustomizationFileName = "kustomization.yaml"
+var ErrorNotFound = errors.New("not found")
+
type AppManager struct {
repoIO soft.RepoIO
nsCreator NamespaceCreator
@@ -85,22 +89,22 @@
return ret, nil
}
-func (m *AppManager) FindInstance(id string) (AppInstanceConfig, error) {
+func (m *AppManager) FindInstance(id string) (*AppInstanceConfig, error) {
kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
if err != nil {
- return AppInstanceConfig{}, err
+ return nil, err
}
for _, app := range kust.Resources {
if app == id {
cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
if err != nil {
- return AppInstanceConfig{}, err
+ return nil, err
}
cfg.Id = id
- return cfg, nil
+ return &cfg, nil
}
}
- return AppInstanceConfig{}, nil
+ return nil, ErrorNotFound
}
func (m *AppManager) AppConfig(name string) (AppInstanceConfig, error) {
@@ -163,6 +167,15 @@
return nil
}
+type Resource struct {
+ Name string `json:"name"`
+ Namespace string `json:"namespace"`
+}
+
+type ReleaseResources struct {
+ Helm []Resource
+}
+
// TODO(gio): rename to CommitApp
func InstallApp(
repo soft.RepoIO,
@@ -172,11 +185,12 @@
ports []PortForward,
resources CueAppData,
data CueAppData,
- opts ...soft.DoOption) error {
+ opts ...soft.DoOption,
+) (ReleaseResources, error) {
// if err := openPorts(rendered.Ports); err != nil {
// return err
// }
- return repo.Do(func(r soft.RepoFS) (string, error) {
+ return ReleaseResources{}, repo.Do(func(r soft.RepoFS) (string, error) {
if err := r.RemoveDir(appDir); err != nil {
return "", err
}
@@ -230,17 +244,17 @@
}
// TODO(gio): commit instanceId -> appDir mapping as well
-func (m *AppManager) Install(app EnvApp, instanceId string, appDir string, namespace string, values map[string]any) error {
+func (m *AppManager) Install(app EnvApp, instanceId string, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
appDir = filepath.Clean(appDir)
if err := m.repoIO.Pull(); err != nil {
- return err
+ return ReleaseResources{}, err
}
if err := m.nsCreator.Create(namespace); err != nil {
- return err
+ return ReleaseResources{}, err
}
env, err := m.Config()
if err != nil {
- return err
+ return ReleaseResources{}, err
}
release := Release{
AppInstanceId: instanceId,
@@ -250,24 +264,51 @@
}
rendered, err := app.Render(release, env, values)
if err != nil {
- return err
+ return ReleaseResources{}, err
}
- return InstallApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
+ if _, err := InstallApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data); err != nil {
+ return ReleaseResources{}, err
+ }
+ return ReleaseResources{
+ Helm: extractHelm(rendered.Resources),
+ }, nil
}
-func (m *AppManager) Update(app EnvApp, instanceId string, values map[string]any, opts ...soft.DoOption) error {
+type helmRelease struct {
+ Metadata Resource `json:"metadata"`
+ Status struct {
+ Conditions []struct {
+ Type string `json:"type"`
+ Status string `json:"status"`
+ } `json:"conditions"`
+ } `json:"status,omitempty"`
+}
+
+func extractHelm(resources CueAppData) []Resource {
+ ret := make([]Resource, 0, len(resources))
+ for _, contents := range resources {
+ var h helmRelease
+ if err := yaml.Unmarshal(contents, &h); err != nil {
+ panic(err) // TODO(gio): handle
+ }
+ ret = append(ret, h.Metadata)
+ }
+ return ret
+}
+
+func (m *AppManager) Update(app EnvApp, instanceId string, values map[string]any, opts ...soft.DoOption) (ReleaseResources, error) {
if err := m.repoIO.Pull(); err != nil {
- return err
+ return ReleaseResources{}, err
}
env, err := m.Config()
if err != nil {
- return err
+ return ReleaseResources{}, err
}
instanceDir := filepath.Join(m.appDirRoot, instanceId)
instanceConfigPath := filepath.Join(instanceDir, "config.json")
config, err := m.appConfig(instanceConfigPath)
if err != nil {
- return err
+ return ReleaseResources{}, err
}
release := Release{
AppInstanceId: instanceId,
@@ -277,7 +318,7 @@
}
rendered, err := app.Render(release, env, values)
if err != nil {
- return err
+ return ReleaseResources{}, err
}
return InstallApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
}
@@ -368,17 +409,17 @@
return InfraAppInstanceConfig{}, nil
}
-func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) error {
+func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
appDir = filepath.Clean(appDir)
if err := m.repoIO.Pull(); err != nil {
- return err
+ return ReleaseResources{}, err
}
if err := m.nsCreator.Create(namespace); err != nil {
- return err
+ return ReleaseResources{}, err
}
infra, err := m.Config()
if err != nil {
- return err
+ return ReleaseResources{}, err
}
release := Release{
Namespace: namespace,
@@ -387,24 +428,24 @@
}
rendered, err := app.Render(release, infra, values)
if err != nil {
- return err
+ return ReleaseResources{}, err
}
return InstallApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
}
-func (m *InfraAppManager) Update(app InfraApp, instanceId string, values map[string]any, opts ...soft.DoOption) error {
+func (m *InfraAppManager) Update(app InfraApp, instanceId string, values map[string]any, opts ...soft.DoOption) (ReleaseResources, error) {
if err := m.repoIO.Pull(); err != nil {
- return err
+ return ReleaseResources{}, err
}
env, err := m.Config()
if err != nil {
- return err
+ return ReleaseResources{}, err
}
instanceDir := filepath.Join("/infrastructure", instanceId)
instanceConfigPath := filepath.Join(instanceDir, "config.json")
config, err := m.appConfig(instanceConfigPath)
if err != nil {
- return err
+ return ReleaseResources{}, err
}
release := Release{
AppInstanceId: instanceId,
@@ -414,7 +455,7 @@
}
rendered, err := app.Render(release, env, values)
if err != nil {
- return err
+ return ReleaseResources{}, err
}
return InstallApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
}
diff --git a/core/installer/bootstrapper.go b/core/installer/bootstrapper.go
index e8b77ec..c581f42 100644
--- a/core/installer/bootstrapper.go
+++ b/core/installer/bootstrapper.go
@@ -418,7 +418,8 @@
}
namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
appDir := filepath.Join("/infrastructure", app.Slug())
- return mgr.Install(app, appDir, namespace, map[string]any{})
+ _, err = mgr.Install(app, appDir, namespace, map[string]any{})
+ return err
}
appsToInstall := []string{
"resource-renderer-controller",
@@ -512,12 +513,13 @@
}
namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
appDir := filepath.Join("/infrastructure", app.Slug())
- return mgr.Install(app, appDir, namespace, map[string]any{
+ _, err = mgr.Install(app, appDir, namespace, map[string]any{
"repoIP": env.ServiceIPs.ConfigRepo,
"repoPort": 22,
"repoName": "config",
"sshPrivateKey": string(keys.RawPrivateKey()),
})
+ return err
}
func (b Bootstrapper) installIngressPublic(mgr *InfraAppManager, ss soft.Client, env BootstrapConfig) error {
@@ -538,9 +540,10 @@
}
namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
appDir := filepath.Join("/infrastructure", app.Slug())
- return mgr.Install(app, appDir, namespace, map[string]any{
+ _, err = mgr.Install(app, appDir, namespace, map[string]any{
"sshPrivateKey": string(keys.RawPrivateKey()),
})
+ return err
}
func (b Bootstrapper) installOryHydraMaester(mgr *InfraAppManager, env BootstrapConfig) error {
@@ -550,7 +553,8 @@
}
namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
appDir := filepath.Join("/infrastructure", app.Slug())
- return mgr.Install(app, appDir, namespace, map[string]any{})
+ _, err = mgr.Install(app, appDir, namespace, map[string]any{})
+ return err
}
func (b Bootstrapper) installDNSZoneManager(mgr *InfraAppManager, env BootstrapConfig) error {
@@ -560,9 +564,10 @@
}
namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
appDir := filepath.Join("/infrastructure", app.Slug())
- return mgr.Install(app, appDir, namespace, map[string]any{
+ _, err = mgr.Install(app, appDir, namespace, map[string]any{
"servers": []EnvDNS{},
})
+ return err
}
func (b Bootstrapper) installFluxcdReconciler(mgr *InfraAppManager, ss soft.Client, env BootstrapConfig) error {
@@ -572,7 +577,8 @@
}
namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
appDir := filepath.Join("/infrastructure", app.Slug())
- return mgr.Install(app, appDir, namespace, map[string]any{})
+ _, err = mgr.Install(app, appDir, namespace, map[string]any{})
+ return err
}
type HelmActionConfigFactory interface {
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index ee21ba0..abdd475 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -104,6 +104,10 @@
} else {
r = installer.NewInMemoryAppRepository(installer.CreateStoreApps())
}
+ helmMon, err := newHelmReleaseMonitor()
+ if err != nil {
+ return err
+ }
s := welcome.NewAppManagerServer(
appManagerFlags.port,
m,
@@ -112,6 +116,7 @@
"http://fluxcd-reconciler.dodo-fluxcd-reconciler.svc.cluster.local",
env.Id,
),
+ helmMon,
)
return s.Start()
}
diff --git a/core/installer/cmd/kube.go b/core/installer/cmd/kube.go
index d06ae59..4c6ab59 100644
--- a/core/installer/cmd/kube.go
+++ b/core/installer/cmd/kube.go
@@ -11,3 +11,7 @@
func newZoneFetcher() (installer.ZoneStatusFetcher, error) {
return installer.NewZoneStatusFetcher(rootFlags.kubeConfig)
}
+
+func newHelmReleaseMonitor() (installer.HelmReleaseMonitor, error) {
+ return installer.NewHelmReleaseMonitor(rootFlags.kubeConfig)
+}
diff --git a/core/installer/cmd/rewrite.go b/core/installer/cmd/rewrite.go
index 44f499a..8bb9173 100644
--- a/core/installer/cmd/rewrite.go
+++ b/core/installer/cmd/rewrite.go
@@ -84,7 +84,7 @@
return err
}
v := inst.InputToValues(app.Schema())
- if err := mgr.Update(app, inst.Id, v, soft.WithNoCommit()); err != nil {
+ if _, err := mgr.Update(app, inst.Id, v, soft.WithNoCommit()); err != nil {
return err
}
}
diff --git a/core/installer/kube.go b/core/installer/kube.go
index db2fb85..a8ed275 100644
--- a/core/installer/kube.go
+++ b/core/installer/kube.go
@@ -3,6 +3,7 @@
import (
"bytes"
"context"
+ "encoding/json"
"fmt"
"io"
"net/http"
@@ -10,6 +11,8 @@
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
@@ -76,6 +79,45 @@
return &realZoneStatusFetcher{}, nil
}
+type HelmReleaseMonitor interface {
+ IsReleased(namespace, name string) (bool, error)
+}
+
+type realHelmReleaseMonitor struct {
+ d dynamic.Interface
+}
+
+func (m *realHelmReleaseMonitor) IsReleased(namespace, name string) (bool, error) {
+ ctx := context.Background()
+ res, err := m.d.Resource(schema.GroupVersionResource{"helm.toolkit.fluxcd.io", "v2beta1", "helmreleases"}).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
+ if err != nil {
+ return false, err
+ }
+ b, err := res.MarshalJSON()
+ if err != nil {
+ return false, err
+ }
+ var hr helmRelease
+ if err := json.Unmarshal(b, &hr); err != nil {
+ return false, err
+ }
+ for _, c := range hr.Status.Conditions {
+ if c.Type == "Ready" && c.Status == "True" {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func NewHelmReleaseMonitor(kubeconfig string) (HelmReleaseMonitor, error) {
+ c, err := NewKubeConfig(kubeconfig)
+ if err != nil {
+ return nil, err
+ }
+ d := dynamic.New(c.RESTClient())
+ return &realHelmReleaseMonitor{d}, nil
+}
+
func NewKubeConfig(kubeconfig string) (*kubernetes.Clientset, error) {
if kubeconfig == "" {
config, err := rest.InClusterConfig()
diff --git a/core/installer/tasks/dns.go b/core/installer/tasks/dns.go
index 1316dc0..0424cfe 100644
--- a/core/installer/tasks/dns.go
+++ b/core/installer/tasks/dns.go
@@ -49,7 +49,7 @@
instanceId := app.Slug()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"addressPool": addressPool,
"inClusterIP": env.Network.DNSInClusterIP.String(),
"publicIP": join(env.PublicIP, ","),
@@ -84,7 +84,7 @@
env.Domain,
env.Network.DNSInClusterIP.String(),
})
- if err := st.infraAppManager.Update(app, "dns-gateway", map[string]any{
+ if _, err := st.infraAppManager.Update(app, "dns-gateway", map[string]any{
"servers": servers,
}); err != nil {
return err
diff --git a/core/installer/tasks/infra.go b/core/installer/tasks/infra.go
index 746b9cf..91d0fd9 100644
--- a/core/installer/tasks/infra.go
+++ b/core/installer/tasks/infra.go
@@ -125,7 +125,7 @@
instanceId := fmt.Sprintf("%s-ingress-private", app.Slug())
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s-ingress-private", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"name": fmt.Sprintf("%s-ingress-private", env.Id),
"from": env.Network.Ingress.String(),
"to": env.Network.Ingress.String(),
@@ -139,7 +139,7 @@
instanceId := fmt.Sprintf("%s-headscale", app.Slug())
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s-ingress-private", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"name": fmt.Sprintf("%s-headscale", env.Id),
"from": env.Network.Headscale.String(),
"to": env.Network.Headscale.String(),
@@ -153,7 +153,7 @@
instanceId := app.Slug()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"name": env.Id,
"from": env.Network.ServicesFrom.String(),
"to": env.Network.ServicesTo.String(),
@@ -183,7 +183,7 @@
instanceId := app.Slug()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"privateNetwork": map[string]any{
"hostname": "private-network-proxy",
"username": "private-network-proxy",
@@ -208,7 +208,7 @@
instanceId := app.Slug()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{}); err != nil {
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{}); err != nil {
return err
}
return nil
@@ -221,7 +221,7 @@
instanceId := app.Slug()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{}); err != nil {
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{}); err != nil {
return err
}
return nil
@@ -238,7 +238,7 @@
instanceId := app.Slug()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
}); err != nil {
return err
@@ -262,7 +262,7 @@
instanceId := app.Slug()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"authGroups": strings.Join(initGroups, ","),
}); err != nil {
return err
@@ -286,7 +286,7 @@
instanceId := app.Slug()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"subdomain": "headscale",
"ipSubnet": fmt.Sprintf("%s/24", env.Network.DNS.String()),
}); err != nil {
@@ -322,7 +322,7 @@
instanceId := app.Slug()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"repoAddr": st.ssClient.GetRepoAddress("config"),
"sshPrivateKey": string(keys.RawPrivateKey()),
}); err != nil {
@@ -358,7 +358,7 @@
instanceId := app.Slug()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"repoAddr": st.ssClient.GetRepoAddress("config"),
"sshPrivateKey": string(keys.RawPrivateKey()),
"authGroups": strings.Join(initGroups, ","),
@@ -390,7 +390,7 @@
instanceId := app.Name()
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+ if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"repoAddr": st.ssClient.GetRepoAddress("config"),
"sshPrivateKey": string(keys.RawPrivateKey()),
}); err != nil {
diff --git a/core/installer/tasks/init.go b/core/installer/tasks/init.go
index 20e428d..f55344d 100644
--- a/core/installer/tasks/init.go
+++ b/core/installer/tasks/init.go
@@ -53,11 +53,12 @@
return err
}
appDir := filepath.Join("/environments", env.Id, "config-repo")
- return st.infraAppManager.Install(app, appDir, env.Id, map[string]any{
+ _, err = st.infraAppManager.Install(app, appDir, env.Id, map[string]any{
"privateKey": string(keys.RawPrivateKey()),
"publicKey": string(keys.RawAuthorizedKey()),
"adminKey": string(adminKeys.RawAuthorizedKey()),
})
+ return err
})
return &t
}
diff --git a/core/installer/tasks/reconciler.go b/core/installer/tasks/reconciler.go
index 461ffc8..0e8133e 100644
--- a/core/installer/tasks/reconciler.go
+++ b/core/installer/tasks/reconciler.go
@@ -27,7 +27,7 @@
func (r fluxcdReconciler) Reconcile(ctx context.Context) {
for {
select {
- case <-time.After(30 * time.Second):
+ case <-time.After(3 * time.Second):
for _, res := range r.resources {
http.Get(res)
}
diff --git a/core/installer/tasks/release.go b/core/installer/tasks/release.go
new file mode 100644
index 0000000..53e5e74
--- /dev/null
+++ b/core/installer/tasks/release.go
@@ -0,0 +1,29 @@
+package tasks
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/giolekva/pcloud/core/installer"
+)
+
+func NewMonitorRelease(mon installer.HelmReleaseMonitor, rr installer.ReleaseResources) Task {
+ var t []Task
+ for _, h := range rr.Helm {
+ t = append(t, newMonitorHelm(mon, h))
+ }
+ return newConcurrentParentTask("Monitor", true, t...)
+}
+
+func newMonitorHelm(mon installer.HelmReleaseMonitor, h installer.Resource) Task {
+ t := newLeafTask(fmt.Sprintf("%s/%s", h.Namespace, h.Name), func() error {
+ for {
+ if ok, err := mon.IsReleased(h.Namespace, h.Name); err == nil && ok {
+ break
+ }
+ time.Sleep(5 * time.Second)
+ }
+ return nil
+ })
+ return &t
+}
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,