installer: fully automate initial bootstrap and env creation
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
+}