DodoApp: Support dev virtual machines
Change-Id: Ib7641adb5be477bdde7cd9a06df4b45aa65a1c01
diff --git a/core/installer/welcome/app_tmpl.go b/core/installer/welcome/app_tmpl.go
index 33f8d8d..fb512fa 100644
--- a/core/installer/welcome/app_tmpl.go
+++ b/core/installer/welcome/app_tmpl.go
@@ -1,15 +1,14 @@
package welcome
import (
+ "bytes"
"fmt"
- "io"
"io/fs"
"sort"
"strings"
"text/template"
"github.com/giolekva/pcloud/core/installer"
- "github.com/giolekva/pcloud/core/installer/soft"
)
const tmplSuffix = ".gotmpl"
@@ -69,7 +68,7 @@
}
type AppTmpl interface {
- Render(network installer.Network, subdomain string, out soft.RepoFS) error
+ Render(network installer.Network, subdomain string) (map[string][]byte, error)
}
type appTmplFS struct {
@@ -109,29 +108,20 @@
return &appTmplFS{files, tmpls}, nil
}
-func (a *appTmplFS) Render(network installer.Network, subdomain string, out soft.RepoFS) error {
+func (a *appTmplFS) Render(network installer.Network, subdomain string) (map[string][]byte, error) {
+ ret := map[string][]byte{}
+ for path, contents := range a.files {
+ ret[path] = contents
+ }
for path, tmpl := range a.tmpls {
- f, err := out.Writer(path)
- if err != nil {
- return err
- }
- defer f.Close()
- if err := tmpl.Execute(f, map[string]any{
+ var buf bytes.Buffer
+ if err := tmpl.Execute(&buf, map[string]any{
"Network": network,
"Subdomain": subdomain,
}); err != nil {
- return err
+ return nil, err
}
+ ret[path] = buf.Bytes()
}
- for path, contents := range a.files {
- f, err := out.Writer(path)
- if err != nil {
- return err
- }
- defer f.Close()
- if _, err := io.WriteString(f, string(contents)); err != nil {
- return err
- }
- }
- return nil
+ return ret, nil
}
diff --git a/core/installer/welcome/app_tmpl_test.go b/core/installer/welcome/app_tmpl_test.go
index daaf4c8..00e5a3c 100644
--- a/core/installer/welcome/app_tmpl_test.go
+++ b/core/installer/welcome/app_tmpl_test.go
@@ -6,10 +6,7 @@
"io/fs"
"testing"
- "github.com/go-git/go-billy/v5/memfs"
-
"github.com/giolekva/pcloud/core/installer"
- "github.com/giolekva/pcloud/core/installer/soft"
)
//go:embed app-tmpl
@@ -38,8 +35,7 @@
if err != nil {
t.Fatal(err)
}
- out := soft.NewBillyRepoFS(memfs.New())
- if err := a.Render(network, "testapp", out); err != nil {
+ if _, err := a.Render(network, "testapp"); err != nil {
t.Fatal(err)
}
}
@@ -57,8 +53,7 @@
if err != nil {
t.Fatal(err)
}
- out := soft.NewBillyRepoFS(memfs.New())
- if err := a.Render(network, "testapp", out); err != nil {
+ if _, err := a.Render(network, "testapp"); err != nil {
t.Fatal(err)
}
}
@@ -76,8 +71,7 @@
if err != nil {
t.Fatal(err)
}
- out := soft.NewBillyRepoFS(memfs.New())
- if err := a.Render(network, "testapp", out); err != nil {
+ if _, err := a.Render(network, "testapp"); err != nil {
t.Fatal(err)
}
}
diff --git a/core/installer/welcome/appmanager-tmpl/app.html b/core/installer/welcome/appmanager-tmpl/app.html
index b25f5b1..c6874cb 100644
--- a/core/installer/welcome/appmanager-tmpl/app.html
+++ b/core/installer/welcome/appmanager-tmpl/app.html
@@ -31,9 +31,9 @@
{{ else if eq $schema.Kind 1 }}
<label {{ if $schema.Advanced }}hidden{{ end }}>
{{ $schema.Name }}
- <input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}" />
+ <input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}" />
+ </label>
{{ else if eq $schema.Kind 4 }}
- </label>
<label {{ if $schema.Advanced }}hidden{{ end }}>
{{ $schema.Name }}
<input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}" />
diff --git a/core/installer/welcome/appmanager.go b/core/installer/welcome/appmanager.go
index 82421e9..7f168dc 100644
--- a/core/installer/welcome/appmanager.go
+++ b/core/installer/welcome/appmanager.go
@@ -163,7 +163,7 @@
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- instances, err := s.m.FindAllAppInstances(slug)
+ instances, err := s.m.GetAllAppInstances(slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -182,7 +182,7 @@
http.Error(w, "empty slug", http.StatusBadRequest)
return
}
- instance, err := s.m.FindInstance(slug)
+ instance, err := s.m.GetInstance(slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -339,7 +339,7 @@
}
resp := make([]app, 0)
for _, a := range apps {
- instances, err := s.m.FindAllAppInstances(a.Slug())
+ instances, err := s.m.GetAllAppInstances(a.Slug())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -393,7 +393,7 @@
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- instances, err := s.m.FindAllAppInstances(slug)
+ instances, err := s.m.GetAllAppInstances(slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -427,7 +427,7 @@
return
}
t, ok := s.tasks[slug]
- instance, err := s.m.FindInstance(slug)
+ instance, err := s.m.GetInstance(slug)
if err != nil && !ok {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -446,7 +446,7 @@
panic("MUST NOT REACH!")
}
}
- instances, err := s.m.FindAllAppInstances(a.Slug())
+ instances, err := s.m.GetAllAppInstances(a.Slug())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
diff --git a/core/installer/welcome/dodo-app-tmpl/app_status.html b/core/installer/welcome/dodo-app-tmpl/app_status.html
index e96b801..916525e 100644
--- a/core/installer/welcome/dodo-app-tmpl/app_status.html
+++ b/core/installer/welcome/dodo-app-tmpl/app_status.html
@@ -3,11 +3,23 @@
{{ end }}
{{- define "content" -}}
{{ .GitCloneCommand }}<br/>
+<form action="/{{ .Name }}/dev-branch/create" method="POST">
+ <fieldset class="grid">
+ <input type="text" name="branch" placeholder="branch" />
+ <button id="create-dev-branch-button" aria-busy="false" type="submit" name="create-dev-branch">create dev branch</button>
+ </fieldset>
+</form>
<a href="/{{ .Name }}/logs">Logs</a>
<hr class="divider">
{{- template "resources" .LastCommit -}}
<hr class="divider">
+<h3>Commit History</h3>
{{- range .Commits -}}
{{if eq .Status "OK" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="black" d="M21 7L9 19l-5.5-5.5l1.41-1.41L9 16.17L19.59 5.59z"/></svg>{{ else }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><path fill="black" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M6 11L11 6L24 19L37 6L42 11L29 24L42 37L37 42L24 29L11 42L6 37L19 24L6 11Z" clip-rule="evenodd"/></svg>{{ end }} <a href="/{{ $.Name }}/{{ .Hash }}">{{ .Hash }}</a> {{ .Message }}<br/>
{{- end -}}
+<hr class="divider">
+<h3>Branches</h3>
+{{- range .Branches -}}
+<a href="/{{ $.Name }}/branch/{{ . }}">{{ . }}</a><br/>
+{{- end -}}
{{- end -}}
diff --git a/core/installer/welcome/dodo-app-tmpl/base.html b/core/installer/welcome/dodo-app-tmpl/base.html
index 644ff41..4e9401b 100644
--- a/core/installer/welcome/dodo-app-tmpl/base.html
+++ b/core/installer/welcome/dodo-app-tmpl/base.html
@@ -20,11 +20,20 @@
</body>
</html>
{{ define "resources" }}
-{{- if gt (len .Volume) 0 -}}
-<h3>Volumes</h3>
-{{- range $v := .Volume -}}
-Name: {{ $v.Name }}<br/>
-Size: {{ $v.Size }}<br/>
+{{- if gt (len .Ingress) 0 -}}
+<h3>Ingress</h3>
+{{- range $i := .Ingress -}}
+Host: <a href="{{ $i.Host }}">{{ $i.Host }}</a><br/>
+<br/>
+{{- end -}}
+{{- end -}}
+{{- if gt (len .VirtualMachine) 0 -}}
+<h3>Virtual Machine</h3>
+{{- range $i := .VirtualMachine -}}
+Name: {{ $i.Name }}<br/>
+User: {{ $i.User }}<br/>
+CPU Cores: {{ $i.CPUCores }}<br/>
+Memory: {{ $i.Memory }}<br/>
<br/>
{{- end -}}
{{- end -}}
@@ -37,10 +46,11 @@
<br/>
{{- end -}}
{{- end -}}
-{{- if gt (len .Ingress) 0 -}}
-<h3>Ingress</h3>
-{{- range $i := .Ingress -}}
-Host: {{ $i.Host }}<br/>
+{{- if gt (len .Volume) 0 -}}
+<h3>Volumes</h3>
+{{- range $v := .Volume -}}
+Name: {{ $v.Name }}<br/>
+Size: {{ $v.Size }}<br/>
<br/>
{{- end -}}
{{- end -}}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index bd72aef..57de8b2 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -12,6 +12,7 @@
"io/fs"
"net/http"
"slices"
+ "strconv"
"strings"
"sync"
"time"
@@ -23,6 +24,7 @@
"github.com/giolekva/pcloud/core/installer/soft"
"github.com/giolekva/pcloud/core/installer/tasks"
+ "cuelang.org/go/cue"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
)
@@ -194,7 +196,21 @@
return s, nil
}
+func (s *DodoAppServer) getAppConfig(app, branch string) appConfig {
+ return s.appConfigs[fmt.Sprintf("%s-%s", app, branch)]
+}
+
+func (s *DodoAppServer) setAppConfig(app, branch string, cfg appConfig) {
+ s.appConfigs[fmt.Sprintf("%s-%s", app, branch)] = cfg
+}
+
func (s *DodoAppServer) Start() error {
+ // if err := s.client.DisableKeyless(); err != nil {
+ // return err
+ // }
+ // if err := s.client.DisableAnonAccess(); err != nil {
+ // return err
+ // }
e := make(chan error)
go func() {
r := mux.NewRouter()
@@ -207,6 +223,8 @@
r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
r.HandleFunc("/{app-name}/logs", s.handleAppLogs).Methods(http.MethodGet)
r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
+ r.HandleFunc("/{app-name}/dev-branch/create", s.handleCreateDevBranch).Methods(http.MethodPost)
+ r.HandleFunc("/{app-name}/branch/{branch}", s.handleAppStatus).Methods(http.MethodGet)
r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
@@ -216,7 +234,7 @@
r := mux.NewRouter()
r.HandleFunc("/update", s.handleAPIUpdate)
r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
- r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
+ r.HandleFunc("/api/add-public-key", s.handleAPIAddPublicKey).Methods(http.MethodPost)
if !s.external {
r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
}
@@ -224,7 +242,6 @@
}()
if !s.external {
go func() {
- rand.Seed(uint64(time.Now().UnixNano()))
s.syncUsers()
for {
delay := time.Duration(rand.Intn(60)+60) * time.Second
@@ -434,6 +451,7 @@
GitCloneCommand string
Commits []CommitMeta
LastCommit resourceData
+ Branches []string
}
func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
@@ -443,6 +461,10 @@
http.Error(w, "missing app-name", http.StatusBadRequest)
return
}
+ branch, ok := vars["branch"]
+ if !ok || branch == "" {
+ branch = "master"
+ }
u := r.Context().Value(userCtx)
if u == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
@@ -462,7 +484,7 @@
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
- commits, err := s.st.GetCommitHistory(appName)
+ commits, err := s.st.GetCommitHistory(appName, branch)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -481,6 +503,11 @@
}
lastCommitResources = r
}
+ branches, err := s.st.GetBranches(appName)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
data := appStatusData{
Navigation: []navItem{
navItem{"Home", "/"},
@@ -490,6 +517,10 @@
GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
Commits: commits,
LastCommit: lastCommitResources,
+ Branches: branches,
+ }
+ if branch != "master" {
+ data.Navigation = append(data.Navigation, navItem{branch, fmt.Sprintf("/%s/branch/%s", appName, branch)})
}
if err := s.tmplts.appStatus.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -512,10 +543,18 @@
Host string
}
+type vm struct {
+ Name string
+ User string
+ CPUCores int
+ Memory string
+}
+
type resourceData struct {
- Volume []volume
- PostgreSQL []postgresql
- Ingress []ingress
+ Volume []volume
+ PostgreSQL []postgresql
+ Ingress []ingress
+ VirtualMachine []vm
}
type commitStatusData struct {
@@ -659,7 +698,12 @@
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
+ if strings.HasPrefix(req.Ref, "refs/heads/dodo_") || req.Repository.Name == ConfigRepoName {
+ return
+ }
+ branch, ok := strings.CutPrefix(req.Ref, "refs/heads/")
+ if !ok {
+ http.Error(w, "invalid branch", http.StatusBadRequest)
return
}
// TODO(gio): Create commit record on app init as well
@@ -690,8 +734,10 @@
fmt.Printf("Error: could not find commit message")
return
}
- resources, err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks)
- if err = s.createCommit(req.Repository.Name, req.After, commitMsg, err, resources); err != nil {
+ s.l.Lock()
+ defer s.l.Unlock()
+ resources, err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, branch, s.getAppConfig(req.Repository.Name, branch).Namespace, networks, owner)
+ if err = s.createCommit(req.Repository.Name, branch, req.After, commitMsg, err, resources); err != nil {
fmt.Printf("Error: %s\n", err.Error())
return
}
@@ -710,6 +756,7 @@
}
func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
+ // TODO(gio): lock
vars := mux.Vars(r)
appName, ok := vars["app-name"]
if !ok || appName == "" {
@@ -782,6 +829,35 @@
http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
}
+func (s *DodoAppServer) handleCreateDevBranch(w http.ResponseWriter, r *http.Request) {
+ u := r.Context().Value(userCtx)
+ if u == nil {
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+ user, ok := u.(string)
+ if !ok {
+ http.Error(w, "could not get user", http.StatusInternalServerError)
+ return
+ }
+ vars := mux.Vars(r)
+ appName, ok := vars["app-name"]
+ if !ok || appName == "" {
+ http.Error(w, "missing app-name", http.StatusBadRequest)
+ return
+ }
+ branch := r.FormValue("branch")
+ if branch == "" {
+ http.Error(w, "missing network", http.StatusBadRequest)
+ return
+ }
+ if err := s.createDevBranch(appName, "master", branch, user); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
+}
+
type apiCreateAppReq struct {
AppType string `json:"type"`
AdminPublicKey string `json:"adminPublicKey"`
@@ -889,7 +965,52 @@
if err != nil {
return err
}
- commit, err := s.initRepo(appRepo, appType, n, subdomain)
+ files, err := s.renderAppConfigTemplate(appType, n, subdomain)
+ if err != nil {
+ return err
+ }
+ return s.createAppForBranch(appRepo, appName, "master", user, network, files)
+}
+
+func (s *DodoAppServer) createDevBranch(appName, fromBranch, toBranch, user string) error {
+ s.l.Lock()
+ defer s.l.Unlock()
+ fmt.Printf("Creating dev branch app: %s %s %s\n", appName, fromBranch, toBranch)
+ appRepo, err := s.client.GetRepoBranch(appName, fromBranch)
+ if err != nil {
+ return err
+ }
+ appCfg, err := soft.ReadFile(appRepo, "app.cue")
+ if err != nil {
+ return err
+ }
+ network, branchCfg, err := createDevBranchAppConfig(appCfg, toBranch, user)
+ if err != nil {
+ return err
+ }
+ return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.cue": branchCfg})
+}
+
+func (s *DodoAppServer) createAppForBranch(
+ repo soft.RepoIO,
+ appName string,
+ branch string,
+ user string,
+ network string,
+ files map[string][]byte,
+) error {
+ commit, err := repo.Do(func(fs soft.RepoFS) (string, error) {
+ for path, contents := range files {
+ if err := soft.WriteFile(fs, path, string(contents)); err != nil {
+ return "", err
+ }
+ }
+ return "init", nil
+ }, soft.WithCommitToBranch(branch))
+ if err != nil {
+ return err
+ }
+ networks, err := s.getNetworks(user)
if err != nil {
return err
}
@@ -908,12 +1029,13 @@
return err
}
namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
- s.appConfigs[appName] = appConfig{namespace, network}
- resources, err := s.updateDodoApp(instanceAppStatus, appName, namespace, networks)
+ s.setAppConfig(appName, branch, appConfig{namespace, network})
+ resources, err := s.updateDodoApp(instanceAppStatus, appName, branch, namespace, networks, user)
if err != nil {
+ fmt.Printf("Error: %s\n", err.Error())
return err
}
- if err = s.createCommit(appName, commit, initCommitMsg, err, resources); err != nil {
+ if err = s.createCommit(appName, branch, commit, initCommitMsg, err, resources); err != nil {
fmt.Printf("Error: %s\n", err.Error())
return err
}
@@ -926,6 +1048,7 @@
if err != nil {
return err
}
+ appPath := fmt.Sprintf("/%s/%s", appName, branch)
_, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
w, err := fs.Writer(appConfigsFile)
if err != nil {
@@ -938,11 +1061,12 @@
if _, err := m.Install(
instanceApp,
appName,
- "/"+appName,
+ appPath,
namespace,
map[string]any{
"repoAddr": s.client.GetRepoAddress(appName),
"repoHost": strings.Split(s.client.Address(), ":")[0],
+ "branch": fmt.Sprintf("dodo_%s", branch),
"gitRepoPublicKey": s.gitRepoPublicKey,
},
installer.WithConfig(&s.env),
@@ -957,7 +1081,11 @@
if err != nil {
return err
}
- cfg, err := m.FindInstance(appName)
+ return s.initAppACLs(m, appPath, appName, branch, user)
+}
+
+func (s *DodoAppServer) initAppACLs(m *installer.AppManager, path, appName, branch, user string) error {
+ cfg, err := m.GetInstance(path)
if err != nil {
return err
}
@@ -980,13 +1108,16 @@
return err
}
}
+ if branch != "master" {
+ return nil
+ }
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 {
+ if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
return err
}
- if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
+ 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 !s.external {
@@ -1010,16 +1141,25 @@
}
type apiAddAdminKeyReq struct {
- Public string `json:"public"`
+ User string `json:"user"`
+ PublicKey string `json:"publicKey"`
}
-func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
+func (s *DodoAppServer) handleAPIAddPublicKey(w http.ResponseWriter, r *http.Request) {
var req apiAddAdminKeyReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- if err := s.client.AddPublicKey("admin", req.Public); err != nil {
+ if req.User == "" {
+ http.Error(w, "invalid user", http.StatusBadRequest)
+ return
+ }
+ if req.PublicKey == "" {
+ http.Error(w, "invalid public key", http.StatusBadRequest)
+ return
+ }
+ if err := s.client.AddPublicKey(req.User, req.PublicKey); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -1037,12 +1177,16 @@
} `json:"input"`
}
+// TODO(gio): must not require owner, now we need it to bootstrap dev vm.
func (s *DodoAppServer) updateDodoApp(
appStatus installer.EnvApp,
- name, namespace string,
+ name string,
+ branch string,
+ namespace string,
networks []installer.Network,
+ owner string,
) (installer.ReleaseResources, error) {
- repo, err := s.client.GetRepo(name)
+ repo, err := s.client.GetRepoBranch(name, branch)
if err != nil {
return installer.ReleaseResources{}, err
}
@@ -1068,10 +1212,13 @@
"/.dodo/app",
namespace,
map[string]any{
- "repoAddr": repo.FullAddress(),
- "managerAddr": fmt.Sprintf("http://%s", s.self),
- "appId": name,
- "sshPrivateKey": s.sshKey,
+ "repoAddr": repo.FullAddress(),
+ "repoPublicAddr": s.repoPublicAddr,
+ "managerAddr": fmt.Sprintf("http://%s", s.self),
+ "appId": name,
+ "branch": branch,
+ "sshPrivateKey": s.sshKey,
+ "username": owner,
},
installer.WithNoPull(),
installer.WithNoPublish(),
@@ -1108,7 +1255,7 @@
}
return "install app", nil
},
- soft.WithCommitToBranch("dodo"),
+ soft.WithCommitToBranch(fmt.Sprintf("dodo_%s", branch)),
soft.WithForce(),
); err != nil {
return installer.ReleaseResources{}, err
@@ -1118,18 +1265,13 @@
return ret, nil
}
-func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) (string, error) {
+func (s *DodoAppServer) renderAppConfigTemplate(appType string, network installer.Network, subdomain string) (map[string][]byte, error) {
appType = strings.Replace(appType, ":", "-", 1)
appTmpl, err := s.appTmpls.Find(appType)
if err != nil {
- return "", err
+ return nil, err
}
- return repo.Do(func(fs soft.RepoFS) (string, error) {
- if err := appTmpl.Render(network, subdomain, repo); err != nil {
- return "", err
- }
- return initCommitMsg, nil
- })
+ return appTmpl.Render(network, subdomain)
}
func generatePassword() string {
@@ -1183,10 +1325,10 @@
}
}
-func (s *DodoAppServer) createCommit(name, hash, message string, err error, resources installer.ReleaseResources) error {
+func (s *DodoAppServer) createCommit(name, branch, hash, message string, err error, resources installer.ReleaseResources) error {
if err != nil {
fmt.Printf("Error: %s\n", err.Error())
- if err := s.st.CreateCommit(name, hash, message, "FAILED", err.Error(), nil); err != nil {
+ if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
fmt.Printf("Error: %s\n", err.Error())
return err
}
@@ -1194,13 +1336,13 @@
}
var resB bytes.Buffer
if err := json.NewEncoder(&resB).Encode(resources); err != nil {
- if err := s.st.CreateCommit(name, hash, message, "FAILED", err.Error(), nil); err != nil {
+ if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
fmt.Printf("Error: %s\n", err.Error())
return err
}
return err
}
- if err := s.st.CreateCommit(name, hash, message, "OK", "", resB.Bytes()); err != nil {
+ if err := s.st.CreateCommit(name, branch, hash, message, "OK", "", resB.Bytes()); err != nil {
fmt.Printf("Error: %s\n", err.Error())
return err
}
@@ -1446,9 +1588,75 @@
return resourceData{}, fmt.Errorf("no host")
}
ret.Ingress = append(ret.Ingress, ingress{host})
+ case "virtual-machine":
+ name, ok := r.Annotations["dodo.cloud/resource.virtual-machine.name"]
+ if !ok {
+ return resourceData{}, fmt.Errorf("no name")
+ }
+ user, ok := r.Annotations["dodo.cloud/resource.virtual-machine.user"]
+ if !ok {
+ return resourceData{}, fmt.Errorf("no user")
+ }
+ cpuCoresS, ok := r.Annotations["dodo.cloud/resource.virtual-machine.cpu-cores"]
+ if !ok {
+ return resourceData{}, fmt.Errorf("no cpu cores")
+ }
+ cpuCores, err := strconv.Atoi(cpuCoresS)
+ if err != nil {
+ return resourceData{}, fmt.Errorf("invalid cpu cores: %s", cpuCoresS)
+ }
+ memory, ok := r.Annotations["dodo.cloud/resource.virtual-machine.memory"]
+ if !ok {
+ return resourceData{}, fmt.Errorf("no memory")
+ }
+ ret.VirtualMachine = append(ret.VirtualMachine, vm{name, user, cpuCores, memory})
default:
fmt.Printf("Unknown resource: %+v\n", r.Annotations)
}
}
return ret, nil
}
+
+func createDevBranchAppConfig(from []byte, branch, username string) (string, []byte, error) {
+ cfg, err := installer.ParseCueAppConfig(installer.CueAppData{"app.cue": from})
+ if err != nil {
+ return "", nil, err
+ }
+ if err := cfg.Err(); err != nil {
+ return "", nil, err
+ }
+ if err := cfg.Validate(); err != nil {
+ return "", nil, err
+ }
+ subdomain := cfg.LookupPath(cue.ParsePath("app.ingress.subdomain"))
+ if err := subdomain.Err(); err != nil {
+ return "", nil, err
+ }
+ subdomainStr, err := subdomain.String()
+ network := cfg.LookupPath(cue.ParsePath("app.ingress.network"))
+ if err := network.Err(); err != nil {
+ return "", nil, err
+ }
+ networkStr, err := network.String()
+ if err != nil {
+ return "", nil, err
+ }
+ newCfg := map[string]any{}
+ if err := cfg.Decode(&newCfg); err != nil {
+ return "", nil, err
+ }
+ app, ok := newCfg["app"].(map[string]any)
+ if !ok {
+ return "", nil, fmt.Errorf("not a map")
+ }
+ app["ingress"].(map[string]any)["subdomain"] = fmt.Sprintf("%s-%s", branch, subdomainStr)
+ app["dev"] = map[string]any{
+ "enabled": true,
+ "username": username,
+ }
+ buf, err := json.MarshalIndent(newCfg, "", "\t")
+ if err != nil {
+ return "", nil, err
+ }
+ return networkStr, buf, nil
+}
diff --git a/core/installer/welcome/dodo_app_test.go b/core/installer/welcome/dodo_app_test.go
new file mode 100644
index 0000000..0f0f526
--- /dev/null
+++ b/core/installer/welcome/dodo_app_test.go
@@ -0,0 +1,24 @@
+package welcome
+
+import (
+ "testing"
+)
+
+func TestCreateDevBranch(t *testing.T) {
+ cfg := []byte(`
+app: {
+ type: "golang:1.22.0"
+ run: "main.go"
+ ingress: {
+ network: "private"
+ subdomain: "testapp"
+ auth: enabled: false
+ }
+}`)
+ network, newCfg, err := createDevBranchAppConfig(cfg, "foo", "bar")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(network)
+ t.Log(string(newCfg))
+}
diff --git a/core/installer/welcome/env_test.go b/core/installer/welcome/env_test.go
index f41110d..ee80e4e 100644
--- a/core/installer/welcome/env_test.go
+++ b/core/installer/welcome/env_test.go
@@ -15,6 +15,7 @@
"golang.org/x/crypto/ssh"
+ "cuelang.org/go/cue/errors"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-billy/v5/util"
@@ -117,6 +118,10 @@
return mockRepoIO{soft.NewBillyRepoFS(f.envFS), "foo.bar", f.t, &l}, nil
}
+func (f fakeSoftServeClient) GetRepoBranch(name, branch string) (soft.RepoIO, error) {
+ return f.GetRepo(name)
+}
+
func (f fakeSoftServeClient) GetAllRepos() ([]string, error) {
return []string{}, nil
}
@@ -176,6 +181,14 @@
return nil
}
+func (f fakeSoftServeClient) DisableAnonAccess() error {
+ return nil
+}
+
+func (f fakeSoftServeClient) DisableKeyless() error {
+ return nil
+}
+
type fakeClientGetter struct {
t *testing.T
envFS billy.Filesystem
@@ -279,6 +292,9 @@
if _, err := infraMgr.Install(app, "/infrastructure/dns-gateway", "dns-gateway", map[string]any{
"servers": []installer.EnvDNS{},
}); err != nil {
+ for _, e := range errors.Errors(err) {
+ t.Log(e)
+ }
t.Fatal(err)
}
}
diff --git a/core/installer/welcome/launcher.go b/core/installer/welcome/launcher.go
index a828f1a..88047d8 100644
--- a/core/installer/welcome/launcher.go
+++ b/core/installer/welcome/launcher.go
@@ -42,7 +42,7 @@
}
func (d *AppManagerDirectory) GetAllApps() ([]AppLauncherInfo, error) {
- all, err := d.AppManager.FindAllInstances()
+ all, err := d.AppManager.GetAllInstances()
if err != nil {
return nil, err
}
diff --git a/core/installer/welcome/store.go b/core/installer/welcome/store.go
index 06be06a..3f8428d 100644
--- a/core/installer/welcome/store.go
+++ b/core/installer/welcome/store.go
@@ -40,9 +40,10 @@
GetUserApps(username string) ([]string, error)
CreateApp(name, username string) error
GetAppOwner(name string) (string, error)
- CreateCommit(name, hash, message, status, error string, resources []byte) error
- GetCommitHistory(name string) ([]CommitMeta, error)
+ CreateCommit(name, branch, hash, message, status, error string, resources []byte) error
+ GetCommitHistory(name, branch string) ([]CommitMeta, error)
GetCommit(hash string) (Commit, error)
+ GetBranches(name string) ([]string, error)
}
func NewStore(cf soft.RepoIO, db *sql.DB) (Store, error) {
@@ -71,6 +72,7 @@
);
CREATE TABLE IF NOT EXISTS commits (
app_name TEXT,
+ branch TEXT,
hash TEXT,
message TEXT,
status TEXT,
@@ -186,15 +188,15 @@
return ret, nil
}
-func (s *storeImpl) CreateCommit(name, hash, message, status, error string, resources []byte) error {
- query := `INSERT INTO commits (app_name, hash, message, status, error, resources) VALUES (?, ?, ?, ?, ?, ?)`
- _, err := s.db.Exec(query, name, hash, message, status, error, resources)
+func (s *storeImpl) CreateCommit(name, branch, hash, message, status, error string, resources []byte) error {
+ 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
}
-func (s *storeImpl) GetCommitHistory(name string) ([]CommitMeta, error) {
- query := `SELECT hash, message, status, error FROM commits WHERE app_name = ?`
- rows, err := s.db.Query(query, name)
+func (s *storeImpl) GetCommitHistory(name, branch string) ([]CommitMeta, error) {
+ query := `SELECT hash, message, status, error FROM commits WHERE app_name = ? AND branch = ?`
+ rows, err := s.db.Query(query, name, branch)
if err != nil {
return nil, err
}
@@ -231,3 +233,25 @@
}
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)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ ret := []string{}
+ for rows.Next() {
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ var b string
+ if err := rows.Scan(&b); err != nil {
+ return nil, err
+ }
+ ret = append(ret, b)
+
+ }
+ return ret, nil
+}