diff --git a/bake/bake_test.go b/bake/bake_test.go index bf72d8af..1670382d 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -278,9 +278,19 @@ services: `), } + fp3 := File{ + Name: "docker-compose3.yml", + Data: []byte( + `version: "3" +services: + webapp: + entrypoint: echo 1 +`), + } + ctx := context.TODO() - m, g, err := ReadTargets(ctx, []File{fp, fp2}, []string{"default"}, nil, nil) + m, g, err := ReadTargets(ctx, []File{fp, fp2, fp3}, []string{"default"}, nil, nil) require.NoError(t, err) require.Equal(t, 3, len(m)) @@ -446,6 +456,40 @@ func TestReadContextFromTargetUnknown(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "failed to find target bar") } + +func TestReadEmptyTargets(t *testing.T) { + t.Parallel() + + fp := File{ + Name: "docker-bake.hcl", + Data: []byte(`target "app1" {}`), + } + + fp2 := File{ + Name: "docker-compose.yml", + Data: []byte(` +services: + app2: {} +`), + } + + ctx := context.TODO() + + m, _, err := ReadTargets(ctx, []File{fp, fp2}, []string{"app1", "app2"}, nil, nil) + require.NoError(t, err) + + require.Equal(t, 2, len(m)) + _, ok := m["app1"] + require.True(t, ok) + _, ok = m["app2"] + require.True(t, ok) + + require.Equal(t, "Dockerfile", *m["app1"].Dockerfile) + require.Equal(t, ".", *m["app1"].Context) + require.Equal(t, "Dockerfile", *m["app2"].Dockerfile) + require.Equal(t, ".", *m["app2"].Context) +} + func TestReadContextFromTargetChain(t *testing.T) { ctx := context.TODO() fp := File{ diff --git a/bake/compose.go b/bake/compose.go index 08aa96df..3ce1eb8e 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -3,7 +3,6 @@ package bake import ( "fmt" "os" - "reflect" "strings" "github.com/compose-spec/compose-go/loader" @@ -12,6 +11,9 @@ import ( "gopkg.in/yaml.v3" ) +// errComposeInvalid is returned when a compose file is invalid +var errComposeInvalid = errors.New("invalid compose file") + func parseCompose(dt []byte) (*compose.Project, error) { return loader.Load(compose.ConfigDetails{ ConfigFiles: []compose.ConfigFile{ @@ -22,6 +24,7 @@ func parseCompose(dt []byte) (*compose.Project, error) { Environment: envMap(os.Environ()), }, func(options *loader.Options) { options.SkipNormalization = true + options.SkipConsistencyCheck = true }) } @@ -42,9 +45,11 @@ func ParseCompose(dt []byte) (*Config, error) { if err != nil { return nil, err } + if err = composeValidate(cfg); err != nil { + return nil, err + } var c Config - var zeroBuildConfig compose.BuildConfig if len(cfg.Services) > 0 { c.Groups = []*Group{} c.Targets = []*Target{} @@ -52,13 +57,8 @@ func ParseCompose(dt []byte) (*Config, error) { g := &Group{Name: "default"} for _, s := range cfg.Services { - - if s.Build == nil || reflect.DeepEqual(s.Build, zeroBuildConfig) { - // if not make sure they're setting an image or it's invalid d-c.yml - if s.Image == "" { - return nil, fmt.Errorf("compose file invalid: service %s has neither an image nor a build context specified. At least one must be provided", s.Name) - } - continue + if s.Build == nil { + s.Build = &compose.BuildConfig{} } if err = validateTargetName(s.Name); err != nil { @@ -219,6 +219,28 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error { return nil } +// compposeValidate validates a compose file +func composeValidate(project *compose.Project) error { + for _, s := range project.Services { + if s.Build != nil { + for _, secret := range s.Build.Secrets { + if _, ok := project.Secrets[secret.Source]; !ok { + return errors.Wrap(errComposeInvalid, fmt.Sprintf("service %q refers to undefined build secret %s", s.Name, secret.Source)) + } + } + } + } + for name, secret := range project.Secrets { + if secret.External.External { + continue + } + if secret.File == "" && secret.Environment == "" { + return errors.Wrap(errComposeInvalid, fmt.Sprintf("secret %q must declare either `file` or `environment`", name)) + } + } + return nil +} + // composeToBuildkitSecret converts secret from compose format to buildkit's // csv format. func composeToBuildkitSecret(inp compose.ServiceSecretConfig, psecret compose.SecretConfig) (string, error) { diff --git a/bake/compose_test.go b/bake/compose_test.go index 0735ee80..2ef51a05 100644 --- a/bake/compose_test.go +++ b/bake/compose_test.go @@ -153,21 +153,15 @@ services: require.Equal(t, c.Targets[0].Args["BRB"], "FOO") } -func TestBogusCompose(t *testing.T) { +func TestInconsistentComposeFile(t *testing.T) { var dt = []byte(` services: - db: - labels: - - "foo" webapp: - build: - context: . - target: webapp + entrypoint: echo 1 `) _, err := ParseCompose(dt) - require.Error(t, err) - require.Contains(t, err.Error(), "has neither an image nor a build context specified: invalid compose project") + require.NoError(t, err) } func TestAdvancedNetwork(t *testing.T) { @@ -420,3 +414,69 @@ services: }) } } + +func TestValidateComposeSecret(t *testing.T) { + cases := []struct { + name string + dt []byte + wantErr bool + }{ + { + name: "secret set by file", + dt: []byte(` +secrets: + foo: + file: .secret +`), + wantErr: false, + }, + { + name: "secret set by environment", + dt: []byte(` +secrets: + foo: + environment: TOKEN +`), + wantErr: false, + }, + { + name: "external secret", + dt: []byte(` +secrets: + foo: + external: true +`), + wantErr: false, + }, + { + name: "unset secret", + dt: []byte(` +secrets: + foo: {} +`), + wantErr: true, + }, + { + name: "undefined secret", + dt: []byte(` +services: + foo: + build: + secrets: + - token +`), + wantErr: true, + }, + } + for _, tt := range cases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + _, err := ParseCompose(tt.dt) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +}