installer: migrate apps to cuelang
diff --git a/core/installer/app.go b/core/installer/app.go
index de56808..7d22c0b 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -25,6 +25,139 @@
 //go:embed values-tmpl
 var valuesTmpls embed.FS
 
+const cueBaseConfigImports = `
+import (
+    "list"
+)
+`
+
+// TODO(gio): import
+const cueBaseConfig = `
+readme: string | *""
+
+#Network: {
+	name: string
+	ingressClass: string
+	certificateIssuer: string | *""
+	domain: string
+}
+
+#Image: {
+	registry: string | *"docker.io"
+	repository: string
+	name: string
+	tag: string
+	pullPolicy: string | *"IfNotPresent"
+	imageName: "\(repository)/\(name)"
+	fullName: "\(registry)/\(imageName)"
+	fullNameWithTag: "\(fullName):\(tag)"
+}
+
+#Chart: {
+	chart: string
+	sourceRef: #SourceRef
+}
+
+#SourceRef: {
+	kind: "GitRepository" | "HelmRepository"
+	name: string
+	namespace: string // TODO(gio): default global.id
+}
+
+#Global: {
+	id: string | *""
+	pcloudEnvName: string | *""
+	domain: string | *""
+    privateDomain: string | *""
+	namespacePrefix: string | *""
+	...
+}
+
+#Release: {
+	namespace: string
+}
+
+global: #Global
+release: #Release
+
+_ingressPrivate: "\(global.id)-ingress-private"
+_ingressPublic: "\(global.pcloudEnvName)-ingress-public"
+_issuerPrivate: "\(global.id)-private"
+_issuerPublic: "\(global.id)-public"
+
+images: {
+	for key, value in images {
+		"\(key)": #Image & value
+	}
+}
+
+charts: {
+	for key, value in charts {
+		"\(key)": #Chart & value
+	}
+}
+
+#ResourceReference: {
+    name: string
+    namespace: string
+}
+
+#Helm: {
+	name: string
+	dependsOn: [...#Helm] | *[]
+    dependsOnExternal: [...#ResourceReference] | *[]
+	...
+}
+
+helm: {
+	for key, value in helm {
+		"\(key)": #Helm & value & {
+			name: key
+		}
+	}
+}
+
+#HelmRelease: {
+	_name: string
+	_chart: #Chart
+	_values: _
+	_dependencies: [...#Helm] | *[]
+	_externalDependencies: [...#ResourceReference] | *[]
+
+	apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
+	kind: "HelmRelease"
+	metadata: {
+		name: _name
+   		namespace: release.namespace
+	}
+	spec: {
+		interval: "1m0s"
+		dependsOn: list.Concat([_externalDependencies, [
+			for d in _dependencies {
+				name: d.name
+				namespace: release.namespace
+			}
+    	]])
+		chart: {
+			spec: _chart
+		}
+		values: _values
+	}
+}
+
+output: {
+	for name, r in helm {
+		"\(name)": #HelmRelease & {
+			_name: name
+			_chart: r.chart
+			_values: r.values
+			_dependencies: r.dependsOn
+            _externalDependencies: r.dependsOnExternal
+		}
+	}
+}
+`
+
 type Named interface {
 	Nam() string
 }
@@ -55,6 +188,10 @@
 	Resources map[string][]byte
 }
 
+func cleanName(s string) string {
+	return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
+}
+
 func (a App) Render(derived Derived) (Rendered, error) {
 	ret := Rendered{
 		Resources: make(map[string][]byte),
@@ -84,7 +221,7 @@
 			return Rendered{}, err
 		}
 		for i.Next() {
-			name := i.Selector().String()
+			name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
 			contents, err := cueyaml.Encode(i.Value())
 			if err != nil {
 				return Rendered{}, err
@@ -168,12 +305,12 @@
 		CreateAppManager(valuesTmpls, tmpls),
 		CreateIngressPublic(valuesTmpls, tmpls),
 		CreateCertManager(valuesTmpls, tmpls),
-		CreateCertManagerWebhookGandi(valuesTmpls, tmpls),
 		CreateCSIDriverSMB(valuesTmpls, tmpls),
 		CreateResourceRendererController(valuesTmpls, tmpls),
 		CreateHeadscaleController(valuesTmpls, tmpls),
 		CreateDNSZoneManager(valuesTmpls, tmpls),
 		CreateFluxcdReconciler(valuesTmpls, tmpls),
+		CreateAppConfigRepo(valuesTmpls, tmpls),
 	}
 	for _, a := range CreateStoreApps() {
 		ret = append(ret, a.App)
@@ -213,7 +350,7 @@
 
 // TODO(gio): service account needs permission to create/update secret
 func CreateAppIngressPrivate(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/private-network.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/private-network.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -226,46 +363,46 @@
 		},
 		schema,
 		tmpls.Lookup("private-network.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateCertificateIssuerPrivate(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/certificate-issuer-private.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/certificate-issuer-private.cue")
 	if err != nil {
 		panic(err)
 	}
 	return App{
 		"certificate-issuer-private",
-		[]string{},
+		[]string{"ingress-private"},
 		[]*template.Template{
 			tmpls.Lookup("certificate-issuer-private.yaml"),
 		},
 		schema,
 		tmpls.Lookup("certificate-issuer-private.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateCertificateIssuerPublic(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/certificate-issuer-public.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/certificate-issuer-public.cue")
 	if err != nil {
 		panic(err)
 	}
 	return App{
 		"certificate-issuer-public",
-		[]string{},
+		[]string{"ingress-private"},
 		[]*template.Template{
 			tmpls.Lookup("certificate-issuer-public.yaml"),
 		},
 		schema,
 		tmpls.Lookup("certificate-issuer-public.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateAppCoreAuth(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/core-auth.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/core-auth.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -278,12 +415,12 @@
 		},
 		schema,
 		tmpls.Lookup("core-auth.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateAppVaultwarden(fs embed.FS, tmpls *template.Template) StoreApp {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/vaultwarden.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/vaultwarden.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -296,15 +433,15 @@
 			},
 			schema,
 			tmpls.Lookup("vaultwarden.md"),
-			nil,
+			cfg,
 		},
 		Icon:             `<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M35.38 25.63V9.37H24v28.87a34.93 34.93 0 0 0 5.41-3.48q6-4.66 6-9.14Zm4.87-19.5v19.5A11.58 11.58 0 0 1 39.4 30a16.22 16.22 0 0 1-2.11 3.81a23.52 23.52 0 0 1-3 3.24a34.87 34.87 0 0 1-3.22 2.62c-1 .69-2 1.35-3.07 2s-1.82 1-2.27 1.26l-1.08.51a1.53 1.53 0 0 1-1.32 0l-1.08-.51c-.45-.22-1.21-.64-2.27-1.26s-2.09-1.27-3.07-2A34.87 34.87 0 0 1 13.7 37a23.52 23.52 0 0 1-3-3.24A16.22 16.22 0 0 1 8.6 30a11.58 11.58 0 0 1-.85-4.32V6.13A1.64 1.64 0 0 1 9.38 4.5h29.24a1.64 1.64 0 0 1 1.63 1.63Z"/></svg>`,
-		ShortDescription: "Open source implementation of Bitwarden password manager. Can be used with official client applications.",
+		ShortDescription: "Alternative implementation of the Bitwarden server API written in Rust and compatible with upstream Bitwarden clients, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.",
 	}
 }
 
 func CreateAppMatrix(fs embed.FS, tmpls *template.Template) StoreApp {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/matrix.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/matrix.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -318,7 +455,7 @@
 			},
 			schema,
 			nil,
-			nil,
+			cfg,
 		},
 		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 24 24"><path fill="currentColor" d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033a3.312 3.312 0 0 1 1.117-1.024c.433-.245.936-.365 1.5-.365c.54 0 1.033.107 1.481.314c.448.208.785.582 1.02 1.108c.254-.374.6-.706 1.034-.992c.434-.287.95-.43 1.546-.43c.453 0 .872.056 1.26.167c.388.11.716.286.993.53c.276.245.489.559.646.951c.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66a1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166c-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499a1.946 1.946 0 0 0-.231.696a5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688a1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19c-.111 0-.259.024-.439.074c-.18.051-.36.143-.53.282a1.637 1.637 0 0 0-.439.595c-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"/></svg>`,
 		"An open network for secure, decentralised communication",
@@ -326,7 +463,7 @@
 }
 
 func CreateAppPihole(fs embed.FS, tmpls *template.Template) StoreApp {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/pihole.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/pihole.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -339,7 +476,7 @@
 			},
 			schema,
 			tmpls.Lookup("pihole.md"),
-			nil,
+			cfg,
 		},
 		// "simple-icons:pihole",
 		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 24 24"><path fill="currentColor" d="M4.344 0c.238 4.792 3.256 7.056 6.252 7.376c.165-1.692-4.319-5.6-4.319-5.6c-.008-.011.009-.025.019-.014c0 0 4.648 4.01 5.423 5.645c2.762-.15 5.196-1.947 5-4.912c0 0-4.12-.613-5 4.618C11.48 2.753 8.993 0 4.344 0zM12 7.682v.002a3.68 3.68 0 0 0-2.591 1.077L4.94 13.227a3.683 3.683 0 0 0-.86 1.356a3.31 3.31 0 0 0-.237 1.255A3.681 3.681 0 0 0 4.92 18.45l4.464 4.466a3.69 3.69 0 0 0 2.251 1.06l.002.001c.093.01.187.015.28.017l-.1-.008c.06.003.117.009.177.009l-.077-.001L12 24l-.004-.005a3.68 3.68 0 0 0 2.61-1.077l4.469-4.465a3.683 3.683 0 0 0 1.006-1.888l.012-.063a3.682 3.682 0 0 0 .057-.541l.003-.061c0-.017.003-.05.004-.06h-.002a3.683 3.683 0 0 0-1.077-2.607l-4.466-4.468a3.694 3.694 0 0 0-1.564-.927l-.07-.02a3.43 3.43 0 0 0-.946-.133L12 7.682zm3.165 3.357c.023 1.748-1.33 3.078-1.33 4.806c.164 2.227 1.733 3.207 3.266 3.146c-.035.003-.068.007-.104.009c-1.847.135-3.209-1.326-5.002-1.326c-2.23.164-3.21 1.736-3.147 3.27l-.008-.104c-.133-1.847 1.328-3.21 1.328-5.002c-.173-2.32-1.867-3.284-3.46-3.132c.1-.011.203-.021.31-.027c1.847-.133 3.209 1.328 5.002 1.328c2.082-.155 3.074-1.536 3.145-2.968zM4.344 0c.238 4.792 3.256 7.056 6.252 7.376c.165-1.692-4.319-5.6-4.319-5.6c-.008-.011.009-.025.019-.014c0 0 4.648 4.01 5.423 5.645c2.762-.15 5.196-1.947 5-4.912c0 0-4.12-.613-5 4.618C11.48 2.753 8.993 0 4.344 0zM12 7.682v.002a3.68 3.68 0 0 0-2.591 1.077L4.94 13.227a3.683 3.683 0 0 0-.86 1.356a3.31 3.31 0 0 0-.237 1.255A3.681 3.681 0 0 0 4.92 18.45l4.464 4.466a3.69 3.69 0 0 0 2.251 1.06l.002.001c.093.01.187.015.28.017l-.1-.008c.06.003.117.009.177.009l-.077-.001L12 24l-.004-.005a3.68 3.68 0 0 0 2.61-1.077l4.469-4.465a3.683 3.683 0 0 0 1.006-1.888l.012-.063a3.682 3.682 0 0 0 .057-.541l.003-.061c0-.017.003-.05.004-.06h-.002a3.683 3.683 0 0 0-1.077-2.607l-4.466-4.468a3.694 3.694 0 0 0-1.564-.927l-.07-.02a3.43 3.43 0 0 0-.946-.133L12 7.682zm3.165 3.357c.023 1.748-1.33 3.078-1.33 4.806c.164 2.227 1.733 3.207 3.266 3.146c-.035.003-.068.007-.104.009c-1.847.135-3.209-1.326-5.002-1.326c-2.23.164-3.21 1.736-3.147 3.27l-.008-.104c-.133-1.847 1.328-3.21 1.328-5.002c-.173-2.32-1.867-3.284-3.46-3.132c.1-.011.203-.021.31-.027c1.847-.133 3.209 1.328 5.002 1.328c2.082-.155 3.074-1.536 3.145-2.968z"/></svg>`,
@@ -348,7 +485,7 @@
 }
 
 func CreateAppPenpot(fs embed.FS, tmpls *template.Template) StoreApp {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/penpot.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/penpot.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -361,7 +498,7 @@
 			},
 			schema,
 			tmpls.Lookup("penpot.md"),
-			nil,
+			cfg,
 		},
 		// "simple-icons:pihole",
 		`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M7.654 0L5.13 3.554v2.01L2.934 6.608l-.02-.009v13.109l8.563 4.045L12 24l.523-.247l8.563-4.045V6.6l-.017.008l-2.196-1.045V3.555l-.077-.108L16.349.001l-2.524 3.554v.004L11.989.973l-1.823 2.566l-.065-.091zm.447 2.065l.976 1.374H6.232l.964-1.358zm8.694 0l.976 1.374h-2.845l.965-1.358zm-4.36.971l.976 1.375h-2.845l.965-1.359zM5.962 4.132h1.35v4.544l-1.35-.638Zm2.042 0h1.343v5.506l-1.343-.635zm6.652 0h1.35V9l-1.35.637zm2.042 0h1.343v3.905l-1.343.634zm-6.402.972h1.35v5.62l-1.35-.638zm2.042 0h1.343v4.993l-1.343.634zm6.534 1.493l1.188.486l-1.188.561zM5.13 6.6v1.047l-1.187-.561ZM3.96 8.251l7.517 3.55v10.795l-7.516-3.55zm16.08 0v10.794l-7.517 3.55V11.802z"/></svg>`,
@@ -391,7 +528,7 @@
 }
 
 func CreateAppQBittorrent(fs embed.FS, tmpls *template.Template) StoreApp {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/qbittorrent.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/qbittorrent.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -404,7 +541,7 @@
 			},
 			schema,
 			tmpls.Lookup("qbittorrent.md"),
-			nil,
+			cfg,
 		},
 		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><circle cx="24" cy="24" r="21.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M26.651 22.364a5.034 5.034 0 0 1 5.035-5.035h0a5.034 5.034 0 0 1 5.034 5.035v3.272a5.034 5.034 0 0 1-5.034 5.035h0a5.034 5.034 0 0 1-5.035-5.035m0 5.035V10.533m-5.302 15.103a5.034 5.034 0 0 1-5.035 5.035h0a5.034 5.034 0 0 1-5.034-5.035v-3.272a5.034 5.034 0 0 1 5.034-5.035h0a5.034 5.034 0 0 1 5.035 5.035m0-5.035v20.138"/></svg>`,
 		"qBittorrent is a cross-platform free and open-source BitTorrent client written in native C++. It relies on Boost, Qt 6 toolkit and the libtorrent-rasterbar library, with an optional search engine written in Python.",
@@ -412,7 +549,7 @@
 }
 
 func CreateAppJellyfin(fs embed.FS, tmpls *template.Template) StoreApp {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/jellyfin.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/jellyfin.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -425,28 +562,40 @@
 			},
 			schema,
 			nil,
-			nil,
+			cfg,
 		},
 		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M24 20c-1.62 0-6.85 9.48-6.06 11.08s11.33 1.59 12.12 0S25.63 20 24 20Z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M24 5.5c-4.89 0-20.66 28.58-18.25 33.4s34.13 4.77 36.51 0S28.9 5.5 24 5.5Zm12 29.21c-1.56 3.13-22.35 3.17-23.93 0S20.8 12.83 24 12.83s13.52 18.76 12 21.88Z"/></svg>`,
 		"Jellyfin is a free and open-source media server and suite of multimedia applications designed to organize, manage, and share digital media files to networked devices.",
 	}
 }
 
-func CreateAppRpuppy(fs embed.FS, tmpls *template.Template) StoreApp {
-	contents, err := fs.ReadFile("values-tmpl/rpuppy.cue")
-	if err != nil {
-		panic(err)
-	}
+func processCueConfig(contents string) (*cue.Value, Schema, error) {
 	ctx := cuecontext.New()
-	cfg := ctx.CompileBytes(contents)
-	if cfg.Err() != nil {
-		panic(cfg.Err())
+	cfg := ctx.CompileString(cueBaseConfigImports + contents + cueBaseConfig)
+	if err := cfg.Err(); err != nil {
+		return nil, nil, err
 	}
 	if err := cfg.Validate(); err != nil {
-		panic(err)
+		return nil, nil, err
 	}
 	schema, err := NewCueSchema(cfg.LookupPath(cue.ParsePath("input")))
 	if err != nil {
+		return nil, nil, err
+	}
+	return &cfg, schema, nil
+}
+
+func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, Schema, error) {
+	contents, err := fs.ReadFile(f)
+	if err != nil {
+		return nil, nil, err
+	}
+	return processCueConfig(string(contents))
+}
+
+func CreateAppRpuppy(fs embed.FS, tmpls *template.Template) StoreApp {
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/rpuppy.cue")
+	if err != nil {
 		panic(err)
 	}
 	return StoreApp{
@@ -458,15 +607,30 @@
 			},
 			schema,
 			tmpls.Lookup("rpuppy.md"),
-			&cfg,
+			cfg,
 		},
 		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 256 256"><path fill="currentColor" d="M100 140a8 8 0 1 1-8-8a8 8 0 0 1 8 8Zm64 8a8 8 0 1 0-8-8a8 8 0 0 0 8 8Zm64.94-9.11a12.12 12.12 0 0 1-5 1.11a11.83 11.83 0 0 1-9.35-4.62l-2.59-3.29V184a36 36 0 0 1-36 36H80a36 36 0 0 1-36-36v-51.91l-2.53 3.27A11.88 11.88 0 0 1 32.1 140a12.08 12.08 0 0 1-5-1.11a11.82 11.82 0 0 1-6.84-13.14l16.42-88a12 12 0 0 1 14.7-9.43h.16L104.58 44h46.84l53.08-15.6h.16a12 12 0 0 1 14.7 9.43l16.42 88a11.81 11.81 0 0 1-6.84 13.06ZM97.25 50.18L49.34 36.1a4.18 4.18 0 0 0-.92-.1a4 4 0 0 0-3.92 3.26l-16.42 88a4 4 0 0 0 7.08 3.22ZM204 121.75L150 52h-44l-54 69.75V184a28 28 0 0 0 28 28h44v-18.34l-14.83-14.83a4 4 0 0 1 5.66-5.66L128 186.34l13.17-13.17a4 4 0 0 1 5.66 5.66L132 193.66V212h44a28 28 0 0 0 28-28Zm23.92 5.48l-16.42-88a4 4 0 0 0-4.84-3.16l-47.91 14.11l62.11 80.28a4 4 0 0 0 7.06-3.23Z"/></svg>`,
 		"Delights users with randomly generate puppy pictures. Can be configured to be reachable only from private network or publicly.",
 	}
 }
 
+func CreateAppConfigRepo(fs embed.FS, tmpls *template.Template) App {
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/config-repo.cue")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"config-repo",
+		[]string{"config-repo"},
+		[]*template.Template{},
+		schema,
+		nil,
+		cfg,
+	}
+}
+
 func CreateAppSoftServe(fs embed.FS, tmpls *template.Template) StoreApp {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/soft-serve.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/soft-serve.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -479,7 +643,7 @@
 			},
 			schema,
 			tmpls.Lookup("soft-serve.md"),
-			nil,
+			cfg,
 		},
 		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="4"><path stroke-linejoin="round" d="M15.34 22.5L21 37l3 6l3-6l5.66-14.5"/><path d="M19 32h10"/><path stroke-linejoin="round" d="M24 3c-6 0-8 6-8 6s-6 2-6 7s5 7 5 7s3.5-2 9-2s9 2 9 2s5-2 5-7s-6-7-6-7s-2-6-8-6Z"/></g></svg>`,
 		"A tasty, self-hostable Git server for the command line. 🍦",
@@ -487,7 +651,7 @@
 }
 
 func CreateAppHeadscale(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/headscale.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/headscale.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -499,12 +663,12 @@
 		},
 		schema,
 		tmpls.Lookup("headscale.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateAppHeadscaleUser(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/headscale-user.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/headscale-user.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -516,12 +680,12 @@
 		},
 		schema,
 		tmpls.Lookup("headscale-user.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateMetallbIPAddressPool(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/metallb-ipaddresspool.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/metallb-ipaddresspool.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -533,12 +697,12 @@
 		},
 		schema,
 		tmpls.Lookup("metallb-ipaddresspool.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateEnvManager(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/env-manager.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/env-manager.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -550,12 +714,12 @@
 		},
 		schema,
 		tmpls.Lookup("env-manager.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateWelcome(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/welcome.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/welcome.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -567,12 +731,12 @@
 		},
 		schema,
 		tmpls.Lookup("welcome.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateAppManager(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/appmanager.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/appmanager.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -584,12 +748,12 @@
 		},
 		schema,
 		tmpls.Lookup("appmanager.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateIngressPublic(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/ingress-public.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/ingress-public.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -601,12 +765,12 @@
 		},
 		schema,
 		tmpls.Lookup("ingress-public.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateCertManager(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/cert-manager.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/cert-manager.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -618,29 +782,12 @@
 		},
 		schema,
 		tmpls.Lookup("cert-manager.md"),
-		nil,
-	}
-}
-
-func CreateCertManagerWebhookGandi(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/cert-manager-webhook-pcloud.jsonschema")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"cert-manager-webhook-pcloud",
-		[]string{},
-		[]*template.Template{
-			tmpls.Lookup("cert-manager-webhook-pcloud.yaml"),
-		},
-		schema,
-		tmpls.Lookup("cert-manager-webhook-pcloud.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateCSIDriverSMB(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/csi-driver-smb.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/csi-driver-smb.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -652,12 +799,12 @@
 		},
 		schema,
 		tmpls.Lookup("csi-driver-smb.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateResourceRendererController(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/resource-renderer-controller.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/resource-renderer-controller.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -669,12 +816,12 @@
 		},
 		schema,
 		tmpls.Lookup("resource-renderer-controller.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateHeadscaleController(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/headscale-controller.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/headscale-controller.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -686,12 +833,12 @@
 		},
 		schema,
 		tmpls.Lookup("headscale-controller.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateDNSZoneManager(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/dns-zone-controller.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/dns-zone-manager.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -705,12 +852,12 @@
 		},
 		schema,
 		tmpls.Lookup("dns-zone-controller.md"),
-		nil,
+		cfg,
 	}
 }
 
 func CreateFluxcdReconciler(fs embed.FS, tmpls *template.Template) App {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/fluxcd-reconciler.jsonschema")
+	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/fluxcd-reconciler.cue")
 	if err != nil {
 		panic(err)
 	}
@@ -722,7 +869,7 @@
 		},
 		schema,
 		tmpls.Lookup("fluxcd-reconciler.md"),
-		nil,
+		cfg,
 	}
 }
 
diff --git a/core/installer/bootstrapper.go b/core/installer/bootstrapper.go
index 8da05a2..d07a007 100644
--- a/core/installer/bootstrapper.go
+++ b/core/installer/bootstrapper.go
@@ -399,13 +399,13 @@
 			Global: Values{
 				PCloudEnvName: env.Name,
 			},
+			Release: Release{},
+			Values:  make(map[string]any),
 		}
 		if len(namespaces) > 0 {
 			derived.Release.Namespace = namespaces[0]
 		}
-		values := map[string]any{
-			"IngressPublicIP": env.ServiceIPs.IngressPublic.String(),
-		}
+		values := map[string]any{}
 		return repo.InstallApp(*app, filepath.Join("/infrastructure", app.Name), values, derived)
 	}
 	appsToInstall := []string{
@@ -414,7 +414,6 @@
 		"csi-driver-smb",
 		"ingress-public",
 		"cert-manager",
-		"cert-manager-webhook-pcloud",
 	}
 	for _, name := range appsToInstall {
 		if err := install(name); err != nil {
@@ -453,7 +452,7 @@
   interval: 1m0s
   url: https://github.com/giolekva/pcloud
   ref:
-    branch: main
+    branch: cuelang
 `, env.Name)))
 		if err != nil {
 			return err
@@ -507,10 +506,10 @@
 			PCloudEnvName: env.Name,
 		},
 		Values: map[string]any{
-			"RepoIP":        env.ServiceIPs.ConfigRepo,
-			"RepoPort":      22,
-			"RepoName":      "config",
-			"SSHPrivateKey": string(keys.RawPrivateKey()),
+			"repoIP":        env.ServiceIPs.ConfigRepo,
+			"repoPort":      22,
+			"repoName":      "config",
+			"sshPrivateKey": string(keys.RawPrivateKey()),
 		},
 	}
 	if len(namespaces) > 0 {
@@ -542,12 +541,12 @@
 				PCloudEnvName: env.Name,
 			},
 			Values: map[string]any{
-				"Volume": map[string]any{
-					"ClaimName": volumeClaimName,
-					"MountPath": volumeMountPath,
-					"Size":      "1Gi",
+				"volume": map[string]any{
+					"claimName": volumeClaimName,
+					"mountPath": volumeMountPath,
+					"size":      "1Gi",
 				},
-				"APIConfigMapName": dnsAPIConfigMapName,
+				"apiConfigMapName": dnsAPIConfigMapName,
 			},
 			Release: Release{
 				Namespace: ns,
diff --git a/core/installer/cmd/bootstrap.go b/core/installer/cmd/bootstrap.go
index 0aa5230..6e0b247 100644
--- a/core/installer/cmd/bootstrap.go
+++ b/core/installer/cmd/bootstrap.go
@@ -79,6 +79,7 @@
 }
 
 func bootstrapCmdRun(cmd *cobra.Command, args []string) error {
+	// TODO(gio): remove installer.CreateAllApps()
 	adminPubKey, err := os.ReadFile(bootstrapFlags.adminPubKey)
 	if err != nil {
 		return err
diff --git a/core/installer/schema.go b/core/installer/schema.go
index 69e7ec0..249190b 100644
--- a/core/installer/schema.go
+++ b/core/installer/schema.go
@@ -16,6 +16,7 @@
 	KindString       = 1
 	KindStruct       = 2
 	KindNetwork      = 3
+	KindNumber       = 4
 )
 
 type Schema interface {
@@ -27,24 +28,26 @@
 #Network: {
     name: string
 	ingressClass: string
-	certificateIssuer: string
+	certificateIssuer: string | *""
 	domain: string
 }
 
-value: %s
-
-valid: #Network & value
+value: { %s }
 `
 
 func isNetwork(v cue.Value) bool {
 	if v.Value().Kind() != cue.StructKind {
 		return false
 	}
-	value := fmt.Sprintf("%#v", v)
-	s := fmt.Sprintf(networkSchema, value)
+	s := fmt.Sprintf(networkSchema, fmt.Sprintf("%#v", v))
 	c := cuecontext.New()
 	u := c.CompileString(s)
-	return u.Err() == nil && u.Validate() == nil
+	network := u.LookupPath(cue.ParsePath("#Network"))
+	vv := u.LookupPath(cue.ParsePath("value"))
+	if err := network.Subsume(vv); err == nil {
+		return true
+	}
+	return false
 }
 
 type basicSchema struct {
@@ -77,6 +80,8 @@
 		return basicSchema{KindString}, nil
 	case cue.BoolKind:
 		return basicSchema{KindBoolean}, nil
+	case cue.NumberKind:
+		return basicSchema{KindNumber}, nil
 	case cue.StructKind:
 		if isNetwork(v) {
 			return basicSchema{KindNetwork}, nil
diff --git a/core/installer/tasks/activate.go b/core/installer/tasks/activate.go
index c6506aa..0980dee 100644
--- a/core/installer/tasks/activate.go
+++ b/core/installer/tasks/activate.go
@@ -47,7 +47,7 @@
 		for _, key := range ssPublicKeys {
 			fmt.Fprintf(&knownHosts, "%s %s\n", repoHost, key)
 		}
-		for _, tmpl := range tmpls.Templates() {
+		for _, tmpl := range tmpls.Templates() { // TODO(gio): migrate to cue
 			dstPath := path.Join("environments", env.Name, tmpl.Name())
 			dst, err := st.repo.Writer(dstPath)
 			if err != nil {
diff --git a/core/installer/tasks/infra.go b/core/installer/tasks/infra.go
index 995744c..d7ff235 100644
--- a/core/installer/tasks/infra.go
+++ b/core/installer/tasks/infra.go
@@ -81,7 +81,7 @@
   interval: 1m0s
   url: https://github.com/giolekva/pcloud
   ref:
-    branch: main
+    branch: cuelang
 `, env.Name)
 			if err != nil {
 				return err
@@ -114,29 +114,29 @@
 				return err
 			}
 			if err := st.appManager.Install(*app, st.nsGen, installer.NewSuffixGenerator("-ingress-private"), map[string]any{
-				"Name":       fmt.Sprintf("%s-ingress-private", env.Name),
-				"From":       ingressPrivateIP.String(),
-				"To":         ingressPrivateIP.String(),
-				"AutoAssign": false,
-				"Namespace":  "metallb-system",
+				"name":       fmt.Sprintf("%s-ingress-private", env.Name),
+				"from":       ingressPrivateIP.String(),
+				"to":         ingressPrivateIP.String(),
+				"autoAssign": false,
+				"namespace":  "metallb-system",
 			}); err != nil {
 				return err
 			}
 			if err := st.appManager.Install(*app, st.nsGen, installer.NewSuffixGenerator("-headscale"), map[string]any{
-				"Name":       fmt.Sprintf("%s-headscale", env.Name),
-				"From":       headscaleIP.String(),
-				"To":         headscaleIP.String(),
-				"AutoAssign": false,
-				"Namespace":  "metallb-system",
+				"name":       fmt.Sprintf("%s-headscale", env.Name),
+				"from":       headscaleIP.String(),
+				"to":         headscaleIP.String(),
+				"autoAssign": false,
+				"namespace":  "metallb-system",
 			}); err != nil {
 				return err
 			}
 			if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{
-				"Name":       env.Name,
-				"From":       "10.1.0.100", // TODO(gio): auto-generate
-				"To":         "10.1.0.254",
-				"AutoAssign": false,
-				"Namespace":  "metallb-system",
+				"name":       env.Name,
+				"from":       "10.1.0.100", // TODO(gio): auto-generate
+				"to":         "10.1.0.254",
+				"autoAssign": false,
+				"namespace":  "metallb-system",
 			}); err != nil {
 				return err
 			}
@@ -147,10 +147,10 @@
 				return err
 			}
 			if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{
-				"PrivateNetwork": map[string]any{
-					"Hostname": "private-network-proxy",
-					"Username": "private-network-proxy",
-					"IPSubnet": "10.1.0.0/24",
+				"privateNetwork": map[string]any{
+					"hostname": "private-network-proxy",
+					"username": "private-network-proxy",
+					"ipSubnet": "10.1.0.0/24",
 				},
 			}); err != nil {
 				return err
@@ -178,9 +178,9 @@
 			return err
 		}
 		if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{
-			"APIConfigMap": map[string]any{
-				"Name":      "api-config", // TODO(gio): take from global pcloud config
-				"Namespace": fmt.Sprintf("%s-dns-zone-manager", env.PCloudEnvName),
+			"apiConfigMap": map[string]any{
+				"name":      "api-config", // TODO(gio): take from global pcloud config
+				"namespace": fmt.Sprintf("%s-dns-zone-manager", env.PCloudEnvName),
 			},
 		}); err != nil {
 			return err
@@ -197,7 +197,7 @@
 			return err
 		}
 		if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{
-			"Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
+			"subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
 		}); err != nil {
 			return err
 		}
@@ -217,7 +217,7 @@
 			return err
 		}
 		if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{
-			"Subdomain": "headscale",
+			"subdomain": "headscale",
 		}); err != nil {
 			return err
 		}
@@ -248,8 +248,8 @@
 			return err
 		}
 		if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{
-			"RepoAddr":      st.ssClient.GetRepoAddress("config"),
-			"SSHPrivateKey": string(keys.RawPrivateKey()),
+			"repoAddr":      st.ssClient.GetRepoAddress("config"),
+			"sshPrivateKey": string(keys.RawPrivateKey()),
 		}); err != nil {
 			return err
 		}
@@ -280,8 +280,8 @@
 			return err
 		}
 		if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{
-			"RepoAddr":      st.ssClient.GetRepoAddress("config"),
-			"SSHPrivateKey": string(keys.RawPrivateKey()),
+			"repoAddr":      st.ssClient.GetRepoAddress("config"),
+			"sshPrivateKey": string(keys.RawPrivateKey()),
 		}); err != nil {
 			return err
 		}
diff --git a/core/installer/tasks/init.go b/core/installer/tasks/init.go
index f3818e3..d41dde7 100644
--- a/core/installer/tasks/init.go
+++ b/core/installer/tasks/init.go
@@ -21,7 +21,7 @@
 func NewCreateConfigRepoTask(env Env, st *state) Task {
 	t := newLeafTask("Install Git server", func() error {
 		appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
-		ssApp, err := appsRepo.Find("soft-serve")
+		ssApp, err := appsRepo.Find("config-repo")
 		if err != nil {
 			return err
 		}
@@ -35,14 +35,9 @@
 			return err
 		}
 		ssValues := map[string]any{
-			"ChartRepositoryNamespace": env.PCloudEnvName,
-			"ServiceType":              "ClusterIP",
-			"PrivateKey":               string(ssKeys.RawPrivateKey()),
-			"PublicKey":                string(ssKeys.RawAuthorizedKey()),
-			"AdminKey":                 string(ssAdminKeys.RawAuthorizedKey()),
-			"Ingress": map[string]any{
-				"Enabled": false,
-			},
+			"privateKey": string(ssKeys.RawPrivateKey()),
+			"publicKey":  string(ssKeys.RawAuthorizedKey()),
+			"adminKey":   string(ssAdminKeys.RawAuthorizedKey()),
 		}
 		derived := installer.Derived{
 			Global: installer.Values{
diff --git a/core/installer/values-tmpl/appmanager.cue b/core/installer/values-tmpl/appmanager.cue
new file mode 100644
index 0000000..dbf60e7
--- /dev/null
+++ b/core/installer/values-tmpl/appmanager.cue
@@ -0,0 +1,49 @@
+import (
+	"encoding/base64"
+)
+
+input: {
+	repoAddr: string
+	sshPrivateKey: string
+}
+
+images: {
+	appmanager: {
+		repository: "giolekva"
+		name: "pcloud-installer"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+}
+
+charts: {
+	appmanager: {
+		chart: "charts/appmanager"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	appmanager: {
+		chart: charts.appmanager
+		values: {
+			repoAddr: input.repoAddr
+			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+			ingress: {
+				className: _ingressPrivate
+				domain: "apps.\(global.privateDomain)"
+				certificateIssuer: ""
+			}
+			clusterRoleName: "\(global.id)-appmanager"
+			image: {
+				repository: images.appmanager.fullName
+				tag: images.appmanager.tag
+				pullPolicy: images.appmanager.pullPolicy
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/appmanager.jsonschema b/core/installer/values-tmpl/appmanager.jsonschema
deleted file mode 100644
index 8a011ca..0000000
--- a/core/installer/values-tmpl/appmanager.jsonschema
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "RepoAddr": { "type": "string", "default": "ssh://192.168.0.11/example" },
-	"SSHPrivateKey": { "type": "string", "default": "foo bar" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/appmanager.md b/core/installer/values-tmpl/appmanager.md
deleted file mode 100644
index 8fdc4ea..0000000
--- a/core/installer/values-tmpl/appmanager.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs PCloud App Manager
diff --git a/core/installer/values-tmpl/appmanager.yaml b/core/installer/values-tmpl/appmanager.yaml
deleted file mode 100644
index c630a76..0000000
--- a/core/installer/values-tmpl/appmanager.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: appmanager
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/appmanager
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    repoAddr: {{ .Values.RepoAddr }}
-    sshPrivateKey: {{ .Values.SSHPrivateKey | b64enc }}
-    ingress:
-      className: {{ .Global.Id }}-ingress-private
-      domain: apps.{{ .Global.PrivateDomain }}
-      certificateIssuer: ""
-    clusterRoleName: {{ .Global.Id }}-appmanager
diff --git a/core/installer/values-tmpl/cert-manager-webhook-gandi-role.jsonschema b/core/installer/values-tmpl/cert-manager-webhook-gandi-role.jsonschema
deleted file mode 100644
index f42d895..0000000
--- a/core/installer/values-tmpl/cert-manager-webhook-gandi-role.jsonschema
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/cert-manager-webhook-gandi-role.md b/core/installer/values-tmpl/cert-manager-webhook-gandi-role.md
deleted file mode 100644
index e6f01a3..0000000
--- a/core/installer/values-tmpl/cert-manager-webhook-gandi-role.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs rbacs to let cert-manager create gandi resource
diff --git a/core/installer/values-tmpl/cert-manager-webhook-gandi-role.yaml b/core/installer/values-tmpl/cert-manager-webhook-gandi-role.yaml
deleted file mode 100644
index ef0b383..0000000
--- a/core/installer/values-tmpl/cert-manager-webhook-gandi-role.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: cert-manager-webhook-gandi-role
-  namespace: {{ .Global.PCloudEnvName }}-cert-manager
-spec:
-  dependsOn:
-    - name: cert-manager
-      namespace: {{ .Global.PCloudEnvName }}-cert-manager
-  chart:
-    spec:
-      chart: charts/cert-manager-webhook-gandi-role
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  timeout: 20m0s
-  values:
-    certManager:
-      namespace: {{ .Global.PCloudEnvName }}-cert-manager
-      name: {{ .Global.PCloudEnvName }}-cert-manager
diff --git a/core/installer/values-tmpl/cert-manager-webhook-pcloud.jsonschema b/core/installer/values-tmpl/cert-manager-webhook-pcloud.jsonschema
deleted file mode 100644
index a6adce7..0000000
--- a/core/installer/values-tmpl/cert-manager-webhook-pcloud.jsonschema
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-	"APIGroupName": { "type": "string" },
-	"ResolverName": { "type": "string" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/cert-manager-webhook-pcloud.md b/core/installer/values-tmpl/cert-manager-webhook-pcloud.md
deleted file mode 100644
index 39df923..0000000
--- a/core/installer/values-tmpl/cert-manager-webhook-pcloud.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs cert-manager DNS01 resolver for pcloud internal DNS server
diff --git a/core/installer/values-tmpl/cert-manager-webhook-pcloud.yaml b/core/installer/values-tmpl/cert-manager-webhook-pcloud.yaml
deleted file mode 100644
index d75634b..0000000
--- a/core/installer/values-tmpl/cert-manager-webhook-pcloud.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: cert-manager-webhook-pcloud
-  namespace: {{ .Global.PCloudEnvName }}-cert-manager
-spec:
-  dependsOn:
-    - name: cert-manager
-      namespace: {{ .Global.PCloudEnvName }}-cert-manager # TODO(giolekva): derivative
-  chart:
-    spec:
-      chart: charts/cert-manager-webhook-pcloud
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  timeout: 20m0s
-  values:
-    fullnameOverride: {{ .Global.PCloudEnvName }}-cert-manager-webhook-pcloud
-    certManager:
-      namespace: {{ .Global.PCloudEnvName }}-cert-manager
-      name: {{ .Global.PCloudEnvName }}-cert-manager
-    image:
-      repository: giolekva/dns-challenge-solver
-      tag: latest
-      pullPolicy: Always
-    logLevel: 2
-    apiGroupName: dodo.cloud
-    resolverName: dns-resolver-pcloud
diff --git a/core/installer/values-tmpl/cert-manager.cue b/core/installer/values-tmpl/cert-manager.cue
new file mode 100644
index 0000000..dbecd50
--- /dev/null
+++ b/core/installer/values-tmpl/cert-manager.cue
@@ -0,0 +1,104 @@
+input: {}
+
+images: {
+	certManager: {
+		registry: "quay.io"
+		repository: "jetstack"
+		name: "cert-manager-controller"
+		tag: "v1.12.2"
+		pullPolicy: "IfNotPresent"
+	}
+	cainjector: {
+		registry: "quay.io"
+		repository: "jetstack"
+		name: "cert-manager-cainjector"
+		tag: "v1.12.2"
+		pullPolicy: "IfNotPresent"
+	}
+	webhook: {
+		registry: "quay.io"
+		repository: "jetstack"
+		name: "cert-manager-webhook"
+		tag: "v1.12.2"
+		pullPolicy: "IfNotPresent"
+	}
+	dnsChallengeSolver: {
+		repository: "giolekva"
+		name: "dns-challenge-solver"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+}
+
+charts: {
+	certManager: {
+		chart: "charts/cert-manager"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+	dnsChallengeSolver: {
+		chart: "charts/cert-manager-webhook-pcloud"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+}
+
+helm: {
+	"cert-manager": {
+		chart: charts.certManager
+		dependsOnExternal: [{
+			name: "ingress-public"
+			namespace: _ingressPublic
+		}]
+		values: {
+			fullnameOverride: "\(global.pcloudEnvName)-cert-manager"
+			installCRDs: true
+			dns01RecursiveNameserversOnly: true
+			dns01RecursiveNameservers: "1.1.1.1:53,8.8.8.8:53"
+			image: {
+				repository: images.certManager.fullName
+				tag: images.certManager.tag
+				pullPolicy: images.certManager.pullPolicy
+			}
+			cainjector: {
+				image: {
+					repository: images.cainjector.fullName
+					tag: images.cainjector.tag
+					pullPolicy: images.cainjector.pullPolicy
+				}
+			}
+			webhook: {
+				image: {
+					repository: images.webhook.fullName
+					tag: images.webhook.tag
+					pullPolicy: images.webhook.pullPolicy
+				}
+			}
+		}
+	}
+	"cert-manager-webhook-pcloud": {
+		chart: charts.dnsChallengeSolver
+		dependsOn: [helm["cert-manager"]]
+		values: {
+			fullnameOverride: "\(global.pcloudEnvName)-cert-manager-webhook-pcloud"
+			certManager: {
+				name: "\(global.pcloudEnvName)-cert-manager"
+				namespace: "\(global.pcloudEnvName)-cert-manager"
+			}
+			image: {
+				repository: images.dnsChallengeSolver.fullName
+				tag: images.dnsChallengeSolver.tag
+				pullPolicy: images.dnsChallengeSolver.pullPolicy
+			}
+			logLevel: 2
+			apiGroupName: "dodo.cloud"
+			resolverName: "dns-resolver-pcloud"
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/cert-manager.jsonschema b/core/installer/values-tmpl/cert-manager.jsonschema
deleted file mode 100644
index f42d895..0000000
--- a/core/installer/values-tmpl/cert-manager.jsonschema
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/cert-manager.md b/core/installer/values-tmpl/cert-manager.md
deleted file mode 100644
index aba785a..0000000
--- a/core/installer/values-tmpl/cert-manager.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs cert-manager
diff --git a/core/installer/values-tmpl/cert-manager.yaml b/core/installer/values-tmpl/cert-manager.yaml
deleted file mode 100644
index 9f30311..0000000
--- a/core/installer/values-tmpl/cert-manager.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: cert-manager
-  namespace: {{ .Release.Namespace }}
-spec:
-  dependsOn:
-    - name: ingress-public
-      namespace: {{ .Global.PCloudEnvName }}-ingress-public # TODO(giolekva): derivative
-  chart:
-    spec:
-      chart: charts/cert-manager
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  timeout: 20m0s
-  values:
-    fullnameOverride: {{ .Global.PCloudEnvName }}-cert-manager
-    installCRDs: true
-    image:
-      tag: v1.11.1
-      pullPolicy: IfNotPresent
-    dns01RecursiveNameserversOnly: true
-    dns01RecursiveNameservers: "1.1.1.1:53,8.8.8.8:53"
diff --git a/core/installer/values-tmpl/certificate-issuer-private.cue b/core/installer/values-tmpl/certificate-issuer-private.cue
new file mode 100644
index 0000000..5c67a84
--- /dev/null
+++ b/core/installer/values-tmpl/certificate-issuer-private.cue
@@ -0,0 +1,42 @@
+input: {
+	apiConfigMap: {
+		name: string
+		namespace: string
+	}
+}
+
+images: {}
+
+charts: {
+	"certificate-issuer-private": {
+		chart: "charts/certificate-issuer-private"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	"certificate-issuer-private": {
+		chart: charts["certificate-issuer-private"]
+		dependsOnExternal: [{
+			name: "ingress-nginx"
+			namespace: "\(global.namespacePrefix)ingress-private"
+		}]
+		values: {
+			issuer: {
+				name: _issuerPrivate
+				server: "https://acme-v02.api.letsencrypt.org/directory"
+				// server: "https://acme-staging-v02.api.letsencrypt.org/directory"
+				domain: global.privateDomain
+				contactEmail: global.contactEmail
+			}
+			apiConfigMap: {
+				name: input.apiConfigMap.name
+				namespace: input.apiConfigMap.namespace
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/certificate-issuer-private.jsonschema b/core/installer/values-tmpl/certificate-issuer-private.jsonschema
deleted file mode 100644
index 27f907e..0000000
--- a/core/installer/values-tmpl/certificate-issuer-private.jsonschema
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "APIConfigMap": {
-	  "type": "object",
-	  "properties": {
-		"Name": { "type": "string" },
-		"Namespace": { "type": "string" }
-	  },
-	  "additionalProperties": false
-	}
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/certificate-issuer-private.md b/core/installer/values-tmpl/certificate-issuer-private.md
deleted file mode 100644
index 9ee84cc..0000000
--- a/core/installer/values-tmpl/certificate-issuer-private.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs certificate issuer for private domain
diff --git a/core/installer/values-tmpl/certificate-issuer-private.yaml b/core/installer/values-tmpl/certificate-issuer-private.yaml
deleted file mode 100644
index 8654be5..0000000
--- a/core/installer/values-tmpl/certificate-issuer-private.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: certificate-issuer-private
-  namespace: {{ .Global.Id }}-ingress-private
-spec:
-  dependsOn:
-  - name: ingress-private
-    namespace: {{ .Global.Id }}-ingress-private
-  chart:
-    spec:
-      chart: charts/certificate-issuer-private
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    issuer:
-      name: {{ .Global.Id }}-private
-      server: https://acme-v02.api.letsencrypt.org/directory
-      # server: https://acme-staging-v02.api.letsencrypt.org/directory
-      domain: {{ .Global.PrivateDomain }}
-      contactEmail: {{ .Global.ContactEmail }}
-      gandiAPIToken: {{ .Values.GandiAPIToken }}
-    apiConfigMap:
-      name: {{ .Values.APIConfigMap.Name }}
-      namespace: {{ .Values.APIConfigMap.Namespace }}
diff --git a/core/installer/values-tmpl/certificate-issuer-public.cue b/core/installer/values-tmpl/certificate-issuer-public.cue
new file mode 100644
index 0000000..8ad81f9
--- /dev/null
+++ b/core/installer/values-tmpl/certificate-issuer-public.cue
@@ -0,0 +1,34 @@
+input: {}
+
+images: {}
+
+charts: {
+	"certificate-issuer-public": {
+		chart: "charts/certificate-issuer-public"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	"certificate-issuer-public": {
+		chart: charts["certificate-issuer-public"]
+		dependsOnExternal: [{
+			name: "ingress-nginx"
+			namespace: "\(global.namespacePrefix)ingress-private"
+		}]
+		values: {
+			issuer: {
+				name: _issuerPublic
+				server: "https://acme-v02.api.letsencrypt.org/directory"
+				// server: "https://acme-staging-v02.api.letsencrypt.org/directory"
+				domain: global.domain
+				contactEmail: global.contactEmail
+				ingressClass: _ingressPublic
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/certificate-issuer-public.jsonschema b/core/installer/values-tmpl/certificate-issuer-public.jsonschema
deleted file mode 100644
index f42d895..0000000
--- a/core/installer/values-tmpl/certificate-issuer-public.jsonschema
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/certificate-issuer-public.md b/core/installer/values-tmpl/certificate-issuer-public.md
deleted file mode 100644
index 961c3c3..0000000
--- a/core/installer/values-tmpl/certificate-issuer-public.md
+++ /dev/null
@@ -1 +0,0 @@
-Install HTTP01 based certificate issuer for public domain
diff --git a/core/installer/values-tmpl/certificate-issuer-public.yaml b/core/installer/values-tmpl/certificate-issuer-public.yaml
deleted file mode 100644
index bcf0079..0000000
--- a/core/installer/values-tmpl/certificate-issuer-public.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: certificate-issuer-public
-  # TODO(giolekva): is there better namespace for this?
-  namespace: {{ .Global.Id }}-ingress-private
-spec:
-  dependsOn:
-  - name: ingress-private
-    namespace: {{ .Global.Id }}-ingress-private
-  chart:
-    spec:
-      chart: charts/certificate-issuer-public
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    issuer:
-      name: {{ .Global.Id }}-public
-      server: https://acme-v02.api.letsencrypt.org/directory
-      # server: https://acme-staging-v02.api.letsencrypt.org/directory
-      domain: {{ .Global.Domain }}
-      contactEmail: {{ .Global.ContactEmail }}
-      ingressClass: {{ .Global.PCloudEnvName }}-ingress-public
diff --git a/core/installer/values-tmpl/config-repo.cue b/core/installer/values-tmpl/config-repo.cue
new file mode 100644
index 0000000..3f0b432
--- /dev/null
+++ b/core/installer/values-tmpl/config-repo.cue
@@ -0,0 +1,47 @@
+input: {
+	privateKey: string
+	publicKey: string
+	adminKey: string
+}
+
+images: {
+	softserve: {
+		repository: "charmcli"
+		name: "soft-serve"
+		tag: "v0.7.1"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	softserve: {
+		chart: "charts/soft-serve"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+}
+
+helm: {
+	softserve: {
+		chart: charts.softserve
+		values: {
+			serviceType: "ClusterIP"
+			addressPool: ""
+			reservedIP: ""
+			adminKey: input.adminKey
+			privateKey: input.privateKey
+			publicKey: input.publicKey
+			ingress: {
+				enabled: false
+			}
+			image: {
+				repository: images.softserve.fullName
+				tag: images.softserve.tag
+				pullPolicy: images.softserve.pullPolicy
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/core-auth-storage.yaml b/core/installer/values-tmpl/core-auth-storage.yaml
deleted file mode 100644
index 8ae3b71..0000000
--- a/core/installer/values-tmpl/core-auth-storage.yaml
+++ /dev/null
@@ -1,41 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: core-auth-storage
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/postgresql
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    fullnameOverride: postgres
-    image:
-      repository: library/postgres  # arm64v8/postgres
-      tag: 15.3
-    service:
-      type: ClusterIP
-      port: 5432
-    primary:
-      initdb:
-        scripts:
-          init.sql: |
-            CREATE USER kratos WITH PASSWORD 'kratos';
-            CREATE USER hydra WITH PASSWORD 'hydra';
-            CREATE DATABASE kratos WITH OWNER = kratos;
-            CREATE DATABASE hydra WITH OWNER = hydra;
-      persistence:
-        size: 1Gi
-      securityContext:
-        enabled: true
-        fsGroup: 0
-      containerSecurityContext:
-        enabled: true
-        runAsUser: 0
-    volumePermissions:
-      securityContext:
-        runAsUser: 0
diff --git a/core/installer/values-tmpl/core-auth.cue b/core/installer/values-tmpl/core-auth.cue
new file mode 100644
index 0000000..38e6a47
--- /dev/null
+++ b/core/installer/values-tmpl/core-auth.cue
@@ -0,0 +1,480 @@
+input: {
+	subdomain: string
+}
+
+userSchema: ###"""
+{
+  "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "User",
+  "type": "object",
+  "properties": {
+	"traits": {
+	  "type": "object",
+	  "properties": {
+		"username": {
+		  "type": "string",
+		  "format": "username",
+		  "title": "Username",
+		  "minLength": 3,
+		  "ory.sh/kratos": {
+			"credentials": {
+			  "password": {
+				"identifier": true
+			  }
+			}
+		  }
+		}
+	  },
+	  "additionalProperties": false
+	}
+  }
+}
+"""###
+
+images: {
+	kratos: {
+		repository: "oryd"
+		name: "kratos"
+		tag: "v0.13.0"
+		pullPolicy: "IfNotPresent"
+	}
+	hydra: {
+		repository: "oryd"
+		name: "hydra"
+		tag: "v2.1.2"
+		pullPolicy: "IfNotPresent"
+	}
+	"hydra-maester": {
+		repository: "giolekva"
+		name: "ory-hydra-maester"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+	ui: {
+		repository: "giolekva"
+		name: "auth-ui"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+	postgres: {
+		repository: "library"
+		name: "postgres"
+		tag: "15.3"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	auth: {
+		chart: "charts/auth"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+	postgres: {
+		chart: "charts/postgresql"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	postgres: {
+		chart: charts.postgres
+		values: {
+			fullnameOverride: "postgres"
+			image: {
+				registry: images.postgres.registry
+				repository: images.postgres.imageName
+				tag: images.postgres.tag
+				pullPolicy: images.postgres.pullPolicy
+			}
+			service: {
+				type: "ClusterIP"
+				port: 5432
+			}
+			primary: {
+				initdb: {
+					scripts: {
+						"init.sql": """
+						CREATE USER kratos WITH PASSWORD 'kratos';
+						CREATE USER hydra WITH PASSWORD 'hydra';
+						CREATE DATABASE kratos WITH OWNER = kratos;
+						CREATE DATABASE hydra WITH OWNER = hydra;
+						"""
+					}
+				}
+				persistence: {
+					size: "1Gi"
+				}
+				securityContext: {
+					enabled: true
+					fsGroup: 0
+				}
+				containerSecurityContext: {
+					enabled: true
+					runAsUser: 0
+				}
+			}
+			volumePermissions: {
+				securityContext: {
+					runAsUser: 0
+				}
+			}
+		}
+	}
+	auth: {
+		chart: charts.auth
+		dependsOn: [postgres]
+		dependsOnExternal: [{
+			name: "ingress-nginx"
+			namespace: "\(global.namespacePrefix)ingress-private"
+		}]
+		values: {
+			kratos: {
+				fullnameOverride: "kratos"
+				image: {
+					repository: images.kratos.fullName
+					tag: images.kratos.tag
+					pullPolicy: images.kratos.pullPolicy
+				}
+				service: {
+					admin: {
+						enabled: true
+						type: "ClusterIP"
+						port: 80
+						name: "http"
+					}
+					public: {
+						enabled: true
+						type: "ClusterIP"
+						port: 80
+						name: "http"
+					}
+				}
+				ingress: {
+					admin: {
+						enabled: true
+						className: _ingressPrivate
+						hosts: [{
+							host: "kratos.\(global.privateDomain)"
+							paths: [{
+								path: "/"
+								pathType: "Prefix"
+							}]
+						}]
+						tls: [{
+							hosts: [
+								"kratos.\(global.privateDomain)"
+						]
+						}]
+					}
+					public: {
+						enabled: true
+						className: _ingressPublic
+						annotations: {
+							"acme.cert-manager.io/http01-edit-in-place": "true"
+							"cert-manager.io/cluster-issuer": _issuerPublic
+						}
+						hosts: [{
+							host: "accounts.\(global.domain)"
+							paths: [{
+								path: "/"
+								pathType: "Prefix"
+							}]
+						}]
+						tls: [{
+							hosts: ["accounts.\(global.domain)"]
+							secretName: "cert-accounts.\(global.domain)"
+						}]
+					}
+				}
+				secret: {
+					enabled: true
+				}
+				kratos: {
+					automigration: {
+						enabled: true
+					}
+					development: false
+					courier: {
+						enabled: false
+					}
+					config: {
+						version: "v0.7.1-alpha.1"
+						dsn: "postgres://kratos:kratos@postgres.\(global.namespacePrefix)core-auth.svc:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4"
+						serve: {
+							public: {
+								base_url: "https://accounts.\(global.domain)"
+								cors: {
+									enabled: true
+									debug: false
+									allow_credentials: true
+									allowed_origins: [
+										"https://\(global.domain)",
+										"https://*.\(global.domain)",
+								]
+								}
+							}
+							admin: {
+								base_url: "https://kratos.\(global.privateDomain)/"
+							}
+						}
+						selfservice: {
+							default_browser_return_url: "https://accounts-ui.\(global.domain)"
+							methods: {
+								password: {
+									enabled: true
+								}
+							}
+							flows: {
+								error: {
+									ui_url: "https://accounts-ui.\(global.domain)/error"
+								}
+								settings: {
+									ui_url: "https://accounts-ui.\(global.domain)/settings"
+									privileged_session_max_age: "15m"
+								}
+								recovery: {
+									enabled: false
+								}
+								verification: {
+									enabled: false
+								}
+								logout: {
+									after: {
+										default_browser_return_url: "https://accounts-ui.\(global.domain)/login"
+									}
+								}
+								login: {
+									ui_url: "https://accounts-ui.\(global.domain)/login"
+									lifespan: "10m"
+									after: {
+										password: {
+											default_browser_return_url: "https://accounts-ui.\(global.domain)/"
+										}
+									}
+								}
+								registration: {
+									lifespan: "10m"
+									ui_url: "https://accounts-ui.\(global.domain)/register"
+									after: {
+										password: {
+											hooks: [{
+												hook: "session"
+											}]
+											default_browser_return_url: "https://accounts-ui.\(global.domain)/"
+										}
+									}
+								}
+							}
+						}
+						log: {
+							level: "debug"
+							format: "text"
+							leak_sensitive_values: true
+						}
+						cookies: {
+							path: "/"
+							same_site: "None"
+							domain: global.domain
+						}
+						secrets: {
+							cookie: ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"]
+						}
+						hashers: {
+							argon2: {
+								parallelism: 1
+								memory: "128MB"
+								iterations: 2
+								salt_length: 16
+								key_length: 16
+								}
+						}
+						identity: {
+							schemas: [{
+								id: "user"
+								url: "file:///etc/config/identity.schema.json"
+							}]
+							default_schema_id: "user"
+						}
+						courier: {
+							smtp: {
+								connection_uri: "smtps://test-z1VmkYfYPjgdPRgPFgmeZ31esT9rUgS%40\(global.domain):iW%213Kk%5EPPLFrZa%24%21bbpTPN9Wv3b8mvwS6ZJvMLtce%23A2%2A4MotD@mx1.\(global.domain)"
+							}
+						}
+					}
+					identitySchemas: {
+                        "identity.schema.json": userSchema
+					}
+				}
+			}
+			hydra: {
+				fullnameOverride: "hydra"
+				image: {
+					repository: images.hydra.fullName
+					tag: images.hydra.tag
+					pullPolicy: images.hydra.pullPolicy
+				}
+				service: {
+					admin: {
+						enabled: true
+						type: "ClusterIP"
+						port: 80
+						name: "http"
+					}
+					public: {
+						enabled: true
+						type: "ClusterIP"
+						port: 80
+						name: "http"
+					}
+				}
+				ingress: {
+					admin: {
+						enabled: true
+						className: _ingressPrivate
+						hosts: [{
+							host: "hydra.\(global.privateDomain)"
+							paths: [{
+								path: "/"
+								pathType: "Prefix"
+							}]
+							   }]
+						tls: [{
+							hosts: ["hydra.\(global.privateDomain)"]
+						}]
+					}
+					public: {
+						enabled: true
+						className: _ingressPublic
+						annotations: {
+							"acme.cert-manager.io/http01-edit-in-place": "true"
+							"cert-manager.io/cluster-issuer": _issuerPublic
+						}
+						hosts: [{
+							host: "hydra.\(global.domain)"
+							paths: [{
+								path: "/"
+								pathType: "Prefix"
+							}]
+						}]
+						tls: [{
+							hosts: ["hydra.\(global.domain)"]
+							secretName: "cert-hydra.\(global.domain)"
+						}]
+					}
+				}
+				secret: {
+					enabled: true
+				}
+				maester: {
+					enabled: true
+				}
+				"hydra-maester": {
+					adminService: {
+						name: "hydra-admin"
+						port: 80
+					}
+					image: {
+						repository: images["hydra-maester"].fullName
+						tag: images["hydra-maester"].tag
+						pullPolicy: images["hydra-maester"].pullPolicy
+					}
+				}
+				hydra: {
+					automigration: {
+						enabled: true
+					}
+					config: {
+						version: "v1.10.6"
+						dsn: "postgres://hydra:hydra@postgres.\(global.namespacePrefix)core-auth.svc:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4"
+						serve: {
+							cookies: {
+								same_site_mode: "None"
+							}
+							public: {
+								cors: {
+									enabled: true
+									debug: false
+									allow_credentials: true
+									allowed_origins: [
+										"https://\(global.domain)",
+										"https://*.\(global.domain)"
+								]
+								}
+							}
+							admin: {
+								cors: {
+									allowed_origins: [
+										"https://hydra.\(global.privateDomain)"
+								]
+								}
+								tls: {
+									allow_termination_from: [
+										"0.0.0.0/0",
+										"10.42.0.0/16",
+										"10.43.0.0/16",
+								]
+								}
+							}
+							tls: {
+								allow_termination_from: [
+									"0.0.0.0/0",
+									"10.42.0.0/16",
+									"10.43.0.0/16",
+							]
+							}
+						}
+						urls: {
+							self: {
+								public: "https://hydra.\(global.domain)"
+								issuer: "https://hydra.\(global.domain)"
+							}
+							consent: "https://accounts-ui.\(global.domain)/consent"
+							login: "https://accounts-ui.\(global.domain)/login"
+							logout: "https://accounts-ui.\(global.domain)/logout"
+						}
+						secrets: {
+							system: ["youReallyNeedToChangeThis"]
+						}
+						oidc: {
+							subject_identifiers: {
+								supported_types: [
+									"pairwise",
+									"public",
+							]
+								pairwise: {
+									salt: "youReallyNeedToChangeThis"
+								}
+							}
+						}
+						log: {
+							level: "trace"
+							leak_sensitive_values: false
+						}
+					}
+				}
+			}
+			ui: {
+				certificateIssuer: _issuerPublic
+				ingressClassName: _ingressPublic
+				domain: global.domain
+				internalDomain: global.privateDomain
+				hydra: "hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+				enableRegistration: false
+				image: {
+					repository: images.ui.fullName
+					tag: images.ui.tag
+					pullPolicy: images.ui.pullPolicy
+				}
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/core-auth.jsonschema b/core/installer/values-tmpl/core-auth.jsonschema
deleted file mode 100644
index a7ccc8d..0000000
--- a/core/installer/values-tmpl/core-auth.jsonschema
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Subdomain": { "type": "string" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/core-auth.md b/core/installer/values-tmpl/core-auth.md
deleted file mode 100644
index ed24bca..0000000
--- a/core/installer/values-tmpl/core-auth.md
+++ /dev/null
@@ -1 +0,0 @@
-OpenID Connect base Single Sign On solution
diff --git a/core/installer/values-tmpl/core-auth.yaml b/core/installer/values-tmpl/core-auth.yaml
deleted file mode 100644
index 26af983..0000000
--- a/core/installer/values-tmpl/core-auth.yaml
+++ /dev/null
@@ -1,299 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: core-auth
-  namespace: {{ .Release.Namespace }}
-spec:
-  dependsOn:
-  - name: core-auth-storage
-    namespace: {{ .Release.Namespace }}
-  - name: ingress-private # TODO(giolekva): is this needed?
-    namespace: {{ .Global.Id }}-ingress-private
-  chart:
-    spec:
-      chart: charts/auth
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    kratos:
-      fullnameOverride: kratos
-      image:
-        repository: oryd/kratos
-        tag: v0.13.0
-        pullPolicy: IfNotPresent
-      service:
-        admin:
-          enabled: true
-          type: ClusterIP
-          port: 80
-          name: http
-        public:
-          enabled: true
-          type: ClusterIP
-          port: 80
-          name: http
-      ingress:
-        admin:
-          enabled: true
-          className: {{ .Global.Id }}-ingress-private
-          hosts:
-          - host: kratos.p.{{ .Global.Domain }}
-            paths:
-            - path: /
-              pathType: Prefix
-          tls:
-          - hosts:
-            - kratos.p.{{ .Global.Domain }}
-        public:
-          enabled: true
-          className: {{ .Global.PCloudEnvName }}-ingress-public
-          annotations:
-            acme.cert-manager.io/http01-edit-in-place: "true"
-            cert-manager.io/cluster-issuer: {{ .Global.Id }}-public
-          hosts:
-          - host: accounts.{{ .Global.Domain }}
-            paths:
-            - path: /
-              pathType: Prefix
-          tls:
-          - hosts:
-            - accounts.{{ .Global.Domain }}
-            secretName: cert-accounts.{{ .Global.Domain }}
-      secret:
-        enabled: true
-      kratos:
-        automigration:
-          enabled: true
-        development: false
-        courier:
-          enabled: false
-        config:
-          version: v0.7.1-alpha.1
-          dsn: postgres://kratos:kratos@postgres.{{ .Global.Id }}-core-auth.svc:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4
-          serve:
-            public:
-              base_url: https://accounts.{{ .Global.Domain }}
-              cors:
-                enabled: true
-                debug: false
-                allow_credentials: true
-                allowed_origins:
-                - https://{{ .Global.Domain }}
-                - https://*.{{ .Global.Domain }}
-            admin:
-              base_url: https://kratos.p.{{ .Global.Domain }}/
-          selfservice:
-            default_browser_return_url: https://accounts-ui.{{ .Global.Domain }}
-            # whitelisted_return_urls:
-            #   - https://accounts-ui.{{ .Global.Domain }}
-            methods:
-              password:
-                enabled: true
-            flows:
-              error:
-                ui_url: https://accounts-ui.{{ .Global.Domain }}/error
-              settings:
-                ui_url: https://accounts-ui.{{ .Global.Domain }}/settings
-                privileged_session_max_age: 15m
-              recovery:
-                enabled: false
-              verification:
-                enabled: false
-              logout:
-                after:
-                  default_browser_return_url: https://accounts-ui.{{ .Global.Domain }}/login
-              login:
-                ui_url: https://accounts-ui.{{ .Global.Domain }}/login
-                lifespan: 10m
-                after:
-                  password:
-                    default_browser_return_url: https://accounts-ui.{{ .Global.Domain }}/
-              registration:
-                lifespan: 10m
-                ui_url: https://accounts-ui.{{ .Global.Domain }}/register
-                after:
-                  password:
-                    hooks:
-                      -
-                        hook: session
-                    default_browser_return_url: https://accounts-ui.{{ .Global.Domain }}/
-          log:
-            level: debug
-            format: text
-            leak_sensitive_values: true
-          cookies:
-            path: /
-            same_site: None
-            domain: {{ .Global.Domain }}
-          secrets:
-            cookie:
-              - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
-            # cipher:
-            #   - 32-LONG-SECRET-NOT-SECURE-AT-ALL
-          # ciphers:
-          #   algorithm: xchacha20-poly1305
-          hashers:
-            argon2:
-              parallelism: 1
-              memory: 128MB
-              iterations: 2
-              salt_length: 16
-              key_length: 16
-          identity:
-            schemas:
-            - id: user
-              url: file:///etc/config/identity.schema.json
-            default_schema_id: user
-          courier:
-            smtp:
-              connection_uri: smtps://test-z1VmkYfYPjgdPRgPFgmeZ31esT9rUgS%40{{ .Global.Domain }}:iW%213Kk%5EPPLFrZa%24%21bbpTPN9Wv3b8mvwS6ZJvMLtce%23A2%2A4MotD@mx1.{{ .Global.Domain }}
-        identitySchemas:
-          "identity.schema.json": |
-            {
-              "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
-              "$schema": "http://json-schema.org/draft-07/schema#",
-              "title": "User",
-              "type": "object",
-              "properties": {
-                "traits": {
-                  "type": "object",
-                  "properties": {
-                    "username": {
-                      "type": "string",
-                      "format": "username",
-                      "title": "Username",
-                      "minLength": 3,
-                      "ory.sh/kratos": {
-                        "credentials": {
-                          "password": {
-                            "identifier": true
-                          }
-                        }
-                      }
-                    }
-                  },
-                  "additionalProperties": false
-                }
-              }
-            }
-    hydra:
-      fullnameOverride: hydra
-      image:
-        repository: oryd/hydra
-        tag: v2.1.2
-        pullPolicy: IfNotPresent
-        # repository: giolekva/ory-hydra
-        # tag: latest
-        # pullPolicy: Always
-      service:
-        admin:
-          enabled: true
-          type: ClusterIP
-          port: 80
-          name: http
-        public:
-          enabled: true
-          type: ClusterIP
-          port: 80
-          name: http
-      ingress:
-        admin:
-          enabled: true
-          className: {{ .Global.Id }}-ingress-private
-          hosts:
-          - host: hydra.p.{{ .Global.Domain }}
-            paths:
-            - path: /
-              pathType: Prefix
-          tls:
-          - hosts:
-            - hydra.p.{{ .Global.Domain }}
-        public:
-          enabled: true
-          className: {{ .Global.PCloudEnvName }}-ingress-public
-          annotations:
-            acme.cert-manager.io/http01-edit-in-place: "true"
-            cert-manager.io/cluster-issuer: {{ .Global.Id }}-public
-          hosts:
-          - host: hydra.{{ .Global.Domain }}
-            paths:
-            - path: /
-              pathType: Prefix
-          tls:
-          - hosts:
-            - hydra.{{ .Global.Domain }}
-            secretName: cert-hydra.{{ .Global.Domain }}
-      secret:
-        enabled: true
-      maester:
-        enabled: true
-      hydra-maester:
-        adminService:
-          name: hydra-admin
-          port: 80
-        image:
-          repository: giolekva/ory-hydra-maester
-          tag: latest
-          pullPolicy: IfNotPresent
-      hydra:
-        automigration:
-          enabled: true
-        config:
-          version: v1.10.6
-          dsn: postgres://hydra:hydra@postgres.{{ .Global.Id }}-core-auth.svc:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4
-          serve:
-            cookies:
-              same_site_mode: None
-            public:
-              cors:
-                enabled: true
-                debug: false
-                allow_credentials: true
-                allowed_origins:
-                  - https://{{ .Global.Domain }}
-                  - https://*.{{ .Global.Domain }}
-            admin:
-              # host: localhost
-              cors:
-                allowed_origins:
-                  - https://hydra.p.{{ .Global.Domain }}
-              tls:
-                allow_termination_from:
-                  - 0.0.0.0/0
-                  - 10.42.0.0/16
-                  - 10.43.0.0/16
-            tls:
-              allow_termination_from:
-                - 0.0.0.0/0
-                - 10.42.0.0/16
-                - 10.43.0.0/16
-          urls:
-            self:
-              public: https://hydra.{{ .Global.Domain }}
-              issuer: https://hydra.{{ .Global.Domain }}
-            consent: https://accounts-ui.{{ .Global.Domain }}/consent
-            login: https://accounts-ui.{{ .Global.Domain }}/login
-            logout: https://accounts-ui.{{ .Global.Domain }}/logout
-          secrets:
-            system:
-              - youReallyNeedToChangeThis
-          oidc:
-            subject_identifiers:
-              supported_types:
-                - pairwise
-                - public
-              pairwise:
-                salt: youReallyNeedToChangeThis
-          log:
-            level: trace
-            leak_sensitive_values: false
-    ui:
-      certificateIssuer: {{ .Global.Id }}-public
-      ingressClassName: {{ .Global.PCloudEnvName }}-ingress-public
-      domain: {{ .Global.Domain }}
-      internalDomain: p.{{ .Global.Domain }}
-      hydra: hydra-admin.{{ .Global.Id }}-core-auth.svc.cluster.local
-      enableRegistration: false
diff --git a/core/installer/values-tmpl/coredns-keys.yaml b/core/installer/values-tmpl/coredns-keys.yaml
deleted file mode 100644
index a605deb..0000000
--- a/core/installer/values-tmpl/coredns-keys.yaml
+++ /dev/null
@@ -1,37 +0,0 @@
-# apiVersion: v1
-# kind: PersistentVolumeClaim
-# metadata:
-#   name: keys
-#   namespace: dodo-core-coredns
-# spec:
-#   storageClassName: ""
-#   accessModes:
-#     - ReadWriteMany
-#   resources:
-#     requests:
-#       storage: 1Gi
-#   volumeName: keys
----
-apiVersion: v1
-kind: Pod
-metadata:
-  name: keys
-  namespace: dodo-core-coredns
-spec:
-  containers:
-  - name: keys
-    image: busybox:1.36.1
-    command: ["sleep", "infinity"]
-    volumeMounts:
-    - name: dodo
-      mountPath: /etc/dodo
-    - name: dodo-config
-      mountPath: /etc/dodo-config
-  volumes:
-  - name: dodo
-    persistentVolumeClaim:
-      claimName: keys
-  - name: dodo-config
-    configMap:
-      name: dodo-dns
-
diff --git a/core/installer/values-tmpl/coredns.yaml b/core/installer/values-tmpl/coredns.yaml
deleted file mode 100644
index cfd57e6..0000000
--- a/core/installer/values-tmpl/coredns.yaml
+++ /dev/null
@@ -1,85 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: coredns
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/coredns
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  values:
-    image:
-      repository: coredns/coredns
-      tag: 1.11.1
-      pullPolicy: IfNotPresent
-    replicaCount: 1
-    resources:
-      limits:
-        cpu: 100m
-        memory: 128Mi
-      requests:
-        cpu: 100m
-        memory: 128Mi
-    rollingUpdate:
-      maxUnavailable: 1
-      maxSurge: 25%
-    terminationGracePeriodSeconds: 30
-    serviceType: "ClusterIP"
-    service:
-      name: coredns
-    serviceAccount:
-      create: false
-    rbac:
-      create: true
-      pspEnable: false
-    isClusterService: true
-    securityContext:
-      capabilities:
-        add:
-          - NET_BIND_SERVICE
-    servers:
-    - zones:
-      - zone: .
-      port: 53
-      plugins:
-      - name: log
-      - name: health
-        configBlock: |-
-          lameduck 5s
-      - name: ready
-    extraConfig:
-      import:
-        parameters: {{ .Values.Volume.MountPath }}/coredns.conf
-    extraVolumes:
-    - name: zone-configs
-      persistentVolumeClaim:
-        claimName: {{ .Values.Volume.ClaimName }}
-    extraVolumeMounts:
-    - name: zone-configs
-      mountPath: {{ .Values.Volume.MountPath}}
-    livenessProbe:
-      enabled: true
-      initialDelaySeconds: 60
-      periodSeconds: 10
-      timeoutSeconds: 5
-      failureThreshold: 5
-      successThreshold: 1
-    readinessProbe:
-      enabled: true
-      initialDelaySeconds: 30
-      periodSeconds: 10
-      timeoutSeconds: 5
-      failureThreshold: 5
-      successThreshold: 1
-    zoneFiles: []
-    hpa:
-      enabled: false
-    autoscaler:
-      enabled: false
-    deployment:
-      enabled: true
diff --git a/core/installer/values-tmpl/csi-driver-smb.cue b/core/installer/values-tmpl/csi-driver-smb.cue
new file mode 100644
index 0000000..ef0f530
--- /dev/null
+++ b/core/installer/values-tmpl/csi-driver-smb.cue
@@ -0,0 +1,67 @@
+input: {}
+
+_baseImage: {
+	registry: "registry.k8s.io"
+	repository: "sig-storage"
+	pullPolicy: "IfNotPresent"
+}
+
+images: {
+	smb: _baseImage & {
+		name: "smbplugin"
+		tag: "v1.11.0"
+	}
+	csiProvisioner: _baseImage & {
+		name: "csi-provisioner"
+		tag: "v3.5.0"
+	}
+	livenessProbe: _baseImage & {
+		name: "livenessprobe"
+		tag: "v2.10.0"
+	}
+	nodeDriverRegistrar: _baseImage & {
+		name: "csi-node-driver-registrar"
+		tag: "v2.8.0"
+	}
+}
+
+charts: {
+	csiDriverSMB: {
+		chart: "charts/csi-driver-smb"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+}
+
+helm: {
+	"csi-driver-smb": {
+		chart: charts.csiDriverSMB
+		values: {
+			image: {
+				smb: {
+					repository: images.smb.fullName
+					tag: images.smb.tag
+					pullPolicy: images.smb.pullPolicy
+				}
+				csiProvisioner: {
+					repository: images.csiProvisioner.fullName
+					tag: images.csiProvisioner.tag
+					pullPolicy: images.csiProvisioner.pullPolicy
+				}
+				livenessProbe: {
+					repository: images.livenessProbe.fullName
+					tag: images.livenessProbe.tag
+					pullPolicy: images.livenessProbe.pullPolicy
+				}
+				nodeDriverRegistrar: {
+					repository: images.nodeDriverRegistrar.fullName
+					tag: images.nodeDriverRegistrar.tag
+					pullPolicy: images.nodeDriverRegistrar.pullPolicy
+				}
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/csi-driver-smb.jsonschema b/core/installer/values-tmpl/csi-driver-smb.jsonschema
deleted file mode 100644
index f42d895..0000000
--- a/core/installer/values-tmpl/csi-driver-smb.jsonschema
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/csi-driver-smb.md b/core/installer/values-tmpl/csi-driver-smb.md
deleted file mode 100644
index 171a000..0000000
--- a/core/installer/values-tmpl/csi-driver-smb.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs iCSI SMB driver
diff --git a/core/installer/values-tmpl/csi-driver-smb.yaml b/core/installer/values-tmpl/csi-driver-smb.yaml
deleted file mode 100644
index 9297d0f..0000000
--- a/core/installer/values-tmpl/csi-driver-smb.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: csi-driver-smb
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/csi-driver-smb
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  timeout: 20m0s
-  values:
diff --git a/core/installer/values-tmpl/dns-challenge-resolver.yaml b/core/installer/values-tmpl/dns-challenge-resolver.yaml
deleted file mode 100644
index ddabaa8..0000000
--- a/core/installer/values-tmpl/dns-challenge-resolver.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-certManager:
-  namespace: dodo-cert-manager
-  serviceAccountName: dodo-cert-manager
diff --git a/core/installer/values-tmpl/dns-zone-controller.jsonschema b/core/installer/values-tmpl/dns-zone-controller.jsonschema
deleted file mode 100644
index 4c67ac3..0000000
--- a/core/installer/values-tmpl/dns-zone-controller.jsonschema
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Volume": {
-      "type": "object",
-	  "properties": {
-		"ClaimName": { "type": "string" },
-		"MountPath": { "type": "string" }
-	  },
-	  "additionalProperties": false
-	},
-	"APIConfigMapName": { "type": "string" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/dns-zone-controller.md b/core/installer/values-tmpl/dns-zone-controller.md
deleted file mode 100644
index a6abe91..0000000
--- a/core/installer/values-tmpl/dns-zone-controller.md
+++ /dev/null
@@ -1 +0,0 @@
-Sets up DNS zone controller to automatically generate zone files of registered domains.
diff --git a/core/installer/values-tmpl/dns-zone-controller.yaml b/core/installer/values-tmpl/dns-zone-controller.yaml
deleted file mode 100644
index 51fe1e3..0000000
--- a/core/installer/values-tmpl/dns-zone-controller.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: dns-zone-controller
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/dns-ns-controller
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  values:
-    image:
-      repository: giolekva/dns-ns-controller
-      tag: latest
-      pullPolicy: Always
-    installCRDs: true
-    volume:
-      claimName: {{ .Values.Volume.ClaimName }}
-      mountPath: {{ .Values.Volume.MountPath }}
-    apiConfigMapName: {{ .Values.APIConfigMapName }}
diff --git a/core/installer/values-tmpl/dns-zone-manager.cue b/core/installer/values-tmpl/dns-zone-manager.cue
new file mode 100644
index 0000000..4d977d0
--- /dev/null
+++ b/core/installer/values-tmpl/dns-zone-manager.cue
@@ -0,0 +1,175 @@
+input: {
+	apiConfigMapName: string
+	volume: {
+		size: string
+		claimName: string
+		mountPath: string
+	}
+}
+
+images: {
+	dnsZoneController: {
+		repository: "giolekva"
+		name: "dns-ns-controller"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+	kubeRBACProxy: {
+		registry: "gcr.io"
+		repository: "kubebuilder"
+		name: "kube-rbac-proxy"
+		tag: "v0.13.0"
+		pullPolicy: "IfNotPresent"
+	}
+	coredns: {
+		repository: "coredns"
+		name: "coredns"
+		tag: "1.11.1"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	volume: {
+		chart: "charts/volumes"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+	dnsZoneController: {
+		chart: "charts/dns-ns-controller"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+	coredns: {
+		chart: "charts/coredns"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+}
+
+_volumeName: "zone-configs"
+
+helm: {
+	volume: {
+		chart: charts.volume
+		values: {
+			name: input.volume.claimName
+			size: input.volume.size
+			accessMode: "ReadWriteMany"
+		}
+	}
+	"dns-zone-controller": {
+		chart: charts.dnsZoneController
+		values: {
+			installCRDs: true
+			apiConfigMapName: input.apiConfigMapName
+			volume: {
+				claimName: input.volume.claimName
+				mountPath: input.volume.mountPath
+			}
+			image: {
+				repository: images.dnsZoneController.fullName
+				tag: images.dnsZoneController.tag
+				pullPolicy: images.dnsZoneController.pullPolicy
+			}
+			kubeRBACProxy: {
+				image: {
+					repository: images.kubeRBACProxy.fullName
+					tag: images.kubeRBACProxy.tag
+					pullPolicy: images.kubeRBACProxy.pullPolicy
+				}
+			}
+		}
+	}
+	coredns: {
+		chart: charts.coredns
+		values: {
+			image: {
+				repository: images.coredns.fullName
+				tag: images.coredns.tag
+				pullPolicy: images.coredns.pullPolicy
+			}
+			replicaCount: 1
+			resources: {
+				limits: {
+					cpu: "100m"
+					memory: "128Mi"
+				}
+				requests: {
+					cpu: "100m"
+					memory: "128Mi"
+				}
+			}
+			rollingUpdate: {
+				maxUnavailable: 1
+				maxSurge: "25%"
+			}
+			terminationGracePeriodSeconds: 30
+			serviceType: "ClusterIP"
+			service: name: "coredns"
+			serviceAccount: create: false
+			rbac: {
+				create: true
+				pspEnable: false
+			}
+			isClusterService: true
+			securityContext: capabilities: add: ["NET_BIND_SERVICE"]
+			servers: [{
+				zones: [{
+					zone: "."
+				}]
+				port: 53
+				plugins: [
+					{
+						name: "log"
+					},
+					{
+						name: "health"
+						configBlock: "lameduck 5s"
+					},
+					{
+						name: "ready"
+					}
+			]
+			}]
+			extraConfig: import: parameters: "\(input.volume.mountPath)/coredns.conf"
+			extraVolumes: [{
+				name: _volumeName
+				persistentVolumeClaim: claimName: input.volume.claimName
+			}]
+			extraVolumeMounts: [{
+				name: _volumeName
+				mountPath: input.volume.mountPath
+			}]
+			livenessProbe: {
+				enabled: true
+				initialDelaySeconds: 60
+				periodSeconds: 10
+				timeoutSeconds: 5
+				failureThreshold: 5
+				successThreshold: 1
+			}
+			readinessProbe: {
+				enabled: true
+				initialDelaySeconds: 30
+				periodSeconds: 10
+				timeoutSeconds: 5
+				failureThreshold: 5
+				successThreshold: 1
+			}
+			zoneFiles: []
+			hpa: enabled: false
+			autoscaler: enabled: false
+			deployment: enabled: true
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/dns-zone-storage.yaml b/core/installer/values-tmpl/dns-zone-storage.yaml
deleted file mode 100644
index 72b7848..0000000
--- a/core/installer/values-tmpl/dns-zone-storage.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: dns-zone-storage
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/volumes
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 10m0s
-  values:
-    name: {{ .Values.Volume.ClaimName }}
-    size: {{ .Values.Volume.Size }}
-    accessMode: ReadWriteMany
diff --git a/core/installer/values-tmpl/env-manager.cue b/core/installer/values-tmpl/env-manager.cue
new file mode 100644
index 0000000..e34c546
--- /dev/null
+++ b/core/installer/values-tmpl/env-manager.cue
@@ -0,0 +1,48 @@
+import (
+	"encoding/base64"
+)
+
+input: {
+	repoIP: string
+	repoPort: number
+	repoName: string
+	sshPrivateKey: string
+}
+
+images: {
+	envManager: {
+		repository: "giolekva"
+		name: "pcloud-installer"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+}
+
+charts: {
+	envManager: {
+		chart: "charts/env-manager"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+}
+
+helm: {
+	"env-manager": {
+		chart: charts.envManager
+		values: {
+			repoIP: input.repoIP
+			repoPort: input.repoPort
+			repoName: input.repoName
+			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+			clusterRoleName: "\(global.pcloudEnvName)-env-manager"
+			image: {
+				repository: images.envManager.fullName
+				tag: images.envManager.tag
+				pullPolicy: images.envManager.pullPolicy
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/env-manager.jsonschema b/core/installer/values-tmpl/env-manager.jsonschema
deleted file mode 100644
index aa2f01d..0000000
--- a/core/installer/values-tmpl/env-manager.jsonschema
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "RepoIP": { "type": "string", "default": "192.168.0.11" },
-	"SSHPrivateKey": { "type": "string", "default": "foo bar" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/env-manager.md b/core/installer/values-tmpl/env-manager.md
deleted file mode 100644
index ec69eba..0000000
--- a/core/installer/values-tmpl/env-manager.md
+++ /dev/null
@@ -1 +0,0 @@
-PCloud environment manager
diff --git a/core/installer/values-tmpl/env-manager.yaml b/core/installer/values-tmpl/env-manager.yaml
deleted file mode 100644
index 7271dff..0000000
--- a/core/installer/values-tmpl/env-manager.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: env-manager
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/env-manager
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  values:
-    repoIP: {{ .Values.RepoIP }}
-    repoPort: {{ .Values.RepoPort }}
-    repoName: {{ .Values.RepoName }}
-    sshPrivateKey: {{ .Values.SSHPrivateKey | b64enc }}
-    clusterRoleName: {{ .Global.PCloudEnvName }}-env-manager
diff --git a/core/installer/values-tmpl/fluxcd-reconciler.cue b/core/installer/values-tmpl/fluxcd-reconciler.cue
new file mode 100644
index 0000000..8c90648
--- /dev/null
+++ b/core/installer/values-tmpl/fluxcd-reconciler.cue
@@ -0,0 +1,34 @@
+input: {}
+
+images: {
+	fluxcdReconciler: {
+		repository: "giolekva"
+		name: "fluxcd-reconciler"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+}
+
+charts: {
+	fluxcdReconciler: {
+		chart: "charts/fluxcd-reconciler"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+}
+
+helm: {
+	"fluxcd-reconciler": {
+		chart: charts.fluxcdReconciler
+		values: {
+			image: {
+				repository: images.fluxcdReconciler.fullName
+				tag: images.fluxcdReconciler.tag
+				pullPolicy: images.fluxcdReconciler.pullPolicy
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/fluxcd-reconciler.jsonschema b/core/installer/values-tmpl/fluxcd-reconciler.jsonschema
deleted file mode 100644
index 4a7c07d..0000000
--- a/core/installer/values-tmpl/fluxcd-reconciler.jsonschema
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "type": "object",
-  "properties": {},
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/fluxcd-reconciler.md b/core/installer/values-tmpl/fluxcd-reconciler.md
deleted file mode 100644
index f31c256..0000000
--- a/core/installer/values-tmpl/fluxcd-reconciler.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs gateway to Fluxcd API
diff --git a/core/installer/values-tmpl/fluxcd-reconciler.yaml b/core/installer/values-tmpl/fluxcd-reconciler.yaml
deleted file mode 100644
index 51fd8d1..0000000
--- a/core/installer/values-tmpl/fluxcd-reconciler.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: fluxcd-reconciler
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/fluxcd-reconciler
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  values:
diff --git a/core/installer/values-tmpl/harbor.yaml b/core/installer/values-tmpl/harbor.yaml
new file mode 100644
index 0000000..8fe0180
--- /dev/null
+++ b/core/installer/values-tmpl/harbor.yaml
@@ -0,0 +1,918 @@
+expose:
+  # Set how to expose the service. Set the type as "ingress", "clusterIP", "nodePort" or "loadBalancer"
+  # and fill the information in the corresponding section
+  type: ingress
+  tls:
+    # Enable TLS or not.
+    # Delete the "ssl-redirect" annotations in "expose.ingress.annotations" when TLS is disabled and "expose.type" is "ingress"
+    # Note: if the "expose.type" is "ingress" and TLS is disabled,
+    # the port must be included in the command when pulling/pushing images.
+    # Refer to https://github.com/goharbor/harbor/issues/5291 for details.
+    enabled: true
+    # The source of the tls certificate. Set as "auto", "secret"
+    # or "none" and fill the information in the corresponding section
+    # 1) auto: generate the tls certificate automatically
+    # 2) secret: read the tls certificate from the specified secret.
+    # The tls certificate can be generated manually or by cert manager
+    # 3) none: configure no tls certificate for the ingress. If the default
+    # tls certificate is configured in the ingress controller, choose this option
+    certSource: auto
+    auto:
+      # The common name used to generate the certificate, it's necessary
+      # when the type isn't "ingress"
+      commonName: ""
+    secret:
+      # The name of secret which contains keys named:
+      # "tls.crt" - the certificate
+      # "tls.key" - the private key
+      secretName: ""
+  ingress:
+    hosts:
+      core: harbor.t46.lekva.me
+    # set to the type of ingress controller if it has specific requirements.
+    # leave as `default` for most ingress controllers.
+    # set to `gce` if using the GCE ingress controller
+    # set to `ncp` if using the NCP (NSX-T Container Plugin) ingress controller
+    # set to `alb` if using the ALB ingress controller
+    # set to `f5-bigip` if using the F5 BIG-IP ingress controller
+    controller: default
+    ## Allow .Capabilities.KubeVersion.Version to be overridden while creating ingress
+    kubeVersionOverride: ""
+    className: dodo-ingress-public
+    annotations:
+      # note different ingress controllers may require a different ssl-redirect annotation
+      # for Envoy, use ingress.kubernetes.io/force-ssl-redirect: "true" and remove the nginx lines below
+      ingress.kubernetes.io/ssl-redirect: "true"
+      ingress.kubernetes.io/proxy-body-size: "0"
+      nginx.ingress.kubernetes.io/ssl-redirect: "true"
+      nginx.ingress.kubernetes.io/proxy-body-size: "0"
+      acme.cert-manager.io/http01-edit-in-place: "true"
+      cert-manager.io/cluster-issuer: vhrb-public
+    harbor:
+      # harbor ingress-specific annotations
+      annotations: {}
+      # harbor ingress-specific labels
+      labels: {}
+  clusterIP:
+    # The name of ClusterIP service
+    name: harbor
+    # The ip address of the ClusterIP service (leave empty for acquiring dynamic ip)
+    staticClusterIP: ""
+    # Annotations on the ClusterIP service
+    annotations: {}
+    ports:
+      # The service port Harbor listens on when serving HTTP
+      httpPort: 80
+      # The service port Harbor listens on when serving HTTPS
+      httpsPort: 443
+  nodePort:
+    # The name of NodePort service
+    name: harbor
+    ports:
+      http:
+        # The service port Harbor listens on when serving HTTP
+        port: 80
+        # The node port Harbor listens on when serving HTTP
+        nodePort: 30002
+      https:
+        # The service port Harbor listens on when serving HTTPS
+        port: 443
+        # The node port Harbor listens on when serving HTTPS
+        nodePort: 30003
+  loadBalancer:
+    # The name of LoadBalancer service
+    name: harbor
+    # Set the IP if the LoadBalancer supports assigning IP
+    IP: ""
+    ports:
+      # The service port Harbor listens on when serving HTTP
+      httpPort: 80
+      # The service port Harbor listens on when serving HTTPS
+      httpsPort: 443
+    annotations: {}
+    sourceRanges: []
+
+# The external URL for Harbor core service. It is used to
+# 1) populate the docker/helm commands showed on portal
+# 2) populate the token service URL returned to docker client
+#
+# Format: protocol://domain[:port]. Usually:
+# 1) if "expose.type" is "ingress", the "domain" should be
+# the value of "expose.ingress.hosts.core"
+# 2) if "expose.type" is "clusterIP", the "domain" should be
+# the value of "expose.clusterIP.name"
+# 3) if "expose.type" is "nodePort", the "domain" should be
+# the IP address of k8s node
+#
+# If Harbor is deployed behind the proxy, set it as the URL of proxy
+externalURL: https://harbor.t46.lekva.me
+
+# The internal TLS used for harbor components secure communicating. In order to enable https
+# in each component tls cert files need to provided in advance.
+internalTLS:
+  # If internal TLS enabled
+  enabled: false
+  # enable strong ssl ciphers (default: false)
+  strong_ssl_ciphers: false
+  # There are three ways to provide tls
+  # 1) "auto" will generate cert automatically
+  # 2) "manual" need provide cert file manually in following value
+  # 3) "secret" internal certificates from secret
+  certSource: "auto"
+  # The content of trust ca, only available when `certSource` is "manual"
+  trustCa: ""
+  # core related cert configuration
+  core:
+    # secret name for core's tls certs
+    secretName: ""
+    # Content of core's TLS cert file, only available when `certSource` is "manual"
+    crt: ""
+    # Content of core's TLS key file, only available when `certSource` is "manual"
+    key: ""
+  # jobservice related cert configuration
+  jobservice:
+    # secret name for jobservice's tls certs
+    secretName: ""
+    # Content of jobservice's TLS key file, only available when `certSource` is "manual"
+    crt: ""
+    # Content of jobservice's TLS key file, only available when `certSource` is "manual"
+    key: ""
+  # registry related cert configuration
+  registry:
+    # secret name for registry's tls certs
+    secretName: ""
+    # Content of registry's TLS key file, only available when `certSource` is "manual"
+    crt: ""
+    # Content of registry's TLS key file, only available when `certSource` is "manual"
+    key: ""
+  # portal related cert configuration
+  portal:
+    # secret name for portal's tls certs
+    secretName: ""
+    # Content of portal's TLS key file, only available when `certSource` is "manual"
+    crt: ""
+    # Content of portal's TLS key file, only available when `certSource` is "manual"
+    key: ""
+  # trivy related cert configuration
+  trivy:
+    # secret name for trivy's tls certs
+    secretName: ""
+    # Content of trivy's TLS key file, only available when `certSource` is "manual"
+    crt: ""
+    # Content of trivy's TLS key file, only available when `certSource` is "manual"
+    key: ""
+
+ipFamily:
+  # ipv6Enabled set to true if ipv6 is enabled in cluster, currently it affected the nginx related component
+  ipv6:
+    enabled: false
+  # ipv4Enabled set to true if ipv4 is enabled in cluster, currently it affected the nginx related component
+  ipv4:
+    enabled: true
+
+# The persistence is enabled by default and a default StorageClass
+# is needed in the k8s cluster to provision volumes dynamically.
+# Specify another StorageClass in the "storageClass" or set "existingClaim"
+# if you already have existing persistent volumes to use
+#
+# For storing images and charts, you can also use "azure", "gcs", "s3",
+# "swift" or "oss". Set it in the "imageChartStorage" section
+persistence:
+  enabled: true
+  # Setting it to "keep" to avoid removing PVCs during a helm delete
+  # operation. Leaving it empty will delete PVCs after the chart deleted
+  # (this does not apply for PVCs that are created for internal database
+  # and redis components, i.e. they are never deleted automatically)
+  resourcePolicy: "keep"
+  persistentVolumeClaim:
+    registry:
+      # Use the existing PVC which must be created manually before bound,
+      # and specify the "subPath" if the PVC is shared with other components
+      existingClaim: ""
+      # Specify the "storageClass" used to provision the volume. Or the default
+      # StorageClass will be used (the default).
+      # Set it to "-" to disable dynamic provisioning
+      storageClass: ""
+      subPath: ""
+      accessMode: ReadWriteOnce
+      size: 5Gi
+      annotations: {}
+    jobservice:
+      jobLog:
+        existingClaim: ""
+        storageClass: ""
+        subPath: ""
+        accessMode: ReadWriteOnce
+        size: 1Gi
+        annotations: {}
+    # If external database is used, the following settings for database will
+    # be ignored
+    database:
+      existingClaim: ""
+      storageClass: ""
+      subPath: ""
+      accessMode: ReadWriteOnce
+      size: 1Gi
+      annotations: {}
+    # If external Redis is used, the following settings for Redis will
+    # be ignored
+    redis:
+      existingClaim: ""
+      storageClass: ""
+      subPath: ""
+      accessMode: ReadWriteOnce
+      size: 1Gi
+      annotations: {}
+    trivy:
+      existingClaim: ""
+      storageClass: ""
+      subPath: ""
+      accessMode: ReadWriteOnce
+      size: 5Gi
+      annotations: {}
+  # Define which storage backend is used for registry to store
+  # images and charts. Refer to
+  # https://github.com/distribution/distribution/blob/main/docs/configuration.md#storage
+  # for the detail.
+  imageChartStorage:
+    # Specify whether to disable `redirect` for images and chart storage, for
+    # backends which not supported it (such as using minio for `s3` storage type), please disable
+    # it. To disable redirects, simply set `disableredirect` to `true` instead.
+    # Refer to
+    # https://github.com/distribution/distribution/blob/main/docs/configuration.md#redirect
+    # for the detail.
+    disableredirect: false
+    # Specify the "caBundleSecretName" if the storage service uses a self-signed certificate.
+    # The secret must contain keys named "ca.crt" which will be injected into the trust store
+    # of registry's containers.
+    # caBundleSecretName:
+
+    # Specify the type of storage: "filesystem", "azure", "gcs", "s3", "swift",
+    # "oss" and fill the information needed in the corresponding section. The type
+    # must be "filesystem" if you want to use persistent volumes for registry
+    type: filesystem
+    filesystem:
+      rootdirectory: /storage
+      #maxthreads: 100
+
+imagePullPolicy: IfNotPresent
+
+# Use this set to assign a list of default pullSecrets
+imagePullSecrets:
+#  - name: docker-registry-secret
+#  - name: internal-registry-secret
+
+# The update strategy for deployments with persistent volumes(jobservice, registry): "RollingUpdate" or "Recreate"
+# Set it as "Recreate" when "RWM" for volumes isn't supported
+updateStrategy:
+  type: RollingUpdate
+
+# debug, info, warning, error or fatal
+logLevel: info
+
+# The initial password of Harbor admin. Change it from portal after launching Harbor
+# or give an existing secret for it
+# key in secret is given via (default to HARBOR_ADMIN_PASSWORD)
+# existingSecretAdminPassword:
+existingSecretAdminPasswordKey: HARBOR_ADMIN_PASSWORD
+harborAdminPassword: "Harbor12345"
+
+# The name of the secret which contains key named "ca.crt". Setting this enables the
+# download link on portal to download the CA certificate when the certificate isn't
+# generated automatically
+caSecretName: ""
+
+# The secret key used for encryption. Must be a string of 16 chars.
+secretKey: "not-a-secure-key"
+# If using existingSecretSecretKey, the key must be secretKey
+existingSecretSecretKey: ""
+
+# The proxy settings for updating trivy vulnerabilities from the Internet and replicating
+# artifacts from/to the registries that cannot be reached directly
+proxy:
+  httpProxy:
+  httpsProxy:
+  noProxy: 127.0.0.1,localhost,.local,.internal
+  components:
+    - core
+    - jobservice
+    - trivy
+
+# Run the migration job via helm hook
+enableMigrateHelmHook: false
+
+# The custom ca bundle secret, the secret must contain key named "ca.crt"
+# which will be injected into the trust store for core, jobservice, registry, trivy components
+# caBundleSecretName: ""
+
+## UAA Authentication Options
+# If you're using UAA for authentication behind a self-signed
+# certificate you will need to provide the CA Cert.
+# Set uaaSecretName below to provide a pre-created secret that
+# contains a base64 encoded CA Certificate named `ca.crt`.
+# uaaSecretName:
+
+# If service exposed via "ingress", the Nginx will not be used
+nginx:
+  image:
+    repository: goharbor/nginx-photon
+    tag: v2.10.0
+  # set the service account to be used, default if left empty
+  serviceAccountName: ""
+  # mount the service account token
+  automountServiceAccountToken: false
+  replicas: 1
+  revisionHistoryLimit: 10
+  # resources:
+  #  requests:
+  #    memory: 256Mi
+  #    cpu: 100m
+  extraEnvVars: []
+  nodeSelector: {}
+  tolerations: []
+  affinity: {}
+  # Spread Pods across failure-domains like regions, availability zones or nodes
+  topologySpreadConstraints: []
+  # - maxSkew: 1
+  #   topologyKey: topology.kubernetes.io/zone
+  #   nodeTaintsPolicy: Honor
+  #   whenUnsatisfiable: DoNotSchedule
+  ## Additional deployment annotations
+  podAnnotations: {}
+  ## Additional deployment labels
+  podLabels: {}
+  ## The priority class to run the pod as
+  priorityClassName:
+
+portal:
+  image:
+    repository: goharbor/harbor-portal
+    tag: v2.10.0
+  # set the service account to be used, default if left empty
+  serviceAccountName: ""
+  # mount the service account token
+  automountServiceAccountToken: false
+  replicas: 1
+  revisionHistoryLimit: 10
+  # resources:
+  #  requests:
+  #    memory: 256Mi
+  #    cpu: 100m
+  extraEnvVars: []
+  nodeSelector: {}
+  tolerations: []
+  affinity: {}
+  # Spread Pods across failure-domains like regions, availability zones or nodes
+  topologySpreadConstraints: []
+  # - maxSkew: 1
+  #   topologyKey: topology.kubernetes.io/zone
+  #   nodeTaintsPolicy: Honor
+  #   whenUnsatisfiable: DoNotSchedule
+  ## Additional deployment annotations
+  podAnnotations: {}
+  ## Additional deployment labels
+  podLabels: {}
+  ## Additional service annotations
+  serviceAnnotations: {}
+  ## The priority class to run the pod as
+  priorityClassName:
+
+core:
+  image:
+    repository: goharbor/harbor-core
+    tag: v2.10.0
+  # set the service account to be used, default if left empty
+  serviceAccountName: ""
+  # mount the service account token
+  automountServiceAccountToken: false
+  replicas: 1
+  revisionHistoryLimit: 10
+  ## Startup probe values
+  startupProbe:
+    enabled: true
+    initialDelaySeconds: 10
+  # resources:
+  #  requests:
+  #    memory: 256Mi
+  #    cpu: 100m
+  extraEnvVars: []
+  nodeSelector: {}
+  tolerations: []
+  affinity: {}
+  # Spread Pods across failure-domains like regions, availability zones or nodes
+  topologySpreadConstraints: []
+  # - maxSkew: 1
+  #   topologyKey: topology.kubernetes.io/zone
+  #   nodeTaintsPolicy: Honor
+  #   whenUnsatisfiable: DoNotSchedule
+  ## Additional deployment annotations
+  podAnnotations: {}
+  ## Additional deployment labels
+  podLabels: {}
+  ## Additional service annotations
+  serviceAnnotations: {}
+  ## User settings configuration json string
+  configureUserSettings:
+  # The provider for updating project quota(usage), there are 2 options, redis or db.
+  # By default it is implemented by db but you can configure it to redis which
+  # can improve the performance of high concurrent pushing to the same project,
+  # and reduce the database connections spike and occupies.
+  # Using redis will bring up some delay for quota usage updation for display, so only
+  # suggest switch provider to redis if you were ran into the db connections spike around
+  # the scenario of high concurrent pushing to same project, no improvment for other scenes.
+  quotaUpdateProvider: db # Or redis
+  # Secret is used when core server communicates with other components.
+  # If a secret key is not specified, Helm will generate one. Alternatively set existingSecret to use an existing secret
+  # Must be a string of 16 chars.
+  secret: ""
+  # Fill in the name of a kubernetes secret if you want to use your own
+  # If using existingSecret, the key must be secret
+  existingSecret: ""
+  # Fill the name of a kubernetes secret if you want to use your own
+  # TLS certificate and private key for token encryption/decryption.
+  # The secret must contain keys named:
+  # "tls.key" - the private key
+  # "tls.crt" - the certificate
+  secretName: ""
+  # If not specifying a preexisting secret, a secret can be created from tokenKey and tokenCert and used instead.
+  # If none of secretName, tokenKey, and tokenCert are specified, an ephemeral key and certificate will be autogenerated.
+  # tokenKey and tokenCert must BOTH be set or BOTH unset.
+  # The tokenKey value is formatted as a multiline string containing a PEM-encoded RSA key, indented one more than tokenKey on the following line.
+  tokenKey: |
+  # If tokenKey is set, the value of tokenCert must be set as a PEM-encoded certificate signed by tokenKey, and supplied as a multiline string, indented one more than tokenCert on the following line.
+  tokenCert: |
+  # The XSRF key. Will be generated automatically if it isn't specified
+  xsrfKey: ""
+  # If using existingSecret, the key is defined by core.existingXsrfSecretKey
+  existingXsrfSecret: ""
+  # If using existingSecret, the key
+  existingXsrfSecretKey: CSRF_KEY
+  ## The priority class to run the pod as
+  priorityClassName:
+  # The time duration for async update artifact pull_time and repository
+  # pull_count, the unit is second. Will be 10 seconds if it isn't set.
+  # eg. artifactPullAsyncFlushDuration: 10
+  artifactPullAsyncFlushDuration:
+  gdpr:
+    deleteUser: false
+
+jobservice:
+  image:
+    repository: goharbor/harbor-jobservice
+    tag: v2.10.0
+  replicas: 1
+  revisionHistoryLimit: 10
+  # set the service account to be used, default if left empty
+  serviceAccountName: ""
+  # mount the service account token
+  automountServiceAccountToken: false
+  maxJobWorkers: 10
+  # The logger for jobs: "file", "database" or "stdout"
+  jobLoggers:
+    - file
+    # - database
+    # - stdout
+  # The jobLogger sweeper duration (ignored if `jobLogger` is `stdout`)
+  loggerSweeperDuration: 14 #days
+  notification:
+    webhook_job_max_retry: 3
+    webhook_job_http_client_timeout: 3 # in seconds
+  reaper:
+    # the max time to wait for a task to finish, if unfinished after max_update_hours, the task will be mark as error, but the task will continue to run, default value is 24
+    max_update_hours: 24
+    # the max time for execution in running state without new task created
+    max_dangling_hours: 168
+
+  # resources:
+  #   requests:
+  #     memory: 256Mi
+  #     cpu: 100m
+  extraEnvVars: []
+  nodeSelector: {}
+  tolerations: []
+  affinity: {}
+  # Spread Pods across failure-domains like regions, availability zones or nodes
+  topologySpreadConstraints:
+  # - maxSkew: 1
+  #   topologyKey: topology.kubernetes.io/zone
+  #   nodeTaintsPolicy: Honor
+  #   whenUnsatisfiable: DoNotSchedule
+  ## Additional deployment annotations
+  podAnnotations: {}
+  ## Additional deployment labels
+  podLabels: {}
+  # Secret is used when job service communicates with other components.
+  # If a secret key is not specified, Helm will generate one.
+  # Must be a string of 16 chars.
+  secret: ""
+  # Use an existing secret resource
+  existingSecret: ""
+  # Key within the existing secret for the job service secret
+  existingSecretKey: JOBSERVICE_SECRET
+  ## The priority class to run the pod as
+  priorityClassName:
+
+registry:
+  # set the service account to be used, default if left empty
+  serviceAccountName: ""
+  # mount the service account token
+  automountServiceAccountToken: false
+  registry:
+    image:
+      repository: goharbor/registry-photon
+      tag: v2.10.0
+    # resources:
+    #  requests:
+    #    memory: 256Mi
+    #    cpu: 100m
+    extraEnvVars: []
+  controller:
+    image:
+      repository: goharbor/harbor-registryctl
+      tag: dev
+
+    # resources:
+    #  requests:
+    #    memory: 256Mi
+    #    cpu: 100m
+    extraEnvVars: []
+  replicas: 1
+  revisionHistoryLimit: 10
+  nodeSelector: {}
+  tolerations: []
+  affinity: {}
+  # Spread Pods across failure-domains like regions, availability zones or nodes
+  topologySpreadConstraints: []
+  # - maxSkew: 1
+  #   topologyKey: topology.kubernetes.io/zone
+  #   nodeTaintsPolicy: Honor
+  #   whenUnsatisfiable: DoNotSchedule
+  ## Additional deployment annotations
+  podAnnotations: {}
+  ## Additional deployment labels
+  podLabels: {}
+  ## The priority class to run the pod as
+  priorityClassName:
+  # Secret is used to secure the upload state from client
+  # and registry storage backend.
+  # See: https://github.com/distribution/distribution/blob/main/docs/configuration.md#http
+  # If a secret key is not specified, Helm will generate one.
+  # Must be a string of 16 chars.
+  secret: ""
+  # Use an existing secret resource
+  existingSecret: ""
+  # Key within the existing secret for the registry service secret
+  existingSecretKey: REGISTRY_HTTP_SECRET
+  # If true, the registry returns relative URLs in Location headers. The client is responsible for resolving the correct URL.
+  relativeurls: false
+  credentials:
+    username: "harbor_registry_user"
+    password: "harbor_registry_password"
+    # If using existingSecret, the key must be REGISTRY_PASSWD and REGISTRY_HTPASSWD
+    existingSecret: ""
+    # Login and password in htpasswd string format. Excludes `registry.credentials.username`  and `registry.credentials.password`. May come in handy when integrating with tools like argocd or flux. This allows the same line to be generated each time the template is rendered, instead of the `htpasswd` function from helm, which generates different lines each time because of the salt.
+    # htpasswdString: $apr1$XLefHzeG$Xl4.s00sMSCCcMyJljSZb0 # example string
+    htpasswdString: ""
+  middleware:
+    enabled: false
+    type: cloudFront
+    cloudFront:
+      baseurl: example.cloudfront.net
+      keypairid: KEYPAIRID
+      duration: 3000s
+      ipfilteredby: none
+      # The secret key that should be present is CLOUDFRONT_KEY_DATA, which should be the encoded private key
+      # that allows access to CloudFront
+      privateKeySecret: "my-secret"
+  # enable purge _upload directories
+  upload_purging:
+    enabled: true
+    # remove files in _upload directories which exist for a period of time, default is one week.
+    age: 168h
+    # the interval of the purge operations
+    interval: 24h
+    dryrun: false
+
+trivy:
+  # enabled the flag to enable Trivy scanner
+  enabled: true
+  image:
+    # repository the repository for Trivy adapter image
+    repository: goharbor/trivy-adapter-photon
+    # tag the tag for Trivy adapter image
+    tag: dev
+  # set the service account to be used, default if left empty
+  serviceAccountName: ""
+  # mount the service account token
+  automountServiceAccountToken: false
+  # replicas the number of Pod replicas
+  replicas: 1
+  # debugMode the flag to enable Trivy debug mode with more verbose scanning log
+  debugMode: false
+  # vulnType a comma-separated list of vulnerability types. Possible values are `os` and `library`.
+  vulnType: "os,library"
+  # severity a comma-separated list of severities to be checked
+  severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"
+  # ignoreUnfixed the flag to display only fixed vulnerabilities
+  ignoreUnfixed: false
+  # insecure the flag to skip verifying registry certificate
+  insecure: false
+  # gitHubToken the GitHub access token to download Trivy DB
+  #
+  # Trivy DB contains vulnerability information from NVD, Red Hat, and many other upstream vulnerability databases.
+  # It is downloaded by Trivy from the GitHub release page https://github.com/aquasecurity/trivy-db/releases and cached
+  # in the local file system (`/home/scanner/.cache/trivy/db/trivy.db`). In addition, the database contains the update
+  # timestamp so Trivy can detect whether it should download a newer version from the Internet or use the cached one.
+  # Currently, the database is updated every 12 hours and published as a new release to GitHub.
+  #
+  # Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough
+  # for production operations. If, for any reason, it's not enough, you could increase the rate limit to 5000
+  # requests per hour by specifying the GitHub access token. For more details on GitHub rate limiting please consult
+  # https://developer.github.com/v3/#rate-limiting
+  #
+  # You can create a GitHub token by following the instructions in
+  # https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line
+  gitHubToken: ""
+  # skipUpdate the flag to disable Trivy DB downloads from GitHub
+  #
+  # You might want to set the value of this flag to `true` in test or CI/CD environments to avoid GitHub rate limiting issues.
+  # If the value is set to `true` you have to manually download the `trivy.db` file and mount it in the
+  # `/home/scanner/.cache/trivy/db/trivy.db` path.
+  skipUpdate: false
+  # The offlineScan option prevents Trivy from sending API requests to identify dependencies.
+  #
+  # Scanning JAR files and pom.xml may require Internet access for better detection, but this option tries to avoid it.
+  # For example, the offline mode will not try to resolve transitive dependencies in pom.xml when the dependency doesn't
+  # exist in the local repositories. It means a number of detected vulnerabilities might be fewer in offline mode.
+  # It would work if all the dependencies are in local.
+  # This option doesn’t affect DB download. You need to specify skipUpdate as well as offlineScan in an air-gapped environment.
+  offlineScan: false
+  # Comma-separated list of what security issues to detect. Possible values are `vuln`, `config` and `secret`. Defaults to `vuln`.
+  securityCheck: "vuln"
+  # The duration to wait for scan completion
+  timeout: 5m0s
+  resources:
+    requests:
+      cpu: 200m
+      memory: 512Mi
+    limits:
+      cpu: 1
+      memory: 1Gi
+  extraEnvVars: []
+  nodeSelector: {}
+  tolerations: []
+  affinity: {}
+  # Spread Pods across failure-domains like regions, availability zones or nodes
+  topologySpreadConstraints: []
+  # - maxSkew: 1
+  #   topologyKey: topology.kubernetes.io/zone
+  #   nodeTaintsPolicy: Honor
+  #   whenUnsatisfiable: DoNotSchedule
+  ## Additional deployment annotations
+  podAnnotations: {}
+  ## Additional deployment labels
+  podLabels: {}
+  ## The priority class to run the pod as
+  priorityClassName:
+
+database:
+  # if external database is used, set "type" to "external"
+  # and fill the connection information in "external" section
+  type: internal
+  internal:
+    # set the service account to be used, default if left empty
+    serviceAccountName: ""
+    # mount the service account token
+    automountServiceAccountToken: false
+    image:
+      repository: goharbor/harbor-db
+      tag: v2.10.0
+    # The initial superuser password for internal database
+    password: "changeit"
+    # The size limit for Shared memory, pgSQL use it for shared_buffer
+    # More details see:
+    # https://github.com/goharbor/harbor/issues/15034
+    shmSizeLimit: 512Mi
+    # resources:
+    #  requests:
+    #    memory: 256Mi
+    #    cpu: 100m
+    # The timeout used in livenessProbe; 1 to 5 seconds
+    livenessProbe:
+      timeoutSeconds: 1
+    # The timeout used in readinessProbe; 1 to 5 seconds
+    readinessProbe:
+      timeoutSeconds: 1
+    extraEnvVars: []
+    nodeSelector: {}
+    tolerations: []
+    affinity: {}
+    ## The priority class to run the pod as
+    priorityClassName:
+    initContainer:
+      migrator: {}
+      # resources:
+      #  requests:
+      #    memory: 128Mi
+      #    cpu: 100m
+      permissions: {}
+      # resources:
+      #  requests:
+      #    memory: 128Mi
+      #    cpu: 100m
+  external:
+    host: "192.168.0.1"
+    port: "5432"
+    username: "user"
+    password: "password"
+    coreDatabase: "registry"
+    # if using existing secret, the key must be "password"
+    existingSecret: ""
+    # "disable" - No SSL
+    # "require" - Always SSL (skip verification)
+    # "verify-ca" - Always SSL (verify that the certificate presented by the
+    # server was signed by a trusted CA)
+    # "verify-full" - Always SSL (verify that the certification presented by the
+    # server was signed by a trusted CA and the server host name matches the one
+    # in the certificate)
+    sslmode: "disable"
+  # The maximum number of connections in the idle connection pool per pod (core+exporter).
+  # If it <=0, no idle connections are retained.
+  maxIdleConns: 100
+  # The maximum number of open connections to the database per pod (core+exporter).
+  # If it <= 0, then there is no limit on the number of open connections.
+  # Note: the default number of connections is 1024 for postgre of harbor.
+  maxOpenConns: 900
+  ## Additional deployment annotations
+  podAnnotations: {}
+  ## Additional deployment labels
+  podLabels: {}
+
+redis:
+  # if external Redis is used, set "type" to "external"
+  # and fill the connection information in "external" section
+  type: internal
+  internal:
+    # set the service account to be used, default if left empty
+    serviceAccountName: ""
+    # mount the service account token
+    automountServiceAccountToken: false
+    image:
+      repository: goharbor/redis-photon
+      tag: v2.10.0
+    # resources:
+    #  requests:
+    #    memory: 256Mi
+    #    cpu: 100m
+    extraEnvVars: []
+    nodeSelector: {}
+    tolerations: []
+    affinity: {}
+    ## The priority class to run the pod as
+    priorityClassName:
+    # # jobserviceDatabaseIndex defaults to "1"
+    # # registryDatabaseIndex defaults to "2"
+    # # trivyAdapterIndex defaults to "5"
+    # # harborDatabaseIndex defaults to "0", but it can be configured to "6", this config is optional
+    # # cacheLayerDatabaseIndex defaults to "0", but it can be configured to "7", this config is optional
+    jobserviceDatabaseIndex: "1"
+    registryDatabaseIndex: "2"
+    trivyAdapterIndex: "5"
+    # harborDatabaseIndex: "6"
+    # cacheLayerDatabaseIndex: "7"
+  external:
+    # support redis, redis+sentinel
+    # addr for redis: <host_redis>:<port_redis>
+    # addr for redis+sentinel: <host_sentinel1>:<port_sentinel1>,<host_sentinel2>:<port_sentinel2>,<host_sentinel3>:<port_sentinel3>
+    addr: "192.168.0.2:6379"
+    # The name of the set of Redis instances to monitor, it must be set to support redis+sentinel
+    sentinelMasterSet: ""
+    # The "coreDatabaseIndex" must be "0" as the library Harbor
+    # used doesn't support configuring it
+    # harborDatabaseIndex defaults to "0", but it can be configured to "6", this config is optional
+    # cacheLayerDatabaseIndex defaults to "0", but it can be configured to "7", this config is optional
+    coreDatabaseIndex: "0"
+    jobserviceDatabaseIndex: "1"
+    registryDatabaseIndex: "2"
+    trivyAdapterIndex: "5"
+    # harborDatabaseIndex: "6"
+    # cacheLayerDatabaseIndex: "7"
+    # username field can be an empty string, and it will be authenticated against the default user
+    username: ""
+    password: ""
+    # If using existingSecret, the key must be REDIS_PASSWORD
+    existingSecret: ""
+  ## Additional deployment annotations
+  podAnnotations: {}
+  ## Additional deployment labels
+  podLabels: {}
+
+exporter:
+  replicas: 1
+  revisionHistoryLimit: 10
+  # resources:
+  #  requests:
+  #    memory: 256Mi
+  #    cpu: 100m
+  extraEnvVars: []
+  podAnnotations: {}
+  ## Additional deployment labels
+  podLabels: {}
+  serviceAccountName: ""
+  # mount the service account token
+  automountServiceAccountToken: false
+  image:
+    repository: goharbor/harbor-exporter
+    tag: v2.10.0
+  nodeSelector: {}
+  tolerations: []
+  affinity: {}
+  # Spread Pods across failure-domains like regions, availability zones or nodes
+  topologySpreadConstraints: []
+  # - maxSkew: 1
+  #   topologyKey: topology.kubernetes.io/zone
+  #   nodeTaintsPolicy: Honor
+  #   whenUnsatisfiable: DoNotSchedule
+  cacheDuration: 23
+  cacheCleanInterval: 14400
+  ## The priority class to run the pod as
+  priorityClassName:
+
+metrics:
+  enabled: false
+  core:
+    path: /metrics
+    port: 8001
+  registry:
+    path: /metrics
+    port: 8001
+  jobservice:
+    path: /metrics
+    port: 8001
+  exporter:
+    path: /metrics
+    port: 8001
+  ## Create prometheus serviceMonitor to scrape harbor metrics.
+  ## This requires the monitoring.coreos.com/v1 CRD. Please see
+  ## https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/user-guides/getting-started.md
+  ##
+  serviceMonitor:
+    enabled: false
+    additionalLabels: {}
+    # Scrape interval. If not set, the Prometheus default scrape interval is used.
+    interval: ""
+    # Metric relabel configs to apply to samples before ingestion.
+    metricRelabelings:
+      []
+      # - action: keep
+      #   regex: 'kube_(daemonset|deployment|pod|namespace|node|statefulset).+'
+      #   sourceLabels: [__name__]
+    # Relabel configs to apply to samples before ingestion.
+    relabelings:
+      []
+      # - sourceLabels: [__meta_kubernetes_pod_node_name]
+      #   separator: ;
+      #   regex: ^(.*)$
+      #   targetLabel: nodename
+      #   replacement: $1
+      #   action: replace
+
+trace:
+  enabled: false
+  # trace provider: jaeger or otel
+  # jaeger should be 1.26+
+  provider: jaeger
+  # set sample_rate to 1 if you wanna sampling 100% of trace data; set 0.5 if you wanna sampling 50% of trace data, and so forth
+  sample_rate: 1
+  # namespace used to differentiate different harbor services
+  # namespace:
+  # attributes is a key value dict contains user defined attributes used to initialize trace provider
+  # attributes:
+  #   application: harbor
+  jaeger:
+    # jaeger supports two modes:
+    #   collector mode(uncomment endpoint and uncomment username, password if needed)
+    #   agent mode(uncomment agent_host and agent_port)
+    endpoint: http://hostname:14268/api/traces
+    # username:
+    # password:
+    # agent_host: hostname
+    # export trace data by jaeger.thrift in compact mode
+    # agent_port: 6831
+  otel:
+    endpoint: hostname:4318
+    url_path: /v1/traces
+    compression: false
+    insecure: true
+    # timeout is in seconds
+    timeout: 10
+
+# cache layer configurations
+# if this feature enabled, harbor will cache the resource
+# `project/project_metadata/repository/artifact/manifest` in the redis
+# which help to improve the performance of high concurrent pulling manifest.
+cache:
+  # default is not enabled.
+  enabled: false
+  # default keep cache for one day.
+  expireHours: 24
diff --git a/core/installer/values-tmpl/headscale-controller.cue b/core/installer/values-tmpl/headscale-controller.cue
new file mode 100644
index 0000000..8a9756b
--- /dev/null
+++ b/core/installer/values-tmpl/headscale-controller.cue
@@ -0,0 +1,49 @@
+input: {}
+
+images: {
+	headscaleController: {
+		repository: "giolekva"
+		name: "headscale-controller"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+	kubeRBACProxy: {
+		registry: "gcr.io"
+		repository: "kubebuilder"
+		name: "kube-rbac-proxy"
+		tag: "v0.13.0"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	headscaleController: {
+		chart: "charts/headscale-controller"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+}
+
+helm: {
+	"headscale-controller": {
+		chart: charts.headscaleController
+		values: {
+			installCRDs: true
+			image: {
+				repository: images.headscaleController.fullName
+				tag: images.headscaleController.tag
+				pullPolicy: images.headscaleController.pullPolicy
+			}
+			kubeRBACProxy: {
+				image: {
+					repository: images.kubeRBACProxy.fullName
+					tag: images.kubeRBACProxy.tag
+					pullPolicy: images.kubeRBACProxy.pullPolicy
+				}
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/headscale-controller.jsonschema b/core/installer/values-tmpl/headscale-controller.jsonschema
deleted file mode 100644
index f42d895..0000000
--- a/core/installer/values-tmpl/headscale-controller.jsonschema
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/headscale-controller.md b/core/installer/values-tmpl/headscale-controller.md
deleted file mode 100644
index 99d2190..0000000
--- a/core/installer/values-tmpl/headscale-controller.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs headscale controller
diff --git a/core/installer/values-tmpl/headscale-controller.yaml b/core/installer/values-tmpl/headscale-controller.yaml
deleted file mode 100644
index f799d9b..0000000
--- a/core/installer/values-tmpl/headscale-controller.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: headscale-controller
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/headscale-controller
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  values:
diff --git a/core/installer/values-tmpl/headscale-user.cue b/core/installer/values-tmpl/headscale-user.cue
new file mode 100644
index 0000000..c591d12
--- /dev/null
+++ b/core/installer/values-tmpl/headscale-user.cue
@@ -0,0 +1,33 @@
+input: {
+	username: string
+	preAuthKey: {
+		enabled: bool | *false
+	}
+}
+
+images: {}
+
+charts: {
+	headscaleUser: {
+		chart: "charts/headscale-user"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	"headscale-user": {
+		chart: charts.headscaleUser
+		values: {
+			username: input.username
+			headscaleApiAddress: "http://headscale-api.\(global.namespacePrefix)app-headscale.svc.cluster.local"
+			preAuthKey: {
+				enabled: input.preAuthKey.enabled
+				secretName: "\(input.username)-headscale-preauthkey"
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/headscale-user.jsonschema b/core/installer/values-tmpl/headscale-user.jsonschema
deleted file mode 100644
index e4884a6..0000000
--- a/core/installer/values-tmpl/headscale-user.jsonschema
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Username": { "type": "string" },
-	"PreAuthKey": {
-	  "type": "object",
-	  "properties": {
-	    "Enabled": { "type": "boolean" }
-	  },
-	  "additionalProperties": false
-	}
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/headscale-user.md b/core/installer/values-tmpl/headscale-user.md
deleted file mode 100644
index 7246082..0000000
--- a/core/installer/values-tmpl/headscale-user.md
+++ /dev/null
@@ -1 +0,0 @@
-Creates Headscale user resource
diff --git a/core/installer/values-tmpl/headscale-user.yaml b/core/installer/values-tmpl/headscale-user.yaml
deleted file mode 100644
index 0eca8d0..0000000
--- a/core/installer/values-tmpl/headscale-user.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: user-{{ .Values.Username }}
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/headscale-user
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    username: {{ .Values.Username }}
-    headscaleApiAddress: http://headscale-api.{{ .Global.Id }}-app-headscale.svc.cluster.local
-    preAuthKey:
-      enabled: {{ .Values.PreAuthKey.Enabled }}
-      secretName: {{ .Values.Username }}-headscale-preauthkey
diff --git a/core/installer/values-tmpl/headscale.cue b/core/installer/values-tmpl/headscale.cue
new file mode 100644
index 0000000..21dbb75
--- /dev/null
+++ b/core/installer/values-tmpl/headscale.cue
@@ -0,0 +1,68 @@
+input: {
+	subdomain: string
+}
+
+images: {
+	headscale: {
+		repository: "headscale"
+		name: "headscale"
+		tag: "0.22.3"
+		pullPolicy: "IfNotPresent"
+	}
+	api: {
+		repository: "giolekva"
+		name: "headscale-api"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+}
+
+charts: {
+	headscale: {
+		chart: "charts/headscale"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	headscale: {
+		chart: charts.headscale
+		dependsOnExternal: [{
+			name: "auth"
+			namespace: "\(global.namespacePrefix)core-auth"
+		}]
+		values: {
+			image: {
+				repository: images.headscale.fullName
+				tag: images.headscale.tag
+				pullPolicy: images.headscale.pullPolicy
+			}
+			storage: size: "5Gi"
+			ingressClassName: _ingressPublic
+			certificateIssuer: _issuerPublic
+			domain: "\(input.subdomain).\(global.domain)"
+			publicBaseDomain: global.domain
+			oauth2: {
+				hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+				hydraPublic: "https://hydra.\(global.domain)"
+				clientId: "headscale"
+				secretName: "oauth2-client-headscale"
+			}
+			ipAddressPool: "\(global.id)-headscale"
+			api: {
+				port: 8585
+				rootDomain: global.domain
+				image: {
+					repository: images.api.fullName
+					tag: images.api.tag
+					pullPolicy: images.api.pullPolicy
+				}
+			}
+			ui: enabled: false
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/headscale.jsonschema b/core/installer/values-tmpl/headscale.jsonschema
deleted file mode 100644
index a7ccc8d..0000000
--- a/core/installer/values-tmpl/headscale.jsonschema
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Subdomain": { "type": "string" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/headscale.md b/core/installer/values-tmpl/headscale.md
deleted file mode 100644
index 1dec9d9..0000000
--- a/core/installer/values-tmpl/headscale.md
+++ /dev/null
@@ -1 +0,0 @@
-Will install headscale VPN service on https://{{ .Values.Subdomain }}.{{ .Global.Domain }}
diff --git a/core/installer/values-tmpl/headscale.yaml b/core/installer/values-tmpl/headscale.yaml
deleted file mode 100644
index 4eeeacd..0000000
--- a/core/installer/values-tmpl/headscale.yaml
+++ /dev/null
@@ -1,43 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: headscale
-  namespace: {{ .Release.Namespace }}
-spec:
-  dependsOn:
-    - name: core-auth
-      namespace: {{ .Global.NamespacePrefix }}core-auth
-  chart:
-    spec:
-      chart: charts/headscale
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    image:
-      repository: headscale/headscale
-      tag: 0.22.3
-      pullPolicy: IfNotPresent
-    storage:
-      size: 5Gi
-    ingressClassName: {{ .Global.PCloudEnvName }}-ingress-public
-    certificateIssuer: {{ .Global.Id }}-public
-    domain: {{ .Values.Subdomain }}.{{ .Global.Domain }}
-    publicBaseDomain: {{ .Global.Domain }}
-    oauth2:
-      hydraAdmin: http://hydra-admin.{{ .Global.NamespacePrefix }}core-auth.svc.cluster.local
-      hydraPublic: https://hydra.{{ .Global.Domain }}
-      clientId: headscale
-      secretName: oauth2-client-headscale
-    ipAddressPool: {{ .Global.Id }}-headscale
-    api:
-      port: 8585
-      rootDomain: {{ .Global.Domain }}
-      image:
-        repository: giolekva/headscale-api
-        tag: latest
-        pullPolicy: Always
-    ui:
-      enabled: false
diff --git a/core/installer/values-tmpl/ingress-private.yaml b/core/installer/values-tmpl/ingress-private.yaml
deleted file mode 100644
index fb15cd4..0000000
--- a/core/installer/values-tmpl/ingress-private.yaml
+++ /dev/null
@@ -1,32 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: ingress-private
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/ingress-nginx
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    fullnameOverride: {{ .Global.Id }}-nginx-private
-    controller:
-      service:
-        enabled: true
-        type: LoadBalancer
-        annotations:
-          metallb.universe.tf/address-pool: {{ .Global.Id }}-ingress-private
-      ingressClassByName: true
-      ingressClassResource:
-        name: {{ .Global.Id }}-ingress-private
-        enabled: true
-        default: false
-        controllerValue: k8s.io/{{ .Global.Id }}-ingress-private
-      extraArgs:
-        default-ssl-certificate: "{{ .Global.Id }}-ingress-private/cert-wildcard.p.{{ .Global.Domain }}"
-      admissionWebhooks:
-        enabled: false
diff --git a/core/installer/values-tmpl/ingress-public.cue b/core/installer/values-tmpl/ingress-public.cue
new file mode 100644
index 0000000..1717762
--- /dev/null
+++ b/core/installer/values-tmpl/ingress-public.cue
@@ -0,0 +1,57 @@
+input: {}
+
+images: {
+	ingressNginx: {
+		registry: "registry.k8s.io"
+		repository: "ingress-nginx"
+		name: "controller"
+		tag: "v1.8.0"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	ingressNginx: {
+		chart: "charts/ingress-nginx"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+}
+
+helm: {
+	"ingress-public": {
+		chart: charts.ingressNginx
+		values: {
+			fullnameOverride: _ingressPublic
+			controller: {
+				kind: "DaemonSet"
+				hostNetwork: true
+				hostPort: enabled: true
+				service: enabled: false
+				ingressClassByName: true
+				ingressClassResource: {
+					name: _ingressPublic
+					enabled: true
+					default: false
+					controllerValue: "k8s.io/\(_ingressPublic)"
+				}
+				config: "proxy-body-size": "100M" // TODO(giolekva): configurable
+				image: {
+					registry: images.ingressNginx.registry
+					image: images.ingressNginx.imageName
+					tag: images.ingressNginx.tag
+					pullPolicy: images.ingressNginx.pullPolicy
+				}
+			}
+			tcp: {
+				"53": "\(global.pcloudEnvName)-dns-zone-manager/coredns:53"
+			}
+			udp: {
+				"53": "\(global.pcloudEnvName)-dns-zone-manager/coredns:53"
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/ingress-public.jsonschema b/core/installer/values-tmpl/ingress-public.jsonschema
deleted file mode 100644
index f42d895..0000000
--- a/core/installer/values-tmpl/ingress-public.jsonschema
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/ingress-public.md b/core/installer/values-tmpl/ingress-public.md
deleted file mode 100644
index 227c2d4..0000000
--- a/core/installer/values-tmpl/ingress-public.md
+++ /dev/null
@@ -1 +0,0 @@
-Sets up ingress for publicly accessible services
diff --git a/core/installer/values-tmpl/ingress-public.yaml b/core/installer/values-tmpl/ingress-public.yaml
deleted file mode 100644
index 25379d4..0000000
--- a/core/installer/values-tmpl/ingress-public.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: ingress-public
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/ingress-nginx
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  values:
-    fullnameOverride: {{ .Global.PCloudEnvName }}-ingress-public
-    controller:
-      kind: DaemonSet
-      hostNetwork: true
-      hostPort:
-        enabled: true
-      service:
-        enabled: false
-      ingressClassByName: true
-      ingressClassResource:
-        name: {{ .Global.PCloudEnvName }}-ingress-public
-        enabled: true
-        default: false
-        controllerValue: k8s.io/{{ .Global.PCloudEnvName }}-ingress-public
-      config:
-        proxy-body-size: 100M # TODO(giolekva): configurable
-    tcp:
-      "53": "{{ .Global.PCloudEnvName }}-dns-zone-manager/coredns:53"
-    udp:
-      "53": "{{ .Global.PCloudEnvName }}-dns-zone-manager/coredns:53"
diff --git a/core/installer/values-tmpl/jellyfin.cue b/core/installer/values-tmpl/jellyfin.cue
new file mode 100644
index 0000000..3beff46
--- /dev/null
+++ b/core/installer/values-tmpl/jellyfin.cue
@@ -0,0 +1,46 @@
+input: {
+	network: #Network
+	subdomain: string
+}
+
+_domain: "\(input.subdomain).\(input.network.domain)"
+
+readme: "jellyfin application will be installed on \(input.network.name) network and be accessible to any user on https://\(_domain)"
+
+images: {
+	jellyfin: {
+		repository: "jellyfin"
+		name: "jellyfin"
+		tag: "10.8.10"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	jellyfin: {
+		chart: "charts/jellyfin"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	jellyfin: {
+		chart: charts.jellyfin
+		values: {
+			pcloudInstanceId: global.id
+			ingress: {
+				className: input.network.ingressClass
+				domain: _domain
+			}
+			image: {
+				repository: images.jellyfin.fullName
+				tag: images.jellyfin.tag
+				pullPolicy: images.jellyfin.pullPolicy
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/jellyfin.jsonschema b/core/installer/values-tmpl/jellyfin.jsonschema
deleted file mode 100644
index ec6a2c5..0000000
--- a/core/installer/values-tmpl/jellyfin.jsonschema
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Values": {
-      "type": "object",
-      "properties": {
-        "NamespacePrefix": { "type": "string" },
-        "Id": { "type": "string" },
-        "Domain": { "type": "string" }
-      },
-      "additionalProperties": false
-    }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/jellyfin.yaml b/core/installer/values-tmpl/jellyfin.yaml
deleted file mode 100644
index d486072..0000000
--- a/core/installer/values-tmpl/jellyfin.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: jellyfin
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/jellyfin
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    pcloudInstanceId: {{ .Global.Id }}
-    ingress:
-      className: {{ .Global.Id }}-ingress-private
-      domain: {{ .Values.Subdomain }}.{{ .Global.PrivateDomain }}
diff --git a/core/installer/values-tmpl/matrix-storage.yaml b/core/installer/values-tmpl/matrix-storage.yaml
deleted file mode 100644
index 9b9c56c..0000000
--- a/core/installer/values-tmpl/matrix-storage.yaml
+++ /dev/null
@@ -1,39 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: matrix-storage
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/postgresql
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    fullnameOverride: postgres
-    image:
-      repository: library/postgres # arm64v8/postgres
-      tag: 15.3 # 13.4
-    service:
-      type: ClusterIP
-      port: 5432
-    primary:
-      initdb:
-        scripts:
-          init.sql: |
-            CREATE USER matrix WITH PASSWORD 'matrix';
-            CREATE DATABASE matrix WITH OWNER = matrix ENCODING = UTF8 LOCALE = 'C' TEMPLATE = template0;
-      persistence:
-        size: 10Gi
-      securityContext:
-        enabled: true
-        fsGroup: 0
-      containerSecurityContext:
-        enabled: true
-        runAsUser: 0
-    volumePermissions:
-      securityContext:
-        runAsUser: 0
diff --git a/core/installer/values-tmpl/matrix.cue b/core/installer/values-tmpl/matrix.cue
new file mode 100644
index 0000000..606c7f8
--- /dev/null
+++ b/core/installer/values-tmpl/matrix.cue
@@ -0,0 +1,120 @@
+input: {
+	network: #Network
+	subdomain: string
+}
+
+_domain: "\(input.subdomain).\(input.network.domain)"
+
+readme: "matrix application will be installed on \(input.network.name) network and be accessible to any user on https://\(_domain)"
+
+images: {
+	matrix: {
+		repository: "matrixdotorg"
+		name: "synapse"
+		tag: "latest"
+		pullPolicy: "IfNotPresent"
+	}
+	postgres: {
+		repository: "library"
+		name: "postgres"
+		tag: "15.3"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	matrix: {
+		chart: "charts/matrix"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+	postgres: {
+		chart: "charts/postgresql"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	matrix: {
+		dependsOn: [
+			postgres
+	    ]
+		chart: charts.matrix
+		values: {
+			domain: global.domain
+			subdomain: input.subdomain
+			oauth2: {
+				hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+				hydraPublic: "https://hydra.\(global.domain)"
+				secretName: "oauth2-client"
+			}
+			postgresql: {
+				host: "postgres"
+				port: 5432
+				database: "matrix"
+				user: "matrix"
+				password: "matrix"
+			}
+			certificateIssuer: _issuerPublic
+			ingressClassName: _ingressPublic
+			configMerge: {
+				configName: "config-to-merge"
+				fileName: "to-merge.yaml"
+			}
+			image: {
+				repository: images.matrix.fullName
+				tag: images.matrix.tag
+				pullPolicy: images.matrix.pullPolicy
+			}
+		}
+	}
+	postgres: {
+		chart: charts.postgres
+		values: {
+			fullnameOverride: "postgres"
+			image: {
+				registry: images.postgres.registry
+				repository: images.postgres.imageName
+				tag: images.postgres.tag
+				pullPolicy: images.postgres.pullPolicy
+			}
+			service: {
+				type: "ClusterIP"
+				port: 5432
+			}
+			primary: {
+				initdb: {
+					scripts: {
+						"init.sql": """
+						CREATE USER matrix WITH PASSWORD 'matrix';
+						CREATE DATABASE matrix WITH OWNER = matrix ENCODING = UTF8 LOCALE = 'C' TEMPLATE = template0;
+						"""
+					}
+				}
+				persistence: {
+					size: "10Gi"
+				}
+				securityContext: {
+					enabled: true
+					fsGroup: 0
+				}
+				containerSecurityContext: {
+					enabled: true
+					runAsUser: 0
+				}
+			}
+			volumePermissions: {
+				securityContext: {
+					runAsUser: 0
+				}
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/matrix.jsonschema b/core/installer/values-tmpl/matrix.jsonschema
deleted file mode 100644
index 89f64ce..0000000
--- a/core/installer/values-tmpl/matrix.jsonschema
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-	"Subdomain": { "type": "string", "default": "matrix" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/matrix.yaml b/core/installer/values-tmpl/matrix.yaml
deleted file mode 100644
index 579eecd..0000000
--- a/core/installer/values-tmpl/matrix.yaml
+++ /dev/null
@@ -1,39 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: matrix
-  namespace: {{ .Release.Namespace }}
-spec:
-  dependsOn:
-  - name: matrix-storage
-    namespace: {{ .Release.Namespace }}
-  chart:
-    spec:
-      chart: charts/matrix
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    domain: {{ .Global.Domain }}
-    subdomain: {{ .Values.Subdomain }}
-    oauth2:
-      hydraAdmin: http://hydra-admin.{{ .Global.NamespacePrefix }}core-auth.svc.cluster.local
-      hydraPublic: https://hydra.{{ .Global.Domain }}
-      secretName: oauth2-client
-    postgresql:
-      host: postgres
-      port: 5432
-      database: matrix
-      user: matrix
-      password: matrix
-    certificateIssuer: {{ .Global.Id }}-public
-    ingressClassName: {{ .Global.PCloudEnvName }}-ingress-public
-    configMerge:
-      configName: config-to-merge
-      fileName: to-merge.yaml
-    image:
-      repository: matrixdotorg/synapse
-      tag: v1.98.0
-      pullPolicy: IfNotPresent
diff --git a/core/installer/values-tmpl/metallb-ipaddresspool.cue b/core/installer/values-tmpl/metallb-ipaddresspool.cue
new file mode 100644
index 0000000..ba7dee9
--- /dev/null
+++ b/core/installer/values-tmpl/metallb-ipaddresspool.cue
@@ -0,0 +1,33 @@
+input: {
+	name: string
+	from: string
+	to: string
+	autoAssign: bool | *false
+	namespace: string
+}
+
+images: {}
+
+charts: {
+	metallbIPAddressPool: {
+		chart: "charts/metallb-ipaddresspool"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName // TODO(gio): id ?
+		}
+	}
+}
+
+helm: {
+	"metallb-ipaddresspool-\(input.name)": {
+		chart: charts.metallbIPAddressPool
+		values: {
+			name: input.name
+			from: input.from
+			to: input.to
+			autoAssign: input.autoAssign
+			namespace: input.namespace
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/metallb-ipaddresspool.jsonschema b/core/installer/values-tmpl/metallb-ipaddresspool.jsonschema
deleted file mode 100644
index d76700b..0000000
--- a/core/installer/values-tmpl/metallb-ipaddresspool.jsonschema
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Name": { "type": "string" },
-	"From": { "type": "string" },
-    "To": { "type": "string" },
-	"AutoAssign": { "type": "boolean" },
-	"Namespace": { "type": "string" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/metallb-ipaddresspool.md b/core/installer/values-tmpl/metallb-ipaddresspool.md
deleted file mode 100644
index e0ba8e6..0000000
--- a/core/installer/values-tmpl/metallb-ipaddresspool.md
+++ /dev/null
@@ -1 +0,0 @@
-Metallb IPAddressPool
diff --git a/core/installer/values-tmpl/metallb-ipaddresspool.yaml b/core/installer/values-tmpl/metallb-ipaddresspool.yaml
deleted file mode 100644
index 87b55f4..0000000
--- a/core/installer/values-tmpl/metallb-ipaddresspool.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: metallb-ipaddresspool-{{ .Values.Name }}
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/metallb-ipaddresspool
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 10m0s
-  values:
-    name: {{ .Values.Name }}
-    from: {{ .Values.From }}
-    to: {{ .Values.To }}
-    autoAssign: {{ .Values.AutoAssign }}
-    namespace: {{ .Values.Namespace }}
diff --git a/core/installer/values-tmpl/penpot.cue b/core/installer/values-tmpl/penpot.cue
new file mode 100644
index 0000000..75c3b46
--- /dev/null
+++ b/core/installer/values-tmpl/penpot.cue
@@ -0,0 +1,177 @@
+input: {
+	network: #Network
+	subdomain: string
+}
+
+_domain: "\(input.subdomain).\(input.network.domain)"
+
+readme: "penpot application will be installed on \(input.network.name) network and be accessible to any user on https://\(_domain)"
+
+images: {
+	postgres: {
+		repository: "library"
+		name: "postgres"
+		tag: "15.3"
+		pullPolicy: "IfNotPresent"
+	}
+	backend: {
+		repository: "penpotapp"
+		name: "backend"
+		tag: "1.16.0-beta"
+		pullPolicy: "IfNotPresent"
+	}
+	frontend: {
+		repository: "penpotapp"
+		name: "frontend"
+		tag: "1.16.0-beta"
+		pullPolicy: "IfNotPresent"
+	}
+	exporter: {
+		repository: "penpotapp"
+		name: "exporter"
+		tag: "1.16.0-beta"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	postgres: {
+		chart: "charts/postgresql"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+	oauth2Client: {
+		chart: "charts/oauth2-client"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+	penpot: {
+		chart: "charts/penpot"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+_oauth2SecretName: "oauth2-credentials"
+
+helm: {
+	"oauth2-client": {
+		chart: charts.oauth2Client
+		values: {
+			name: "penpot"
+			secretName: _oauth2SecretName
+			grantTypes: ["authorization_code"]
+			responseTypes: ["code"]
+			scope: "openid profile email"
+			redirectUris: ["https://\(_domain)/api/auth/oauth/oidc/callback"]
+			hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+			tokenEndpointAuthMethod: "client_secret_post"
+		}
+	}
+	postgres: {
+		chart: charts.postgres
+		values: {
+			fullnameOverride: "postgres"
+			image: {
+				registry: images.postgres.registry
+				repository: images.postgres.imageName
+				tag: images.postgres.tag
+				pullPolicy: images.postgres.pullPolicy
+			}
+			auth: {
+				username: "penpot"
+				password: "penpot"
+				database: "penpot"
+			}
+		}
+	}
+	penpot: {
+		chart: charts.penpot
+		values: {
+			"global": {
+				postgresqlEnabled: false
+				redisEnabled: true // TODO(gio): provide redis from outside
+			}
+			fullnameOverride: "penpot"
+			backend: {
+				image: {
+					repository: images.backend.fullName
+					tag: images.backend.tag
+					imagePullPolicy: images.backend.pullPolicy
+				}
+			}
+			frontend: {
+				image: {
+					repository: images.frontend.fullName
+					tag: images.frontend.tag
+					imagePullPolicy: images.frontend.pullPolicy
+				}
+				ingress: {
+					enabled: true
+					className: input.network.ingressClass
+					if input.network.certificateIssuer != "" {
+						annotations: {
+							"acme.cert-manager.io/http01-edit-in-place": "true"
+							"cert-manager.io/cluster-issuer": input.network.certificateIssuer
+						}
+					}
+					hosts: [_domain]
+					tls: [{
+						hosts: [_domain]
+						secretName: "cert-\(_domain)"
+					}]
+				}
+			}
+			persistence: enabled: true
+			config: {
+				publicURI: _domain
+				flags: "enable-login-with-oidc enable-registration enable-insecure-register disable-demo-users disable-demo-warning" // TODO(gio): remove enable-insecure-register?
+				postgresql: {
+					host: "postgres.\(release.namespace).svc.cluster.local"
+					database: "penpot"
+					username: "penpot"
+					password: "penpot"
+				}
+				redis: host: "penpot-redis-headless.\(release.namespace).svc.cluster.local"
+				providers: {
+					oidc: {
+						enabled: true
+						baseURI: "https://hydra.\(global.domain)"
+						clientID: ""
+						clientSecret: ""
+						authURI: ""
+						tokenURI: ""
+						userURI: ""
+						roles: ""
+						rolesAttribute: ""
+						scopes: ""
+						nameAttribute: "name"
+						emailAttribute: "email"
+					}
+					existingSecret: _oauth2SecretName
+					secretKeys: {
+						oidcClientIDKey: "client_id"
+						oidcClientSecretKey: "client_secret"
+					}
+				}
+			}
+			exporter: {
+				image: {
+					repository: images.exporter.fullName
+					tag: images.exporter.tag
+					imagePullPolicy: images.exporter.pullPolicy
+				}
+			}
+			redis: image: tag: "7.0.8-debian-11-r16"
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/penpot.jsonschema b/core/installer/values-tmpl/penpot.jsonschema
deleted file mode 100644
index 0824944..0000000
--- a/core/installer/values-tmpl/penpot.jsonschema
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Network": { "type": "string", "default": "Public", "role": "network" },
-    "Subdomain": { "type": "string", "default": "penpot" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/penpot.md b/core/installer/values-tmpl/penpot.md
deleted file mode 100644
index d750a90..0000000
--- a/core/installer/values-tmpl/penpot.md
+++ /dev/null
@@ -1 +0,0 @@
-Penpot application will be installed on {{ .Values.Network.Name }} network and be accessible to any user on https://{{ .Values.Subdomain }}.{{ .Values.Network.Domain }}
diff --git a/core/installer/values-tmpl/penpot.yaml b/core/installer/values-tmpl/penpot.yaml
deleted file mode 100644
index ae67f39..0000000
--- a/core/installer/values-tmpl/penpot.yaml
+++ /dev/null
@@ -1,96 +0,0 @@
-apiVersion: hydra.ory.sh/v1alpha1
-kind: OAuth2Client
-metadata:
-  name: penpot
-  namespace: {{ .Release.Namespace }}
-spec:
-  grantTypes:
-  - authorization_code
-  responseTypes:
-  - code
-  scope: "openid profile email"
-  secretName: oauth2-credentials # TODO(gio): config
-  redirectUris:
-  - https://{{ .Values.Subdomain }}.{{ .Values.Network.Domain }}/api/auth/oauth/oidc/callback # TODO
-  hydraAdmin:
-    endpoint: /admin/clients
-    forwardedProto: https
-    port: 80
-    url: http://hydra-admin.esrt-core-auth.svc.cluster.local
-  tokenEndpointAuthMethod: client_secret_post
----
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: penpot
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/penpot
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    global:
-      postgresqlEnabled: true
-      redisEnabled: true
-    fullnameOverride: penpot
-    frontend:
-      ingress:
-        enabled: true
-        className: {{ .Values.Network.IngressClass }}
-        {{ if .Values.Network.CertificateIssuer }}
-        annotations:
-          acme.cert-manager.io/http01-edit-in-place: "true"
-          cert-manager.io/cluster-issuer: "{{ .Values.Network.CertificateIssuer }}"
-        {{ end }}
-        hosts:
-        - "{{ .Values.Subdomain }}.{{ .Values.Network.Domain }}"
-        tls:
-        - hosts:
-          - "{{ .Values.Subdomain }}.{{ .Values.Network.Domain }}"
-          secretName: cert-{{ .Values.Subdomain }}.{{ .Values.Network.Domain }}
-    persistence:
-      enabled: true
-    config:
-      publicURI: https://{{ .Values.Subdomain }}.{{ .Values.Network.Domain }}
-      # flags: "enable-registration enable-login"
-      flags: "enable-login-with-oidc enable-registration enable-insecure-register disable-demo-users disable-demo-warning" # TODO(gio): remove enable-insecure-register?
-      postgresql:
-        host: penpot-postgresql.{{ .Release.Namespace }}.svc.cluster.local
-        database: penpot
-        username: penpot
-        password: penpot
-      redis:
-        host: penpot-redis-headless.{{ .Release.Namespace }}.svc.cluster.local
-      providers:
-        oidc:
-          enabled: true
-          baseURI: https://hydra.{{ .Global.Domain }}
-          clientID: ""
-          clientSecret: ""
-          authURI: ""
-          tokenURI: ""
-          userURI: ""
-          roles: ""
-          rolesAttribute: ""
-          scopes: ""
-          nameAttribute: "name"
-          emailAttribute: "email"
-        existingSecret: oauth2-credentials
-        secretKeys:
-          oidcClientIDKey: client_id
-          oidcClientSecretKey: client_secret
-    redis:
-      image:
-        tag: 7.0.8-debian-11-r16
-    postgresql:
-      image:
-        tag: 15.3.0-debian-11-r16
-      auth:
-        username: penpot
-        password: penpot
-        database: penpot
diff --git a/core/installer/values-tmpl/pihole.cue b/core/installer/values-tmpl/pihole.cue
new file mode 100644
index 0000000..de4529c
--- /dev/null
+++ b/core/installer/values-tmpl/pihole.cue
@@ -0,0 +1,90 @@
+input: {
+	network: #Network
+	subdomain: string
+}
+
+_domain: "\(input.subdomain).\(input.network.domain)"
+
+readme: "Installs pihole at https://\(_domain)"
+
+images: {
+	pihole: {
+		repository: "pihole"
+		name: "pihole"
+		tag: "v5.8.1"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	pihole: {
+		chart: "charts/pihole"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	pihole: {
+		chart: charts.pihole
+		values: {
+			domain: _domain
+			pihole: {
+				fullnameOverride: "pihole"
+				persistentVolumeClaim: { // TODO(gio): create volume separately as a dependency
+					enabled: true
+					size: "5Gi"
+				}
+				admin: {
+					enabled: false
+				}
+				ingress: {
+					enabled: false
+				}
+				serviceDhcp: {
+					enabled: false
+				}
+				serviceDns: {
+					type: "ClusterIP"
+				}
+				serviceWeb: {
+					type: "ClusterIP"
+					http: {
+						enabled: true
+					}
+					https: {
+						enabled: false
+					}
+				}
+				virtualHost: _domain
+				resources: {
+					requests: {
+						cpu: "250m"
+						memory: "100M"
+					}
+					limits: {
+						cpu: "500m"
+						memory: "250M"
+					}
+				}
+				image: {
+					repository: images.pihole.fullName
+					tag: images.pihole.tag
+					pullPolicy: images.pihole.pullPolicy
+				}
+			}
+			oauth2: {
+				secretName: "oauth2-secret"
+				configName: "oauth2-proxy"
+				hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc"
+			}
+			hydraPublic: "https://hydra.\(global.domain)"
+			profileUrl: "https://accounts-ui.\(global.domain)"
+			ingressClassName: input.network.ingressClass
+			certificateIssuer: input.network.certificateIssuer
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/pihole.jsonschema b/core/installer/values-tmpl/pihole.jsonschema
deleted file mode 100644
index 942b269..0000000
--- a/core/installer/values-tmpl/pihole.jsonschema
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Subdomain": { "type": "string", "default": "pihole" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/pihole.md b/core/installer/values-tmpl/pihole.md
deleted file mode 100644
index c8c690d..0000000
--- a/core/installer/values-tmpl/pihole.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs pihole at https://{{ .Values.Subdomain }}.{{ .Global.PrivateDomain }}
diff --git a/core/installer/values-tmpl/pihole.yaml b/core/installer/values-tmpl/pihole.yaml
deleted file mode 100644
index 1fe405d..0000000
--- a/core/installer/values-tmpl/pihole.yaml
+++ /dev/null
@@ -1,53 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: pihole
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/pihole
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    domain: {{ .Values.Subdomain}}.{{ .Global.PrivateDomain }}
-    pihole:
-      fullnameOverride: pihole
-      image:
-        repository: "pihole/pihole"
-        tag: v5.8.1
-      persistentVolumeClaim:
-        enabled: true
-        size: 5Gi
-      admin:
-        enabled: false
-      ingress:
-        enabled: false
-      serviceDhcp:
-        enabled: false
-      serviceDns:
-        type: ClusterIP
-      serviceWeb:
-        type: ClusterIP
-        http:
-          enabled: true
-        https:
-          enabled: false
-      virtualHost: {{ .Values.Subdomain }}.{{ .Global.PrivateDomain }}
-      resources:
-        requests:
-          cpu: "250m"
-          memory: "100M"
-        limits:
-          cpu: "500m"
-          memory: "250M"
-    oauth2:
-      secretName: oauth2-secret
-      configName: oauth2-proxy
-      hydraAdmin: http://hydra-admin.{{ .Global.NamespacePrefix }}core-auth.svc
-    hydraPublic: https://hydra.{{ .Global.Domain }}
-    profileUrl: https://accounts-ui.{{ .Global.Domain }}
-    ingressClassName: {{ .Global.Id }}-ingress-private
diff --git a/core/installer/values-tmpl/private-network.cue b/core/installer/values-tmpl/private-network.cue
new file mode 100644
index 0000000..d888f27
--- /dev/null
+++ b/core/installer/values-tmpl/private-network.cue
@@ -0,0 +1,95 @@
+input: {
+	privateNetwork: {
+		hostname: string
+		username: string
+		ipSubnet: string // TODO(gio): use cidr type
+	}
+}
+
+images: {
+	"ingress-nginx": {
+		registry: "registry.k8s.io"
+		repository: "ingress-nginx"
+		name: "controller"
+		tag: "v1.8.0"
+		pullPolicy: "IfNotPresent"
+	}
+	"tailscale-proxy": {
+		repository: "tailscale"
+		name: "tailscale"
+		tag: "v1.42.0"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	"ingress-nginx": {
+		chart: "charts/ingress-nginx"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+	"tailscale-proxy": {
+		chart: "charts/tailscale-proxy"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+}
+
+helm: {
+	"ingress-nginx": {
+		chart: charts["ingress-nginx"]
+		values: {
+			fullnameOverride: "\(global.id)-nginx-private"
+			controller: {
+				service: {
+					enabled: true
+					type: "LoadBalancer"
+					annotations: {
+						"metallb.universe.tf/address-pool": _ingressPrivate
+					}
+				}
+				ingressClassByName: true
+				ingressClassResource: {
+					name: _ingressPrivate
+					enabled: true
+					default: false
+					controllerValue: "k8s.io/\(_ingressPrivate)"
+				}
+				extraArgs: {
+					"default-ssl-certificate": "\(_ingressPrivate)/cert-wildcard.\(global.privateDomain)"
+				}
+				admissionWebhooks: {
+					enabled: false
+				}
+				image: {
+					registry: images["ingress-nginx"].registry
+					image: images["ingress-nginx"].imageName
+					tag: images["ingress-nginx"].tag
+					pullPolicy: images["ingress-nginx"].pullPolicy
+				}
+			}
+		}
+	}
+	"tailscale-proxy": {
+		chart: charts["tailscale-proxy"]
+		values: {
+			hostname: input.privateNetwork.hostname
+			apiServer: "http://headscale-api.\(global.namespacePrefix)app-headscale.svc.cluster.local"
+			loginServer: "https://headscale.\(global.domain)" // TODO(gio): take headscale subdomain from configuration
+			ipSubnet: input.privateNetwork.ipSubnet
+			username: input.privateNetwork.username // TODO(gio): maybe install headscale-user chart separately?
+			preAuthKeySecret: "headscale-preauth-key"
+			image: {
+				repository: images["tailscale-proxy"].fullName
+				tag: images["tailscale-proxy"].tag
+				pullPolicy: images["tailscale-proxy"].pullPolicy
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/private-network.jsonschema b/core/installer/values-tmpl/private-network.jsonschema
deleted file mode 100644
index 7f40a40..0000000
--- a/core/installer/values-tmpl/private-network.jsonschema
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-	"PrivateNetwork": {
-	  "type": "object",
-	  "properties": {
-		"Hostname": { "type": "string", "default": "10.1.0.1" },
-		"Username": { "type": "string", "default": "example" },
-		"IPSubnet": { "type": "string", "default": "10.1.0.1" }
-	  }
-	}
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/private-network.md b/core/installer/values-tmpl/private-network.md
deleted file mode 100644
index 35a878f..0000000
--- a/core/installer/values-tmpl/private-network.md
+++ /dev/null
@@ -1 +0,0 @@
-{{ .Global.PrivateDomain }}
diff --git a/core/installer/values-tmpl/qbittorrent.cue b/core/installer/values-tmpl/qbittorrent.cue
new file mode 100644
index 0000000..b5dbf62
--- /dev/null
+++ b/core/installer/values-tmpl/qbittorrent.cue
@@ -0,0 +1,50 @@
+input: {
+	network: #Network
+	subdomain: string
+}
+
+_domain: "\(input.subdomain).\(input.network.domain)"
+
+readme: "qbittorrent application will be installed on \(input.network.name) network and be accessible to any user on https://\(_domain)"
+
+images: {
+	qbittorrent: {
+		registry: "lscr.io"
+		repository: "linuxserver"
+		name: "qbittorrent"
+		tag: "4.5.3"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	qbittorrent: {
+		chart: "charts/qbittorrent"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	qbittorrent: {
+		chart: charts.qbittorrent
+		values: {
+			pcloudInstanceId: global.id
+			ingress: {
+				className: input.network.ingressClass
+				domain: _domain
+			}
+			webui: port: 8080
+			bittorrent: port: 6881
+			storage: size: "100Gi"
+			image: {
+				repository: images.qbittorrent.fullName
+				tag: images.qbittorrent.tag
+				pullPolicy: images.qbittorrent.pullPolicy
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/qbittorrent.jsonschema b/core/installer/values-tmpl/qbittorrent.jsonschema
deleted file mode 100644
index ec6a2c5..0000000
--- a/core/installer/values-tmpl/qbittorrent.jsonschema
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Values": {
-      "type": "object",
-      "properties": {
-        "NamespacePrefix": { "type": "string" },
-        "Id": { "type": "string" },
-        "Domain": { "type": "string" }
-      },
-      "additionalProperties": false
-    }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/qbittorrent.md b/core/installer/values-tmpl/qbittorrent.md
deleted file mode 100644
index 7abc4d9..0000000
--- a/core/installer/values-tmpl/qbittorrent.md
+++ /dev/null
@@ -1 +0,0 @@
-https://{{ .Values.Subdomain }}.{{ .Global.PrivateDomain }}
diff --git a/core/installer/values-tmpl/qbittorrent.yaml b/core/installer/values-tmpl/qbittorrent.yaml
deleted file mode 100644
index 0597595..0000000
--- a/core/installer/values-tmpl/qbittorrent.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: qbittorrent
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/qbittorrent
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    pcloudInstanceId: {{ .Global.Id }}
-    image:
-      repository: lscr.io/linuxserver/qbittorrent
-      tag: 4.5.3
-      pullPolicy: IfNotPresent
-    ingress:
-      className: {{ .Global.Id }}-ingress-private
-      domain: {{ .Values.Subdomain }}.{{ .Global.PrivateDomain }}
-    webui:
-      port: 8080
-    bittorrent:
-      port: 6881
-    storage:
-      size: 1Ti
diff --git a/core/installer/values-tmpl/resource-renderer-controller.cue b/core/installer/values-tmpl/resource-renderer-controller.cue
new file mode 100644
index 0000000..b65a0ea
--- /dev/null
+++ b/core/installer/values-tmpl/resource-renderer-controller.cue
@@ -0,0 +1,49 @@
+input: {}
+
+images: {
+	resourceRenderer: {
+		repository: "giolekva"
+		name: "resource-renderer-controller"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+	kubeRBACProxy: {
+		registry: "gcr.io"
+		repository: "kubebuilder"
+		name: "kube-rbac-proxy"
+		tag: "v0.13.0"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	resourceRenderer: {
+		chart: "charts/resource-renderer-controller"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.pcloudEnvName
+		}
+	}
+}
+
+helm: {
+	"resource-renderer": {
+		chart: charts.resourceRenderer
+		values: {
+			installCRDs: true
+			image: {
+				repository: images.resourceRenderer.fullName
+				tag: images.resourceRenderer.tag
+				pullPolicy: images.resourceRenderer.pullPolicy
+			}
+			kubeRBACProxy: {
+				image: {
+					repository: images.kubeRBACProxy.fullName
+					tag: images.kubeRBACProxy.tag
+					pullPolicy: images.kubeRBACProxy.pullPolicy
+				}
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/resource-renderer-controller.jsonschema b/core/installer/values-tmpl/resource-renderer-controller.jsonschema
deleted file mode 100644
index f42d895..0000000
--- a/core/installer/values-tmpl/resource-renderer-controller.jsonschema
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/resource-renderer-controller.md b/core/installer/values-tmpl/resource-renderer-controller.md
deleted file mode 100644
index 81231e0..0000000
--- a/core/installer/values-tmpl/resource-renderer-controller.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs resource-renderer controller
diff --git a/core/installer/values-tmpl/resource-renderer-controller.yaml b/core/installer/values-tmpl/resource-renderer-controller.yaml
deleted file mode 100644
index 7ffa9d0..0000000
--- a/core/installer/values-tmpl/resource-renderer-controller.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: rr-controller
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/resource-renderer-controller
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.PCloudEnvName }}
-  interval: 1m0s
-  values:
diff --git a/core/installer/values-tmpl/rpuppy.cue b/core/installer/values-tmpl/rpuppy.cue
index b9d21bc..59f559b 100644
--- a/core/installer/values-tmpl/rpuppy.cue
+++ b/core/installer/values-tmpl/rpuppy.cue
@@ -1,10 +1,8 @@
-#Input: {
+input: {
 	network: #Network
 	subdomain: string
 }
 
-input: #Input
-
 _domain: "\(input.subdomain).\(input.network.domain)"
 
 readme: "rpuppy application will be installed on \(input.network.name) network and be accessible to any user on https://\(_domain)"
@@ -44,87 +42,3 @@
 		}
 	}
 }
-
-// TODO(gio): import
-
-#Network: {
-	name: string
-	ingressClass: string
-	certificateIssuer: string
-	domain: string
-}
-
-#Image: {
-	registry: string | *"docker.io"
-	repository: string
-	name: string
-	tag: string
-	pullPolicy: string // TODO(gio): remove?
-	fullName: "\(registry)/\(repository)/\(name)"
-	fullNameWithTag: "\(fullName):\(tag)"
-}
-
-#Chart: {
-	chart: string
-	sourceRef: #SourceRef
-}
-
-#SourceRef: {
-	kind: "GitRepository" | "HelmRepository"
-	name: string
-	namespace: string // TODO(gio): default global.id
-}
-
-#Global: {
-	id: string
-	...
-}
-
-#Release: {
-	namespace: string
-}
-
-global: #Global
-release: #Release
-
-images: {
-	for key, value in images {
-		"\(key)": #Image & value
-	}
-}
-
-charts: {
-	for key, value in charts {
-		"\(key)": #Chart & value
-	}
-}
-
-#HelmRelease: {
-	_name: string
-	_chart: #Chart
-	_values: _
-
-	apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
-	kind: "HelmRelease"
-	metadata: {
-		name: _name
-   		namespace: release.namespace
-	}
-	spec: {
-		interval: "1m0s"
-		chart: {
-			spec: _chart
-		}
-		values: _values
-	}
-}
-
-output: {
-	for name, r in helm {
-		"\(name)": #HelmRelease & {
-			_name: name
-			_chart: r.chart
-			_values: r.values
-		}
-	}
-}
diff --git a/core/installer/values-tmpl/rpuppy.jsonschema b/core/installer/values-tmpl/rpuppy.jsonschema
deleted file mode 100644
index d6e0d0d..0000000
--- a/core/installer/values-tmpl/rpuppy.jsonschema
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Network": { "type": "string", "default": "Public", "role": "network" },
-    "Subdomain": { "type": "string", "default": "woof" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/rpuppy.md b/core/installer/values-tmpl/rpuppy.md
deleted file mode 100644
index ddd992f..0000000
--- a/core/installer/values-tmpl/rpuppy.md
+++ /dev/null
@@ -1 +0,0 @@
-rpuppy application will be installed on {{ .Values.Network.Name }} network and be accessible to any user on https://{{ .Values.Subdomain }}.{{ .Values.Network.Domain }}
diff --git a/core/installer/values-tmpl/rpuppy.yaml b/core/installer/values-tmpl/rpuppy.yaml
deleted file mode 100644
index 10fac11..0000000
--- a/core/installer/values-tmpl/rpuppy.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: rpuppy
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/rpuppy
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    ingressClassName: {{ .Values.Network.IngressClass }}
-    certificateIssuer: {{ .Values.Network.CertificateIssuer }}
-    domain: {{ .Values.Subdomain }}.{{ .Values.Network.Domain }}
diff --git a/core/installer/values-tmpl/soft-serve.cue b/core/installer/values-tmpl/soft-serve.cue
new file mode 100644
index 0000000..1ed8444
--- /dev/null
+++ b/core/installer/values-tmpl/soft-serve.cue
@@ -0,0 +1,50 @@
+input: {
+	subdomain: string
+	adminKey: string
+}
+
+_domain: "\(input.subdomain).\(global.privateDomain)"
+
+readme: "softserve application will be installed on private network and be accessible to any user on https://\(_domain)" // TODO(gio): make public network an option
+
+images: {
+	softserve: {
+		repository: "charmcli"
+		name: "soft-serve"
+		tag: "v0.7.1"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	softserve: {
+		chart: "charts/soft-serve"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	softserve: {
+		chart: charts.softserve
+		values: {
+			serviceType: "LoadBalancer"
+			reservedIP: ""
+			addressPool: global.id
+			adminKey: input.adminKey
+			privateKey: ""
+			publicKey: ""
+			ingress: {
+				enabled: false
+			}
+			image: {
+				repository: images.softserve.fullName
+				tag: images.softserve.tag
+				pullPolicy: images.softserve.pullPolicy
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/soft-serve.jsonschema b/core/installer/values-tmpl/soft-serve.jsonschema
deleted file mode 100644
index 8142797..0000000
--- a/core/installer/values-tmpl/soft-serve.jsonschema
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Network": { "type": "string", "default": "Public", "role": "network" },
-    "Subdomain": { "type": "string", "default": "softserve" },
-	"AdminKey": { "type": "string" },
-	"SourcePort": { "type": "string", "default": "0" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/soft-serve.md b/core/installer/values-tmpl/soft-serve.md
deleted file mode 100644
index 28e43a8..0000000
--- a/core/installer/values-tmpl/soft-serve.md
+++ /dev/null
@@ -1 +0,0 @@
-Soft-Serve with TCP ingress
diff --git a/core/installer/values-tmpl/soft-serve.yaml b/core/installer/values-tmpl/soft-serve.yaml
deleted file mode 100644
index 1c6fa87..0000000
--- a/core/installer/values-tmpl/soft-serve.yaml
+++ /dev/null
@@ -1,36 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: soft-serve
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/soft-serve
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ or .Values.ChartRepositoryNamespace .Global.Id }}
-  interval: 1m0s
-  values:
-    {{- if .Values.ServiceType }}
-    serviceType: {{ .Values.ServiceType }}
-    {{- end }}
-    reservedIP: ""
-    addressPool: {{ .Global.Id }}
-    adminKey: {{ .Values.AdminKey }}
-    {{- if and .Values.PrivateKey .Values.PublicKey }}
-    privateKey: |
-{{ .Values.PrivateKey | indent 6 }}
-    publicKey: {{ .Values.PublicKey }}
-    {{- end }}
-    {{- if .Values.Network }}
-    ingress:
-      enabled: {{ .Values.Ingress.Enabled }}
-      ingressClassName: {{ .Values.Network.IngressClass }}
-      certificateIssuer: {{ .Values.Network.CertificateIssuer }}
-      domain: {{ .Values.Subdomain }}.{{ .Values.Network.Domain }}
-      {{- if .Values.SourcePort }}
-      sourcePort: {{ .Values.SourcePort }}
-      {{- end }}
-    {{- end }}
diff --git a/core/installer/values-tmpl/tailscale-proxy.yaml b/core/installer/values-tmpl/tailscale-proxy.yaml
deleted file mode 100644
index 047c196..0000000
--- a/core/installer/values-tmpl/tailscale-proxy.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: tailscale-proxy
-  namespace: {{ .Release.Namespace }}
-spec:
-  dependsOn:
-    - name: headscale
-      namespace: {{ .Global.NamespacePrefix }}app-headscale
-  chart:
-    spec:
-      chart: charts/tailscale-proxy
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    hostname: {{ .Values.PrivateNetwork.Hostname}}
-    apiServer: http://headscale-api.{{ .Global.Id }}-app-headscale.svc.cluster.local
-    loginServer: https://headscale.{{ .Global.Domain }} # TODO(gio): take headscale subdomain from configuration
-    ipSubnet: {{ .Values.PrivateNetwork.IPSubnet }}
-    username: {{ .Values.PrivateNetwork.Username }} # TODO(gio): maybe install headscale-user chart separately?
-    preAuthKeySecret: headscale-preauth-key
diff --git a/core/installer/values-tmpl/vaultwarden.cue b/core/installer/values-tmpl/vaultwarden.cue
new file mode 100644
index 0000000..58eb476
--- /dev/null
+++ b/core/installer/values-tmpl/vaultwarden.cue
@@ -0,0 +1,44 @@
+input: {
+	network: #Network
+	subdomain: string
+}
+
+_domain: "\(input.subdomain).\(input.network.domain)"
+
+readme: "Installs vaultwarden on private network accessible at \(_domain)"
+
+images: {
+	vaultwarden: {
+		repository: "vaultwarden"
+		name: "server"
+		tag: "1.28.1"
+		pullPolicy: "IfNotPresent"
+	}
+}
+
+charts: {
+	vaultwarden: {
+		chart: "charts/vaultwarden"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	vaultwarden: {
+		chart: charts.vaultwarden
+		values: {
+			ingressClassName: input.network.ingressClass
+			certificateIssuer: input.network.certificateIssuer
+			domain: _domain
+			image: {
+				repository: images.vaultwarden.fullName
+				tag: images.vaultwarden.tag
+				pullPolicy: images.vaultwarden.pullPolicy
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/vaultwarden.jsonschema b/core/installer/values-tmpl/vaultwarden.jsonschema
deleted file mode 100644
index 88c96cb..0000000
--- a/core/installer/values-tmpl/vaultwarden.jsonschema
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "Subdomain": { "type": "string", "default": "vaultwarden" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/vaultwarden.md b/core/installer/values-tmpl/vaultwarden.md
deleted file mode 100644
index ab9d05d..0000000
--- a/core/installer/values-tmpl/vaultwarden.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs vaultwarden on private network accessible at https://{{ .Values.Subdomain }}.{{ .Global.PrivateDomain }}
diff --git a/core/installer/values-tmpl/vaultwarden.yaml b/core/installer/values-tmpl/vaultwarden.yaml
deleted file mode 100644
index 4419776..0000000
--- a/core/installer/values-tmpl/vaultwarden.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: vaultwarden
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/vaultwarden
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    image:
-      repository: vaultwarden/server
-      tag: 1.28.1
-      pullPolicy: IfNotPresent
-    storage:
-      size: 3Gi
-    domain: {{ .Values.Subdomain }}.{{ .Global.PrivateDomain }}
-    certificateIssuer: {{ .Global.Id }}-private
-    ingressClassName: {{ .Global.Id }}-ingress-private
diff --git a/core/installer/values-tmpl/welcome.cue b/core/installer/values-tmpl/welcome.cue
new file mode 100644
index 0000000..2da8f67
--- /dev/null
+++ b/core/installer/values-tmpl/welcome.cue
@@ -0,0 +1,51 @@
+import (
+	"encoding/base64"
+)
+
+input: {
+	repoAddr: string
+	sshPrivateKey: string
+	createAccountAddr: string
+}
+
+images: {
+	welcome: {
+		repository: "giolekva"
+		name: "pcloud-installer"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
+}
+
+charts: {
+	welcome: {
+		chart: "charts/welcome"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+helm: {
+	welcome: {
+		chart: charts.welcome
+		values: {
+			repoAddr: input.repoAddr
+			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+			createAccountAddr: "http://api.\(global.namespacePrefix)core-auth.svc.cluster.local/identities"
+			ingress: {
+				className: _ingressPublic
+				domain: "welcome.\(global.domain)"
+				certificateIssuer: _issuerPublic
+			}
+			clusterRoleName: "\(global.id)-welcome"
+			image: {
+				repository: images.welcome.fullName
+				tag: images.welcome.tag
+				pullPolicy: images.welcome.pullPolicy
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/welcome.jsonschema b/core/installer/values-tmpl/welcome.jsonschema
deleted file mode 100644
index 8a011ca..0000000
--- a/core/installer/values-tmpl/welcome.jsonschema
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "type": "object",
-  "properties": {
-    "RepoAddr": { "type": "string", "default": "ssh://192.168.0.11/example" },
-	"SSHPrivateKey": { "type": "string", "default": "foo bar" }
-  },
-  "additionalProperties": false
-}
diff --git a/core/installer/values-tmpl/welcome.md b/core/installer/values-tmpl/welcome.md
deleted file mode 100644
index 43d5021..0000000
--- a/core/installer/values-tmpl/welcome.md
+++ /dev/null
@@ -1 +0,0 @@
-Installs welcome service
diff --git a/core/installer/values-tmpl/welcome.yaml b/core/installer/values-tmpl/welcome.yaml
deleted file mode 100644
index d871c90..0000000
--- a/core/installer/values-tmpl/welcome.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
-  name: welcome
-  namespace: {{ .Release.Namespace }}
-spec:
-  chart:
-    spec:
-      chart: charts/welcome
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    repoAddr: {{ .Values.RepoAddr }}
-    sshPrivateKey: {{ .Values.SSHPrivateKey | b64enc }}
-    createAccountAddr: http://api.{{ .Global.NamespacePrefix}}core-auth.svc.cluster.local/identities
-    ingress:
-      className: {{ .Global.PCloudEnvName }}-ingress-public
-      domain: welcome.{{ .Global.Domain }}
-      certificateIssuer: {{ .Global.Id }}-public
-    clusterRoleName: {{ .Global.Id }}-welcome
diff --git a/core/installer/welcome/appmanager-tmpl/app.html b/core/installer/welcome/appmanager-tmpl/app.html
index 8d844a8..4fa4766 100644
--- a/core/installer/welcome/appmanager-tmpl/app.html
+++ b/core/installer/welcome/appmanager-tmpl/app.html
@@ -3,7 +3,17 @@
   {{ $networks := .AvailableNetworks }}
   {{ $data := .Data }}
   {{ range $name, $schema := .Schema.Fields }}
-    {{ if or (eq $schema.Kind 0) (eq $schema.Kind 1) }}
+    {{ if eq $schema.Kind 0 }}
+      <label for="{{ $name }}">
+        <span>{{ $name }}</span>
+      </label>
+	  <input type="checkbox" role="swtich" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} {{ if index $data $name }}checked{{ end }}/>
+    {{ else if eq $schema.Kind 1 }}
+      <label for="{{ $name }}">
+        <span>{{ $name }}</span>
+      </label>
+	  <input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}"/>
+    {{ else if eq $schema.Kind 4 }}
       <label for="{{ $name }}">
         <span>{{ $name }}</span>
       </label>
diff --git a/core/installer/welcome/welcome.go b/core/installer/welcome/welcome.go
index 0f3fa98..bcadea4 100644
--- a/core/installer/welcome/welcome.go
+++ b/core/installer/welcome/welcome.go
@@ -153,9 +153,9 @@
 				return
 			}
 			if err := appManager.Install(*app, nsGen, suffixGen, map[string]any{
-				"Username": req.Username,
-				"PreAuthKey": map[string]any{
-					"Enabled": false,
+				"username": req.Username,
+				"preAuthKey": map[string]any{
+					"enabled": false,
 				},
 			}); err != nil {
 				http.Error(w, err.Error(), http.StatusInternalServerError)