DodoApp: API to create new app
Change-Id: I20d73ef17cc03073c913fceb4f3bed7a26754cea
diff --git a/charts/dodo-app/templates/install.yaml b/charts/dodo-app/templates/install.yaml
index 30a6553..b8c1359 100644
--- a/charts/dodo-app/templates/install.yaml
+++ b/charts/dodo-app/templates/install.yaml
@@ -1,23 +1,35 @@
apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
+kind: ClusterRole
metadata:
- name: job-creator
+ name: {{ .Values.clusterRoleName }}
rules:
- apiGroups:
+ - ""
+ resources:
+ - namespaces
+ verbs:
+ - create
+- apiGroups:
- "batch"
resources:
- jobs
verbs:
- create
+- apiGroups:
+ - "helm.toolkit.fluxcd.io"
+ resources:
+ - helmreleases
+ verbs:
+ - get
---
apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
+kind: ClusterRoleBinding
metadata:
- name: job-creator
+ name: {{ .Values.clusterRoleName }}
roleRef:
apiGroup: rbac.authorization.k8s.io
- kind: Role
- name: job-creator
+ kind: ClusterRole
+ name: {{ .Values.clusterRoleName }}
subjects:
- kind: ServiceAccount
name: default
diff --git a/charts/dodo-app/values.yaml b/charts/dodo-app/values.yaml
index c606c50..66588cf 100644
--- a/charts/dodo-app/values.yaml
+++ b/charts/dodo-app/values.yaml
@@ -2,6 +2,7 @@
repository: giolekva/pcloud-installer
tag: latest
pullPolicy: Always
+clusterRoleName: dodo-app-creator
repoAddr: 192.168.0.11
sshPrivateKey: key
self: ""
diff --git a/core/installer/app_configs/dodo_app.cue b/core/installer/app_configs/dodo_app.cue
index e3add4f..32e0c45 100644
--- a/core/installer/app_configs/dodo_app.cue
+++ b/core/installer/app_configs/dodo_app.cue
@@ -6,6 +6,7 @@
input: {
repoAddr: string
+ registerWorkerAddr: string
appId: string
sshPrivateKey: string
}
@@ -158,7 +159,7 @@
repoAddr: input.repoAddr
sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
runCfg: base64.Encode(null, json.Marshal(_app.runConfiguration))
- manager: "http://dodo-app.\(release.namespace).svc.cluster.local/register-worker"
+ manager: input.registerWorkerAddr
volumes: [
for key, value in _app.volumes {
name: value.name
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index db8d5b8..44d2dbd 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -315,9 +315,10 @@
AppDir: "/foo/bar",
}
_, err = app.Render(release, env, map[string]any{
- "repoAddr": "",
- "appId": "",
- "sshPrivateKey": "",
+ "repoAddr": "",
+ "registerWorkerAddr": "",
+ "appId": "",
+ "sshPrivateKey": "",
}, nil)
if err != nil {
t.Fatal(err)
@@ -337,8 +338,8 @@
Namespace: "foo",
}
values := map[string]any{
- "appName": "",
"repoAddr": "",
+ "repoHost": "",
"gitRepoPublicKey": "",
}
rendered, err := a.Render(release, env, values, nil)
diff --git a/core/installer/cmd/dodo_app.go b/core/installer/cmd/dodo_app.go
index 3e4f6bd..5d54147 100644
--- a/core/installer/cmd/dodo_app.go
+++ b/core/installer/cmd/dodo_app.go
@@ -2,8 +2,6 @@
import (
"encoding/json"
- "errors"
- "fmt"
"log"
"os"
@@ -95,7 +93,8 @@
if err != nil {
return err
}
- softClient, err := soft.NewClient(dodoAppFlags.repoAddr, sshKey, log.Default())
+ cg := soft.RealClientGetter{}
+ softClient, err := cg.Get(dodoAppFlags.repoAddr, sshKey, log.Default())
if err != nil {
return err
}
@@ -103,144 +102,25 @@
if err != nil {
return err
}
- if err := softClient.AddRepository("config"); err == nil {
- repo, err := softClient.GetRepo("config")
- if err != nil {
- return err
- }
- appRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
- app, err := installer.FindEnvApp(appRepo, "dodo-app-instance")
- if err != nil {
- return err
- }
- nsc := installer.NewNoOpNamespaceCreator()
- if err != nil {
- return err
- }
- hf := installer.NewGitHelmFetcher()
- m, err := installer.NewAppManager(repo, nsc, jc, hf, "/")
- if err != nil {
- return err
- }
- if _, err := m.Install(app, "app", "/app", dodoAppFlags.namespace, map[string]any{
- "appName": "app",
- "repoAddr": softClient.GetRepoAddress("app"),
- "gitRepoPublicKey": dodoAppFlags.gitRepoPublicKey,
- }, installer.WithConfig(&env)); err != nil {
- return err
- }
- if cfg, err := m.FindInstance("app"); err != nil {
- return err
- } else {
- 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 err := softClient.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
- return err
- }
- if err := softClient.AddReadOnlyCollaborator("app", "fluxcd"); err != nil {
- return err
- }
- }
- } else if !errors.Is(err, soft.ErrorAlreadyExists) {
+ nsc, err := newNSCreator()
+ if err != nil {
return err
}
- if err := softClient.AddRepository("app"); err == nil {
- repo, err := softClient.GetRepo("app")
- if err != nil {
+ s := welcome.NewDodoAppServer(
+ dodoAppFlags.port,
+ dodoAppFlags.self,
+ string(sshKey),
+ dodoAppFlags.gitRepoPublicKey,
+ softClient,
+ dodoAppFlags.namespace,
+ nsc,
+ jc,
+ env,
+ )
+ if dodoAppFlags.appAdminKey != "" {
+ if err := s.CreateApp("app", dodoAppFlags.appAdminKey); err != nil {
return err
}
- if err := initRepo(repo); err != nil {
- return err
- }
- if err := welcome.UpdateDodoApp("app", softClient, dodoAppFlags.namespace, string(sshKey), jc, &env); err != nil {
- return err
- }
- if err := softClient.AddWebhook("app", fmt.Sprintf("http://%s/update", dodoAppFlags.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
- return err
- }
- if err := softClient.AddUser("app", dodoAppFlags.appAdminKey); err != nil {
- return err
- }
- if err := softClient.AddReadWriteCollaborator("app", "app"); err != nil {
- return err
- }
- } else if !errors.Is(err, soft.ErrorAlreadyExists) {
- return err
}
- s := welcome.NewDodoAppServer(dodoAppFlags.port, string(sshKey), softClient, dodoAppFlags.namespace, jc, env)
return s.Start()
}
-
-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/soft/client.go b/core/installer/soft/client.go
index 473efeb..4a5021e 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -26,9 +26,12 @@
Address() string
Signer() ssh.Signer
GetPublicKeys() ([]string, error)
+ RepoExists(name string) (bool, error)
GetRepo(name string) (RepoIO, error)
GetRepoAddress(name string) string
AddRepository(name string) error
+ UserExists(name string) (bool, error)
+ FindUser(pubKey string) (string, error)
AddUser(name, pubKey string) error
AddPublicKey(user string, pubKey string) error
RemovePublicKey(user string, pubKey string) error
@@ -92,6 +95,34 @@
return ss.signer
}
+func (ss *realClient) UserExists(name string) (bool, error) {
+ log.Printf("Adding user %s", name)
+ out, err := ss.RunCommand("user", "list")
+ if err != nil {
+ return false, err
+ }
+ return slices.Contains(strings.Fields(out), name), nil
+}
+
+func (ss *realClient) FindUser(pubKey string) (string, error) {
+ log.Printf("Finding user %s", pubKey)
+ pk := strings.Join(strings.Fields(pubKey)[:2], " ")
+ out, err := ss.RunCommand("user", "list")
+ if err != nil {
+ return "", err
+ }
+ for _, user := range strings.Fields(out) {
+ info, err := ss.RunCommand("user", "info", user)
+ if err != nil {
+ return "", err
+ }
+ if strings.Contains(info, pk) {
+ return user, nil
+ }
+ }
+ return "", nil
+}
+
func (ss *realClient) AddUser(name, pubKey string) error {
log.Printf("Adding user %s", name)
if _, err := ss.RunCommand("user", "create", name); err != nil {
@@ -138,10 +169,7 @@
return buf.String(), err
}
-func (ss *realClient) repoExists(name string) (bool, error) {
- // if err := ss.RunCommand("repo", "info", name); err == nil {
- // return ErrorAlreadyExists
- // }
+func (ss *realClient) RepoExists(name string) (bool, error) {
out, err := ss.RunCommand("repo", "list")
if err != nil {
return false, err
@@ -151,7 +179,7 @@
func (ss *realClient) AddRepository(name string) error {
log.Printf("Adding repository %s", name)
- if ok, err := ss.repoExists(name); ok {
+ if ok, err := ss.RepoExists(name); ok {
return ErrorAlreadyExists
} else if err != nil {
return err
diff --git a/core/installer/values-tmpl/dodo-app-instance.cue b/core/installer/values-tmpl/dodo-app-instance.cue
index e783ffe..e22f177 100644
--- a/core/installer/values-tmpl/dodo-app-instance.cue
+++ b/core/installer/values-tmpl/dodo-app-instance.cue
@@ -3,8 +3,8 @@
)
input: {
- appName: string
repoAddr: string
+ repoHost: string
gitRepoPublicKey: string
// TODO(gio): auto generate
fluxKeys: #SSHKey
@@ -22,7 +22,7 @@
apiVersion: "kustomize.toolkit.fluxcd.io/v1"
kind: "Kustomization"
metadata: {
- name: input.appName
+ name: "app"
namespace: release.namespace
}
spec: {
@@ -41,26 +41,26 @@
kind: "Secret"
type: "Opaque"
metadata: {
- name: input.appName
+ name: "app"
namespace: release.namespace
}
data: {
identity: base64.Encode(null, input.fluxKeys.private)
"identity.pub": base64.Encode(null, input.fluxKeys.public)
- known_hosts: base64.Encode(null, "soft-serve.\(release.namespace).svc.cluster.local \(input.gitRepoPublicKey)")
+ known_hosts: base64.Encode(null, "\(input.repoHost) \(input.gitRepoPublicKey)")
}
}
"config-source": {
apiVersion: "source.toolkit.fluxcd.io/v1"
kind: "GitRepository"
metadata: {
- name: input.appName
+ name: "app"
namespace: release.namespace
}
spec: {
interval: "1m0s"
ref: branch: "dodo"
- secretRef: name: input.appName
+ secretRef: name: "app"
timeout: "60s"
url: input.repoAddr
}
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
}