From a378f8095e9331f704b69214ed1de5b56a24fc49 Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Tue, 9 Aug 2022 15:44:52 +0200 Subject: [PATCH 1/2] test: misplaced expected value in assert Signed-off-by: CrazyMax --- bake/compose_test.go | 52 ++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/bake/compose_test.go b/bake/compose_test.go index 4f370c29e2a..03279f5a2d7 100644 --- a/bake/compose_test.go +++ b/bake/compose_test.go @@ -41,7 +41,7 @@ secrets: require.NoError(t, err) require.Equal(t, 1, len(c.Groups)) - require.Equal(t, c.Groups[0].Name, "default") + require.Equal(t, "default", c.Groups[0].Name) sort.Strings(c.Groups[0].Targets) require.Equal(t, []string{"db", "webapp"}, c.Groups[0].Targets) @@ -58,8 +58,8 @@ secrets: 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, c.Targets[1].CacheFrom, []string{"type=local,src=path/to/cache"}) - require.Equal(t, c.Targets[1].CacheTo, []string{"type=local,dest=path/to/cache"}) + 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", @@ -124,9 +124,9 @@ services: sort.Slice(c.Targets, func(i, j int) bool { return c.Targets[i].Name < c.Targets[j].Name }) - require.Equal(t, c.Targets[0].Name, "db") + require.Equal(t, "db", c.Targets[0].Name) require.Equal(t, "db", *c.Targets[0].Target) - require.Equal(t, c.Targets[1].Name, "webapp") + require.Equal(t, "webapp", c.Targets[1].Name) require.Equal(t, "webapp", *c.Targets[1].Target) } @@ -154,9 +154,9 @@ services: c, err := ParseCompose(dt) require.NoError(t, err) - require.Equal(t, c.Targets[0].Args["FOO"], "bar") - require.Equal(t, c.Targets[0].Args["BAR"], "zzz_foo") - require.Equal(t, c.Targets[0].Args["BRB"], "FOO") + 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) { @@ -210,7 +210,7 @@ services: c, err := ParseCompose(dt) require.NoError(t, err) - require.Equal(t, c.Targets[0].Tags, []string{"foo", "bar"}) + require.Equal(t, []string{"foo", "bar"}, c.Targets[0].Tags) } func TestDependsOnList(t *testing.T) { @@ -304,19 +304,19 @@ services: sort.Slice(c.Targets, func(i, j int) bool { return c.Targets[i].Name < c.Targets[j].Name }) - require.Equal(t, c.Targets[0].Args, map[string]string{"CT_ECR": "foo", "CT_TAG": "bar"}) - require.Equal(t, c.Targets[0].Tags, []string{"ct-addon:baz", "ct-addon:foo", "ct-addon:alp"}) - require.Equal(t, c.Targets[0].Platforms, []string{"linux/amd64", "linux/arm64"}) - require.Equal(t, c.Targets[0].CacheFrom, []string{"user/app:cache", "type=local,src=path/to/cache"}) - require.Equal(t, c.Targets[0].CacheTo, []string{"user/app:cache", "type=local,dest=path/to/cache"}) - require.Equal(t, c.Targets[0].Pull, newBool(true)) - require.Equal(t, c.Targets[0].Contexts, map[string]string{"alpine": "docker-image://alpine:3.13"}) - require.Equal(t, c.Targets[1].Tags, []string{"ct-fake-aws:bar"}) - require.Equal(t, c.Targets[1].Secrets, []string{"id=mysecret,src=/local/secret", "id=mysecret2,src=/local/secret2"}) - require.Equal(t, c.Targets[1].SSH, []string{"default"}) - require.Equal(t, c.Targets[1].Platforms, []string{"linux/arm64"}) - require.Equal(t, c.Targets[1].Outputs, []string{"type=docker"}) - require.Equal(t, c.Targets[1].NoCache, newBool(true)) + 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) { @@ -345,9 +345,9 @@ services: c, err := ParseCompose(dt) require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) - require.Equal(t, c.Targets[0].Tags, []string{"ct-addon:foo", "ct-addon:baz"}) - require.Equal(t, c.Targets[0].CacheFrom, []string{"user/app:cache", "type=local,src=path/to/cache"}) - require.Equal(t, c.Targets[0].CacheTo, []string{"user/app:cache", "type=local,dest=path/to/cache"}) + 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) { @@ -377,7 +377,7 @@ services: c, err := ParseCompose(dt) require.NoError(t, err) - require.Equal(t, c.Targets[0].Args, map[string]string{"CT_ECR": "foo", "FOO": "bsdf -csdf", "NODE_ENV": "test"}) + require.Equal(t, map[string]string{"CT_ECR": "foo", "FOO": "bsdf -csdf", "NODE_ENV": "test"}, c.Targets[0].Args) } func TestPorts(t *testing.T) { From 7d8a6bc1d7d9011f99a6d14840805bde77b9945f Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Wed, 10 Aug 2022 03:24:33 +0200 Subject: [PATCH 2/2] bake: load .env file from working dir for compose files Signed-off-by: CrazyMax --- bake/bake.go | 26 +++++++++++- bake/compose.go | 60 +++++++++++++++++--------- bake/compose_test.go | 72 +++++++++++++++++++++++++------- docs/guides/bake/compose-file.md | 50 ++++++++++++++++++++++ 4 files changed, 171 insertions(+), 37 deletions(-) diff --git a/bake/bake.go b/bake/bake.go index 5f93157267f..573556cb14f 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -179,6 +179,21 @@ func dedupMap(ms ...map[string]string) map[string]string { return res } +func sliceToMap(env []string) (res map[string]string) { + res = make(map[string]string) + for _, s := range env { + kv := strings.SplitN(s, "=", 2) + key := kv[0] + switch { + case len(kv) == 1: + res[key] = "" + default: + res[key] = kv[1] + } + } + return +} + func ParseFiles(files []File, defaults map[string]string) (_ *Config, err error) { defer func() { err = formatHCLError(err, files) @@ -242,15 +257,22 @@ func ParseFile(dt []byte, fn string) (*Config, error) { } func ParseComposeFile(dt []byte, fn string) (*Config, bool, error) { + envs := sliceToMap(os.Environ()) + if wd, err := os.Getwd(); err == nil { + envs, err = loadDotEnv(envs, wd) + if err != nil { + return nil, true, err + } + } fnl := strings.ToLower(fn) if strings.HasSuffix(fnl, ".yml") || strings.HasSuffix(fnl, ".yaml") { - cfg, err := ParseCompose(dt) + cfg, err := ParseCompose(dt, envs) return cfg, true, err } if strings.HasSuffix(fnl, ".json") || strings.HasSuffix(fnl, ".hcl") { return nil, false, nil } - cfg, err := ParseCompose(dt) + cfg, err := ParseCompose(dt, envs) return cfg, err == nil, err } diff --git a/bake/compose.go b/bake/compose.go index 1f0a3b5d1ee..d89c5d8a4d8 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -3,8 +3,10 @@ package bake import ( "fmt" "os" + "path/filepath" "strings" + "github.com/compose-spec/compose-go/dotenv" "github.com/compose-spec/compose-go/loader" compose "github.com/compose-spec/compose-go/types" "github.com/pkg/errors" @@ -14,34 +16,18 @@ import ( // 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{ +func ParseCompose(dt []byte, envs map[string]string) (*Config, error) { + cfg, err := loader.Load(compose.ConfigDetails{ ConfigFiles: []compose.ConfigFile{ { Content: dt, }, }, - Environment: envMap(os.Environ()), + Environment: envs, }, func(options *loader.Options) { options.SkipNormalization = true options.SkipConsistencyCheck = true }) -} - -func envMap(env []string) map[string]string { - result := make(map[string]string, len(env)) - for _, s := range env { - kv := strings.SplitN(s, "=", 2) - if len(kv) != 2 { - continue - } - result[kv[0]] = kv[1] - } - return result -} - -func ParseCompose(dt []byte) (*Config, error) { - cfg, err := parseCompose(dt) if err != nil { return nil, err } @@ -124,6 +110,42 @@ func ParseCompose(dt []byte) (*Config, error) { return &c, nil } +func loadDotEnv(curenv map[string]string, workingDir string) (map[string]string, error) { + if curenv == nil { + curenv = make(map[string]string) + } + + ef, err := filepath.Abs(filepath.Join(workingDir, ".env")) + if err != nil { + return nil, err + } + + if _, err = os.Stat(ef); os.IsNotExist(err) { + return curenv, nil + } else if err != nil { + return nil, err + } + + dt, err := os.ReadFile(ef) + if err != nil { + return nil, err + } + + envs, err := dotenv.UnmarshalBytes(dt) + if err != nil { + return nil, err + } + + for k, v := range envs { + if _, set := curenv[k]; set { + continue + } + curenv[k] = v + } + + return curenv, nil +} + func flatten(in compose.MappingWithEquals) compose.Mapping { if len(in) == 0 { return nil diff --git a/bake/compose_test.go b/bake/compose_test.go index 03279f5a2d7..4a075ab2245 100644 --- a/bake/compose_test.go +++ b/bake/compose_test.go @@ -2,6 +2,7 @@ package bake import ( "os" + "path/filepath" "sort" "testing" @@ -37,7 +38,7 @@ secrets: file: /root/.aws/credentials `) - c, err := ParseCompose(dt) + c, err := ParseCompose(dt, nil) require.NoError(t, err) require.Equal(t, 1, len(c.Groups)) @@ -75,7 +76,7 @@ services: webapp: build: ./db `) - c, err := ParseCompose(dt) + c, err := ParseCompose(dt, nil) require.NoError(t, err) require.Equal(t, 1, len(c.Groups)) } @@ -93,7 +94,7 @@ services: target: webapp `) - c, err := ParseCompose(dt) + c, err := ParseCompose(dt, nil) require.NoError(t, err) require.Equal(t, 2, len(c.Targets)) @@ -118,7 +119,7 @@ services: target: webapp `) - c, err := ParseCompose(dt) + c, err := ParseCompose(dt, nil) require.NoError(t, err) require.Equal(t, 2, len(c.Targets)) sort.Slice(c.Targets, func(i, j int) bool { @@ -152,7 +153,7 @@ services: os.Setenv("ZZZ_BAR", "zzz_foo") defer os.Unsetenv("ZZZ_BAR") - c, err := ParseCompose(dt) + c, err := ParseCompose(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"]) @@ -166,7 +167,7 @@ services: entrypoint: echo 1 `) - _, err := ParseCompose(dt) + _, err := ParseCompose(dt, nil) require.NoError(t, err) } @@ -191,7 +192,7 @@ networks: gateway: 10.5.0.254 `) - _, err := ParseCompose(dt) + _, err := ParseCompose(dt, nil) require.NoError(t, err) } @@ -208,7 +209,7 @@ services: - bar `) - c, err := ParseCompose(dt) + c, err := ParseCompose(dt, nil) require.NoError(t, err) require.Equal(t, []string{"foo", "bar"}, c.Targets[0].Tags) } @@ -245,7 +246,7 @@ networks: name: test-net `) - _, err := ParseCompose(dt) + _, err := ParseCompose(dt, nil) require.NoError(t, err) } @@ -298,7 +299,7 @@ services: no-cache: true `) - c, err := ParseCompose(dt) + c, err := ParseCompose(dt, nil) require.NoError(t, err) require.Equal(t, 2, len(c.Targets)) sort.Slice(c.Targets, func(i, j int) bool { @@ -342,7 +343,7 @@ services: - type=local,dest=path/to/cache `) - c, err := ParseCompose(dt) + c, err := ParseCompose(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) @@ -375,11 +376,32 @@ services: - ` + envf.Name() + ` `) - c, err := ParseCompose(dt) + c, err := ParseCompose(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 := ParseComposeFile(dt, "docker-compose.yml") + 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: @@ -397,7 +419,7 @@ services: published: "3306" protocol: tcp `) - _, err := ParseCompose(dt) + _, err := ParseCompose(dt, nil) require.NoError(t, err) } @@ -445,10 +467,10 @@ func TestServiceName(t *testing.T) { t.Run(tt.svc, func(t *testing.T) { _, err := ParseCompose([]byte(` services: - ` + tt.svc + `: + `+tt.svc+`: build: context: . -`)) +`), nil) if tt.wantErr { require.Error(t, err) } else { @@ -514,7 +536,7 @@ services: for _, tt := range cases { tt := tt t.Run(tt.name, func(t *testing.T) { - _, err := ParseCompose(tt.dt) + _, err := ParseCompose(tt.dt, nil) if tt.wantErr { require.Error(t, err) } else { @@ -523,3 +545,21 @@ services: }) } } + +// 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) + } + }) +} diff --git a/docs/guides/bake/compose-file.md b/docs/guides/bake/compose-file.md index 5d40a8c7966..6fcc43cb60c 100644 --- a/docs/guides/bake/compose-file.md +++ b/docs/guides/bake/compose-file.md @@ -94,6 +94,56 @@ limitations with the compose format: * Specifying variables or global scope attributes is not yet supported * `inherits` service field is not supported, but you can use [YAML anchors](https://docs.docker.com/compose/compose-file/#fragments) to reference other services like the example above +## `.env` file + +You can declare default environment variables in an environment file named +`.env`. This file will be loaded from the current working directory, +where the command is executed and applied to compose definitions passed +with `-f`. + +```yaml +# docker-compose.yml +services: + webapp: + image: docker.io/username/webapp:${TAG:-v1.0.0} + build: + dockerfile: Dockerfile +``` + +``` +# .env +TAG=v1.1.0 +``` + +```console +$ docker buildx bake --print +``` +```json +{ + "group": { + "default": { + "targets": [ + "webapp" + ] + } + }, + "target": { + "webapp": { + "context": ".", + "dockerfile": "Dockerfile", + "tags": [ + "docker.io/username/webapp:v1.1.0" + ] + } + } +} +``` + +> **Note** +> +> System environment variables take precedence over environment variables +> in `.env` file. + ## Extension field with `x-bake` Even if some fields are not (yet) available in the compose specification, you