Skip to content

Commit 941f1d1

Browse files
committed
fix(docker): Sync service.yml when compose expose is updated
The compose expose field was not parsed, so changes to it never propagated to service.yml. Added Expose to composeService struct, use it for container port extraction and primary service detection, and regenerate service.yml metadata when compose file is updated. Closes #104 Signed-off-by: nfebe <fenn25.fn@gmail.com>
1 parent a23b1dc commit 941f1d1

File tree

2 files changed

+168
-2
lines changed

2 files changed

+168
-2
lines changed

internal/docker/discovery.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type composeFile struct {
2828
type composeService struct {
2929
Image string `yaml:"image"`
3030
Ports []interface{} `yaml:"ports"`
31+
Expose []interface{} `yaml:"expose"`
3132
Networks []string `yaml:"networks"`
3233
Volumes []string `yaml:"volumes"`
3334
}
@@ -196,6 +197,12 @@ func (d *Discovery) parseComposeServices(composePath string) ([]models.Service,
196197
service.Ports = append(service.Ports, portStr)
197198
}
198199
}
200+
for _, p := range svc.Expose {
201+
portStr := d.parsePort(p)
202+
if portStr != "" {
203+
service.Ports = append(service.Ports, portStr)
204+
}
205+
}
199206

200207
services = append(services, service)
201208
}
@@ -538,7 +545,25 @@ func (d *Discovery) UpdateComposeFile(name string, content string) error {
538545
_ = os.WriteFile(backup, data, 0644)
539546
}
540547

541-
return os.WriteFile(composePath, []byte(content), 0644)
548+
if err := os.WriteFile(composePath, []byte(content), 0644); err != nil {
549+
return err
550+
}
551+
552+
metadataPath := filepath.Join(dirPath, "service.yml")
553+
if _, err := os.Stat(metadataPath); err == nil {
554+
if newMeta := d.generateMetadataFromCompose(composePath, name); newMeta != nil {
555+
existing, err := d.loadMetadata(metadataPath)
556+
if err == nil {
557+
existing.Networking.ContainerPort = newMeta.Networking.ContainerPort
558+
if newMeta.Networking.Service != "" {
559+
existing.Networking.Service = newMeta.Networking.Service
560+
}
561+
d.SaveMetadata(name, existing)
562+
}
563+
}
564+
}
565+
566+
return nil
542567
}
543568

544569
func (d *Discovery) SaveMetadata(name string, metadata *models.ServiceMetadata) error {
@@ -602,6 +627,12 @@ func (d *Discovery) generateMetadataFromCompose(composePath, name string) *model
602627
metadata.Networking.ContainerPort = port
603628
}
604629
}
630+
} else if len(svc.Expose) > 0 {
631+
if portStr := d.parsePort(svc.Expose[0]); portStr != "" {
632+
if port := d.extractContainerPort(portStr); port > 0 {
633+
metadata.Networking.ContainerPort = port
634+
}
635+
}
605636
}
606637
}
607638

@@ -618,7 +649,7 @@ func (d *Discovery) pickPrimaryService(services map[string]composeService) (stri
618649
}
619650
}
620651
for name, svc := range services {
621-
if len(svc.Ports) > 0 {
652+
if len(svc.Ports) > 0 || len(svc.Expose) > 0 {
622653
return name, svc
623654
}
624655
}

internal/docker/discovery_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"path/filepath"
77
"sort"
88
"testing"
9+
10+
"github.com/flatrun/agent/pkg/models"
11+
"gopkg.in/yaml.v3"
912
)
1013

1114
func TestExtractBindMountPath(t *testing.T) {
@@ -408,6 +411,138 @@ func TestGenerateMetadataFromCompose_ServiceName(t *testing.T) {
408411
}
409412
}
410413

414+
func TestGenerateMetadataFromCompose_Expose(t *testing.T) {
415+
tests := []struct {
416+
name string
417+
compose string
418+
wantPort int
419+
}{
420+
{
421+
name: "expose sets container port",
422+
compose: `services:
423+
app:
424+
image: myapp:latest
425+
expose:
426+
- "80"
427+
`,
428+
wantPort: 80,
429+
},
430+
{
431+
name: "ports takes precedence over expose",
432+
compose: `services:
433+
app:
434+
image: myapp:latest
435+
ports:
436+
- "8080:3000"
437+
expose:
438+
- "80"
439+
`,
440+
wantPort: 3000,
441+
},
442+
{
443+
name: "expose picks primary service in multi-service",
444+
compose: `services:
445+
app:
446+
image: myapp:latest
447+
expose:
448+
- "8080"
449+
db:
450+
image: postgres:15
451+
`,
452+
wantPort: 8080,
453+
},
454+
}
455+
456+
for _, tt := range tests {
457+
t.Run(tt.name, func(t *testing.T) {
458+
tmpDir, err := os.MkdirTemp("", "expose-test-*")
459+
if err != nil {
460+
t.Fatalf("Failed to create temp dir: %v", err)
461+
}
462+
defer os.RemoveAll(tmpDir)
463+
464+
composePath := filepath.Join(tmpDir, "docker-compose.yml")
465+
if err := os.WriteFile(composePath, []byte(tt.compose), 0644); err != nil {
466+
t.Fatalf("Failed to write compose file: %v", err)
467+
}
468+
469+
d := NewDiscovery(tmpDir)
470+
metadata := d.generateMetadataFromCompose(composePath, "test")
471+
if metadata == nil {
472+
t.Fatal("generateMetadataFromCompose returned nil")
473+
}
474+
475+
if metadata.Networking.ContainerPort != tt.wantPort {
476+
t.Errorf("ContainerPort = %d, want %d", metadata.Networking.ContainerPort, tt.wantPort)
477+
}
478+
})
479+
}
480+
}
481+
482+
func TestUpdateComposeFile_SyncsMetadata(t *testing.T) {
483+
tmpDir, err := os.MkdirTemp("", "sync-test-*")
484+
if err != nil {
485+
t.Fatalf("Failed to create temp dir: %v", err)
486+
}
487+
defer os.RemoveAll(tmpDir)
488+
489+
deployDir := filepath.Join(tmpDir, "myapp")
490+
if err := os.MkdirAll(deployDir, 0755); err != nil {
491+
t.Fatalf("Failed to create deploy dir: %v", err)
492+
}
493+
494+
compose := `services:
495+
app:
496+
image: myapp:latest
497+
expose:
498+
- "3000"
499+
`
500+
composePath := filepath.Join(deployDir, "docker-compose.yml")
501+
if err := os.WriteFile(composePath, []byte(compose), 0644); err != nil {
502+
t.Fatalf("Failed to write compose: %v", err)
503+
}
504+
505+
d := NewDiscovery(tmpDir)
506+
metadata := &models.ServiceMetadata{
507+
Name: "myapp",
508+
Networking: models.NetworkingConfig{
509+
ContainerPort: 3000,
510+
Expose: true,
511+
},
512+
}
513+
if err := d.SaveMetadata("myapp", metadata); err != nil {
514+
t.Fatalf("Failed to save metadata: %v", err)
515+
}
516+
517+
updatedCompose := `services:
518+
app:
519+
image: myapp:latest
520+
expose:
521+
- "8080"
522+
`
523+
if err := d.UpdateComposeFile("myapp", updatedCompose); err != nil {
524+
t.Fatalf("UpdateComposeFile failed: %v", err)
525+
}
526+
527+
metadataPath := filepath.Join(deployDir, "service.yml")
528+
data, err := os.ReadFile(metadataPath)
529+
if err != nil {
530+
t.Fatalf("Failed to read service.yml: %v", err)
531+
}
532+
533+
var updated models.ServiceMetadata
534+
if err := yaml.Unmarshal(data, &updated); err != nil {
535+
t.Fatalf("Failed to parse service.yml: %v", err)
536+
}
537+
538+
if updated.Networking.ContainerPort != 8080 {
539+
t.Errorf("ContainerPort = %d, want 8080", updated.Networking.ContainerPort)
540+
}
541+
if !updated.Networking.Expose {
542+
t.Error("Expose should be preserved as true")
543+
}
544+
}
545+
411546
func TestExtractBindMounts(t *testing.T) {
412547
tests := []struct {
413548
name string

0 commit comments

Comments
 (0)