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/Masterminds/semver/v3 v3.2.1
|
||||||
github.com/Microsoft/go-winio v0.6.2
|
github.com/Microsoft/go-winio v0.6.2
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.26.6
|
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/console v1.0.4
|
||||||
github.com/containerd/containerd v1.7.22
|
github.com/containerd/containerd v1.7.22
|
||||||
github.com/containerd/continuity v0.4.4
|
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/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 h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
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.4.1 h1:tEg6Qn/9LZnKg42fZlFmxN4lxSqnCvsiG5TXnxzvI4c=
|
||||||
github.com/compose-spec/compose-go/v2 v2.2.0/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
|
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 v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||||
github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
|
github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
|
||||||
github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE=
|
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()
|
return os.Getwd()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ProjectOptions) GetConfigFiles() ([]types.ConfigFile, error) {
|
// ReadConfigFiles reads ConfigFiles and populates the content field
|
||||||
configPaths, err := o.getConfigPaths()
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
configs := make([][]byte, len(config.ConfigFiles))
|
||||||
|
|
||||||
var configs []types.ConfigFile
|
for i, c := range config.ConfigFiles {
|
||||||
for _, f := range configPaths {
|
var err error
|
||||||
var b []byte
|
var b []byte
|
||||||
if f == "-" {
|
if c.Filename == "-" {
|
||||||
b, err = io.ReadAll(os.Stdin)
|
b, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
f, err := filepath.Abs(f)
|
f, err := filepath.Abs(c.Filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -427,27 +429,31 @@ func (o *ProjectOptions) GetConfigFiles() ([]types.ConfigFile, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
configs = append(configs, types.ConfigFile{
|
configs[i] = b
|
||||||
Filename: f,
|
|
||||||
Content: 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
|
// LoadProject loads compose file according to options and bind to types.Project go structs
|
||||||
func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error) {
|
func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error) {
|
||||||
configDetails, err := o.prepare()
|
config, err := o.prepare(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, config := range configDetails.ConfigFiles {
|
for _, config := range config.ConfigFiles {
|
||||||
project.ComposeFiles = append(project.ComposeFiles, config.Filename)
|
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
|
// 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) {
|
func (o *ProjectOptions) LoadModel(ctx context.Context) (map[string]any, error) {
|
||||||
configDetails, err := o.prepare()
|
configDetails, err := o.prepare(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// prepare converts ProjectOptions into loader's types.ConfigDetails and configures default load options
|
||||||
func (o *ProjectOptions) prepare() (types.ConfigDetails, error) {
|
func (o *ProjectOptions) prepare(ctx context.Context) (*types.ConfigDetails, error) {
|
||||||
configs, err := o.GetConfigFiles()
|
defaultDir, err := o.GetWorkingDir()
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
return types.ConfigDetails{}, err
|
return configDetails, err
|
||||||
}
|
|
||||||
|
|
||||||
configDetails := types.ConfigDetails{
|
|
||||||
ConfigFiles: configs,
|
|
||||||
WorkingDir: workingDir,
|
|
||||||
Environment: o.Environment,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
o.loadOptions = append(o.loadOptions,
|
o.loadOptions = append(o.loadOptions,
|
||||||
withNamePrecedenceLoad(workingDir, o),
|
withNamePrecedenceLoad(defaultDir, o),
|
||||||
withConvertWindowsPaths(o),
|
withConvertWindowsPaths(o),
|
||||||
withListeners(o))
|
withListeners(o))
|
||||||
|
|
||||||
return configDetails, nil
|
return configDetails, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -502,8 +503,13 @@ func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(
|
||||||
} else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" {
|
} else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" {
|
||||||
opts.SetProjectName(nameFromEnv, true)
|
opts.SetProjectName(nameFromEnv, true)
|
||||||
} else {
|
} 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(
|
opts.SetProjectName(
|
||||||
loader.NormalizeProjectName(filepath.Base(absWorkingDir)),
|
loader.NormalizeProjectName(dirname),
|
||||||
false,
|
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)
|
envMap := make(map[string]string)
|
||||||
|
|
||||||
for _, filename := range filenames {
|
for _, filename := range filenames {
|
||||||
individualEnvMap, individualErr := readFile(filename, lookupFn)
|
individualEnvMap, individualErr := ReadFile(filename, lookupFn)
|
||||||
|
|
||||||
if individualErr != nil {
|
if individualErr != nil {
|
||||||
return envMap, individualErr
|
return envMap, individualErr
|
||||||
|
@ -129,7 +129,7 @@ func filenamesOrDefault(filenames []string) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadFile(filename string, overload bool) error {
|
func loadFile(filename string, overload bool) error {
|
||||||
envMap, err := readFile(filename, nil)
|
envMap, err := ReadFile(filename, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -150,7 +150,7 @@ func loadFile(filename string, overload bool) error {
|
||||||
return nil
|
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)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -119,7 +119,7 @@ loop:
|
||||||
offset = i + 1
|
offset = i + 1
|
||||||
inherited = rune == '\n'
|
inherited = rune == '\n'
|
||||||
break loop
|
break loop
|
||||||
case '_', '.', '[', ']':
|
case '_', '.', '-', '[', ']':
|
||||||
default:
|
default:
|
||||||
// variable name should match [A-Za-z0-9_.-]
|
// variable name should match [A-Za-z0-9_.-]
|
||||||
if unicode.IsLetter(rune) || unicode.IsNumber(rune) {
|
if unicode.IsLetter(rune) || unicode.IsNumber(rune) {
|
||||||
|
@ -136,6 +136,10 @@ loop:
|
||||||
return "", "", inherited, errors.New("zero length string")
|
return "", "", inherited, errors.New("zero length string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inherited && strings.IndexByte(key, ' ') == -1 {
|
||||||
|
p.line++
|
||||||
|
}
|
||||||
|
|
||||||
// trim whitespace
|
// trim whitespace
|
||||||
key = strings.TrimRightFunc(key, unicode.IsSpace)
|
key = strings.TrimRightFunc(key, unicode.IsSpace)
|
||||||
cutset := strings.TrimLeftFunc(src[offset:], isSpace)
|
cutset := strings.TrimLeftFunc(src[offset:], isSpace)
|
||||||
|
|
|
@ -95,7 +95,7 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolu
|
||||||
if isBindOption(option) {
|
if isBindOption(option) {
|
||||||
setBindOption(volume, option)
|
setBindOption(volume, option)
|
||||||
}
|
}
|
||||||
// ignore unknown options
|
// ignore unknown options FIXME why not report an error here?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/consts"
|
"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"
|
interp "github.com/compose-spec/compose-go/v2/interpolation"
|
||||||
"github.com/compose-spec/compose-go/v2/override"
|
"github.com/compose-spec/compose-go/v2/override"
|
||||||
"github.com/compose-spec/compose-go/v2/paths"
|
"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)
|
return filepath.Join(l.WorkingDir, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l localResourceLoader) Accept(p string) bool {
|
func (l localResourceLoader) Accept(_ string) bool {
|
||||||
_, err := os.Stat(l.abs(p))
|
// LocalResourceLoader is the last loader tested so it always should accept the config and try to get the content.
|
||||||
return err == nil
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l localResourceLoader) Load(_ context.Context, p string) (string, error) {
|
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
|
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.
|
// Load reads a ConfigDetails and returns a fully loaded configuration.
|
||||||
// Deprecated: use LoadWithContext.
|
// Deprecated: use LoadWithContext.
|
||||||
func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dict = OmitEmpty(dict)
|
||||||
|
|
||||||
// Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override
|
// Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override
|
||||||
dict, err = override.EnforceUnicity(dict)
|
dict, err = override.EnforceUnicity(dict)
|
||||||
return err
|
return err
|
||||||
|
@ -675,6 +723,7 @@ func NormalizeProjectName(s string) string {
|
||||||
|
|
||||||
var userDefinedKeys = []tree.Path{
|
var userDefinedKeys = []tree.Path{
|
||||||
"services",
|
"services",
|
||||||
|
"services.*.depends_on",
|
||||||
"volumes",
|
"volumes",
|
||||||
"networks",
|
"networks",
|
||||||
"secrets",
|
"secrets",
|
||||||
|
@ -687,7 +736,7 @@ func processExtensions(dict map[string]any, p tree.Path, extensions map[string]a
|
||||||
for key, value := range dict {
|
for key, value := range dict {
|
||||||
skip := false
|
skip := false
|
||||||
for _, uk := range userDefinedKeys {
|
for _, uk := range userDefinedKeys {
|
||||||
if uk.Matches(p) {
|
if p.Matches(uk) {
|
||||||
skip = true
|
skip = true
|
||||||
break
|
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
|
// 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 from.Kind() == reflect.Map && to == reflect.TypeOf(types.SecretConfig{}) {
|
||||||
if v, ok := data.(map[string]interface{}); ok {
|
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 {
|
if val, ok := ext[types.SecretConfigXValue].(string); ok {
|
||||||
// Return a map with the Content field populated
|
// Return a map with the Content field populated
|
||||||
v["Content"] = val
|
v["Content"] = val
|
||||||
delete(ext, types.SecretConfigXValue)
|
delete(ext, types.SecretConfigXValue)
|
||||||
|
|
||||||
if len(ext) == 0 {
|
if len(ext) == 0 {
|
||||||
delete(v, "#extensions")
|
delete(v, consts.Extensions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package loader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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 {
|
if n, ok := service["volumes_from"]; ok {
|
||||||
volumesFrom := n.([]any)
|
volumesFrom := n.([]any)
|
||||||
for _, v := range volumesFrom {
|
for _, v := range volumesFrom {
|
||||||
|
@ -123,9 +135,9 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
|
||||||
}
|
}
|
||||||
services[name] = service
|
services[name] = service
|
||||||
}
|
}
|
||||||
|
|
||||||
dict["services"] = services
|
dict["services"] = services
|
||||||
}
|
}
|
||||||
|
|
||||||
setNameFromKey(dict)
|
setNameFromKey(dict)
|
||||||
|
|
||||||
return dict, nil
|
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 {
|
type ResetProcessor struct {
|
||||||
target interface{}
|
target interface{}
|
||||||
paths []tree.Path
|
paths []tree.Path
|
||||||
|
visitedNodes map[*yaml.Node]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML implement yaml.Unmarshaler
|
// UnmarshalYAML implement yaml.Unmarshaler
|
||||||
func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
|
func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
|
||||||
resolved, err := p.resolveReset(value, tree.NewPath())
|
resolved, err := p.resolveReset(value, tree.NewPath())
|
||||||
|
p.visitedNodes = nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// 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) {
|
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 the path contains "<<", removing the "<<" element and merging the path
|
||||||
if strings.Contains(path.String(), ".<<") {
|
if strings.Contains(pathStr, ".<<") {
|
||||||
path = tree.NewPath(strings.Replace(path.String(), ".<<", "", 1))
|
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 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 {
|
if node.Kind == yaml.AliasNode {
|
||||||
return p.resolveReset(node.Alias, path)
|
return p.resolveReset(node.Alias, path)
|
||||||
|
|
|
@ -47,7 +47,7 @@ func init() {
|
||||||
mergeSpecials["services.*.build"] = mergeBuild
|
mergeSpecials["services.*.build"] = mergeBuild
|
||||||
mergeSpecials["services.*.build.args"] = mergeToSequence
|
mergeSpecials["services.*.build.args"] = mergeToSequence
|
||||||
mergeSpecials["services.*.build.additional_contexts"] = 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.*.build.labels"] = mergeToSequence
|
||||||
mergeSpecials["services.*.command"] = override
|
mergeSpecials["services.*.command"] = override
|
||||||
mergeSpecials["services.*.depends_on"] = mergeDependsOn
|
mergeSpecials["services.*.depends_on"] = mergeDependsOn
|
||||||
|
@ -58,7 +58,7 @@ func init() {
|
||||||
mergeSpecials["services.*.entrypoint"] = override
|
mergeSpecials["services.*.entrypoint"] = override
|
||||||
mergeSpecials["services.*.env_file"] = mergeToSequence
|
mergeSpecials["services.*.env_file"] = mergeToSequence
|
||||||
mergeSpecials["services.*.environment"] = mergeToSequence
|
mergeSpecials["services.*.environment"] = mergeToSequence
|
||||||
mergeSpecials["services.*.extra_hosts"] = mergeToSequence
|
mergeSpecials["services.*.extra_hosts"] = mergeExtraHosts
|
||||||
mergeSpecials["services.*.healthcheck.test"] = override
|
mergeSpecials["services.*.healthcheck.test"] = override
|
||||||
mergeSpecials["services.*.labels"] = mergeToSequence
|
mergeSpecials["services.*.labels"] = mergeToSequence
|
||||||
mergeSpecials["services.*.logging"] = mergeLogging
|
mergeSpecials["services.*.logging"] = mergeLogging
|
||||||
|
@ -163,6 +163,22 @@ func mergeNetworks(c any, o any, path tree.Path) (any, error) {
|
||||||
return mergeMappings(right, left, path)
|
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) {
|
func mergeToSequence(c any, o any, _ tree.Path) (any, error) {
|
||||||
right := convertIntoSequence(c)
|
right := convertIntoSequence(c)
|
||||||
left := convertIntoSequence(o)
|
left := convertIntoSequence(o)
|
||||||
|
@ -172,15 +188,21 @@ func mergeToSequence(c any, o any, _ tree.Path) (any, error) {
|
||||||
func convertIntoSequence(value any) []any {
|
func convertIntoSequence(value any) []any {
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case map[string]any:
|
case map[string]any:
|
||||||
seq := make([]any, len(v))
|
var seq []any
|
||||||
i := 0
|
for k, val := range v {
|
||||||
for k, v := range v {
|
if val == nil {
|
||||||
if v == nil {
|
seq = append(seq, k)
|
||||||
seq[i] = k
|
|
||||||
} else {
|
} 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 {
|
slices.SortFunc(seq, func(a, b any) int {
|
||||||
return cmp.Compare(a.(string), b.(string))
|
return cmp.Compare(a.(string), b.(string))
|
||||||
|
|
|
@ -267,6 +267,7 @@
|
||||||
},
|
},
|
||||||
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
"extra_hosts": {"$ref": "#/definitions/extra_hosts"},
|
"extra_hosts": {"$ref": "#/definitions/extra_hosts"},
|
||||||
|
"gpus": {"$ref": "#/definitions/gpus"},
|
||||||
"group_add": {
|
"group_add": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
@ -370,6 +371,8 @@
|
||||||
},
|
},
|
||||||
"uniqueItems": true
|
"uniqueItems": true
|
||||||
},
|
},
|
||||||
|
"post_start": {"type": "array", "items": {"$ref": "#/definitions/service_hook"}},
|
||||||
|
"pre_stop": {"type": "array", "items": {"$ref": "#/definitions/service_hook"}},
|
||||||
"privileged": {"type": ["boolean", "string"]},
|
"privileged": {"type": ["boolean", "string"]},
|
||||||
"profiles": {"$ref": "#/definitions/list_of_strings"},
|
"profiles": {"$ref": "#/definitions/list_of_strings"},
|
||||||
"pull_policy": {"type": "string", "enum": [
|
"pull_policy": {"type": "string", "enum": [
|
||||||
|
@ -416,6 +419,7 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"propagation": {"type": "string"},
|
"propagation": {"type": "string"},
|
||||||
"create_host_path": {"type": ["boolean", "string"]},
|
"create_host_path": {"type": ["boolean", "string"]},
|
||||||
|
"recursive": {"type": "string", "enum": ["enabled", "disabled", "writable", "readonly"]},
|
||||||
"selinux": {"type": "string", "enum": ["z", "Z"]}
|
"selinux": {"type": "string", "enum": ["z", "Z"]}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
@ -500,11 +504,11 @@
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"patternProperties": {"^x-": {}}
|
"patternProperties": {"^x-": {}}
|
||||||
},
|
}
|
||||||
"additionalProperties": false,
|
|
||||||
"patternProperties": {"^x-": {}}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
},
|
},
|
||||||
"deployment": {
|
"deployment": {
|
||||||
"id": "#/definitions/deployment",
|
"id": "#/definitions/deployment",
|
||||||
|
@ -632,6 +636,26 @@
|
||||||
"devices": {
|
"devices": {
|
||||||
"id": "#/definitions/devices",
|
"id": "#/definitions/devices",
|
||||||
"type": "array",
|
"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": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"env_file": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{"type": "string"},
|
{"type": "string"},
|
||||||
|
@ -828,6 +866,9 @@
|
||||||
"path": {
|
"path": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"required": {
|
"required": {
|
||||||
"type": ["boolean", "string"],
|
"type": ["boolean", "string"],
|
||||||
"default": true
|
"default": true
|
||||||
|
@ -878,7 +919,8 @@
|
||||||
"patternProperties": {
|
"patternProperties": {
|
||||||
".+": {
|
".+": {
|
||||||
"type": ["string", "array"]
|
"type": ["string", "array"]
|
||||||
}
|
},
|
||||||
|
"uniqueItems": false
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,6 +33,7 @@ func init() {
|
||||||
transformers["services.*.extends"] = transformExtends
|
transformers["services.*.extends"] = transformExtends
|
||||||
transformers["services.*.networks"] = transformServiceNetworks
|
transformers["services.*.networks"] = transformServiceNetworks
|
||||||
transformers["services.*.volumes.*"] = transformVolumeMount
|
transformers["services.*.volumes.*"] = transformVolumeMount
|
||||||
|
transformers["services.*.dns"] = transformStringOrList
|
||||||
transformers["services.*.devices.*"] = transformDeviceMapping
|
transformers["services.*.devices.*"] = transformDeviceMapping
|
||||||
transformers["services.*.secrets.*"] = transformFileMount
|
transformers["services.*.secrets.*"] = transformFileMount
|
||||||
transformers["services.*.configs.*"] = transformFileMount
|
transformers["services.*.configs.*"] = transformFileMount
|
||||||
|
@ -48,6 +49,15 @@ func init() {
|
||||||
transformers["include.*"] = transformInclude
|
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
|
// Canonical transforms a compose model into canonical syntax
|
||||||
func Canonical(yaml map[string]any, ignoreParseError bool) (map[string]any, error) {
|
func Canonical(yaml map[string]any, ignoreParseError bool) (map[string]any, error) {
|
||||||
canonical, err := transform(yaml, tree.NewPath(), ignoreParseError)
|
canonical, err := transform(yaml, tree.NewPath(), ignoreParseError)
|
||||||
|
|
|
@ -26,6 +26,8 @@ func init() {
|
||||||
defaultValues["services.*.build"] = defaultBuildContext
|
defaultValues["services.*.build"] = defaultBuildContext
|
||||||
defaultValues["services.*.secrets.*"] = defaultSecretMount
|
defaultValues["services.*.secrets.*"] = defaultSecretMount
|
||||||
defaultValues["services.*.ports.*"] = portDefaults
|
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
|
// 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"`
|
Driver string `yaml:"driver,omitempty" json:"driver,omitempty"`
|
||||||
Count DeviceCount `yaml:"count,omitempty" json:"count,omitempty"`
|
Count DeviceCount `yaml:"count,omitempty" json:"count,omitempty"`
|
||||||
IDs []string `yaml:"device_ids,omitempty" json:"device_ids,omitempty"`
|
IDs []string `yaml:"device_ids,omitempty" json:"device_ids,omitempty"`
|
||||||
|
Options Mapping `yaml:"options,omitempty" json:"options,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeviceCount int64
|
type DeviceCount int64
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
type EnvFile struct {
|
type EnvFile struct {
|
||||||
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
||||||
Required bool `yaml:"required" json:"required"`
|
Required bool `yaml:"required" json:"required"`
|
||||||
|
Format string `yaml:"format,omitempty" json:"format,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalYAML makes EnvFile implement yaml.Marshaler
|
// 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 {
|
for _, envFile := range service.EnvFiles {
|
||||||
if _, err := os.Stat(envFile.Path); os.IsNotExist(err) {
|
vars, err := loadEnvFile(envFile, resolve)
|
||||||
if envFile.Required {
|
|
||||||
return nil, fmt.Errorf("env file %s not found: %w", envFile.Path, err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
b, err := os.ReadFile(envFile.Path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load %s: %w", envFile.Path, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
environment.OverrideBy(vars.ToMappingWithEquals())
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
service.Environment = environment.OverrideBy(service.Environment)
|
service.Environment = environment.OverrideBy(service.Environment)
|
||||||
|
@ -644,6 +633,31 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project
|
||||||
return newProject, nil
|
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 {
|
func (p *Project) deepCopy() *Project {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -20,7 +20,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
@ -82,6 +81,7 @@ type ServiceConfig struct {
|
||||||
ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"`
|
ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"`
|
||||||
ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
|
ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
|
||||||
GroupAdd []string `yaml:"group_add,omitempty" json:"group_add,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"`
|
Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
|
||||||
HealthCheck *HealthCheckConfig `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"`
|
HealthCheck *HealthCheckConfig `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"`
|
||||||
Image string `yaml:"image,omitempty" json:"image,omitempty"`
|
Image string `yaml:"image,omitempty" json:"image,omitempty"`
|
||||||
|
@ -132,6 +132,8 @@ type ServiceConfig struct {
|
||||||
Volumes []ServiceVolumeConfig `yaml:"volumes,omitempty" json:"volumes,omitempty"`
|
Volumes []ServiceVolumeConfig `yaml:"volumes,omitempty" json:"volumes,omitempty"`
|
||||||
VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"`
|
VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"`
|
||||||
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,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:"-"`
|
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
||||||
}
|
}
|
||||||
|
@ -388,30 +390,6 @@ type Resource struct {
|
||||||
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
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
|
// GenericResource represents a "user defined" resource which can
|
||||||
// only be an integer (e.g: SSD=3) for a service
|
// only be an integer (e.g: SSD=3) for a service
|
||||||
type GenericResource struct {
|
type GenericResource struct {
|
||||||
|
@ -552,9 +530,6 @@ func (s ServiceVolumeConfig) String() string {
|
||||||
if s.Volume != nil && s.Volume.NoCopy {
|
if s.Volume != nil && s.Volume.NoCopy {
|
||||||
options = append(options, "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, ","))
|
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"`
|
SELinux string `yaml:"selinux,omitempty" json:"selinux,omitempty"`
|
||||||
Propagation string `yaml:"propagation,omitempty" json:"propagation,omitempty"`
|
Propagation string `yaml:"propagation,omitempty" json:"propagation,omitempty"`
|
||||||
CreateHostPath bool `yaml:"create_host_path,omitempty" json:"create_host_path,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:"-"`
|
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ var checks = map[tree.Path]checkerFunc{
|
||||||
"configs.*": checkFileObject("file", "environment", "content"),
|
"configs.*": checkFileObject("file", "environment", "content"),
|
||||||
"secrets.*": checkFileObject("file", "environment"),
|
"secrets.*": checkFileObject("file", "environment"),
|
||||||
"services.*.develop.watch.*.path": checkPath,
|
"services.*.develop.watch.*.path": checkPath,
|
||||||
|
"services.*.deploy.resources.reservations.devices.*": checkDeviceRequest,
|
||||||
|
"services.*.gpus.*": checkDeviceRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Validate(dict map[string]any) error {
|
func Validate(dict map[string]any) error {
|
||||||
|
@ -94,3 +96,13 @@ func checkPath(value any, p tree.Path) error {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
# github.com/cespare/xxhash/v2 v2.3.0
|
||||||
## explicit; go 1.11
|
## explicit; go 1.11
|
||||||
github.com/cespare/xxhash/v2
|
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
|
## explicit; go 1.21
|
||||||
github.com/compose-spec/compose-go/v2/cli
|
github.com/compose-spec/compose-go/v2/cli
|
||||||
github.com/compose-spec/compose-go/v2/consts
|
github.com/compose-spec/compose-go/v2/consts
|
||||||
|
|
Loading…
Reference in New Issue