DodoApp: VMs load dodo specific env vars from the dodo app manager
Change-Id: I522619a3ba6cd6c78eb4fe1dd8c91ec490759fdf
diff --git a/core/installer/app_configs/app_base.cue b/core/installer/app_configs/app_base.cue
index f058c96..ac2d159 100644
--- a/core/installer/app_configs/app_base.cue
+++ b/core/installer/app_configs/app_base.cue
@@ -106,6 +106,7 @@
content: string
owner: string
permissions: string
+ defer: bool | *true
}
#CloudInit: {
@@ -234,7 +235,7 @@
content: """
[user]
name = \(username)
- email = \(username)@.\(domain)
+ email = \(username)@\(domain)
"""
owner: "\(username):\(username)"
diff --git a/core/installer/app_configs/dodo_app.cue b/core/installer/app_configs/dodo_app.cue
index 84f236b..2456838 100644
--- a/core/installer/app_configs/dodo_app.cue
+++ b/core/installer/app_configs/dodo_app.cue
@@ -35,24 +35,29 @@
cpuCores: 1
memory: "1Gi"
cloudInit: {
+ _loadEnvFile: "/home/\(username)/.dodo_env.sh"
+ writeFiles: [{
+ path: _loadEnvFile
+ content: "source <(curl -fsSL \(input.managerAddr)/api/apps/\(input.appId)/branch/\(input.branch)/env-profile)"
+ owner: "\(username):\(username)"
+ permissions: "0700"
+ },
+ {
+ path: "/home/\(username)/.bash_profile"
+ content: "source \(_loadEnvFile)"
+ owner: "\(username):\(username)"
+ permissions: "0700"
+ }]
runCmd: list.Concat([[
["sh", "-c", "chown \(username):\(username) /home/\(username)/.cache"],
["sh", "-c", "GIT_SSH_COMMAND='ssh -i /home/\(username)/.ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new' git clone --branch \(input.branch) \(input.repoPublicAddr)/\(input.appId) /home/\(username)/code"],
["sh", "-c", "chown -R \(username):\(username) /home/\(username)/code"],
- ["sh", "-c", "chown \(username):\(username) /home/\(username)/.gitconfig"],
+ ["sh", "-c", "chown -R \(username):\(username) /home/\(username)"],
], app.vm.cloudInit.runCmd])
}
}
}
-_vmName: "\(input.appId)-\(input.branch)"
-
-out: {
- vm: {
- "\(_vmName)": _devVM
- }
-}
-
#AppIngress: {
network: string
subdomain: string
@@ -62,31 +67,6 @@
baseURL: "https://\(subdomain).\(_network.domain)"
}
-#Volumes: {
- ...
-}
-
-#PostgreSQLs: {
- ...
-}
-
-app: {
- volumes: {
- for key, value in volumes {
- "\(key)": #volume & value & {
- name: key
- }
- }
- }
- postgresql: {
- for key, value in postgresql {
- "\(key)": #PostgreSQL & value & {
- name: key
- }
- }
- }
-}
-
#Command: {
bin: string
args: [...string] | *[]
@@ -106,20 +86,59 @@
#VMCustomization: {
cloudInit: #CloudInit
+ env: [...string] | *[]
}
#AppTmpl: {
type: string
ingress: #AppIngress
- volumes: #Volumes
- postgresql: #PostgreSQLs
+ volumes: {
+ for k, v in volumes {
+ "\(k)": #volume & v & {
+ name: k
+ }
+ }
+ ...
+ }
+ postgresql: {
+ for k, v in postgresql {
+ "\(k)": #PostgreSQL & v & {
+ name: k
+ }
+ }
+ ...
+ }
rootDir: string
runConfiguration: [...#Command]
dev: #Dev | *{ enabled: false }
vm: #VMCustomization
+
+ lastCmdEnv: [
+ for k, v in volumes {
+ "DODO_VOLUME_\(strings.ToUpper(k))=/dodo-volume/\(v.name)"
+ }
+ for k, v in postgresql {
+ "DODO_POSTGRESQL_\(strings.ToUpper(k))_ADDRESS=postgres-\(v.name).\(release.namespace).svc.cluster.local"
+ }
+ for k, v in postgresql {
+ "DODO_POSTGRESQL_\(strings.ToUpper(k))_USERNAME=postgres"
+ }
+ for k, v in postgresql {
+ "DODO_POSTGRESQL_\(strings.ToUpper(k))_PASSWORD=postgres"
+ }
+ for k, v in postgresql {
+ "DODO_POSTGRESQL_\(strings.ToUpper(k))_DATABASE=postgres"
+ }
+ ]
+
...
}
+envProfile: strings.Join(list.Concat([
+ app.vm.env,
+ [for e in app.lastCmdEnv { "export \(e)" }]
+]), "\n")
+
// Go app
_goVer1220: "golang:1.22.0"
@@ -128,12 +147,13 @@
#GoAppTmpl: #AppTmpl & {
type: _goVer1220 | _goVer1200
run: string | *"main.go"
- ingress: #AppIngress
- volumes: #Volumes
- postgresql: #PostgreSQLs
port: int | *8080
rootDir: _appDir
+ volumes: {...}
+ postgresql: {...}
+ lastCmdEnv: [...string]
+
runConfiguration: [{
bin: "/usr/local/go/bin/go",
args: ["mod", "tidy"]
@@ -142,34 +162,22 @@
args: ["build", "-o", ".app", run]
}, {
bin: ".app",
- env: [
- for k, v in volumes {
- "DODO_VOLUME_\(strings.ToUpper(k))=/dodo-volume/\(v.name)"
- }
- for k, v in postgresql {
- "DODO_POSTGRESQL_\(strings.ToUpper(k))_ADDRESS=postgres-\(v.name).\(release.namespace).svc.cluster.local"
- }
- for k, v in postgresql {
- "DODO_POSTGRESQL_\(strings.ToUpper(k))_USERNAME=postgres"
- }
- for k, v in postgresql {
- "DODO_POSTGRESQL_\(strings.ToUpper(k))_PASSWORD=postgres"
- }
- for k, v in postgresql {
- "DODO_POSTGRESQL_\(strings.ToUpper(k))_DATABASE=postgres"
- }
- ]
+ env: lastCmdEnv
}]
}
#GoApp1200: #GoAppTmpl & {
type: _goVer1200
- vm: cloudInit: runCmd: [
- ["sh", "-c", "wget https://go.dev/dl/go1.20.linux-amd64.tar.gz -O /tmp/go.tar.gz"],
- ["sh", "-c", "rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tar.gz"],
- ["sh", "-c", "echo \"export PATH=$PATH:/usr/local/go/bin\\n\" > /etc/environment"],
- ["sh", "-c", "rm /tmp/go.tar.gz"],
- ]
+ vm: {
+ env: [
+ "export PATH=$PATH:/usr/local/go/bin"
+ ]
+ cloudInit: runCmd: [
+ ["sh", "-c", "wget https://go.dev/dl/go1.20.linux-amd64.tar.gz -O /tmp/go.tar.gz"],
+ ["sh", "-c", "rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tar.gz"],
+ ["sh", "-c", "rm /tmp/go.tar.gz"],
+ ]
+ }
}
#GoApp1220: #GoAppTmpl & {
@@ -185,11 +193,13 @@
#HugoAppTmpl: #AppTmpl & {
type: _hugoLatest
ingress: #AppIngress
- volumes: {}
- postgresql: {}
port: int | *8080
rootDir: _appDir
+ volumes: {...}
+ postgresql: {...}
+ lastCmdEnv: [...string]
+
runConfiguration: [{
bin: "/usr/bin/hugo",
}, {
@@ -201,7 +211,8 @@
"--port=\(port)",
"--baseURL=\(ingress.baseURL)",
"--appendPort=false",
- ]
+ ]
+ env: lastCmdEnv
}]
}
@@ -211,19 +222,16 @@
#PHPAppTmpl: #AppTmpl & {
type: "php:8.2-apache"
- ingress: #AppIngress
- volumes: {}
- postgresql: {}
port: int | *80
rootDir: "/var/www/html"
+ volumes: {...}
+ postgresql: {...}
+ lastCmdEnv: [...string]
+
runConfiguration: [{
bin: "/usr/local/bin/apache2-foreground",
- env: [
- for k, v in volumes {
- "DODO_VOLUME_\(strings.ToUpper(k))=/dodo-volume/\(v.name)"
- }
- ]
+ env: lastCmdEnv
}]
}
@@ -232,67 +240,66 @@
#App: #GoApp | #HugoApp | #PHPApp
app: #App
-
_app: app
if !_app.dev.enabled {
{
- out: {
- ingress: {
- app: {
- network: networks[strings.ToLower(_app.ingress.network)]
- subdomain: _app.ingress.subdomain
- auth: _app.ingress.auth
- service: {
- name: "app-app"
- port: name: "app"
+ out: {
+ ingress: {
+ app: {
+ network: networks[strings.ToLower(_app.ingress.network)]
+ subdomain: _app.ingress.subdomain
+ auth: _app.ingress.auth
+ service: {
+ name: "app-app"
+ port: name: "app"
+ }
}
}
- }
- images: {
- app: {
- repository: "giolekva"
- name: "app-runner"
- tag: strings.Replace(_app.type, ":", "-", -1)
- pullPolicy: "Always"
+ images: {
+ app: {
+ repository: "giolekva"
+ name: "app-runner"
+ tag: strings.Replace(_app.type, ":", "-", -1)
+ pullPolicy: "Always"
+ }
}
- }
- charts: {
- app: {
- kind: "GitRepository"
- address: "https://code.v1.dodo.cloud/helm-charts"
- branch: "main"
- path: "charts/app-runner"
+ charts: {
+ app: {
+ kind: "GitRepository"
+ address: "https://code.v1.dodo.cloud/helm-charts"
+ branch: "main"
+ path: "charts/app-runner"
+ }
}
- }
- helm: {
- app: {
- chart: charts.app
- values: {
- image: {
- repository: images.app.fullName
- tag: images.app.tag
- pullPolicy: images.app.pullPolicy
- }
- runtimeClassName: "untrusted-external" // TODO(gio): make this part of the infra config
- appPort: _app.port
- appDir: _app.rootDir
- appId: input.appId
- repoAddr: input.repoAddr
- sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
- runCfg: base64.Encode(null, json.Marshal(_app.runConfiguration))
- managerAddr: input.managerAddr
- volumes: [
- for key, value in _app.volumes {
- name: value.name
- mountPath: "/dodo-volume/\(key)"
+ helm: {
+ app: {
+ chart: charts.app
+ values: {
+ image: {
+ repository: images.app.fullName
+ tag: images.app.tag
+ pullPolicy: images.app.pullPolicy
}
- ]
+ runtimeClassName: "untrusted-external" // TODO(gio): make this part of the infra config
+ appPort: _app.port
+ appDir: _app.rootDir
+ appId: input.appId
+ repoAddr: input.repoAddr
+ sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+ runCfg: base64.Encode(null, json.Marshal(_app.runConfiguration))
+ managerAddr: input.managerAddr
+ volumes: [
+ for key, value in _app.volumes {
+ name: value.name
+ mountPath: "/dodo-volume/\(key)"
+ }
+ ]
+ }
}
}
}
}
- }
}
if _app.dev.enabled {
@@ -322,4 +329,14 @@
}
}
+_vmName: "\(input.appId)-\(input.branch)"
+
+out: {
+ volumes: app.volumes
+ postgresql: app.postgresql
+ vm: {
+ "\(_vmName)": _devVM
+ }
+}
+
_appDir: "/dodo-app"
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index 2987c80..18cf85c 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -353,7 +353,7 @@
var dodoAppDevEnabledCue = `
app: {
- type: "golang:1.22.0"
+ type: "golang:1.20.0"
run: "main.go"
ingress: {
network: "private"
@@ -364,6 +364,12 @@
enabled: true
username: "gio"
}
+ volumes: {
+ data: size: "5Gi"
+ }
+ postgresql: {
+ db: size: "10Gi"
+ }
}`
func TestDodoAppDevDisabled(t *testing.T) {
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 9f60449..7f3d383 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -235,6 +235,7 @@
r.HandleFunc("/update", s.handleAPIUpdate)
r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
r.HandleFunc("/api/add-public-key", s.handleAPIAddPublicKey).Methods(http.MethodPost)
+ r.HandleFunc("/api/apps/{app-name}/branch/{branch}/env-profile", s.handleBranchEnvProfile).Methods(http.MethodGet)
if !s.external {
r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
}
@@ -528,6 +529,34 @@
}
}
+type appEnv struct {
+ Profile string `json:"envProfile"`
+}
+
+func (s *DodoAppServer) handleBranchEnvProfile(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ appName, ok := vars["app-name"]
+ if !ok || appName == "" {
+ http.Error(w, "missing app-name", http.StatusBadRequest)
+ return
+ }
+ branch, ok := vars["branch"]
+ if !ok || branch == "" {
+ branch = "master"
+ }
+ info, err := s.st.GetLastCommitInfo(appName, branch)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ var e appEnv
+ if err := json.NewDecoder(bytes.NewReader(info.Resources.RenderedRaw)).Decode(&e); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ fmt.Fprintln(w, e.Profile)
+}
+
type volume struct {
Name string
Size string
diff --git a/core/installer/welcome/store.go b/core/installer/welcome/store.go
index 3f8428d..7efdeaf 100644
--- a/core/installer/welcome/store.go
+++ b/core/installer/welcome/store.go
@@ -32,6 +32,11 @@
Resources installer.ReleaseResources
}
+type LastCommitInfo struct {
+ Hash string
+ Resources installer.ReleaseResources
+}
+
type Store interface {
CreateUser(username string, password []byte, network string) error
GetUserPassword(username string) ([]byte, error)
@@ -43,6 +48,7 @@
CreateCommit(name, branch, hash, message, status, error string, resources []byte) error
GetCommitHistory(name, branch string) ([]CommitMeta, error)
GetCommit(hash string) (Commit, error)
+ GetLastCommitInfo(name, branch string) (LastCommitInfo, error)
GetBranches(name string) ([]string, error)
}
@@ -79,6 +85,12 @@
error TEXT,
resources JSONB
);
+ CREATE TABLE IF NOT EXISTS branches (
+ app_name TEXT,
+ branch TEXT,
+ hash TEXT,
+ resources JSONB
+ );
`)
return err
@@ -189,9 +201,38 @@
}
func (s *storeImpl) CreateCommit(name, branch, hash, message, status, error string, resources []byte) error {
+ tx, err := s.db.Begin()
+ if err != nil {
+ return err
+ }
query := `INSERT INTO commits (app_name, branch, hash, message, status, error, resources) VALUES (?, ?, ?, ?, ?, ?, ?)`
- _, err := s.db.Exec(query, name, branch, hash, message, status, error, resources)
- return err
+ _, err = tx.Exec(query, name, branch, hash, message, status, error, resources)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+ branchQuery := `UPDATE branches SET hash = ?, resources = ? WHERE app_name = ? AND branch = ?`
+ r, err := tx.Exec(branchQuery, hash, resources, name, branch)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+ if cnt, err := r.RowsAffected(); err != nil {
+ tx.Rollback()
+ return err
+ } else if cnt == 0 {
+ branchQuery := `INSERT INTO branches (app_name, branch, hash, resources) VALUES (?, ?, ?, ?)`
+ _, err := tx.Exec(branchQuery, name, branch, hash, resources)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+ }
+ if err := tx.Commit(); err != nil {
+ tx.Rollback()
+ return err
+ }
+ return nil
}
func (s *storeImpl) GetCommitHistory(name, branch string) ([]CommitMeta, error) {
@@ -234,6 +275,23 @@
return ret, nil
}
+func (s *storeImpl) GetLastCommitInfo(name, branch string) (LastCommitInfo, error) {
+ query := `SELECT hash, resources FROM branches WHERE app_name = ? AND branch = ?`
+ row := s.db.QueryRow(query, name, branch)
+ if err := row.Err(); err != nil {
+ return LastCommitInfo{}, err
+ }
+ var ret LastCommitInfo
+ var res []byte
+ if err := row.Scan(&ret.Hash, &res); err != nil {
+ return LastCommitInfo{}, err
+ }
+ if err := json.NewDecoder(bytes.NewBuffer(res)).Decode(&ret.Resources); err != nil {
+ return LastCommitInfo{}, err
+ }
+ return ret, nil
+}
+
func (s *storeImpl) GetBranches(name string) ([]string, error) {
query := `SELECT DISTINCT branch FROM commits WHERE app_name = ?`
rows, err := s.db.Query(query, name)