installer: fully automate initial bootstrap and env creation
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index b1e8135..9fd2045 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -63,7 +63,7 @@
if err != nil {
return err
}
- m, err := installer.NewAppManager(repo, signer)
+ m, err := installer.NewAppManager(installer.NewRepoIO(repo, signer))
if err != nil {
return err
}
diff --git a/core/installer/cmd/apps.go b/core/installer/cmd/apps.go
index baab5bf..9cfe955 100644
--- a/core/installer/cmd/apps.go
+++ b/core/installer/cmd/apps.go
@@ -60,10 +60,10 @@
if err != nil {
return err
}
- m, err := installer.NewAppManager(
+ m, err := installer.NewAppManager(installer.NewRepoIO(
repo,
signer,
- )
+ ))
if err != nil {
return err
}
diff --git a/core/installer/cmd/bootstrap.go b/core/installer/cmd/bootstrap.go
index e5f3f2b..135b6b4 100644
--- a/core/installer/cmd/bootstrap.go
+++ b/core/installer/cmd/bootstrap.go
@@ -1,6 +1,3 @@
-// TODO
-// * ns pcloud not found
-
package main
import (
@@ -22,12 +19,12 @@
)
var bootstrapFlags struct {
+ pcloudEnvName string
chartsDir string
adminPubKey string
- adminPrivKey string
storageDir string
volumeDefaultReplicaCount int
- softServeIP string
+ softServeIP string // TODO(giolekva): reserve using metallb IPAddressPool
}
func bootstrapCmd() *cobra.Command {
@@ -36,6 +33,12 @@
RunE: bootstrapCmdRun,
}
cmd.Flags().StringVar(
+ &bootstrapFlags.pcloudEnvName,
+ "pcloud-env-name",
+ "pcloud",
+ "",
+ )
+ cmd.Flags().StringVar(
&bootstrapFlags.chartsDir,
"charts-dir",
"",
@@ -48,12 +51,6 @@
"",
)
cmd.Flags().StringVar(
- &bootstrapFlags.adminPrivKey,
- "admin-priv-key",
- "",
- "",
- )
- cmd.Flags().StringVar(
&bootstrapFlags.storageDir,
"storage-dir",
"",
@@ -75,73 +72,68 @@
}
func bootstrapCmdRun(cmd *cobra.Command, args []string) error {
- adminPubKey, adminPrivKey, err := readAdminKeys()
+ adminPubKey, err := os.ReadFile(bootstrapFlags.adminPubKey)
if err != nil {
return err
}
- softServePub, softServePriv, err := installer.GenerateSSHKeys()
+ bootstrapJobKeys, err := installer.NewSSHKeyPair()
if err != nil {
return err
}
- if err := installMetallbNamespace(); err != nil {
- return err
- }
if err := installMetallb(); err != nil {
return err
}
- time.Sleep(1 * time.Minute)
- if err := installMetallbConfig(); err != nil {
- return err
- }
if err := installLonghorn(); err != nil {
return err
}
- time.Sleep(2 * time.Minute)
- if err := installSoftServe(softServePub, softServePriv, string(adminPubKey)); err != nil {
+ time.Sleep(5 * time.Minute) // TODO(giolekva): implement proper wait
+ if err := installSoftServe(bootstrapJobKeys.Public); err != nil {
return err
}
- time.Sleep(2 * time.Minute)
- ss, err := soft.NewClient(bootstrapFlags.softServeIP, 22, adminPrivKey, log.Default())
+ time.Sleep(2 * time.Minute) // TODO(giolekva): implement proper wait
+ ss, err := soft.NewClient(bootstrapFlags.softServeIP, 22, []byte(bootstrapJobKeys.Private), log.Default())
if err != nil {
return err
}
- fluxPub, fluxPriv, err := installer.GenerateSSHKeys()
+ if ss.AddPublicKey("admin", string(adminPubKey)); err != nil {
+ return err
+ }
+ if err := installFluxcd(ss, bootstrapFlags.pcloudEnvName); err != nil {
+ return err
+ }
+ repo, err := ss.GetRepo(bootstrapFlags.pcloudEnvName)
if err != nil {
return err
}
- if err := ss.AddUser("flux", fluxPub); err != nil {
+ repoIO := installer.NewRepoIO(repo, ss.Signer)
+ if err := configurePCloudRepo(repoIO); err != nil {
return err
}
- if err := ss.MakeUserAdmin("flux"); err != nil {
+ // TODO(giolekva): commit this to the repo above
+ global := map[string]any{
+ "PCloudEnvName": bootstrapFlags.pcloudEnvName,
+ }
+ if err := installInfrastructureServices(repoIO, global); err != nil {
return err
}
- fmt.Println("Creating /pcloud repo")
- if err := ss.AddRepository("pcloud", "# PCloud Systems"); err != nil {
+ if err := installEnvManager(ss, repoIO, global); err != nil {
return err
}
- fmt.Println("Installing Flux")
- if err := installFlux("ssh://soft-serve.pcloud.svc.cluster.local:22/pcloud", "soft-serve.pcloud.svc.cluster.local", softServePub, fluxPriv); err != nil {
+ if ss.RemovePublicKey("admin", bootstrapJobKeys.Public); err != nil {
return err
}
- pcloudRepo, err := ss.GetRepo("pcloud") // TODO(giolekva): configurable
- if err != nil {
+
+ return nil
+}
+
+func installMetallb() error {
+ if err := installMetallbNamespace(); err != nil {
return err
}
- if err := configurePCloudRepo(installer.NewRepoIO(pcloudRepo, ss.Signer)); err != nil {
+ if err := installMetallbService(); err != nil {
return err
}
- // TODO(giolekva): everything below must be installed using Flux
- if err := installIngressPublic(); err != nil {
- return err
- }
- if err := installCertManager(); err != nil {
- return err
- }
- if err := installCertManagerWebhookGandi(); err != nil {
- return err
- }
- // TODO(giolekva): ideally should be installed automatically if any of the user installed apps requires it
- if err := installSmbDriver(); err != nil {
+ if err := installMetallbConfig(); err != nil {
return err
}
return nil
@@ -150,7 +142,7 @@
func installMetallbNamespace() error {
fmt.Println("Installing metallb namespace")
// config, err := createActionConfig("default")
- config, err := createActionConfig("pcloud")
+ config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
if err != nil {
return err
}
@@ -168,7 +160,7 @@
},
}
installer := action.NewInstall(config)
- installer.Namespace = "pcloud"
+ installer.Namespace = bootstrapFlags.pcloudEnvName
installer.ReleaseName = "metallb-ns"
installer.Wait = true
installer.WaitForJobs = true
@@ -178,7 +170,7 @@
return nil
}
-func installMetallb() error {
+func installMetallbService() error {
fmt.Println("Installing metallb")
// config, err := createActionConfig("default")
config, err := createActionConfig("metallb-system")
@@ -251,7 +243,7 @@
func installLonghorn() error {
fmt.Println("Installing Longhorn")
- config, err := createActionConfig("pcloud")
+ config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
if err != nil {
return err
}
@@ -288,9 +280,13 @@
return nil
}
-func installSoftServe(pubKey, privKey, adminKey string) error {
+func installSoftServe(adminPublicKey string) error {
fmt.Println("Installing SoftServe")
- config, err := createActionConfig("pcloud")
+ keys, err := installer.NewSSHKeyPair()
+ if err != nil {
+ return err
+ }
+ config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
if err != nil {
return err
}
@@ -299,13 +295,13 @@
return err
}
values := map[string]interface{}{
- "privateKey": privKey,
- "publicKey": pubKey,
- "adminKey": adminKey,
+ "privateKey": keys.Private,
+ "publicKey": keys.Public,
+ "adminKey": adminPublicKey,
"reservedIP": bootstrapFlags.softServeIP,
}
installer := action.NewInstall(config)
- installer.Namespace = "pcloud"
+ installer.Namespace = bootstrapFlags.pcloudEnvName
installer.CreateNamespace = true
installer.ReleaseName = "soft-serve"
installer.Wait = true
@@ -317,8 +313,39 @@
return nil
}
-func installFlux(repoAddr, repoHost, repoHostPubKey, privateKey string) error {
- config, err := createActionConfig("pcloud")
+func installFluxcd(ss *soft.Client, pcloudEnvName string) error {
+ keys, err := installer.NewSSHKeyPair()
+ if err != nil {
+ return err
+ }
+ if err := ss.AddUser("flux", keys.Public); err != nil {
+ return err
+ }
+ if err := ss.MakeUserAdmin("flux"); err != nil {
+ return err
+ }
+ fmt.Printf("Creating /%s repo", pcloudEnvName)
+ if err := ss.AddRepository(pcloudEnvName, "# PCloud Systems"); err != nil {
+ return err
+ }
+ fmt.Println("Installing Flux")
+ ssPublic, err := ss.GetPublicKey()
+ if err != nil {
+ return err
+ }
+ if err := installFluxBootstrap(
+ ss.GetRepoAddress(pcloudEnvName),
+ ss.IP,
+ string(ssPublic),
+ keys.Private,
+ ); err != nil {
+ return err
+ }
+ return nil
+}
+
+func installFluxBootstrap(repoAddr, repoHost, repoHostPubKey, privateKey string) error {
+ config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
if err != nil {
return err
}
@@ -333,7 +360,7 @@
"privateKey": privateKey,
}
installer := action.NewInstall(config)
- installer.Namespace = "pcloud"
+ installer.Namespace = bootstrapFlags.pcloudEnvName
installer.CreateNamespace = true
installer.ReleaseName = "flux"
installer.Wait = true
@@ -345,150 +372,103 @@
return nil
}
-func installIngressPublic() error {
- config, err := createActionConfig("pcloud")
- if err != nil {
- return err
+func installInfrastructureServices(repo installer.RepoIO, global map[string]any) error {
+ values := map[string]any{
+ "Global": global,
}
- chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "ingress-nginx"))
- if err != nil {
- return err
+ appRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+ install := func(name string) error {
+ app, err := appRepo.Find(name)
+ if err != nil {
+ return err
+ }
+ return repo.InstallApp(*app, "infrastructure", values)
}
- values := map[string]interface{}{
- "fullnameOverride": "pcloud-ingress-public",
- "controller": map[string]interface{}{
- "service": map[string]interface{}{
- "type": "LoadBalancer",
- },
- "ingressClassByName": true,
- "ingressClassResource": map[string]interface{}{
- "name": "pcloud-ingress-public",
- "enabled": true,
- "default": false,
- "controllerValue": "k8s.io/pcloud-ingress-public",
- },
- "config": map[string]interface{}{
- "proxy-body-size": "100M",
- },
- },
- "udp": map[string]interface{}{
- "6881": "lekva-app-qbittorrent/torrent:6881",
- },
- "tcp": map[string]interface{}{
- "6881": "lekva-app-qbittorrent/torrent:6881",
- },
+ appsToInstall := []string{
+ "resource-renderer-controller",
+ "headscale-controller",
+ "csi-driver-smb",
+ "ingress-public",
+ "cert-manager",
+ "cert-manager-webhook-gandi",
+ "cert-manager-webhook-gandi-role",
}
- installer := action.NewInstall(config)
- installer.Namespace = "pcloud-ingress-public"
- installer.CreateNamespace = true
- installer.ReleaseName = "ingress-public"
- installer.Wait = true
- installer.WaitForJobs = true
- installer.Timeout = 20 * time.Minute
- if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
- return err
- }
- return nil
-}
-
-func installCertManager() error {
- config, err := createActionConfig("pcloud-cert-manager")
- if err != nil {
- return err
- }
- chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "cert-manager"))
- if err != nil {
- return err
- }
- values := map[string]interface{}{
- "fullnameOverride": "pcloud-cert-manager",
- "installCRDs": true,
- "image": map[string]interface{}{
- "tag": "v1.11.1",
- "pullPolicy": "IfNotPresent",
- },
- }
- installer := action.NewInstall(config)
- installer.Namespace = "pcloud-cert-manager"
- installer.CreateNamespace = true
- installer.ReleaseName = "cert-manager"
- installer.Wait = true
- installer.WaitForJobs = true
- installer.Timeout = 20 * time.Minute
- if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
- return err
- }
- return nil
-}
-
-func installCertManagerWebhookGandi() error {
- config, err := createActionConfig("pcloud-cert-manager")
- if err != nil {
- return err
- }
- chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "cert-manager-webhook-gandi"))
- if err != nil {
- return err
- }
- values := map[string]interface{}{
- "fullnameOverride": "pcloud-cert-manager-webhook-gandi",
- "certManager": map[string]interface{}{
- "namespace": "pcloud-cert-manager",
- "serviceAccountName": "pcloud-cert-manager",
- },
- "image": map[string]interface{}{
- "repository": "giolekva/cert-manager-webhook-gandi",
- "tag": "v0.2.0",
- "pullPolicy": "IfNotPresent",
- },
- "logLevel": 2,
- }
- installer := action.NewInstall(config)
- installer.Namespace = "pcloud-cert-manager"
- installer.CreateNamespace = false
- installer.ReleaseName = "cert-manager-webhook-gandi"
- installer.Wait = true
- installer.WaitForJobs = true
- installer.Timeout = 20 * time.Minute
- if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
- return err
- }
- return nil
-}
-
-func installSmbDriver() error {
- config, err := createActionConfig("pcloud-csi-driver-smb")
- if err != nil {
- return err
- }
- chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "csi-driver-smb"))
- if err != nil {
- return err
- }
- values := map[string]interface{}{}
- installer := action.NewInstall(config)
- installer.Namespace = "pcloud-csi-driver-smb"
- installer.CreateNamespace = true
- installer.ReleaseName = "csi-driver-smb"
- installer.Wait = true
- installer.WaitForJobs = true
- installer.Timeout = 20 * time.Minute
- if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
- return err
+ for _, name := range appsToInstall {
+ if err := install(name); err != nil {
+ return err
+ }
}
return nil
}
func configurePCloudRepo(repo installer.RepoIO) error {
- kust := installer.NewKustomization()
- kust.AddResources("pcloud-flux", "environments")
- if err := repo.WriteKustomization("kustomization.yaml", kust); err != nil {
+ {
+ kust := installer.NewKustomization()
+ kust.AddResources("pcloud-flux", "infrastructure", "environments")
+ if err := repo.WriteKustomization("kustomization.yaml", kust); err != nil {
+ return err
+ }
+ {
+ out, err := repo.Writer("infrastructure/pcloud-charts.yaml")
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ _, err = out.Write([]byte(`
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: GitRepository
+metadata:
+ name: pcloud # TODO(giolekva): use more generic name
+ namespace: pcloud # TODO(giolekva): configurable
+spec:
+ interval: 1m0s
+ url: https://github.com/giolekva/pcloud
+ ref:
+ branch: main
+`))
+ if err != nil {
+ return err
+ }
+ }
+ infraKust := installer.NewKustomization()
+ infraKust.AddResources("pcloud-charts.yaml")
+ if err := repo.WriteKustomization("infrastructure/kustomization.yaml", infraKust); err != nil {
+ return err
+ }
+ if err := repo.WriteKustomization("environments/kustomization.yaml", installer.NewKustomization()); err != nil {
+ return err
+ }
+ if err := repo.CommitAndPush("initialize pcloud directory structure"); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func installEnvManager(ss *soft.Client, repo installer.RepoIO, global map[string]any) error {
+ keys, err := installer.NewSSHKeyPair()
+ if err != nil {
return err
}
- if err := repo.WriteKustomization("environments/kustomization.yaml", installer.NewKustomization()); err != nil {
+ user := fmt.Sprintf("%s-env-manager", bootstrapFlags.pcloudEnvName)
+ if err := ss.AddUser(user, keys.Public); err != nil {
return err
}
- return repo.CommitAndPush("initialize pcloud directory structure, environments with kustomization.yaml-s")
+ if err := ss.MakeUserAdmin(user); err != nil {
+ return err
+ }
+ appRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+ envManager, err := appRepo.Find("env-manager")
+ if err != nil {
+ return err
+ }
+ return repo.InstallApp(*envManager, "infrastructure", map[string]any{
+ "Global": global,
+ "Values": map[string]any{
+ "RepoIP": bootstrapFlags.softServeIP,
+ "SSHPrivateKey": keys.Private,
+ },
+ })
}
func createActionConfig(namespace string) (*action.Configuration, error) {
@@ -506,15 +486,3 @@
}
return config, nil
}
-
-func readAdminKeys() ([]byte, []byte, error) {
- pubKey, err := os.ReadFile(bootstrapFlags.adminPubKey)
- if err != nil {
- return nil, nil, err
- }
- privKey, err := os.ReadFile(bootstrapFlags.adminPrivKey)
- if err != nil {
- return nil, nil, err
- }
- return pubKey, privKey, nil
-}
diff --git a/core/installer/cmd/env.go b/core/installer/cmd/env.go
index 8b7dc1e..045b258 100644
--- a/core/installer/cmd/env.go
+++ b/core/installer/cmd/env.go
@@ -7,6 +7,7 @@
"embed"
"encoding/base64"
"fmt"
+ "github.com/spf13/cobra"
"log"
"os"
"path"
@@ -14,17 +15,17 @@
"github.com/giolekva/pcloud/core/installer"
"github.com/giolekva/pcloud/core/installer/soft"
- "github.com/spf13/cobra"
)
//go:embed env-tmpl
var filesTmpls embed.FS
var createEnvFlags struct {
- name string
- ip string
- port int
- adminPrivKey string
+ name string
+ ip string
+ port int
+ adminPrivKey string
+ adminUsername string
}
func createEnvCmd() *cobra.Command {
@@ -56,6 +57,12 @@
"",
"",
)
+ cmd.Flags().StringVar(
+ &createEnvFlags.adminUsername,
+ "admin-username",
+ "",
+ "",
+ )
return cmd
}
@@ -72,61 +79,190 @@
if err != nil {
return err
}
- fmt.Println(string(ssPubKey))
- pub, priv, err := installer.GenerateSSHKeys()
- {
- _ = priv
- }
+ keys, err := installer.NewSSHKeyPair()
if err != nil {
return err
}
- readme := fmt.Sprintf("# %s PCloud environment", createEnvFlags.name)
- if err := ss.AddRepository(createEnvFlags.name, readme); err != nil {
+ if 1 == 2 {
+ readme := fmt.Sprintf("# %s PCloud environment", createEnvFlags.name)
+ if err := ss.AddRepository(createEnvFlags.name, readme); err != nil {
+ return err
+ }
+ fluxUserName := fmt.Sprintf("flux-%s", createEnvFlags.name)
+ if err := ss.AddUser(fluxUserName, keys.Public); err != nil {
+ return err
+ }
+ if err := ss.AddCollaborator(createEnvFlags.name, fluxUserName); err != nil {
+ return err
+ }
+ }
+ envRepo, err := ss.GetRepo(createEnvFlags.name)
+ if envRepo == nil {
return err
}
- fluxUserName := fmt.Sprintf("flux-%s", createEnvFlags.name)
- if err := ss.AddUser(fluxUserName, pub); err != nil {
+ if err := initEnvRepo(installer.NewRepoIO(envRepo, ss.Signer)); err != nil {
return err
}
- if err := ss.AddCollaborator(createEnvFlags.name, fluxUserName); err != nil {
- return err
- }
- repo, err := ss.GetRepo("pcloud")
- if err != nil {
- return err
- }
- repoIO := installer.NewRepoIO(repo, ss.Signer)
- kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
- if err != nil {
- return err
- }
- kust.AddResources(createEnvFlags.name)
- tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
- if err != nil {
- return err
- }
- for _, tmpl := range tmpls.Templates() {
- dstPath := path.Join("environments", createEnvFlags.name, tmpl.Name())
- dst, err := repoIO.Writer(dstPath)
+ if 1 == 2 {
+ repo, err := ss.GetRepo("pcloud")
if err != nil {
return err
}
- defer dst.Close()
- if err := tmpl.Execute(dst, map[string]string{
- "Name": createEnvFlags.name,
- "PrivateKey": base64.StdEncoding.EncodeToString([]byte(priv)),
- "PublicKey": base64.StdEncoding.EncodeToString([]byte(pub)),
- "GitHost": createEnvFlags.ip,
- "KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", createEnvFlags.ip, ssPubKey))),
+ repoIO := installer.NewRepoIO(repo, ss.Signer)
+ kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
+ if err != nil {
+ return err
+ }
+ kust.AddResources(createEnvFlags.name)
+ tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
+ if err != nil {
+ return err
+ }
+ for _, tmpl := range tmpls.Templates() {
+ dstPath := path.Join("environments", createEnvFlags.name, tmpl.Name())
+ dst, err := repoIO.Writer(dstPath)
+ if err != nil {
+ return err
+ }
+ defer dst.Close()
+ if err := tmpl.Execute(dst, map[string]string{
+ "Name": createEnvFlags.name,
+ "PrivateKey": base64.StdEncoding.EncodeToString([]byte(keys.Private)),
+ "PublicKey": base64.StdEncoding.EncodeToString([]byte(keys.Public)),
+ "GitHost": createEnvFlags.ip,
+ "KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", createEnvFlags.ip, ssPubKey))),
+ }); err != nil {
+ return err
+ }
+ }
+ if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
+ return err
+ }
+ if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", createEnvFlags.name)); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func initEnvRepo(r installer.RepoIO) error {
+ appManager, err := installer.NewAppManager(r)
+ if err != nil {
+ return err
+ }
+ appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+ if 1 == 2 {
+ config := installer.Config{ // TODO(gioleka): configurable
+ Values: installer.Values{
+ PCloudEnvName: "pcloud",
+ Id: "lekva",
+ ContactEmail: "giolekva@gmail.com",
+ Domain: "lekva.me",
+ PrivateDomain: "p.lekva.me",
+ PublicIP: "46.49.35.44",
+ NamespacePrefix: "lekva-",
+ },
+ }
+ 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 = out.Write([]byte(`
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: GitRepository
+metadata:
+ name: pcloud
+ namespace: lekva
+spec:
+ interval: 1m0s
+ url: https://github.com/giolekva/pcloud
+ ref:
+ branch: main
+`))
+ if err != nil {
+ return err
+ }
+ }
+ rootKust := installer.NewKustomization()
+ rootKust.AddResources("pcloud-charts.yaml", "apps")
+ if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
+ return err
+ }
+ appsKust := installer.NewKustomization()
+ if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
+ return err
+ }
+ r.CommitAndPush("initialize config")
+ {
+ app, err := appsRepo.Find("metallb-config-env")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, map[string]any{
+ "IngressPrivate": "10.1.0.1",
+ "Headscale": "10.1.0.2",
+ "SoftServe": "10.1.0.3",
+ "Rest": map[string]any{
+ "From": "10.1.0.100",
+ "To": "10.1.0.255",
+ },
+ }); err != nil {
+ return err
+ }
+ }
+ {
+ app, err := appsRepo.Find("ingress-private")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, map[string]any{
+ "GandiAPIToken": "", // TODO(gioleka): configurable
+ }); err != nil {
+ return err
+ }
+ }
+ {
+ app, err := appsRepo.Find("core-auth")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, 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, map[string]any{
+ "Subdomain": "headscale",
}); err != nil {
return err
}
}
- if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
- return err
- }
- if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", createEnvFlags.name)); err != nil {
- return err
+ if 1 == 2 {
+ {
+ app, err := appsRepo.Find("tailscale-proxy")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, map[string]any{
+ "Username": createEnvFlags.adminUsername,
+ "IPSubnet": "10.1.0.0/24",
+ }); err != nil {
+ return err
+ }
+ // TODO(giolekva): headscale accept routes
+ }
}
return nil
}
diff --git a/core/installer/cmd/env_manager.go b/core/installer/cmd/env_manager.go
new file mode 100644
index 0000000..4279445
--- /dev/null
+++ b/core/installer/cmd/env_manager.go
@@ -0,0 +1,323 @@
+package main
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "path"
+ "text/template"
+
+ "github.com/labstack/echo/v4"
+ "github.com/spf13/cobra"
+
+ "github.com/giolekva/pcloud/core/installer"
+ "github.com/giolekva/pcloud/core/installer/soft"
+)
+
+var envManagerFlags struct {
+ repoIP string
+ repoPort int
+ sshKey string
+ port int
+}
+
+func envManagerCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "envmanager",
+ RunE: envManagerCmdRun,
+ }
+ cmd.Flags().StringVar(
+ &envManagerFlags.repoIP,
+ "repo-ip",
+ "",
+ "",
+ )
+ cmd.Flags().IntVar(
+ &envManagerFlags.repoPort,
+ "repo-port",
+ 22,
+ "",
+ )
+ cmd.Flags().StringVar(
+ &envManagerFlags.sshKey,
+ "ssh-key",
+ "",
+ "",
+ )
+ cmd.Flags().IntVar(
+ &envManagerFlags.port,
+ "port",
+ 8080,
+ "",
+ )
+ return cmd
+}
+
+func envManagerCmdRun(cmd *cobra.Command, args []string) error {
+ sshKey, err := os.ReadFile(envManagerFlags.sshKey)
+ if err != nil {
+ return err
+ }
+ fmt.Println(string(sshKey))
+ ss, err := soft.NewClient(envManagerFlags.repoIP, envManagerFlags.repoPort, sshKey, log.Default())
+ if err != nil {
+ return err
+ }
+ b, err := ss.GetPublicKey()
+ if err != nil {
+ return err
+ }
+ fmt.Println(string(b))
+ fmt.Println(111)
+ repo, err := ss.GetRepo("pcloud")
+ fmt.Println(222)
+ if err != nil {
+ return err
+ }
+ fmt.Println(333)
+ repoIO := installer.NewRepoIO(repo, ss.Signer)
+ s := &envServer{
+ port: envManagerFlags.port,
+ ss: ss,
+ repo: repoIO,
+ }
+ s.start()
+ return nil
+}
+
+type envServer struct {
+ port int
+ ss *soft.Client
+ repo installer.RepoIO
+}
+
+func (s *envServer) start() {
+ e := echo.New()
+ e.POST("/env", s.createEnv)
+ log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
+}
+
+type createEnvReq struct {
+ Name string `json:"name"`
+ ContactEmail string `json:"contactEmail"`
+ Domain string `json:"domain"`
+ GandiAPIToken string `json:"gandiAPIToken"`
+ AdminUsername string `json:"adminUsername"`
+ // TODO(giolekva): take admin password as well
+}
+
+func (s *envServer) createEnv(c echo.Context) error {
+ var req createEnvReq
+ if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil {
+ return err
+ }
+ keys, err := installer.NewSSHKeyPair()
+ if err != nil {
+ return err
+ }
+ {
+ readme := fmt.Sprintf("# %s PCloud environment", req.Name)
+ if err := s.ss.AddRepository(req.Name, readme); err != nil {
+ return err
+ }
+ fluxUserName := fmt.Sprintf("flux-%s", req.Name)
+ if err := s.ss.AddUser(fluxUserName, keys.Public); err != nil {
+ return err
+ }
+ if err := s.ss.AddCollaborator(req.Name, fluxUserName); err != nil {
+ return err
+ }
+ }
+ {
+ repo, err := s.ss.GetRepo(req.Name)
+ if repo == nil {
+ return err
+ }
+ if err := initNewEnv(installer.NewRepoIO(repo, s.ss.Signer), req); err != nil {
+ return err
+ }
+ }
+ {
+ repo, err := s.ss.GetRepo("pcloud")
+ if err != nil {
+ return err
+ }
+ ssPubKey, err := s.ss.GetPublicKey()
+ if err != nil {
+ return err
+ }
+ if err := addNewEnv(
+ installer.NewRepoIO(repo, s.ss.Signer),
+ req,
+ keys,
+ ssPubKey,
+ ); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func initNewEnv(r installer.RepoIO, req createEnvReq) error {
+ appManager, err := installer.NewAppManager(r)
+ if err != nil {
+ return err
+ }
+ appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+ // TODO(giolekva): env name and ip should come from pcloud repo config.yaml
+ // TODO(giolekva): private domain can be configurable as well
+ config := installer.Config{
+ Values: installer.Values{
+ PCloudEnvName: "pcloud",
+ Id: req.Name,
+ ContactEmail: req.ContactEmail,
+ Domain: req.Domain,
+ PrivateDomain: fmt.Sprintf("p.%s", req.Domain),
+ PublicIP: "46.49.35.44",
+ NamespacePrefix: fmt.Sprintf("%s-", req.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 = out.Write([]byte(`
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: GitRepository
+metadata:
+ name: pcloud
+ namespace: lekva
+spec:
+ interval: 1m0s
+ url: https://github.com/giolekva/pcloud
+ ref:
+ branch: main
+`))
+ if err != nil {
+ return err
+ }
+ }
+ rootKust := installer.NewKustomization()
+ rootKust.AddResources("pcloud-charts.yaml", "apps")
+ if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
+ return err
+ }
+ appsKust := installer.NewKustomization()
+ if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
+ return err
+ }
+ r.CommitAndPush("initialize config")
+ {
+ app, err := appsRepo.Find("metallb-config-env")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, map[string]any{
+ "IngressPrivate": "10.1.0.1",
+ "Headscale": "10.1.0.2",
+ "SoftServe": "10.1.0.3",
+ "Rest": map[string]any{
+ "From": "10.1.0.100",
+ "To": "10.1.0.255",
+ },
+ }); err != nil {
+ return err
+ }
+ }
+ {
+ app, err := appsRepo.Find("ingress-private")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, map[string]any{
+ "GandiAPIToken": req.GandiAPIToken,
+ }); err != nil {
+ return err
+ }
+ }
+ {
+ app, err := appsRepo.Find("core-auth")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, 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, map[string]any{
+ "Subdomain": "headscale",
+ }); err != nil {
+ return err
+ }
+ }
+ {
+ app, err := appsRepo.Find("tailscale-proxy")
+ if err != nil {
+ return err
+ }
+ if err := appManager.Install(*app, map[string]any{
+ "Username": req.AdminUsername,
+ "IPSubnet": "10.1.0.0/24",
+ }); err != nil {
+ return err
+ }
+ // TODO(giolekva): headscale accept routes
+ }
+
+ return nil
+}
+
+func addNewEnv(
+ repoIO installer.RepoIO,
+ req createEnvReq,
+ keys installer.KeyPair,
+ pcloudRepoPublicKey []byte,
+) error {
+ kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
+ if err != nil {
+ return err
+ }
+ kust.AddResources(req.Name)
+ tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
+ if err != nil {
+ return err
+ }
+ for _, tmpl := range tmpls.Templates() {
+ dstPath := path.Join("environments", req.Name, tmpl.Name())
+ dst, err := repoIO.Writer(dstPath)
+ if err != nil {
+ return err
+ }
+ defer dst.Close()
+ if err := tmpl.Execute(dst, map[string]string{
+ "Name": req.Name,
+ "PrivateKey": base64.StdEncoding.EncodeToString([]byte(keys.Private)),
+ "PublicKey": base64.StdEncoding.EncodeToString([]byte(keys.Public)),
+ "GitHost": envManagerFlags.repoIP,
+ "KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", envManagerFlags.repoIP, pcloudRepoPublicKey))),
+ }); err != nil {
+ return err
+ }
+ }
+ if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
+ return err
+ }
+ if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", req.Name)); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/core/installer/cmd/main.go b/core/installer/cmd/main.go
index 7a71b90..e82b338 100644
--- a/core/installer/cmd/main.go
+++ b/core/installer/cmd/main.go
@@ -26,6 +26,7 @@
rootCmd.AddCommand(createEnvCmd())
rootCmd.AddCommand(installCmd())
rootCmd.AddCommand(appManagerCmd())
+ rootCmd.AddCommand(envManagerCmd())
}
func main() {