Merge pull request #3292 from crazy-max/compose-filtered-spec

bake: ignore unrelated fields when parsing compose files
This commit is contained in:
Tõnis Tiigi
2025-07-13 23:36:05 -07:00
committed by GitHub
15 changed files with 694 additions and 488 deletions
+64 -25
View File
@@ -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
+36 -11
View File
@@ -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) {