mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 18:13:42 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			743 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			743 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
   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 (
 | 
						|
	"bytes"
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"reflect"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/compose-spec/compose-go/v2/consts"
 | 
						|
	interp "github.com/compose-spec/compose-go/v2/interpolation"
 | 
						|
	"github.com/compose-spec/compose-go/v2/override"
 | 
						|
	"github.com/compose-spec/compose-go/v2/paths"
 | 
						|
	"github.com/compose-spec/compose-go/v2/schema"
 | 
						|
	"github.com/compose-spec/compose-go/v2/template"
 | 
						|
	"github.com/compose-spec/compose-go/v2/transform"
 | 
						|
	"github.com/compose-spec/compose-go/v2/tree"
 | 
						|
	"github.com/compose-spec/compose-go/v2/types"
 | 
						|
	"github.com/compose-spec/compose-go/v2/validation"
 | 
						|
	"github.com/mitchellh/mapstructure"
 | 
						|
	"github.com/sirupsen/logrus"
 | 
						|
	"gopkg.in/yaml.v3"
 | 
						|
)
 | 
						|
 | 
						|
// Options supported by Load
 | 
						|
type Options struct {
 | 
						|
	// Skip schema validation
 | 
						|
	SkipValidation bool
 | 
						|
	// Skip interpolation
 | 
						|
	SkipInterpolation bool
 | 
						|
	// Skip normalization
 | 
						|
	SkipNormalization bool
 | 
						|
	// Resolve path
 | 
						|
	ResolvePaths bool
 | 
						|
	// Convert Windows path
 | 
						|
	ConvertWindowsPaths bool
 | 
						|
	// Skip consistency check
 | 
						|
	SkipConsistencyCheck bool
 | 
						|
	// Skip extends
 | 
						|
	SkipExtends bool
 | 
						|
	// SkipInclude will ignore `include` and only load model from file(s) set by ConfigDetails
 | 
						|
	SkipInclude bool
 | 
						|
	// SkipResolveEnvironment will ignore computing `environment` for services
 | 
						|
	SkipResolveEnvironment bool
 | 
						|
	// Interpolation options
 | 
						|
	Interpolate *interp.Options
 | 
						|
	// Discard 'env_file' entries after resolving to 'environment' section
 | 
						|
	discardEnvFiles bool
 | 
						|
	// Set project projectName
 | 
						|
	projectName string
 | 
						|
	// Indicates when the projectName was imperatively set or guessed from path
 | 
						|
	projectNameImperativelySet bool
 | 
						|
	// Profiles set profiles to enable
 | 
						|
	Profiles []string
 | 
						|
	// ResourceLoaders manages support for remote resources
 | 
						|
	ResourceLoaders []ResourceLoader
 | 
						|
}
 | 
						|
 | 
						|
// ResourceLoader is a plugable remote resource resolver
 | 
						|
type ResourceLoader interface {
 | 
						|
	// Accept returns `true` is the resource reference matches ResourceLoader supported protocol(s)
 | 
						|
	Accept(path string) bool
 | 
						|
	// Load returns the path to a local copy of remote resource identified by `path`.
 | 
						|
	Load(ctx context.Context, path string) (string, error)
 | 
						|
	// Dir computes path to resource"s parent folder, made relative if possible
 | 
						|
	Dir(path string) string
 | 
						|
}
 | 
						|
 | 
						|
// RemoteResourceLoaders excludes localResourceLoader from ResourceLoaders
 | 
						|
func (o Options) RemoteResourceLoaders() []ResourceLoader {
 | 
						|
	var loaders []ResourceLoader
 | 
						|
	for i, loader := range o.ResourceLoaders {
 | 
						|
		if _, ok := loader.(localResourceLoader); ok {
 | 
						|
			if i != len(o.ResourceLoaders)-1 {
 | 
						|
				logrus.Warning("misconfiguration of ResourceLoaders: localResourceLoader should be last")
 | 
						|
			}
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		loaders = append(loaders, loader)
 | 
						|
	}
 | 
						|
	return loaders
 | 
						|
}
 | 
						|
 | 
						|
type localResourceLoader struct {
 | 
						|
	WorkingDir string
 | 
						|
}
 | 
						|
 | 
						|
func (l localResourceLoader) abs(p string) string {
 | 
						|
	if filepath.IsAbs(p) {
 | 
						|
		return p
 | 
						|
	}
 | 
						|
	return filepath.Join(l.WorkingDir, p)
 | 
						|
}
 | 
						|
 | 
						|
func (l localResourceLoader) Accept(p string) bool {
 | 
						|
	_, err := os.Stat(l.abs(p))
 | 
						|
	return err == nil
 | 
						|
}
 | 
						|
 | 
						|
func (l localResourceLoader) Load(_ context.Context, p string) (string, error) {
 | 
						|
	return l.abs(p), nil
 | 
						|
}
 | 
						|
 | 
						|
func (l localResourceLoader) Dir(path string) string {
 | 
						|
	path = l.abs(filepath.Dir(path))
 | 
						|
	rel, err := filepath.Rel(l.WorkingDir, path)
 | 
						|
	if err != nil {
 | 
						|
		return path
 | 
						|
	}
 | 
						|
	return rel
 | 
						|
}
 | 
						|
 | 
						|
func (o *Options) clone() *Options {
 | 
						|
	return &Options{
 | 
						|
		SkipValidation:             o.SkipValidation,
 | 
						|
		SkipInterpolation:          o.SkipInterpolation,
 | 
						|
		SkipNormalization:          o.SkipNormalization,
 | 
						|
		ResolvePaths:               o.ResolvePaths,
 | 
						|
		ConvertWindowsPaths:        o.ConvertWindowsPaths,
 | 
						|
		SkipConsistencyCheck:       o.SkipConsistencyCheck,
 | 
						|
		SkipExtends:                o.SkipExtends,
 | 
						|
		SkipInclude:                o.SkipInclude,
 | 
						|
		Interpolate:                o.Interpolate,
 | 
						|
		discardEnvFiles:            o.discardEnvFiles,
 | 
						|
		projectName:                o.projectName,
 | 
						|
		projectNameImperativelySet: o.projectNameImperativelySet,
 | 
						|
		Profiles:                   o.Profiles,
 | 
						|
		ResourceLoaders:            o.ResourceLoaders,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (o *Options) SetProjectName(name string, imperativelySet bool) {
 | 
						|
	o.projectName = name
 | 
						|
	o.projectNameImperativelySet = imperativelySet
 | 
						|
}
 | 
						|
 | 
						|
func (o Options) GetProjectName() (string, bool) {
 | 
						|
	return o.projectName, o.projectNameImperativelySet
 | 
						|
}
 | 
						|
 | 
						|
// serviceRef identifies a reference to a service. It's used to detect cyclic
 | 
						|
// references in "extends".
 | 
						|
type serviceRef struct {
 | 
						|
	filename string
 | 
						|
	service  string
 | 
						|
}
 | 
						|
 | 
						|
type cycleTracker struct {
 | 
						|
	loaded []serviceRef
 | 
						|
}
 | 
						|
 | 
						|
func (ct *cycleTracker) Add(filename, service string) (*cycleTracker, error) {
 | 
						|
	toAdd := serviceRef{filename: filename, service: service}
 | 
						|
	for _, loaded := range ct.loaded {
 | 
						|
		if toAdd == loaded {
 | 
						|
			// Create an error message of the form:
 | 
						|
			// Circular reference:
 | 
						|
			//   service-a in docker-compose.yml
 | 
						|
			//   extends service-b in docker-compose.yml
 | 
						|
			//   extends service-a in docker-compose.yml
 | 
						|
			errLines := []string{
 | 
						|
				"Circular reference:",
 | 
						|
				fmt.Sprintf("  %s in %s", ct.loaded[0].service, ct.loaded[0].filename),
 | 
						|
			}
 | 
						|
			for _, service := range append(ct.loaded[1:], toAdd) {
 | 
						|
				errLines = append(errLines, fmt.Sprintf("  extends %s in %s", service.service, service.filename))
 | 
						|
			}
 | 
						|
 | 
						|
			return nil, errors.New(strings.Join(errLines, "\n"))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	var branch []serviceRef
 | 
						|
	branch = append(branch, ct.loaded...)
 | 
						|
	branch = append(branch, toAdd)
 | 
						|
	return &cycleTracker{
 | 
						|
		loaded: branch,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to
 | 
						|
// the `environment` section
 | 
						|
func WithDiscardEnvFiles(opts *Options) {
 | 
						|
	opts.discardEnvFiles = true
 | 
						|
}
 | 
						|
 | 
						|
// WithSkipValidation sets the Options to skip validation when loading sections
 | 
						|
func WithSkipValidation(opts *Options) {
 | 
						|
	opts.SkipValidation = true
 | 
						|
}
 | 
						|
 | 
						|
// WithProfiles sets profiles to be activated
 | 
						|
func WithProfiles(profiles []string) func(*Options) {
 | 
						|
	return func(opts *Options) {
 | 
						|
		opts.Profiles = profiles
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
 | 
						|
// structure, and returns it.
 | 
						|
func ParseYAML(source []byte) (map[string]interface{}, error) {
 | 
						|
	r := bytes.NewReader(source)
 | 
						|
	decoder := yaml.NewDecoder(r)
 | 
						|
	m, _, err := parseYAML(decoder)
 | 
						|
	return m, err
 | 
						|
}
 | 
						|
 | 
						|
// PostProcessor is used to tweak compose model based on metadata extracted during yaml Unmarshal phase
 | 
						|
// that hardly can be implemented using go-yaml and mapstructure
 | 
						|
type PostProcessor interface {
 | 
						|
	yaml.Unmarshaler
 | 
						|
 | 
						|
	// Apply changes to compose model based on recorder metadata
 | 
						|
	Apply(interface{}) error
 | 
						|
}
 | 
						|
 | 
						|
func parseYAML(decoder *yaml.Decoder) (map[string]interface{}, PostProcessor, error) {
 | 
						|
	var cfg interface{}
 | 
						|
	processor := ResetProcessor{target: &cfg}
 | 
						|
 | 
						|
	if err := decoder.Decode(&processor); err != nil {
 | 
						|
		return nil, nil, err
 | 
						|
	}
 | 
						|
	stringMap, ok := cfg.(map[string]interface{})
 | 
						|
	if ok {
 | 
						|
		converted, err := convertToStringKeysRecursive(stringMap, "")
 | 
						|
		if err != nil {
 | 
						|
			return nil, nil, err
 | 
						|
		}
 | 
						|
		return converted.(map[string]interface{}), &processor, nil
 | 
						|
	}
 | 
						|
	cfgMap, ok := cfg.(map[interface{}]interface{})
 | 
						|
	if !ok {
 | 
						|
		return nil, nil, errors.New("Top-level object must be a mapping")
 | 
						|
	}
 | 
						|
	converted, err := convertToStringKeysRecursive(cfgMap, "")
 | 
						|
	if err != nil {
 | 
						|
		return nil, nil, err
 | 
						|
	}
 | 
						|
	return converted.(map[string]interface{}), &processor, nil
 | 
						|
}
 | 
						|
 | 
						|
// Load reads a ConfigDetails and returns a fully loaded configuration.
 | 
						|
// Deprecated: use LoadWithContext.
 | 
						|
func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
 | 
						|
	return LoadWithContext(context.Background(), configDetails, options...)
 | 
						|
}
 | 
						|
 | 
						|
// LoadWithContext reads a ConfigDetails and returns a fully loaded configuration
 | 
						|
func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
 | 
						|
	if len(configDetails.ConfigFiles) < 1 {
 | 
						|
		return nil, errors.New("No files specified")
 | 
						|
	}
 | 
						|
 | 
						|
	opts := &Options{
 | 
						|
		Interpolate: &interp.Options{
 | 
						|
			Substitute:      template.Substitute,
 | 
						|
			LookupValue:     configDetails.LookupEnv,
 | 
						|
			TypeCastMapping: interpolateTypeCastMapping,
 | 
						|
		},
 | 
						|
		ResolvePaths: true,
 | 
						|
	}
 | 
						|
 | 
						|
	for _, op := range options {
 | 
						|
		op(opts)
 | 
						|
	}
 | 
						|
	opts.ResourceLoaders = append(opts.ResourceLoaders, localResourceLoader{configDetails.WorkingDir})
 | 
						|
 | 
						|
	projectName, err := projectName(configDetails, opts)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	opts.projectName = projectName
 | 
						|
 | 
						|
	// TODO(milas): this should probably ALWAYS set (overriding any existing)
 | 
						|
	if _, ok := configDetails.Environment[consts.ComposeProjectName]; !ok && projectName != "" {
 | 
						|
		if configDetails.Environment == nil {
 | 
						|
			configDetails.Environment = map[string]string{}
 | 
						|
		}
 | 
						|
		configDetails.Environment[consts.ComposeProjectName] = projectName
 | 
						|
	}
 | 
						|
 | 
						|
	return load(ctx, configDetails, opts, nil)
 | 
						|
}
 | 
						|
 | 
						|
func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Options, ct *cycleTracker, included []string) (map[string]interface{}, error) {
 | 
						|
	var (
 | 
						|
		dict = map[string]interface{}{}
 | 
						|
		err  error
 | 
						|
	)
 | 
						|
	for _, file := range config.ConfigFiles {
 | 
						|
		fctx := context.WithValue(ctx, consts.ComposeFileKey{}, file.Filename)
 | 
						|
		if len(file.Content) == 0 && file.Config == nil {
 | 
						|
			content, err := os.ReadFile(file.Filename)
 | 
						|
			if err != nil {
 | 
						|
				return 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")
 | 
						|
			}
 | 
						|
 | 
						|
			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
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			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)
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			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
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	dict, err = transform.Canonical(dict)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	if !opts.SkipInclude {
 | 
						|
		included = append(included, config.ConfigFiles[0].Filename)
 | 
						|
		err = ApplyInclude(ctx, config, dict, opts, included)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if !opts.SkipValidation {
 | 
						|
		if err := validation.Validate(dict); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if opts.ResolvePaths {
 | 
						|
		var remotes []paths.RemoteResource
 | 
						|
		for _, loader := range opts.RemoteResourceLoaders() {
 | 
						|
			remotes = append(remotes, loader.Accept)
 | 
						|
		}
 | 
						|
		err = paths.ResolveRelativePaths(dict, config.WorkingDir, remotes)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return dict, nil
 | 
						|
}
 | 
						|
 | 
						|
func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (*types.Project, error) {
 | 
						|
	mainFile := configDetails.ConfigFiles[0].Filename
 | 
						|
	for _, f := range loaded {
 | 
						|
		if f == mainFile {
 | 
						|
			loaded = append(loaded, mainFile)
 | 
						|
			return nil, fmt.Errorf("include cycle detected:\n%s\n include %s", loaded[0], strings.Join(loaded[1:], "\n include "))
 | 
						|
		}
 | 
						|
	}
 | 
						|
	loaded = append(loaded, mainFile)
 | 
						|
 | 
						|
	includeRefs := make(map[string][]types.IncludeConfig)
 | 
						|
 | 
						|
	dict, err := loadYamlModel(ctx, configDetails, opts, &cycleTracker{}, nil)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	if len(dict) == 0 {
 | 
						|
		return nil, errors.New("empty compose file")
 | 
						|
	}
 | 
						|
 | 
						|
	project := &types.Project{
 | 
						|
		Name:        opts.projectName,
 | 
						|
		WorkingDir:  configDetails.WorkingDir,
 | 
						|
		Environment: configDetails.Environment,
 | 
						|
	}
 | 
						|
	delete(dict, "name") // project name set by yaml must be identified by caller as opts.projectName
 | 
						|
 | 
						|
	dict = groupXFieldsIntoExtensions(dict, tree.NewPath())
 | 
						|
	err = Transform(dict, project)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	if len(includeRefs) != 0 {
 | 
						|
		project.IncludeReferences = includeRefs
 | 
						|
	}
 | 
						|
 | 
						|
	if !opts.SkipNormalization {
 | 
						|
		err := Normalize(project)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if opts.ConvertWindowsPaths {
 | 
						|
		for i, service := range project.Services {
 | 
						|
			for j, volume := range service.Volumes {
 | 
						|
				service.Volumes[j] = convertVolumePath(volume)
 | 
						|
			}
 | 
						|
			project.Services[i] = service
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if !opts.SkipConsistencyCheck {
 | 
						|
		err := checkConsistency(project)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if project, err = project.WithProfiles(opts.Profiles); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	if !opts.SkipResolveEnvironment {
 | 
						|
		project, err = project.WithServicesEnvironmentResolved(opts.discardEnvFiles)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return project, nil
 | 
						|
}
 | 
						|
 | 
						|
func InvalidProjectNameErr(v string) error {
 | 
						|
	return fmt.Errorf(
 | 
						|
		"invalid project name %q: must consist only of lowercase alphanumeric characters, hyphens, and underscores as well as start with a letter or number",
 | 
						|
		v,
 | 
						|
	)
 | 
						|
}
 | 
						|
 | 
						|
// projectName determines the canonical name to use for the project considering
 | 
						|
// the loader Options as well as `name` fields in Compose YAML fields (which
 | 
						|
// also support interpolation).
 | 
						|
//
 | 
						|
// TODO(milas): restructure loading so that we don't need to re-parse the YAML
 | 
						|
// here, as it's both wasteful and makes this code error-prone.
 | 
						|
func projectName(details types.ConfigDetails, opts *Options) (string, error) {
 | 
						|
	projectName, projectNameImperativelySet := opts.GetProjectName()
 | 
						|
 | 
						|
	// if user did NOT provide a name explicitly, then see if one is defined
 | 
						|
	// in any of the config files
 | 
						|
	if !projectNameImperativelySet {
 | 
						|
		var pjNameFromConfigFile string
 | 
						|
		for _, configFile := range details.ConfigFiles {
 | 
						|
			content := configFile.Content
 | 
						|
			if content == nil {
 | 
						|
				// This can be hit when Filename is set but Content is not. One
 | 
						|
				// example is when using ToConfigFiles().
 | 
						|
				d, err := os.ReadFile(configFile.Filename)
 | 
						|
				if err != nil {
 | 
						|
					return "", fmt.Errorf("failed to read file %q: %w", configFile.Filename, err)
 | 
						|
				}
 | 
						|
				content = d
 | 
						|
			}
 | 
						|
			yml, err := ParseYAML(content)
 | 
						|
			if err != nil {
 | 
						|
				// HACK: the way that loading is currently structured, this is
 | 
						|
				// a duplicative parse just for the `name`. if it fails, we
 | 
						|
				// give up but don't return the error, knowing that it'll get
 | 
						|
				// caught downstream for us
 | 
						|
				return "", nil
 | 
						|
			}
 | 
						|
			if val, ok := yml["name"]; ok && val != "" {
 | 
						|
				sVal, ok := val.(string)
 | 
						|
				if !ok {
 | 
						|
					// HACK: see above - this is a temporary parsed version
 | 
						|
					// that hasn't been schema-validated, but we don't want
 | 
						|
					// to be the ones to actually report that, so give up,
 | 
						|
					// knowing that it'll get caught downstream for us
 | 
						|
					return "", nil
 | 
						|
				}
 | 
						|
				pjNameFromConfigFile = sVal
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if !opts.SkipInterpolation {
 | 
						|
			interpolated, err := interp.Interpolate(
 | 
						|
				map[string]interface{}{"name": pjNameFromConfigFile},
 | 
						|
				*opts.Interpolate,
 | 
						|
			)
 | 
						|
			if err != nil {
 | 
						|
				return "", err
 | 
						|
			}
 | 
						|
			pjNameFromConfigFile = interpolated["name"].(string)
 | 
						|
		}
 | 
						|
		pjNameFromConfigFile = NormalizeProjectName(pjNameFromConfigFile)
 | 
						|
		if pjNameFromConfigFile != "" {
 | 
						|
			projectName = pjNameFromConfigFile
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if projectName == "" {
 | 
						|
		return "", errors.New("project name must not be empty")
 | 
						|
	}
 | 
						|
 | 
						|
	if NormalizeProjectName(projectName) != projectName {
 | 
						|
		return "", InvalidProjectNameErr(projectName)
 | 
						|
	}
 | 
						|
 | 
						|
	return projectName, nil
 | 
						|
}
 | 
						|
 | 
						|
func NormalizeProjectName(s string) string {
 | 
						|
	r := regexp.MustCompile("[a-z0-9_-]")
 | 
						|
	s = strings.ToLower(s)
 | 
						|
	s = strings.Join(r.FindAllString(s, -1), "")
 | 
						|
	return strings.TrimLeft(s, "_-")
 | 
						|
}
 | 
						|
 | 
						|
var userDefinedKeys = []tree.Path{
 | 
						|
	"services",
 | 
						|
	"volumes",
 | 
						|
	"networks",
 | 
						|
	"secrets",
 | 
						|
	"configs",
 | 
						|
}
 | 
						|
 | 
						|
func groupXFieldsIntoExtensions(dict map[string]interface{}, p tree.Path) map[string]interface{} {
 | 
						|
	extras := map[string]interface{}{}
 | 
						|
	for key, value := range dict {
 | 
						|
		skip := false
 | 
						|
		for _, uk := range userDefinedKeys {
 | 
						|
			if uk.Matches(p) {
 | 
						|
				skip = true
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if !skip && strings.HasPrefix(key, "x-") {
 | 
						|
			extras[key] = value
 | 
						|
			delete(dict, key)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		switch v := value.(type) {
 | 
						|
		case map[string]interface{}:
 | 
						|
			dict[key] = groupXFieldsIntoExtensions(v, p.Next(key))
 | 
						|
		case []interface{}:
 | 
						|
			for i, e := range v {
 | 
						|
				if m, ok := e.(map[string]interface{}); ok {
 | 
						|
					v[i] = groupXFieldsIntoExtensions(m, p.Next(strconv.Itoa(i)))
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if len(extras) > 0 {
 | 
						|
		dict[consts.Extensions] = extras
 | 
						|
	}
 | 
						|
	return dict
 | 
						|
}
 | 
						|
 | 
						|
// Transform converts the source into the target struct with compose types transformer
 | 
						|
// and the specified transformers if any.
 | 
						|
func Transform(source interface{}, target interface{}) error {
 | 
						|
	data := mapstructure.Metadata{}
 | 
						|
	config := &mapstructure.DecoderConfig{
 | 
						|
		DecodeHook: mapstructure.ComposeDecodeHookFunc(
 | 
						|
			nameServices,
 | 
						|
			decoderHook,
 | 
						|
			cast),
 | 
						|
		Result:   target,
 | 
						|
		TagName:  "yaml",
 | 
						|
		Metadata: &data,
 | 
						|
	}
 | 
						|
	decoder, err := mapstructure.NewDecoder(config)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return decoder.Decode(source)
 | 
						|
}
 | 
						|
 | 
						|
// nameServices create implicit `name` key for convenience accessing service
 | 
						|
func nameServices(from reflect.Value, to reflect.Value) (interface{}, error) {
 | 
						|
	if to.Type() == reflect.TypeOf(types.Services{}) {
 | 
						|
		nameK := reflect.ValueOf("name")
 | 
						|
		iter := from.MapRange()
 | 
						|
		for iter.Next() {
 | 
						|
			name := iter.Key()
 | 
						|
			elem := iter.Value()
 | 
						|
			elem.Elem().SetMapIndex(nameK, name)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return from.Interface(), nil
 | 
						|
}
 | 
						|
 | 
						|
// keys need to be converted to strings for jsonschema
 | 
						|
func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
 | 
						|
	if mapping, ok := value.(map[string]interface{}); ok {
 | 
						|
		for key, entry := range mapping {
 | 
						|
			var newKeyPrefix string
 | 
						|
			if keyPrefix == "" {
 | 
						|
				newKeyPrefix = key
 | 
						|
			} else {
 | 
						|
				newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, key)
 | 
						|
			}
 | 
						|
			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
			mapping[key] = convertedEntry
 | 
						|
		}
 | 
						|
		return mapping, nil
 | 
						|
	}
 | 
						|
	if mapping, ok := value.(map[interface{}]interface{}); ok {
 | 
						|
		dict := make(map[string]interface{})
 | 
						|
		for key, entry := range mapping {
 | 
						|
			str, ok := key.(string)
 | 
						|
			if !ok {
 | 
						|
				return nil, formatInvalidKeyError(keyPrefix, key)
 | 
						|
			}
 | 
						|
			var newKeyPrefix string
 | 
						|
			if keyPrefix == "" {
 | 
						|
				newKeyPrefix = str
 | 
						|
			} else {
 | 
						|
				newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)
 | 
						|
			}
 | 
						|
			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
			dict[str] = convertedEntry
 | 
						|
		}
 | 
						|
		return dict, nil
 | 
						|
	}
 | 
						|
	if list, ok := value.([]interface{}); ok {
 | 
						|
		var convertedList []interface{}
 | 
						|
		for index, entry := range list {
 | 
						|
			newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
 | 
						|
			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
			convertedList = append(convertedList, convertedEntry)
 | 
						|
		}
 | 
						|
		return convertedList, nil
 | 
						|
	}
 | 
						|
	return value, nil
 | 
						|
}
 | 
						|
 | 
						|
func formatInvalidKeyError(keyPrefix string, key interface{}) error {
 | 
						|
	var location string
 | 
						|
	if keyPrefix == "" {
 | 
						|
		location = "at top level"
 | 
						|
	} else {
 | 
						|
		location = fmt.Sprintf("in %s", keyPrefix)
 | 
						|
	}
 | 
						|
	return fmt.Errorf("Non-string key %s: %#v", location, key)
 | 
						|
}
 | 
						|
 | 
						|
// Windows path, c:\\my\\path\\shiny, need to be changed to be compatible with
 | 
						|
// the Engine. Volume path are expected to be linux style /c/my/path/shiny/
 | 
						|
func convertVolumePath(volume types.ServiceVolumeConfig) types.ServiceVolumeConfig {
 | 
						|
	volumeName := strings.ToLower(filepath.VolumeName(volume.Source))
 | 
						|
	if len(volumeName) != 2 {
 | 
						|
		return volume
 | 
						|
	}
 | 
						|
 | 
						|
	convertedSource := fmt.Sprintf("/%c%s", volumeName[0], volume.Source[len(volumeName):])
 | 
						|
	convertedSource = strings.ReplaceAll(convertedSource, "\\", "/")
 | 
						|
 | 
						|
	volume.Source = convertedSource
 | 
						|
	return volume
 | 
						|
}
 |