diff --git a/bake/compose.go b/bake/compose.go index 923b661fd..06467b466 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -11,6 +11,7 @@ import ( "github.com/compose-spec/compose-go/v2/consts" "github.com/compose-spec/compose-go/v2/dotenv" "github.com/compose-spec/compose-go/v2/loader" + composeschema "github.com/compose-spec/compose-go/v2/schema" composetypes "github.com/compose-spec/compose-go/v2/types" "github.com/docker/buildx/util/buildflags" dockeropts "github.com/docker/cli/opts" @@ -35,21 +36,7 @@ func ParseComposeFiles(fs []File) (*Config, error) { } func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Config, error) { - if envs == nil { - envs = make(map[string]string) - } - cfg, err := loader.LoadWithContext(context.Background(), composetypes.ConfigDetails{ - ConfigFiles: cfgs, - Environment: envs, - }, func(options *loader.Options) { - projectName := "bake" - if v, ok := envs[consts.ComposeProjectName]; ok && v != "" { - projectName = v - } - options.SetProjectName(projectName, false) - options.SkipNormalization = true - options.Profiles = []string{"*"} - }) + cfg, err := loadComposeFiles(cfgs, envs) if err != nil { return nil, err } @@ -208,6 +195,67 @@ func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Conf return &c, nil } +func loadComposeFiles(cfgs []composetypes.ConfigFile, envs map[string]string, options ...func(*loader.Options)) (*composetypes.Project, error) { + if envs == nil { + envs = make(map[string]string) + } + + cfgDetails := composetypes.ConfigDetails{ + ConfigFiles: cfgs, + Environment: envs, + } + + raw, err := loader.LoadModelWithContext(context.Background(), cfgDetails, append([]func(*loader.Options){func(opts *loader.Options) { + projectName := "bake" + if v, ok := envs[consts.ComposeProjectName]; ok && v != "" { + projectName = v + } + opts.SetProjectName(projectName, false) + opts.SkipNormalization = true + opts.SkipValidation = true + }}, options...)...) + if err != nil { + return nil, err + } + + filtered := make(map[string]any) + for _, key := range []string{"services", "secrets"} { + if key == "services" { + if services, ok := raw["services"].(map[string]any); ok { + filteredServices := make(map[string]any) + for svcName, svc := range services { + if svc == nil { + filteredServices[svcName] = map[string]any{} + } else if svcMap, ok := svc.(map[string]any); ok { + filteredService := make(map[string]any) + for _, svcField := range []string{"image", "build", "environment", "env_file"} { + if val, ok := svcMap[svcField]; ok { + filteredService[svcField] = val + } + } + filteredServices[svcName] = filteredService + } + } + filtered["services"] = filteredServices + } + } else if v, ok := raw[key]; ok { + filtered[key] = v + } + } + + if err := composeschema.Validate(filtered); err != nil { + return nil, err + } + + return loader.ModelToProject(filtered, loader.ToOptions(&cfgDetails, append([]func(*loader.Options){func(options *loader.Options) { + options.SkipNormalization = true + options.Profiles = []string{"*"} + }}, options...)), composetypes.ConfigDetails{ + ConfigFiles: cfgs, + Environment: envs, + }) +} + func validateComposeFile(dt []byte, fn string) (bool, error) { envs, err := composeEnv() if err != nil { @@ -225,16 +273,7 @@ func validateComposeFile(dt []byte, fn string) (bool, error) { } func validateCompose(dt []byte, envs map[string]string) error { - _, err := loader.LoadWithContext(context.Background(), composetypes.ConfigDetails{ - ConfigFiles: []composetypes.ConfigFile{ - { - Content: dt, - }, - }, - Environment: envs, - }, func(options *loader.Options) { - options.SetProjectName("bake", false) - options.SkipNormalization = true + _, err := loadComposeFiles([]composetypes.ConfigFile{{Content: dt}}, envs, func(options *loader.Options) { // consistency is checked later in ParseCompose to ensure multiple // compose files can be merged together options.SkipConsistencyCheck = true diff --git a/bake/compose_test.go b/bake/compose_test.go index 7d7a2cc74..018ed6434 100644 --- a/bake/compose_test.go +++ b/bake/compose_test.go @@ -192,6 +192,7 @@ services: _, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.Error(t, err) + require.ErrorContains(t, err, `has neither an image nor a build context specified`) } func TestAdvancedNetwork(t *testing.T) { @@ -610,7 +611,6 @@ func TestValidateComposeFile(t *testing.T) { fn string dt []byte isCompose bool - wantErr bool }{ { name: "empty service", @@ -620,7 +620,6 @@ services: foo: `), isCompose: true, - wantErr: true, }, { name: "build", @@ -631,7 +630,6 @@ services: build: . `), isCompose: true, - wantErr: false, }, { name: "image", @@ -642,7 +640,6 @@ services: image: nginx `), isCompose: true, - wantErr: false, }, { name: "unknown ext", @@ -653,7 +650,6 @@ services: image: nginx `), isCompose: true, - wantErr: false, }, { name: "hcl", @@ -664,18 +660,13 @@ target "default" { } `), isCompose: false, - wantErr: false, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { isCompose, err := validateComposeFile(tt.dt, tt.fn) assert.Equal(t, tt.isCompose, isCompose) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } + require.NoError(t, err) }) } } @@ -888,6 +879,40 @@ services: require.Equal(t, map[string]*string{"TEST_VALUE": ptrstr("abc"), "FOO_VALUE": ptrstr("abc")}, c.Targets[0].Args) } +func TestUnknownField(t *testing.T) { + tmpdir := t.TempDir() + dt := []byte(` +services: + webapp: + bar: baz + build: + context: . + +foo: + - bar.baz +`) + + chdir(t, tmpdir) + _, err := ParseComposeFiles([]File{{Name: "compose.yml", Data: dt}}) + require.NoError(t, err) +} + +func TestUnknownBuildField(t *testing.T) { + tmpdir := t.TempDir() + dt := []byte(` +services: + webapp: + build: + context: . + foo: bar +`) + + chdir(t, tmpdir) + _, err := ParseComposeFiles([]File{{Name: "compose.yml", Data: dt}}) + require.Error(t, err) + require.ErrorContains(t, err, `additional properties 'foo' not allowed`) +} + // chdir changes the current working directory to the named directory, // and then restore the original working directory at the end of the test. func chdir(t *testing.T, dir string) {