bump compose-go to v2.1.4

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
This commit is contained in:
Guillaume Lours
2024-07-17 16:57:39 +02:00
parent 3005743f7c
commit ca452c47d8
18 changed files with 625 additions and 220 deletions

View File

@ -22,9 +22,14 @@ import (
"github.com/compose-spec/compose-go/v2/types"
)
// Will update the environment variables for the format {- VAR} (without interpolation)
// This function should resolve context environment vars for include (passed in env_file)
func resolveServicesEnvironment(dict map[string]any, config types.ConfigDetails) {
// ResolveEnvironment update the environment variables for the format {- VAR} (without interpolation)
func ResolveEnvironment(dict map[string]any, environment types.Mapping) {
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)
if !ok {
return
@ -45,7 +50,7 @@ func resolveServicesEnvironment(dict map[string]any, config types.ConfigDetails)
if !ok {
continue
}
if found, ok := config.Environment[varEnv]; ok {
if found, ok := environment[varEnv]; ok {
envs = append(envs, fmt.Sprintf("%s=%s", varEnv, found))
} else {
// 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
}
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"
"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/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"
)
@ -67,25 +71,43 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
)
switch v := extends.(type) {
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)
file = v["file"]
opts.ProcessEvent("extends", v)
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
opts.ProcessEvent("extends", map[string]any{"service": ref})
}
var base any
var (
base any
processor PostProcessor
)
if file != nil {
filename = file.(string)
services, err = getExtendsBaseFromFile(ctx, ref, filename, opts, tracker)
refFilename := file.(string)
services, processor, err = getExtendsBaseFromFile(ctx, name, ref, filename, refFilename, opts, tracker)
post = append(post, processor)
if err != nil {
return nil, err
}
filename = refFilename
} else {
_, ok := services[ref]
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
}
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 {
if !loader.Accept(path) {
if !loader.Accept(refPath) {
continue
}
local, err := loader.Load(ctx, path)
local, err := loader.Load(ctx, refPath)
if err != nil {
return nil, err
return nil, nil, err
}
localdir := filepath.Dir(local)
relworkingdir := loader.Dir(path)
relworkingdir := loader.Dir(refPath)
extendsOpts := opts.clone()
// replace localResourceLoader with a new flavour, using extended file base path
extendsOpts.ResourceLoaders = append(opts.RemoteResourceLoaders(), localResourceLoader{
WorkingDir: localdir,
})
extendsOpts.ResolvePaths = true
extendsOpts.ResolvePaths = false // we do relative path resolution after file has been loaded
extendsOpts.SkipNormalization = true
extendsOpts.SkipConsistencyCheck = true
extendsOpts.SkipInclude = true
extendsOpts.SkipExtends = true // we manage extends recursively based on raw service definition
extendsOpts.SkipValidation = true // we validate the merge result
extendsOpts.SkipDefaultValues = true
source, err := loadYamlModel(ctx, types.ConfigDetails{
WorkingDir: relworkingdir,
ConfigFiles: []types.ConfigFile{
{Filename: local},
},
}, extendsOpts, ct, nil)
source, processor, err := loadYamlFile(ctx, types.ConfigFile{Filename: local},
extendsOpts, relworkingdir, nil, ct, map[string]any{}, nil)
if err != nil {
return nil, err
return nil, nil, err
}
services := source["services"].(map[string]any)
_, ok := services[name]
_, ok := services[ref]
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 {

View File

@ -30,7 +30,7 @@ import (
)
// 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 {
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
err := Transform(source, &requires)
return requires, err
}
func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model map[string]any, options *Options, included []string) error {
includeConfig, err := loadIncludeConfig(model["include"])
func ApplyInclude(ctx context.Context, workingDir string, environment types.Mapping, model map[string]any, options *Options, included []string) error {
includeConfig, err := loadIncludeConfig(model["include"], options)
if err != nil {
return err
}
for _, r := range includeConfig {
for _, listener := range options.Listeners {
listener("include", map[string]any{
"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)
case !filepath.IsAbs(r.ProjectDirectory):
relworkingdir = loader.Dir(r.ProjectDirectory)
r.ProjectDirectory = filepath.Join(configDetails.WorkingDir, r.ProjectDirectory)
r.ProjectDirectory = filepath.Join(workingDir, r.ProjectDirectory)
default:
relworkingdir = r.ProjectDirectory
@ -117,7 +128,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
envFile := []string{}
for _, f := range r.EnvFile {
if !filepath.IsAbs(f) {
f = filepath.Join(configDetails.WorkingDir, f)
f = filepath.Join(workingDir, f)
s, err := os.Stat(f)
if err != nil {
return err
@ -131,7 +142,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
r.EnvFile = envFile
}
envFromFile, err := dotenv.GetEnvFromFile(configDetails.Environment, r.EnvFile)
envFromFile, err := dotenv.GetEnvFromFile(environment, r.EnvFile)
if err != nil {
return err
}
@ -139,7 +150,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
config := types.ConfigDetails{
WorkingDir: relworkingdir,
ConfigFiles: types.ToConfigFiles(r.Path),
Environment: configDetails.Environment.Clone().Merge(envFromFile),
Environment: environment.Clone().Merge(envFromFile),
}
loadOptions.Interpolate = &interp.Options{
Substitute: options.Interpolate.Substitute,

View File

@ -89,7 +89,7 @@ var versionWarning []string
func (o *Options) warnObsoleteVersion(file string) {
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)
}
@ -358,100 +358,19 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
dict = map[string]interface{}{}
err error
)
workingDir, environment := config.WorkingDir, config.Environment
for _, file := range config.ConfigFiles {
fctx := 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, err
}
file.Content = content
dict, _, err = loadYamlFile(ctx, file, opts, workingDir, environment, ct, dict, included)
if err != nil {
return nil, err
}
}
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")
}
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
}
if opts.Interpolate != nil && !opts.SkipInterpolation {
dict, err = interp.Interpolate(dict, *opts.Interpolate)
if err != nil {
return nil, err
}
}
@ -460,7 +379,6 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
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)
if err != nil {
return nil, err
@ -489,11 +407,98 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
return nil, err
}
}
resolveServicesEnvironment(dict, config)
ResolveEnvironment(dict, config.Environment)
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) {
mainFile := configDetails.ConfigFiles[0].Filename
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 {
build["args"], _ = resolve(a, fn)
build["args"], _ = resolve(a, fn, false)
}
service["build"] = build
}
if e, ok := service["environment"]; ok {
service["environment"], _ = resolve(e, fn)
service["environment"], _ = resolve(e, fn, true)
}
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) {
case []any:
var resolved []any
for _, val := range v {
if r, ok := resolve(val, fn); ok {
if r, ok := resolve(val, fn, keepEmpty); ok {
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 {
resolved[key] = s
} else if keepEmpty {
resolved[key] = nil
}
}
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 {
return fmt.Sprintf("%s=%s", v, val), true
}
if keepEmpty {
return v, true
}
return "", false
}
return v, true