diff --git a/bake/bake.go b/bake/bake.go index 1b4f5f25..aea0832b 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -12,11 +12,13 @@ import ( "strconv" "strings" + composecli "github.com/compose-spec/compose-go/cli" "github.com/docker/buildx/bake/hclparser" "github.com/docker/buildx/build" controllerapi "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/platformutil" + "github.com/docker/cli/cli/config" hcl "github.com/hashicorp/hcl/v2" "github.com/moby/buildkit/client/llb" @@ -42,14 +44,15 @@ type Override struct { } func defaultFilenames() []string { - return []string{ - "docker-compose.yml", // support app - "docker-compose.yaml", // support app + names := []string{} + names = append(names, composecli.DefaultFileNames...) + names = append(names, []string{ "docker-bake.json", "docker-bake.override.json", "docker-bake.hcl", "docker-bake.override.hcl", - } + }...) + return names } func ReadLocalFiles(names []string) ([]File, error) { diff --git a/bake/bake_test.go b/bake/bake_test.go index b42a7ee6..cd543ff3 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -2,6 +2,7 @@ package bake import ( "context" + "os" "sort" "strings" "testing" @@ -1363,3 +1364,56 @@ func TestJSONNullVars(t *testing.T) { require.NoError(t, err) require.Equal(t, map[string]*string{"bar": ptrstr("baz")}, m["default"].Args) } + +func TestReadLocalFilesDefault(t *testing.T) { + tests := []struct { + filenames []string + expected []string + }{ + { + filenames: []string{"abc.yml", "docker-compose.yml"}, + expected: []string{"docker-compose.yml"}, + }, + { + filenames: []string{"test.foo", "compose.yml", "docker-bake.hcl"}, + expected: []string{"compose.yml", "docker-bake.hcl"}, + }, + { + filenames: []string{"compose.yaml", "docker-compose.yml", "docker-bake.hcl"}, + expected: []string{"compose.yaml", "docker-compose.yml", "docker-bake.hcl"}, + }, + { + filenames: []string{"test.txt", "compsoe.yaml"}, // intentional misspell + expected: []string{}, + }, + } + pwd, err := os.Getwd() + require.NoError(t, err) + + for _, tt := range tests { + t.Run(strings.Join(tt.filenames, "-"), func(t *testing.T) { + dir := t.TempDir() + t.Cleanup(func() { _ = os.Chdir(pwd) }) + require.NoError(t, os.Chdir(dir)) + for _, tf := range tt.filenames { + require.NoError(t, os.WriteFile(tf, []byte(tf), 0644)) + } + files, err := ReadLocalFiles(nil) + require.NoError(t, err) + if len(files) == 0 { + require.Equal(t, len(tt.expected), len(files)) + } else { + found := false + for _, exp := range tt.expected { + for _, f := range files { + if f.Name == exp { + found = true + break + } + } + require.True(t, found, exp) + } + } + }) + } +} diff --git a/vendor/github.com/compose-spec/compose-go/cli/options.go b/vendor/github.com/compose-spec/compose-go/cli/options.go new file mode 100644 index 00000000..ebe61d2e --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/cli/options.go @@ -0,0 +1,445 @@ +/* + Copyright 2020 The Compose Specification Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cli + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/compose-spec/compose-go/consts" + "github.com/compose-spec/compose-go/dotenv" + "github.com/compose-spec/compose-go/errdefs" + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/utils" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// ProjectOptions groups the command line options recommended for a Compose implementation +type ProjectOptions struct { + Name string + WorkingDir string + ConfigPaths []string + Environment map[string]string + EnvFile string + loadOptions []func(*loader.Options) +} + +type ProjectOptionsFn func(*ProjectOptions) error + +// NewProjectOptions creates ProjectOptions +func NewProjectOptions(configs []string, opts ...ProjectOptionsFn) (*ProjectOptions, error) { + options := &ProjectOptions{ + ConfigPaths: configs, + Environment: map[string]string{}, + } + for _, o := range opts { + err := o(options) + if err != nil { + return nil, err + } + } + return options, nil +} + +// WithName defines ProjectOptions' name +func WithName(name string) ProjectOptionsFn { + return func(o *ProjectOptions) error { + if name != loader.NormalizeProjectName(name) { + return fmt.Errorf("%q is not a valid project name", name) + } + o.Name = name + return nil + } +} + +// WithWorkingDirectory defines ProjectOptions' working directory +func WithWorkingDirectory(wd string) ProjectOptionsFn { + return func(o *ProjectOptions) error { + if wd == "" { + return nil + } + abs, err := filepath.Abs(wd) + if err != nil { + return err + } + o.WorkingDir = abs + return nil + } +} + +// WithConfigFileEnv allow to set compose config file paths by COMPOSE_FILE environment variable +func WithConfigFileEnv(o *ProjectOptions) error { + if len(o.ConfigPaths) > 0 { + return nil + } + sep := o.Environment[consts.ComposePathSeparator] + if sep == "" { + sep = string(os.PathListSeparator) + } + f, ok := o.Environment[consts.ComposeFilePath] + if ok { + paths, err := absolutePaths(strings.Split(f, sep)) + o.ConfigPaths = paths + return err + } + return nil +} + +// WithDefaultConfigPath searches for default config files from working directory +func WithDefaultConfigPath(o *ProjectOptions) error { + if len(o.ConfigPaths) > 0 { + return nil + } + pwd, err := o.GetWorkingDir() + if err != nil { + return err + } + for { + candidates := findFiles(DefaultFileNames, pwd) + if len(candidates) > 0 { + winner := candidates[0] + if len(candidates) > 1 { + logrus.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", ")) + logrus.Warnf("Using %s", winner) + } + o.ConfigPaths = append(o.ConfigPaths, winner) + + overrides := findFiles(DefaultOverrideFileNames, pwd) + if len(overrides) > 0 { + if len(overrides) > 1 { + logrus.Warnf("Found multiple override files with supported names: %s", strings.Join(overrides, ", ")) + logrus.Warnf("Using %s", overrides[0]) + } + o.ConfigPaths = append(o.ConfigPaths, overrides[0]) + } + return nil + } + parent := filepath.Dir(pwd) + if parent == pwd { + // no config file found, but that's not a blocker if caller only needs project name + return nil + } + pwd = parent + } +} + +// WithEnv defines a key=value set of variables used for compose file interpolation +func WithEnv(env []string) ProjectOptionsFn { + return func(o *ProjectOptions) error { + for k, v := range utils.GetAsEqualsMap(env) { + o.Environment[k] = v + } + return nil + } +} + +// WithDiscardEnvFiles sets discards the `env_file` section after resolving to +// the `environment` section +func WithDiscardEnvFile(o *ProjectOptions) error { + o.loadOptions = append(o.loadOptions, loader.WithDiscardEnvFiles) + return nil +} + +// WithLoadOptions provides a hook to control how compose files are loaded +func WithLoadOptions(loadOptions ...func(*loader.Options)) ProjectOptionsFn { + return func(o *ProjectOptions) error { + o.loadOptions = append(o.loadOptions, loadOptions...) + return nil + } +} + +// WithProfiles sets profiles to be activated +func WithProfiles(profiles []string) ProjectOptionsFn { + return func(o *ProjectOptions) error { + o.loadOptions = append(o.loadOptions, loader.WithProfiles(profiles)) + return nil + } +} + +// WithOsEnv imports environment variables from OS +func WithOsEnv(o *ProjectOptions) error { + for k, v := range utils.GetAsEqualsMap(os.Environ()) { + if _, set := o.Environment[k]; set { + continue + } + o.Environment[k] = v + } + return nil +} + +// WithEnvFile set an alternate env file +func WithEnvFile(file string) ProjectOptionsFn { + return func(options *ProjectOptions) error { + options.EnvFile = file + return nil + } +} + +// WithDotEnv imports environment variables from .env file +func WithDotEnv(o *ProjectOptions) error { + wd, err := o.GetWorkingDir() + if err != nil { + return err + } + envMap, err := GetEnvFromFile(o.Environment, wd, o.EnvFile) + if err != nil { + return err + } + for k, v := range envMap { + o.Environment[k] = v + if osVal, ok := os.LookupEnv(k); ok { + o.Environment[k] = osVal + } + } + return nil +} + +func GetEnvFromFile(currentEnv map[string]string, workingDir string, filename string) (map[string]string, error) { + envMap := make(map[string]string) + + dotEnvFile := filename + if dotEnvFile == "" { + dotEnvFile = filepath.Join(workingDir, ".env") + } + abs, err := filepath.Abs(dotEnvFile) + if err != nil { + return envMap, err + } + dotEnvFile = abs + + s, err := os.Stat(dotEnvFile) + if os.IsNotExist(err) { + if filename != "" { + return nil, errors.Errorf("Couldn't find env file: %s", filename) + } + return envMap, nil + } + if err != nil { + return envMap, err + } + + if s.IsDir() { + if filename == "" { + return envMap, nil + } + return envMap, errors.Errorf("%s is a directory", dotEnvFile) + } + + file, err := os.Open(dotEnvFile) + if err != nil { + return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile) + } + defer file.Close() + + env, err := dotenv.ParseWithLookup(file, func(k string) (string, bool) { + v, ok := currentEnv[k] + if !ok { + return "", false + } + return v, true + }) + if err != nil { + return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile) + } + for k, v := range env { + envMap[k] = v + } + + return envMap, nil +} + +// WithInterpolation set ProjectOptions to enable/skip interpolation +func WithInterpolation(interpolation bool) ProjectOptionsFn { + return func(o *ProjectOptions) error { + o.loadOptions = append(o.loadOptions, func(options *loader.Options) { + options.SkipInterpolation = !interpolation + }) + return nil + } +} + +// WithNormalization set ProjectOptions to enable/skip normalization +func WithNormalization(normalization bool) ProjectOptionsFn { + return func(o *ProjectOptions) error { + o.loadOptions = append(o.loadOptions, func(options *loader.Options) { + options.SkipNormalization = !normalization + }) + return nil + } +} + +// WithConsistency set ProjectOptions to enable/skip consistency +func WithConsistency(consistency bool) ProjectOptionsFn { + return func(o *ProjectOptions) error { + o.loadOptions = append(o.loadOptions, func(options *loader.Options) { + options.SkipConsistencyCheck = !consistency + }) + return nil + } +} + +// WithResolvedPaths set ProjectOptions to enable paths resolution +func WithResolvedPaths(resolve bool) ProjectOptionsFn { + return func(o *ProjectOptions) error { + o.loadOptions = append(o.loadOptions, func(options *loader.Options) { + options.ResolvePaths = resolve + }) + return nil + } +} + +// DefaultFileNames defines the Compose file names for auto-discovery (in order of preference) +var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"} + +// DefaultOverrideFileNames defines the Compose override file names for auto-discovery (in order of preference) +var DefaultOverrideFileNames = []string{"compose.override.yml", "compose.override.yaml", "docker-compose.override.yml", "docker-compose.override.yaml"} + +func (o ProjectOptions) GetWorkingDir() (string, error) { + if o.WorkingDir != "" { + return o.WorkingDir, nil + } + for _, path := range o.ConfigPaths { + if path != "-" { + absPath, err := filepath.Abs(path) + if err != nil { + return "", err + } + return filepath.Dir(absPath), nil + } + } + return os.Getwd() +} + +// ProjectFromOptions load a compose project based on command line options +func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) { + configPaths, err := getConfigPathsFromOptions(options) + if err != nil { + return nil, err + } + + var configs []types.ConfigFile + for _, f := range configPaths { + var b []byte + if f == "-" { + b, err = io.ReadAll(os.Stdin) + if err != nil { + return nil, err + } + } else { + f, err := filepath.Abs(f) + if err != nil { + return nil, err + } + b, err = os.ReadFile(f) + if err != nil { + return nil, err + } + } + configs = append(configs, types.ConfigFile{ + Filename: f, + Content: b, + }) + } + + workingDir, err := options.GetWorkingDir() + if err != nil { + return nil, err + } + absWorkingDir, err := filepath.Abs(workingDir) + if err != nil { + return nil, err + } + + options.loadOptions = append(options.loadOptions, + withNamePrecedenceLoad(absWorkingDir, options), + withConvertWindowsPaths(options)) + + project, err := loader.Load(types.ConfigDetails{ + ConfigFiles: configs, + WorkingDir: workingDir, + Environment: options.Environment, + }, options.loadOptions...) + if err != nil { + return nil, err + } + + project.ComposeFiles = configPaths + return project, nil +} + +func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(*loader.Options) { + return func(opts *loader.Options) { + if options.Name != "" { + opts.SetProjectName(options.Name, true) + } else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" { + opts.SetProjectName(nameFromEnv, true) + } else { + opts.SetProjectName(filepath.Base(absWorkingDir), false) + } + } +} + +func withConvertWindowsPaths(options *ProjectOptions) func(*loader.Options) { + return func(o *loader.Options) { + o.ConvertWindowsPaths = utils.StringToBool(options.Environment["COMPOSE_CONVERT_WINDOWS_PATHS"]) + o.ResolvePaths = true + } +} + +// getConfigPathsFromOptions retrieves the config files for project based on project options +func getConfigPathsFromOptions(options *ProjectOptions) ([]string, error) { + if len(options.ConfigPaths) != 0 { + return absolutePaths(options.ConfigPaths) + } + return nil, errors.Wrap(errdefs.ErrNotFound, "no configuration file provided") +} + +func findFiles(names []string, pwd string) []string { + candidates := []string{} + for _, n := range names { + f := filepath.Join(pwd, n) + if _, err := os.Stat(f); err == nil { + candidates = append(candidates, f) + } + } + return candidates +} + +func absolutePaths(p []string) ([]string, error) { + var paths []string + for _, f := range p { + if f == "-" { + paths = append(paths, f) + continue + } + abs, err := filepath.Abs(f) + if err != nil { + return nil, err + } + f = abs + if _, err := os.Stat(f); err != nil { + return nil, err + } + paths = append(paths, f) + } + return paths, nil +} diff --git a/vendor/github.com/compose-spec/compose-go/utils/stringutils.go b/vendor/github.com/compose-spec/compose-go/utils/stringutils.go new file mode 100644 index 00000000..182ddf83 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/utils/stringutils.go @@ -0,0 +1,58 @@ +/* + Copyright 2020 The Compose Specification Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package utils + +import ( + "fmt" + "strconv" + "strings" +) + +// StringContains check if an array contains a specific value +func StringContains(array []string, needle string) bool { + for _, val := range array { + if val == needle { + return true + } + } + return false +} + +// StringToBool converts a string to a boolean ignoring errors +func StringToBool(s string) bool { + b, _ := strconv.ParseBool(strings.ToLower(strings.TrimSpace(s))) + return b +} + +// GetAsEqualsMap split key=value formatted strings into a key : value map +func GetAsEqualsMap(em []string) map[string]string { + m := make(map[string]string) + for _, v := range em { + kv := strings.SplitN(v, "=", 2) + m[kv[0]] = kv[1] + } + return m +} + +// GetAsEqualsMap format a key : value map into key=value strings +func GetAsStringList(em map[string]string) []string { + m := make([]string, 0, len(em)) + for k, v := range em { + m = append(m, fmt.Sprintf("%s=%s", k, v)) + } + return m +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e82c49b4..d492e3c5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -146,6 +146,7 @@ github.com/cespare/xxhash/v2 ## explicit # github.com/compose-spec/compose-go v1.9.0 ## explicit; go 1.18 +github.com/compose-spec/compose-go/cli github.com/compose-spec/compose-go/consts github.com/compose-spec/compose-go/dotenv github.com/compose-spec/compose-go/errdefs @@ -154,6 +155,7 @@ github.com/compose-spec/compose-go/loader github.com/compose-spec/compose-go/schema github.com/compose-spec/compose-go/template github.com/compose-spec/compose-go/types +github.com/compose-spec/compose-go/utils # github.com/containerd/console v1.0.3 ## explicit; go 1.13 github.com/containerd/console