From d38312fec67c8de7d9ca4fdf8cd7ec4db59d0315 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Tue, 31 Mar 2026 08:37:35 +0200 Subject: [PATCH] feat: print warning on some unsupported compose features (#288) * feat: error on unsupported compose features This implements a check for the unimplemented features of the uncloud support matrix and points to the matrix in the error. Fixes: #237 This redos #277, as I messed up my branch, (basically rebase gone wrong). Signed-off-by: Miek Gieben * Fix tests by capturing stderr The error should be nil, but we _do_ want to know if the user saw an warning. Redirect stderr to capture this as the error is emited by printing to stderr. Signed-off-by: Miek Gieben --------- Signed-off-by: Miek Gieben --- pkg/client/compose/project.go | 5 ++ pkg/client/compose/project_test.go | 89 ++++++++++++++++++++++++++++++ pkg/client/compose/service.go | 49 ++++++++++++++++ 3 files changed, 143 insertions(+) diff --git a/pkg/client/compose/project.go b/pkg/client/compose/project.go index 0cc4de36..0e3d3ebe 100644 --- a/pkg/client/compose/project.go +++ b/pkg/client/compose/project.go @@ -12,6 +12,7 @@ import ( "github.com/compose-spec/compose-go/v2/transform" "github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/types" + "github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/pkg/api" ) @@ -66,6 +67,10 @@ func LoadProject(ctx context.Context, paths []string, opts ...composecli.Project return nil, err } + if err = validateServicesFeatures(project); err != nil { + tui.PrintWarning(err.Error()) + } + // Process image templates in services to expand Go template expressions using git repo state. if project, err = ProcessImageTemplates(project); err != nil { return nil, err diff --git a/pkg/client/compose/project_test.go b/pkg/client/compose/project_test.go index bcda8e34..dc4a50da 100644 --- a/pkg/client/compose/project_test.go +++ b/pkg/client/compose/project_test.go @@ -2,6 +2,7 @@ package compose import ( "context" + "io" "os" "path/filepath" "testing" @@ -186,3 +187,91 @@ REDIS_URL=redis://localhost:6379 }) } } + +// TestLoadProject_Unsupported checks that unsupported features lead to an error. +func TestLoadProject_Unsupported(t *testing.T) { + requireWarning := func(t *testing.T, r io.Reader) { + p := make([]byte, 15) // also room for the (color) escapes + io.ReadFull(r, p) + require.Contains(t, string(p), "WARNING:") + } + + tests := []struct { + name string + composeYAML string + verify func(t *testing.T, projectDir string) + }{ + { + name: "unsupported dns", + composeYAML: `services: + app: + image: myapp:latest + dns: 8.8.8.8 +`, + verify: func(t *testing.T, projectDir string) { + old := os.Stderr + var r io.ReadCloser + defer func() { os.Stderr = old }() + r, os.Stderr, _ = os.Pipe() + defer r.Close() + defer os.Stderr.Close() + + _, err := LoadProject(context.Background(), []string{filepath.Join(projectDir, "compose.yaml")}) + require.NoError(t, err) + requireWarning(t, r) + }, + }, + { + name: "unsupported networks", + composeYAML: `services: + app: + image: myapp:latest + networks: + - frontend + +networks: + frontend: +`, + verify: func(t *testing.T, projectDir string) { + old := os.Stderr + var r io.ReadCloser + defer func() { os.Stderr = old }() + r, os.Stderr, _ = os.Pipe() + defer r.Close() + defer os.Stderr.Close() + + _, err := LoadProject(context.Background(), []string{filepath.Join(projectDir, "compose.yaml")}) + require.NoError(t, err) + requireWarning(t, r) + }, + }, + { + name: "supported networks", + composeYAML: `services: + app: + image: myapp:latest + networks: + - default + +networks: + default: {} +`, + verify: func(t *testing.T, projectDir string) { + _, err := LoadProject(context.Background(), []string{filepath.Join(projectDir, "compose.yaml")}) + require.NoError(t, err) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + + composeFile := filepath.Join(tempDir, "compose.yaml") + err := os.WriteFile(composeFile, []byte(tt.composeYAML), 0o644) + require.NoError(t, err) + + tt.verify(t, tempDir) + }) + } +} diff --git a/pkg/client/compose/service.go b/pkg/client/compose/service.go index 9636cd20..7bb2fcf5 100644 --- a/pkg/client/compose/service.go +++ b/pkg/client/compose/service.go @@ -422,3 +422,52 @@ func validateServicesExtensions(project *types.Project) error { return nil } + +// validateServicesFeatures checks the service for unsupported features and returns an error for this first one found. +func validateServicesFeatures(project *types.Project) error { + err := func(service, feature string) error { + return fmt.Errorf("service: '%s': unsupported feature: '%s', see %s", service, feature, "https://uncloud.run/docs/compose-file-reference/support-matrix") + } + + for _, service := range project.Services { + if service.SecurityOpt != nil { + return err(service.Name, "security_opt") + } + if service.DNS != nil { + return err(service.Name, "dns") + } + if service.DNSSearch != nil { + return err(service.Name, "dns_search") + } + if service.Labels != nil { + return err(service.Name, "labels") + } + if service.Links != nil { + return err(service.Name, "links") + } + if service.MemSwappiness > 0 { + return err(service.Name, "mem_swappiness") + } + if service.MemSwapLimit > 0 { + return err(service.Name, "memswap_limit") + } + if service.Secrets != nil { + return err(service.Name, "secrets") + } + if service.StorageOpt != nil { + return err(service.Name, "storage_opt") + } + // we only allow the 'default' network, nothing else. + if x := service.Networks; x != nil { + if len(x) != 1 { + return err(service.Name, "networks") + } + config, ok := x["default"] + if !ok || config != nil { + return err(service.Name, "networks") + } + } + } + + return nil +}