DodoApp: Use JSON file for configuration.
Specify json schema so code editors can validate user input.
Update auth proxy to disable auth on specified paths.
Change-Id: Ic6667d802a9553444d3630c4ff73f4b33304ccfd
diff --git a/core/installer/welcome/app-tmpl/golang-1.20.0/app.cue.gotmpl b/core/installer/welcome/app-tmpl/golang-1.20.0/app.cue.gotmpl
deleted file mode 100755
index 9cd1442..0000000
--- a/core/installer/welcome/app-tmpl/golang-1.20.0/app.cue.gotmpl
+++ /dev/null
@@ -1,9 +0,0 @@
-app: {
- type: "golang:1.20.0"
- run: "main.go"
- ingress: {
- network: "{{ .Network.Name }}"
- subdomain: "{{ .Subdomain }}"
- auth: enabled: false
- }
-}
diff --git a/core/installer/welcome/app-tmpl/golang-1.20.0/app.json.gotmpl b/core/installer/welcome/app-tmpl/golang-1.20.0/app.json.gotmpl
new file mode 100755
index 0000000..60356c6
--- /dev/null
+++ b/core/installer/welcome/app-tmpl/golang-1.20.0/app.json.gotmpl
@@ -0,0 +1,14 @@
+{
+ "$schema": "{{ .SchemaAddr }}",
+ "app": {
+ "type": "golang:1.20.0",
+ "run": "main.go",
+ "ingress": {
+ "network": "{{ .Network.Name }}",
+ "subdomain": "{{ .Subdomain }}",
+ "auth": {
+ "enabled": false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/installer/welcome/app-tmpl/hugo-latest/app.cue.gotmpl b/core/installer/welcome/app-tmpl/hugo-latest/app.cue.gotmpl
deleted file mode 100644
index cbf7b23..0000000
--- a/core/installer/welcome/app-tmpl/hugo-latest/app.cue.gotmpl
+++ /dev/null
@@ -1,8 +0,0 @@
-app: {
- type: "hugo:latest"
- ingress: {
- network: "{{ .Network.Name }}"
- subdomain: "{{ .Subdomain }}"
- auth: enabled: false
- }
-}
diff --git a/core/installer/welcome/app-tmpl/hugo-latest/app.json.gotmpl b/core/installer/welcome/app-tmpl/hugo-latest/app.json.gotmpl
new file mode 100644
index 0000000..bc71973
--- /dev/null
+++ b/core/installer/welcome/app-tmpl/hugo-latest/app.json.gotmpl
@@ -0,0 +1,13 @@
+{
+ "$schema": "{{ .SchemaAddr }}",
+ "app": {
+ "type": "hugo:latest",
+ "ingress": {
+ "network": "{{ .Network.Name }}",
+ "subdomain": "{{ .Subdomain }}",
+ "auth": {
+ "enabled": false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/installer/welcome/app-tmpl/php-8.2-apache/app.cue.gotmpl b/core/installer/welcome/app-tmpl/php-8.2-apache/app.cue.gotmpl
index fa925fe..f5a061d 100755
--- a/core/installer/welcome/app-tmpl/php-8.2-apache/app.cue.gotmpl
+++ b/core/installer/welcome/app-tmpl/php-8.2-apache/app.cue.gotmpl
@@ -1,8 +1,13 @@
-app: {
- type: "php:8.2-apache"
- ingress: {
- network: "{{ .Network.Name }}"
- subdomain: "{{ .Subdomain }}"
- auth: enabled: false
+{
+ "$schema": "{{ .SchemaAddr }}",
+ "app": {
+ "type": "php:8.2-apache",
+ "ingress": {
+ "network": "{{ .Network.Name }}",
+ "subdomain": "{{ .Subdomain }}",
+ "auth": {
+ "enabled": false
+ }
+ }
}
-}
+}
\ No newline at end of file
diff --git a/core/installer/welcome/app_tmpl.go b/core/installer/welcome/app_tmpl.go
index fb512fa..911b9b8 100644
--- a/core/installer/welcome/app_tmpl.go
+++ b/core/installer/welcome/app_tmpl.go
@@ -68,7 +68,7 @@
}
type AppTmpl interface {
- Render(network installer.Network, subdomain string) (map[string][]byte, error)
+ Render(schemaAddr string, network installer.Network, subdomain string) (map[string][]byte, error)
}
type appTmplFS struct {
@@ -108,7 +108,7 @@
return &appTmplFS{files, tmpls}, nil
}
-func (a *appTmplFS) Render(network installer.Network, subdomain string) (map[string][]byte, error) {
+func (a *appTmplFS) Render(schemaAddr string, network installer.Network, subdomain string) (map[string][]byte, error) {
ret := map[string][]byte{}
for path, contents := range a.files {
ret[path] = contents
@@ -116,8 +116,9 @@
for path, tmpl := range a.tmpls {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, map[string]any{
- "Network": network,
- "Subdomain": subdomain,
+ "SchemaAddr": schemaAddr,
+ "Network": network,
+ "Subdomain": subdomain,
}); err != nil {
return nil, err
}
diff --git a/core/installer/welcome/app_tmpl_test.go b/core/installer/welcome/app_tmpl_test.go
index 00e5a3c..f3ff71e 100644
--- a/core/installer/welcome/app_tmpl_test.go
+++ b/core/installer/welcome/app_tmpl_test.go
@@ -35,7 +35,7 @@
if err != nil {
t.Fatal(err)
}
- if _, err := a.Render(network, "testapp"); err != nil {
+ if _, err := a.Render("schema.json", network, "testapp"); err != nil {
t.Fatal(err)
}
}
@@ -53,7 +53,7 @@
if err != nil {
t.Fatal(err)
}
- if _, err := a.Render(network, "testapp"); err != nil {
+ if _, err := a.Render("schema.json", network, "testapp"); err != nil {
t.Fatal(err)
}
}
@@ -71,7 +71,7 @@
if err != nil {
t.Fatal(err)
}
- if _, err := a.Render(network, "testapp"); err != nil {
+ if _, err := a.Render("schema.json", network, "testapp"); err != nil {
t.Fatal(err)
}
}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 4307866..d71bf83 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -35,12 +35,16 @@
//go:embed all:app-tmpl
var appTmplsFS embed.FS
+//go:embed stat/schemas/app.schema.json
+var dodoAppJsonSchema []byte
+
const (
ConfigRepoName = "config"
appConfigsFile = "/apps.json"
loginPath = "/login"
logoutPath = "/logout"
staticPath = "/stat/"
+ schemasPath = "/schemas/"
apiPublicData = "/api/public-data"
apiCreateApp = "/api/apps"
sessionCookie = "dodo-app-session"
@@ -94,6 +98,7 @@
port int
apiPort int
self string
+ selfPublic string
repoPublicAddr string
sshKey string
gitRepoPublicKey string
@@ -128,6 +133,7 @@
port int,
apiPort int,
self string,
+ selfPublic string,
repoPublicAddr string,
sshKey string,
gitRepoPublicKey string,
@@ -163,6 +169,7 @@
port,
apiPort,
self,
+ selfPublic,
repoPublicAddr,
sshKey,
gitRepoPublicKey,
@@ -218,6 +225,7 @@
go func() {
r := mux.NewRouter()
r.Use(s.mwAuth)
+ r.HandleFunc(schemasPath+"app.schema.json", s.handleSchema).Methods(http.MethodGet)
r.PathPrefix(staticPath).Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
r.HandleFunc(apiPublicData, s.handleAPIPublicData)
@@ -320,6 +328,7 @@
if strings.HasSuffix(r.URL.Path, loginPath) ||
strings.HasPrefix(r.URL.Path, logoutPath) ||
strings.HasPrefix(r.URL.Path, staticPath) ||
+ strings.HasPrefix(r.URL.Path, schemasPath) ||
strings.HasPrefix(r.URL.Path, apiPublicData) ||
strings.HasPrefix(r.URL.Path, apiCreateApp) {
next.ServeHTTP(w, r)
@@ -340,6 +349,11 @@
})
}
+func (s *DodoAppServer) handleSchema(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/schema+json")
+ w.Write(dodoAppJsonSchema)
+}
+
func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
// TODO(gio): move to UserGetter
http.SetCookie(w, &http.Cookie{
@@ -1017,7 +1031,7 @@
if err != nil {
return err
}
- appCfg, err := soft.ReadFile(appRepo, "app.cue")
+ appCfg, err := soft.ReadFile(appRepo, "app.json")
if err != nil {
return err
}
@@ -1025,7 +1039,7 @@
if err != nil {
return err
}
- return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.cue": branchCfg})
+ return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.json": branchCfg})
}
func (s *DodoAppServer) createAppForBranch(
@@ -1238,7 +1252,7 @@
if err != nil {
return installer.ReleaseResources{}, err
}
- appCfg, err := soft.ReadFile(repo, "app.cue")
+ appCfg, err := soft.ReadFile(repo, "app.json")
if err != nil {
return installer.ReleaseResources{}, err
}
@@ -1316,7 +1330,7 @@
if err != nil {
return nil, err
}
- return appTmpl.Render(network, subdomain)
+ return appTmpl.Render(fmt.Sprintf("%s/stat/schemas/dodo_app.jsonschema", s.selfPublic), network, subdomain)
}
func generatePassword() string {
@@ -1677,7 +1691,9 @@
}
func createDevBranchAppConfig(from []byte, branch, username string) (string, []byte, error) {
- cfg, err := installer.ParseCueAppConfig(installer.CueAppData{"app.cue": from})
+ cfg, err := installer.ParseCueAppConfig(installer.CueAppData{
+ "app.cue": from,
+ })
if err != nil {
return "", nil, err
}
diff --git a/core/installer/welcome/dodo_app_test.go b/core/installer/welcome/dodo_app_test.go
index 0f0f526..8fc83e7 100644
--- a/core/installer/welcome/dodo_app_test.go
+++ b/core/installer/welcome/dodo_app_test.go
@@ -5,14 +5,17 @@
)
func TestCreateDevBranch(t *testing.T) {
- cfg := []byte(`
-app: {
- type: "golang:1.22.0"
- run: "main.go"
- ingress: {
- network: "private"
- subdomain: "testapp"
- auth: enabled: false
+ cfg := []byte(`{
+ "app": {
+ "type": "golang:1.22.0",
+ "run": "main.go",
+ "ingress": {
+ "network": "private",
+ "subdomain": "testapp",
+ "auth": {
+ "enabled": false
+ }
+ }
}
}`)
network, newCfg, err := createDevBranchAppConfig(cfg, "foo", "bar")
diff --git a/core/installer/welcome/stat/schemas/app.schema.json b/core/installer/welcome/stat/schemas/app.schema.json
new file mode 100644
index 0000000..f6264de
--- /dev/null
+++ b/core/installer/welcome/stat/schemas/app.schema.json
@@ -0,0 +1,210 @@
+{
+ "$id": "https://dodo.cloud/schemas/app.schema.json",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "app": {
+ "type": "object",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/golang"
+ },
+ {
+ "$ref": "#/definitions/hugo"
+ },
+ {
+ "$ref": "#/definitions/php"
+ }
+ ]
+ }
+ },
+ "definitions": {
+ "golang": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "oneOf": [
+ {
+ "const": "golang:1.22.0"
+ },
+ {
+ "const": "golang:1.20.0"
+ }
+ ]
+ },
+ "run": {
+ "type": "string"
+ },
+ "ingress": {
+ "$ref": "#/definitions/ingress"
+ },
+ "volumes": {
+ "$ref": "#/definitions/volumes"
+ },
+ "postgresql": {
+ "$ref": "#/definitions/postgresql"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ "hugo": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "oneOf": [
+ {
+ "const": "hugo:latest"
+ }
+ ]
+ },
+ "ingress": {
+ "$ref": "#/definitions/ingress"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ "php": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "oneOf": [
+ {
+ "const": "php:8.2-apache"
+ }
+ ]
+ },
+ "ingress": {
+ "$ref": "#/definitions/ingress"
+ },
+ "volumes": {
+ "$ref": "#/definitions/volumes"
+ },
+ "postgresql": {
+ "$ref": "#/definitions/postgresql"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ "volume": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "size": {
+ "$ref": "#/definitions/size"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "size"
+ ]
+ },
+ "volumes": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/volume"
+ }
+ },
+ "postgre": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "size": {
+ "$ref": "#/definitions/size"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "size"
+ ]
+ },
+ "postgresql": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/postgre"
+ }
+ },
+ "size": {
+ "type": "string",
+ "pattern": "[1-9][0-9]*(Mi|Gi)"
+ },
+ "ingress": {
+ "type": "object",
+ "properties": {
+ "network": {
+ "type": "string",
+ "minLength": 1
+ },
+ "subdomain": {
+ "type": "string",
+ "minLength": 1
+ },
+ "auth": {
+ "type": "object",
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "enum": [
+ false
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "enabled"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "enum": [
+ true
+ ]
+ },
+ "groups": {
+ "type": "string"
+ },
+ "noAuthPathPrefixes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^/.*"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "enabled"
+ ]
+ }
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "network",
+ "subdomain"
+ ]
+ }
+ }
+}