package bake import ( "os" "path/filepath" "sort" "testing" compose "github.com/compose-spec/compose-go/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestParseCompose(t *testing.T) { var dt = []byte(` services: db: build: ./db command: ./entrypoint.sh image: docker.io/tonistiigi/db webapp: build: context: ./dir dockerfile: Dockerfile-alternate network: none args: buildno: 123 cache_from: - type=local,src=path/to/cache cache_to: - type=local,dest=path/to/cache secrets: - token - aws secrets: token: environment: ENV_TOKEN aws: file: /root/.aws/credentials `) c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 1, len(c.Groups)) require.Equal(t, "default", c.Groups[0].Name) sort.Strings(c.Groups[0].Targets) require.Equal(t, []string{"db", "webapp"}, c.Groups[0].Targets) require.Equal(t, 2, len(c.Targets)) sort.Slice(c.Targets, func(i, j int) bool { return c.Targets[i].Name < c.Targets[j].Name }) require.Equal(t, "db", c.Targets[0].Name) require.Equal(t, "./db", *c.Targets[0].Context) require.Equal(t, []string{"docker.io/tonistiigi/db"}, c.Targets[0].Tags) require.Equal(t, "webapp", c.Targets[1].Name) require.Equal(t, "./dir", *c.Targets[1].Context) require.Equal(t, "Dockerfile-alternate", *c.Targets[1].Dockerfile) require.Equal(t, 1, len(c.Targets[1].Args)) require.Equal(t, "123", c.Targets[1].Args["buildno"]) require.Equal(t, []string{"type=local,src=path/to/cache"}, c.Targets[1].CacheFrom) require.Equal(t, []string{"type=local,dest=path/to/cache"}, c.Targets[1].CacheTo) require.Equal(t, "none", *c.Targets[1].NetworkMode) require.Equal(t, []string{ "id=token,env=ENV_TOKEN", "id=aws,src=/root/.aws/credentials", }, c.Targets[1].Secrets) } func TestNoBuildOutOfTreeService(t *testing.T) { var dt = []byte(` services: external: image: "verycooldb:1337" webapp: build: ./db `) c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 1, len(c.Groups)) require.Equal(t, 1, len(c.Targets)) } func TestParseComposeTarget(t *testing.T) { var dt = []byte(` services: db: build: context: ./db target: db webapp: build: context: . target: webapp `) c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 2, len(c.Targets)) sort.Slice(c.Targets, func(i, j int) bool { return c.Targets[i].Name < c.Targets[j].Name }) require.Equal(t, "db", c.Targets[0].Name) require.Equal(t, "db", *c.Targets[0].Target) require.Equal(t, "webapp", c.Targets[1].Name) require.Equal(t, "webapp", *c.Targets[1].Target) } func TestComposeBuildWithoutContext(t *testing.T) { var dt = []byte(` services: db: build: target: db webapp: build: context: . target: webapp `) c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 2, len(c.Targets)) sort.Slice(c.Targets, func(i, j int) bool { return c.Targets[i].Name < c.Targets[j].Name }) require.Equal(t, "db", c.Targets[0].Name) require.Equal(t, "db", *c.Targets[0].Target) require.Equal(t, "webapp", c.Targets[1].Name) require.Equal(t, "webapp", *c.Targets[1].Target) } func TestBuildArgEnvCompose(t *testing.T) { var dt = []byte(` version: "3.8" services: example: image: example build: context: . dockerfile: Dockerfile args: FOO: BAR: $ZZZ_BAR BRB: FOO `) os.Setenv("FOO", "bar") defer os.Unsetenv("FOO") os.Setenv("BAR", "foo") defer os.Unsetenv("BAR") os.Setenv("ZZZ_BAR", "zzz_foo") defer os.Unsetenv("ZZZ_BAR") c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, sliceToMap(os.Environ())) require.NoError(t, err) require.Equal(t, "bar", c.Targets[0].Args["FOO"]) require.Equal(t, "zzz_foo", c.Targets[0].Args["BAR"]) require.Equal(t, "FOO", c.Targets[0].Args["BRB"]) } func TestInconsistentComposeFile(t *testing.T) { var dt = []byte(` services: webapp: entrypoint: echo 1 `) _, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.Error(t, err) } func TestAdvancedNetwork(t *testing.T) { var dt = []byte(` services: db: networks: - example.com build: context: ./db target: db networks: example.com: name: example.com driver: bridge ipam: config: - subnet: 10.5.0.0/24 ip_range: 10.5.0.0/24 gateway: 10.5.0.254 `) _, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) } func TestTags(t *testing.T) { var dt = []byte(` services: example: image: example build: context: . dockerfile: Dockerfile tags: - foo - bar `) c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, []string{"foo", "bar"}, c.Targets[0].Tags) } func TestDependsOnList(t *testing.T) { var dt = []byte(` version: "3.8" services: example-container: image: example/fails:latest build: context: . dockerfile: Dockerfile depends_on: other-container: condition: service_healthy networks: default: aliases: - integration-tests other-container: image: example/other:latest healthcheck: test: ["CMD", "echo", "success"] retries: 5 interval: 5s timeout: 10s start_period: 5s networks: default: name: test-net `) _, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) } func TestComposeExt(t *testing.T) { var dt = []byte(` services: addon: image: ct-addon:bar build: context: . dockerfile: ./Dockerfile cache_from: - user/app:cache cache_to: - user/app:cache tags: - ct-addon:baz args: CT_ECR: foo CT_TAG: bar x-bake: contexts: alpine: docker-image://alpine:3.13 tags: - ct-addon:foo - ct-addon:alp platforms: - linux/amd64 - linux/arm64 cache-from: - type=local,src=path/to/cache cache-to: - type=local,dest=path/to/cache pull: true aws: image: ct-fake-aws:bar build: dockerfile: ./aws.Dockerfile args: CT_ECR: foo CT_TAG: bar x-bake: secret: - id=mysecret,src=/local/secret - id=mysecret2,src=/local/secret2 ssh: default platforms: linux/arm64 output: type=docker no-cache: true `) c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 2, len(c.Targets)) sort.Slice(c.Targets, func(i, j int) bool { return c.Targets[i].Name < c.Targets[j].Name }) require.Equal(t, map[string]string{"CT_ECR": "foo", "CT_TAG": "bar"}, c.Targets[0].Args) require.Equal(t, []string{"ct-addon:baz", "ct-addon:foo", "ct-addon:alp"}, c.Targets[0].Tags) require.Equal(t, []string{"linux/amd64", "linux/arm64"}, c.Targets[0].Platforms) require.Equal(t, []string{"user/app:cache", "type=local,src=path/to/cache"}, c.Targets[0].CacheFrom) require.Equal(t, []string{"user/app:cache", "type=local,dest=path/to/cache"}, c.Targets[0].CacheTo) require.Equal(t, newBool(true), c.Targets[0].Pull) require.Equal(t, map[string]string{"alpine": "docker-image://alpine:3.13"}, c.Targets[0].Contexts) require.Equal(t, []string{"ct-fake-aws:bar"}, c.Targets[1].Tags) require.Equal(t, []string{"id=mysecret,src=/local/secret", "id=mysecret2,src=/local/secret2"}, c.Targets[1].Secrets) require.Equal(t, []string{"default"}, c.Targets[1].SSH) require.Equal(t, []string{"linux/arm64"}, c.Targets[1].Platforms) require.Equal(t, []string{"type=docker"}, c.Targets[1].Outputs) require.Equal(t, newBool(true), c.Targets[1].NoCache) } func TestComposeExtDedup(t *testing.T) { var dt = []byte(` services: webapp: image: app:bar build: cache_from: - user/app:cache cache_to: - user/app:cache tags: - ct-addon:foo x-bake: tags: - ct-addon:foo - ct-addon:baz cache-from: - user/app:cache - type=local,src=path/to/cache cache-to: - type=local,dest=path/to/cache `) c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) require.Equal(t, []string{"ct-addon:foo", "ct-addon:baz"}, c.Targets[0].Tags) require.Equal(t, []string{"user/app:cache", "type=local,src=path/to/cache"}, c.Targets[0].CacheFrom) require.Equal(t, []string{"user/app:cache", "type=local,dest=path/to/cache"}, c.Targets[0].CacheTo) } func TestEnv(t *testing.T) { envf, err := os.CreateTemp("", "env") require.NoError(t, err) defer os.Remove(envf.Name()) _, err = envf.WriteString("FOO=bsdf -csdf\n") require.NoError(t, err) var dt = []byte(` services: scratch: build: context: . args: CT_ECR: foo FOO: NODE_ENV: environment: - NODE_ENV=test - AWS_ACCESS_KEY_ID=dummy - AWS_SECRET_ACCESS_KEY=dummy env_file: - ` + envf.Name() + ` `) c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, map[string]string{"CT_ECR": "foo", "FOO": "bsdf -csdf", "NODE_ENV": "test"}, c.Targets[0].Args) } func TestDotEnv(t *testing.T) { tmpdir := t.TempDir() err := os.WriteFile(filepath.Join(tmpdir, ".env"), []byte("FOO=bar"), 0644) require.NoError(t, err) var dt = []byte(` services: scratch: build: context: . args: FOO: `) chdir(t, tmpdir) c, err := ParseComposeFiles([]File{{ Name: "docker-compose.yml", Data: dt, }}) require.NoError(t, err) require.Equal(t, map[string]string{"FOO": "bar"}, c.Targets[0].Args) } func TestPorts(t *testing.T) { var dt = []byte(` services: foo: build: context: . ports: - 3306:3306 bar: build: context: . ports: - mode: ingress target: 3306 published: "3306" protocol: tcp `) _, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) } func newBool(val bool) *bool { b := val return &b } func TestServiceName(t *testing.T) { cases := []struct { svc string wantErr bool }{ { svc: "a", wantErr: false, }, { svc: "abc", wantErr: false, }, { svc: "a.b", wantErr: false, }, { svc: "_a", wantErr: false, }, { svc: "a_b", wantErr: false, }, { svc: "AbC", wantErr: false, }, { svc: "AbC-0123", wantErr: false, }, } for _, tt := range cases { tt := tt t.Run(tt.svc, func(t *testing.T) { _, err := ParseCompose([]compose.ConfigFile{{Content: []byte(` services: ` + tt.svc + `: build: context: . `)}}, nil) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } }) } } 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([]compose.ConfigFile{{Content: tt.dt}}, nil) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestValidateComposeFile(t *testing.T) { cases := []struct { name string fn string dt []byte isCompose bool wantErr bool }{ { name: "empty service", fn: "docker-compose.yml", dt: []byte(` services: foo: `), isCompose: true, wantErr: true, }, { name: "build", fn: "docker-compose.yml", dt: []byte(` services: foo: build: . `), isCompose: true, wantErr: false, }, { name: "image", fn: "docker-compose.yml", dt: []byte(` services: simple: image: nginx `), isCompose: true, wantErr: false, }, { name: "unknown ext", fn: "docker-compose.foo", dt: []byte(` services: simple: image: nginx `), isCompose: true, wantErr: false, }, { name: "hcl", fn: "docker-bake.hcl", dt: []byte(` target "default" { dockerfile = "test" } `), isCompose: false, wantErr: false, }, } for _, tt := range cases { tt := tt 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) } }) } } // 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) { olddir, err := os.Getwd() if err != nil { t.Fatalf("chdir: %v", err) } if err := os.Chdir(dir); err != nil { t.Fatalf("chdir %s: %v", dir, err) } t.Cleanup(func() { if err := os.Chdir(olddir); err != nil { t.Errorf("chdir to original working directory %s: %v", olddir, err) os.Exit(1) } }) }