DodoApp: Deploy Ingress resource for status page
Change-Id: I0f102664d655d060d0ba37a63e3681816457f79b
diff --git a/charts/ingress/templates/install.yaml b/charts/ingress/templates/install.yaml
index f2c839b..c50a741 100644
--- a/charts/ingress/templates/install.yaml
+++ b/charts/ingress/templates/install.yaml
@@ -1,13 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
- name: ingress
+ name: ingress-{{ .Values.domain }}
namespace: {{ .Release.Namespace }}
- {{- if .Values.certificateIssuer }}
+ {{- if or .Values.certificateIssuer .Values.appRoot }}
annotations:
+ {{- if .Values.certificateIssuer }}
acme.cert-manager.io/http01-edit-in-place: "true"
cert-manager.io/cluster-issuer: {{ .Values.certificateIssuer }}
# nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
+ {{- end }}
+ {{- if .Values.appRoot }}
+ nginx.ingress.kubernetes.io/app-root: {{ .Values.appRoot }}
+ {{- end }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingressClassName }}
@@ -15,7 +20,7 @@
tls:
- hosts:
- {{ .Values.domain }}
- secretName: cert-rpuppy
+ secretName: cert-{{ .Values.domain }}
{{- end }}
rules:
- host: {{ .Values.domain }}
diff --git a/charts/ingress/values.yaml b/charts/ingress/values.yaml
index 18477aa..0640557 100644
--- a/charts/ingress/values.yaml
+++ b/charts/ingress/values.yaml
@@ -1,6 +1,7 @@
ingressClassName: ingress-public
certificateIssuer: example-public
domain: woof.example.com
+appRoot: ""
service:
name: woof
port:
diff --git a/core/installer/app.go b/core/installer/app.go
index d2c5d00..d86c9f3 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -41,6 +41,7 @@
URL string
Help []HelpDocument
Icon string
+ Raw []byte
}
type HelpDocument struct {
@@ -318,6 +319,7 @@
if err != nil {
return rendered{}, err
}
+ ret.Raw = full
ret.Data["rendered.json"] = full
readme, err := res.LookupPath(cue.ParsePath("readme")).String()
if err != nil {
diff --git a/core/installer/app_configs/app_base.cue b/core/installer/app_configs/app_base.cue
index 477a68d..34e0a72 100644
--- a/core/installer/app_configs/app_base.cue
+++ b/core/installer/app_configs/app_base.cue
@@ -189,11 +189,8 @@
}
for key, value in _ingressValidate {
for ing, ingValue in value.out.helm {
- // TODO(gio): support multiple ingresses
- // "\(key)-\(ing)": #Helm & ingValue & {
- "\(ing)": #Helm & ingValue & {
- // name: "\(key)-\(ing)"
- name: ing
+ "\(key)-\(ing)": #Helm & ingValue & {
+ name: "\(key)-\(ing)"
}
}
}
diff --git a/core/installer/app_configs/app_global_env.cue b/core/installer/app_configs/app_global_env.cue
index a9edbde..9a306f5 100644
--- a/core/installer/app_configs/app_global_env.cue
+++ b/core/installer/app_configs/app_global_env.cue
@@ -23,12 +23,14 @@
auth: #Auth
network: #Network
subdomain: string
+ appRoot: string | *""
service: close({
name: string
port: close({ name: string }) | close({ number: int & > 0 })
})
_domain: "\(subdomain).\(network.domain)"
+ _appRoot: appRoot
_authProxyHTTPPortName: "http"
out: {
@@ -81,12 +83,13 @@
}
}
}
- ingress: {
+ "\(_domain)": {
chart: charts.ingress
_service: service
info: "Generating TLS certificate for https://\(_domain)"
values: {
domain: _domain
+ appRoot: _appRoot
ingressClassName: network.ingressClass
certificateIssuer: network.certificateIssuer
service: {
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 4772076..7274e03 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -288,7 +288,9 @@
}
type ReleaseResources struct {
- Helm []Resource
+ Release Release
+ Helm []Resource
+ RenderedRaw []byte
}
// TODO(gio): rename to CommitApp
@@ -297,21 +299,21 @@
appDir string,
name string,
config any,
- ports []PortForward,
resources CueAppData,
data CueAppData,
opts ...InstallOption,
-) (ReleaseResources, error) {
+) error {
var o installOptions
for _, i := range opts {
i(&o)
}
dopts := []soft.DoOption{}
- // NOTE(gio): Expects caller to have pulled already
- dopts = append(dopts, soft.WithNoPull())
if o.Branch != "" {
dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
}
+ if o.NoPull {
+ dopts = append(dopts, soft.WithNoPull())
+ }
if o.NoPublish {
dopts = append(dopts, soft.WithNoCommit())
}
@@ -321,7 +323,7 @@
if o.NoLock {
dopts = append(dopts, soft.WithNoLock())
}
- return ReleaseResources{}, repo.Do(func(r soft.RepoFS) (string, error) {
+ return repo.Do(func(r soft.RepoFS) (string, error) {
if err := r.RemoveDir(appDir); err != nil {
return "", err
}
@@ -329,49 +331,55 @@
if err := r.CreateDir(resourcesDir); err != nil {
return "", err
}
- {
+ if err := func() error {
if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
- return "", err
+ return err
}
if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
- return "", err
+ return err
}
if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
- return "", err
+ return err
}
for name, contents := range data {
if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
- return "", fmt.Errorf("%s is forbidden", name)
+ return fmt.Errorf("%s is forbidden", name)
}
w, err := r.Writer(path.Join(appDir, name))
if err != nil {
- return "", err
+ return err
}
defer w.Close()
if _, err := w.Write(contents); err != nil {
- return "", err
+ return err
}
}
+ return nil
+ }(); err != nil {
+ return "", err
}
- {
+ if err := func() error {
if err := createKustomizationChain(r, resourcesDir); err != nil {
- return "", err
+ return err
}
appKust := gio.NewKustomization()
for name, contents := range resources {
appKust.AddResources(name)
w, err := r.Writer(path.Join(resourcesDir, name))
if err != nil {
- return "", err
+ return err
}
defer w.Close()
if _, err := w.Write(contents); err != nil {
- return "", err
+ return err
}
}
if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
- return "", err
+ return err
}
+ return nil
+ }(); err != nil {
+ return "", err
}
return fmt.Sprintf("install: %s", name), nil
}, dopts...)
@@ -399,9 +407,12 @@
i(o)
}
appDir = filepath.Clean(appDir)
- if err := m.repoIO.Pull(); err != nil {
- return ReleaseResources{}, err
+ if !o.NoPull {
+ if err := m.repoIO.Pull(); err != nil {
+ return ReleaseResources{}, err
+ }
}
+ opts = append(opts, WithNoPull())
if err := m.nsc.Create(namespace); err != nil {
return ReleaseResources{}, err
}
@@ -472,7 +483,7 @@
if err != nil {
return ReleaseResources{}, err
}
- if _, err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...); err != nil {
+ if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
return ReleaseResources{}, err
}
// TODO(gio): add ingress-nginx to release resources
@@ -480,7 +491,9 @@
return ReleaseResources{}, err
}
return ReleaseResources{
- Helm: extractHelm(rendered.Resources),
+ Release: rendered.Config.Release,
+ RenderedRaw: rendered.Raw,
+ Helm: extractHelm(rendered.Resources),
}, nil
}
@@ -559,7 +572,14 @@
if err != nil {
return ReleaseResources{}, err
}
- return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
+ if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
+ return ReleaseResources{}, err
+ }
+ return ReleaseResources{
+ Release: rendered.Config.Release,
+ RenderedRaw: rendered.Raw,
+ Helm: extractHelm(rendered.Resources),
+ }, nil
}
func (m *AppManager) Remove(instanceId string) error {
@@ -632,6 +652,7 @@
}
type installOptions struct {
+ NoPull bool
NoPublish bool
Env *EnvConfig
Networks []Network
@@ -690,6 +711,12 @@
}
}
+func WithNoPull() InstallOption {
+ return func(o *installOptions) {
+ o.NoPull = true
+ }
+}
+
func WithNoLock() InstallOption {
return func(o *installOptions) {
o.NoLock = true
@@ -785,7 +812,14 @@
if err != nil {
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.Resources, rendered.Data); err != nil {
+ return ReleaseResources{}, err
+ }
+ return ReleaseResources{
+ Release: rendered.Config.Release,
+ RenderedRaw: rendered.Raw,
+ Helm: extractHelm(rendered.Resources),
+ }, nil
}
// TODO(gio): take app configuration from the repo
@@ -823,7 +857,14 @@
if err != nil {
return ReleaseResources{}, err
}
- return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
+ if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
+ return ReleaseResources{}, err
+ }
+ return ReleaseResources{
+ Release: rendered.Config.Release,
+ RenderedRaw: rendered.Raw,
+ Helm: extractHelm(rendered.Resources),
+ }, nil
}
func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
diff --git a/core/installer/app_repository.go b/core/installer/app_repository.go
index df35ae8..62ca38a 100644
--- a/core/installer/app_repository.go
+++ b/core/installer/app_repository.go
@@ -38,6 +38,7 @@
var envAppConfigs = []string{
"values-tmpl/dodo-app-instance.cue",
+ "values-tmpl/dodo-app-instance-status.cue",
"values-tmpl/certificate-issuer-private.cue",
"values-tmpl/certificate-issuer-public.cue",
"values-tmpl/appmanager.cue",
diff --git a/core/installer/soft/repoio.go b/core/installer/soft/repoio.go
index a0b0459..f4c05bb 100644
--- a/core/installer/soft/repoio.go
+++ b/core/installer/soft/repoio.go
@@ -295,6 +295,7 @@
if err != nil {
return err
}
+ defer out.Close()
serialized, err := yaml.Marshal(data)
if err != nil {
return err
diff --git a/core/installer/values-tmpl/dodo-app-instance-status.cue b/core/installer/values-tmpl/dodo-app-instance-status.cue
new file mode 100644
index 0000000..ad66153
--- /dev/null
+++ b/core/installer/values-tmpl/dodo-app-instance-status.cue
@@ -0,0 +1,22 @@
+input: {
+ appName: string
+ network: #Network
+ appSubdomain: string
+}
+
+name: "Dodo App Instance Status"
+
+_subdomain: "status.\(input.appSubdomain)"
+
+ingress: {
+ "status-\(input.appName)": {
+ auth: enabled: false
+ network: input.network
+ subdomain: _subdomain
+ appRoot: "/\(input.appName)"
+ service: {
+ name: "web"
+ port: name: "http"
+ }
+ }
+}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 462a95d..afcd627 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -1,6 +1,7 @@
package welcome
import (
+ "bytes"
"context"
"embed"
"encoding/json"
@@ -392,6 +393,25 @@
http.Error(w, "missing app-name", http.StatusBadRequest)
return
}
+ u := r.Context().Value(userCtx)
+ if u == nil {
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+ user, ok := u.(string)
+ if !ok {
+ http.Error(w, "could not get user", http.StatusInternalServerError)
+ return
+ }
+ owner, err := s.st.GetAppOwner(appName)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if owner != user {
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
commits, err := s.st.GetCommitHistory(appName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -440,7 +460,12 @@
if err != nil {
return
}
- if err := s.updateDodoApp(req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks); err != nil {
+ apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+ instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
+ if err != nil {
+ return
+ }
+ if err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks); err != nil {
if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
fmt.Printf("Error: %s\n", err.Error())
return
@@ -652,7 +677,11 @@
return err
}
apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
- app, err := installer.FindEnvApp(apps, "dodo-app-instance")
+ instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
+ if err != nil {
+ return err
+ }
+ instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
if err != nil {
return err
}
@@ -661,9 +690,9 @@
if err != nil {
return err
}
- namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
+ namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
s.appConfigs[appName] = appConfig{namespace, network}
- if err := s.updateDodoApp(appName, namespace, networks); err != nil {
+ if err := s.updateDodoApp(instanceAppStatus, appName, namespace, networks); err != nil {
return err
}
configRepo, err := s.client.GetRepo(ConfigRepoName)
@@ -685,7 +714,7 @@
return "", err
}
if _, err := m.Install(
- app,
+ instanceApp,
appName,
"/"+appName,
namespace,
@@ -756,7 +785,19 @@
}
}
-func (s *DodoAppServer) updateDodoApp(name, namespace string, networks []installer.Network) error {
+type dodoAppRendered struct {
+ App struct {
+ Ingress struct {
+ Network string `json:"network"`
+ Subdomain string `json:"subdomain"`
+ } `json:"ingress"`
+ } `json:"app"`
+ Input struct {
+ AppId string `json:"appId"`
+ } `json:"input"`
+}
+
+func (s *DodoAppServer) updateDodoApp(appStatus installer.EnvApp, name, namespace string, networks []installer.Network) error {
repo, err := s.client.GetRepo(name)
if err != nil {
return err
@@ -775,26 +816,56 @@
return err
}
lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
- if _, err := m.Install(
- app,
- "app",
- "/.dodo/app",
- namespace,
- map[string]any{
- "repoAddr": repo.FullAddress(),
- "managerAddr": fmt.Sprintf("http://%s", s.self),
- "appId": name,
- "sshPrivateKey": s.sshKey,
- },
- installer.WithConfig(&s.env),
- installer.WithNetworks(networks),
- installer.WithLocalChartGenerator(lg),
- installer.WithBranch("dodo"),
- installer.WithForce(),
- ); err != nil {
- return err
- }
- return nil
+ return repo.Do(func(r soft.RepoFS) (string, error) {
+ res, err := m.Install(
+ app,
+ "app",
+ "/.dodo/app",
+ namespace,
+ map[string]any{
+ "repoAddr": repo.FullAddress(),
+ "managerAddr": fmt.Sprintf("http://%s", s.self),
+ "appId": name,
+ "sshPrivateKey": s.sshKey,
+ },
+ installer.WithNoPull(),
+ installer.WithNoPublish(),
+ installer.WithConfig(&s.env),
+ installer.WithNetworks(networks),
+ installer.WithLocalChartGenerator(lg),
+ installer.WithNoLock(),
+ )
+ if err != nil {
+ return "", err
+ }
+ var rendered dodoAppRendered
+ if err := json.NewDecoder(bytes.NewReader(res.RenderedRaw)).Decode(&rendered); err != nil {
+ return "", nil
+ }
+ if _, err := m.Install(
+ appStatus,
+ "status",
+ "/.dodo/status",
+ s.namespace,
+ map[string]any{
+ "appName": rendered.Input.AppId,
+ "network": rendered.App.Ingress.Network,
+ "appSubdomain": rendered.App.Ingress.Subdomain,
+ },
+ installer.WithNoPull(),
+ installer.WithNoPublish(),
+ installer.WithConfig(&s.env),
+ installer.WithNetworks(networks),
+ installer.WithLocalChartGenerator(lg),
+ installer.WithNoLock(),
+ ); err != nil {
+ return "", err
+ }
+ return "install app", nil
+ },
+ soft.WithCommitToBranch("dodo"),
+ soft.WithForce(),
+ )
}
func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) error {