AppManager: cache helm charts and container images to local registry

Caching container images is disabled until we figure out how to run
container registry behind TLS.

Change-Id: I0253f2a862e5adddff18a82b102f67258151c070
diff --git a/core/installer/app.go b/core/installer/app.go
index 2a16c1e..86447fe 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -15,343 +15,32 @@
 	"cuelang.org/go/cue/cuecontext"
 	"cuelang.org/go/cue/load"
 	cueyaml "cuelang.org/go/encoding/yaml"
+	helmv2 "github.com/fluxcd/helm-controller/api/v2"
 )
 
-//go:embed pcloud_app.cue
-var DodoAppCue []byte
+//go:embed app_configs/dodo_app.cue
+var dodoAppCue []byte
 
-// TODO(gio): import
-const cueEnvAppGlobal = `
-import (
-    "net"
-)
+//go:embed app_configs/app_base.cue
+var cueBaseConfig []byte
 
-#Global: {
-	id: string | *""
-	pcloudEnvName: string | *""
-	domain: string | *""
-    privateDomain: string | *""
-    contactEmail: string | *""
-    adminPublicKey: string | *""
-    publicIP: [...string] | *[]
-    nameserverIP: [...string] | *[]
-	namespacePrefix: string | *""
-	network: #EnvNetwork
-}
+//go:embed app_configs/app_global_env.cue
+var cueEnvAppGlobal []byte
 
-#EnvNetwork: {
-	dns: net.IPv4
-	dnsInClusterIP: net.IPv4
-	ingress: net.IPv4
-	headscale: net.IPv4
-	servicesFrom: net.IPv4
-	servicesTo: net.IPv4
-}
-
-// TODO(gio): remove
-ingressPrivate: "\(global.id)-ingress-private"
-ingressPublic: "\(global.pcloudEnvName)-ingress-public"
-issuerPrivate: "\(global.id)-private"
-issuerPublic: "\(global.id)-public"
-
-#Ingress: {
-	auth: #Auth
-	network: #Network
-	subdomain: string
-	service: close({
-		name: string
-		port: close({ name: string }) | close({ number: int & > 0 })
-	})
-
-	_domain: "\(subdomain).\(network.domain)"
-    _authProxyHTTPPortName: "http"
-
-	out: {
-		images: {
-			authProxy: #Image & {
-				repository: "giolekva"
-				name: "auth-proxy"
-				tag: "latest"
-				pullPolicy: "Always"
-			}
-		}
-		charts: {
-			ingress: #Chart & {
-				chart: "charts/ingress"
-				sourceRef: {
-					kind: "GitRepository"
-					name: "pcloud"
-					namespace: global.id
-				}
-			}
-			authProxy: #Chart & {
-				chart: "charts/auth-proxy"
-				sourceRef: {
-					kind: "GitRepository"
-					name: "pcloud"
-					namespace: global.id
-				}
-			}
-		}
-		helm: {
-			if auth.enabled {
-				"auth-proxy": {
-					chart: charts.authProxy
-					values: {
-						image: {
-							repository: images.authProxy.fullName
-							tag: images.authProxy.tag
-							pullPolicy: images.authProxy.pullPolicy
-						}
-						upstream: "\(service.name).\(release.namespace).svc.cluster.local"
-						whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
-						loginAddr: "https://accounts-ui.\(global.domain)/login"
-						membershipAddr: "http://memberships-api.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
-						groups: auth.groups
-						portName: _authProxyHTTPPortName
-					}
-				}
-			}
-			ingress: {
-				chart: charts.ingress
-				_service: service
-				values: {
-					domain: _domain
-					ingressClassName: network.ingressClass
-					certificateIssuer: network.certificateIssuer
-					service: {
-						if auth.enabled {
-							name: "auth-proxy"
-                            port: name: _authProxyHTTPPortName
-						}
-						if !auth.enabled {
-							name: _service.name
-							if _service.port.name != _|_ {
-								port: name: _service.port.name
-							}
-							if _service.port.number != _|_ {
-								port: number: _service.port.number
-							}
-						}
-					}
-				}
-			}
-		}
-	}
-}
-
-ingress: {}
-
-_ingressValidate: {
-	for key, value in ingress {
-		"\(key)": #Ingress & value
-	}
-}
-`
-
-const cueInfraAppGlobal = `
-#Global: {
-	pcloudEnvName: string | *""
-    publicIP: [...string] | *[]
-	namespacePrefix: string | *""
-    infraAdminPublicKey: string | *""
-}
-
-// TODO(gio): remove
-ingressPublic: "\(global.pcloudEnvName)-ingress-public"
-
-ingress: {}
-_ingressValidate: {}
-`
-
-const cueBaseConfig = `
-name: string | *""
-description: string | *""
-readme: string | *""
-icon: string | *""
-namespace: string | *""
-
-help: [...#HelpDocument] | *[]
-
-#HelpDocument: {
-	title: string
-	contents: string
-	children: [...#HelpDocument] | *[]
-}
-
-url: string | *""
-
-#AppType: "infra" | "env"
-appType: #AppType | *"env"
-
-#Release: {
-	appInstanceId: string
-	namespace: string
-	repoAddr: string
-	appDir: string
-}
-
-#Network: {
-	name: string
-	ingressClass: string
-	certificateIssuer: string | *""
-	domain: string
-	allocatePortAddr: string
-}
-
-#Auth: {
-  enabled: bool | *false // TODO(gio): enabled by default?
-  groups: string | *"" // TODO(gio): []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
-}
-
-#PortForward: {
-	allocator: string
-	protocol: "TCP" | "UDP" | *"TCP"
-	sourcePort: int
-	targetService: string
-	targetPort: int
-}
-
-portForward: [...#PortForward] | *[]
-
-global: #Global
-release: #Release
-
-images: {
-	for key, value in images {
-		"\(key)": #Image & value
-	}
-    for _, value in _ingressValidate {
-        for name, image in value.out.images {
-            "\(name)": #Image & image
-        }
-    }
-}
-
-charts: {
-	for key, value in charts {
-		"\(key)": #Chart & value
-	}
-    for _, value in _ingressValidate {
-        for name, chart in value.out.charts {
-            "\(name)": #Chart & chart
-        }
-    }
-}
-
-#ResourceReference: {
-    name: string
-    namespace: string
-}
-
-#Helm: {
-	name: string
-	dependsOn: [...#ResourceReference] | *[]
-	...
-}
-
-_helmValidate: {
-	for key, value in helm {
-		"\(key)": #Helm & value & {
-			name: key
-		}
-	}
-	for key, value in _ingressValidate {
-		for ing, ingValue in value.out.helm {
-            // TODO(gio): support multiple ingresses
-			// "\(key)-\(ing)": #Helm & ingValue & {
-			"\(ing)": #Helm & ingValue & {
-				// name: "\(key)-\(ing)"
-				name: ing
-			}
-		}
-	}
-}
-
-resources: {}
-
-#HelmRelease: {
-	_name: string
-	_chart: #Chart
-	_values: _
-	_dependencies: [...#ResourceReference] | *[]
-
-	apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
-	kind: "HelmRelease"
-	metadata: {
-		name: _name
-   		namespace: release.namespace
-	}
-	spec: {
-		interval: "1m0s"
-		dependsOn: _dependencies
-		chart: {
-			spec: _chart
-		}
-		values: _values
-	}
-}
-
-output: {
-	for name, r in _helmValidate {
-		"\(name)": #HelmRelease & {
-			_name: name
-			_chart: r.chart
-			_values: r.values
-			_dependencies: r.dependsOn
-		}
-	}
-}
-
-#SSHKey: {
-	public: string
-	private: string
-}
-
-#HelpDocument: {
-    title: string
-    contents: string
-    children: [...#HelpDocument]
-}
-
-help: [...#HelpDocument] | *[]
-
-url: string | *""
-
-networks: {}
-`
+//go:embed app_configs/app_global_infra.cue
+var cueInfraAppGlobal []byte
 
 type rendered struct {
-	Name      string
-	Readme    string
-	Resources CueAppData
-	Ports     []PortForward
-	Data      CueAppData
-	URL       string
-	Help      []HelpDocument
-	Icon      string
+	Name            string
+	Readme          string
+	Resources       CueAppData
+	HelmCharts      HelmCharts
+	ContainerImages map[string]ContainerImage
+	Ports           []PortForward
+	Data            CueAppData
+	URL             string
+	Help            []HelpDocument
+	Icon            string
 }
 
 type HelpDocument struct {
@@ -360,6 +49,27 @@
 	Children []HelpDocument
 }
 
+type ContainerImage struct {
+	Registry   string `json:"registry"`
+	Repository string `json:"repository"`
+	Name       string `json:"name"`
+	Tag        string `json:"tag"`
+}
+
+type helmChartRef struct {
+	Kind string `json:"kind"`
+}
+
+type HelmCharts struct {
+	Git map[string]HelmChartGitRepo
+}
+
+type HelmChartGitRepo struct {
+	Address string `json:"address"`
+	Branch  string `json:"branch"`
+	Path    string `json:"path"`
+}
+
 type EnvAppRendered struct {
 	rendered
 	Config AppInstanceConfig
@@ -404,7 +114,7 @@
 
 type InfraApp interface {
 	App
-	Render(release Release, infra InfraConfig, values map[string]any) (InfraAppRendered, error)
+	Render(release Release, infra InfraConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error)
 }
 
 type EnvNetwork struct {
@@ -459,7 +169,6 @@
 	}, nil
 }
 
-// TODO(gio): rename to EnvConfig
 type EnvConfig struct {
 	Id              string     `json:"id,omitempty"`
 	InfraName       string     `json:"pcloudEnvName,omitempty"`
@@ -475,7 +184,7 @@
 
 type EnvApp interface {
 	App
-	Render(release Release, env EnvConfig, values map[string]any) (EnvAppRendered, error)
+	Render(release Release, env EnvConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (EnvAppRendered, error)
 }
 
 type cueApp struct {
@@ -583,8 +292,12 @@
 	ret := rendered{
 		Name:      a.Slug(),
 		Resources: make(CueAppData),
-		Ports:     make([]PortForward, 0),
-		Data:      a.data,
+		HelmCharts: HelmCharts{
+			Git: make(map[string]HelmChartGitRepo),
+		},
+		ContainerImages: make(map[string]ContainerImage),
+		Ports:           make([]PortForward, 0),
+		Data:            a.data,
 	}
 	var buf bytes.Buffer
 	if err := json.NewEncoder(&buf).Encode(values); err != nil {
@@ -613,6 +326,40 @@
 		return rendered{}, err
 	}
 	{
+		charts := res.LookupPath(cue.ParsePath("charts"))
+		i, err := charts.Fields()
+		if err != nil {
+			return rendered{}, err
+		}
+		for i.Next() {
+			var chartRef helmChartRef
+			if err := i.Value().Decode(&chartRef); err != nil {
+				return rendered{}, err
+			}
+			if chartRef.Kind == "GitRepository" {
+				var chart HelmChartGitRepo
+				if err := i.Value().Decode(&chart); err != nil {
+					return rendered{}, err
+				}
+				ret.HelmCharts.Git[cleanName(i.Selector().String())] = chart
+			}
+		}
+	}
+	{
+		images := res.LookupPath(cue.ParsePath("images"))
+		i, err := images.Fields()
+		if err != nil {
+			return rendered{}, err
+		}
+		for i.Next() {
+			var img ContainerImage
+			if err := i.Value().Decode(&img); err != nil {
+				return rendered{}, err
+			}
+			ret.ContainerImages[cleanName(i.Selector().String())] = img
+		}
+	}
+	{
 		output := res.LookupPath(cue.ParsePath("output"))
 		i, err := output.Fields()
 		if err != nil {
@@ -677,7 +424,7 @@
 	return NewCueEnvApp(CueAppData{
 		"app.cue":        appCfg,
 		"base.cue":       []byte(cueBaseConfig),
-		"pcloud_app.cue": DodoAppCue,
+		"pcloud_app.cue": dodoAppCue,
 		"env_app.cue":    []byte(cueEnvAppGlobal),
 	})
 }
@@ -686,17 +433,21 @@
 	return AppTypeEnv
 }
 
-func (a cueEnvApp) Render(release Release, env EnvConfig, values map[string]any) (EnvAppRendered, error) {
+func (a cueEnvApp) Render(release Release, env EnvConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (EnvAppRendered, error) {
 	networks := CreateNetworks(env)
 	derived, err := deriveValues(values, a.Schema(), networks)
 	if err != nil {
 		return EnvAppRendered{}, nil
 	}
+	if charts == nil {
+		charts = make(map[string]helmv2.HelmChartTemplateSpec)
+	}
 	ret, err := a.cueApp.render(map[string]any{
-		"global":   env,
-		"release":  release,
-		"input":    derived,
-		"networks": networkMap(networks),
+		"global":      env,
+		"release":     release,
+		"input":       derived,
+		"localCharts": charts,
+		"networks":    networkMap(networks),
 	})
 	if err != nil {
 		return EnvAppRendered{}, err
@@ -732,11 +483,15 @@
 	return AppTypeInfra
 }
 
-func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any) (InfraAppRendered, error) {
+func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error) {
+	if charts == nil {
+		charts = make(map[string]helmv2.HelmChartTemplateSpec)
+	}
 	ret, err := a.cueApp.render(map[string]any{
-		"global":  infra,
-		"release": release,
-		"input":   values,
+		"global":      infra,
+		"release":     release,
+		"input":       values,
+		"localCharts": charts,
 	})
 	if err != nil {
 		return InfraAppRendered{}, err