mirror of https://github.com/docker/buildx.git
Merge pull request #2760 from tonistiigi/update-compose-v2.4.1
vendor: update compose to v2.4.1
This commit is contained in:
commit
704b2cc52d
2
go.mod
2
go.mod
|
@ -6,7 +6,7 @@ require (
|
|||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6
|
||||
github.com/compose-spec/compose-go/v2 v2.2.0
|
||||
github.com/compose-spec/compose-go/v2 v2.4.1
|
||||
github.com/containerd/console v1.0.4
|
||||
github.com/containerd/containerd v1.7.22
|
||||
github.com/containerd/continuity v0.4.4
|
||||
|
|
4
go.sum
4
go.sum
|
@ -83,8 +83,8 @@ github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnTh
|
|||
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
github.com/compose-spec/compose-go/v2 v2.2.0 h1:VsQosGhuO+H9wh5laiIiAe4TVd73kQ5NWwmNrdm0HRA=
|
||||
github.com/compose-spec/compose-go/v2 v2.2.0/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
|
||||
github.com/compose-spec/compose-go/v2 v2.4.1 h1:tEg6Qn/9LZnKg42fZlFmxN4lxSqnCvsiG5TXnxzvI4c=
|
||||
github.com/compose-spec/compose-go/v2 v2.4.1/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
|
||||
github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE=
|
||||
|
|
|
@ -403,22 +403,24 @@ func (o *ProjectOptions) GetWorkingDir() (string, error) {
|
|||
return os.Getwd()
|
||||
}
|
||||
|
||||
func (o *ProjectOptions) GetConfigFiles() ([]types.ConfigFile, error) {
|
||||
configPaths, err := o.getConfigPaths()
|
||||
// ReadConfigFiles reads ConfigFiles and populates the content field
|
||||
func (o *ProjectOptions) ReadConfigFiles(ctx context.Context, workingDir string, options *ProjectOptions) (*types.ConfigDetails, error) {
|
||||
config, err := loader.LoadConfigFiles(ctx, options.ConfigPaths, workingDir, options.loadOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configs := make([][]byte, len(config.ConfigFiles))
|
||||
|
||||
var configs []types.ConfigFile
|
||||
for _, f := range configPaths {
|
||||
for i, c := range config.ConfigFiles {
|
||||
var err error
|
||||
var b []byte
|
||||
if f == "-" {
|
||||
if c.Filename == "-" {
|
||||
b, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
f, err := filepath.Abs(f)
|
||||
f, err := filepath.Abs(c.Filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -427,27 +429,31 @@ func (o *ProjectOptions) GetConfigFiles() ([]types.ConfigFile, error) {
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
configs = append(configs, types.ConfigFile{
|
||||
Filename: f,
|
||||
Content: b,
|
||||
})
|
||||
configs[i] = b
|
||||
}
|
||||
return configs, err
|
||||
for i, c := range configs {
|
||||
config.ConfigFiles[i].Content = c
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// LoadProject loads compose file according to options and bind to types.Project go structs
|
||||
func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error) {
|
||||
configDetails, err := o.prepare()
|
||||
config, err := o.prepare(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := loader.LoadWithContext(ctx, configDetails, o.loadOptions...)
|
||||
project, err := loader.LoadWithContext(ctx, types.ConfigDetails{
|
||||
ConfigFiles: config.ConfigFiles,
|
||||
WorkingDir: config.WorkingDir,
|
||||
Environment: o.Environment,
|
||||
}, o.loadOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, config := range configDetails.ConfigFiles {
|
||||
for _, config := range config.ConfigFiles {
|
||||
project.ComposeFiles = append(project.ComposeFiles, config.Filename)
|
||||
}
|
||||
|
||||
|
@ -456,36 +462,31 @@ func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error
|
|||
|
||||
// LoadModel loads compose file according to options and returns a raw (yaml tree) model
|
||||
func (o *ProjectOptions) LoadModel(ctx context.Context) (map[string]any, error) {
|
||||
configDetails, err := o.prepare()
|
||||
configDetails, err := o.prepare(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return loader.LoadModelWithContext(ctx, configDetails, o.loadOptions...)
|
||||
return loader.LoadModelWithContext(ctx, *configDetails, o.loadOptions...)
|
||||
}
|
||||
|
||||
// prepare converts ProjectOptions into loader's types.ConfigDetails and configures default load options
|
||||
func (o *ProjectOptions) prepare() (types.ConfigDetails, error) {
|
||||
configs, err := o.GetConfigFiles()
|
||||
func (o *ProjectOptions) prepare(ctx context.Context) (*types.ConfigDetails, error) {
|
||||
defaultDir, err := o.GetWorkingDir()
|
||||
if err != nil {
|
||||
return types.ConfigDetails{}, err
|
||||
return &types.ConfigDetails{}, err
|
||||
}
|
||||
|
||||
workingDir, err := o.GetWorkingDir()
|
||||
configDetails, err := o.ReadConfigFiles(ctx, defaultDir, o)
|
||||
if err != nil {
|
||||
return types.ConfigDetails{}, err
|
||||
}
|
||||
|
||||
configDetails := types.ConfigDetails{
|
||||
ConfigFiles: configs,
|
||||
WorkingDir: workingDir,
|
||||
Environment: o.Environment,
|
||||
return configDetails, err
|
||||
}
|
||||
|
||||
o.loadOptions = append(o.loadOptions,
|
||||
withNamePrecedenceLoad(workingDir, o),
|
||||
withNamePrecedenceLoad(defaultDir, o),
|
||||
withConvertWindowsPaths(o),
|
||||
withListeners(o))
|
||||
|
||||
return configDetails, nil
|
||||
}
|
||||
|
||||
|
@ -502,8 +503,13 @@ func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(
|
|||
} else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" {
|
||||
opts.SetProjectName(nameFromEnv, true)
|
||||
} else {
|
||||
dirname := filepath.Base(absWorkingDir)
|
||||
symlink, err := filepath.EvalSymlinks(absWorkingDir)
|
||||
if err == nil && filepath.Base(symlink) != dirname {
|
||||
logrus.Warnf("project has been loaded without an explicit name from a symlink. Using name %q", dirname)
|
||||
}
|
||||
opts.SetProjectName(
|
||||
loader.NormalizeProjectName(filepath.Base(absWorkingDir)),
|
||||
loader.NormalizeProjectName(dirname),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
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 dotenv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var formats = map[string]Parser{}
|
||||
|
||||
type Parser func(r io.Reader, filename string, lookup func(key string) (string, bool)) (map[string]string, error)
|
||||
|
||||
func RegisterFormat(format string, p Parser) {
|
||||
formats[format] = p
|
||||
}
|
||||
|
||||
func ParseWithFormat(r io.Reader, filename string, resolve LookupFn, format string) (map[string]string, error) {
|
||||
parser, ok := formats[format]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported env_file format %q", format)
|
||||
}
|
||||
return parser(r, filename, resolve)
|
||||
}
|
|
@ -86,7 +86,7 @@ func ReadWithLookup(lookupFn LookupFn, filenames ...string) (map[string]string,
|
|||
envMap := make(map[string]string)
|
||||
|
||||
for _, filename := range filenames {
|
||||
individualEnvMap, individualErr := readFile(filename, lookupFn)
|
||||
individualEnvMap, individualErr := ReadFile(filename, lookupFn)
|
||||
|
||||
if individualErr != nil {
|
||||
return envMap, individualErr
|
||||
|
@ -129,7 +129,7 @@ func filenamesOrDefault(filenames []string) []string {
|
|||
}
|
||||
|
||||
func loadFile(filename string, overload bool) error {
|
||||
envMap, err := readFile(filename, nil)
|
||||
envMap, err := ReadFile(filename, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ func loadFile(filename string, overload bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func readFile(filename string, lookupFn LookupFn) (map[string]string, error) {
|
||||
func ReadFile(filename string, lookupFn LookupFn) (map[string]string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -119,7 +119,7 @@ loop:
|
|||
offset = i + 1
|
||||
inherited = rune == '\n'
|
||||
break loop
|
||||
case '_', '.', '[', ']':
|
||||
case '_', '.', '-', '[', ']':
|
||||
default:
|
||||
// variable name should match [A-Za-z0-9_.-]
|
||||
if unicode.IsLetter(rune) || unicode.IsNumber(rune) {
|
||||
|
@ -136,6 +136,10 @@ loop:
|
|||
return "", "", inherited, errors.New("zero length string")
|
||||
}
|
||||
|
||||
if inherited && strings.IndexByte(key, ' ') == -1 {
|
||||
p.line++
|
||||
}
|
||||
|
||||
// trim whitespace
|
||||
key = strings.TrimRightFunc(key, unicode.IsSpace)
|
||||
cutset := strings.TrimLeftFunc(src[offset:], isSpace)
|
||||
|
|
|
@ -95,7 +95,7 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolu
|
|||
if isBindOption(option) {
|
||||
setBindOption(volume, option)
|
||||
}
|
||||
// ignore unknown options
|
||||
// ignore unknown options FIXME why not report an error here?
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/consts"
|
||||
"github.com/compose-spec/compose-go/v2/errdefs"
|
||||
interp "github.com/compose-spec/compose-go/v2/interpolation"
|
||||
"github.com/compose-spec/compose-go/v2/override"
|
||||
"github.com/compose-spec/compose-go/v2/paths"
|
||||
|
@ -139,9 +140,9 @@ func (l localResourceLoader) abs(p string) string {
|
|||
return filepath.Join(l.WorkingDir, p)
|
||||
}
|
||||
|
||||
func (l localResourceLoader) Accept(p string) bool {
|
||||
_, err := os.Stat(l.abs(p))
|
||||
return err == nil
|
||||
func (l localResourceLoader) Accept(_ string) bool {
|
||||
// LocalResourceLoader is the last loader tested so it always should accept the config and try to get the content.
|
||||
return true
|
||||
}
|
||||
|
||||
func (l localResourceLoader) Load(_ context.Context, p string) (string, error) {
|
||||
|
@ -300,6 +301,51 @@ func parseYAML(decoder *yaml.Decoder) (map[string]interface{}, PostProcessor, er
|
|||
return converted.(map[string]interface{}), &processor, nil
|
||||
}
|
||||
|
||||
// LoadConfigFiles ingests config files with ResourceLoader and returns config details with paths to local copies
|
||||
func LoadConfigFiles(ctx context.Context, configFiles []string, workingDir string, options ...func(*Options)) (*types.ConfigDetails, error) {
|
||||
if len(configFiles) < 1 {
|
||||
return &types.ConfigDetails{}, fmt.Errorf("no configuration file provided: %w", errdefs.ErrNotFound)
|
||||
}
|
||||
|
||||
opts := &Options{}
|
||||
config := &types.ConfigDetails{
|
||||
ConfigFiles: make([]types.ConfigFile, len(configFiles)),
|
||||
}
|
||||
|
||||
for _, op := range options {
|
||||
op(opts)
|
||||
}
|
||||
opts.ResourceLoaders = append(opts.ResourceLoaders, localResourceLoader{})
|
||||
|
||||
for i, p := range configFiles {
|
||||
for _, loader := range opts.ResourceLoaders {
|
||||
_, isLocalResourceLoader := loader.(localResourceLoader)
|
||||
if !loader.Accept(p) {
|
||||
continue
|
||||
}
|
||||
local, err := loader.Load(ctx, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.WorkingDir == "" && !isLocalResourceLoader {
|
||||
config.WorkingDir = filepath.Dir(local)
|
||||
}
|
||||
abs, err := filepath.Abs(local)
|
||||
if err != nil {
|
||||
abs = local
|
||||
}
|
||||
config.ConfigFiles[i] = types.ConfigFile{
|
||||
Filename: abs,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if config.WorkingDir == "" {
|
||||
config.WorkingDir = workingDir
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Load reads a ConfigDetails and returns a fully loaded configuration.
|
||||
// Deprecated: use LoadWithContext.
|
||||
func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
|
||||
|
@ -470,6 +516,8 @@ func loadYamlFile(ctx context.Context, file types.ConfigFile, opts *Options, wor
|
|||
return err
|
||||
}
|
||||
|
||||
dict = OmitEmpty(dict)
|
||||
|
||||
// Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override
|
||||
dict, err = override.EnforceUnicity(dict)
|
||||
return err
|
||||
|
@ -675,6 +723,7 @@ func NormalizeProjectName(s string) string {
|
|||
|
||||
var userDefinedKeys = []tree.Path{
|
||||
"services",
|
||||
"services.*.depends_on",
|
||||
"volumes",
|
||||
"networks",
|
||||
"secrets",
|
||||
|
@ -687,7 +736,7 @@ func processExtensions(dict map[string]any, p tree.Path, extensions map[string]a
|
|||
for key, value := range dict {
|
||||
skip := false
|
||||
for _, uk := range userDefinedKeys {
|
||||
if uk.Matches(p) {
|
||||
if p.Matches(uk) {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
|
@ -770,14 +819,14 @@ func secretConfigDecoderHook(from, to reflect.Type, data interface{}) (interface
|
|||
// Check if the input is a map and we're decoding into a SecretConfig
|
||||
if from.Kind() == reflect.Map && to == reflect.TypeOf(types.SecretConfig{}) {
|
||||
if v, ok := data.(map[string]interface{}); ok {
|
||||
if ext, ok := v["#extensions"].(map[string]interface{}); ok {
|
||||
if ext, ok := v[consts.Extensions].(map[string]interface{}); ok {
|
||||
if val, ok := ext[types.SecretConfigXValue].(string); ok {
|
||||
// Return a map with the Content field populated
|
||||
v["Content"] = val
|
||||
delete(ext, types.SecretConfigXValue)
|
||||
|
||||
if len(ext) == 0 {
|
||||
delete(v, "#extensions")
|
||||
delete(v, consts.Extensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package loader
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -102,6 +103,17 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if v, ok := service["volumes"]; ok {
|
||||
volumes := v.([]any)
|
||||
for i, volume := range volumes {
|
||||
vol := volume.(map[string]any)
|
||||
target := vol["target"].(string)
|
||||
vol["target"] = path.Clean(target)
|
||||
volumes[i] = vol
|
||||
}
|
||||
service["volumes"] = volumes
|
||||
}
|
||||
|
||||
if n, ok := service["volumes_from"]; ok {
|
||||
volumesFrom := n.([]any)
|
||||
for _, v := range volumesFrom {
|
||||
|
@ -123,9 +135,9 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
|
|||
}
|
||||
services[name] = service
|
||||
}
|
||||
|
||||
dict["services"] = services
|
||||
}
|
||||
|
||||
setNameFromKey(dict)
|
||||
|
||||
return dict, nil
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
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 loader
|
||||
|
||||
import "github.com/compose-spec/compose-go/v2/tree"
|
||||
|
||||
var omitempty = []tree.Path{
|
||||
"services.*.dns"}
|
||||
|
||||
// OmitEmpty removes empty attributes which are irrelevant when unset
|
||||
func OmitEmpty(yaml map[string]any) map[string]any {
|
||||
cleaned := omitEmpty(yaml, tree.NewPath())
|
||||
return cleaned.(map[string]any)
|
||||
}
|
||||
|
||||
func omitEmpty(data any, p tree.Path) any {
|
||||
switch v := data.(type) {
|
||||
case map[string]any:
|
||||
for k, e := range v {
|
||||
if isEmpty(e) && mustOmit(p) {
|
||||
delete(v, k)
|
||||
continue
|
||||
}
|
||||
|
||||
v[k] = omitEmpty(e, p.Next(k))
|
||||
}
|
||||
return v
|
||||
case []any:
|
||||
var c []any
|
||||
for _, e := range v {
|
||||
if isEmpty(e) && mustOmit(p) {
|
||||
continue
|
||||
}
|
||||
|
||||
c = append(c, omitEmpty(e, p.Next("[]")))
|
||||
}
|
||||
return c
|
||||
default:
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
func mustOmit(p tree.Path) bool {
|
||||
for _, pattern := range omitempty {
|
||||
if p.Matches(pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEmpty(e any) bool {
|
||||
if e == nil {
|
||||
return true
|
||||
}
|
||||
if v, ok := e.(string); ok && v == "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -26,13 +26,15 @@ import (
|
|||
)
|
||||
|
||||
type ResetProcessor struct {
|
||||
target interface{}
|
||||
paths []tree.Path
|
||||
target interface{}
|
||||
paths []tree.Path
|
||||
visitedNodes map[*yaml.Node]string
|
||||
}
|
||||
|
||||
// UnmarshalYAML implement yaml.Unmarshaler
|
||||
func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
|
||||
resolved, err := p.resolveReset(value, tree.NewPath())
|
||||
p.visitedNodes = nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -41,10 +43,28 @@ func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
|
|||
|
||||
// resolveReset detects `!reset` tag being set on yaml nodes and record position in the yaml tree
|
||||
func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.Node, error) {
|
||||
pathStr := path.String()
|
||||
// If the path contains "<<", removing the "<<" element and merging the path
|
||||
if strings.Contains(path.String(), ".<<") {
|
||||
path = tree.NewPath(strings.Replace(path.String(), ".<<", "", 1))
|
||||
if strings.Contains(pathStr, ".<<") {
|
||||
path = tree.NewPath(strings.Replace(pathStr, ".<<", "", 1))
|
||||
}
|
||||
|
||||
// Check for cycle
|
||||
if p.visitedNodes == nil {
|
||||
p.visitedNodes = make(map[*yaml.Node]string)
|
||||
}
|
||||
|
||||
// Check for cycle by seeing if the node has already been visited at this path
|
||||
if previousPath, found := p.visitedNodes[node]; found {
|
||||
// If the current node has been visited, we have a cycle if the previous path is a prefix
|
||||
if strings.HasPrefix(pathStr, previousPath) {
|
||||
return nil, fmt.Errorf("cycle detected at path: %s", pathStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the current node as visited
|
||||
p.visitedNodes[node] = pathStr
|
||||
|
||||
// If the node is an alias, We need to process the alias field in order to consider the !override and !reset tags
|
||||
if node.Kind == yaml.AliasNode {
|
||||
return p.resolveReset(node.Alias, path)
|
||||
|
|
|
@ -47,7 +47,7 @@ func init() {
|
|||
mergeSpecials["services.*.build"] = mergeBuild
|
||||
mergeSpecials["services.*.build.args"] = mergeToSequence
|
||||
mergeSpecials["services.*.build.additional_contexts"] = mergeToSequence
|
||||
mergeSpecials["services.*.build.extra_hosts"] = mergeToSequence
|
||||
mergeSpecials["services.*.build.extra_hosts"] = mergeExtraHosts
|
||||
mergeSpecials["services.*.build.labels"] = mergeToSequence
|
||||
mergeSpecials["services.*.command"] = override
|
||||
mergeSpecials["services.*.depends_on"] = mergeDependsOn
|
||||
|
@ -58,7 +58,7 @@ func init() {
|
|||
mergeSpecials["services.*.entrypoint"] = override
|
||||
mergeSpecials["services.*.env_file"] = mergeToSequence
|
||||
mergeSpecials["services.*.environment"] = mergeToSequence
|
||||
mergeSpecials["services.*.extra_hosts"] = mergeToSequence
|
||||
mergeSpecials["services.*.extra_hosts"] = mergeExtraHosts
|
||||
mergeSpecials["services.*.healthcheck.test"] = override
|
||||
mergeSpecials["services.*.labels"] = mergeToSequence
|
||||
mergeSpecials["services.*.logging"] = mergeLogging
|
||||
|
@ -163,6 +163,22 @@ func mergeNetworks(c any, o any, path tree.Path) (any, error) {
|
|||
return mergeMappings(right, left, path)
|
||||
}
|
||||
|
||||
func mergeExtraHosts(c any, o any, _ tree.Path) (any, error) {
|
||||
right := convertIntoSequence(c)
|
||||
left := convertIntoSequence(o)
|
||||
// Rewrite content of left slice to remove duplicate elements
|
||||
i := 0
|
||||
for _, v := range left {
|
||||
if !slices.Contains(right, v) {
|
||||
left[i] = v
|
||||
i++
|
||||
}
|
||||
}
|
||||
// keep only not duplicated elements from left slice
|
||||
left = left[:i]
|
||||
return append(right, left...), nil
|
||||
}
|
||||
|
||||
func mergeToSequence(c any, o any, _ tree.Path) (any, error) {
|
||||
right := convertIntoSequence(c)
|
||||
left := convertIntoSequence(o)
|
||||
|
@ -172,15 +188,21 @@ func mergeToSequence(c any, o any, _ tree.Path) (any, error) {
|
|||
func convertIntoSequence(value any) []any {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
seq := make([]any, len(v))
|
||||
i := 0
|
||||
for k, v := range v {
|
||||
if v == nil {
|
||||
seq[i] = k
|
||||
var seq []any
|
||||
for k, val := range v {
|
||||
if val == nil {
|
||||
seq = append(seq, k)
|
||||
} else {
|
||||
seq[i] = fmt.Sprintf("%s=%v", k, v)
|
||||
switch vl := val.(type) {
|
||||
// if val is an array we need to add the key with each value one by one
|
||||
case []any:
|
||||
for _, vlv := range vl {
|
||||
seq = append(seq, fmt.Sprintf("%s=%v", k, vlv))
|
||||
}
|
||||
default:
|
||||
seq = append(seq, fmt.Sprintf("%s=%v", k, val))
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
slices.SortFunc(seq, func(a, b any) int {
|
||||
return cmp.Compare(a.(string), b.(string))
|
||||
|
|
|
@ -267,6 +267,7 @@
|
|||
},
|
||||
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"extra_hosts": {"$ref": "#/definitions/extra_hosts"},
|
||||
"gpus": {"$ref": "#/definitions/gpus"},
|
||||
"group_add": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -370,6 +371,8 @@
|
|||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"post_start": {"type": "array", "items": {"$ref": "#/definitions/service_hook"}},
|
||||
"pre_stop": {"type": "array", "items": {"$ref": "#/definitions/service_hook"}},
|
||||
"privileged": {"type": ["boolean", "string"]},
|
||||
"profiles": {"$ref": "#/definitions/list_of_strings"},
|
||||
"pull_policy": {"type": "string", "enum": [
|
||||
|
@ -416,6 +419,7 @@
|
|||
"properties": {
|
||||
"propagation": {"type": "string"},
|
||||
"create_host_path": {"type": ["boolean", "string"]},
|
||||
"recursive": {"type": "string", "enum": ["enabled", "disabled", "writable", "readonly"]},
|
||||
"selinux": {"type": "string", "enum": ["z", "Z"]}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
@ -500,11 +504,11 @@
|
|||
},
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {"^x-": {}}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {"^x-": {}}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {"^x-": {}}
|
||||
},
|
||||
"deployment": {
|
||||
"id": "#/definitions/deployment",
|
||||
|
@ -632,6 +636,26 @@
|
|||
"devices": {
|
||||
"id": "#/definitions/devices",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capabilities": {"$ref": "#/definitions/list_of_strings"},
|
||||
"count": {"type": ["string", "integer"]},
|
||||
"device_ids": {"$ref": "#/definitions/list_of_strings"},
|
||||
"driver":{"type": "string"},
|
||||
"options":{"$ref": "#/definitions/list_or_dict"}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {"^x-": {}},
|
||||
"required": [
|
||||
"capabilities"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"gpus": {
|
||||
"id": "#/definitions/gpus",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -813,6 +837,20 @@
|
|||
]
|
||||
},
|
||||
|
||||
"service_hook": {
|
||||
"id": "#/definitions/service_hook",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {"$ref": "#/definitions/command"},
|
||||
"user": {"type": "string"},
|
||||
"privileged": {"type": ["boolean", "string"]},
|
||||
"working_dir": {"type": "string"},
|
||||
"environment": {"$ref": "#/definitions/list_or_dict"}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {"^x-": {}}
|
||||
},
|
||||
|
||||
"env_file": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
|
@ -828,6 +866,9 @@
|
|||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": ["boolean", "string"],
|
||||
"default": true
|
||||
|
@ -878,7 +919,8 @@
|
|||
"patternProperties": {
|
||||
".+": {
|
||||
"type": ["string", "array"]
|
||||
}
|
||||
},
|
||||
"uniqueItems": false
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
|
|
@ -33,6 +33,7 @@ func init() {
|
|||
transformers["services.*.extends"] = transformExtends
|
||||
transformers["services.*.networks"] = transformServiceNetworks
|
||||
transformers["services.*.volumes.*"] = transformVolumeMount
|
||||
transformers["services.*.dns"] = transformStringOrList
|
||||
transformers["services.*.devices.*"] = transformDeviceMapping
|
||||
transformers["services.*.secrets.*"] = transformFileMount
|
||||
transformers["services.*.configs.*"] = transformFileMount
|
||||
|
@ -48,6 +49,15 @@ func init() {
|
|||
transformers["include.*"] = transformInclude
|
||||
}
|
||||
|
||||
func transformStringOrList(data any, _ tree.Path, _ bool) (any, error) {
|
||||
switch t := data.(type) {
|
||||
case string:
|
||||
return []any{t}, nil
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Canonical transforms a compose model into canonical syntax
|
||||
func Canonical(yaml map[string]any, ignoreParseError bool) (map[string]any, error) {
|
||||
canonical, err := transform(yaml, tree.NewPath(), ignoreParseError)
|
||||
|
|
|
@ -26,6 +26,8 @@ func init() {
|
|||
defaultValues["services.*.build"] = defaultBuildContext
|
||||
defaultValues["services.*.secrets.*"] = defaultSecretMount
|
||||
defaultValues["services.*.ports.*"] = portDefaults
|
||||
defaultValues["services.*.deploy.resources.reservations.devices.*"] = deviceRequestDefaults
|
||||
defaultValues["services.*.gpus.*"] = deviceRequestDefaults
|
||||
}
|
||||
|
||||
// SetDefaultValues transforms a compose model to set default values to missing attributes
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
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 transform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/tree"
|
||||
)
|
||||
|
||||
func deviceRequestDefaults(data any, p tree.Path, _ bool) (any, error) {
|
||||
v, ok := data.(map[string]any)
|
||||
if !ok {
|
||||
return data, fmt.Errorf("%s: invalid type %T for device request", p, v)
|
||||
}
|
||||
_, hasCount := v["count"]
|
||||
_, hasIds := v["device_ids"]
|
||||
if !hasCount && !hasIds {
|
||||
v["count"] = "all"
|
||||
}
|
||||
return v, nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
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 types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type NanoCPUs float32
|
||||
|
||||
func (n *NanoCPUs) DecodeMapstructure(a any) error {
|
||||
switch v := a.(type) {
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*n = NanoCPUs(f)
|
||||
case int:
|
||||
*n = NanoCPUs(v)
|
||||
case float32:
|
||||
*n = NanoCPUs(v)
|
||||
case float64:
|
||||
*n = NanoCPUs(v)
|
||||
default:
|
||||
return fmt.Errorf("unexpected value type %T for cpus", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NanoCPUs) Value() float32 {
|
||||
return float32(*n)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -27,6 +27,7 @@ type DeviceRequest struct {
|
|||
Driver string `yaml:"driver,omitempty" json:"driver,omitempty"`
|
||||
Count DeviceCount `yaml:"count,omitempty" json:"count,omitempty"`
|
||||
IDs []string `yaml:"device_ids,omitempty" json:"device_ids,omitempty"`
|
||||
Options Mapping `yaml:"options,omitempty" json:"options,omitempty"`
|
||||
}
|
||||
|
||||
type DeviceCount int64
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
type EnvFile struct {
|
||||
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
||||
Required bool `yaml:"required" json:"required"`
|
||||
Format string `yaml:"format,omitempty" json:"format,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalYAML makes EnvFile implement yaml.Marshaler
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
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 types
|
||||
|
||||
// ServiceHook is a command to exec inside container by some lifecycle events
|
||||
type ServiceHook struct {
|
||||
Command ShellCommand `yaml:"command,omitempty" json:"command"`
|
||||
User string `yaml:"user,omitempty" json:"user,omitempty"`
|
||||
Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"`
|
||||
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
|
||||
Environment MappingWithEquals `yaml:"environment,omitempty" json:"environment,omitempty"`
|
||||
|
||||
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
||||
}
|
|
@ -616,22 +616,11 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project
|
|||
}
|
||||
|
||||
for _, envFile := range service.EnvFiles {
|
||||
if _, err := os.Stat(envFile.Path); os.IsNotExist(err) {
|
||||
if envFile.Required {
|
||||
return nil, fmt.Errorf("env file %s not found: %w", envFile.Path, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
b, err := os.ReadFile(envFile.Path)
|
||||
vars, err := loadEnvFile(envFile, resolve)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load %s: %w", envFile.Path, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %s: %w", envFile.Path, err)
|
||||
}
|
||||
environment.OverrideBy(Mapping(fileVars).ToMappingWithEquals())
|
||||
environment.OverrideBy(vars.ToMappingWithEquals())
|
||||
}
|
||||
|
||||
service.Environment = environment.OverrideBy(service.Environment)
|
||||
|
@ -644,6 +633,31 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project
|
|||
return newProject, nil
|
||||
}
|
||||
|
||||
func loadEnvFile(envFile EnvFile, resolve dotenv.LookupFn) (Mapping, error) {
|
||||
if _, err := os.Stat(envFile.Path); os.IsNotExist(err) {
|
||||
if envFile.Required {
|
||||
return nil, fmt.Errorf("env file %s not found: %w", envFile.Path, err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
file, err := os.Open(envFile.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close() //nolint:errcheck
|
||||
|
||||
var fileVars map[string]string
|
||||
if envFile.Format != "" {
|
||||
fileVars, err = dotenv.ParseWithFormat(file, envFile.Path, resolve, envFile.Format)
|
||||
} else {
|
||||
fileVars, err = dotenv.ParseWithLookup(file, resolve)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileVars, nil
|
||||
}
|
||||
|
||||
func (p *Project) deepCopy() *Project {
|
||||
if p == nil {
|
||||
return nil
|
||||
|
|
|
@ -20,7 +20,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/go-connections/nat"
|
||||
|
@ -82,6 +81,7 @@ type ServiceConfig struct {
|
|||
ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"`
|
||||
ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
|
||||
GroupAdd []string `yaml:"group_add,omitempty" json:"group_add,omitempty"`
|
||||
Gpus []DeviceRequest `yaml:"gpus,omitempty" json:"gpus,omitempty"`
|
||||
Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
|
||||
HealthCheck *HealthCheckConfig `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"`
|
||||
Image string `yaml:"image,omitempty" json:"image,omitempty"`
|
||||
|
@ -132,6 +132,8 @@ type ServiceConfig struct {
|
|||
Volumes []ServiceVolumeConfig `yaml:"volumes,omitempty" json:"volumes,omitempty"`
|
||||
VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"`
|
||||
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
|
||||
PostStart []ServiceHook `yaml:"post_start,omitempty" json:"post_start,omitempty"`
|
||||
PreStop []ServiceHook `yaml:"pre_stop,omitempty" json:"pre_stop,omitempty"`
|
||||
|
||||
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
||||
}
|
||||
|
@ -388,30 +390,6 @@ type Resource struct {
|
|||
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
||||
}
|
||||
|
||||
type NanoCPUs float32
|
||||
|
||||
func (n *NanoCPUs) DecodeMapstructure(a any) error {
|
||||
switch v := a.(type) {
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*n = NanoCPUs(f)
|
||||
case float32:
|
||||
*n = NanoCPUs(v)
|
||||
case float64:
|
||||
*n = NanoCPUs(v)
|
||||
default:
|
||||
return fmt.Errorf("unexpected value type %T for cpus", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NanoCPUs) Value() float32 {
|
||||
return float32(*n)
|
||||
}
|
||||
|
||||
// GenericResource represents a "user defined" resource which can
|
||||
// only be an integer (e.g: SSD=3) for a service
|
||||
type GenericResource struct {
|
||||
|
@ -552,9 +530,6 @@ func (s ServiceVolumeConfig) String() string {
|
|||
if s.Volume != nil && s.Volume.NoCopy {
|
||||
options = append(options, "nocopy")
|
||||
}
|
||||
if s.Volume != nil && s.Volume.Subpath != "" {
|
||||
options = append(options, s.Volume.Subpath)
|
||||
}
|
||||
return fmt.Sprintf("%s:%s:%s", s.Source, s.Target, strings.Join(options, ","))
|
||||
}
|
||||
|
||||
|
@ -581,6 +556,7 @@ type ServiceVolumeBind struct {
|
|||
SELinux string `yaml:"selinux,omitempty" json:"selinux,omitempty"`
|
||||
Propagation string `yaml:"propagation,omitempty" json:"propagation,omitempty"`
|
||||
CreateHostPath bool `yaml:"create_host_path,omitempty" json:"create_host_path,omitempty"`
|
||||
Recursive string `yaml:"recursive,omitempty" json:"recursive,omitempty"`
|
||||
|
||||
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ var checks = map[tree.Path]checkerFunc{
|
|||
"configs.*": checkFileObject("file", "environment", "content"),
|
||||
"secrets.*": checkFileObject("file", "environment"),
|
||||
"services.*.develop.watch.*.path": checkPath,
|
||||
"services.*.deploy.resources.reservations.devices.*": checkDeviceRequest,
|
||||
"services.*.gpus.*": checkDeviceRequest,
|
||||
}
|
||||
|
||||
func Validate(dict map[string]any) error {
|
||||
|
@ -94,3 +96,13 @@ func checkPath(value any, p tree.Path) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDeviceRequest(value any, p tree.Path) error {
|
||||
v := value.(map[string]any)
|
||||
_, hasCount := v["count"]
|
||||
_, hasIds := v["device_ids"]
|
||||
if hasCount && hasIds {
|
||||
return fmt.Errorf(`%s: "count" and "device_ids" attributes are exclusive`, p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ github.com/cenkalti/backoff/v4
|
|||
# github.com/cespare/xxhash/v2 v2.3.0
|
||||
## explicit; go 1.11
|
||||
github.com/cespare/xxhash/v2
|
||||
# github.com/compose-spec/compose-go/v2 v2.2.0
|
||||
# github.com/compose-spec/compose-go/v2 v2.4.1
|
||||
## explicit; go 1.21
|
||||
github.com/compose-spec/compose-go/v2/cli
|
||||
github.com/compose-spec/compose-go/v2/consts
|
||||
|
|
Loading…
Reference in New Issue