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/auth/proxy/main.go b/core/auth/proxy/main.go
index 2114127..2c10258 100644
--- a/core/auth/proxy/main.go
+++ b/core/auth/proxy/main.go
@@ -25,6 +25,7 @@
var membershipPublicAddr = flag.String("membership-public-addr", "", "Public address of membership service")
var groups = flag.String("groups", "", "Comma separated list of groups. User must be part of at least one of them. If empty group membership will not be checked.")
var upstream = flag.String("upstream", "", "Upstream service address")
+var noAuthPathPrefixes = flag.String("no-auth-path-prefixes", "", "Path prefixes to disable authentication for")
//go:embed unauthorized.html
var unauthorizedHTML embed.FS
@@ -92,46 +93,60 @@
}
func handle(w http.ResponseWriter, r *http.Request) {
- user, err := queryWhoAmI(r.Cookies())
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if user == nil {
- if r.Method != http.MethodGet {
- http.Error(w, "Unauthorized", http.StatusUnauthorized)
- return
+ reqAuth := true
+ for _, p := range strings.Split(*noAuthPathPrefixes, ",") {
+ if strings.HasPrefix(r.URL.Path, p) {
+ reqAuth = false
+ break
}
- curr, err := getAddr(r)
+ }
+ var user *user
+ if reqAuth {
+ var err error
+ user, err = queryWhoAmI(r.Cookies())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- addr := fmt.Sprintf("%s?return_to=%s", *loginAddr, curr.String())
- http.Redirect(w, r, addr, http.StatusSeeOther)
- return
- }
- if *groups != "" {
- hasPermission := false
- tg, err := getTransitiveGroups(user.Identity.Traits.Username)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- for _, i := range strings.Split(*groups, ",") {
- if slices.Contains(tg, strings.TrimSpace(i)) {
- hasPermission = true
- break
+ if user == nil {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
}
- }
- if !hasPermission {
- groupList := strings.Split(*groups, ",")
- renderUnauthorizedPage(w, groupList)
+ curr, err := getAddr(r)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ addr := fmt.Sprintf("%s?return_to=%s", *loginAddr, curr.String())
+ http.Redirect(w, r, addr, http.StatusSeeOther)
return
}
+ if *groups != "" {
+ hasPermission := false
+ tg, err := getTransitiveGroups(user.Identity.Traits.Username)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ for _, i := range strings.Split(*groups, ",") {
+ if slices.Contains(tg, strings.TrimSpace(i)) {
+ hasPermission = true
+ break
+ }
+ }
+ if !hasPermission {
+ groupList := strings.Split(*groups, ",")
+ renderUnauthorizedPage(w, groupList)
+ return
+ }
+ }
}
rc := r.Clone(context.Background())
- rc.Header.Add("X-User", user.Identity.Traits.Username)
+ if user != nil {
+ // TODO(gio): Rename to X-Forwarded-User
+ rc.Header.Add("X-User", user.Identity.Traits.Username)
+ }
ru, err := url.Parse(fmt.Sprintf("http://%s%s", *upstream, r.URL.RequestURI()))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -237,7 +252,7 @@
if *groups != "" && (*membershipAddr == "" || *membershipPublicAddr == "") {
log.Fatal("membership-addr and membership-public-addr flags are required when groups are provided")
}
- http.Handle("/static/", cachingHandler{http.FileServer(http.FS(f))})
+ http.Handle("/.auth/static/", http.StripPrefix("/.auth", cachingHandler{http.FileServer(http.FS(f))}))
http.HandleFunc("/", handle)
fmt.Printf("Starting HTTP server on port: %d\n", *port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
diff --git a/core/auth/proxy/unauthorized.html b/core/auth/proxy/unauthorized.html
index 35fafab..130cf81 100644
--- a/core/auth/proxy/unauthorized.html
+++ b/core/auth/proxy/unauthorized.html
@@ -4,8 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auth Proxy</title>
- <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
- <link rel="stylesheet" href="/static/main.css?v=0.0.1">
+ <link rel="stylesheet" href="/.auth/static/pico.2.0.6.min.css">
+ <link rel="stylesheet" href="/.auth/static/main.css?v=0.0.1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css">
</head>
<body>
diff --git a/core/installer/app.go b/core/installer/app.go
index d7f6989..1bf632c 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -255,8 +255,8 @@
return cue.Value{}, fmt.Errorf("invalid")
}
ret := ctx.BuildInstance(instances[0])
- if ret.Err() != nil {
- return cue.Value{}, ret.Err()
+ if err := ret.Err(); err != nil {
+ return cue.Value{}, err
}
if err := ret.Validate(); err != nil {
return cue.Value{}, err
diff --git a/core/installer/app_configs/app_base.cue b/core/installer/app_configs/app_base.cue
index 3f0036b..4075281 100644
--- a/core/installer/app_configs/app_base.cue
+++ b/core/installer/app_configs/app_base.cue
@@ -36,8 +36,9 @@
appType: #AppType | *"env"
#Auth: {
- enabled: bool | *false // TODO(gio): enabled by default?
- groups: string | *"" // TODO(gio): []string
+ enabled: bool | *false // TODO(gio): enabled by default?
+ groups: string | *"" // TODO(gio): []string
+ noAuthPathPrefixes: [...string] | *[]
}
#Image: {
diff --git a/core/installer/app_configs/app_global_env.cue b/core/installer/app_configs/app_global_env.cue
index e6e8de8..fba9dcf 100644
--- a/core/installer/app_configs/app_global_env.cue
+++ b/core/installer/app_configs/app_global_env.cue
@@ -114,6 +114,7 @@
membershipPublicAddr: "https://memberships.\(g.privateDomain)"
}
groups: auth.groups
+ noAuthPathPrefixes: strings.Join(auth.noAuthPathPrefixes, ",")
portName: _authProxyHTTPPortName
}
}
diff --git a/core/installer/app_configs/dodo_app.cue b/core/installer/app_configs/dodo_app.cue
index a4bbf0c..b40eaed 100644
--- a/core/installer/app_configs/dodo_app.cue
+++ b/core/installer/app_configs/dodo_app.cue
@@ -93,42 +93,28 @@
type: string
cluster?: string
ingress: #AppIngress
- volumes: {
- for k, v in volumes {
- "\(k)": #volume & v & {
- name: k
- }
- }
- ...
- }
- postgresql: {
- for k, v in postgresql {
- "\(k)": #PostgreSQL & v & {
- name: k
- }
- }
- ...
- }
+ volumes: [...#volume]
+ postgresql: [...#PostgreSQL]
rootDir: string
runConfiguration: [...#Command]
dev: #Dev | *{ enabled: false }
vm: #VMCustomization
lastCmdEnv: [
- for k, v in volumes {
- "DODO_VOLUME_\(strings.ToUpper(k))=/dodo-volume/\(v.name)"
+ for v in volumes {
+ "DODO_VOLUME_\(strings.ToUpper(v.name))=/dodo-volume/\(v.name)"
}
- for k, v in postgresql {
- "DODO_POSTGRESQL_\(strings.ToUpper(k))_ADDRESS=postgres-\(v.name).\(release.namespace).svc.cluster.local"
+ for v in postgresql {
+ "DODO_POSTGRESQL_\(strings.ToUpper(v.name))_ADDRESS=postgres-\(v.name).\(release.namespace).svc.cluster.local"
}
- for k, v in postgresql {
- "DODO_POSTGRESQL_\(strings.ToUpper(k))_USERNAME=postgres"
+ for v in postgresql {
+ "DODO_POSTGRESQL_\(strings.ToUpper(v.name))_USERNAME=postgres"
}
- for k, v in postgresql {
- "DODO_POSTGRESQL_\(strings.ToUpper(k))_PASSWORD=postgres"
+ for v in postgresql {
+ "DODO_POSTGRESQL_\(strings.ToUpper(v.name))_PASSWORD=postgres"
}
- for k, v in postgresql {
- "DODO_POSTGRESQL_\(strings.ToUpper(k))_DATABASE=postgres"
+ for v in postgresql {
+ "DODO_POSTGRESQL_\(strings.ToUpper(v.name))_DATABASE=postgres"
}
]
@@ -151,8 +137,6 @@
port: int | *8080
rootDir: _appDir
- volumes: {...}
- postgresql: {...}
lastCmdEnv: [...string]
runConfiguration: [{
@@ -197,8 +181,6 @@
port: int | *8080
rootDir: _appDir
- volumes: {...}
- postgresql: {...}
lastCmdEnv: [...string]
runConfiguration: [{
@@ -226,8 +208,6 @@
port: int | *80
rootDir: "/var/www/html"
- volumes: {...}
- postgresql: {...}
lastCmdEnv: [...string]
runConfiguration: [{
@@ -337,9 +317,9 @@
runCfg: base64.Encode(null, json.Marshal(_app.runConfiguration))
managerAddr: input.managerAddr
volumes: [
- for key, value in _app.volumes {
- name: value.name
- mountPath: "/dodo-volume/\(key)"
+ for v in _app.volumes {
+ name: v.name
+ mountPath: "/dodo-volume/\(v.name)"
}
]
}
@@ -382,8 +362,16 @@
if app.cluster != _|_ {
cluster: clusterMap[strings.ToLower(app.cluster)]
}
- volumes: app.volumes
- postgresql: app.postgresql
+ volumes: {
+ for v in app.volumes {
+ "\(v.name)": v
+ }
+ }
+ postgresql: {
+ for v in app.postgresql {
+ "\(v.name)": v
+ }
+ }
vm: {
"\(_vmName)": _devVM
}
diff --git a/core/installer/app_repository.go b/core/installer/app_repository.go
index 67d2833..e4f462e 100644
--- a/core/installer/app_repository.go
+++ b/core/installer/app_repository.go
@@ -21,7 +21,7 @@
var storeEnvAppConfigs = []string{
"values-tmpl/dodo-app.cue",
"values-tmpl/virtual-machine.cue",
- "values-tmpl/coder.cue",
+ // "values-tmpl/coder.cue",
"values-tmpl/url-shortener.cue",
"values-tmpl/matrix.cue",
"values-tmpl/vaultwarden.cue",
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index bcad861..47eb02c 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -479,12 +479,14 @@
enabled: true
username: "gio"
}
- volumes: {
- data: size: "5Gi"
- }
- postgresql: {
- db: size: "10Gi"
- }
+ volumes: [{
+ name: "data"
+ size: "5Gi"
+ }]
+ postgresql: [{
+ name: "db"
+ size: "10Gi"
+ }]
}`
func TestDodoAppDevDisabled(t *testing.T) {
diff --git a/core/installer/cmd/dodo_app.go b/core/installer/cmd/dodo_app.go
index bcd8a68..7954690 100644
--- a/core/installer/cmd/dodo_app.go
+++ b/core/installer/cmd/dodo_app.go
@@ -29,6 +29,7 @@
sshKey string
repoAddr string
self string
+ selfPublic string
repoPublicAddr string
namespace string
envAppManagerAddr string
@@ -88,6 +89,12 @@
"",
)
cmd.Flags().StringVar(
+ &dodoAppFlags.selfPublic,
+ "self-public",
+ "",
+ "",
+ )
+ cmd.Flags().StringVar(
&dodoAppFlags.fetchUsersAddr,
"fetch-users-addr",
"",
@@ -214,6 +221,7 @@
dodoAppFlags.port,
dodoAppFlags.apiPort,
dodoAppFlags.self,
+ dodoAppFlags.selfPublic,
dodoAppFlags.repoPublicAddr,
string(sshKey),
dodoAppFlags.gitRepoPublicKey,
diff --git a/core/installer/values-tmpl/dodo-app.cue b/core/installer/values-tmpl/dodo-app.cue
index 8bb57c0..cb61b45 100644
--- a/core/installer/values-tmpl/dodo-app.cue
+++ b/core/installer/values-tmpl/dodo-app.cue
@@ -105,6 +105,11 @@
}
if !input.external {
enabled: true
+ noAuthPathPrefixes: [
+ "/static/",
+ "/schemas/",
+ "/api/public-data",
+ ]
}
}
network: input.network
@@ -150,6 +155,7 @@
repoAddr: "soft-serve.\(release.namespace).svc.cluster.local:22"
sshPrivateKey: base64.Encode(null, input.dAppKeys.private)
self: "api.\(release.namespace).svc.cluster.local"
+ selfPublic: url
repoPublicAddr: "ssh://\(_domain):\(input.sshPort)"
namespace: release.namespace
envAppManagerAddr: "http://appmanager.\(global.namespacePrefix)appmanager.svc.cluster.local"
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"
+ ]
+ }
+ }
+}