mirror of https://github.com/docker/buildx.git
vendor: github.com/compose-spec/compose-go v1.18.3
- Parse service device count to int if possible - introduce ResourceResolver to accept remote resources - use include.env_file to resolve variables in included compose.yaml file - remove potential dependencies to disabled services in ForServices - ability to convert a mapping (back) to KEY=VALUE strings - load: include details about included files on Project - include disabled services - local environment to override included .env - load: move env var profile detection to option - add support for multi-document yaml full diff: https://github.com/compose-spec/compose-go/compare/v1.17.0...v1.18.3 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
31d88398bc
commit
51c94cd2a6
2
go.mod
2
go.mod
|
@ -5,7 +5,7 @@ go 1.20
|
|||
require (
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.16
|
||||
github.com/compose-spec/compose-go v1.17.0
|
||||
github.com/compose-spec/compose-go v1.18.3
|
||||
github.com/containerd/console v1.0.3
|
||||
github.com/containerd/containerd v1.7.2
|
||||
github.com/containerd/continuity v0.4.1
|
||||
|
|
4
go.sum
4
go.sum
|
@ -126,8 +126,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
|
|||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||
github.com/compose-spec/compose-go v1.17.0 h1:cvje90CU94dQyTnJoHJYjx9yE4Iggse1XmGcO3Qi5ts=
|
||||
github.com/compose-spec/compose-go v1.17.0/go.mod h1:zR2tP1+kZHi5vJz7PjpW6oMoDji/Js3GHjP+hfjf70Q=
|
||||
github.com/compose-spec/compose-go v1.18.3 h1:hiwTZ8ED1l+CB2G2G4LFv/bIaoUfG2ZBalz4S7MOy5w=
|
||||
github.com/compose-spec/compose-go v1.18.3/go.mod h1:zR2tP1+kZHi5vJz7PjpW6oMoDji/Js3GHjP+hfjf70Q=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -35,6 +36,8 @@ import (
|
|||
|
||||
// ProjectOptions provides common configuration for loading a project.
|
||||
type ProjectOptions struct {
|
||||
ctx context.Context
|
||||
|
||||
// Name is a valid Compose project name to be used or empty.
|
||||
//
|
||||
// If empty, the project loader will automatically infer a reasonable
|
||||
|
@ -63,7 +66,7 @@ type ProjectOptions struct {
|
|||
// NOTE: For security, the loader does not automatically expose any
|
||||
// process environment variables. For convenience, WithOsEnv can be
|
||||
// used if appropriate.
|
||||
Environment map[string]string
|
||||
Environment types.Mapping
|
||||
|
||||
// EnvFiles are file paths to ".env" files with additional environment
|
||||
// variable data.
|
||||
|
@ -191,7 +194,7 @@ func WithEnv(env []string) ProjectOptionsFn {
|
|||
}
|
||||
}
|
||||
|
||||
// WithDiscardEnvFiles sets discards the `env_file` section after resolving to
|
||||
// WithDiscardEnvFile sets discards the `env_file` section after resolving to
|
||||
// the `environment` section
|
||||
func WithDiscardEnvFile(o *ProjectOptions) error {
|
||||
o.loadOptions = append(o.loadOptions, loader.WithDiscardEnvFiles)
|
||||
|
@ -206,6 +209,15 @@ func WithLoadOptions(loadOptions ...func(*loader.Options)) ProjectOptionsFn {
|
|||
}
|
||||
}
|
||||
|
||||
// WithDefaultProfiles uses the provided profiles (if any), and falls back to
|
||||
// profiles specified via the COMPOSE_PROFILES environment variable otherwise.
|
||||
func WithDefaultProfiles(profile ...string) ProjectOptionsFn {
|
||||
if len(profile) == 0 {
|
||||
profile = strings.Split(os.Getenv(consts.ComposeProfiles), ",")
|
||||
}
|
||||
return WithProfiles(profile)
|
||||
}
|
||||
|
||||
// WithProfiles sets profiles to be activated
|
||||
func WithProfiles(profiles []string) ProjectOptionsFn {
|
||||
return func(o *ProjectOptions) error {
|
||||
|
@ -225,8 +237,9 @@ func WithOsEnv(o *ProjectOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// WithEnvFile set an alternate env file
|
||||
// deprecated - use WithEnvFiles
|
||||
// WithEnvFile sets an alternate env file.
|
||||
//
|
||||
// Deprecated: use WithEnvFiles instead.
|
||||
func WithEnvFile(file string) ProjectOptionsFn {
|
||||
var files []string
|
||||
if file != "" {
|
||||
|
@ -253,11 +266,7 @@ func WithDotEnv(o *ProjectOptions) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range envMap {
|
||||
if _, set := o.Environment[k]; !set {
|
||||
o.Environment[k] = v
|
||||
}
|
||||
}
|
||||
o.Environment.Merge(envMap)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -301,6 +310,24 @@ func WithResolvedPaths(resolve bool) ProjectOptionsFn {
|
|||
}
|
||||
}
|
||||
|
||||
// WithContext sets the context used to load model and resources
|
||||
func WithContext(ctx context.Context) ProjectOptionsFn {
|
||||
return func(o *ProjectOptions) error {
|
||||
o.ctx = ctx
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithResourceLoader register support for ResourceLoader to manage remote resources
|
||||
func WithResourceLoader(r loader.ResourceLoader) ProjectOptionsFn {
|
||||
return func(o *ProjectOptions) error {
|
||||
o.loadOptions = append(o.loadOptions, func(options *loader.Options) {
|
||||
options.ResourceLoaders = append(options.ResourceLoaders, r)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultFileNames defines the Compose file names for auto-discovery (in order of preference)
|
||||
var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"}
|
||||
|
||||
|
@ -367,7 +394,12 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
|
|||
withNamePrecedenceLoad(absWorkingDir, options),
|
||||
withConvertWindowsPaths(options))
|
||||
|
||||
project, err := loader.Load(types.ConfigDetails{
|
||||
ctx := options.ctx
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
project, err := loader.LoadWithContext(ctx, types.ConfigDetails{
|
||||
ConfigFiles: configs,
|
||||
WorkingDir: workingDir,
|
||||
Environment: options.Environment,
|
||||
|
|
|
@ -17,10 +17,12 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/compose-spec/compose-go/dotenv"
|
||||
interp "github.com/compose-spec/compose-go/interpolation"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
@ -43,12 +45,23 @@ var transformIncludeConfig TransformerFunc = func(data interface{}) (interface{}
|
|||
}
|
||||
}
|
||||
|
||||
func loadInclude(configDetails types.ConfigDetails, model *types.Config, options *Options, loaded []string) (*types.Config, error) {
|
||||
func loadInclude(ctx context.Context, filename string, configDetails types.ConfigDetails, model *types.Config, options *Options, loaded []string) (*types.Config, map[string][]types.IncludeConfig, error) {
|
||||
included := make(map[string][]types.IncludeConfig)
|
||||
for _, r := range model.Include {
|
||||
included[filename] = append(included[filename], r)
|
||||
|
||||
for i, p := range r.Path {
|
||||
if !filepath.IsAbs(p) {
|
||||
r.Path[i] = filepath.Join(configDetails.WorkingDir, p)
|
||||
for _, loader := range options.ResourceLoaders {
|
||||
if loader.Accept(p) {
|
||||
path, err := loader.Load(ctx, p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
p = path
|
||||
break
|
||||
}
|
||||
}
|
||||
r.Path[i] = absPath(configDetails.WorkingDir, p)
|
||||
}
|
||||
if r.ProjectDirectory == "" {
|
||||
r.ProjectDirectory = filepath.Dir(r.Path[0])
|
||||
|
@ -60,27 +73,36 @@ func loadInclude(configDetails types.ConfigDetails, model *types.Config, options
|
|||
loadOptions.SkipNormalization = true
|
||||
loadOptions.SkipConsistencyCheck = true
|
||||
|
||||
env, err := dotenv.GetEnvFromFile(configDetails.Environment, r.ProjectDirectory, r.EnvFile)
|
||||
envFromFile, err := dotenv.GetEnvFromFile(configDetails.Environment, r.ProjectDirectory, r.EnvFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
imported, err := load(types.ConfigDetails{
|
||||
config := types.ConfigDetails{
|
||||
WorkingDir: r.ProjectDirectory,
|
||||
ConfigFiles: types.ToConfigFiles(r.Path),
|
||||
Environment: env,
|
||||
}, loadOptions, loaded)
|
||||
Environment: configDetails.Environment.Clone().Merge(envFromFile),
|
||||
}
|
||||
loadOptions.Interpolate = &interp.Options{
|
||||
Substitute: options.Interpolate.Substitute,
|
||||
LookupValue: config.LookupEnv,
|
||||
TypeCastMapping: options.Interpolate.TypeCastMapping,
|
||||
}
|
||||
imported, err := load(ctx, config, loadOptions, loaded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
for k, v := range imported.IncludeReferences {
|
||||
included[k] = append(included[k], v...)
|
||||
}
|
||||
|
||||
err = importResources(model, imported, r.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
model.Include = nil
|
||||
return model, nil
|
||||
return model, included, nil
|
||||
}
|
||||
|
||||
// importResources import into model all resources defined by imported, and report error on conflict
|
||||
|
@ -92,6 +114,12 @@ func importResources(model *types.Config, imported *types.Project, path []string
|
|||
}
|
||||
model.Services = append(model.Services, service)
|
||||
}
|
||||
for _, service := range imported.DisabledServices {
|
||||
if _, ok := services[service.Name]; ok {
|
||||
return fmt.Errorf("imported compose file %s defines conflicting service %s", path, service.Name)
|
||||
}
|
||||
model.Services = append(model.Services, service)
|
||||
}
|
||||
for n, network := range imported.Networks {
|
||||
if _, ok := model.Networks[n]; ok {
|
||||
return fmt.Errorf("imported compose file %s defines conflicting network %s", path, n)
|
||||
|
|
|
@ -17,7 +17,10 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
paths "path"
|
||||
"path/filepath"
|
||||
|
@ -68,6 +71,16 @@ type Options struct {
|
|||
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)
|
||||
}
|
||||
|
||||
func (o *Options) clone() *Options {
|
||||
|
@ -85,6 +98,7 @@ func (o *Options) clone() *Options {
|
|||
projectName: o.projectName,
|
||||
projectNameImperativelySet: o.projectNameImperativelySet,
|
||||
Profiles: o.Profiles,
|
||||
ResourceLoaders: o.ResourceLoaders,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,7 +168,9 @@ func WithProfiles(profiles []string) func(*Options) {
|
|||
// 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) {
|
||||
m, _, err := parseYAML(source)
|
||||
r := bytes.NewReader(source)
|
||||
decoder := yaml.NewDecoder(r)
|
||||
m, _, err := parseYAML(decoder)
|
||||
return m, err
|
||||
}
|
||||
|
||||
|
@ -167,11 +183,11 @@ type PostProcessor interface {
|
|||
Apply(config *types.Config) error
|
||||
}
|
||||
|
||||
func parseYAML(source []byte) (map[string]interface{}, PostProcessor, error) {
|
||||
func parseYAML(decoder *yaml.Decoder) (map[string]interface{}, PostProcessor, error) {
|
||||
var cfg interface{}
|
||||
processor := ResetProcessor{target: &cfg}
|
||||
|
||||
if err := yaml.Unmarshal(source, &processor); err != nil {
|
||||
if err := decoder.Decode(&processor); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
stringMap, ok := cfg.(map[string]interface{})
|
||||
|
@ -193,8 +209,14 @@ func parseYAML(source []byte) (map[string]interface{}, PostProcessor, error) {
|
|||
return converted.(map[string]interface{}), &processor, nil
|
||||
}
|
||||
|
||||
// Load reads a ConfigDetails and returns a fully loaded configuration
|
||||
// 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.Errorf("No files specified")
|
||||
}
|
||||
|
@ -217,10 +239,10 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
|
|||
return nil, err
|
||||
}
|
||||
opts.projectName = projectName
|
||||
return load(configDetails, opts, nil)
|
||||
return load(ctx, configDetails, opts, nil)
|
||||
}
|
||||
|
||||
func load(configDetails types.ConfigDetails, opts *Options, loaded []string) (*types.Project, error) {
|
||||
func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (*types.Project, error) {
|
||||
var model *types.Config
|
||||
|
||||
mainFile := configDetails.ConfigFiles[0].Filename
|
||||
|
@ -232,9 +254,56 @@ func load(configDetails types.ConfigDetails, opts *Options, loaded []string) (*t
|
|||
}
|
||||
loaded = append(loaded, mainFile)
|
||||
|
||||
for i, file := range configDetails.ConfigFiles {
|
||||
includeRefs := make(map[string][]types.IncludeConfig)
|
||||
first := true
|
||||
for _, file := range configDetails.ConfigFiles {
|
||||
var postProcessor PostProcessor
|
||||
configDict := file.Config
|
||||
|
||||
processYaml := func() error {
|
||||
if !opts.SkipValidation {
|
||||
if err := schema.Validate(configDict); err != nil {
|
||||
return fmt.Errorf("validating %s: %w", file.Filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
configDict = groupXFieldsIntoExtensions(configDict)
|
||||
|
||||
cfg, err := loadSections(ctx, file.Filename, configDict, configDetails, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.SkipInclude {
|
||||
var included map[string][]types.IncludeConfig
|
||||
cfg, included, err = loadInclude(ctx, file.Filename, configDetails, cfg, opts, loaded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range included {
|
||||
includeRefs[k] = append(includeRefs[k], v...)
|
||||
}
|
||||
}
|
||||
|
||||
if first {
|
||||
first = false
|
||||
model = cfg
|
||||
return nil
|
||||
}
|
||||
merged, err := merge([]*types.Config{model, cfg})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if postProcessor != nil {
|
||||
err = postProcessor.Apply(merged)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
model = merged
|
||||
return nil
|
||||
}
|
||||
|
||||
if configDict == nil {
|
||||
if len(file.Content) == 0 {
|
||||
content, err := os.ReadFile(file.Filename)
|
||||
|
@ -243,52 +312,29 @@ func load(configDetails types.ConfigDetails, opts *Options, loaded []string) (*t
|
|||
}
|
||||
file.Content = content
|
||||
}
|
||||
dict, p, err := parseConfig(file.Content, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing %s: %w", file.Filename, err)
|
||||
|
||||
r := bytes.NewReader(file.Content)
|
||||
decoder := yaml.NewDecoder(r)
|
||||
for {
|
||||
dict, p, err := parseConfig(decoder, opts)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return nil, fmt.Errorf("parsing %s: %w", file.Filename, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
configDict = dict
|
||||
postProcessor = p
|
||||
|
||||
if err := processYaml(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
configDict = dict
|
||||
file.Config = dict
|
||||
configDetails.ConfigFiles[i] = file
|
||||
postProcessor = p
|
||||
}
|
||||
|
||||
if !opts.SkipValidation {
|
||||
if err := schema.Validate(configDict); err != nil {
|
||||
return nil, fmt.Errorf("validating %s: %w", file.Filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
configDict = groupXFieldsIntoExtensions(configDict)
|
||||
|
||||
cfg, err := loadSections(file.Filename, configDict, configDetails, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !opts.SkipInclude {
|
||||
cfg, err = loadInclude(configDetails, cfg, opts, loaded)
|
||||
if err != nil {
|
||||
} else {
|
||||
if err := processYaml(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
model = cfg
|
||||
continue
|
||||
}
|
||||
|
||||
merged, err := merge([]*types.Config{model, cfg})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if postProcessor != nil {
|
||||
err = postProcessor.Apply(merged)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
model = merged
|
||||
}
|
||||
|
||||
project := &types.Project{
|
||||
|
@ -303,6 +349,10 @@ func load(configDetails types.ConfigDetails, opts *Options, loaded []string) (*t
|
|||
Extensions: model.Extensions,
|
||||
}
|
||||
|
||||
if len(includeRefs) != 0 {
|
||||
project.IncludeReferences = includeRefs
|
||||
}
|
||||
|
||||
if !opts.SkipNormalization {
|
||||
err := Normalize(project)
|
||||
if err != nil {
|
||||
|
@ -333,9 +383,6 @@ func load(configDetails types.ConfigDetails, opts *Options, loaded []string) (*t
|
|||
}
|
||||
}
|
||||
|
||||
if profiles, ok := project.Environment[consts.ComposeProfiles]; ok && len(opts.Profiles) == 0 {
|
||||
opts.Profiles = strings.Split(profiles, ",")
|
||||
}
|
||||
project.ApplyProfiles(opts.Profiles)
|
||||
|
||||
err := project.ResolveServicesEnvironment(opts.discardEnvFiles)
|
||||
|
@ -422,8 +469,8 @@ func NormalizeProjectName(s string) string {
|
|||
return strings.TrimLeft(s, "_-")
|
||||
}
|
||||
|
||||
func parseConfig(b []byte, opts *Options) (map[string]interface{}, PostProcessor, error) {
|
||||
yml, postProcessor, err := parseYAML(b)
|
||||
func parseConfig(decoder *yaml.Decoder, opts *Options) (map[string]interface{}, PostProcessor, error) {
|
||||
yml, postProcessor, err := parseYAML(decoder)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -453,7 +500,7 @@ func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interfac
|
|||
return dict
|
||||
}
|
||||
|
||||
func loadSections(filename string, config map[string]interface{}, configDetails types.ConfigDetails, opts *Options) (*types.Config, error) {
|
||||
func loadSections(ctx context.Context, filename string, config map[string]interface{}, configDetails types.ConfigDetails, opts *Options) (*types.Config, error) {
|
||||
var err error
|
||||
cfg := types.Config{
|
||||
Filename: filename,
|
||||
|
@ -466,7 +513,7 @@ func loadSections(filename string, config map[string]interface{}, configDetails
|
|||
}
|
||||
}
|
||||
cfg.Name = name
|
||||
cfg.Services, err = LoadServices(filename, getSection(config, "services"), configDetails.WorkingDir, configDetails.LookupEnv, opts)
|
||||
cfg.Services, err = LoadServices(ctx, filename, getSection(config, "services"), configDetails.WorkingDir, configDetails.LookupEnv, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -659,7 +706,7 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error {
|
|||
|
||||
// LoadServices produces a ServiceConfig map from a compose file Dict
|
||||
// the servicesDict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadServices(filename string, servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, opts *Options) ([]types.ServiceConfig, error) {
|
||||
func LoadServices(ctx context.Context, filename string, servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, opts *Options) ([]types.ServiceConfig, error) {
|
||||
var services []types.ServiceConfig
|
||||
|
||||
x, ok := servicesDict[extensions]
|
||||
|
@ -672,7 +719,7 @@ func LoadServices(filename string, servicesDict map[string]interface{}, workingD
|
|||
}
|
||||
|
||||
for name := range servicesDict {
|
||||
serviceConfig, err := loadServiceWithExtends(filename, name, servicesDict, workingDir, lookupEnv, opts, &cycleTracker{})
|
||||
serviceConfig, err := loadServiceWithExtends(ctx, filename, name, servicesDict, workingDir, lookupEnv, opts, &cycleTracker{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -683,7 +730,7 @@ func LoadServices(filename string, servicesDict map[string]interface{}, workingD
|
|||
return services, nil
|
||||
}
|
||||
|
||||
func loadServiceWithExtends(filename, name string, servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, opts *Options, ct *cycleTracker) (*types.ServiceConfig, error) {
|
||||
func loadServiceWithExtends(ctx context.Context, filename, name string, servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, opts *Options, ct *cycleTracker) (*types.ServiceConfig, error) {
|
||||
if err := ct.Add(filename, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -707,11 +754,21 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
|
|||
var baseService *types.ServiceConfig
|
||||
file := serviceConfig.Extends.File
|
||||
if file == "" {
|
||||
baseService, err = loadServiceWithExtends(filename, baseServiceName, servicesDict, workingDir, lookupEnv, opts, ct)
|
||||
baseService, err = loadServiceWithExtends(ctx, filename, baseServiceName, servicesDict, workingDir, lookupEnv, opts, ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
for _, loader := range opts.ResourceLoaders {
|
||||
if loader.Accept(file) {
|
||||
path, err := loader.Load(ctx, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file = path
|
||||
break
|
||||
}
|
||||
}
|
||||
// Resolve the path to the imported file, and load it.
|
||||
baseFilePath := absPath(workingDir, file)
|
||||
|
||||
|
@ -720,13 +777,16 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
|
|||
return nil, err
|
||||
}
|
||||
|
||||
baseFile, _, err := parseConfig(b, opts)
|
||||
r := bytes.NewReader(b)
|
||||
decoder := yaml.NewDecoder(r)
|
||||
|
||||
baseFile, _, err := parseConfig(decoder, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseFileServices := getSection(baseFile, "services")
|
||||
baseService, err = loadServiceWithExtends(baseFilePath, baseServiceName, baseFileServices, filepath.Dir(baseFilePath), lookupEnv, opts, ct)
|
||||
baseService, err = loadServiceWithExtends(ctx, baseFilePath, baseServiceName, baseFileServices, filepath.Dir(baseFilePath), lookupEnv, opts, ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1038,7 +1098,12 @@ var transformServiceDeviceRequest TransformerFunc = func(data interface{}) (inte
|
|||
value["count"] = -1
|
||||
return value, nil
|
||||
}
|
||||
return data, errors.Errorf("invalid string value for 'count' (the only value allowed is 'all')")
|
||||
i, err := strconv.ParseInt(val, 10, 64)
|
||||
if err == nil {
|
||||
value["count"] = i
|
||||
return value, nil
|
||||
}
|
||||
return data, errors.Errorf("invalid string value for 'count' (the only value allowed is 'all' or a number)")
|
||||
default:
|
||||
return data, errors.Errorf("invalid type %T for device count", val)
|
||||
}
|
||||
|
|
|
@ -64,6 +64,25 @@ func ResolveRelativePaths(project *types.Project) error {
|
|||
project.Volumes[name] = config
|
||||
}
|
||||
}
|
||||
|
||||
// don't coerce a nil map to an empty map
|
||||
if project.IncludeReferences != nil {
|
||||
absIncludes := make(map[string][]types.IncludeConfig, len(project.IncludeReferences))
|
||||
for filename, config := range project.IncludeReferences {
|
||||
filename = absPath(project.WorkingDir, filename)
|
||||
absConfigs := make([]types.IncludeConfig, len(config))
|
||||
for i, c := range config {
|
||||
absConfigs[i] = types.IncludeConfig{
|
||||
Path: resolvePaths(project.WorkingDir, c.Path),
|
||||
ProjectDirectory: absPath(project.WorkingDir, c.ProjectDirectory),
|
||||
EnvFile: resolvePaths(project.WorkingDir, c.EnvFile),
|
||||
}
|
||||
}
|
||||
absIncludes[filename] = absConfigs
|
||||
}
|
||||
project.IncludeReferences = absIncludes
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -133,3 +152,14 @@ func isRemoteContext(maybeURL string) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func resolvePaths(basePath string, in types.StringList) types.StringList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
ret := make(types.StringList, len(in))
|
||||
for i := range in {
|
||||
ret[i] = absPath(basePath, in[i])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ type ConfigDetails struct {
|
|||
Version string
|
||||
WorkingDir string
|
||||
ConfigFiles []ConfigFile
|
||||
Environment map[string]string
|
||||
Environment Mapping
|
||||
}
|
||||
|
||||
// LookupEnv provides a lookup function for environment variables
|
||||
|
|
|
@ -36,16 +36,22 @@ import (
|
|||
|
||||
// Project is the result of loading a set of compose files
|
||||
type Project struct {
|
||||
Name string `yaml:"name,omitempty" json:"name,omitempty"`
|
||||
WorkingDir string `yaml:"-" json:"-"`
|
||||
Services Services `yaml:"services" json:"services"`
|
||||
Networks Networks `yaml:"networks,omitempty" json:"networks,omitempty"`
|
||||
Volumes Volumes `yaml:"volumes,omitempty" json:"volumes,omitempty"`
|
||||
Secrets Secrets `yaml:"secrets,omitempty" json:"secrets,omitempty"`
|
||||
Configs Configs `yaml:"configs,omitempty" json:"configs,omitempty"`
|
||||
Extensions Extensions `yaml:"#extensions,inline" json:"-"` // https://github.com/golang/go/issues/6213
|
||||
ComposeFiles []string `yaml:"-" json:"-"`
|
||||
Environment Mapping `yaml:"-" json:"-"`
|
||||
Name string `yaml:"name,omitempty" json:"name,omitempty"`
|
||||
WorkingDir string `yaml:"-" json:"-"`
|
||||
Services Services `yaml:"services" json:"services"`
|
||||
Networks Networks `yaml:"networks,omitempty" json:"networks,omitempty"`
|
||||
Volumes Volumes `yaml:"volumes,omitempty" json:"volumes,omitempty"`
|
||||
Secrets Secrets `yaml:"secrets,omitempty" json:"secrets,omitempty"`
|
||||
Configs Configs `yaml:"configs,omitempty" json:"configs,omitempty"`
|
||||
Extensions Extensions `yaml:"#extensions,inline" json:"-"` // https://github.com/golang/go/issues/6213
|
||||
|
||||
// IncludeReferences is keyed by Compose YAML filename and contains config for
|
||||
// other Compose YAML files it directly triggered a load of via `include`.
|
||||
//
|
||||
// Note: this is
|
||||
IncludeReferences map[string][]IncludeConfig `yaml:"-" json:"-"`
|
||||
ComposeFiles []string `yaml:"-" json:"-"`
|
||||
Environment Mapping `yaml:"-" json:"-"`
|
||||
|
||||
// DisabledServices track services which have been disable as profile is not active
|
||||
DisabledServices Services `yaml:"-" json:"-"`
|
||||
|
@ -423,13 +429,24 @@ func (p *Project) ForServices(names []string, options ...DependencyOption) error
|
|||
}
|
||||
enabled = append(enabled, s)
|
||||
} else {
|
||||
p.DisabledServices = append(p.DisabledServices, s)
|
||||
p.DisableService(s)
|
||||
}
|
||||
}
|
||||
p.Services = enabled
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Project) DisableService(service ServiceConfig) {
|
||||
// We should remove all dependencies which reference the disabled service
|
||||
for i, s := range p.Services {
|
||||
if _, ok := s.DependsOn[service.Name]; ok {
|
||||
delete(s.DependsOn, service.Name)
|
||||
p.Services[i] = s
|
||||
}
|
||||
}
|
||||
p.DisabledServices = append(p.DisabledServices, service)
|
||||
}
|
||||
|
||||
// ResolveImages updates services images to include digest computed by a resolver function
|
||||
func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.Digest, error)) error {
|
||||
eg := errgroup.Group{}
|
||||
|
|
|
@ -491,6 +491,16 @@ func NewMapping(values []string) Mapping {
|
|||
return mapping
|
||||
}
|
||||
|
||||
// convert values into a set of KEY=VALUE strings
|
||||
func (m Mapping) Values() []string {
|
||||
values := make([]string, 0, len(m))
|
||||
for k, v := range m {
|
||||
values = append(values, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
sort.Strings(values)
|
||||
return values
|
||||
}
|
||||
|
||||
// ToMappingWithEquals converts Mapping into a MappingWithEquals with pointer references
|
||||
func (m Mapping) ToMappingWithEquals() MappingWithEquals {
|
||||
mapping := MappingWithEquals{}
|
||||
|
@ -506,6 +516,24 @@ func (m Mapping) Resolve(s string) (string, bool) {
|
|||
return v, ok
|
||||
}
|
||||
|
||||
func (m Mapping) Clone() Mapping {
|
||||
clone := Mapping{}
|
||||
for k, v := range m {
|
||||
clone[k] = v
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// Merge adds all values from second mapping which are not already defined
|
||||
func (m Mapping) Merge(o Mapping) Mapping {
|
||||
for k, v := range o {
|
||||
if _, set := m[k]; !set {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Labels is a mapping type for labels
|
||||
type Labels map[string]string
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ github.com/cenkalti/backoff/v4
|
|||
# github.com/cespare/xxhash/v2 v2.2.0
|
||||
## explicit; go 1.11
|
||||
github.com/cespare/xxhash/v2
|
||||
# github.com/compose-spec/compose-go v1.17.0
|
||||
# github.com/compose-spec/compose-go v1.18.3
|
||||
## explicit; go 1.19
|
||||
github.com/compose-spec/compose-go/cli
|
||||
github.com/compose-spec/compose-go/consts
|
||||
|
|
Loading…
Reference in New Issue