diff --git a/app/backend/internal/api/templategraph_test.go b/app/backend/internal/api/templategraph_test.go index d701dad..45ffdb5 100644 --- a/app/backend/internal/api/templategraph_test.go +++ b/app/backend/internal/api/templategraph_test.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "strings" "testing" ) @@ -157,3 +158,45 @@ func TestValidateTemplateGraph(t *testing.T) { t.Fatal("expected missing runtime error") } } + +// TestOfficialTemplatesAllParse walks every seeded template and asserts the +// config parses, graph templates topo-sort + validate, and a dry-run resolve +// produces no unresolved refs (defaults cover all expressions). +func TestOfficialTemplatesAllParse(t *testing.T) { + for _, tpl := range SeedTemplates() { + var cfg TemplateConfig + if err := json.Unmarshal([]byte(tpl.Config), &cfg); err != nil { + t.Errorf("%s: config parse: %v", tpl.ID, err) + continue + } + if len(cfg.Services) == 0 { + continue + } + sorted, err := topoSortServices(cfg.Services) + if err != nil { + t.Errorf("%s: topo sort: %v", tpl.ID, err) + continue + } + if err := validateTemplateGraph(sorted); err != nil { + t.Errorf("%s: validate: %v", tpl.ID, err) + continue + } + var vars []TemplateVariable + if tpl.Variables != "" { + if err := json.Unmarshal([]byte(tpl.Variables), &vars); err != nil { + t.Errorf("%s: variables parse: %v", tpl.ID, err) + continue + } + } + userVars := map[string]string{} + for _, v := range vars { + if v.Default != "" { + userVars[v.Key] = v.Default + } + } + graph := resolveTemplateGraph(sorted, userVars) + if len(graph.unresolved) > 0 { + t.Errorf("%s: unresolved refs: %v", tpl.ID, graph.unresolved) + } + } +} diff --git a/app/backend/internal/api/templates.go b/app/backend/internal/api/templates.go index 966cb9a..46d8fd1 100644 --- a/app/backend/internal/api/templates.go +++ b/app/backend/internal/api/templates.go @@ -1006,6 +1006,74 @@ func SeedTemplates() []ServiceTemplate { Variables: `[]`, IsOfficial: true, }, + { + ID: "tpl-ghost-stack", + Name: "Ghost + MySQL", + Description: "Ghost publishing platform backed by a managed MySQL database", + Category: "stack", + Logo: "https://cdn.simpleicons.org/ghost", + Config: `{"version":2,"services":[ + {"key":"db","type":"database","runtime":"mysql","environment":{"MYSQL_DATABASE":"ghost"}}, + {"key":"web","name":"ghost","type":"web","runtime":"ghost:5-alpine","port":2368,"depends_on":["db"], + "environment":{"url":"{{APP_URL:-http://localhost:2368}}","database__client":"mysql","database__connection__host":"{{service.db.host}}","database__connection__user":"{{service.db.user}}","database__connection__password":"{{service.db.password}}","database__connection__database":"{{service.db.database}}"}}]}`, + Variables: `[{"key":"APP_URL","label":"Public URL","default":"","required":false,"secret":false}]`, + IsOfficial: true, + }, + { + ID: "tpl-gitea-stack", + Name: "Gitea + PostgreSQL", + Description: "Self-hosted Git service backed by a managed PostgreSQL database", + Category: "stack", + Logo: "https://cdn.simpleicons.org/gitea", + Config: `{"version":2,"services":[ + {"key":"db","type":"database","runtime":"postgresql","environment":{"POSTGRES_DB":"gitea"}}, + {"key":"web","name":"gitea","type":"web","runtime":"gitea/gitea:1","port":3000,"health_check":"/api/healthz","depends_on":["db"], + "environment":{"GITEA__database__DB_TYPE":"postgres","GITEA__database__HOST":"{{service.db.host}}:{{service.db.port}}","GITEA__database__NAME":"{{service.db.database}}","GITEA__database__USER":"{{service.db.user}}","GITEA__database__PASSWD":"{{service.db.password}}","GITEA__security__SECRET_KEY":"{{secret}}","GITEA__security__INTERNAL_TOKEN":"{{secret(64)}}"}}]}`, + Variables: `[]`, + IsOfficial: true, + }, + { + ID: "tpl-nextcloud-stack", + Name: "Nextcloud + PostgreSQL + Redis", + Description: "Nextcloud with a managed PostgreSQL database and Redis cache", + Category: "stack", + Logo: "https://cdn.simpleicons.org/nextcloud", + Config: `{"version":2,"services":[ + {"key":"db","type":"database","runtime":"postgresql","environment":{"POSTGRES_DB":"nextcloud"}}, + {"key":"cache","type":"database","runtime":"redis"}, + {"key":"web","name":"nextcloud","type":"web","runtime":"nextcloud:30-apache","port":80,"health_check":"/status.php","depends_on":["db","cache"], + "environment":{"POSTGRES_HOST":"{{service.db.host}}","POSTGRES_DB":"{{service.db.database}}","POSTGRES_USER":"{{service.db.user}}","POSTGRES_PASSWORD":"{{service.db.password}}","REDIS_HOST":"{{service.cache.host}}","REDIS_HOST_PASSWORD":"{{service.cache.password}}","NEXTCLOUD_ADMIN_USER":"admin","NEXTCLOUD_ADMIN_PASSWORD":"{{secret}}","NEXTCLOUD_TRUSTED_DOMAINS":"{{APP_DOMAIN:-localhost}}"}}]}`, + Variables: `[{"key":"APP_DOMAIN","label":"Trusted domain","default":"","required":false,"secret":false}]`, + IsOfficial: true, + }, + { + ID: "tpl-paperless-stack", + Name: "Paperless-ngx + PostgreSQL + Redis", + Description: "Document management with managed PostgreSQL and Redis", + Category: "stack", + Logo: "https://cdn.simpleicons.org/paperlessngx", + Config: `{"version":2,"services":[ + {"key":"db","type":"database","runtime":"postgresql","environment":{"POSTGRES_DB":"paperless"}}, + {"key":"cache","type":"database","runtime":"redis"}, + {"key":"web","name":"paperless","type":"web","runtime":"ghcr.io/paperless-ngx/paperless-ngx:latest","port":8000,"depends_on":["db","cache"], + "environment":{"PAPERLESS_DBHOST":"{{service.db.host}}","PAPERLESS_DBPORT":"{{service.db.port}}","PAPERLESS_DBNAME":"{{service.db.database}}","PAPERLESS_DBUSER":"{{service.db.user}}","PAPERLESS_DBPASS":"{{service.db.password}}","PAPERLESS_REDIS":"{{service.cache.url}}","PAPERLESS_ADMIN_USER":"admin","PAPERLESS_ADMIN_PASSWORD":"{{secret}}","PAPERLESS_SECRET_KEY":"{{secret(48)}}","PAPERLESS_URL":"{{APP_URL:-http://localhost:8000}}"}}]}`, + Variables: `[{"key":"APP_URL","label":"Public URL","default":"","required":false,"secret":false}]`, + IsOfficial: true, + }, + { + ID: "tpl-plausible-stack", + Name: "Plausible + PostgreSQL + ClickHouse", + Description: "Privacy-friendly analytics with managed PostgreSQL and ClickHouse", + Category: "stack", + Logo: "https://cdn.simpleicons.org/plausibleanalytics", + Config: `{"version":2,"services":[ + {"key":"db","type":"database","runtime":"postgresql","environment":{"POSTGRES_DB":"plausible"}}, + {"key":"events","type":"database","runtime":"clickhouse","environment":{"CLICKHOUSE_DB":"plausible"}}, + {"key":"web","name":"plausible","type":"web","runtime":"ghcr.io/plausible/community-edition:v2","port":8000,"health_check":"/api/health","depends_on":["db","events"], + "environment":{"BASE_URL":"{{APP_URL:-http://localhost:8000}}","SECRET_KEY_BASE":"{{secret(64)}}","DATABASE_URL":"{{service.db.url}}","CLICKHOUSE_DATABASE_URL":"http://{{service.events.host}}:{{service.events.port}}/{{service.events.database}}","TOTP_VAULT_KEY":"{{secret(32)}}"}}]}`, + Variables: `[{"key":"APP_URL","label":"Public URL","default":"","required":false,"secret":false}]`, + IsOfficial: true, + }, } return templates }