DodoApp: API to create new app
Change-Id: I20d73ef17cc03073c913fceb4f3bed7a26754cea
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index e032393..f903d9b 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -10,42 +10,58 @@
"github.com/giolekva/pcloud/core/installer"
"github.com/giolekva/pcloud/core/installer/soft"
+
+ "github.com/gorilla/mux"
)
type DodoAppServer struct {
- port int
- sshKey string
- client soft.Client
- namespace string
- env installer.EnvConfig
- jc installer.JobCreator
- workers map[string]map[string]struct{}
+ port int
+ self string
+ sshKey string
+ gitRepoPublicKey string
+ client soft.Client
+ namespace string
+ env installer.EnvConfig
+ nsc installer.NamespaceCreator
+ jc installer.JobCreator
+ workers map[string]map[string]struct{}
+ appNs map[string]string
}
+// TODO(gio): Initialize appNs on startup
func NewDodoAppServer(
port int,
+ self string,
sshKey string,
+ gitRepoPublicKey string,
client soft.Client,
namespace string,
+ nsc installer.NamespaceCreator,
jc installer.JobCreator,
env installer.EnvConfig,
) *DodoAppServer {
return &DodoAppServer{
port,
+ self,
sshKey,
+ gitRepoPublicKey,
client,
namespace,
env,
+ nsc,
jc,
map[string]map[string]struct{}{},
+ map[string]string{},
}
}
func (s *DodoAppServer) Start() error {
- http.HandleFunc("/update", s.handleUpdate)
- http.HandleFunc("/register-worker", s.handleRegisterWorker)
- http.HandleFunc("/api/add-admin-key", s.handleAddAdminKey)
- return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
+ r := mux.NewRouter()
+ r.HandleFunc("/update", s.handleUpdate)
+ r.HandleFunc("/register-worker", s.handleRegisterWorker).Methods(http.MethodPost)
+ r.HandleFunc("/api/apps", s.handleCreateApp).Methods(http.MethodPost)
+ r.HandleFunc("/api/add-admin-key", s.handleAddAdminKey).Methods(http.MethodPost)
+ return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
}
type updateReq struct {
@@ -66,12 +82,12 @@
fmt.Println(err)
return
}
- if req.Ref != "refs/heads/master" || strings.HasPrefix(req.Repository.Name, "dodo") {
+ if req.Ref != "refs/heads/master" || strings.HasPrefix(req.Repository.Name, "config") {
return
}
go func() {
time.Sleep(20 * time.Second)
- if err := UpdateDodoApp(req.Repository.Name, s.client, s.namespace, s.sshKey, s.jc, &s.env); err != nil {
+ if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name]); err != nil {
fmt.Println(err)
}
}()
@@ -100,6 +116,138 @@
s.workers[req.AppId][req.Address] = struct{}{}
}
+type createAppReq struct {
+ AdminPublicKey string `json:"adminPublicKey"`
+}
+
+type createAppResp struct {
+ AppName string `json:"appName"`
+}
+
+func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
+ var req createAppReq
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ g := installer.NewFixedLengthRandomNameGenerator(3)
+ appName, err := g.Generate()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := s.CreateApp(appName, req.AdminPublicKey); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ resp := createAppResp{appName}
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func (s *DodoAppServer) CreateApp(appName, adminPublicKey string) error {
+ fmt.Printf("Creating app: %s\n", appName)
+ if ok, err := s.client.RepoExists(appName); err != nil {
+ return err
+ } else if ok {
+ return nil
+ }
+ if err := s.client.AddRepository(appName); err != nil {
+ return err
+ }
+ appRepo, err := s.client.GetRepo(appName)
+ if err != nil {
+ return err
+ }
+ if err := InitRepo(appRepo); err != nil {
+ return err
+ }
+ apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+ app, err := installer.FindEnvApp(apps, "dodo-app-instance")
+ if err != nil {
+ return err
+ }
+ suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
+ suffix, err := suffixGen.Generate()
+ if err != nil {
+ return err
+ }
+ namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
+ s.appNs[appName] = namespace
+ if err := s.updateDodoApp(appName, namespace); err != nil {
+ return err
+ }
+ if ok, err := s.client.RepoExists("config"); err != nil {
+ return err
+ } else if !ok {
+ if err := s.client.AddRepository("config"); err != nil {
+ return err
+ }
+ }
+ repo, err := s.client.GetRepo("config")
+ if err != nil {
+ return err
+ }
+ hf := installer.NewGitHelmFetcher()
+ m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
+ if err != nil {
+ return err
+ }
+ if _, err := m.Install(app, appName, "/"+appName, namespace, map[string]any{
+ "repoAddr": s.client.GetRepoAddress(appName),
+ "repoHost": strings.Split(s.client.Address(), ":")[0],
+ "gitRepoPublicKey": s.gitRepoPublicKey,
+ }, installer.WithConfig(&s.env)); err != nil {
+ return err
+ }
+ cfg, err := m.FindInstance(appName)
+ if err != nil {
+ return err
+ }
+ fluxKeys, ok := cfg.Input["fluxKeys"]
+ if !ok {
+ return fmt.Errorf("Fluxcd keys not found")
+ }
+ fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
+ if !ok {
+ return fmt.Errorf("Fluxcd keys not found")
+ }
+ if ok, err := s.client.UserExists("fluxcd"); err != nil {
+ return err
+ } else if ok {
+ if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
+ return err
+ }
+ } else {
+ if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
+ return err
+ }
+ }
+ if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
+ return err
+ }
+ if err := s.client.AddWebhook(appName, fmt.Sprintf("http://%s/update", s.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
+ return err
+ }
+ if user, err := s.client.FindUser(adminPublicKey); err != nil {
+ return err
+ } else if user != "" {
+ if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
+ return err
+ }
+ } else {
+ if err := s.client.AddUser(appName, adminPublicKey); err != nil {
+ return err
+ }
+ if err := s.client.AddReadWriteCollaborator(appName, appName); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
type addAdminKeyReq struct {
Public string `json:"public"`
}
@@ -116,22 +264,17 @@
}
}
-func UpdateDodoApp(name string, client soft.Client, namespace string, sshKey string, jc installer.JobCreator, env *installer.EnvConfig) error {
- repo, err := client.GetRepo(name)
- if err != nil {
- return err
- }
- nsc := installer.NewNoOpNamespaceCreator()
+func (s *DodoAppServer) updateDodoApp(name, namespace string) error {
+ repo, err := s.client.GetRepo(name)
if err != nil {
return err
}
hf := installer.NewGitHelmFetcher()
- m, err := installer.NewAppManager(repo, nsc, jc, hf, "/.dodo")
+ m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
if err != nil {
return err
}
appCfg, err := soft.ReadFile(repo, "app.cue")
- fmt.Println(string(appCfg))
if err != nil {
return err
}
@@ -146,11 +289,12 @@
"/.dodo/app",
namespace,
map[string]any{
- "repoAddr": repo.FullAddress(),
- "appId": name,
- "sshPrivateKey": sshKey,
+ "repoAddr": repo.FullAddress(),
+ "registerWorkerAddr": fmt.Sprintf("http://%s/register-worker", s.self),
+ "appId": name,
+ "sshPrivateKey": s.sshKey,
},
- installer.WithConfig(env),
+ installer.WithConfig(&s.env),
installer.WithLocalChartGenerator(lg),
installer.WithBranch("dodo"),
installer.WithForce(),
@@ -159,3 +303,71 @@
}
return nil
}
+
+const goMod = `module dodo.app
+
+go 1.18
+`
+
+const mainGo = `package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+)
+
+var port = flag.Int("port", 8080, "Port to listen on")
+
+func handler(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, "Hello from Dodo App!")
+}
+
+func main() {
+ flag.Parse()
+ http.HandleFunc("/", handler)
+ log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
+}
+`
+
+const appCue = `app: {
+ type: "golang:1.22.0"
+ run: "main.go"
+ ingress: {
+ network: "Private" // or Public
+ subdomain: "testapp"
+ auth: enabled: false
+ }
+}
+`
+
+func InitRepo(repo soft.RepoIO) error {
+ return repo.Do(func(fs soft.RepoFS) (string, error) {
+ {
+ w, err := fs.Writer("go.mod")
+ if err != nil {
+ return "", err
+ }
+ defer w.Close()
+ fmt.Fprint(w, goMod)
+ }
+ {
+ w, err := fs.Writer("main.go")
+ if err != nil {
+ return "", err
+ }
+ defer w.Close()
+ fmt.Fprintf(w, "%s", mainGo)
+ }
+ {
+ w, err := fs.Writer("app.cue")
+ if err != nil {
+ return "", err
+ }
+ defer w.Close()
+ fmt.Fprint(w, appCue)
+ }
+ return "go web app template", nil
+ })
+}
diff --git a/core/installer/welcome/env_test.go b/core/installer/welcome/env_test.go
index d3f5d42..4acc576 100644
--- a/core/installer/welcome/env_test.go
+++ b/core/installer/welcome/env_test.go
@@ -109,6 +109,10 @@
return []string{}, nil
}
+func (f fakeSoftServeClient) RepoExists(name string) (bool, error) {
+ return false, nil
+}
+
func (f fakeSoftServeClient) GetRepo(name string) (soft.RepoIO, error) {
var l sync.Mutex
return mockRepoIO{soft.NewBillyRepoFS(f.envFS), "foo.bar", f.t, &l}, nil
@@ -122,6 +126,13 @@
return nil
}
+func (f fakeSoftServeClient) UserExists(name string) (bool, error) {
+ return false, nil
+}
+func (f fakeSoftServeClient) FindUser(pubKey string) (string, error) {
+ return "", nil
+}
+
func (f fakeSoftServeClient) AddUser(name, pubKey string) error {
return nil
}