Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 88 additions & 3 deletions internal/app/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"fmt"
"strconv"
"strings"

"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -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:"
Expand Down Expand Up @@ -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
}
76 changes: 74 additions & 2 deletions internal/app/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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")
}
Expand Down Expand Up @@ -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)
}
}
Loading
Loading