diff --git a/bake/compose.go b/bake/compose.go index 528e1fd05a2b..8841d96bfb29 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -266,6 +266,10 @@ func loadComposeFiles(cfgs []composetypes.ConfigFile, envs map[string]string, op return nil, errors.New("empty compose file") } + // compose-go schema validation does a JSON round trip that converts nil slices + // from YAML [] values into null, so keep empty lists as arrays before validation. + // buildx#3849 + normalizeEmptyLists(filtered) if err := composeschema.Validate(filtered); err != nil { return nil, err } @@ -279,6 +283,23 @@ func loadComposeFiles(cfgs []composetypes.ConfigFile, envs map[string]string, op }) } +func normalizeEmptyLists(value any) any { + switch v := value.(type) { + case []any: + if v == nil { + return []any{} + } + for i, e := range v { + v[i] = normalizeEmptyLists(e) + } + case map[string]any: + for k, e := range v { + v[k] = normalizeEmptyLists(e) + } + } + return value +} + func validateComposeFile(dt []byte, fn string, envOverrides map[string]string) (bool, error) { envs, err := composeEnv(envOverrides) if err != nil { diff --git a/bake/compose_test.go b/bake/compose_test.go index 8a18fa792b3e..da8ba2cfa9f1 100644 --- a/bake/compose_test.go +++ b/bake/compose_test.go @@ -93,6 +93,23 @@ secrets: require.Equal(t, "FROM alpine\n", *c.Targets[2].DockerfileInline) } +func TestParseComposeEmptyCacheLists(t *testing.T) { + dt := []byte(` +services: + webapp: + build: + context: ./dir + cache_from: [] + cache_to: [] +`) + + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) + require.NoError(t, err) + require.Len(t, c.Targets, 1) + require.Empty(t, c.Targets[0].CacheFrom) + require.Empty(t, c.Targets[0].CacheTo) +} + func TestNoBuildOutOfTreeService(t *testing.T) { dt := []byte(` services: