diff --git a/src/cmd/cli/command/compose.go b/src/cmd/cli/command/compose.go index 53d7167de..174fab665 100644 --- a/src/cmd/cli/command/compose.go +++ b/src/cmd/cli/command/compose.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "os" "slices" "strings" "time" @@ -553,6 +554,104 @@ func makeComposeConfigCmd() *cobra.Command { } } +func makeComposeLintCmd() *cobra.Command { + var fix bool + cmd := &cobra.Command{ + Use: "lint", + Args: cobra.NoArgs, + Short: "Validate a Compose file without deploying", + RunE: func(cmd *cobra.Command, args []string) error { + loader := newLoaderForCommand(cmd) + + project, loadErr := loader.LoadProject(cmd.Context()) + if loadErr != nil { + return handleInvalidComposeFileErr(cmd.Context(), loadErr) + } + + if fix { + fixes := compose.FixProject(project) + printFixResults(fixes) + if len(fixes) > 0 { + if err := writeFixedCompose(project); err != nil { + return err + } + } + } + + var errs []error + + if err := compose.ValidateServiceDockerfiles(project); err != nil { + errs = append(errs, err) + } + + if err := compose.ValidateProject(project, modes.ModeUnspecified); err != nil { + errs = append(errs, err) + } + + if len(errs) > 0 { + return fmt.Errorf("compose file has errors:\n%w", errors.Join(errs...)) + } + + if term.HadWarnings() { + term.Info("Compose file is valid with warnings") + } else { + term.Info("Compose file is valid") + } + return nil + }, + } + cmd.Flags().BoolVar(&fix, "fix", false, "apply safe mechanical fixes to the Compose file") + return cmd +} + +func writeFixedCompose(project *compose.Project) error { + if len(project.ComposeFiles) == 0 { + return fmt.Errorf("no compose file to write to") + } + data, err := compose.MarshalYAML(project) + if err != nil { + return err + } + path := project.ComposeFiles[0] + if err := os.WriteFile(path, data, 0644); err != nil { + return fmt.Errorf("writing %s: %w", path, err) + } + term.Info("Updated", path) + return nil +} + +func printFixResults(fixes []compose.FixResult) { + if len(fixes) == 0 { + term.Info("No fixes needed.") + return + } + term.Println("Fixes applied:") + for _, fix := range fixes { + term.Printf(" service %q: %s\n", fix.Service, describeFixResult(fix)) + } + term.Printf("\n%d fix(es) applied.\n", len(fixes)) +} + +func describeFixResult(fix compose.FixResult) string { + switch fix.Action { + case "removed": + return fmt.Sprintf("removed unsupported directive: %s (%s)", fix.Field, fix.Reason) + case "changed": + if fix.Before != "" { + return fmt.Sprintf("changed %s from %q to %q (%s)", fix.Field, fix.Before, fix.After, fix.Reason) + } + return fmt.Sprintf("changed %s to %q (%s)", fix.Field, fix.After, fix.Reason) + default: + if fix.Field == "mode" { + return fmt.Sprintf("added mode: %s to %s", fix.After, fix.Reason) + } + if fix.Reason != "" { + return fmt.Sprintf("added %s: %s (%s)", fix.Field, fix.After, fix.Reason) + } + return fmt.Sprintf("added %s: %s", fix.Field, fix.After) + } +} + func makeComposePsCmd() *cobra.Command { getServicesCmd := &cobra.Command{ Use: "ps", @@ -763,6 +862,7 @@ services: composeCmd.PersistentFlags().StringVar(&byoc.DefangPulumiBackend, "pulumi-backend", "", `specify an alternate Pulumi backend URL or "pulumi-cloud"`) composeCmd.AddCommand(makeComposeUpCmd()) composeCmd.AddCommand(makeComposeConfigCmd()) + composeCmd.AddCommand(makeComposeLintCmd()) composeCmd.AddCommand(makeComposeDownCmd()) composeCmd.AddCommand(makeComposePsCmd()) composeCmd.AddCommand(makeLogsCmd()) diff --git a/src/cmd/cli/command/compose_test.go b/src/cmd/cli/command/compose_test.go index ccd324664..538d9b3d7 100644 --- a/src/cmd/cli/command/compose_test.go +++ b/src/cmd/cli/command/compose_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "os" + "strings" "testing" "connectrpc.com/connect" @@ -79,3 +80,43 @@ func TestComposeConfig(t *testing.T) { } }) } + +func TestComposeLint(t *testing.T) { + defaultTerm := term.DefaultTerm + t.Cleanup(func() { + term.DefaultTerm = defaultTerm + }) + + t.Run("Valid", func(t *testing.T) { + t.Chdir("testdata/without-stack") + var stdout, stderr bytes.Buffer + term.DefaultTerm = term.NewTerm(os.Stdin, &stdout, &stderr) + + cmd := makeComposeLintCmd() + err := cmd.Execute() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !strings.Contains(stdout.String(), "Compose file is valid") { + t.Fatalf("expected valid lint output, got stdout %q stderr %q", stdout.String(), stderr.String()) + } + }) + + t.Run("Invalid", func(t *testing.T) { + t.Chdir("testdata/lint-invalid") + var stdout, stderr bytes.Buffer + term.DefaultTerm = term.NewTerm(os.Stdin, &stdout, &stderr) + + cmd := makeComposeLintCmd() + err := cmd.Execute() + if err == nil { + t.Fatal("expected lint error") + } + if !strings.Contains(err.Error(), "compose file has errors:") { + t.Fatalf("expected error heading, got error %q stdout %q stderr %q", err.Error(), stdout.String(), stderr.String()) + } + if !strings.Contains(err.Error(), "unsupported compose directive: hostname; use 'domainname' instead") { + t.Fatalf("expected remediation hint, got error %q", err.Error()) + } + }) +} diff --git a/src/cmd/cli/command/testdata/lint-invalid/compose.yaml b/src/cmd/cli/command/testdata/lint-invalid/compose.yaml new file mode 100644 index 000000000..82ca2ad72 --- /dev/null +++ b/src/cmd/cli/command/testdata/lint-invalid/compose.yaml @@ -0,0 +1,4 @@ +services: + web: + image: nginx:latest + hostname: web diff --git a/src/pkg/cli/compose/fix.go b/src/pkg/cli/compose/fix.go new file mode 100644 index 000000000..b3aaf72bd --- /dev/null +++ b/src/pkg/cli/compose/fix.go @@ -0,0 +1,194 @@ +package compose + +import ( + "fmt" + "sort" + + composeTypes "github.com/compose-spec/compose-go/v2/types" +) + +const defaultRestartPolicy = "unless-stopped" + +type FixResult struct { + Service string + Field string + Action string // "added", "removed", "changed" + Before string + After string + Reason string +} + +func FixProject(project *Project) []FixResult { + if project == nil { + return nil + } + + var results []FixResult + for _, name := range sortedServiceNames(project) { + service := project.Services[name] + results = append(results, fixService(&service)...) + project.Services[name] = service + } + return results +} + +func sortedServiceNames(project *Project) []string { + names := make([]string, 0, len(project.Services)) + for name := range project.Services { + names = append(names, name) + } + sort.Strings(names) + return names +} + +func fixService(service *composeTypes.ServiceConfig) []FixResult { + var results []FixResult + repo := GetImageRepo(service.Image) + isManagedStoreImage := IsPostgresRepo(repo) || IsRedisRepo(repo) || IsMongoRepo(repo) + + results = append(results, fixPorts(service, isManagedStoreImage)...) + results = append(results, fixLimitsToReservations(service)...) + results = append(results, fixRestart(service)...) + results = append(results, fixUnsupportedDirectives(service)...) + + return results +} + +func fixPorts(service *composeTypes.ServiceConfig, isManagedStoreImage bool) []FixResult { + var results []FixResult + for i := range service.Ports { + port := &service.Ports[i] + if port.Mode != "" { + continue + } + + mode := Mode_INGRESS + reason := "" + if port.Protocol == Protocol_UDP { + mode = Mode_HOST + reason = "UDP port" + } else if isManagedStoreImage { + mode = Mode_HOST + reason = "database image" + } + port.Mode = mode + results = append(results, FixResult{ + Service: service.Name, + Field: "mode", + Action: "added", + After: mode, + Reason: portReason(port.Target, reason), + }) + } + return results +} + +func portReason(target uint32, reason string) string { + if reason == "" { + return fmt.Sprintf("port %d", target) + } + return fmt.Sprintf("port %d (%s)", target, reason) +} + +func fixLimitsToReservations(service *composeTypes.ServiceConfig) []FixResult { + if service.Deploy == nil { + return nil + } + if service.Deploy.Resources.Limits == nil || service.Deploy.Resources.Reservations != nil { + return nil + } + limits := *service.Deploy.Resources.Limits + service.Deploy.Resources.Reservations = &limits + return []FixResult{{ + Service: service.Name, + Field: "deploy.resources.reservations", + Action: "added", + After: "copied from deploy.resources.limits", + Reason: "Defang uses reservations for scheduling, not limits", + }} +} + +func fixRestart(service *composeTypes.ServiceConfig) []FixResult { + restart := restartFromDeployPolicy(service) + if restart == "" && isSupportedRestart(service.Restart) { + return nil + } + + before := service.Restart + if restart == "" { + restart = defaultRestartPolicy + } + service.Restart = restart + + reason := "unsupported restart policy" + if service.Deploy != nil && service.Deploy.RestartPolicy != nil { + reason = "deploy.restart_policy is unsupported; converted to service-level restart" + service.Deploy.RestartPolicy = nil + } else if before == "" { + reason = "missing restart policy" + } + + action := "changed" + if before == "" { + action = "added" + } + return []FixResult{{ + Service: service.Name, + Field: "restart", + Action: action, + Before: before, + After: restart, + Reason: reason, + }} +} + +func restartFromDeployPolicy(service *composeTypes.ServiceConfig) string { + if service.Deploy == nil || service.Deploy.RestartPolicy == nil { + return "" + } + switch service.Deploy.RestartPolicy.Condition { + case "", "any": + return "always" + default: + return defaultRestartPolicy + } +} + +func isSupportedRestart(restart string) bool { + return restart == "always" || restart == defaultRestartPolicy +} + +func fixUnsupportedDirectives(service *composeTypes.ServiceConfig) []FixResult { + var results []FixResult + if len(service.DNS) != 0 { + service.DNS = nil + results = append(results, removedDirective(service.Name, "dns")) + } + if len(service.DNSSearch) != 0 { + service.DNSSearch = nil + results = append(results, removedDirective(service.Name, "dns_search")) + } + if len(service.Devices) != 0 { + service.Devices = nil + results = append(results, removedDirective(service.Name, "devices")) + } + if len(service.DeviceCgroupRules) != 0 { + service.DeviceCgroupRules = nil + results = append(results, removedDirective(service.Name, "device_cgroup_rules")) + } + if len(service.GroupAdd) != 0 { + service.GroupAdd = nil + results = append(results, removedDirective(service.Name, "group_add")) + } + return results +} + +func removedDirective(service, field string) FixResult { + return FixResult{ + Service: service, + Field: field, + Action: "removed", + Before: "present", + Reason: "unsupported directive", + } +} diff --git a/src/pkg/cli/compose/fix_test.go b/src/pkg/cli/compose/fix_test.go new file mode 100644 index 000000000..87078a914 --- /dev/null +++ b/src/pkg/cli/compose/fix_test.go @@ -0,0 +1,197 @@ +package compose + +import ( + "reflect" + "testing" + + "github.com/DefangLabs/defang/src/pkg/modes" + composeTypes "github.com/compose-spec/compose-go/v2/types" +) + +func TestFixProject(t *testing.T) { + tests := []struct { + name string + project *Project + want []FixResult + check func(*testing.T, *Project) + }{ + { + name: "web service port mode and restart", + project: &Project{Services: Services{ + "web": { + Name: "web", + Image: "nginx", + Ports: []composeTypes.ServicePortConfig{{Target: 8080}}, + }, + }}, + want: []FixResult{ + {Service: "web", Field: "mode", Action: "added", After: Mode_INGRESS, Reason: "port 8080"}, + {Service: "web", Field: "restart", Action: "added", After: defaultRestartPolicy, Reason: "missing restart policy"}, + }, + check: func(t *testing.T, project *Project) { + service := project.Services["web"] + if service.Ports[0].Mode != Mode_INGRESS { + t.Fatalf("port mode = %q, want %q", service.Ports[0].Mode, Mode_INGRESS) + } + if service.Restart != defaultRestartPolicy { + t.Fatalf("restart = %q, want %q", service.Restart, defaultRestartPolicy) + } + }, + }, + { + name: "managed postgres defaults to host mode", + project: &Project{Services: Services{ + "db": { + Name: "db", + Image: "postgres:16", + Ports: []composeTypes.ServicePortConfig{{Target: 5432}}, + }, + }}, + want: []FixResult{ + {Service: "db", Field: "mode", Action: "added", After: Mode_HOST, Reason: "port 5432 (database image)"}, + {Service: "db", Field: "restart", Action: "added", After: defaultRestartPolicy, Reason: "missing restart policy"}, + }, + check: func(t *testing.T, project *Project) { + service := project.Services["db"] + if service.Ports[0].Mode != Mode_HOST { + t.Fatalf("port mode = %q, want %q", service.Ports[0].Mode, Mode_HOST) + } + }, + }, + { + name: "limits copied to reservations", + project: &Project{Services: Services{ + "api": { + Name: "api", + Image: "api", + Restart: defaultRestartPolicy, + Deploy: &composeTypes.DeployConfig{Resources: composeTypes.Resources{ + Limits: &composeTypes.Resource{MemoryBytes: 1024 * MiB}, + }}, + }, + }}, + want: []FixResult{ + {Service: "api", Field: "deploy.resources.reservations", Action: "added", After: "copied from deploy.resources.limits", Reason: "Defang uses reservations for scheduling, not limits"}, + }, + check: func(t *testing.T, project *Project) { + service := project.Services["api"] + if service.Deploy.Resources.Reservations == nil { + t.Fatal("reservations were not added") + } + if service.Deploy.Resources.Reservations.MemoryBytes != 1024*MiB { + t.Fatalf("memory = %d, want %d", service.Deploy.Resources.Reservations.MemoryBytes, 1024*MiB) + } + }, + }, + { + name: "unsupported directives removed", + project: &Project{Services: Services{ + "worker": { + Name: "worker", + Image: "worker", + Restart: defaultRestartPolicy, + DNS: composeTypes.StringList{"1.1.1.1"}, + DNSSearch: composeTypes.StringList{"example.com"}, + Devices: []composeTypes.DeviceMapping{{Source: "/dev/null", Target: "/dev/null"}}, + DeviceCgroupRules: []string{"c 1:3 mr"}, + GroupAdd: []string{"audio"}, + }, + }}, + want: []FixResult{ + {Service: "worker", Field: "dns", Action: "removed", Before: "present", Reason: "unsupported directive"}, + {Service: "worker", Field: "dns_search", Action: "removed", Before: "present", Reason: "unsupported directive"}, + {Service: "worker", Field: "devices", Action: "removed", Before: "present", Reason: "unsupported directive"}, + {Service: "worker", Field: "device_cgroup_rules", Action: "removed", Before: "present", Reason: "unsupported directive"}, + {Service: "worker", Field: "group_add", Action: "removed", Before: "present", Reason: "unsupported directive"}, + }, + check: func(t *testing.T, project *Project) { + service := project.Services["worker"] + if len(service.DNS) != 0 || len(service.DNSSearch) != 0 || len(service.Devices) != 0 || len(service.DeviceCgroupRules) != 0 || len(service.GroupAdd) != 0 { + t.Fatal("unsupported directives were not removed") + } + }, + }, + { + name: "deploy restart policy converted to service restart", + project: &Project{Services: Services{ + "api": { + Name: "api", + Image: "api", + Deploy: &composeTypes.DeployConfig{ + RestartPolicy: &composeTypes.RestartPolicy{Condition: "any"}, + }, + }, + }}, + want: []FixResult{ + {Service: "api", Field: "restart", Action: "added", Before: "", After: "always", Reason: "deploy.restart_policy is unsupported; converted to service-level restart"}, + }, + check: func(t *testing.T, project *Project) { + service := project.Services["api"] + if service.Restart != "always" { + t.Fatalf("restart = %q, want %q", service.Restart, "always") + } + if service.Deploy.RestartPolicy != nil { + t.Fatal("deploy.restart_policy was not removed") + } + }, + }, + { + name: "udp port defaults to host mode", + project: &Project{Services: Services{ + "dns": { + Name: "dns", + Image: "coredns", + Restart: "always", + Ports: []composeTypes.ServicePortConfig{{Target: 53, Protocol: Protocol_UDP}}, + }, + }}, + want: []FixResult{ + {Service: "dns", Field: "mode", Action: "added", After: Mode_HOST, Reason: "port 53 (UDP port)"}, + }, + check: func(t *testing.T, project *Project) { + service := project.Services["dns"] + if service.Ports[0].Mode != Mode_HOST { + t.Fatalf("port mode = %q, want %q", service.Ports[0].Mode, Mode_HOST) + } + }, + }, + { + name: "no fixes needed", + project: &Project{Services: Services{ + "app": { + Name: "app", + Image: "myapp", + Restart: "always", + Ports: []composeTypes.ServicePortConfig{{Target: 80, Mode: Mode_INGRESS}}, + }, + }}, + want: nil, + check: func(t *testing.T, project *Project) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FixProject(tt.project) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("FixProject() = %#v, want %#v", got, tt.want) + } + tt.check(t, tt.project) + }) + } +} + +func TestFixProjectOutputValidates(t *testing.T) { + loader := NewLoader(WithPath("../../../testdata/compose-fix/compose.yaml")) + project, err := loader.LoadProject(t.Context()) + if err != nil { + t.Fatalf("LoadProject() failed: %v", err) + } + + if fixes := FixProject(project); len(fixes) == 0 { + t.Fatal("expected fixes") + } + if err := ValidateProject(project, modes.ModeUnspecified); err != nil { + t.Fatalf("ValidateProject() after FixProject() failed: %v", err) + } +} diff --git a/src/pkg/cli/compose/fixup.go b/src/pkg/cli/compose/fixup.go index ee667de68..6d6880789 100644 --- a/src/pkg/cli/compose/fixup.go +++ b/src/pkg/cli/compose/fixup.go @@ -550,7 +550,7 @@ func GetImageRepo(image string) string { func fixupPort(port composeTypes.ServicePortConfig) composeTypes.ServicePortConfig { switch port.Mode { case "": - term.Warnf("port %d: no 'mode' was specified; defaulting to 'ingress' (add 'mode: ingress' to silence)", port.Target) + term.Warnf("port %d: no 'mode' was specified; defaulting to 'ingress'; add 'mode: ingress' for public or 'mode: host' for internal", port.Target) fallthrough case Mode_INGRESS: // This code is unnecessarily complex because compose-go silently converts short `ports:` syntax to ingress+tcp diff --git a/src/pkg/cli/compose/validation.go b/src/pkg/cli/compose/validation.go index b8f4e35ce..9a51764ee 100644 --- a/src/pkg/cli/compose/validation.go +++ b/src/pkg/cli/compose/validation.go @@ -71,7 +71,7 @@ func validateService(svccfg *composeTypes.ServiceConfig, project *composeTypes.P term.Debugf("service %q: unsupported compose directive: container_name", svccfg.Name) } if svccfg.Hostname != "" { - return fmt.Errorf("service %q: unsupported compose directive: hostname; consider using 'domainname' instead", svccfg.Name) + return fmt.Errorf("service %q: unsupported compose directive: hostname; use 'domainname' instead", svccfg.Name) } if len(svccfg.DNSSearch) != 0 { return fmt.Errorf("service %q: unsupported compose directive: dns_search", svccfg.Name) @@ -89,7 +89,7 @@ func validateService(svccfg *composeTypes.ServiceConfig, project *composeTypes.P return fmt.Errorf("service %q: unsupported compose directive: device_cgroup_rules", svccfg.Name) } if len(svccfg.Entrypoint) > 0 { - return fmt.Errorf("service %q: unsupported compose directive: entrypoint", svccfg.Name) + return fmt.Errorf("service %q: unsupported compose directive: entrypoint; move logic to Dockerfile ENTRYPOINT or use 'command'", svccfg.Name) } if len(svccfg.GroupAdd) > 0 { return fmt.Errorf("service %q: unsupported compose directive: group_add", svccfg.Name) @@ -122,7 +122,7 @@ func validateService(svccfg *composeTypes.ServiceConfig, project *composeTypes.P } } if len(svccfg.Volumes) > 0 { - term.Warnf("service %q: unsupported compose directive: volumes", svccfg.Name) // TODO: add support for volumes + term.Warnf("service %q: unsupported compose directive: volumes; for databases, consider x-defang-postgres/redis/mongodb for managed storage", svccfg.Name) // TODO: add support for volumes } if len(svccfg.VolumesFrom) > 0 { term.Warnf("service %q: unsupported compose directive: volumes_from", svccfg.Name) // TODO: add support for volumes_from @@ -177,7 +177,7 @@ func validateService(svccfg *composeTypes.ServiceConfig, project *composeTypes.P return fmt.Errorf("service %q: unsupported compose directive: build privileged", svccfg.Name) } if svccfg.Build.DockerfileInline != "" { - return fmt.Errorf("service %q: unsupported compose directive: build dockerfile_inline", svccfg.Name) + return fmt.Errorf("service %q: unsupported compose directive: build dockerfile_inline; move inline content to a Dockerfile", svccfg.Name) } if svccfg.Build.AdditionalContexts != nil { return fmt.Errorf("service %q: unsupported compose directive: build additional_contexts", svccfg.Name) @@ -269,7 +269,7 @@ func validateService(svccfg *composeTypes.ServiceConfig, project *composeTypes.P return fmt.Errorf("service %q: unsupported compose directive: deploy rollback_config", svccfg.Name) } if svccfg.Deploy.RestartPolicy != nil { - return fmt.Errorf("service %q: unsupported compose directive: deploy restart_policy", svccfg.Name) + return fmt.Errorf("service %q: unsupported compose directive: deploy restart_policy; use service-level 'restart' field instead", svccfg.Name) } if svccfg.Deploy.EndpointMode != "" { return fmt.Errorf("service %q: unsupported compose directive: deploy endpoint_mode", svccfg.Name) @@ -351,7 +351,7 @@ func validateService(svccfg *composeTypes.ServiceConfig, project *composeTypes.P } if !managedRedis && !managedPostgres && !managedMongodb && isStatefulImage(svccfg.Image) { - term.Warnf("service %q: stateful service will lose data on restart; use a managed service instead", svccfg.Name) + term.Warnf("service %q: stateful service will lose data on restart; use a managed service instead; consider x-defang-postgres/redis/mongodb: true", svccfg.Name) } for k := range svccfg.Extensions { diff --git a/src/testdata/compose-fix/compose.yaml b/src/testdata/compose-fix/compose.yaml new file mode 100644 index 000000000..e24f8632d --- /dev/null +++ b/src/testdata/compose-fix/compose.yaml @@ -0,0 +1,12 @@ +name: compose-fix +services: + web: + image: nginx + ports: + - target: 8080 + dns: + - 1.1.1.1 + db: + image: postgres:16 + ports: + - target: 5432 diff --git a/src/testdata/compose-fix/compose.yaml.fixup b/src/testdata/compose-fix/compose.yaml.fixup new file mode 100644 index 000000000..16f9a6fba --- /dev/null +++ b/src/testdata/compose-fix/compose.yaml.fixup @@ -0,0 +1,36 @@ +{ + "db": { + "command": null, + "entrypoint": null, + "image": "postgres:16", + "networks": { + "default": null + }, + "ports": [ + { + "mode": "host", + "target": 5432, + "protocol": "tcp" + } + ] + }, + "web": { + "command": null, + "dns": [ + "1.1.1.1" + ], + "entrypoint": null, + "image": "nginx", + "networks": { + "default": null + }, + "ports": [ + { + "mode": "ingress", + "target": 8080, + "protocol": "tcp", + "app_protocol": "http" + } + ] + } +} \ No newline at end of file diff --git a/src/testdata/compose-fix/compose.yaml.golden b/src/testdata/compose-fix/compose.yaml.golden new file mode 100644 index 000000000..ea34d6304 --- /dev/null +++ b/src/testdata/compose-fix/compose.yaml.golden @@ -0,0 +1,23 @@ +name: compose-fix +services: + db: + image: postgres:16 + networks: + default: null + ports: + - mode: ingress + target: 5432 + protocol: tcp + web: + dns: + - 1.1.1.1 + image: nginx + networks: + default: null + ports: + - mode: ingress + target: 8080 + protocol: tcp +networks: + default: + name: compose-fix_default diff --git a/src/testdata/compose-fix/compose.yaml.warnings b/src/testdata/compose-fix/compose.yaml.warnings new file mode 100644 index 000000000..d31dc75d6 --- /dev/null +++ b/src/testdata/compose-fix/compose.yaml.warnings @@ -0,0 +1,3 @@ + ! service "db": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors + ! service "db": stateful service will lose data on restart; use a managed service instead +Error: service "web": unsupported compose directive: dns diff --git a/src/testdata/heroku/compose.yaml.warnings b/src/testdata/heroku/compose.yaml.warnings index a7033feb3..ba87e8faf 100644 --- a/src/testdata/heroku/compose.yaml.warnings +++ b/src/testdata/heroku/compose.yaml.warnings @@ -1,6 +1,6 @@ ! service "app": environment "DATABASE_URL" may contain sensitive information; consider using 'defang config set DATABASE_URL' to securely store this value ! service "app": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "postgres": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors - ! service "postgres": stateful service will lose data on restart; use a managed service instead + ! service "postgres": stateful service will lose data on restart; use a managed service instead; consider x-defang-postgres/redis/mongodb: true ! service "redis": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors - ! service "redis": stateful service will lose data on restart; use a managed service instead + ! service "redis": stateful service will lose data on restart; use a managed service instead; consider x-defang-postgres/redis/mongodb: true diff --git a/src/testdata/mongo/compose.yaml.warnings b/src/testdata/mongo/compose.yaml.warnings index b1afad3c7..66c8b3a6a 100644 --- a/src/testdata/mongo/compose.yaml.warnings +++ b/src/testdata/mongo/compose.yaml.warnings @@ -5,16 +5,16 @@ ! service "mongo-port1234": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "mongo-port1235": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "mongo-port1236": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors - ! service "mongo-port1236": stateful service will lose data on restart; use a managed service instead + ! service "mongo-port1236": stateful service will lose data on restart; use a managed service instead; consider x-defang-postgres/redis/mongodb: true ! service "mongo-port1237": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "mongo-port1238": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "mongo-port1239": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "mongo-port27018": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "mongo-port27019": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "mongo-unmanaged": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors - ! service "mongo-unmanaged": stateful service will lose data on restart; use a managed service instead + ! service "mongo-unmanaged": stateful service will lose data on restart; use a managed service instead; consider x-defang-postgres/redis/mongodb: true ! service "mongo-wrong-image": managed MongoDB service should use a mongo image ! service "mongo-wrong-image": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "mongo-wrong-image": service name is longer than 16 characters, you may run into issues with resource name length ! service "short-ports": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors - ! service "short-ports": stateful service will lose data on restart; use a managed service instead + ! service "short-ports": stateful service will lose data on restart; use a managed service instead; consider x-defang-postgres/redis/mongodb: true diff --git a/src/testdata/postgres/compose.yaml.warnings b/src/testdata/postgres/compose.yaml.warnings index 35121a7b1..6b6d29131 100644 --- a/src/testdata/postgres/compose.yaml.warnings +++ b/src/testdata/postgres/compose.yaml.warnings @@ -1,5 +1,5 @@ ! service "no-ext": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors - ! service "no-ext": stateful service will lose data on restart; use a managed service instead + ! service "no-ext": stateful service will lose data on restart; use a managed service instead; consider x-defang-postgres/redis/mongodb: true ! service "no-ports": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "no-ports-override": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "no-ports-override": service name is longer than 16 characters, you may run into issues with resource name length diff --git a/src/testdata/redis/compose.yaml.warnings b/src/testdata/redis/compose.yaml.warnings index 78024026c..7e641a7f8 100644 --- a/src/testdata/redis/compose.yaml.warnings +++ b/src/testdata/redis/compose.yaml.warnings @@ -1,5 +1,5 @@ ! service "no-ext": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors - ! service "no-ext": stateful service will lose data on restart; use a managed service instead + ! service "no-ext": stateful service will lose data on restart; use a managed service instead; consider x-defang-postgres/redis/mongodb: true ! service "no-ports": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "no-ports-override": missing memory reservation; using provider-specific defaults. Specify deploy.resources.reservations.memory to avoid out-of-memory errors ! service "no-ports-override": service name is longer than 16 characters, you may run into issues with resource name length