DodoApp: Prepare dodo-app to support multiple app repositories
Previously Git repository storing configuration for PCloud
environment, linked dodo-app repositories directly. After this change,
dodo-app will first create config repository which will link
individual application repositories. And PCloud env will link to the
config repo. That way dodo-app manger will be able to create multiple
app repositories per installation.
Change-Id: I647cacda7a9a4f241d2acc28ae5d8bbd8c6424d6
diff --git a/apps/app-runner/main.go b/apps/app-runner/main.go
index 10d74ae..dce8ffe 100644
--- a/apps/app-runner/main.go
+++ b/apps/app-runner/main.go
@@ -16,6 +16,7 @@
)
var port = flag.Int("port", 3000, "Port to listen on")
+var appId = flag.String("app-id", "", "Application ID")
var repoAddr = flag.String("repo-addr", "", "Git repository address")
var sshKey = flag.String("ssh-key", "", "Private SSH key to access Git repository")
var appDir = flag.String("app-dir", "", "Path to store application repository locally")
@@ -97,6 +98,6 @@
if err := json.NewDecoder(r).Decode(&cmds); err != nil {
panic(err)
}
- s := NewServer(*port, *repoAddr, signer, *appDir, cmds, self, *manager)
+ s := NewServer(*port, *appId, *repoAddr, signer, *appDir, cmds, self, *manager)
s.Start()
}
diff --git a/apps/app-runner/server.go b/apps/app-runner/server.go
index 865f724..7985ac6 100644
--- a/apps/app-runner/server.go
+++ b/apps/app-runner/server.go
@@ -16,6 +16,7 @@
type Server struct {
l sync.Locker
port int
+ appId string
ready bool
cmd *exec.Cmd
repoAddr string
@@ -26,11 +27,12 @@
manager string
}
-func NewServer(port int, repoAddr string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
+func NewServer(port int, appId string, repoAddr string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
return &Server{
l: &sync.Mutex{},
port: port,
ready: false,
+ appId: appId,
repoAddr: repoAddr,
signer: signer,
appDir: appDir,
@@ -118,6 +120,7 @@
}
type pingReq struct {
+ AppId string `json:"appId"`
Address string `json:"address"`
}
@@ -128,7 +131,7 @@
s.pingManager()
}()
}()
- buf, err := json.Marshal(pingReq{s.self})
+ buf, err := json.Marshal(pingReq{s.appId, s.self})
if err != nil {
return
}
diff --git a/charts/app-runner/templates/install.yaml b/charts/app-runner/templates/install.yaml
index d2b7f70..1c33df9 100644
--- a/charts/app-runner/templates/install.yaml
+++ b/charts/app-runner/templates/install.yaml
@@ -92,6 +92,7 @@
command:
- app-runner
- --port=3000
+ - --app-id={{ .Values.appId }}
- --app-dir=/dodo-app
- --repo-addr={{ .Values.repoAddr }}
- --ssh-key=/pcloud/ssh-key/private
diff --git a/charts/app-runner/values.yaml b/charts/app-runner/values.yaml
index 97e793f..f0625e4 100644
--- a/charts/app-runner/values.yaml
+++ b/charts/app-runner/values.yaml
@@ -4,6 +4,7 @@
pullPolicy: Always
repoAddr: 192.168.0.11
sshPrivateKey: key
+appId: ""
runCfg: ""
appDir: /dodo-app
appPort: 8080
diff --git a/charts/dodo-app/templates/install.yaml b/charts/dodo-app/templates/install.yaml
index 4746869..30a6553 100644
--- a/charts/dodo-app/templates/install.yaml
+++ b/charts/dodo-app/templates/install.yaml
@@ -83,6 +83,8 @@
- --self={{ .Values.self }}
- --namespace={{ .Values.namespace }} # TODO(gio): maybe use .Release.Namespace ?
- --env-config=/pcloud/env-config/config.json
+ - --app-admin-key={{ .Values.appAdminKey }}
+ - --git-repo-public-key={{ .Values.gitRepoPublicKey }}
volumeMounts:
- name: ssh-key
readOnly: true
diff --git a/charts/dodo-app/values.yaml b/charts/dodo-app/values.yaml
index dcdc380..c606c50 100644
--- a/charts/dodo-app/values.yaml
+++ b/charts/dodo-app/values.yaml
@@ -7,3 +7,5 @@
self: ""
namespace: ""
envConfig: ""
+appAdminKey: ""
+gitRepoPublicKey: ""
diff --git a/core/installer/app_configs/app_base.cue b/core/installer/app_configs/app_base.cue
index 4138024..477a68d 100644
--- a/core/installer/app_configs/app_base.cue
+++ b/core/installer/app_configs/app_base.cue
@@ -173,6 +173,7 @@
...
}
+helm: {}
_helmValidate: {
for key, value in helm {
"\(key)": #Helm & value & {
diff --git a/core/installer/app_configs/dodo_app.cue b/core/installer/app_configs/dodo_app.cue
index c721863..e3add4f 100644
--- a/core/installer/app_configs/dodo_app.cue
+++ b/core/installer/app_configs/dodo_app.cue
@@ -6,6 +6,7 @@
input: {
repoAddr: string
+ appId: string
sshPrivateKey: string
}
@@ -153,6 +154,7 @@
}
appPort: _appPort
appDir: _appDir
+ appId: input.appId
repoAddr: input.repoAddr
sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
runCfg: base64.Encode(null, json.Marshal(_app.runConfiguration))
diff --git a/core/installer/app_repository.go b/core/installer/app_repository.go
index f35eb97..27b9b43 100644
--- a/core/installer/app_repository.go
+++ b/core/installer/app_repository.go
@@ -36,6 +36,7 @@
}
var envAppConfigs = []string{
+ "values-tmpl/dodo-app-instance.cue",
"values-tmpl/certificate-issuer-private.cue",
"values-tmpl/certificate-issuer-public.cue",
"values-tmpl/appmanager.cue",
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index 559de31..db8d5b8 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -316,9 +316,36 @@
}
_, err = app.Render(release, env, map[string]any{
"repoAddr": "",
+ "appId": "",
"sshPrivateKey": "",
}, nil)
if err != nil {
t.Fatal(err)
}
}
+
+func TestDodoAppInstance(t *testing.T) {
+ r := NewInMemoryAppRepository(CreateAllApps())
+ a, err := FindEnvApp(r, "dodo-app-instance")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if a == nil {
+ t.Fatal("returned app is nil")
+ }
+ release := Release{
+ Namespace: "foo",
+ }
+ values := map[string]any{
+ "appName": "",
+ "repoAddr": "",
+ "gitRepoPublicKey": "",
+ }
+ rendered, err := a.Render(release, env, values, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, r := range rendered.Resources {
+ t.Log(string(r))
+ }
+}
diff --git a/core/installer/cmd/dodo_app.go b/core/installer/cmd/dodo_app.go
index c45cd35..3e4f6bd 100644
--- a/core/installer/cmd/dodo_app.go
+++ b/core/installer/cmd/dodo_app.go
@@ -15,12 +15,14 @@
)
var dodoAppFlags struct {
- port int
- sshKey string
- repoAddr string
- self string
- namespace string
- envConfig string
+ port int
+ sshKey string
+ repoAddr string
+ self string
+ namespace string
+ envConfig string
+ appAdminKey string
+ gitRepoPublicKey string
}
func dodoAppCmd() *cobra.Command {
@@ -64,6 +66,18 @@
"",
"",
)
+ cmd.Flags().StringVar(
+ &dodoAppFlags.appAdminKey,
+ "app-admin-key",
+ "",
+ "",
+ )
+ cmd.Flags().StringVar(
+ &dodoAppFlags.gitRepoPublicKey,
+ "git-repo-public-key",
+ "",
+ "",
+ )
return cmd
}
@@ -89,6 +103,53 @@
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) {
+ return err
+ }
if err := softClient.AddRepository("app"); err == nil {
repo, err := softClient.GetRepo("app")
if err != nil {
@@ -97,12 +158,18 @@
if err := initRepo(repo); err != nil {
return err
}
- if err := welcome.UpdateDodoApp(softClient, dodoAppFlags.namespace, string(sshKey), jc, &env); err != nil {
+ 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
}
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index 1c93666..473efeb 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -8,6 +8,7 @@
"net"
"os"
"regexp"
+ "slices"
"strings"
"time"
@@ -93,7 +94,7 @@
func (ss *realClient) AddUser(name, pubKey string) error {
log.Printf("Adding user %s", name)
- if err := ss.RunCommand("user", "create", name); err != nil {
+ if _, err := ss.RunCommand("user", "create", name); err != nil {
return err
}
return ss.AddPublicKey(name, pubKey)
@@ -101,61 +102,83 @@
func (ss *realClient) MakeUserAdmin(name string) error {
log.Printf("Making user %s admin", name)
- return ss.RunCommand("user", "set-admin", name, "true")
+ _, err := ss.RunCommand("user", "set-admin", name, "true")
+ return err
}
func (ss *realClient) AddPublicKey(user string, pubKey string) error {
log.Printf("Adding public key: %s %s\n", user, pubKey)
- return ss.RunCommand("user", "add-pubkey", user, pubKey)
+ _, err := ss.RunCommand("user", "add-pubkey", user, pubKey)
+ return err
}
func (ss *realClient) RemovePublicKey(user string, pubKey string) error {
log.Printf("Removing public key: %s %s\n", user, pubKey)
- return ss.RunCommand("user", "remove-pubkey", user, pubKey)
+ _, err := ss.RunCommand("user", "remove-pubkey", user, pubKey)
+ return err
}
-func (ss *realClient) RunCommand(args ...string) error {
+func (ss *realClient) RunCommand(args ...string) (string, error) {
cmd := strings.Join(args, " ")
log.Printf("Running command %s", cmd)
client, err := ssh.Dial("tcp", ss.addr, ss.sshClientConfig())
if err != nil {
- return err
+ return "", err
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
- return err
+ return "", err
}
defer session.Close()
- session.Stdout = os.Stdout
+ var buf strings.Builder
+ session.Stdout = &buf
session.Stderr = os.Stderr
- return session.Run(cmd)
+ err = session.Run(cmd)
+ return buf.String(), err
+}
+
+func (ss *realClient) repoExists(name string) (bool, error) {
+ // if err := ss.RunCommand("repo", "info", name); err == nil {
+ // return ErrorAlreadyExists
+ // }
+ out, err := ss.RunCommand("repo", "list")
+ if err != nil {
+ return false, err
+ }
+ return slices.Contains(strings.Fields(out), name), nil
}
func (ss *realClient) AddRepository(name string) error {
log.Printf("Adding repository %s", name)
- if err := ss.RunCommand("repo", "info", name); err == nil {
+ if ok, err := ss.repoExists(name); ok {
return ErrorAlreadyExists
+ } else if err != nil {
+ return err
}
- return ss.RunCommand("repo", "create", name)
+ _, err := ss.RunCommand("repo", "create", name)
+ return err
}
func (ss *realClient) AddReadWriteCollaborator(repo, user string) error {
log.Printf("Adding read-write collaborator %s %s", repo, user)
- return ss.RunCommand("repo", "collab", "add", repo, user, "read-write")
+ _, err := ss.RunCommand("repo", "collab", "add", repo, user, "read-write")
+ return err
}
func (ss *realClient) AddReadOnlyCollaborator(repo, user string) error {
log.Printf("Adding read-only collaborator %s %s", repo, user)
- return ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
+ _, err := ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
+ return err
}
func (ss *realClient) AddWebhook(repo, url string, opts ...string) error {
log.Printf("Adding webhook %s %s", repo, url)
- return ss.RunCommand(append(
+ _, err := ss.RunCommand(append(
[]string{"repo", "webhook", "create", repo, url},
opts...,
)...)
+ return err
}
type Repository struct {
diff --git a/core/installer/values-tmpl/dodo-app-instance.cue b/core/installer/values-tmpl/dodo-app-instance.cue
new file mode 100644
index 0000000..e783ffe
--- /dev/null
+++ b/core/installer/values-tmpl/dodo-app-instance.cue
@@ -0,0 +1,68 @@
+import (
+ "encoding/base64"
+)
+
+input: {
+ appName: string
+ repoAddr: string
+ gitRepoPublicKey: string
+ // TODO(gio): auto generate
+ fluxKeys: #SSHKey
+}
+
+name: "Dodo App Instance"
+namespace: "dodo-app-instance"
+readme: "Deploy app by pushing to Git repository"
+description: "Deploy app by pushing to Git repository"
+icon: ""
+_domain: "\(input.subdomain).\(input.network.domain)"
+
+resources: {
+ "config-kustomization": {
+ apiVersion: "kustomize.toolkit.fluxcd.io/v1"
+ kind: "Kustomization"
+ metadata: {
+ name: input.appName
+ namespace: release.namespace
+ }
+ spec: {
+ interval: "1m"
+ path: "./"
+ sourceRef: {
+ kind: "GitRepository"
+ name: "app"
+ namespace: release.namespace
+ }
+ prune: true
+ }
+ }
+ "config-secret": {
+ apiVersion: "v1"
+ kind: "Secret"
+ type: "Opaque"
+ metadata: {
+ name: input.appName
+ 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)")
+ }
+ }
+ "config-source": {
+ apiVersion: "source.toolkit.fluxcd.io/v1"
+ kind: "GitRepository"
+ metadata: {
+ name: input.appName
+ namespace: release.namespace
+ }
+ spec: {
+ interval: "1m0s"
+ ref: branch: "dodo"
+ secretRef: name: input.appName
+ timeout: "60s"
+ url: input.repoAddr
+ }
+ }
+}
diff --git a/core/installer/values-tmpl/dodo-app.cue b/core/installer/values-tmpl/dodo-app.cue
index 80d0e4c..abc09b2 100644
--- a/core/installer/values-tmpl/dodo-app.cue
+++ b/core/installer/values-tmpl/dodo-app.cue
@@ -70,7 +70,7 @@
serviceType: "ClusterIP"
addressPool: ""
reservedIP: ""
- adminKey: strings.Join([input.adminKey, input.fluxKeys.public, input.dAppKeys.public], "\n")
+ adminKey: strings.Join([input.fluxKeys.public, input.dAppKeys.public], "\n")
privateKey: input.ssKeys.private
publicKey: input.ssKeys.public
ingress: {
@@ -97,6 +97,8 @@
self: "dodo-app.\(release.namespace).svc.cluster.local"
namespace: release.namespace
envConfig: base64.Encode(null, json.Marshal(global))
+ appAdminKey: input.adminKey
+ gitRepoPublicKey: input.ssKeys.public
}
}
}
@@ -106,15 +108,15 @@
apiVersion: "kustomize.toolkit.fluxcd.io/v1"
kind: "Kustomization"
metadata: {
- name: "app"
+ name: "config"
namespace: release.namespace
}
spec: {
interval: "1m"
- path: "./.dodo"
+ path: "./"
sourceRef: {
kind: "GitRepository"
- name: "app"
+ name: "config"
namespace: release.namespace
}
prune: true
@@ -125,7 +127,7 @@
kind: "Secret"
type: "Opaque"
metadata: {
- name: "app"
+ name: "config"
namespace: release.namespace
}
data: {
@@ -138,15 +140,15 @@
apiVersion: "source.toolkit.fluxcd.io/v1"
kind: "GitRepository"
metadata: {
- name: "app"
+ name: "config"
namespace: release.namespace
}
spec: {
interval: "1m0s"
- ref: branch: "dodo"
- secretRef: name: "app"
+ ref: branch: "master"
+ secretRef: name: "config"
timeout: "60s"
- url: "ssh://soft-serve.\(release.namespace).svc.cluster.local:22/app"
+ url: "ssh://soft-serve.\(release.namespace).svc.cluster.local:22/config"
}
}
}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 39d3a03..e032393 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -19,7 +19,7 @@
namespace string
env installer.EnvConfig
jc installer.JobCreator
- workers map[string]struct{}
+ workers map[string]map[string]struct{}
}
func NewDodoAppServer(
@@ -37,7 +37,7 @@
namespace,
env,
jc,
- map[string]struct{}{},
+ map[string]map[string]struct{}{},
}
}
@@ -49,7 +49,10 @@
}
type updateReq struct {
- Ref string `json:"ref"`
+ Ref string `json:"ref"`
+ Repository struct {
+ Name string `json:"name"`
+ } `json:"repository"`
}
func (s *DodoAppServer) handleUpdate(w http.ResponseWriter, r *http.Request) {
@@ -63,16 +66,16 @@
fmt.Println(err)
return
}
- if req.Ref != "refs/heads/master" {
+ if req.Ref != "refs/heads/master" || strings.HasPrefix(req.Repository.Name, "dodo") {
return
}
go func() {
time.Sleep(20 * time.Second)
- if err := UpdateDodoApp(s.client, s.namespace, s.sshKey, s.jc, &s.env); err != nil {
+ if err := UpdateDodoApp(req.Repository.Name, s.client, s.namespace, s.sshKey, s.jc, &s.env); err != nil {
fmt.Println(err)
}
}()
- for addr, _ := range s.workers {
+ for addr, _ := range s.workers[req.Repository.Name] {
go func() {
// TODO(gio): make port configurable
http.Get(fmt.Sprintf("http://%s:3000/update", addr))
@@ -81,6 +84,7 @@
}
type registerWorkerReq struct {
+ AppId string `json:"appId"`
Address string `json:"address"`
}
@@ -90,8 +94,10 @@
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- s.workers[req.Address] = struct{}{}
- fmt.Printf("registered worker: %s\n", req.Address)
+ if _, ok := s.workers[req.AppId]; !ok {
+ s.workers[req.AppId] = map[string]struct{}{}
+ }
+ s.workers[req.AppId][req.Address] = struct{}{}
}
type addAdminKeyReq struct {
@@ -110,8 +116,8 @@
}
}
-func UpdateDodoApp(client soft.Client, namespace string, sshKey string, jc installer.JobCreator, env *installer.EnvConfig) error {
- repo, err := client.GetRepo("app")
+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
}
@@ -141,6 +147,7 @@
namespace,
map[string]any{
"repoAddr": repo.FullAddress(),
+ "appId": name,
"sshPrivateKey": sshKey,
},
installer.WithConfig(env),