bake: fix compose consistency check

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax 2022-06-23 13:09:13 +02:00
parent a8bb25d1b5
commit 5ce3909c48
No known key found for this signature in database
GPG Key ID: 3248E46B6BB8C7F7
3 changed files with 145 additions and 19 deletions

View File

@ -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{

View File

@ -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) {

View File

@ -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)
}
})
}
}