installer: create individual soft-serve instances for each env
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 029f6df..aa3e061 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -4,12 +4,16 @@
"embed"
"encoding/base64"
"encoding/json"
+ "errors"
"fmt"
htemplate "html/template"
"io"
+ "io/fs"
"log"
"net/http"
"path"
+ "path/filepath"
+ "strings"
"text/template"
"github.com/charmbracelet/keygen"
@@ -99,15 +103,19 @@
}
type createEnvReq struct {
- Name string
- ContactEmail string `json:"contactEmail"`
- Domain string `json:"domain"`
- SecretToken string `json:"secretToken"`
+ Name string
+ ContactEmail string `json:"contactEmail"`
+ Domain string `json:"domain"`
+ AdminPublicKey string `json:"adminPublicKey"`
+ SecretToken string `json:"secretToken"`
}
func (s *EnvServer) readInvitations() ([]invitation, error) {
r, err := s.repo.Reader("invitations")
if err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ return make([]invitation, 0), nil
+ }
return nil, err
}
defer r.Close()
@@ -138,7 +146,7 @@
return s.repo.CommitAndPush("Generated new invitation")
}
-func (s *EnvServer) createEnv(w http.ResponseWriter, r *http.Request) {
+func extractRequest(r *http.Request) (createEnvReq, error) {
var req createEnvReq
if err := func() error {
var err error
@@ -154,31 +162,55 @@
if req.ContactEmail, err = getFormValue(r.PostForm, "contact-email"); err != nil {
return err
}
+ if req.AdminPublicKey, err = getFormValue(r.PostForm, "admin-public-key"); err != nil {
+ return err
+ }
return nil
}(); err != nil {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
+ return createEnvReq{}, err
}
}
+ return req, nil
+}
+
+func (s *EnvServer) acceptInvitation(token string) error {
invitations, err := s.readInvitations()
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
+ return err
}
found := false
- for _, i := range invitations {
- if i.Token == req.SecretToken && i.Status == StatusActive {
- i.Status = StatusAccepted
+ for i := range invitations {
+ if invitations[i].Token == token && invitations[i].Status == StatusActive {
+ invitations[i].Status = StatusAccepted
found = true
break
}
}
if !found {
- http.Error(w, "Invalid invitation", http.StatusNotFound)
+ return fmt.Errorf("Invitation not found")
+ }
+ return s.writeInvitations(invitations)
+}
+
+func (s *EnvServer) createEnv(w http.ResponseWriter, r *http.Request) {
+ req, err := extractRequest(r)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- if err := s.writeInvitations(invitations); err != nil {
+ var env installer.EnvConfig
+ cr, err := s.repo.Reader("config.yaml")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer cr.Close()
+ if err := installer.ReadYaml(cr, &env); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := s.acceptInvitation(req.SecretToken); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -188,6 +220,74 @@
} else {
req.Name = name
}
+ appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+ ssApp, err := appsRepo.Find("soft-serve")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ ssAdminKeys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-admin-keys", req.Name))
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ ssKeys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-keys", req.Name))
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ ssValues := map[string]any{
+ "ChartRepositoryNamespace": env.Name,
+ "ServiceType": "ClusterIP",
+ "PrivateKey": string(ssKeys.RawPrivateKey()),
+ "PublicKey": string(ssKeys.RawAuthorizedKey()),
+ "AdminKey": string(ssAdminKeys.RawAuthorizedKey()),
+ "Ingress": map[string]any{
+ "Enabled": false,
+ },
+ }
+ derived := installer.Derived{
+ Global: installer.Values{
+ Id: req.Name,
+ PCloudEnvName: env.Name,
+ },
+ Release: installer.Release{
+ Namespace: req.Name,
+ },
+ Values: ssValues,
+ }
+ if err := s.nsCreator.Create(req.Name); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := s.repo.InstallApp(*ssApp, filepath.Join("/environments", req.Name, "config-repo"), ssValues, derived); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ k := installer.NewKustomization()
+ k.AddResources("config-repo")
+ if err := s.repo.WriteKustomization(filepath.Join("/environments", req.Name, "kustomization.yaml"), k); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ ssClient, err := soft.WaitForClient(
+ fmt.Sprintf("soft-serve.%s.svc.cluster.local:%d", req.Name, 22),
+ ssAdminKeys.RawPrivateKey(),
+ log.Default())
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := ssClient.AddPublicKey("admin", req.AdminPublicKey); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer func() {
+ if err := ssClient.RemovePublicKey("admin", string(ssAdminKeys.RawAuthorizedKey())); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }()
fluxUserName := fmt.Sprintf("flux-%s", req.Name)
keys, err := installer.NewSSHKeyPair(fluxUserName)
if err != nil {
@@ -195,44 +295,42 @@
return
}
{
- readme := fmt.Sprintf("# %s PCloud environment", req.Name)
- if err := s.ss.AddRepository(req.Name, readme); err != nil {
+ if err := ssClient.AddRepository("config"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- if err := s.ss.AddUser(fluxUserName, keys.AuthorizedKey()); err != nil {
+ repo, err := ssClient.GetRepo("config")
+ if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- if err := s.ss.AddCollaborator(req.Name, fluxUserName); err != nil {
+ repoIO := installer.NewRepoIO(repo, ssClient.Signer)
+ if err := repoIO.WriteCommitAndPush("README.md", fmt.Sprintf("# %s PCloud environment", req.Name), "readme"); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := ssClient.AddUser(fluxUserName, keys.AuthorizedKey()); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := ssClient.AddReadOnlyCollaborator("config", fluxUserName); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
{
- repo, err := s.ss.GetRepo(req.Name)
+ repo, err := ssClient.GetRepo("config")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- var env installer.EnvConfig
- r, err := s.repo.Reader("config.yaml")
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- defer r.Close()
- if err := installer.ReadYaml(r, &env); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if err := initNewEnv(s.ss, installer.NewRepoIO(repo, s.ss.Signer), s.nsCreator, req, env); err != nil {
+ if err := initNewEnv(ssClient, installer.NewRepoIO(repo, ssClient.Signer), s.nsCreator, req, env.Name, env.PublicIP); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
{
- ssPubKey, err := s.ss.GetPublicKey()
+ ssPubKey, err := ssClient.GetPublicKey()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -240,6 +338,7 @@
if err := addNewEnv(
s.repo,
req,
+ strings.Split(ssClient.Addr, ":")[0],
keys,
ssPubKey,
); err != nil {
@@ -265,7 +364,8 @@
r installer.RepoIO,
nsCreator installer.NamespaceCreator,
req createEnvReq,
- env installer.EnvConfig,
+ pcloudEnvName string,
+ pcloudPublicIP string,
) error {
appManager, err := installer.NewAppManager(r, nsCreator)
if err != nil {
@@ -275,12 +375,12 @@
// TODO(giolekva): private domain can be configurable as well
config := installer.Config{
Values: installer.Values{
- PCloudEnvName: env.Name,
+ PCloudEnvName: pcloudEnvName,
Id: req.Name,
ContactEmail: req.ContactEmail,
Domain: req.Domain,
PrivateDomain: fmt.Sprintf("p.%s", req.Domain),
- PublicIP: env.PublicIP,
+ PublicIP: pcloudPublicIP,
NamespacePrefix: fmt.Sprintf("%s-", req.Name),
},
}
@@ -344,15 +444,6 @@
}); err != nil {
return err
}
- if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-soft-serve"), map[string]any{
- "Name": fmt.Sprintf("%s-soft-serve", req.Name), // TODO(giolekva): rename to config repo
- "From": "10.1.0.3",
- "To": "10.1.0.3",
- "AutoAssign": false,
- "Namespace": "metallb-system",
- }); err != nil {
- return err
- }
if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
"Name": req.Name,
"From": "10.1.0.100",
@@ -412,7 +503,7 @@
if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
return err
}
- if err := ss.AddCollaborator(req.Name, user); err != nil {
+ if err := ss.AddReadWriteCollaborator("config", user); err != nil {
return err
}
app, err := appsRepo.Find("welcome")
@@ -420,7 +511,7 @@
return err
}
if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
- "RepoAddr": ss.GetRepoAddress(req.Name),
+ "RepoAddr": ss.GetRepoAddress("config"),
"SSHPrivateKey": string(keys.RawPrivateKey()),
}); err != nil {
return err
@@ -435,7 +526,7 @@
if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
return err
}
- if err := ss.AddCollaborator(req.Name, user); err != nil {
+ if err := ss.AddReadWriteCollaborator("config", user); err != nil {
return err
}
app, err := appsRepo.Find("app-manager") // TODO(giolekva): configure
@@ -443,7 +534,7 @@
return err
}
if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
- "RepoAddr": ss.GetRepoAddress(req.Name),
+ "RepoAddr": ss.GetRepoAddress("config"),
"SSHPrivateKey": string(keys.RawPrivateKey()),
}); err != nil {
return err
@@ -455,6 +546,7 @@
func addNewEnv(
repoIO installer.RepoIO,
req createEnvReq,
+ repoHost string,
keys *keygen.KeyPair,
pcloudRepoPublicKey []byte,
) error {
@@ -467,7 +559,6 @@
if err != nil {
return err
}
- repoIP := repoIO.Addr().Addr().String()
for _, tmpl := range tmpls.Templates() {
dstPath := path.Join("environments", req.Name, tmpl.Name())
dst, err := repoIO.Writer(dstPath)
@@ -479,8 +570,9 @@
"Name": req.Name,
"PrivateKey": base64.StdEncoding.EncodeToString(keys.RawPrivateKey()),
"PublicKey": base64.StdEncoding.EncodeToString(keys.RawAuthorizedKey()),
- "GitHost": repoIP,
- "KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", repoIP, pcloudRepoPublicKey))),
+ "RepoHost": repoHost,
+ "RepoName": "config",
+ "KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", repoHost, pcloudRepoPublicKey))),
}); err != nil {
return err
}