Merge pull request #1261 from crazy-max/bake-env

bake: load .env file from working dir for compose files
pull/1266/head
Tõnis Tiigi 2 years ago committed by GitHub
commit 5b2e1d3ce4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -179,6 +179,21 @@ func dedupMap(ms ...map[string]string) map[string]string {
return res 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) { func ParseFiles(files []File, defaults map[string]string) (_ *Config, err error) {
defer func() { defer func() {
err = formatHCLError(err, files) 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) { 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) fnl := strings.ToLower(fn)
if strings.HasSuffix(fnl, ".yml") || strings.HasSuffix(fnl, ".yaml") { if strings.HasSuffix(fnl, ".yml") || strings.HasSuffix(fnl, ".yaml") {
cfg, err := ParseCompose(dt) cfg, err := ParseCompose(dt, envs)
return cfg, true, err return cfg, true, err
} }
if strings.HasSuffix(fnl, ".json") || strings.HasSuffix(fnl, ".hcl") { if strings.HasSuffix(fnl, ".json") || strings.HasSuffix(fnl, ".hcl") {
return nil, false, nil return nil, false, nil
} }
cfg, err := ParseCompose(dt) cfg, err := ParseCompose(dt, envs)
return cfg, err == nil, err return cfg, err == nil, err
} }

@ -3,8 +3,10 @@ package bake
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/compose-spec/compose-go/dotenv"
"github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/loader"
compose "github.com/compose-spec/compose-go/types" compose "github.com/compose-spec/compose-go/types"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -14,34 +16,18 @@ import (
// errComposeInvalid is returned when a compose file is invalid // errComposeInvalid is returned when a compose file is invalid
var errComposeInvalid = errors.New("invalid compose file") var errComposeInvalid = errors.New("invalid compose file")
func parseCompose(dt []byte) (*compose.Project, error) { func ParseCompose(dt []byte, envs map[string]string) (*Config, error) {
return loader.Load(compose.ConfigDetails{ cfg, err := loader.Load(compose.ConfigDetails{
ConfigFiles: []compose.ConfigFile{ ConfigFiles: []compose.ConfigFile{
{ {
Content: dt, Content: dt,
}, },
}, },
Environment: envMap(os.Environ()), Environment: envs,
}, func(options *loader.Options) { }, func(options *loader.Options) {
options.SkipNormalization = true options.SkipNormalization = true
options.SkipConsistencyCheck = 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 { if err != nil {
return nil, err return nil, err
} }
@ -124,6 +110,42 @@ func ParseCompose(dt []byte) (*Config, error) {
return &c, nil 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 { func flatten(in compose.MappingWithEquals) compose.Mapping {
if len(in) == 0 { if len(in) == 0 {
return nil return nil

@ -2,6 +2,7 @@ package bake
import ( import (
"os" "os"
"path/filepath"
"sort" "sort"
"testing" "testing"
@ -37,11 +38,11 @@ secrets:
file: /root/.aws/credentials file: /root/.aws/credentials
`) `)
c, err := ParseCompose(dt) c, err := ParseCompose(dt, nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(c.Groups)) 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) sort.Strings(c.Groups[0].Targets)
require.Equal(t, []string{"db", "webapp"}, c.Groups[0].Targets) require.Equal(t, []string{"db", "webapp"}, c.Groups[0].Targets)
@ -58,8 +59,8 @@ secrets:
require.Equal(t, "Dockerfile-alternate", *c.Targets[1].Dockerfile) require.Equal(t, "Dockerfile-alternate", *c.Targets[1].Dockerfile)
require.Equal(t, 1, len(c.Targets[1].Args)) require.Equal(t, 1, len(c.Targets[1].Args))
require.Equal(t, "123", c.Targets[1].Args["buildno"]) 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, []string{"type=local,src=path/to/cache"}, c.Targets[1].CacheFrom)
require.Equal(t, c.Targets[1].CacheTo, []string{"type=local,dest=path/to/cache"}) 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, "none", *c.Targets[1].NetworkMode)
require.Equal(t, []string{ require.Equal(t, []string{
"id=token,env=ENV_TOKEN", "id=token,env=ENV_TOKEN",
@ -75,7 +76,7 @@ services:
webapp: webapp:
build: ./db build: ./db
`) `)
c, err := ParseCompose(dt) c, err := ParseCompose(dt, nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(c.Groups)) require.Equal(t, 1, len(c.Groups))
} }
@ -93,7 +94,7 @@ services:
target: webapp target: webapp
`) `)
c, err := ParseCompose(dt) c, err := ParseCompose(dt, nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 2, len(c.Targets)) require.Equal(t, 2, len(c.Targets))
@ -118,15 +119,15 @@ services:
target: webapp target: webapp
`) `)
c, err := ParseCompose(dt) c, err := ParseCompose(dt, nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 2, len(c.Targets)) require.Equal(t, 2, len(c.Targets))
sort.Slice(c.Targets, func(i, j int) bool { sort.Slice(c.Targets, func(i, j int) bool {
return c.Targets[i].Name < c.Targets[j].Name 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, "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) require.Equal(t, "webapp", *c.Targets[1].Target)
} }
@ -152,11 +153,11 @@ services:
os.Setenv("ZZZ_BAR", "zzz_foo") os.Setenv("ZZZ_BAR", "zzz_foo")
defer os.Unsetenv("ZZZ_BAR") defer os.Unsetenv("ZZZ_BAR")
c, err := ParseCompose(dt) c, err := ParseCompose(dt, sliceToMap(os.Environ()))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, c.Targets[0].Args["FOO"], "bar") require.Equal(t, "bar", c.Targets[0].Args["FOO"])
require.Equal(t, c.Targets[0].Args["BAR"], "zzz_foo") require.Equal(t, "zzz_foo", c.Targets[0].Args["BAR"])
require.Equal(t, c.Targets[0].Args["BRB"], "FOO") require.Equal(t, "FOO", c.Targets[0].Args["BRB"])
} }
func TestInconsistentComposeFile(t *testing.T) { func TestInconsistentComposeFile(t *testing.T) {
@ -166,7 +167,7 @@ services:
entrypoint: echo 1 entrypoint: echo 1
`) `)
_, err := ParseCompose(dt) _, err := ParseCompose(dt, nil)
require.NoError(t, err) require.NoError(t, err)
} }
@ -191,7 +192,7 @@ networks:
gateway: 10.5.0.254 gateway: 10.5.0.254
`) `)
_, err := ParseCompose(dt) _, err := ParseCompose(dt, nil)
require.NoError(t, err) require.NoError(t, err)
} }
@ -208,9 +209,9 @@ services:
- bar - bar
`) `)
c, err := ParseCompose(dt) c, err := ParseCompose(dt, nil)
require.NoError(t, err) 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) { func TestDependsOnList(t *testing.T) {
@ -245,7 +246,7 @@ networks:
name: test-net name: test-net
`) `)
_, err := ParseCompose(dt) _, err := ParseCompose(dt, nil)
require.NoError(t, err) require.NoError(t, err)
} }
@ -298,25 +299,25 @@ services:
no-cache: true no-cache: true
`) `)
c, err := ParseCompose(dt) c, err := ParseCompose(dt, nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 2, len(c.Targets)) require.Equal(t, 2, len(c.Targets))
sort.Slice(c.Targets, func(i, j int) bool { sort.Slice(c.Targets, func(i, j int) bool {
return c.Targets[i].Name < c.Targets[j].Name 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, map[string]string{"CT_ECR": "foo", "CT_TAG": "bar"}, c.Targets[0].Args)
require.Equal(t, c.Targets[0].Tags, []string{"ct-addon:baz", "ct-addon:foo", "ct-addon:alp"}) require.Equal(t, []string{"ct-addon:baz", "ct-addon:foo", "ct-addon:alp"}, c.Targets[0].Tags)
require.Equal(t, c.Targets[0].Platforms, []string{"linux/amd64", "linux/arm64"}) require.Equal(t, []string{"linux/amd64", "linux/arm64"}, c.Targets[0].Platforms)
require.Equal(t, c.Targets[0].CacheFrom, []string{"user/app:cache", "type=local,src=path/to/cache"}) require.Equal(t, []string{"user/app:cache", "type=local,src=path/to/cache"}, c.Targets[0].CacheFrom)
require.Equal(t, c.Targets[0].CacheTo, []string{"user/app:cache", "type=local,dest=path/to/cache"}) require.Equal(t, []string{"user/app:cache", "type=local,dest=path/to/cache"}, c.Targets[0].CacheTo)
require.Equal(t, c.Targets[0].Pull, newBool(true)) require.Equal(t, newBool(true), c.Targets[0].Pull)
require.Equal(t, c.Targets[0].Contexts, map[string]string{"alpine": "docker-image://alpine:3.13"}) require.Equal(t, map[string]string{"alpine": "docker-image://alpine:3.13"}, c.Targets[0].Contexts)
require.Equal(t, c.Targets[1].Tags, []string{"ct-fake-aws:bar"}) require.Equal(t, []string{"ct-fake-aws:bar"}, c.Targets[1].Tags)
require.Equal(t, c.Targets[1].Secrets, []string{"id=mysecret,src=/local/secret", "id=mysecret2,src=/local/secret2"}) require.Equal(t, []string{"id=mysecret,src=/local/secret", "id=mysecret2,src=/local/secret2"}, c.Targets[1].Secrets)
require.Equal(t, c.Targets[1].SSH, []string{"default"}) require.Equal(t, []string{"default"}, c.Targets[1].SSH)
require.Equal(t, c.Targets[1].Platforms, []string{"linux/arm64"}) require.Equal(t, []string{"linux/arm64"}, c.Targets[1].Platforms)
require.Equal(t, c.Targets[1].Outputs, []string{"type=docker"}) require.Equal(t, []string{"type=docker"}, c.Targets[1].Outputs)
require.Equal(t, c.Targets[1].NoCache, newBool(true)) require.Equal(t, newBool(true), c.Targets[1].NoCache)
} }
func TestComposeExtDedup(t *testing.T) { func TestComposeExtDedup(t *testing.T) {
@ -342,12 +343,12 @@ services:
- type=local,dest=path/to/cache - type=local,dest=path/to/cache
`) `)
c, err := ParseCompose(dt) c, err := ParseCompose(dt, nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(c.Targets)) require.Equal(t, 1, len(c.Targets))
require.Equal(t, c.Targets[0].Tags, []string{"ct-addon:foo", "ct-addon:baz"}) require.Equal(t, []string{"ct-addon:foo", "ct-addon:baz"}, c.Targets[0].Tags)
require.Equal(t, c.Targets[0].CacheFrom, []string{"user/app:cache", "type=local,src=path/to/cache"}) require.Equal(t, []string{"user/app:cache", "type=local,src=path/to/cache"}, c.Targets[0].CacheFrom)
require.Equal(t, c.Targets[0].CacheTo, []string{"user/app:cache", "type=local,dest=path/to/cache"}) require.Equal(t, []string{"user/app:cache", "type=local,dest=path/to/cache"}, c.Targets[0].CacheTo)
} }
func TestEnv(t *testing.T) { func TestEnv(t *testing.T) {
@ -375,9 +376,30 @@ services:
- ` + envf.Name() + ` - ` + 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.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{"FOO": "bar"}, c.Targets[0].Args)
} }
func TestPorts(t *testing.T) { func TestPorts(t *testing.T) {
@ -397,7 +419,7 @@ services:
published: "3306" published: "3306"
protocol: tcp protocol: tcp
`) `)
_, err := ParseCompose(dt) _, err := ParseCompose(dt, nil)
require.NoError(t, err) require.NoError(t, err)
} }
@ -445,10 +467,10 @@ func TestServiceName(t *testing.T) {
t.Run(tt.svc, func(t *testing.T) { t.Run(tt.svc, func(t *testing.T) {
_, err := ParseCompose([]byte(` _, err := ParseCompose([]byte(`
services: services:
` + tt.svc + `: `+tt.svc+`:
build: build:
context: . context: .
`)) `), nil)
if tt.wantErr { if tt.wantErr {
require.Error(t, err) require.Error(t, err)
} else { } else {
@ -514,7 +536,7 @@ services:
for _, tt := range cases { for _, tt := range cases {
tt := tt tt := tt
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
_, err := ParseCompose(tt.dt) _, err := ParseCompose(tt.dt, nil)
if tt.wantErr { if tt.wantErr {
require.Error(t, err) require.Error(t, err)
} else { } 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)
}
})
}

@ -94,6 +94,56 @@ limitations with the compose format:
* Specifying variables or global scope attributes is not yet supported * 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 * `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` ## Extension field with `x-bake`
Even if some fields are not (yet) available in the compose specification, you Even if some fields are not (yet) available in the compose specification, you

Loading…
Cancel
Save