installer-env: wait for services to be reachable
diff --git a/core/installer/tasks/activate.go b/core/installer/tasks/activate.go
index f02e7a8..c6506aa 100644
--- a/core/installer/tasks/activate.go
+++ b/core/installer/tasks/activate.go
@@ -20,7 +20,15 @@
}
func NewActivateEnvTask(env Env, st *state) Task {
- t := newLeafTask(fmt.Sprintf("Activate %s environment", env.Name), func() error {
+ return newSequentialParentTask(
+ fmt.Sprintf("Activate new %s instance", env.PCloudEnvName),
+ AddNewEnvTask(env, st),
+ // TODO(gio): sync dodo-flux
+ )
+}
+
+func AddNewEnvTask(env Env, st *state) Task {
+ t := newLeafTask("Commit initial configuration", func() error {
ssPublicKeys, err := st.ssClient.GetPublicKeys()
if err != nil {
return err
diff --git a/core/installer/tasks/dns.go b/core/installer/tasks/dns.go
index 08cb863..4a33670 100644
--- a/core/installer/tasks/dns.go
+++ b/core/installer/tasks/dns.go
@@ -2,6 +2,7 @@
import (
"context"
+ "fmt"
"net"
"text/template"
"time"
@@ -13,13 +14,20 @@
type Check func(ch Check) error
-func NewDNSResolverTask(
+func SetupZoneTask(env Env, st *state) Task {
+ return newSequentialParentTask(
+ fmt.Sprintf("Setup DNS zone records for %s", env.Domain),
+ CreateZoneRecords(env.Domain, st.publicIPs, env, st),
+ WaitToPropagate(env.Domain, st.publicIPs),
+ )
+}
+
+func CreateZoneRecords(
name string,
expected []net.IP,
env Env,
st *state,
) Task {
- ctx := context.TODO()
t := newLeafTask("Configure DNS", func() error {
repo, err := st.ssClient.GetRepo("config")
if err != nil {
@@ -87,7 +95,17 @@
}
r.CommitAndPush("configure dns zone")
}
+ return nil
+ })
+ return &t
+}
+func WaitToPropagate(
+ name string,
+ expected []net.IP,
+) Task {
+ t := newLeafTask("Propagate DNS records", func() error {
+ ctx := context.TODO()
gotExpectedIPs := func(actual []net.IP) bool {
for _, a := range actual {
found := false
diff --git a/core/installer/tasks/env.go b/core/installer/tasks/env.go
index 353eb4c..48ab18e 100644
--- a/core/installer/tasks/env.go
+++ b/core/installer/tasks/env.go
@@ -10,12 +10,17 @@
)
type state struct {
- publicIPs []net.IP
- nsCreator installer.NamespaceCreator
- repo installer.RepoIO
- ssClient *soft.Client
- fluxUserName string
- keys *keygen.KeyPair
+ publicIPs []net.IP
+ nsCreator installer.NamespaceCreator
+ repo installer.RepoIO
+ ssAdminKeys *keygen.KeyPair
+ ssClient *soft.Client
+ fluxUserName string
+ keys *keygen.KeyPair
+ appManager *installer.AppManager
+ appsRepo installer.AppRepository[installer.App]
+ nsGen installer.NamespaceGenerator
+ emptySuffixGen installer.SuffixGenerator
}
type Env struct {
@@ -37,13 +42,15 @@
nsCreator: nsCreator,
repo: repo,
}
- t := newSequentialParentTask(
+ return newSequentialParentTask(
"Create env",
- NewCreateConfigRepoTask(env, &st),
- NewInitConfigRepoTask(env, &st),
- NewActivateEnvTask(env, &st),
- NewDNSResolverTask(env.Domain, publicIPs, env, &st),
- NewSetupInfraAppsTask(env, &st),
+ append(
+ []Task{
+ SetupConfigRepoTask(env, &st),
+ NewActivateEnvTask(env, &st),
+ SetupZoneTask(env, &st),
+ },
+ SetupInfra(env, &st)...,
+ )...,
)
- return &t
}
diff --git a/core/installer/tasks/infra.go b/core/installer/tasks/infra.go
index 06de0dc..995744c 100644
--- a/core/installer/tasks/infra.go
+++ b/core/installer/tasks/infra.go
@@ -7,27 +7,10 @@
"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 (t *setupInfraAppsTask) initNewEnv(
- ss *soft.Client,
- r installer.RepoIO,
- nsCreator installer.NamespaceCreator,
- pcloudEnvName string,
- pcloudPublicIP string,
-) error {
- return nil
-}
-
-func NewSetupInfraAppsTask(env Env, st *state) Task {
- t := newLeafTask("Configure environment infrastructure", func() error {
+func SetupInfra(env Env, st *state) []Task {
+ t := newLeafTask("Create client", func() error {
repo, err := st.ssClient.GetRepo("config")
if err != nil {
return err
@@ -37,22 +20,51 @@
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: env.PCloudEnvName,
- Id: env.Name,
- ContactEmail: env.ContactEmail,
- Domain: env.Domain,
- PrivateDomain: fmt.Sprintf("p.%s", env.Domain),
- PublicIP: st.publicIPs[0].String(),
- NamespacePrefix: fmt.Sprintf("%s-", env.Name),
- },
- }
- if err := r.WriteYaml("config.yaml", config); err != nil {
+ st.appManager = appManager
+ st.appsRepo = installer.NewInMemoryAppRepository(installer.CreateAllApps())
+ st.nsGen = installer.NewPrefixGenerator(env.Name + "-")
+ st.emptySuffixGen = installer.NewEmptySuffixGenerator()
+ return nil
+ })
+ return []Task{
+ CommitEnvironmentConfiguration(env, st),
+ &t,
+ newConcurrentParentTask(
+ "Core services",
+ SetupNetwork(env, st),
+ SetupCertificateIssuers(env, st),
+ SetupAuth(env, st),
+ SetupHeadscale(env, st),
+ SetupWelcome(env, st),
+ SetupAppStore(env, st),
+ ),
+ }
+}
+
+func CommitEnvironmentConfiguration(env Env, st *state) Task {
+ t := newLeafTask("Configure environment infrastructure", func() error {
+ repo, err := st.ssClient.GetRepo("config")
+ if err != nil {
return err
}
+ r := installer.NewRepoIO(repo, st.ssClient.Signer)
+ {
+ // TODO(giolekva): private domain can be configurable as well
+ config := installer.Config{
+ Values: installer.Values{
+ PCloudEnvName: env.PCloudEnvName,
+ Id: env.Name,
+ ContactEmail: env.ContactEmail,
+ Domain: env.Domain,
+ PrivateDomain: fmt.Sprintf("p.%s", env.Domain),
+ PublicIP: st.publicIPs[0].String(),
+ NamespacePrefix: fmt.Sprintf("%s-", env.Name),
+ },
+ }
+ if err := r.WriteYaml("config.yaml", config); err != nil {
+ return err
+ }
+ }
{
out, err := r.Writer("pcloud-charts.yaml")
if err != nil {
@@ -74,29 +86,34 @@
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")
}
- 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(env.Name + "-")
- emptySuffixGen := installer.NewEmptySuffixGenerator()
+ return nil
+ })
+ return &t
+}
+
+func SetupNetwork(env Env, st *state) Task {
+ t := newLeafTask("Setup network", func() error {
ingressPrivateIP, err := netip.ParseAddr("10.1.0.1")
if err != nil {
return err
}
{
headscaleIP := ingressPrivateIP.Next()
- app, err := appsRepo.Find("metallb-ipaddresspool")
+ app, err := st.appsRepo.Find("metallb-ipaddresspool")
if err != nil {
return err
}
- if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-ingress-private"), map[string]any{
+ 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(),
@@ -105,7 +122,7 @@
}); err != nil {
return err
}
- if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-headscale"), map[string]any{
+ 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(),
@@ -114,7 +131,7 @@
}); err != nil {
return err
}
- if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
+ 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",
@@ -125,11 +142,11 @@
}
}
{
- app, err := appsRepo.Find("private-network")
+ app, err := st.appsRepo.Find("private-network")
if err != nil {
return err
}
- if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
+ 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",
@@ -139,96 +156,134 @@
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
- }
+ return nil
+ })
+ return &t
+}
+
+func SetupCertificateIssuers(env Env, st *state) Task {
+ pub := newLeafTask(fmt.Sprintf("Public %s", env.Domain), func() error {
+ app, err := st.appsRepo.Find("certificate-issuer-public")
+ if 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", env.PCloudEnvName),
- },
- }); err != nil {
- return err
- }
+ if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{}); 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
- }
+ return nil
+ })
+ priv := newLeafTask(fmt.Sprintf("Private p.%s", env.Domain), func() error {
+ app, err := st.appsRepo.Find("certificate-issuer-private")
+ if 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
- }
+ 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),
+ },
+ }); err != nil {
+ return err
}
- {
- keys, err := installer.NewSSHKeyPair("welcome")
- if err != nil {
- return err
- }
- user := fmt.Sprintf("%s-welcome", env.Name)
- if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
- return err
- }
- if err := st.ssClient.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": st.ssClient.GetRepoAddress("config"),
- "SSHPrivateKey": string(keys.RawPrivateKey()),
- }); err != nil {
- return err
- }
+ return nil
+ })
+ return newSequentialParentTask("Configure TLS certificate issuers", &pub, &priv)
+}
+
+func SetupAuth(env Env, st *state) Task {
+ t := newLeafTask("Setup", func() error {
+ app, err := st.appsRepo.Find("core-auth")
+ if err != nil {
+ return err
}
- {
- user := fmt.Sprintf("%s-appmanager", env.Name)
- keys, err := installer.NewSSHKeyPair(user)
- if err != nil {
- return err
- }
- if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
- return err
- }
- if err := st.ssClient.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": st.ssClient.GetRepoAddress("config"),
- "SSHPrivateKey": string(keys.RawPrivateKey()),
- }); err != nil {
- 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
+ }); err != nil {
+ return err
+ }
+ return nil
+ })
+ return newSequentialParentTask(
+ "Authentication services",
+ &t,
+ waitForAddr(fmt.Sprintf("https://accounts-ui.%s", env.Domain)),
+ )
+}
+
+func SetupHeadscale(env Env, st *state) Task {
+ t := newLeafTask("Setup", func() error {
+ app, err := st.appsRepo.Find("headscale")
+ if err != nil {
+ return err
+ }
+ if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{
+ "Subdomain": "headscale",
+ }); err != nil {
+ return err
+ }
+ return nil
+ })
+ return newSequentialParentTask(
+ "Headscale service",
+ &t,
+ waitForAddr(fmt.Sprintf("https://headscale.%s/apple", env.Domain)),
+ )
+}
+
+func SetupWelcome(env Env, st *state) Task {
+ t := newLeafTask("Setup", func() error {
+ keys, err := installer.NewSSHKeyPair("welcome")
+ if err != nil {
+ return err
+ }
+ user := fmt.Sprintf("%s-welcome", env.Name)
+ if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
+ return err
+ }
+ if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
+ return err
+ }
+ app, err := st.appsRepo.Find("welcome")
+ if err != nil {
+ return err
+ }
+ if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{
+ "RepoAddr": st.ssClient.GetRepoAddress("config"),
+ "SSHPrivateKey": string(keys.RawPrivateKey()),
+ }); err != nil {
+ return err
+ }
+ return nil
+ })
+ return newSequentialParentTask(
+ "Welcome service",
+ &t,
+ waitForAddr(fmt.Sprintf("https://welcome.%s", env.Domain)),
+ )
+}
+
+func SetupAppStore(env Env, st *state) Task {
+ t := newLeafTask("Application marketplace", func() error {
+ user := fmt.Sprintf("%s-appmanager", env.Name)
+ keys, err := installer.NewSSHKeyPair(user)
+ if err != nil {
+ return err
+ }
+ if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
+ return err
+ }
+ if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
+ return err
+ }
+ app, err := st.appsRepo.Find("app-manager") // TODO(giolekva): configure
+ if err != nil {
+ return err
+ }
+ if err := st.appManager.Install(*app, st.nsGen, st.emptySuffixGen, map[string]any{
+ "RepoAddr": st.ssClient.GetRepoAddress("config"),
+ "SSHPrivateKey": string(keys.RawPrivateKey()),
+ }); err != nil {
+ return err
}
return nil
})
diff --git a/core/installer/tasks/init.go b/core/installer/tasks/init.go
index 2457c4d..f3818e3 100644
--- a/core/installer/tasks/init.go
+++ b/core/installer/tasks/init.go
@@ -9,6 +9,15 @@
"github.com/giolekva/pcloud/core/installer/soft"
)
+func SetupConfigRepoTask(env Env, st *state) Task {
+ return newSequentialParentTask(
+ "Configuration repository",
+ NewCreateConfigRepoTask(env, st),
+ CreateGitClientTask(env, st),
+ NewInitConfigRepoTask(env, st),
+ )
+}
+
func NewCreateConfigRepoTask(env Env, st *state) Task {
t := newLeafTask("Install Git server", func() error {
appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
@@ -20,6 +29,7 @@
if err != nil {
return err
}
+ st.ssAdminKeys = ssAdminKeys
ssKeys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-keys", env.Name))
if err != nil {
return err
@@ -50,9 +60,16 @@
if err := st.repo.InstallApp(*ssApp, filepath.Join("/environments", env.Name, "config-repo"), ssValues, derived); err != nil {
return err
}
+ return nil
+ })
+ return &t
+}
+
+func CreateGitClientTask(env Env, st *state) Task {
+ t := newLeafTask("Wait git server to come up", func() error {
ssClient, err := soft.WaitForClient(
fmt.Sprintf("soft-serve.%s.svc.cluster.local:%d", env.Name, 22),
- ssAdminKeys.RawPrivateKey(),
+ st.ssAdminKeys.RawPrivateKey(),
log.Default())
if err != nil {
return err
diff --git a/core/installer/tasks/tasks.go b/core/installer/tasks/tasks.go
index 9b0d533..75e8a68 100644
--- a/core/installer/tasks/tasks.go
+++ b/core/installer/tasks/tasks.go
@@ -104,7 +104,7 @@
parentTask
}
-func newSequentialParentTask(title string, subtasks ...Task) sequentialParentTask {
+func newSequentialParentTask(title string, subtasks ...Task) *sequentialParentTask {
start := func() error {
errCh := make(chan error)
for i := range subtasks[:len(subtasks)-1] {
@@ -123,8 +123,34 @@
go subtasks[0].Start()
return <-errCh
}
- t := sequentialParentTask{
+ return &sequentialParentTask{
parentTask: newParentTask(title, start, subtasks...),
}
- return t
+}
+
+type concurrentParentTask struct {
+ parentTask
+}
+
+func newConcurrentParentTask(title string, subtasks ...Task) *concurrentParentTask {
+ start := func() error {
+ errCh := make(chan error)
+ for i := range subtasks {
+ subtasks[i].OnDone(func(err error) {
+ errCh <- err
+ })
+ go subtasks[i].Start()
+ }
+ return <-errCh
+ for _ = range subtasks {
+ err := <-errCh
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+ return &concurrentParentTask{
+ parentTask: newParentTask(title, start, subtasks...),
+ }
}
diff --git a/core/installer/tasks/web.go b/core/installer/tasks/web.go
new file mode 100644
index 0000000..5136287
--- /dev/null
+++ b/core/installer/tasks/web.go
@@ -0,0 +1,20 @@
+package tasks
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+)
+
+func waitForAddr(addr string) Task {
+ t := newLeafTask(fmt.Sprintf("Wait for %s to come up", addr), func() error {
+ for {
+ if resp, err := http.Get(addr); err != nil || resp.StatusCode != http.StatusOK {
+ time.Sleep(2 * time.Second)
+ } else {
+ return nil
+ }
+ }
+ })
+ return &t
+}