installer: split up new env creation into chain of tasks
diff --git a/core/installer/tasks/infra.go b/core/installer/tasks/infra.go
new file mode 100644
index 0000000..e57c789
--- /dev/null
+++ b/core/installer/tasks/infra.go
@@ -0,0 +1,271 @@
+package tasks
+
+import (
+ "fmt"
+ "net/netip"
+
+ "github.com/miekg/dns"
+
+ "github.com/giolekva/pcloud/core/installer"
+ "github.com/giolekva/pcloud/core/installer/soft"
+)
+
+type setupInfraAppsTask struct {
+ basicTask
+ env Env
+ st *state
+}
+
+func NewSetupInfraAppsTask(env Env, st *state) Task {
+ return &setupInfraAppsTask{
+ basicTask: basicTask{
+ title: "Configure environment infrastructure",
+ },
+ env: env,
+ st: st,
+ }
+}
+
+func (t *setupInfraAppsTask) Start() {
+ repo, err := t.st.ssClient.GetRepo("config")
+ if err != nil {
+ t.callDoneListeners(err)
+ return
+ }
+ if err := t.initNewEnv(t.st.ssClient, installer.NewRepoIO(repo, t.st.ssClient.Signer), t.st.nsCreator, t.env.PCloudEnvName, t.st.publicIPs[0].String()); err != nil {
+ t.callDoneListeners(err)
+ return
+ }
+ t.callDoneListeners(nil)
+}
+
+func (t *setupInfraAppsTask) initNewEnv(
+ ss *soft.Client,
+ r installer.RepoIO,
+ nsCreator installer.NamespaceCreator,
+ pcloudEnvName string,
+ pcloudPublicIP string,
+) error {
+ appManager, err := installer.NewAppManager(r, nsCreator)
+ if err != nil {
+ return err
+ }
+ appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+ // TODO(giolekva): private domain can be configurable as well
+ config := installer.Config{
+ Values: installer.Values{
+ PCloudEnvName: pcloudEnvName,
+ Id: t.env.Name,
+ ContactEmail: t.env.ContactEmail,
+ Domain: t.env.Domain,
+ PrivateDomain: fmt.Sprintf("p.%s", t.env.Domain),
+ PublicIP: pcloudPublicIP,
+ NamespacePrefix: fmt.Sprintf("%s-", t.env.Name),
+ },
+ }
+ if err := r.WriteYaml("config.yaml", config); err != nil {
+ return err
+ }
+ {
+ out, err := r.Writer("pcloud-charts.yaml")
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ _, err = fmt.Fprintf(out, `
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: GitRepository
+metadata:
+ name: pcloud
+ namespace: %s
+spec:
+ interval: 1m0s
+ url: https://github.com/giolekva/pcloud
+ ref:
+ branch: main
+`, t.env.Name)
+ if err != nil {
+ return err
+ }
+ }
+ rootKust, err := r.ReadKustomization("kustomization.yaml")
+ if err != nil {
+ return err
+ }
+ rootKust.AddResources("pcloud-charts.yaml")
+ if err := r.WriteKustomization("kustomization.yaml", *rootKust); err != nil {
+ return err
+ }
+ r.CommitAndPush("configure charts repo")
+ nsGen := installer.NewPrefixGenerator(t.env.Name + "-")
+ emptySuffixGen := installer.NewEmptySuffixGenerator()
+ ingressPrivateIP, err := netip.ParseAddr("10.1.0.1")
+ if err != nil {
+ return err
+ }
+ {
+ headscaleIP := ingressPrivateIP.Next()
+ app, err := appsRepo.Find("metallb-ipaddresspool")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-ingress-private"), map[string]any{
+ "Name": fmt.Sprintf("%s-ingress-private", t.env.Name),
+ "From": ingressPrivateIP.String(),
+ "To": ingressPrivateIP.String(),
+ "AutoAssign": false,
+ "Namespace": "metallb-system",
+ }); err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-headscale"), map[string]any{
+ "Name": fmt.Sprintf("%s-headscale", t.env.Name),
+ "From": headscaleIP.String(),
+ "To": headscaleIP.String(),
+ "AutoAssign": false,
+ "Namespace": "metallb-system",
+ }); err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
+ "Name": t.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
+ }
+ }
+ {
+ app, err := appsRepo.Find("private-network")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
+ "PrivateNetwork": map[string]any{
+ "Hostname": "private-network-proxy",
+ "Username": "private-network-proxy",
+ "IPSubnet": "10.1.0.0/24",
+ },
+ }); err != nil {
+ return err
+ }
+ }
+ {
+ app, err := appsRepo.Find("certificate-issuer-public")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{}); err != nil {
+ return err
+ }
+ }
+ {
+ app, err := appsRepo.Find("certificate-issuer-private")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, nsGen, 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", pcloudEnvName),
+ },
+ }); err != nil {
+ return err
+ }
+ }
+ {
+ app, err := appsRepo.Find("core-auth")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
+ "Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
+ }); err != nil {
+ return err
+ }
+ }
+ {
+ app, err := appsRepo.Find("headscale")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
+ "Subdomain": "headscale",
+ }); err != nil {
+ return err
+ }
+ }
+ {
+ keys, err := installer.NewSSHKeyPair("welcome")
+ if err != nil {
+ return err
+ }
+ user := fmt.Sprintf("%s-welcome", t.env.Name)
+ if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
+ return err
+ }
+ if err := ss.AddReadWriteCollaborator("config", user); err != nil {
+ return err
+ }
+ app, err := appsRepo.Find("welcome")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
+ "RepoAddr": ss.GetRepoAddress("config"),
+ "SSHPrivateKey": string(keys.RawPrivateKey()),
+ }); err != nil {
+ return err
+ }
+ }
+ {
+ user := fmt.Sprintf("%s-appmanager", t.env.Name)
+ keys, err := installer.NewSSHKeyPair(user)
+ if err != nil {
+ return err
+ }
+ if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
+ return err
+ }
+ if err := ss.AddReadWriteCollaborator("config", user); err != nil {
+ return err
+ }
+ app, err := appsRepo.Find("app-manager") // TODO(giolekva): configure
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
+ "RepoAddr": ss.GetRepoAddress("config"),
+ "SSHPrivateKey": string(keys.RawPrivateKey()),
+ }); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+type DNSSecKey struct {
+ Basename string `json:"basename,omitempty"`
+ Key []byte `json:"key,omitempty"`
+ Private []byte `json:"private,omitempty"`
+ DS []byte `json:"ds,omitempty"`
+}
+
+func newDNSSecKey(zone string) (DNSSecKey, error) {
+ key := &dns.DNSKEY{
+ Hdr: dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
+ Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
+ }
+ priv, err := key.Generate(256)
+ if err != nil {
+ return DNSSecKey{}, err
+ }
+ return DNSSecKey{
+ Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
+ Key: []byte(key.String()),
+ Private: []byte(key.PrivateKeyString(priv)),
+ DS: []byte(key.ToDS(dns.SHA256).String()),
+ }, nil
+}