diff --git a/internal/app/compose.go b/internal/app/compose.go index ec4f378..5104ceb 100644 --- a/internal/app/compose.go +++ b/internal/app/compose.go @@ -2,6 +2,7 @@ package app import ( "fmt" + "strconv" "strings" "gopkg.in/yaml.v3" @@ -34,13 +35,17 @@ func getServiceName(compose string) string { return names[0] } -func generateCompose(image string, port int, envVars string, volumes string) string { +func generateCompose(image string, port int, envVars string, volumes string, appName string) string { + svcName := "app" + if appName != "" { + svcName = sanitizeAppName(appName) + } compose := fmt.Sprintf(`services: - app: + %s: image: %s restart: unless-stopped networks: - - dockify`, image) + - dockify`, svcName, image) if envVars != "" { compose += "\n environment:" @@ -178,3 +183,83 @@ func splitEnvVars(envVars string) []string { } return result } + +type simpleFields struct { + Image string + Port int + EnvVars string + Volumes string +} + +func parseSimpleFields(compose string) simpleFields { + var doc yaml.Node + if err := yaml.Unmarshal([]byte(compose), &doc); err != nil { + return simpleFields{} + } + if doc.Kind != yaml.DocumentNode || len(doc.Content) == 0 { + return simpleFields{} + } + + root := doc.Content[0] + if root.Kind != yaml.MappingNode { + return simpleFields{} + } + + // Find services node + var servicesNode *yaml.Node + for i := 0; i+1 < len(root.Content); i += 2 { + if root.Content[i].Value == "services" { + servicesNode = root.Content[i+1] + break + } + } + if servicesNode == nil || servicesNode.Kind != yaml.MappingNode || len(servicesNode.Content) < 2 { + return simpleFields{} + } + + // Take the first service + svc := servicesNode.Content[1] + if svc.Kind != yaml.MappingNode { + return simpleFields{} + } + + var sf simpleFields + for i := 0; i+1 < len(svc.Content); i += 2 { + key := svc.Content[i].Value + val := svc.Content[i+1] + + switch key { + case "image": + sf.Image = val.Value + case "expose": + if val.Kind == yaml.SequenceNode && len(val.Content) > 0 { + portStr := strings.Trim(val.Content[0].Value, "\"") + if p, err := strconv.Atoi(portStr); err == nil { + sf.Port = p + } + } + case "environment": + if val.Kind == yaml.SequenceNode { + var lines []string + for _, item := range val.Content { + if item.Value != "" { + lines = append(lines, item.Value) + } + } + sf.EnvVars = strings.Join(lines, "\n") + } + case "volumes": + if val.Kind == yaml.SequenceNode { + var lines []string + for _, item := range val.Content { + if item.Value != "" { + lines = append(lines, item.Value) + } + } + sf.Volumes = strings.Join(lines, "\n") + } + } + } + + return sf +} diff --git a/internal/app/compose_test.go b/internal/app/compose_test.go index 80d6c6f..14f7576 100644 --- a/internal/app/compose_test.go +++ b/internal/app/compose_test.go @@ -6,7 +6,7 @@ import ( ) func TestGenerateSimple(t *testing.T) { - c := generateCompose("nginx:alpine", 80, "FOO=bar,BAZ=qux", "") + c := generateCompose("nginx:alpine", 80, "FOO=bar,BAZ=qux", "", "") if c == "" { t.Fatal("empty compose") } @@ -25,8 +25,28 @@ func TestGenerateSimple(t *testing.T) { } } +func TestGenerateSimpleWithAppName(t *testing.T) { + c := generateCompose("nginx:alpine", 80, "FOO=bar,BAZ=qux", "", "my-app") + if c == "" { + t.Fatal("empty compose") + } + + names, err := parseServiceNames(c) + if err != nil { + t.Fatalf("parse: %v", err) + } + if len(names) != 1 || names[0] != "my-app" { + t.Fatalf("expected service [my-app], got %v", names) + } + + sn := getServiceName(c) + if sn != "my-app" { + t.Fatalf("expected my-app, got %s", sn) + } +} + func TestGenerateWithVolumes(t *testing.T) { - c := generateCompose("postgres:16", 5432, "POSTGRES_PASSWORD=secret", "./db:/var/lib/postgresql/data") + c := generateCompose("postgres:16", 5432, "POSTGRES_PASSWORD=secret", "./db:/var/lib/postgresql/data", "") if c == "" { t.Fatal("empty compose") } @@ -131,3 +151,55 @@ func TestRenameFirstServiceNoServices(t *testing.T) { t.Fatal("compose without services should be returned unchanged") } } + +func TestParseSimpleFields(t *testing.T) { + compose := `services: + my-app: + image: nginx:alpine + restart: unless-stopped + networks: + - dockify + environment: + - FOO=bar + - BAZ=qux + expose: + - "80" + volumes: + - ./data:/data +networks: + dockify: + external: true` + + sf := parseSimpleFields(compose) + if sf.Image != "nginx:alpine" { + t.Fatalf("expected nginx:alpine, got %q", sf.Image) + } + if sf.Port != 80 { + t.Fatalf("expected 80, got %d", sf.Port) + } + if sf.EnvVars != "FOO=bar\nBAZ=qux" { + t.Fatalf("expected FOO=bar\\nBAZ=qux, got %q", sf.EnvVars) + } + if sf.Volumes != "./data:/data" { + t.Fatalf("expected ./data:/data, got %q", sf.Volumes) + } +} + +func TestParseSimpleFieldsEmpty(t *testing.T) { + sf := parseSimpleFields("") + if sf.Image != "" || sf.Port != 0 || sf.EnvVars != "" || sf.Volumes != "" { + t.Fatal("expected empty fields for empty compose") + } +} + +func TestParseSimpleFieldsMultipleServices(t *testing.T) { + compose := `services: + web: + image: nginx + db: + image: postgres` + sf := parseSimpleFields(compose) + if sf.Image != "nginx" { + t.Fatalf("expected nginx, got %q", sf.Image) + } +} diff --git a/internal/app/handler.go b/internal/app/handler.go index f46dc07..f080dcb 100644 --- a/internal/app/handler.go +++ b/internal/app/handler.go @@ -2,6 +2,7 @@ package app import ( "encoding/json" + "fmt" "net/http" "strconv" "strings" @@ -28,19 +29,18 @@ func (h *Handler) List(w http.ResponseWriter, r *http.Request) { func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { var input struct { - Name string `json:"name"` - ServerID int64 `json:"server_id"` - Domain string `json:"domain"` - Port int `json:"port"` - Compose string `json:"compose"` - Image string `json:"image"` - EnvVars string `json:"env_vars"` - Volumes string `json:"volumes"` - GitRepo string `json:"git_repo"` - GitBranch string `json:"git_branch"` - AuthUser string `json:"auth_user"` - AuthPass string `json:"auth_pass"` - UniqueServiceName bool `json:"unique_service_name"` + Name string `json:"name"` + ServerID int64 `json:"server_id"` + Domain string `json:"domain"` + Port int `json:"port"` + Compose string `json:"compose"` + Image string `json:"image"` + EnvVars string `json:"env_vars"` + Volumes string `json:"volumes"` + GitRepo string `json:"git_repo"` + GitBranch string `json:"git_branch"` + AuthUser string `json:"auth_user"` + AuthPass string `json:"auth_pass"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { @@ -55,7 +55,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { compose := input.Compose if compose == "" && input.Image != "" { - compose = generateCompose(input.Image, input.Port, input.EnvVars, input.Volumes) + compose = generateCompose(input.Image, input.Port, input.EnvVars, input.Volumes, input.Name) } if compose == "" { jsonResponse(w, http.StatusBadRequest, map[string]string{"error": "provide either compose or image"}) @@ -71,17 +71,21 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { input.ServerID = id } + composeMode := "advanced" + if input.Image != "" && (input.Compose == "" || !strings.Contains(input.Compose, "services:")) { + composeMode = "simple" + } app := &App{ - Name: input.Name, - ServerID: input.ServerID, - Domain: input.Domain, - Port: input.Port, - Compose: compose, - GitRepo: input.GitRepo, - GitBranch: input.GitBranch, - AuthUser: input.AuthUser, - AuthPass: input.AuthPass, - UniqueServiceName: input.UniqueServiceName, + Name: input.Name, + ServerID: input.ServerID, + Domain: input.Domain, + Port: input.Port, + Compose: compose, + GitRepo: input.GitRepo, + GitBranch: input.GitBranch, + AuthUser: input.AuthUser, + AuthPass: input.AuthPass, + ComposeMode: composeMode, } if err := h.service.Create(app); err != nil { @@ -347,13 +351,16 @@ func (h *WebHandler) AppAddForm(w http.ResponseWriter, r *http.Request, render R gitBranch = "main" } + mode := r.FormValue("mode") compose := strings.TrimSpace(r.FormValue("compose")) image := strings.TrimSpace(r.FormValue("image")) envVars := strings.TrimSpace(r.FormValue("env_vars")) volumes := strings.TrimSpace(r.FormValue("volumes")) - if image != "" && (compose == "" || !strings.Contains(compose, "services:")) { - compose = generateCompose(image, port, envVars, volumes) + composeMode := "advanced" + if mode == "simple" && image != "" { + compose = generateCompose(image, port, envVars, volumes, strings.TrimSpace(r.FormValue("name"))) + composeMode = "simple" } app := &App{ @@ -366,7 +373,7 @@ func (h *WebHandler) AppAddForm(w http.ResponseWriter, r *http.Request, render R GitBranch: gitBranch, AuthUser: strings.TrimSpace(r.FormValue("auth_user")), AuthPass: strings.TrimSpace(r.FormValue("auth_pass")), - UniqueServiceName: r.FormValue("unique_service_name") == "1", + ComposeMode: composeMode, } if app.Name == "" || app.Compose == "" { @@ -421,13 +428,28 @@ func (h *WebHandler) AppEditPage(w http.ResponseWriter, r *http.Request, render servers, _ := h.serverRepo.List() secrets, _ := h.service.ListSecrets(id) files, _ := h.service.ListFiles(id) + + // Parse simple fields from compose for pre-filling the edit form + sf := parseSimpleFields(app.Compose) + image := sf.Image + envVars := sf.EnvVars + volumes := sf.Volumes + if sf.Port > 0 { + app.Port = sf.Port + } + // If compose generated with app name as service, image is reliable. + // Otherwise fall back to showing compose textarea (advanced mode in template) + render(w, r, http.StatusOK, "apps_add.html", map[string]interface{}{ - "Title": "Edit " + app.Name, - "Servers": servers, - "App": app, - "Secrets": secrets, - "Files": files, - "IsEdit": true, + "Title": "Edit " + app.Name, + "Servers": servers, + "App": app, + "Secrets": secrets, + "Files": files, + "IsEdit": true, + "Image": image, + "EnvVars": envVars, + "Volumes": volumes, }) } @@ -456,13 +478,21 @@ func (h *WebHandler) AppEditForm(w http.ResponseWriter, r *http.Request, render } port, _ := strconv.Atoi(r.FormValue("port")) - compose := strings.TrimSpace(r.FormValue("compose")) - image := strings.TrimSpace(r.FormValue("image")) envVars := strings.TrimSpace(r.FormValue("env_vars")) volumes := strings.TrimSpace(r.FormValue("volumes")) - if image != "" && (compose == "" || !strings.Contains(compose, "services:")) { - compose = generateCompose(image, port, envVars, volumes) + mode := r.FormValue("mode") + compose := strings.TrimSpace(r.FormValue("compose")) + image := strings.TrimSpace(r.FormValue("image")) + + if mode == "simple" && image != "" { + compose = generateCompose(image, port, envVars, volumes, app.Name) + app.ComposeMode = "simple" + } else { + if compose == "" { + compose = app.Compose + } + app.ComposeMode = "advanced" } app.Name = strings.TrimSpace(r.FormValue("name")) @@ -474,7 +504,6 @@ func (h *WebHandler) AppEditForm(w http.ResponseWriter, r *http.Request, render app.GitBranch = strings.TrimSpace(r.FormValue("git_branch")) app.AuthUser = strings.TrimSpace(r.FormValue("auth_user")) app.AuthPass = strings.TrimSpace(r.FormValue("auth_pass")) - app.UniqueServiceName = r.FormValue("unique_service_name") == "1" if app.GitBranch == "" { app.GitBranch = "main" @@ -532,9 +561,21 @@ func (h *WebHandler) AppDetailPage(w http.ResponseWriter, r *http.Request, rende secrets, _ := h.service.ListSecrets(id) files, _ := h.service.ListFiles(id) + // Resolve server name for display + serverName := fmt.Sprintf("#%d", app.ServerID) + if servers, err := h.serverRepo.List(); err == nil { + for _, svr := range servers { + if svr.ID == app.ServerID { + serverName = svr.Name + break + } + } + } + render(w, r, http.StatusOK, "apps_detail.html", map[string]interface{}{ "Title": app.Name, "App": app, + "ServerName": serverName, "Deployments": deps, "Secrets": secrets, "Files": files, diff --git a/internal/app/model.go b/internal/app/model.go index 1853fc4..92a3813 100644 --- a/internal/app/model.go +++ b/internal/app/model.go @@ -14,7 +14,7 @@ type App struct { AuthUser string `json:"auth_user"` AuthPass string `json:"auth_pass"` Status string `json:"status"` - UniqueServiceName bool `json:"unique_service_name"` + ComposeMode string `json:"compose_mode"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/internal/app/repository.go b/internal/app/repository.go index d598308..72439d3 100644 --- a/internal/app/repository.go +++ b/internal/app/repository.go @@ -16,7 +16,7 @@ func NewRepository(db *sql.DB) *Repository { func (r *Repository) List() ([]App, error) { rows, err := r.db.Query(` SELECT id, name, server_id, domain, port, compose, - git_repo, git_branch, auth_user, auth_pass, status, unique_service_name, created_at, updated_at + git_repo, git_branch, auth_user, auth_pass, status, compose_mode, created_at, updated_at FROM apps ORDER BY name ASC `) if err != nil { @@ -30,7 +30,7 @@ func (r *Repository) List() ([]App, error) { var gitRepo, gitBranch sql.NullString if err := rows.Scan( &a.ID, &a.Name, &a.ServerID, &a.Domain, &a.Port, &a.Compose, - &gitRepo, &gitBranch, &a.AuthUser, &a.AuthPass, &a.Status, &a.UniqueServiceName, &a.CreatedAt, &a.UpdatedAt, + &gitRepo, &gitBranch, &a.AuthUser, &a.AuthPass, &a.Status, &a.ComposeMode, &a.CreatedAt, &a.UpdatedAt, ); err != nil { return nil, err } @@ -46,11 +46,11 @@ func (r *Repository) Get(id int64) (*App, error) { var gitRepo, gitBranch sql.NullString err := r.db.QueryRow(` SELECT id, name, server_id, domain, port, compose, - git_repo, git_branch, auth_user, auth_pass, status, unique_service_name, created_at, updated_at + git_repo, git_branch, auth_user, auth_pass, status, compose_mode, created_at, updated_at FROM apps WHERE id = ? `, id).Scan( &a.ID, &a.Name, &a.ServerID, &a.Domain, &a.Port, &a.Compose, - &gitRepo, &gitBranch, &a.AuthUser, &a.AuthPass, &a.Status, &a.UniqueServiceName, &a.CreatedAt, &a.UpdatedAt, + &gitRepo, &gitBranch, &a.AuthUser, &a.AuthPass, &a.Status, &a.ComposeMode, &a.CreatedAt, &a.UpdatedAt, ) if err == sql.ErrNoRows { return nil, nil @@ -66,7 +66,7 @@ func (r *Repository) Get(id int64) (*App, error) { func (r *Repository) FindAllByGitRepo(repo, branch string) ([]App, error) { rows, err := r.db.Query(` SELECT id, name, server_id, domain, port, compose, - git_repo, git_branch, auth_user, auth_pass, status, unique_service_name, created_at, updated_at + git_repo, git_branch, auth_user, auth_pass, status, compose_mode, created_at, updated_at FROM apps WHERE git_repo = ? AND git_branch = ? ORDER BY id `, repo, branch) if err != nil { @@ -80,7 +80,7 @@ func (r *Repository) FindAllByGitRepo(repo, branch string) ([]App, error) { var gitRepo, gitBranch sql.NullString if err := rows.Scan( &a.ID, &a.Name, &a.ServerID, &a.Domain, &a.Port, &a.Compose, - &gitRepo, &gitBranch, &a.AuthUser, &a.AuthPass, &a.Status, &a.UniqueServiceName, &a.CreatedAt, &a.UpdatedAt, + &gitRepo, &gitBranch, &a.AuthUser, &a.AuthPass, &a.Status, &a.ComposeMode, &a.CreatedAt, &a.UpdatedAt, ); err != nil { return nil, err } @@ -96,11 +96,11 @@ func (r *Repository) FindByGitRepo(repo, branch string) (*App, error) { var gitRepo, gitBranch sql.NullString err := r.db.QueryRow(` SELECT id, name, server_id, domain, port, compose, - git_repo, git_branch, auth_user, auth_pass, status, unique_service_name, created_at, updated_at + git_repo, git_branch, auth_user, auth_pass, status, compose_mode, created_at, updated_at FROM apps WHERE git_repo = ? AND git_branch = ? LIMIT 1 `, repo, branch).Scan( &a.ID, &a.Name, &a.ServerID, &a.Domain, &a.Port, &a.Compose, - &gitRepo, &gitBranch, &a.AuthUser, &a.AuthPass, &a.Status, &a.UniqueServiceName, &a.CreatedAt, &a.UpdatedAt, + &gitRepo, &gitBranch, &a.AuthUser, &a.AuthPass, &a.Status, &a.ComposeMode, &a.CreatedAt, &a.UpdatedAt, ) if err == sql.ErrNoRows { return nil, nil @@ -116,7 +116,7 @@ func (r *Repository) FindByGitRepo(repo, branch string) (*App, error) { func (r *Repository) ListByServer(serverID int64) ([]App, error) { rows, err := r.db.Query(` SELECT id, name, server_id, domain, port, compose, - git_repo, git_branch, auth_user, auth_pass, status, unique_service_name, created_at, updated_at + git_repo, git_branch, auth_user, auth_pass, status, compose_mode, created_at, updated_at FROM apps WHERE server_id = ? ORDER BY created_at DESC `, serverID) if err != nil { @@ -130,7 +130,7 @@ func (r *Repository) ListByServer(serverID int64) ([]App, error) { var gitRepo, gitBranch sql.NullString if err := rows.Scan( &a.ID, &a.Name, &a.ServerID, &a.Domain, &a.Port, &a.Compose, - &gitRepo, &gitBranch, &a.AuthUser, &a.AuthPass, &a.Status, &a.UniqueServiceName, &a.CreatedAt, &a.UpdatedAt, + &gitRepo, &gitBranch, &a.AuthUser, &a.AuthPass, &a.Status, &a.ComposeMode, &a.CreatedAt, &a.UpdatedAt, ); err != nil { return nil, err } @@ -143,9 +143,9 @@ func (r *Repository) ListByServer(serverID int64) ([]App, error) { func (r *Repository) Create(a *App) error { result, err := r.db.Exec(` - INSERT INTO apps (name, server_id, domain, port, compose, git_repo, git_branch, auth_user, auth_pass, status, unique_service_name) + INSERT INTO apps (name, server_id, domain, port, compose, git_repo, git_branch, auth_user, auth_pass, status, compose_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, a.Name, a.ServerID, a.Domain, a.Port, a.Compose, nullString(a.GitRepo), nullString(a.GitBranch), a.AuthUser, a.AuthPass, "created", a.UniqueServiceName) + `, a.Name, a.ServerID, a.Domain, a.Port, a.Compose, nullString(a.GitRepo), nullString(a.GitBranch), a.AuthUser, a.AuthPass, "created", a.ComposeMode) if err != nil { return fmt.Errorf("insert app: %w", err) } @@ -158,10 +158,10 @@ func (r *Repository) Update(a *App) error { _, err := r.db.Exec(` UPDATE apps SET name=?, server_id=?, domain=?, port=?, compose=?, - git_repo=?, git_branch=?, auth_user=?, auth_pass=?, status=?, unique_service_name=?, updated_at=CURRENT_TIMESTAMP + git_repo=?, git_branch=?, auth_user=?, auth_pass=?, status=?, compose_mode=?, updated_at=CURRENT_TIMESTAMP WHERE id=? `, a.Name, a.ServerID, a.Domain, a.Port, a.Compose, - nullString(a.GitRepo), nullString(a.GitBranch), a.AuthUser, a.AuthPass, a.Status, a.UniqueServiceName, a.ID) + nullString(a.GitRepo), nullString(a.GitBranch), a.AuthUser, a.AuthPass, a.Status, a.ComposeMode, a.ID) return err } diff --git a/internal/app/service.go b/internal/app/service.go index 25bb012..8b14bd5 100644 --- a/internal/app/service.go +++ b/internal/app/service.go @@ -138,7 +138,7 @@ func (s *Service) deployWithCommit(id int64, commitSHA string) { composeCmd := dockerComposeCmd(client) - remoteDir := fmt.Sprintf("/opt/dockify/apps/%s", app.Name) + remoteDir := fmt.Sprintf("/opt/dockify/apps/app-%d", app.ID) composePath := fmt.Sprintf("%s/docker-compose.yml", remoteDir) log.Printf("Deploying %q to %s...", app.Name, svr.Name) @@ -165,10 +165,10 @@ func (s *Service) deployWithCommit(id int64, commitSHA string) { composeContent := ensureDockifyNetwork(app.Compose) - if app.UniqueServiceName { + if app.ComposeMode == "simple" { newName := sanitizeAppName(app.Name) composeContent = renameFirstService(composeContent, newName) - log.Printf("Renamed first service to %q for unique service name", newName) + log.Printf("Renamed first service to %q (compose_mode=simple)", newName) } if err := client.WriteFile(composePath, composeContent, 0644); err != nil { @@ -181,7 +181,7 @@ func (s *Service) deployWithCommit(id int64, commitSHA string) { client.Exec("docker network inspect dockify >/dev/null 2>&1 || docker network create dockify") - if out, err := client.Exec(fmt.Sprintf("%s -f %s up -d --force-recreate 2>&1", composeCmd, composePath)); err != nil { + if out, err := client.Exec(fmt.Sprintf("%s -f %s up -d --force-recreate --remove-orphans 2>&1", composeCmd, composePath)); err != nil { logs = append(logs, fmt.Sprintf("compose up: %v", err)) logs = append(logs, out) s.recordDeployment(id, svr.ID, StatusFailed, strings.Join(logs, "\n"), commitSHA, app.Compose) @@ -242,7 +242,7 @@ func (s *Service) FetchLogs(id int64, tail int) (string, error) { } defer client.Close() - composePath := fmt.Sprintf("/opt/dockify/apps/%s/docker-compose.yml", app.Name) + composePath := fmt.Sprintf("/opt/dockify/apps/app-%d/docker-compose.yml", app.ID) dc := dockerComposeCmd(client) out, err := client.Exec(fmt.Sprintf("%s -f %s logs --tail=%d 2>&1", dc, composePath, tail)) if err != nil { @@ -278,7 +278,7 @@ func (s *Service) Undeploy(id int64) error { dc := dockerComposeCmd(client) - remoteDir := fmt.Sprintf("/opt/dockify/apps/%s", app.Name) + remoteDir := fmt.Sprintf("/opt/dockify/apps/app-%d", app.ID) composePath := fmt.Sprintf("%s/docker-compose.yml", remoteDir) log.Printf("Undeploying %q from %s...", app.Name, svr.Name) @@ -382,7 +382,7 @@ func (s *Service) Stop(id int64) error { defer client.Close() dc := dockerComposeCmd(client) - composePath := fmt.Sprintf("/opt/dockify/apps/%s/docker-compose.yml", app.Name) + composePath := fmt.Sprintf("/opt/dockify/apps/app-%d/docker-compose.yml", app.ID) log.Printf("Stopping %q on %s...", app.Name, svr.Name) if app.Domain != "" { @@ -419,7 +419,7 @@ func (s *Service) Start(id int64) error { defer client.Close() dc := dockerComposeCmd(client) - composePath := fmt.Sprintf("/opt/dockify/apps/%s/docker-compose.yml", app.Name) + composePath := fmt.Sprintf("/opt/dockify/apps/app-%d/docker-compose.yml", app.ID) log.Printf("Starting %q on %s...", app.Name, svr.Name) if out, err := client.Exec(fmt.Sprintf("%s -f %s start 2>&1", dc, composePath)); err != nil { diff --git a/internal/backup/backup.go b/internal/backup/backup.go index ff6fde8..0a48cea 100644 --- a/internal/backup/backup.go +++ b/internal/backup/backup.go @@ -51,7 +51,7 @@ type ExportApp struct { GitBranch string `yaml:"git_branch,omitempty"` AuthUser string `yaml:"auth_user,omitempty"` AuthPass string `yaml:"auth_pass,omitempty"` - UniqueServiceName bool `yaml:"unique_service_name,omitempty"` + ComposeMode string `yaml:"compose_mode,omitempty"` Secrets []ExportSecret `yaml:"secrets,omitempty"` Files []ExportFile `yaml:"files,omitempty"` } @@ -175,7 +175,7 @@ func (s *Service) Export(passphrase string) (string, error) { GitRepo: a.GitRepo, GitBranch: a.GitBranch, AuthUser: a.AuthUser, - UniqueServiceName: a.UniqueServiceName, + ComposeMode: a.ComposeMode, } if a.AuthPass != "" { @@ -341,7 +341,7 @@ func (s *Service) Import(yamlData, passphrase, mode string) (string, error) { GitBranch: ea.GitBranch, AuthUser: ea.AuthUser, AuthPass: authPass, - UniqueServiceName: ea.UniqueServiceName, + ComposeMode: ea.ComposeMode, } if ap.GitBranch == "" { ap.GitBranch = "main" diff --git a/internal/db/db.go b/internal/db/db.go index 7b80ecf..b23cf37 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -46,5 +46,9 @@ func Open(path string) (*sql.DB, error) { // Migrasi: tambah kolom unique_service_name (v0.3.0) db.Exec("ALTER TABLE apps ADD COLUMN unique_service_name INTEGER DEFAULT 0") + // Migrasi: tambah kolom compose_mode, gantikan unique_service_name (v0.5.0) + db.Exec("ALTER TABLE apps ADD COLUMN compose_mode TEXT DEFAULT 'advanced'") + db.Exec("UPDATE apps SET compose_mode = 'simple' WHERE unique_service_name = 1") + return db, nil } diff --git a/internal/db/schema.sql b/internal/db/schema.sql index 8d2329b..2ab248d 100644 --- a/internal/db/schema.sql +++ b/internal/db/schema.sql @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS apps ( auth_user TEXT DEFAULT '', auth_pass TEXT DEFAULT '', status TEXT DEFAULT 'created', - unique_service_name INTEGER DEFAULT 0, + compose_mode TEXT DEFAULT 'advanced', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); diff --git a/internal/http/templates/apps_add.html b/internal/http/templates/apps_add.html index 22614f6..5ea021d 100644 --- a/internal/http/templates/apps_add.html +++ b/internal/http/templates/apps_add.html @@ -31,37 +31,24 @@

Basic

-
-
- - -
- - Rename the first service to match the app name to avoid conflicts with other apps on the same server -
- {{if .IsEdit}} -

Docker Compose

- - {{else}}

Compose Mode

- - + +
-
- - - +
+ + +
-
diff --git a/internal/http/templates/apps_detail.html b/internal/http/templates/apps_detail.html index 11d7b75..0dd3a01 100644 --- a/internal/http/templates/apps_detail.html +++ b/internal/http/templates/apps_detail.html @@ -21,10 +21,9 @@

Details

- + {{if .App.AuthUser}}{{end}} - {{if .App.GitRepo}}{{end}}
Domain{{.App.Domain}}
Port{{.App.Port}}
Server#{{.App.ServerID}}
Server{{.ServerName}}
Status{{.App.Status}}
Authbasic auth ({{.App.AuthUser}})
Unique{{if .App.UniqueServiceName}}yes{{else}}no{{end}}
Git Repo{{.App.GitRepo}}
Branch{{.App.GitBranch}}