Installer: Separate infrastructure and environment apps.

Have two separate application managers, one for installing apps on the
dodo infra, and nother installing on individual environments.

Change-Id: I1b24f008e30c5533c48c22ea92328bc4bb7abc54
diff --git a/core/installer/app.go b/core/installer/app.go
index 35c6d11..18f9e75 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -1,67 +1,17 @@
 package installer
 
 import (
-	"archive/tar"
 	"bytes"
-	"compress/gzip"
-	"embed"
 	"encoding/json"
 	"fmt"
 	template "html/template"
-	"io"
-	"log"
-	"net/http"
+	"net"
 	"strings"
 
 	"cuelang.org/go/cue"
-	"cuelang.org/go/cue/cuecontext"
 	cueyaml "cuelang.org/go/encoding/yaml"
-	"github.com/go-git/go-billy/v5"
-	"sigs.k8s.io/yaml"
 )
 
-//go:embed values-tmpl
-var valuesTmpls embed.FS
-
-var storeAppConfigs = []string{
-	"values-tmpl/jellyfin.cue",
-	// "values-tmpl/maddy.cue",
-	"values-tmpl/matrix.cue",
-	"values-tmpl/penpot.cue",
-	"values-tmpl/pihole.cue",
-	"values-tmpl/qbittorrent.cue",
-	"values-tmpl/rpuppy.cue",
-	"values-tmpl/soft-serve.cue",
-	"values-tmpl/vaultwarden.cue",
-	"values-tmpl/url-shortener.cue",
-	"values-tmpl/gerrit.cue",
-	"values-tmpl/jenkins.cue",
-	"values-tmpl/zot.cue",
-}
-
-var infraAppConfigs = []string{
-	"values-tmpl/appmanager.cue",
-	"values-tmpl/cert-manager.cue",
-	"values-tmpl/certificate-issuer-private.cue",
-	"values-tmpl/certificate-issuer-public.cue",
-	"values-tmpl/config-repo.cue",
-	"values-tmpl/core-auth.cue",
-	"values-tmpl/csi-driver-smb.cue",
-	"values-tmpl/dns-zone-manager.cue",
-	"values-tmpl/env-manager.cue",
-	"values-tmpl/fluxcd-reconciler.cue",
-	"values-tmpl/headscale-controller.cue",
-	"values-tmpl/headscale-user.cue",
-	"values-tmpl/headscale.cue",
-	"values-tmpl/ingress-public.cue",
-	"values-tmpl/metallb-ipaddresspool.cue",
-	"values-tmpl/private-network.cue",
-	"values-tmpl/resource-renderer-controller.cue",
-	"values-tmpl/welcome.cue",
-	"values-tmpl/memberships.cue",
-	"values-tmpl/hydra-maester.cue",
-}
-
 // TODO(gio): import
 const cueBaseConfig = `
 name: string | *""
@@ -131,6 +81,7 @@
 }
 
 #Release: {
+	appInstanceId: string
 	namespace: string
 	repoAddr: string
 	appDir: string
@@ -309,27 +260,64 @@
 }
 `
 
-type appConfig struct {
-	Name        string        `json:"name"`
-	Version     string        `json:"version"`
-	Description string        `json:"description"`
-	Namespaces  []string      `json:"namespaces"`
-	Icon        template.HTML `json:"icon"`
-}
-
 type Rendered struct {
+	Name      string
 	Readme    string
 	Resources map[string][]byte
 	Ports     []PortForward
+	Config    AppInstanceConfig
 }
 
+type PortForward struct {
+	Allocator     string `json:"allocator"`
+	Protocol      string `json:"protocol"`
+	SourcePort    int    `json:"sourcePort"`
+	TargetService string `json:"targetService"`
+	TargetPort    int    `json:"targetPort"`
+}
+
+type AppType int
+
+const (
+	AppTypeInfra AppType = iota
+	AppTypeEnv
+)
+
 type App interface {
+	Type() AppType
 	Name() string
 	Description() string
 	Icon() template.HTML
 	Schema() Schema
 	Namespace() string
-	Render(derived Derived) (Rendered, error)
+}
+
+type InfraConfig struct {
+	Name                 string   `json:"pcloudEnvName"` // #TODO(gio): change to name
+	PublicIP             []net.IP `json:"publicIP"`
+	InfraNamespacePrefix string   `json:"namespacePrefix"`
+	InfraAdminPublicKey  []byte   `json:"infraAdminPublicKey"`
+}
+
+type InfraApp interface {
+	App
+	Render(release Release, infra InfraConfig, values map[string]any) (Rendered, error)
+}
+
+// TODO(gio): rename to EnvConfig
+type AppEnvConfig struct {
+	Id              string   `json:"id"`
+	InfraName       string   `json:"pcloudEnvName"`
+	Domain          string   `json:"domain"`
+	PrivateDomain   string   `json:"privateDomain"`
+	ContactEmail    string   `json:"contactEmail"`
+	PublicIP        []net.IP `json:"publicIP"`
+	NamespacePrefix string   `json:"namespacePrefix"`
+}
+
+type EnvApp interface {
+	App
+	Render(release Release, env AppEnvConfig, values map[string]any) (Rendered, error)
 }
 
 type cueApp struct {
@@ -341,18 +329,16 @@
 	cfg         *cue.Value
 }
 
-type cueAppConfig struct {
-	Name        string `json:"name"`
-	Namespace   string `json:"namespace"`
-	Description string `json:"description"`
-	Icon        string `json:"icon"`
-}
-
 func newCueApp(config *cue.Value) (cueApp, error) {
 	if config == nil {
 		return cueApp{}, fmt.Errorf("config not provided")
 	}
-	var cfg cueAppConfig
+	cfg := struct {
+		Name        string `json:"name"`
+		Namespace   string `json:"namespace"`
+		Description string `json:"description"`
+		Icon        string `json:"icon"`
+	}{}
 	if err := config.Decode(&cfg); err != nil {
 		return cueApp{}, err
 	}
@@ -390,21 +376,14 @@
 	return a.namespace
 }
 
-type PortForward struct {
-	Allocator     string `json:"allocator"`
-	Protocol      string `json:"protocol"`
-	SourcePort    int    `json:"sourcePort"`
-	TargetService string `json:"targetService"`
-	TargetPort    int    `json:"targetPort"`
-}
-
-func (a cueApp) Render(derived Derived) (Rendered, error) {
+func (a cueApp) render(values map[string]any) (Rendered, error) {
 	ret := Rendered{
+		Name:      a.Name(),
 		Resources: make(map[string][]byte),
 		Ports:     make([]PortForward, 0),
 	}
 	var buf bytes.Buffer
-	if err := json.NewEncoder(&buf).Encode(derived); err != nil {
+	if err := json.NewEncoder(&buf).Encode(values); err != nil {
 		return Rendered{}, err
 	}
 	ctx := a.cfg.Context()
@@ -440,254 +419,70 @@
 	return ret, nil
 }
 
-type AppRepository interface {
-	GetAll() ([]App, error)
-	Find(name string) (App, error)
+type cueEnvApp struct {
+	cueApp
 }
 
-type InMemoryAppRepository struct {
-	apps []App
-}
-
-func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
-	return InMemoryAppRepository{apps}
-}
-
-func (r InMemoryAppRepository) Find(name string) (App, error) {
-	for _, a := range r.apps {
-		if a.Name() == name {
-			return a, nil
-		}
-	}
-	return nil, fmt.Errorf("Application not found: %s", name)
-}
-
-func (r InMemoryAppRepository) GetAll() ([]App, error) {
-	return r.apps, nil
-}
-
-func CreateAllApps() []App {
-	return append(
-		createApps(infraAppConfigs),
-		CreateStoreApps()...,
-	)
-}
-
-func CreateStoreApps() []App {
-	return createApps(storeAppConfigs)
-}
-
-func createApps(configs []string) []App {
-	ret := make([]App, 0)
-	for _, cfgFile := range configs {
-		cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
-		if err != nil {
-			panic(err)
-		}
-		if app, err := newCueApp(cfg); err != nil {
-			panic(err)
-		} else {
-			ret = append(ret, app)
-		}
-	}
-	return ret
-}
-
-// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
-// 	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
-// 	if err != nil {
-// 		panic(err)
-// 	}
-// 	return StoreApp{
-// 		App{
-// 			"maddy",
-// 			[]string{"app-maddy"},
-// 			[]*template.Template{
-// 				tmpls.Lookup("maddy.yaml"),
-// 			},
-// 			schema,
-// 			nil,
-// 			nil,
-// 		},
-// 		`<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="M9.5 13c13.687 13.574 14.825 13.09 29 0"/><rect width="37" height="31" x="5.5" y="8.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" rx="2"/></svg>`,
-// 		"SMPT/IMAP server to communicate via email.",
-// 	}
-// }
-
-type httpAppRepository struct {
-	apps []App
-}
-
-type appVersion struct {
-	Version string   `json:"version"`
-	Urls    []string `json:"urls"`
-}
-
-type allAppsResp struct {
-	ApiVersion string                  `json:"apiVersion"`
-	Entries    map[string][]appVersion `json:"entries"`
-}
-
-func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
-	resp, err := http.Get(addr)
-	if err != nil {
-		return err
-	}
-	b, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return err
-	}
-	var apps allAppsResp
-	if err := yaml.Unmarshal(b, &apps); err != nil {
-		return err
-	}
-	for name, conf := range apps.Entries {
-		for _, version := range conf {
-			resp, err := http.Get(version.Urls[0])
-			if err != nil {
-				return err
-			}
-			nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
-			if err := fs.MkdirAll(nameVersion, 0700); err != nil {
-				return err
-			}
-			sub, err := fs.Chroot(nameVersion)
-			if err != nil {
-				return err
-			}
-			if err := extractApp(resp.Body, sub); err != nil {
-				return err
-			}
-		}
-	}
-	return nil
-}
-
-func extractApp(archive io.Reader, fs billy.Filesystem) error {
-	uncompressed, err := gzip.NewReader(archive)
-	if err != nil {
-		return err
-	}
-	tarReader := tar.NewReader(uncompressed)
-	for true {
-		header, err := tarReader.Next()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return err
-		}
-		switch header.Typeflag {
-		case tar.TypeDir:
-			if err := fs.MkdirAll(header.Name, 0755); err != nil {
-				return err
-			}
-		case tar.TypeReg:
-			out, err := fs.Create(header.Name)
-			if err != nil {
-				return err
-			}
-			defer out.Close()
-			if _, err := io.Copy(out, tarReader); err != nil {
-				return err
-			}
-		default:
-			return fmt.Errorf("Uknown type: %s", header.Name)
-		}
-	}
-	return nil
-}
-
-type fsAppRepository struct {
-	InMemoryAppRepository
-	fs billy.Filesystem
-}
-
-func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
-	all, err := fs.ReadDir(".")
+func NewCueEnvApp(config *cue.Value) (EnvApp, error) {
+	app, err := newCueApp(config)
 	if err != nil {
 		return nil, err
 	}
-	apps := make([]App, 0)
-	for _, e := range all {
-		if !e.IsDir() {
-			continue
-		}
-		appFS, err := fs.Chroot(e.Name())
-		if err != nil {
-			return nil, err
-		}
-		app, err := loadApp(appFS)
-		if err != nil {
-			log.Printf("Ignoring directory %s: %s", e.Name(), err)
-			continue
-		}
-		apps = append(apps, app)
-	}
-	return &fsAppRepository{
-		NewInMemoryAppRepository(apps),
-		fs,
-	}, nil
+	return cueEnvApp{app}, nil
 }
 
-func loadApp(fs billy.Filesystem) (App, error) {
-	items, err := fs.ReadDir(".")
+func (a cueEnvApp) Type() AppType {
+	return AppTypeEnv
+}
+
+func (a cueEnvApp) Render(release Release, env AppEnvConfig, values map[string]any) (Rendered, error) {
+	networks := CreateNetworks(env)
+	derived, err := deriveValues(values, a.Schema(), networks)
+	if err != nil {
+		return Rendered{}, nil
+	}
+	ret, err := a.cueApp.render(map[string]any{
+		"global":  env,
+		"release": release,
+		"input":   derived,
+	})
+	if err != nil {
+		return Rendered{}, err
+	}
+	ret.Config = AppInstanceConfig{
+		AppId:   a.Name(),
+		Env:     env,
+		Release: release,
+		Values:  values,
+		Input:   derived,
+	}
+	return ret, nil
+}
+
+type cueInfraApp struct {
+	cueApp
+}
+
+func NewCueInfraApp(config *cue.Value) (InfraApp, error) {
+	app, err := newCueApp(config)
 	if err != nil {
 		return nil, err
 	}
-	var contents bytes.Buffer
-	for _, i := range items {
-		if i.IsDir() {
-			continue
-		}
-		f, err := fs.Open(i.Name())
-		if err != nil {
-			return nil, err
-		}
-		defer f.Close()
-		if _, err := io.Copy(&contents, f); err != nil {
-			return nil, err
-		}
-	}
-	cfg, err := processCueConfig(contents.String())
-	if err != nil {
-		return nil, err
-	}
-	return newCueApp(cfg)
+	return cueInfraApp{app}, nil
+}
+
+func (a cueInfraApp) Type() AppType {
+	return AppTypeInfra
+}
+
+func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any) (Rendered, error) {
+	return a.cueApp.render(map[string]any{
+		"global":  infra,
+		"release": release,
+		"input":   values,
+	})
 }
 
 func cleanName(s string) string {
 	return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
 }
-
-func processCueConfig(contents string) (*cue.Value, error) {
-	ctx := cuecontext.New()
-	cfg := ctx.CompileString(contents + cueBaseConfig)
-	if err := cfg.Err(); err != nil {
-		return nil, err
-	}
-	if err := cfg.Validate(); err != nil {
-		return nil, err
-	}
-	return &cfg, nil
-}
-
-func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
-	contents, err := fs.ReadFile(f)
-	if err != nil {
-		return nil, err
-	}
-	return processCueConfig(string(contents))
-}
-
-func createApp(fs embed.FS, configFile string) App {
-	cfg, err := readCueConfigFromFile(fs, configFile)
-	if err != nil {
-		panic(err)
-	}
-	if app, err := newCueApp(cfg); err != nil {
-		panic(err)
-	} else {
-		return app
-	}
-}