Merge pull request #2609 from glours/bump-compose-go-v2.1.4

bump compose-go to v2.1.4
This commit is contained in:
CrazyMax 2024-07-17 18:18:12 +02:00 committed by GitHub
commit 12d431d1b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 625 additions and 220 deletions

2
go.mod
View File

@ -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.1.3 github.com/compose-spec/compose-go/v2 v2.1.4
github.com/containerd/console v1.0.4 github.com/containerd/console v1.0.4
github.com/containerd/containerd v1.7.19 github.com/containerd/containerd v1.7.19
github.com/containerd/continuity v0.4.3 github.com/containerd/continuity v0.4.3

4
go.sum
View File

@ -84,8 +84,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
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.1.3 h1:bD67uqLuL/XgkAK6ir3xZvNLFPxPScEi1KW7R5esrLE= github.com/compose-spec/compose-go/v2 v2.1.4 h1:+1UKMvbBJo22Bpulgb9KAeZwRT99hANf3tDQVeG6ZJo=
github.com/compose-spec/compose-go/v2 v2.1.3/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= github.com/compose-spec/compose-go/v2 v2.1.4/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 v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=

View File

@ -22,9 +22,14 @@ import (
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
) )
// Will update the environment variables for the format {- VAR} (without interpolation) // ResolveEnvironment update the environment variables for the format {- VAR} (without interpolation)
// This function should resolve context environment vars for include (passed in env_file) func ResolveEnvironment(dict map[string]any, environment types.Mapping) {
func resolveServicesEnvironment(dict map[string]any, config types.ConfigDetails) { resolveServicesEnvironment(dict, environment)
resolveSecretsEnvironment(dict, environment)
resolveConfigsEnvironment(dict, environment)
}
func resolveServicesEnvironment(dict map[string]any, environment types.Mapping) {
services, ok := dict["services"].(map[string]any) services, ok := dict["services"].(map[string]any)
if !ok { if !ok {
return return
@ -45,7 +50,7 @@ func resolveServicesEnvironment(dict map[string]any, config types.ConfigDetails)
if !ok { if !ok {
continue continue
} }
if found, ok := config.Environment[varEnv]; ok { if found, ok := environment[varEnv]; ok {
envs = append(envs, fmt.Sprintf("%s=%s", varEnv, found)) envs = append(envs, fmt.Sprintf("%s=%s", varEnv, found))
} else { } else {
// either does not exist or it was already resolved in interpolation // either does not exist or it was already resolved in interpolation
@ -57,3 +62,49 @@ func resolveServicesEnvironment(dict map[string]any, config types.ConfigDetails)
} }
dict["services"] = services dict["services"] = services
} }
func resolveSecretsEnvironment(dict map[string]any, environment types.Mapping) {
secrets, ok := dict["secrets"].(map[string]any)
if !ok {
return
}
for name, cfg := range secrets {
secret, ok := cfg.(map[string]any)
if !ok {
continue
}
env, ok := secret["environment"].(string)
if !ok {
continue
}
if found, ok := environment[env]; ok {
secret["content"] = found
}
secrets[name] = secret
}
dict["secrets"] = secrets
}
func resolveConfigsEnvironment(dict map[string]any, environment types.Mapping) {
configs, ok := dict["configs"].(map[string]any)
if !ok {
return
}
for name, cfg := range configs {
config, ok := cfg.(map[string]any)
if !ok {
continue
}
env, ok := config["environment"].(string)
if !ok {
continue
}
if found, ok := environment[env]; ok {
config["content"] = found
}
configs[name] = config
}
dict["configs"] = configs
}

View File

@ -22,7 +22,11 @@ import (
"path/filepath" "path/filepath"
"github.com/compose-spec/compose-go/v2/consts" "github.com/compose-spec/compose-go/v2/consts"
"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/template"
"github.com/compose-spec/compose-go/v2/transform"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
) )
@ -67,25 +71,43 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
) )
switch v := extends.(type) { switch v := extends.(type) {
case map[string]any: case map[string]any:
if opts.Interpolate != nil {
v, err = interpolation.Interpolate(v, *opts.Interpolate)
if err != nil {
return nil, err
}
}
ref = v["service"].(string) ref = v["service"].(string)
file = v["file"] file = v["file"]
opts.ProcessEvent("extends", v) opts.ProcessEvent("extends", v)
case string: case string:
if opts.Interpolate != nil {
v, err = opts.Interpolate.Substitute(v, template.Mapping(opts.Interpolate.LookupValue))
if err != nil {
return nil, err
}
}
ref = v ref = v
opts.ProcessEvent("extends", map[string]any{"service": ref}) opts.ProcessEvent("extends", map[string]any{"service": ref})
} }
var base any var (
base any
processor PostProcessor
)
if file != nil { if file != nil {
filename = file.(string) refFilename := file.(string)
services, err = getExtendsBaseFromFile(ctx, ref, filename, opts, tracker) services, processor, err = getExtendsBaseFromFile(ctx, name, ref, filename, refFilename, opts, tracker)
post = append(post, processor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
filename = refFilename
} else { } else {
_, ok := services[ref] _, ok := services[ref]
if !ok { if !ok {
return nil, fmt.Errorf("cannot extend service %q in %s: service not found", name, filename) return nil, fmt.Errorf("cannot extend service %q in %s: service %q not found", name, filename, ref)
} }
} }
@ -121,47 +143,71 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
return merged, nil return merged, nil
} }
func getExtendsBaseFromFile(ctx context.Context, name string, path string, opts *Options, ct *cycleTracker) (map[string]any, error) { func getExtendsBaseFromFile(
ctx context.Context,
name, ref string,
path, refPath string,
opts *Options,
ct *cycleTracker,
) (map[string]any, PostProcessor, error) {
for _, loader := range opts.ResourceLoaders { for _, loader := range opts.ResourceLoaders {
if !loader.Accept(path) { if !loader.Accept(refPath) {
continue continue
} }
local, err := loader.Load(ctx, path) local, err := loader.Load(ctx, refPath)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
localdir := filepath.Dir(local) localdir := filepath.Dir(local)
relworkingdir := loader.Dir(path) relworkingdir := loader.Dir(refPath)
extendsOpts := opts.clone() extendsOpts := opts.clone()
// replace localResourceLoader with a new flavour, using extended file base path // replace localResourceLoader with a new flavour, using extended file base path
extendsOpts.ResourceLoaders = append(opts.RemoteResourceLoaders(), localResourceLoader{ extendsOpts.ResourceLoaders = append(opts.RemoteResourceLoaders(), localResourceLoader{
WorkingDir: localdir, WorkingDir: localdir,
}) })
extendsOpts.ResolvePaths = true extendsOpts.ResolvePaths = false // we do relative path resolution after file has been loaded
extendsOpts.SkipNormalization = true extendsOpts.SkipNormalization = true
extendsOpts.SkipConsistencyCheck = true extendsOpts.SkipConsistencyCheck = true
extendsOpts.SkipInclude = true extendsOpts.SkipInclude = true
extendsOpts.SkipExtends = true // we manage extends recursively based on raw service definition extendsOpts.SkipExtends = true // we manage extends recursively based on raw service definition
extendsOpts.SkipValidation = true // we validate the merge result extendsOpts.SkipValidation = true // we validate the merge result
extendsOpts.SkipDefaultValues = true extendsOpts.SkipDefaultValues = true
source, err := loadYamlModel(ctx, types.ConfigDetails{ source, processor, err := loadYamlFile(ctx, types.ConfigFile{Filename: local},
WorkingDir: relworkingdir, extendsOpts, relworkingdir, nil, ct, map[string]any{}, nil)
ConfigFiles: []types.ConfigFile{
{Filename: local},
},
}, extendsOpts, ct, nil)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
services := source["services"].(map[string]any) services := source["services"].(map[string]any)
_, ok := services[name] _, ok := services[ref]
if !ok { if !ok {
return nil, fmt.Errorf("cannot extend service %q in %s: service not found", name, path) return nil, nil, fmt.Errorf(
"cannot extend service %q in %s: service %q not found in %s",
name,
path,
ref,
refPath,
)
} }
return services, nil
// Attempt to make a canonical model so ResolveRelativePaths can operate on source:target short syntaxes
source, err = transform.Canonical(source, true)
if err != nil {
return nil, nil, err
}
var remotes []paths.RemoteResource
for _, loader := range opts.RemoteResourceLoaders() {
remotes = append(remotes, loader.Accept)
}
err = paths.ResolveRelativePaths(source, relworkingdir, remotes)
if err != nil {
return nil, nil, err
}
return services, processor, nil
} }
return nil, fmt.Errorf("cannot read %s", path) return nil, nil, fmt.Errorf("cannot read %s", refPath)
} }
func deepClone(value any) any { func deepClone(value any) any {

View File

@ -30,7 +30,7 @@ import (
) )
// loadIncludeConfig parse the required config from raw yaml // loadIncludeConfig parse the required config from raw yaml
func loadIncludeConfig(source any) ([]types.IncludeConfig, error) { func loadIncludeConfig(source any, options *Options) ([]types.IncludeConfig, error) {
if source == nil { if source == nil {
return nil, nil return nil, nil
} }
@ -45,21 +45,32 @@ func loadIncludeConfig(source any) ([]types.IncludeConfig, error) {
} }
} }
} }
if options.Interpolate != nil {
for i, config := range configs {
interpolated, err := interp.Interpolate(config.(map[string]any), *options.Interpolate)
if err != nil {
return nil, err
}
configs[i] = interpolated
}
}
var requires []types.IncludeConfig var requires []types.IncludeConfig
err := Transform(source, &requires) err := Transform(source, &requires)
return requires, err return requires, err
} }
func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model map[string]any, options *Options, included []string) error { func ApplyInclude(ctx context.Context, workingDir string, environment types.Mapping, model map[string]any, options *Options, included []string) error {
includeConfig, err := loadIncludeConfig(model["include"]) includeConfig, err := loadIncludeConfig(model["include"], options)
if err != nil { if err != nil {
return err return err
} }
for _, r := range includeConfig { for _, r := range includeConfig {
for _, listener := range options.Listeners { for _, listener := range options.Listeners {
listener("include", map[string]any{ listener("include", map[string]any{
"path": r.Path, "path": r.Path,
"workingdir": configDetails.WorkingDir, "workingdir": workingDir,
}) })
} }
@ -83,7 +94,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
r.ProjectDirectory = filepath.Dir(path) r.ProjectDirectory = filepath.Dir(path)
case !filepath.IsAbs(r.ProjectDirectory): case !filepath.IsAbs(r.ProjectDirectory):
relworkingdir = loader.Dir(r.ProjectDirectory) relworkingdir = loader.Dir(r.ProjectDirectory)
r.ProjectDirectory = filepath.Join(configDetails.WorkingDir, r.ProjectDirectory) r.ProjectDirectory = filepath.Join(workingDir, r.ProjectDirectory)
default: default:
relworkingdir = r.ProjectDirectory relworkingdir = r.ProjectDirectory
@ -117,7 +128,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
envFile := []string{} envFile := []string{}
for _, f := range r.EnvFile { for _, f := range r.EnvFile {
if !filepath.IsAbs(f) { if !filepath.IsAbs(f) {
f = filepath.Join(configDetails.WorkingDir, f) f = filepath.Join(workingDir, f)
s, err := os.Stat(f) s, err := os.Stat(f)
if err != nil { if err != nil {
return err return err
@ -131,7 +142,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
r.EnvFile = envFile r.EnvFile = envFile
} }
envFromFile, err := dotenv.GetEnvFromFile(configDetails.Environment, r.EnvFile) envFromFile, err := dotenv.GetEnvFromFile(environment, r.EnvFile)
if err != nil { if err != nil {
return err return err
} }
@ -139,7 +150,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
config := types.ConfigDetails{ config := types.ConfigDetails{
WorkingDir: relworkingdir, WorkingDir: relworkingdir,
ConfigFiles: types.ToConfigFiles(r.Path), ConfigFiles: types.ToConfigFiles(r.Path),
Environment: configDetails.Environment.Clone().Merge(envFromFile), Environment: environment.Clone().Merge(envFromFile),
} }
loadOptions.Interpolate = &interp.Options{ loadOptions.Interpolate = &interp.Options{
Substitute: options.Interpolate.Substitute, Substitute: options.Interpolate.Substitute,

View File

@ -89,7 +89,7 @@ var versionWarning []string
func (o *Options) warnObsoleteVersion(file string) { func (o *Options) warnObsoleteVersion(file string) {
if !slices.Contains(versionWarning, file) { if !slices.Contains(versionWarning, file) {
logrus.Warning(fmt.Sprintf("%s: `version` is obsolete", file)) logrus.Warning(fmt.Sprintf("%s: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion", file))
} }
versionWarning = append(versionWarning, file) versionWarning = append(versionWarning, file)
} }
@ -358,100 +358,19 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
dict = map[string]interface{}{} dict = map[string]interface{}{}
err error err error
) )
workingDir, environment := config.WorkingDir, config.Environment
for _, file := range config.ConfigFiles { for _, file := range config.ConfigFiles {
fctx := context.WithValue(ctx, consts.ComposeFileKey{}, file.Filename) dict, _, err = loadYamlFile(ctx, file, opts, workingDir, environment, ct, dict, included)
if file.Content == nil && file.Config == nil { if err != nil {
content, err := os.ReadFile(file.Filename) return nil, err
if err != nil {
return nil, err
}
file.Content = content
} }
}
processRawYaml := func(raw interface{}, processors ...PostProcessor) error { if opts.Interpolate != nil && !opts.SkipInterpolation {
converted, err := convertToStringKeysRecursive(raw, "") dict, err = interp.Interpolate(dict, *opts.Interpolate)
if err != nil { if err != nil {
return err return nil, err
}
cfg, ok := converted.(map[string]interface{})
if !ok {
return errors.New("Top-level object must be a mapping")
}
if opts.Interpolate != nil && !opts.SkipInterpolation {
cfg, err = interp.Interpolate(cfg, *opts.Interpolate)
if err != nil {
return err
}
}
fixEmptyNotNull(cfg)
if !opts.SkipExtends {
err = ApplyExtends(fctx, cfg, opts, ct, processors...)
if err != nil {
return err
}
}
for _, processor := range processors {
if err := processor.Apply(dict); err != nil {
return err
}
}
if !opts.SkipInclude {
included = append(included, config.ConfigFiles[0].Filename)
err = ApplyInclude(ctx, config, cfg, opts, included)
if err != nil {
return err
}
}
dict, err = override.Merge(dict, cfg)
if err != nil {
return err
}
dict, err = override.EnforceUnicity(dict)
if err != nil {
return err
}
if !opts.SkipValidation {
if err := schema.Validate(dict); err != nil {
return fmt.Errorf("validating %s: %w", file.Filename, err)
}
if _, ok := dict["version"]; ok {
opts.warnObsoleteVersion(file.Filename)
delete(dict, "version")
}
}
return err
}
if file.Config == nil {
r := bytes.NewReader(file.Content)
decoder := yaml.NewDecoder(r)
for {
var raw interface{}
processor := &ResetProcessor{target: &raw}
err := decoder.Decode(processor)
if err != nil && errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
if err := processRawYaml(raw, processor); err != nil {
return nil, err
}
}
} else {
if err := processRawYaml(file.Config); err != nil {
return nil, err
}
} }
} }
@ -460,7 +379,6 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
return nil, err return nil, err
} }
// 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)
if err != nil { if err != nil {
return nil, err return nil, err
@ -489,11 +407,98 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
return nil, err return nil, err
} }
} }
resolveServicesEnvironment(dict, config) ResolveEnvironment(dict, config.Environment)
return dict, nil return dict, nil
} }
func loadYamlFile(ctx context.Context, file types.ConfigFile, opts *Options, workingDir string, environment types.Mapping, ct *cycleTracker, dict map[string]interface{}, included []string) (map[string]interface{}, PostProcessor, error) {
ctx = context.WithValue(ctx, consts.ComposeFileKey{}, file.Filename)
if file.Content == nil && file.Config == nil {
content, err := os.ReadFile(file.Filename)
if err != nil {
return nil, nil, err
}
file.Content = content
}
processRawYaml := func(raw interface{}, processors ...PostProcessor) error {
converted, err := convertToStringKeysRecursive(raw, "")
if err != nil {
return err
}
cfg, ok := converted.(map[string]interface{})
if !ok {
return errors.New("Top-level object must be a mapping")
}
fixEmptyNotNull(cfg)
if !opts.SkipExtends {
err = ApplyExtends(ctx, cfg, opts, ct, processors...)
if err != nil {
return err
}
}
for _, processor := range processors {
if err := processor.Apply(dict); err != nil {
return err
}
}
if !opts.SkipInclude {
included = append(included, file.Filename)
err = ApplyInclude(ctx, workingDir, environment, cfg, opts, included)
if err != nil {
return err
}
}
dict, err = override.Merge(dict, cfg)
if err != nil {
return err
}
if !opts.SkipValidation {
if err := schema.Validate(dict); err != nil {
return fmt.Errorf("validating %s: %w", file.Filename, err)
}
if _, ok := dict["version"]; ok {
opts.warnObsoleteVersion(file.Filename)
delete(dict, "version")
}
}
return nil
}
var processor PostProcessor
if file.Config == nil {
r := bytes.NewReader(file.Content)
decoder := yaml.NewDecoder(r)
for {
var raw interface{}
reset := &ResetProcessor{target: &raw}
err := decoder.Decode(reset)
if err != nil && errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, nil, err
}
processor = reset
if err := processRawYaml(raw, processor); err != nil {
return nil, nil, err
}
}
} else {
if err := processRawYaml(file.Config); err != nil {
return nil, nil, err
}
}
return dict, processor, nil
}
func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (map[string]interface{}, error) { func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (map[string]interface{}, error) {
mainFile := configDetails.ConfigFiles[0].Filename mainFile := configDetails.ConfigFiles[0].Filename
for _, f := range loaded { for _, f := range loaded {

View File

@ -52,14 +52,14 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
} }
if a, ok := build["args"]; ok { if a, ok := build["args"]; ok {
build["args"], _ = resolve(a, fn) build["args"], _ = resolve(a, fn, false)
} }
service["build"] = build service["build"] = build
} }
if e, ok := service["environment"]; ok { if e, ok := service["environment"]; ok {
service["environment"], _ = resolve(e, fn) service["environment"], _ = resolve(e, fn, true)
} }
var dependsOn map[string]any var dependsOn map[string]any
@ -178,12 +178,12 @@ func normalizeNetworks(dict map[string]any) {
} }
} }
func resolve(a any, fn func(s string) (string, bool)) (any, bool) { func resolve(a any, fn func(s string) (string, bool), keepEmpty bool) (any, bool) {
switch v := a.(type) { switch v := a.(type) {
case []any: case []any:
var resolved []any var resolved []any
for _, val := range v { for _, val := range v {
if r, ok := resolve(val, fn); ok { if r, ok := resolve(val, fn, keepEmpty); ok {
resolved = append(resolved, r) resolved = append(resolved, r)
} }
} }
@ -197,6 +197,8 @@ func resolve(a any, fn func(s string) (string, bool)) (any, bool) {
} }
if s, ok := fn(key); ok { if s, ok := fn(key); ok {
resolved[key] = s resolved[key] = s
} else if keepEmpty {
resolved[key] = nil
} }
} }
return resolved, true return resolved, true
@ -205,6 +207,9 @@ func resolve(a any, fn func(s string) (string, bool)) (any, bool) {
if val, ok := fn(v); ok { if val, ok := fn(v); ok {
return fmt.Sprintf("%s=%s", v, val), true return fmt.Sprintf("%s=%s", v, val), true
} }
if keepEmpty {
return v, true
}
return "", false return "", false
} }
return v, true return v, true

View File

@ -38,7 +38,7 @@ func ResolveRelativePaths(project map[string]any, base string, remotes []RemoteR
"services.*.build.additional_contexts.*": r.absContextPath, "services.*.build.additional_contexts.*": r.absContextPath,
"services.*.env_file.*.path": r.absPath, "services.*.env_file.*.path": r.absPath,
"services.*.extends.file": r.absExtendsPath, "services.*.extends.file": r.absExtendsPath,
"services.*.develop.watch.*.path": r.absPath, "services.*.develop.watch.*.path": r.absSymbolicLink,
"services.*.volumes.*": r.absVolumeMount, "services.*.volumes.*": r.absVolumeMount,
"configs.*.file": r.maybeUnixPath, "configs.*.file": r.maybeUnixPath,
"secrets.*.file": r.maybeUnixPath, "secrets.*.file": r.maybeUnixPath,

View File

@ -19,6 +19,8 @@ package paths
import ( import (
"path" "path"
"path/filepath" "path/filepath"
"github.com/compose-spec/compose-go/v2/utils"
) )
func (r *relativePathsResolver) maybeUnixPath(a any) (any, error) { func (r *relativePathsResolver) maybeUnixPath(a any) (any, error) {
@ -38,3 +40,15 @@ func (r *relativePathsResolver) maybeUnixPath(a any) (any, error) {
} }
return p, nil return p, nil
} }
func (r *relativePathsResolver) absSymbolicLink(value any) (any, error) {
abs, err := r.absPath(value)
if err != nil {
return nil, err
}
str, ok := abs.(string)
if !ok {
return abs, nil
}
return utils.ResolveSymbolicLink(str)
}

View File

@ -13,7 +13,6 @@
"name": { "name": {
"type": "string", "type": "string",
"pattern": "^[a-z0-9][a-z0-9_-]*$",
"description": "define the Compose project name, until user defines one explicitly." "description": "define the Compose project name, until user defines one explicitly."
}, },
@ -94,7 +93,7 @@
"develop": {"$ref": "#/definitions/development"}, "develop": {"$ref": "#/definitions/development"},
"deploy": {"$ref": "#/definitions/deployment"}, "deploy": {"$ref": "#/definitions/deployment"},
"annotations": {"$ref": "#/definitions/list_or_dict"}, "annotations": {"$ref": "#/definitions/list_or_dict"},
"attach": {"type": "boolean"}, "attach": {"type": ["boolean", "string"]},
"build": { "build": {
"oneOf": [ "oneOf": [
{"type": "string"}, {"type": "string"},
@ -110,15 +109,15 @@
"labels": {"$ref": "#/definitions/list_or_dict"}, "labels": {"$ref": "#/definitions/list_or_dict"},
"cache_from": {"type": "array", "items": {"type": "string"}}, "cache_from": {"type": "array", "items": {"type": "string"}},
"cache_to": {"type": "array", "items": {"type": "string"}}, "cache_to": {"type": "array", "items": {"type": "string"}},
"no_cache": {"type": "boolean"}, "no_cache": {"type": ["boolean", "string"]},
"additional_contexts": {"$ref": "#/definitions/list_or_dict"}, "additional_contexts": {"$ref": "#/definitions/list_or_dict"},
"network": {"type": "string"}, "network": {"type": "string"},
"pull": {"type": "boolean"}, "pull": {"type": ["boolean", "string"]},
"target": {"type": "string"}, "target": {"type": "string"},
"shm_size": {"type": ["integer", "string"]}, "shm_size": {"type": ["integer", "string"]},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"}, "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"isolation": {"type": "string"}, "isolation": {"type": "string"},
"privileged": {"type": "boolean"}, "privileged": {"type": ["boolean", "string"]},
"secrets": {"$ref": "#/definitions/service_config_or_secret"}, "secrets": {"$ref": "#/definitions/service_config_or_secret"},
"tags": {"type": "array", "items": {"type": "string"}}, "tags": {"type": "array", "items": {"type": "string"}},
"ulimits": {"$ref": "#/definitions/ulimits"}, "ulimits": {"$ref": "#/definitions/ulimits"},
@ -148,7 +147,7 @@
"type": "array", "type": "array",
"items": {"$ref": "#/definitions/blkio_limit"} "items": {"$ref": "#/definitions/blkio_limit"}
}, },
"weight": {"type": "integer"}, "weight": {"type": ["integer", "string"]},
"weight_device": { "weight_device": {
"type": "array", "type": "array",
"items": {"$ref": "#/definitions/blkio_weight"} "items": {"$ref": "#/definitions/blkio_weight"}
@ -156,15 +155,21 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "cap_add": {"type": "array", "items": {"type": "string"}},
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "cap_drop": {"type": "array", "items": {"type": "string"}},
"cgroup": {"type": "string", "enum": ["host", "private"]}, "cgroup": {"type": "string", "enum": ["host", "private"]},
"cgroup_parent": {"type": "string"}, "cgroup_parent": {"type": "string"},
"command": {"$ref": "#/definitions/command"}, "command": {"$ref": "#/definitions/command"},
"configs": {"$ref": "#/definitions/service_config_or_secret"}, "configs": {"$ref": "#/definitions/service_config_or_secret"},
"container_name": {"type": "string"}, "container_name": {"type": "string"},
"cpu_count": {"type": "integer", "minimum": 0}, "cpu_count": {"oneOf": [
"cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100}, {"type": "string"},
{"type": "integer", "minimum": 0}
]},
"cpu_percent": {"oneOf": [
{"type": "string"},
{"type": "integer", "minimum": 0, "maximum": 100}
]},
"cpu_shares": {"type": ["number", "string"]}, "cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]}, "cpu_quota": {"type": ["number", "string"]},
"cpu_period": {"type": ["number", "string"]}, "cpu_period": {"type": ["number", "string"]},
@ -192,8 +197,9 @@
"^[a-zA-Z0-9._-]+$": { "^[a-zA-Z0-9._-]+$": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"patternProperties": {"^x-": {}},
"properties": { "properties": {
"restart": {"type": "boolean"}, "restart": {"type": ["boolean", "string"]},
"required": { "required": {
"type": "boolean", "type": "boolean",
"default": true "default": true
@ -210,9 +216,9 @@
] ]
}, },
"device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"}, "device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"},
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "devices": {"type": "array", "items": {"type": "string"}},
"dns": {"$ref": "#/definitions/string_or_list"}, "dns": {"$ref": "#/definitions/string_or_list"},
"dns_opt": {"type": "array","items": {"type": "string"}, "uniqueItems": true}, "dns_opt": {"type": "array","items": {"type": "string"}},
"dns_search": {"$ref": "#/definitions/string_or_list"}, "dns_search": {"$ref": "#/definitions/string_or_list"},
"domainname": {"type": "string"}, "domainname": {"type": "string"},
"entrypoint": {"$ref": "#/definitions/command"}, "entrypoint": {"$ref": "#/definitions/command"},
@ -224,8 +230,7 @@
"items": { "items": {
"type": ["string", "number"], "type": ["string", "number"],
"format": "expose" "format": "expose"
}, }
"uniqueItems": true
}, },
"extends": { "extends": {
"oneOf": [ "oneOf": [
@ -242,23 +247,22 @@
} }
] ]
}, },
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "external_links": {"type": "array", "items": {"type": "string"}},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"}, "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"group_add": { "group_add": {
"type": "array", "type": "array",
"items": { "items": {
"type": ["string", "number"] "type": ["string", "number"]
}, }
"uniqueItems": true
}, },
"healthcheck": {"$ref": "#/definitions/healthcheck"}, "healthcheck": {"$ref": "#/definitions/healthcheck"},
"hostname": {"type": "string"}, "hostname": {"type": "string"},
"image": {"type": "string"}, "image": {"type": "string"},
"init": {"type": "boolean"}, "init": {"type": ["boolean", "string"]},
"ipc": {"type": "string"}, "ipc": {"type": "string"},
"isolation": {"type": "string"}, "isolation": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"}, "labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "links": {"type": "array", "items": {"type": "string"}},
"logging": { "logging": {
"type": "object", "type": "object",
@ -277,7 +281,7 @@
"mac_address": {"type": "string"}, "mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]}, "mem_limit": {"type": ["number", "string"]},
"mem_reservation": {"type": ["string", "integer"]}, "mem_reservation": {"type": ["string", "integer"]},
"mem_swappiness": {"type": "integer"}, "mem_swappiness": {"type": ["integer", "string"]},
"memswap_limit": {"type": ["number", "string"]}, "memswap_limit": {"type": ["number", "string"]},
"network_mode": {"type": "string"}, "network_mode": {"type": "string"},
"networks": { "networks": {
@ -315,8 +319,11 @@
} }
] ]
}, },
"oom_kill_disable": {"type": "boolean"}, "oom_kill_disable": {"type": ["boolean", "string"]},
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, "oom_score_adj": {"oneOf": [
{"type": "string"},
{"type": "integer", "minimum": -1000, "maximum": 1000}
]},
"pid": {"type": ["string", "null"]}, "pid": {"type": ["string", "null"]},
"pids_limit": {"type": ["number", "string"]}, "pids_limit": {"type": ["number", "string"]},
"platform": {"type": "string"}, "platform": {"type": "string"},
@ -324,15 +331,15 @@
"type": "array", "type": "array",
"items": { "items": {
"oneOf": [ "oneOf": [
{"type": "number", "format": "ports"}, {"type": "number"},
{"type": "string", "format": "ports"}, {"type": "string"},
{ {
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"type": "string"}, "name": {"type": "string"},
"mode": {"type": "string"}, "mode": {"type": "string"},
"host_ip": {"type": "string"}, "host_ip": {"type": "string"},
"target": {"type": "integer"}, "target": {"type": ["integer", "string"]},
"published": {"type": ["string", "integer"]}, "published": {"type": ["string", "integer"]},
"protocol": {"type": "string"}, "protocol": {"type": "string"},
"app_protocol": {"type": "string"} "app_protocol": {"type": "string"}
@ -341,32 +348,31 @@
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
} }
] ]
}, }
"uniqueItems": true
}, },
"privileged": {"type": "boolean"}, "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": [
"always", "never", "if_not_present", "build", "missing" "always", "never", "if_not_present", "build", "missing"
]}, ]},
"read_only": {"type": "boolean"}, "read_only": {"type": ["boolean", "string"]},
"restart": {"type": "string"}, "restart": {"type": "string"},
"runtime": { "runtime": {
"type": "string" "type": "string"
}, },
"scale": { "scale": {
"type": "integer" "type": ["integer", "string"]
}, },
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "security_opt": {"type": "array", "items": {"type": "string"}},
"shm_size": {"type": ["number", "string"]}, "shm_size": {"type": ["number", "string"]},
"secrets": {"$ref": "#/definitions/service_config_or_secret"}, "secrets": {"$ref": "#/definitions/service_config_or_secret"},
"sysctls": {"$ref": "#/definitions/list_or_dict"}, "sysctls": {"$ref": "#/definitions/list_or_dict"},
"stdin_open": {"type": "boolean"}, "stdin_open": {"type": ["boolean", "string"]},
"stop_grace_period": {"type": "string", "format": "duration"}, "stop_grace_period": {"type": "string"},
"stop_signal": {"type": "string"}, "stop_signal": {"type": "string"},
"storage_opt": {"type": "object"}, "storage_opt": {"type": "object"},
"tmpfs": {"$ref": "#/definitions/string_or_list"}, "tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"}, "tty": {"type": ["boolean", "string"]},
"ulimits": {"$ref": "#/definitions/ulimits"}, "ulimits": {"$ref": "#/definitions/ulimits"},
"user": {"type": "string"}, "user": {"type": "string"},
"uts": {"type": "string"}, "uts": {"type": "string"},
@ -383,13 +389,13 @@
"type": {"type": "string"}, "type": {"type": "string"},
"source": {"type": "string"}, "source": {"type": "string"},
"target": {"type": "string"}, "target": {"type": "string"},
"read_only": {"type": "boolean"}, "read_only": {"type": ["boolean", "string"]},
"consistency": {"type": "string"}, "consistency": {"type": "string"},
"bind": { "bind": {
"type": "object", "type": "object",
"properties": { "properties": {
"propagation": {"type": "string"}, "propagation": {"type": "string"},
"create_host_path": {"type": "boolean"}, "create_host_path": {"type": ["boolean", "string"]},
"selinux": {"type": "string", "enum": ["z", "Z"]} "selinux": {"type": "string", "enum": ["z", "Z"]}
}, },
"additionalProperties": false, "additionalProperties": false,
@ -398,7 +404,7 @@
"volume": { "volume": {
"type": "object", "type": "object",
"properties": { "properties": {
"nocopy": {"type": "boolean"}, "nocopy": {"type": ["boolean", "string"]},
"subpath": {"type": "string"} "subpath": {"type": "string"}
}, },
"additionalProperties": false, "additionalProperties": false,
@ -413,7 +419,7 @@
{"type": "string"} {"type": "string"}
] ]
}, },
"mode": {"type": "number"} "mode": {"type": ["number", "string"]}
}, },
"additionalProperties": false, "additionalProperties": false,
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
@ -423,13 +429,11 @@
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
} }
] ]
}, }
"uniqueItems": true
}, },
"volumes_from": { "volumes_from": {
"type": "array", "type": "array",
"items": {"type": "string"}, "items": {"type": "string"}
"uniqueItems": true
}, },
"working_dir": {"type": "string"} "working_dir": {"type": "string"}
}, },
@ -441,18 +445,18 @@
"id": "#/definitions/healthcheck", "id": "#/definitions/healthcheck",
"type": "object", "type": "object",
"properties": { "properties": {
"disable": {"type": "boolean"}, "disable": {"type": ["boolean", "string"]},
"interval": {"type": "string", "format": "duration"}, "interval": {"type": "string"},
"retries": {"type": "number"}, "retries": {"type": ["number", "string"]},
"test": { "test": {
"oneOf": [ "oneOf": [
{"type": "string"}, {"type": "string"},
{"type": "array", "items": {"type": "string"}} {"type": "array", "items": {"type": "string"}}
] ]
}, },
"timeout": {"type": "string", "format": "duration"}, "timeout": {"type": "string"},
"start_period": {"type": "string", "format": "duration"}, "start_period": {"type": "string"},
"start_interval": {"type": "string", "format": "duration"} "start_interval": {"type": "string"}
}, },
"additionalProperties": false, "additionalProperties": false,
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
@ -484,16 +488,16 @@
"properties": { "properties": {
"mode": {"type": "string"}, "mode": {"type": "string"},
"endpoint_mode": {"type": "string"}, "endpoint_mode": {"type": "string"},
"replicas": {"type": "integer"}, "replicas": {"type": ["integer", "string"]},
"labels": {"$ref": "#/definitions/list_or_dict"}, "labels": {"$ref": "#/definitions/list_or_dict"},
"rollback_config": { "rollback_config": {
"type": "object", "type": "object",
"properties": { "properties": {
"parallelism": {"type": "integer"}, "parallelism": {"type": ["integer", "string"]},
"delay": {"type": "string", "format": "duration"}, "delay": {"type": "string"},
"failure_action": {"type": "string"}, "failure_action": {"type": "string"},
"monitor": {"type": "string", "format": "duration"}, "monitor": {"type": "string"},
"max_failure_ratio": {"type": "number"}, "max_failure_ratio": {"type": ["number", "string"]},
"order": {"type": "string", "enum": [ "order": {"type": "string", "enum": [
"start-first", "stop-first" "start-first", "stop-first"
]} ]}
@ -504,11 +508,11 @@
"update_config": { "update_config": {
"type": "object", "type": "object",
"properties": { "properties": {
"parallelism": {"type": "integer"}, "parallelism": {"type": ["integer", "string"]},
"delay": {"type": "string", "format": "duration"}, "delay": {"type": "string"},
"failure_action": {"type": "string"}, "failure_action": {"type": "string"},
"monitor": {"type": "string", "format": "duration"}, "monitor": {"type": "string"},
"max_failure_ratio": {"type": "number"}, "max_failure_ratio": {"type": ["number", "string"]},
"order": {"type": "string", "enum": [ "order": {"type": "string", "enum": [
"start-first", "stop-first" "start-first", "stop-first"
]} ]}
@ -524,7 +528,7 @@
"properties": { "properties": {
"cpus": {"type": ["number", "string"]}, "cpus": {"type": ["number", "string"]},
"memory": {"type": "string"}, "memory": {"type": "string"},
"pids": {"type": "integer"} "pids": {"type": ["integer", "string"]}
}, },
"additionalProperties": false, "additionalProperties": false,
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
@ -548,9 +552,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"condition": {"type": "string"}, "condition": {"type": "string"},
"delay": {"type": "string", "format": "duration"}, "delay": {"type": "string"},
"max_attempts": {"type": "integer"}, "max_attempts": {"type": ["integer", "string"]},
"window": {"type": "string", "format": "duration"} "window": {"type": "string"}
}, },
"additionalProperties": false, "additionalProperties": false,
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
@ -570,7 +574,7 @@
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
} }
}, },
"max_replicas_per_node": {"type": "integer"} "max_replicas_per_node": {"type": ["integer", "string"]}
}, },
"additionalProperties": false, "additionalProperties": false,
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
@ -590,7 +594,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"kind": {"type": "string"}, "kind": {"type": "string"},
"value": {"type": "number"} "value": {"type": ["number", "string"]}
}, },
"additionalProperties": false, "additionalProperties": false,
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
@ -655,7 +659,7 @@
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
"subnet": {"type": "string", "format": "subnet_ip_address"}, "subnet": {"type": "string"},
"ip_range": {"type": "string"}, "ip_range": {"type": "string"},
"gateway": {"type": "string"}, "gateway": {"type": "string"},
"aux_addresses": { "aux_addresses": {
@ -678,7 +682,7 @@
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
}, },
"external": { "external": {
"type": ["boolean", "object"], "type": ["boolean", "string", "object"],
"properties": { "properties": {
"name": { "name": {
"deprecated": true, "deprecated": true,
@ -688,9 +692,9 @@
"additionalProperties": false, "additionalProperties": false,
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
}, },
"internal": {"type": "boolean"}, "internal": {"type": ["boolean", "string"]},
"enable_ipv6": {"type": "boolean"}, "enable_ipv6": {"type": ["boolean", "string"]},
"attachable": {"type": "boolean"}, "attachable": {"type": ["boolean", "string"]},
"labels": {"$ref": "#/definitions/list_or_dict"} "labels": {"$ref": "#/definitions/list_or_dict"}
}, },
"additionalProperties": false, "additionalProperties": false,
@ -710,7 +714,7 @@
} }
}, },
"external": { "external": {
"type": ["boolean", "object"], "type": ["boolean", "string", "object"],
"properties": { "properties": {
"name": { "name": {
"deprecated": true, "deprecated": true,
@ -734,7 +738,7 @@
"environment": {"type": "string"}, "environment": {"type": "string"},
"file": {"type": "string"}, "file": {"type": "string"},
"external": { "external": {
"type": ["boolean", "object"], "type": ["boolean", "string", "object"],
"properties": { "properties": {
"name": {"type": "string"} "name": {"type": "string"}
} }
@ -762,7 +766,7 @@
"environment": {"type": "string"}, "environment": {"type": "string"},
"file": {"type": "string"}, "file": {"type": "string"},
"external": { "external": {
"type": ["boolean", "object"], "type": ["boolean", "string", "object"],
"properties": { "properties": {
"name": { "name": {
"deprecated": true, "deprecated": true,
@ -801,7 +805,7 @@
"type": "string" "type": "string"
}, },
"required": { "required": {
"type": "boolean", "type": ["boolean", "string"],
"default": true "default": true
} }
}, },
@ -824,8 +828,7 @@
"list_of_strings": { "list_of_strings": {
"type": "array", "type": "array",
"items": {"type": "string"}, "items": {"type": "string"}
"uniqueItems": true
}, },
"list_or_dict": { "list_or_dict": {
@ -839,7 +842,7 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
{"type": "array", "items": {"type": "string"}, "uniqueItems": true} {"type": "array", "items": {"type": "string"}}
] ]
}, },
@ -855,7 +858,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"path": {"type": "string"}, "path": {"type": "string"},
"weight": {"type": "integer"} "weight": {"type": ["integer", "string"]}
}, },
"additionalProperties": false "additionalProperties": false
}, },
@ -871,7 +874,7 @@
"target": {"type": "string"}, "target": {"type": "string"},
"uid": {"type": "string"}, "uid": {"type": "string"},
"gid": {"type": "string"}, "gid": {"type": "string"},
"mode": {"type": "number"} "mode": {"type": ["number", "string"]}
}, },
"additionalProperties": false, "additionalProperties": false,
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
@ -884,12 +887,12 @@
"patternProperties": { "patternProperties": {
"^[a-z]+$": { "^[a-z]+$": {
"oneOf": [ "oneOf": [
{"type": "integer"}, {"type": ["integer", "string"]},
{ {
"type": "object", "type": "object",
"properties": { "properties": {
"hard": {"type": "integer"}, "hard": {"type": ["integer", "string"]},
"soft": {"type": "integer"} "soft": {"type": ["integer", "string"]}
}, },
"required": ["soft", "hard"], "required": ["soft", "hard"],
"additionalProperties": false, "additionalProperties": false,

View File

@ -0,0 +1,123 @@
name: ${VARIABLE}
services:
foo:
deploy:
mode: ${VARIABLE}
replicas: ${VARIABLE}
rollback_config:
parallelism: ${VARIABLE}
delay: ${VARIABLE}
failure_action: ${VARIABLE}
monitor: ${VARIABLE}
max_failure_ratio: ${VARIABLE}
update_config:
parallelism: ${VARIABLE}
delay: ${VARIABLE}
failure_action: ${VARIABLE}
monitor: ${VARIABLE}
max_failure_ratio: ${VARIABLE}
resources:
limits:
memory: ${VARIABLE}
reservations:
memory: ${VARIABLE}
generic_resources:
- discrete_resource_spec:
kind: ${VARIABLE}
value: ${VARIABLE}
- discrete_resource_spec:
kind: ${VARIABLE}
value: ${VARIABLE}
restart_policy:
condition: ${VARIABLE}
delay: ${VARIABLE}
max_attempts: ${VARIABLE}
window: ${VARIABLE}
placement:
max_replicas_per_node: ${VARIABLE}
preferences:
- spread: ${VARIABLE}
endpoint_mode: ${VARIABLE}
expose:
- ${VARIABLE}
external_links:
- ${VARIABLE}
extra_hosts:
- ${VARIABLE}
hostname: ${VARIABLE}
healthcheck:
test: ${VARIABLE}
interval: ${VARIABLE}
timeout: ${VARIABLE}
retries: ${VARIABLE}
start_period: ${VARIABLE}
start_interval: ${VARIABLE}
image: ${VARIABLE}
mac_address: ${VARIABLE}
networks:
some-network:
aliases:
- ${VARIABLE}
other-network:
ipv4_address: ${VARIABLE}
ipv6_address: ${VARIABLE}
mac_address: ${VARIABLE}
ports:
- ${VARIABLE}
privileged: ${VARIABLE}
read_only: ${VARIABLE}
restart: ${VARIABLE}
secrets:
- source: ${VARIABLE}
target: ${VARIABLE}
uid: ${VARIABLE}
gid: ${VARIABLE}
mode: ${VARIABLE}
stdin_open: ${VARIABLE}
stop_grace_period: ${VARIABLE}
stop_signal: ${VARIABLE}
storage_opt:
size: ${VARIABLE}
sysctls:
net.core.somaxconn: ${VARIABLE}
tmpfs:
- ${VARIABLE}
tty: ${VARIABLE}
ulimits:
nproc: ${VARIABLE}
nofile:
soft: ${VARIABLE}
hard: ${VARIABLE}
user: ${VARIABLE}
volumes:
- ${VARIABLE}:${VARIABLE}
- type: tmpfs
target: ${VARIABLE}
tmpfs:
size: ${VARIABLE}
networks:
network:
ipam:
driver: ${VARIABLE}
config:
- subnet: ${VARIABLE}
ip_range: ${VARIABLE}
gateway: ${VARIABLE}
aux_addresses:
host1: ${VARIABLE}
external-network:
external: ${VARIABLE}
volumes:
external-volume:
external: ${VARIABLE}
configs:
config1:
external: ${VARIABLE}
secrets:
secret1:
external: ${VARIABLE}

View File

@ -25,6 +25,7 @@ var defaultValues = map[tree.Path]transformFunc{}
func init() { func init() {
defaultValues["services.*.build"] = defaultBuildContext defaultValues["services.*.build"] = defaultBuildContext
defaultValues["services.*.secrets.*"] = defaultSecretMount defaultValues["services.*.secrets.*"] = defaultSecretMount
defaultValues["services.*.ports.*"] = portDefaults
} }
// SetDefaultValues transforms a compose model to set default values to missing attributes // SetDefaultValues transforms a compose model to set default values to missing attributes

View File

@ -87,3 +87,18 @@ func encode(v any) (map[string]any, error) {
err = decoder.Decode(v) err = decoder.Decode(v)
return m, err return m, err
} }
func portDefaults(data any, _ tree.Path, _ bool) (any, error) {
switch v := data.(type) {
case map[string]any:
if _, ok := v["protocol"]; !ok {
v["protocol"] = "tcp"
}
if _, ok := v["mode"]; !ok {
v["mode"] = "ingress"
}
return v, nil
default:
return data, nil
}
}

View File

@ -1341,7 +1341,12 @@ func deriveDeepCopy_21(dst, src *NetworkConfig) {
} else { } else {
dst.Labels = nil dst.Labels = nil
} }
dst.EnableIPv6 = src.EnableIPv6 if src.EnableIPv6 == nil {
dst.EnableIPv6 = nil
} else {
dst.EnableIPv6 = new(bool)
*dst.EnableIPv6 = *src.EnableIPv6
}
if src.Extensions != nil { if src.Extensions != nil {
dst.Extensions = make(map[string]any, len(src.Extensions)) dst.Extensions = make(map[string]any, len(src.Extensions))
src.Extensions.DeepCopy(dst.Extensions) src.Extensions.DeepCopy(dst.Extensions)

View File

@ -782,9 +782,41 @@ type ExtendsConfig struct {
// SecretConfig for a secret // SecretConfig for a secret
type SecretConfig FileObjectConfig type SecretConfig FileObjectConfig
// MarshalYAML makes SecretConfig implement yaml.Marshaller
func (s SecretConfig) MarshalYAML() (interface{}, error) {
// secret content is set while loading model. Never marshall it
s.Content = ""
return FileObjectConfig(s), nil
}
// MarshalJSON makes SecretConfig implement json.Marshaller
func (s SecretConfig) MarshalJSON() ([]byte, error) {
// secret content is set while loading model. Never marshall it
s.Content = ""
return json.Marshal(FileObjectConfig(s))
}
// ConfigObjConfig is the config for the swarm "Config" object // ConfigObjConfig is the config for the swarm "Config" object
type ConfigObjConfig FileObjectConfig type ConfigObjConfig FileObjectConfig
// MarshalYAML makes ConfigObjConfig implement yaml.Marshaller
func (s ConfigObjConfig) MarshalYAML() (interface{}, error) {
// config content may have been set from environment while loading model. Marshall actual source
if s.Environment != "" {
s.Content = ""
}
return FileObjectConfig(s), nil
}
// MarshalJSON makes ConfigObjConfig implement json.Marshaller
func (s ConfigObjConfig) MarshalJSON() ([]byte, error) {
// config content may have been set from environment while loading model. Marshall actual source
if s.Environment != "" {
s.Content = ""
}
return json.Marshal(FileObjectConfig(s))
}
type IncludeConfig struct { type IncludeConfig struct {
Path StringList `yaml:"path,omitempty" json:"path,omitempty"` Path StringList `yaml:"path,omitempty" json:"path,omitempty"`
ProjectDirectory string `yaml:"project_directory,omitempty" json:"project_directory,omitempty"` ProjectDirectory string `yaml:"project_directory,omitempty" json:"project_directory,omitempty"`

View File

@ -0,0 +1,92 @@
/*
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 (
"os"
"path/filepath"
"strings"
)
// ResolveSymbolicLink converts the section of an absolute path if it is a
// symbolic link
//
// Parameters:
// - path: an absolute path
//
// Returns:
// - converted path if it has a symbolic link or the same path if there is
// no symbolic link
func ResolveSymbolicLink(path string) (string, error) {
sym, part, err := getSymbolinkLink(path)
if err != nil {
return "", err
}
if sym == "" && part == "" {
// no symbolic link detected
return path, nil
}
return strings.Replace(path, part, sym, 1), nil
}
// getSymbolinkLink parses all parts of the path and returns the
// the symbolic link part as well as the correspondent original part
// Parameters:
// - path: an absolute path
//
// Returns:
// - string section of the path that is a symbolic link
// - string correspondent path section of the symbolic link
// - An error
func getSymbolinkLink(path string) (string, string, error) {
parts := strings.Split(path, string(os.PathSeparator))
// Reconstruct the path step by step, checking each component
var currentPath string
if filepath.IsAbs(path) {
currentPath = string(os.PathSeparator)
}
for _, part := range parts {
if part == "" {
continue
}
currentPath = filepath.Join(currentPath, part)
if isSymLink := isSymbolicLink(currentPath); isSymLink {
// return symbolic link, and correspondent part
target, err := filepath.EvalSymlinks(currentPath)
if err != nil {
return "", "", err
}
return target, currentPath, nil
}
}
return "", "", nil // no symbolic link
}
// isSymbolicLink validates if the path is a symbolic link
func isSymbolicLink(path string) bool {
info, err := os.Lstat(path)
if err != nil {
return false
}
// Check if the file mode indicates a symbolic link
return info.Mode()&os.ModeSymlink != 0
}

View File

@ -32,8 +32,10 @@ func StringToBool(s string) bool {
func GetAsEqualsMap(em []string) map[string]string { func GetAsEqualsMap(em []string) map[string]string {
m := make(map[string]string) m := make(map[string]string)
for _, v := range em { for _, v := range em {
kv := strings.SplitN(v, "=", 2) key, val, found := strings.Cut(v, "=")
m[kv[0]] = kv[1] if found {
m[key] = val
}
} }
return m return m
} }

2
vendor/modules.txt vendored
View File

@ -128,7 +128,7 @@ github.com/cenkalti/backoff/v4
# github.com/cespare/xxhash/v2 v2.2.0 # github.com/cespare/xxhash/v2 v2.2.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.1.3 # github.com/compose-spec/compose-go/v2 v2.1.4
## 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