mirror of https://github.com/docker/buildx.git
Merge pull request #2391 from crazy-max/update-compose
vendor: update compose-go to v2.0.2
This commit is contained in:
commit
e40c630758
2
go.mod
2
go.mod
|
@ -6,7 +6,7 @@ require (
|
|||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/Microsoft/go-winio v0.6.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6
|
||||
github.com/compose-spec/compose-go/v2 v2.0.0-rc.8
|
||||
github.com/compose-spec/compose-go/v2 v2.0.2
|
||||
github.com/containerd/console v1.0.4
|
||||
github.com/containerd/containerd v1.7.14
|
||||
github.com/containerd/continuity v0.4.3
|
||||
|
|
4
go.sum
4
go.sum
|
@ -88,8 +88,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/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/compose-spec/compose-go/v2 v2.0.0-rc.8 h1:b7l+GqFF+2W4M4kLQUDRTGhqmTiRwT3bYd9X7xrxp5Q=
|
||||
github.com/compose-spec/compose-go/v2 v2.0.0-rc.8/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc=
|
||||
github.com/compose-spec/compose-go/v2 v2.0.2 h1:zhXMV7VWI00Su0LdKt8/sxeXxcjLWhmGmpEyw+ZYznI=
|
||||
github.com/compose-spec/compose-go/v2 v2.0.2/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc=
|
||||
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/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
|
||||
|
|
|
@ -217,7 +217,10 @@ func WithLoadOptions(loadOptions ...func(*loader.Options)) ProjectOptionsFn {
|
|||
// 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), ",")
|
||||
for _, s := range strings.Split(os.Getenv(consts.ComposeProfiles), ",") {
|
||||
profile = append(profile, strings.TrimSpace(s))
|
||||
}
|
||||
|
||||
}
|
||||
return WithProfiles(profile)
|
||||
}
|
||||
|
@ -379,7 +382,7 @@ var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.y
|
|||
// DefaultOverrideFileNames defines the Compose override file names for auto-discovery (in order of preference)
|
||||
var DefaultOverrideFileNames = []string{"compose.override.yml", "compose.override.yaml", "docker-compose.override.yml", "docker-compose.override.yaml"}
|
||||
|
||||
func (o ProjectOptions) GetWorkingDir() (string, error) {
|
||||
func (o *ProjectOptions) GetWorkingDir() (string, error) {
|
||||
if o.WorkingDir != "" {
|
||||
return filepath.Abs(o.WorkingDir)
|
||||
}
|
||||
|
@ -395,7 +398,7 @@ func (o ProjectOptions) GetWorkingDir() (string, error) {
|
|||
return os.Getwd()
|
||||
}
|
||||
|
||||
func (o ProjectOptions) GeConfigFiles() ([]types.ConfigFile, error) {
|
||||
func (o *ProjectOptions) GeConfigFiles() ([]types.ConfigFile, error) {
|
||||
configPaths, err := o.getConfigPaths()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -427,39 +430,66 @@ func (o ProjectOptions) GeConfigFiles() ([]types.ConfigFile, error) {
|
|||
return configs, err
|
||||
}
|
||||
|
||||
// ProjectFromOptions load a compose project based on command line options
|
||||
func ProjectFromOptions(ctx context.Context, options *ProjectOptions) (*types.Project, error) {
|
||||
configs, err := options.GeConfigFiles()
|
||||
// LoadProject loads compose file according to options and bind to types.Project go structs
|
||||
func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error) {
|
||||
configDetails, err := o.prepare()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workingDir, err := options.GetWorkingDir()
|
||||
project, err := loader.LoadWithContext(ctx, configDetails, o.loadOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
options.loadOptions = append(options.loadOptions,
|
||||
withNamePrecedenceLoad(workingDir, options),
|
||||
withConvertWindowsPaths(options),
|
||||
withListeners(options))
|
||||
|
||||
project, err := loader.LoadWithContext(ctx, types.ConfigDetails{
|
||||
ConfigFiles: configs,
|
||||
WorkingDir: workingDir,
|
||||
Environment: options.Environment,
|
||||
}, options.loadOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
for _, config := range configDetails.ConfigFiles {
|
||||
project.ComposeFiles = append(project.ComposeFiles, config.Filename)
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
// LoadModel loads compose file according to options and returns a raw (yaml tree) model
|
||||
func (o *ProjectOptions) LoadModel(ctx context.Context) (map[string]any, error) {
|
||||
configDetails, err := o.prepare()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return loader.LoadModelWithContext(ctx, configDetails, o.loadOptions...)
|
||||
}
|
||||
|
||||
// prepare converts ProjectOptions into loader's types.ConfigDetails and configures default load options
|
||||
func (o *ProjectOptions) prepare() (types.ConfigDetails, error) {
|
||||
configs, err := o.GeConfigFiles()
|
||||
if err != nil {
|
||||
return types.ConfigDetails{}, err
|
||||
}
|
||||
|
||||
workingDir, err := o.GetWorkingDir()
|
||||
if err != nil {
|
||||
return types.ConfigDetails{}, err
|
||||
}
|
||||
|
||||
configDetails := types.ConfigDetails{
|
||||
ConfigFiles: configs,
|
||||
WorkingDir: workingDir,
|
||||
Environment: o.Environment,
|
||||
}
|
||||
|
||||
o.loadOptions = append(o.loadOptions,
|
||||
withNamePrecedenceLoad(workingDir, o),
|
||||
withConvertWindowsPaths(o),
|
||||
withListeners(o))
|
||||
return configDetails, nil
|
||||
}
|
||||
|
||||
// ProjectFromOptions load a compose project based on command line options
|
||||
// Deprecated: use ProjectOptions.LoadProject or ProjectOptions.LoadModel
|
||||
func ProjectFromOptions(ctx context.Context, options *ProjectOptions) (*types.Project, error) {
|
||||
return options.LoadProject(ctx)
|
||||
}
|
||||
|
||||
func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(*loader.Options) {
|
||||
return func(opts *loader.Options) {
|
||||
if options.Name != "" {
|
||||
|
|
|
@ -30,6 +30,9 @@ var (
|
|||
|
||||
// ErrIncompatible is returned when a compose project uses an incompatible attribute
|
||||
ErrIncompatible = errors.New("incompatible attribute")
|
||||
|
||||
// ErrDisabled is returned when a resource was found in model but is disabled
|
||||
ErrDisabled = errors.New("disabled")
|
||||
)
|
||||
|
||||
// IsNotFoundError returns true if the unwrapped error is ErrNotFound
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
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 graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/compose-spec/compose-go/v2/utils"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// CheckCycle analyze project's depends_on relation and report an error on cycle detection
|
||||
func CheckCycle(project *types.Project) error {
|
||||
g, err := newGraph(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.checkCycle()
|
||||
}
|
||||
|
||||
func (g *graph[T]) checkCycle() error {
|
||||
// iterate on vertices in a name-order to render a predicable error message
|
||||
// this is required by tests and enforce command reproducibility by user, which otherwise could be confusing
|
||||
names := utils.MapKeys(g.vertices)
|
||||
for _, name := range names {
|
||||
err := searchCycle([]string{name}, g.vertices[name])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func searchCycle[T any](path []string, v *vertex[T]) error {
|
||||
names := utils.MapKeys(v.children)
|
||||
for _, name := range names {
|
||||
if i := slices.Index(path, name); i > 0 {
|
||||
return fmt.Errorf("dependency cycle detected: %s", strings.Join(path[i:], " -> "))
|
||||
}
|
||||
ch := v.children[name]
|
||||
err := searchCycle(append(path, name), ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -16,14 +16,6 @@
|
|||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/utils"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// graph represents project as service dependencies
|
||||
type graph[T any] struct {
|
||||
vertices map[string]*vertex[T]
|
||||
|
@ -72,34 +64,6 @@ func (g *graph[T]) leaves() []*vertex[T] {
|
|||
return res
|
||||
}
|
||||
|
||||
func (g *graph[T]) checkCycle() error {
|
||||
// iterate on vertices in a name-order to render a predicable error message
|
||||
// this is required by tests and enforce command reproducibility by user, which otherwise could be confusing
|
||||
names := utils.MapKeys(g.vertices)
|
||||
for _, name := range names {
|
||||
err := searchCycle([]string{name}, g.vertices[name])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func searchCycle[T any](path []string, v *vertex[T]) error {
|
||||
names := utils.MapKeys(v.children)
|
||||
for _, name := range names {
|
||||
if i := slices.Index(path, name); i > 0 {
|
||||
return fmt.Errorf("dependency cycle detected: %s", strings.Join(path[i:], " -> "))
|
||||
}
|
||||
ch := v.children[name]
|
||||
err := searchCycle(append(path, name), ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// descendents return all descendents for a vertex, might contain duplicates
|
||||
func (v *vertex[T]) descendents() []string {
|
||||
var vx []string
|
||||
|
|
|
@ -20,7 +20,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/consts"
|
||||
"github.com/compose-spec/compose-go/v2/override"
|
||||
|
@ -106,11 +105,6 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
|
|||
}
|
||||
source := deepClone(base).(map[string]any)
|
||||
|
||||
err = validateExtendSource(source, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, processor := range post {
|
||||
processor.Apply(map[string]any{
|
||||
"services": map[string]any{
|
||||
|
@ -127,30 +121,6 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
|
|||
return merged, nil
|
||||
}
|
||||
|
||||
// validateExtendSource check the source for `extends` doesn't refer to another container/service
|
||||
func validateExtendSource(source map[string]any, ref string) error {
|
||||
forbidden := []string{"links", "volumes_from", "depends_on"}
|
||||
for _, key := range forbidden {
|
||||
if _, ok := source[key]; ok {
|
||||
return fmt.Errorf("service %q can't be used with `extends` as it declare `%s`", ref, key)
|
||||
}
|
||||
}
|
||||
|
||||
sharedNamespace := []string{"network_mode", "ipc", "pid", "net", "cgroup", "userns_mode", "uts"}
|
||||
for _, key := range sharedNamespace {
|
||||
if v, ok := source[key]; ok {
|
||||
val := v.(string)
|
||||
if strings.HasPrefix(val, types.ContainerPrefix) {
|
||||
return fmt.Errorf("service %q can't be used with `extends` as it shares `%s` with another container", ref, key)
|
||||
}
|
||||
if strings.HasPrefix(val, types.ServicePrefix) {
|
||||
return fmt.Errorf("service %q can't be used with `extends` as it shares `%s` with another service", ref, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getExtendsBaseFromFile(ctx context.Context, name string, path string, opts *Options, ct *cycleTracker) (map[string]any, error) {
|
||||
for _, loader := range opts.ResourceLoaders {
|
||||
if !loader.Accept(path) {
|
||||
|
|
|
@ -60,43 +60,41 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
|
|||
})
|
||||
}
|
||||
|
||||
var relworkingdir string
|
||||
for i, p := range r.Path {
|
||||
for _, loader := range options.ResourceLoaders {
|
||||
if loader.Accept(p) {
|
||||
path, err := loader.Load(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
if !loader.Accept(p) {
|
||||
continue
|
||||
}
|
||||
path, err := loader.Load(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p = path
|
||||
|
||||
if i == 0 { // This is the "main" file, used to define project-directory. Others are overrides
|
||||
relworkingdir = loader.Dir(path)
|
||||
if r.ProjectDirectory == "" {
|
||||
r.ProjectDirectory = filepath.Dir(path)
|
||||
}
|
||||
|
||||
for _, f := range included {
|
||||
if f == path {
|
||||
included = append(included, path)
|
||||
return fmt.Errorf("include cycle detected:\n%s\n include %s", included[0], strings.Join(included[1:], "\n include "))
|
||||
}
|
||||
}
|
||||
p = path
|
||||
break
|
||||
}
|
||||
}
|
||||
r.Path[i] = p
|
||||
}
|
||||
|
||||
mainFile := r.Path[0]
|
||||
for _, f := range included {
|
||||
if f == mainFile {
|
||||
included = append(included, mainFile)
|
||||
return fmt.Errorf("include cycle detected:\n%s\n include %s", included[0], strings.Join(included[1:], "\n include "))
|
||||
}
|
||||
}
|
||||
|
||||
if r.ProjectDirectory == "" {
|
||||
r.ProjectDirectory = filepath.Dir(mainFile)
|
||||
}
|
||||
relworkingdir, err := filepath.Rel(configDetails.WorkingDir, r.ProjectDirectory)
|
||||
if err != nil {
|
||||
// included file path is not inside project working directory => use absolute path
|
||||
relworkingdir = r.ProjectDirectory
|
||||
}
|
||||
|
||||
loadOptions := options.clone()
|
||||
loadOptions.ResolvePaths = true
|
||||
loadOptions.SkipNormalization = true
|
||||
loadOptions.SkipConsistencyCheck = true
|
||||
loadOptions.ResourceLoaders = append(loadOptions.RemoteResourceLoaders(), localResourceLoader{
|
||||
WorkingDir: relworkingdir,
|
||||
WorkingDir: r.ProjectDirectory,
|
||||
})
|
||||
|
||||
if len(r.EnvFile) == 0 {
|
||||
|
|
|
@ -41,6 +41,7 @@ import (
|
|||
"github.com/compose-spec/compose-go/v2/validation"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
@ -84,6 +85,15 @@ type Options struct {
|
|||
Listeners []Listener
|
||||
}
|
||||
|
||||
var versionWarning []string
|
||||
|
||||
func (o *Options) warnObsoleteVersion(file string) {
|
||||
if !slices.Contains(versionWarning, file) {
|
||||
logrus.Warning(fmt.Sprintf("%s: `version` is obsolete", file))
|
||||
}
|
||||
versionWarning = append(versionWarning, file)
|
||||
}
|
||||
|
||||
type Listener = func(event string, metadata map[string]any)
|
||||
|
||||
// Invoke all listeners for an event
|
||||
|
@ -285,12 +295,45 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
|
|||
return LoadWithContext(context.Background(), configDetails, options...)
|
||||
}
|
||||
|
||||
// LoadWithContext reads a ConfigDetails and returns a fully loaded configuration
|
||||
// LoadWithContext reads a ConfigDetails and returns a fully loaded configuration as a compose-go Project
|
||||
func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
|
||||
opts := toOptions(&configDetails, options)
|
||||
dict, err := loadModelWithContext(ctx, &configDetails, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return modelToProject(dict, opts, configDetails)
|
||||
}
|
||||
|
||||
// LoadModelWithContext reads a ConfigDetails and returns a fully loaded configuration as a yaml dictionary
|
||||
func LoadModelWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (map[string]any, error) {
|
||||
opts := toOptions(&configDetails, options)
|
||||
return loadModelWithContext(ctx, &configDetails, opts)
|
||||
}
|
||||
|
||||
// LoadModelWithContext reads a ConfigDetails and returns a fully loaded configuration as a yaml dictionary
|
||||
func loadModelWithContext(ctx context.Context, configDetails *types.ConfigDetails, opts *Options) (map[string]any, error) {
|
||||
if len(configDetails.ConfigFiles) < 1 {
|
||||
return nil, errors.New("No files specified")
|
||||
}
|
||||
|
||||
err := projectName(*configDetails, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(milas): this should probably ALWAYS set (overriding any existing)
|
||||
if _, ok := configDetails.Environment[consts.ComposeProjectName]; !ok && opts.projectName != "" {
|
||||
if configDetails.Environment == nil {
|
||||
configDetails.Environment = map[string]string{}
|
||||
}
|
||||
configDetails.Environment[consts.ComposeProjectName] = opts.projectName
|
||||
}
|
||||
|
||||
return load(ctx, *configDetails, opts, nil)
|
||||
}
|
||||
|
||||
func toOptions(configDetails *types.ConfigDetails, options []func(*Options)) *Options {
|
||||
opts := &Options{
|
||||
Interpolate: &interp.Options{
|
||||
Substitute: template.Substitute,
|
||||
|
@ -304,21 +347,7 @@ func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, opt
|
|||
op(opts)
|
||||
}
|
||||
opts.ResourceLoaders = append(opts.ResourceLoaders, localResourceLoader{configDetails.WorkingDir})
|
||||
|
||||
err := projectName(configDetails, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(milas): this should probably ALWAYS set (overriding any existing)
|
||||
if _, ok := configDetails.Environment[consts.ComposeProjectName]; !ok && opts.projectName != "" {
|
||||
if configDetails.Environment == nil {
|
||||
configDetails.Environment = map[string]string{}
|
||||
}
|
||||
configDetails.Environment[consts.ComposeProjectName] = opts.projectName
|
||||
}
|
||||
|
||||
return load(ctx, configDetails, opts, nil)
|
||||
return opts
|
||||
}
|
||||
|
||||
func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Options, ct *cycleTracker, included []string) (map[string]interface{}, error) {
|
||||
|
@ -390,6 +419,10 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
|
|||
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
|
||||
|
@ -458,7 +491,7 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
|
|||
return dict, nil
|
||||
}
|
||||
|
||||
func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (*types.Project, error) {
|
||||
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 {
|
||||
if f == mainFile {
|
||||
|
@ -481,6 +514,19 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
|
|||
return nil, errors.New("project name must not be empty")
|
||||
}
|
||||
|
||||
if !opts.SkipNormalization {
|
||||
dict["name"] = opts.projectName
|
||||
dict, err = Normalize(dict, configDetails.Environment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return dict, nil
|
||||
}
|
||||
|
||||
// modelToProject binds a canonical yaml dict into compose-go structs
|
||||
func modelToProject(dict map[string]interface{}, opts *Options, configDetails types.ConfigDetails) (*types.Project, error) {
|
||||
project := &types.Project{
|
||||
Name: opts.projectName,
|
||||
WorkingDir: configDetails.WorkingDir,
|
||||
|
@ -488,6 +534,7 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
|
|||
}
|
||||
delete(dict, "name") // project name set by yaml must be identified by caller as opts.projectName
|
||||
|
||||
var err error
|
||||
dict, err = processExtensions(dict, tree.NewPath(), opts.KnownExtensions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -498,13 +545,6 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -514,6 +554,10 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
|
|||
}
|
||||
}
|
||||
|
||||
if project, err = project.WithProfiles(opts.Profiles); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !opts.SkipConsistencyCheck {
|
||||
err := checkConsistency(project)
|
||||
if err != nil {
|
||||
|
@ -521,17 +565,12 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -18,266 +18,202 @@ package loader
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/errdefs"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
|
||||
func Normalize(project *types.Project) error {
|
||||
if project.Networks == nil {
|
||||
project.Networks = make(map[string]types.NetworkConfig)
|
||||
func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
|
||||
dict["networks"] = normalizeNetworks(dict)
|
||||
|
||||
if d, ok := dict["services"]; ok {
|
||||
services := d.(map[string]any)
|
||||
for name, s := range services {
|
||||
service := s.(map[string]any)
|
||||
_, hasNetworks := service["networks"]
|
||||
_, hasNetworkMode := service["network_mode"]
|
||||
if !hasNetworks && !hasNetworkMode {
|
||||
// Service without explicit network attachment are implicitly exposed on default network
|
||||
service["networks"] = map[string]any{"default": nil}
|
||||
}
|
||||
|
||||
if service["pull_policy"] == types.PullPolicyIfNotPresent {
|
||||
service["pull_policy"] = types.PullPolicyMissing
|
||||
}
|
||||
|
||||
fn := func(s string) (string, bool) {
|
||||
v, ok := env[s]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
if b, ok := service["build"]; ok {
|
||||
build := b.(map[string]any)
|
||||
if build["context"] == nil {
|
||||
build["context"] = "."
|
||||
}
|
||||
if build["dockerfile"] == nil && build["dockerfile_inline"] == nil {
|
||||
build["dockerfile"] = "Dockerfile"
|
||||
}
|
||||
|
||||
if a, ok := build["args"]; ok {
|
||||
build["args"], _ = resolve(a, fn)
|
||||
}
|
||||
|
||||
service["build"] = build
|
||||
}
|
||||
|
||||
if e, ok := service["environment"]; ok {
|
||||
service["environment"], _ = resolve(e, fn)
|
||||
}
|
||||
|
||||
var dependsOn map[string]any
|
||||
if d, ok := service["depends_on"]; ok {
|
||||
dependsOn = d.(map[string]any)
|
||||
} else {
|
||||
dependsOn = map[string]any{}
|
||||
}
|
||||
if l, ok := service["links"]; ok {
|
||||
links := l.([]any)
|
||||
for _, e := range links {
|
||||
link := e.(string)
|
||||
parts := strings.Split(link, ":")
|
||||
if len(parts) == 2 {
|
||||
link = parts[0]
|
||||
}
|
||||
if _, ok := dependsOn[link]; !ok {
|
||||
dependsOn[link] = map[string]any{
|
||||
"condition": types.ServiceConditionStarted,
|
||||
"restart": true,
|
||||
"required": true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, namespace := range []string{"network_mode", "ipc", "pid", "uts", "cgroup"} {
|
||||
if n, ok := service[namespace]; ok {
|
||||
ref := n.(string)
|
||||
if strings.HasPrefix(ref, types.ServicePrefix) {
|
||||
shared := ref[len(types.ServicePrefix):]
|
||||
if _, ok := dependsOn[shared]; !ok {
|
||||
dependsOn[shared] = map[string]any{
|
||||
"condition": types.ServiceConditionStarted,
|
||||
"restart": true,
|
||||
"required": true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if n, ok := service["volumes_from"]; ok {
|
||||
volumesFrom := n.([]any)
|
||||
for _, v := range volumesFrom {
|
||||
vol := v.(string)
|
||||
if !strings.HasPrefix(vol, types.ContainerPrefix) {
|
||||
spec := strings.Split(vol, ":")
|
||||
if _, ok := dependsOn[spec[0]]; !ok {
|
||||
dependsOn[spec[0]] = map[string]any{
|
||||
"condition": types.ServiceConditionStarted,
|
||||
"restart": false,
|
||||
"required": true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(dependsOn) > 0 {
|
||||
service["depends_on"] = dependsOn
|
||||
}
|
||||
services[name] = service
|
||||
}
|
||||
dict["services"] = services
|
||||
}
|
||||
|
||||
// If not declared explicitly, Compose model involves an implicit "default" network
|
||||
if _, ok := project.Networks["default"]; !ok {
|
||||
project.Networks["default"] = types.NetworkConfig{}
|
||||
}
|
||||
setNameFromKey(dict)
|
||||
|
||||
for name, s := range project.Services {
|
||||
if len(s.Networks) == 0 && s.NetworkMode == "" {
|
||||
// Service without explicit network attachment are implicitly exposed on default network
|
||||
s.Networks = map[string]*types.ServiceNetworkConfig{"default": nil}
|
||||
}
|
||||
|
||||
if s.PullPolicy == types.PullPolicyIfNotPresent {
|
||||
s.PullPolicy = types.PullPolicyMissing
|
||||
}
|
||||
|
||||
fn := func(s string) (string, bool) {
|
||||
v, ok := project.Environment[s]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
if s.Build != nil {
|
||||
if s.Build.Context == "" {
|
||||
s.Build.Context = "."
|
||||
}
|
||||
if s.Build.Dockerfile == "" && s.Build.DockerfileInline == "" {
|
||||
s.Build.Dockerfile = "Dockerfile"
|
||||
}
|
||||
s.Build.Args = s.Build.Args.Resolve(fn)
|
||||
}
|
||||
s.Environment = s.Environment.Resolve(fn)
|
||||
|
||||
for _, link := range s.Links {
|
||||
parts := strings.Split(link, ":")
|
||||
if len(parts) == 2 {
|
||||
link = parts[0]
|
||||
}
|
||||
s.DependsOn = setIfMissing(s.DependsOn, link, types.ServiceDependency{
|
||||
Condition: types.ServiceConditionStarted,
|
||||
Restart: true,
|
||||
Required: true,
|
||||
})
|
||||
}
|
||||
|
||||
for _, namespace := range []string{s.NetworkMode, s.Ipc, s.Pid, s.Uts, s.Cgroup} {
|
||||
if strings.HasPrefix(namespace, types.ServicePrefix) {
|
||||
name := namespace[len(types.ServicePrefix):]
|
||||
s.DependsOn = setIfMissing(s.DependsOn, name, types.ServiceDependency{
|
||||
Condition: types.ServiceConditionStarted,
|
||||
Restart: true,
|
||||
Required: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, vol := range s.VolumesFrom {
|
||||
if !strings.HasPrefix(vol, types.ContainerPrefix) {
|
||||
spec := strings.Split(vol, ":")
|
||||
s.DependsOn = setIfMissing(s.DependsOn, spec[0], types.ServiceDependency{
|
||||
Condition: types.ServiceConditionStarted,
|
||||
Restart: false,
|
||||
Required: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
err := relocateLogDriver(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = relocateLogOpt(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = relocateDockerfile(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inferImplicitDependencies(&s)
|
||||
|
||||
project.Services[name] = s
|
||||
}
|
||||
|
||||
setNameFromKey(project)
|
||||
|
||||
return nil
|
||||
return dict, nil
|
||||
}
|
||||
|
||||
// IsServiceDependency check the relation set by ref refers to a service
|
||||
func IsServiceDependency(ref string) (string, bool) {
|
||||
if strings.HasPrefix(
|
||||
ref,
|
||||
types.ServicePrefix,
|
||||
) {
|
||||
return ref[len(types.ServicePrefix):], true
|
||||
func normalizeNetworks(dict map[string]any) map[string]any {
|
||||
var networks map[string]any
|
||||
if n, ok := dict["networks"]; ok {
|
||||
networks = n.(map[string]any)
|
||||
} else {
|
||||
networks = map[string]any{}
|
||||
}
|
||||
return "", false
|
||||
if _, ok := networks["default"]; !ok {
|
||||
// If not declared explicitly, Compose model involves an implicit "default" network
|
||||
networks["default"] = nil
|
||||
}
|
||||
return networks
|
||||
}
|
||||
|
||||
func inferImplicitDependencies(service *types.ServiceConfig) {
|
||||
var dependencies []string
|
||||
|
||||
maybeReferences := []string{
|
||||
service.NetworkMode,
|
||||
service.Ipc,
|
||||
service.Pid,
|
||||
service.Uts,
|
||||
service.Cgroup,
|
||||
}
|
||||
for _, ref := range maybeReferences {
|
||||
if dep, ok := IsServiceDependency(ref); ok {
|
||||
dependencies = append(dependencies, dep)
|
||||
}
|
||||
}
|
||||
|
||||
for _, vol := range service.VolumesFrom {
|
||||
spec := strings.Split(vol, ":")
|
||||
if len(spec) == 0 {
|
||||
continue
|
||||
}
|
||||
if spec[0] == "container" {
|
||||
continue
|
||||
}
|
||||
dependencies = append(dependencies, spec[0])
|
||||
}
|
||||
|
||||
for _, link := range service.Links {
|
||||
dependencies = append(dependencies, strings.Split(link, ":")[0])
|
||||
}
|
||||
|
||||
if len(dependencies) > 0 && service.DependsOn == nil {
|
||||
service.DependsOn = make(types.DependsOnConfig)
|
||||
}
|
||||
|
||||
for _, d := range dependencies {
|
||||
if _, ok := service.DependsOn[d]; !ok {
|
||||
service.DependsOn[d] = types.ServiceDependency{
|
||||
Condition: types.ServiceConditionStarted,
|
||||
Required: true,
|
||||
func resolve(a any, fn func(s string) (string, bool)) (any, bool) {
|
||||
switch v := a.(type) {
|
||||
case []any:
|
||||
var resolved []any
|
||||
for _, val := range v {
|
||||
if r, ok := resolve(val, fn); ok {
|
||||
resolved = append(resolved, r)
|
||||
}
|
||||
}
|
||||
return resolved, true
|
||||
case map[string]any:
|
||||
resolved := map[string]any{}
|
||||
for key, val := range v {
|
||||
if val != nil {
|
||||
resolved[key] = val
|
||||
continue
|
||||
}
|
||||
if s, ok := fn(key); ok {
|
||||
resolved[key] = s
|
||||
}
|
||||
}
|
||||
return resolved, true
|
||||
case string:
|
||||
if !strings.Contains(v, "=") {
|
||||
if val, ok := fn(v); ok {
|
||||
return fmt.Sprintf("%s=%s", v, val), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
return v, true
|
||||
default:
|
||||
return v, false
|
||||
}
|
||||
}
|
||||
|
||||
// setIfMissing adds a ServiceDependency for service if not already defined
|
||||
func setIfMissing(d types.DependsOnConfig, service string, dep types.ServiceDependency) types.DependsOnConfig {
|
||||
if d == nil {
|
||||
d = types.DependsOnConfig{}
|
||||
}
|
||||
if _, ok := d[service]; !ok {
|
||||
d[service] = dep
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Resources with no explicit name are actually named by their key in map
|
||||
func setNameFromKey(project *types.Project) {
|
||||
for key, n := range project.Networks {
|
||||
if n.Name == "" {
|
||||
if n.External {
|
||||
n.Name = key
|
||||
} else {
|
||||
n.Name = fmt.Sprintf("%s_%s", project.Name, key)
|
||||
}
|
||||
project.Networks[key] = n
|
||||
func setNameFromKey(dict map[string]any) {
|
||||
for _, r := range []string{"networks", "volumes", "configs", "secrets"} {
|
||||
a, ok := dict[r]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for key, v := range project.Volumes {
|
||||
if v.Name == "" {
|
||||
if v.External {
|
||||
v.Name = key
|
||||
toplevel := a.(map[string]any)
|
||||
for key, r := range toplevel {
|
||||
var resource map[string]any
|
||||
if r != nil {
|
||||
resource = r.(map[string]any)
|
||||
} else {
|
||||
v.Name = fmt.Sprintf("%s_%s", project.Name, key)
|
||||
resource = map[string]any{}
|
||||
}
|
||||
project.Volumes[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
for key, c := range project.Configs {
|
||||
if c.Name == "" {
|
||||
if c.External {
|
||||
c.Name = key
|
||||
} else {
|
||||
c.Name = fmt.Sprintf("%s_%s", project.Name, key)
|
||||
if resource["name"] == nil {
|
||||
if x, ok := resource["external"]; ok && isTrue(x) {
|
||||
resource["name"] = key
|
||||
} else {
|
||||
resource["name"] = fmt.Sprintf("%s_%s", dict["name"], key)
|
||||
}
|
||||
}
|
||||
project.Configs[key] = c
|
||||
}
|
||||
}
|
||||
|
||||
for key, s := range project.Secrets {
|
||||
if s.Name == "" {
|
||||
if s.External {
|
||||
s.Name = key
|
||||
} else {
|
||||
s.Name = fmt.Sprintf("%s_%s", project.Name, key)
|
||||
}
|
||||
project.Secrets[key] = s
|
||||
toplevel[key] = resource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func relocateLogOpt(s *types.ServiceConfig) error {
|
||||
if len(s.LogOpt) != 0 {
|
||||
logrus.Warn("`log_opts` is deprecated. Use the `logging` element")
|
||||
if s.Logging == nil {
|
||||
s.Logging = &types.LoggingConfig{}
|
||||
}
|
||||
for k, v := range s.LogOpt {
|
||||
if _, ok := s.Logging.Options[k]; !ok {
|
||||
s.Logging.Options[k] = v
|
||||
} else {
|
||||
return fmt.Errorf("can't use both 'log_opt' (deprecated) and 'logging.options': %w", errdefs.ErrInvalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func relocateLogDriver(s *types.ServiceConfig) error {
|
||||
if s.LogDriver != "" {
|
||||
logrus.Warn("`log_driver` is deprecated. Use the `logging` element")
|
||||
if s.Logging == nil {
|
||||
s.Logging = &types.LoggingConfig{}
|
||||
}
|
||||
if s.Logging.Driver == "" {
|
||||
s.Logging.Driver = s.LogDriver
|
||||
} else {
|
||||
return fmt.Errorf("can't use both 'log_driver' (deprecated) and 'logging.driver': %w", errdefs.ErrInvalid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func relocateDockerfile(s *types.ServiceConfig) error {
|
||||
if s.Dockerfile != "" {
|
||||
logrus.Warn("`dockerfile` is deprecated. Use the `build` element")
|
||||
if s.Build == nil {
|
||||
s.Build = &types.BuildConfig{}
|
||||
}
|
||||
if s.Dockerfile == "" {
|
||||
s.Build.Dockerfile = s.Dockerfile
|
||||
} else {
|
||||
return fmt.Errorf("can't use both 'dockerfile' (deprecated) and 'build.dockerfile': %w", errdefs.ErrInvalid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
func isTrue(x any) bool {
|
||||
parseBool, _ := strconv.ParseBool(fmt.Sprint(x))
|
||||
return parseBool
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
@ -71,17 +70,14 @@ func checkConsistency(project *types.Project) error {
|
|||
}
|
||||
}
|
||||
|
||||
for dependedService := range s.DependsOn {
|
||||
for dependedService, cfg := range s.DependsOn {
|
||||
if _, err := project.GetService(dependedService); err != nil {
|
||||
return fmt.Errorf("service %q depends on undefined service %s: %w", s.Name, dependedService, errdefs.ErrInvalid)
|
||||
if errors.Is(err, errdefs.ErrDisabled) && !cfg.Required {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("service %q depends on undefined service %q: %w", s.Name, dependedService, errdefs.ErrInvalid)
|
||||
}
|
||||
}
|
||||
// Check there isn't a cycle in depends_on declarations
|
||||
if err := graph.InDependencyOrder(context.Background(), project, func(ctx context.Context, s string, config types.ServiceConfig) error {
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s.NetworkMode, types.ServicePrefix) {
|
||||
serviceName := s.NetworkMode[len(types.ServicePrefix):]
|
||||
|
@ -124,6 +120,31 @@ func checkConsistency(project *types.Project) error {
|
|||
s.Deploy.Replicas = s.Scale
|
||||
}
|
||||
|
||||
if s.CPUS != 0 && s.Deploy != nil {
|
||||
if s.Deploy.Resources.Limits != nil && s.Deploy.Resources.Limits.NanoCPUs.Value() != s.CPUS {
|
||||
return fmt.Errorf("services.%s: can't set distinct values on 'cpus' and 'deploy.resources.limits.cpus': %w",
|
||||
s.Name, errdefs.ErrInvalid)
|
||||
}
|
||||
}
|
||||
if s.MemLimit != 0 && s.Deploy != nil {
|
||||
if s.Deploy.Resources.Limits != nil && s.Deploy.Resources.Limits.MemoryBytes != s.MemLimit {
|
||||
return fmt.Errorf("services.%s: can't set distinct values on 'mem_limit' and 'deploy.resources.limits.memory': %w",
|
||||
s.Name, errdefs.ErrInvalid)
|
||||
}
|
||||
}
|
||||
if s.MemReservation != 0 && s.Deploy != nil {
|
||||
if s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.MemoryBytes != s.MemReservation {
|
||||
return fmt.Errorf("services.%s: can't set distinct values on 'mem_reservation' and 'deploy.resources.reservations.memory': %w",
|
||||
s.Name, errdefs.ErrInvalid)
|
||||
}
|
||||
}
|
||||
if s.PidsLimit != 0 && s.Deploy != nil {
|
||||
if s.Deploy.Resources.Limits != nil && s.Deploy.Resources.Limits.Pids != s.PidsLimit {
|
||||
return fmt.Errorf("services.%s: can't set distinct values on 'pids_limit' and 'deploy.resources.limits.pids': %w",
|
||||
s.Name, errdefs.ErrInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
if s.ContainerName != "" {
|
||||
if existing, ok := containerNames[s.ContainerName]; ok {
|
||||
return fmt.Errorf(`"services.%s": container name "%s" is already in use by "services.%s": %w`, s.Name, s.ContainerName, existing, errdefs.ErrInvalid)
|
||||
|
@ -159,5 +180,5 @@ func checkConsistency(project *types.Project) error {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return graph.CheckCycle(project)
|
||||
}
|
||||
|
|
|
@ -322,11 +322,13 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"mode": {"type": "string"},
|
||||
"host_ip": {"type": "string"},
|
||||
"target": {"type": "integer"},
|
||||
"published": {"type": ["string", "integer"]},
|
||||
"protocol": {"type": "string"}
|
||||
"protocol": {"type": "string"},
|
||||
"app_protocol": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {"^x-": {}}
|
||||
|
@ -389,7 +391,8 @@
|
|||
"volume": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nocopy": {"type": "boolean"}
|
||||
"nocopy": {"type": "boolean"},
|
||||
"subpath": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {"^x-": {}}
|
||||
|
|
|
@ -44,7 +44,7 @@ var patternString = fmt.Sprintf(
|
|||
groupInvalid,
|
||||
)
|
||||
|
||||
var defaultPattern = regexp.MustCompile(patternString)
|
||||
var DefaultPattern = regexp.MustCompile(patternString)
|
||||
|
||||
// InvalidTemplateError is returned when a variable template is not in a valid
|
||||
// format
|
||||
|
@ -121,7 +121,7 @@ func SubstituteWithOptions(template string, mapping Mapping, options ...Option)
|
|||
var returnErr error
|
||||
|
||||
cfg := &Config{
|
||||
pattern: defaultPattern,
|
||||
pattern: DefaultPattern,
|
||||
replacementFunc: DefaultReplacementFunc,
|
||||
logging: true,
|
||||
}
|
||||
|
@ -268,14 +268,14 @@ func getFirstBraceClosingIndex(s string) int {
|
|||
|
||||
// Substitute variables in the string with their values
|
||||
func Substitute(template string, mapping Mapping) (string, error) {
|
||||
return SubstituteWith(template, mapping, defaultPattern)
|
||||
return SubstituteWith(template, mapping, DefaultPattern)
|
||||
}
|
||||
|
||||
// ExtractVariables returns a map of all the variables defined in the specified
|
||||
// composefile (dict representation) and their default value if any.
|
||||
func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]Variable {
|
||||
if pattern == nil {
|
||||
pattern = defaultPattern
|
||||
pattern = DefaultPattern
|
||||
}
|
||||
return recurseExtract(configDict, pattern)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package types
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
@ -25,6 +26,7 @@ import (
|
|||
"sort"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/dotenv"
|
||||
"github.com/compose-spec/compose-go/v2/errdefs"
|
||||
"github.com/compose-spec/compose-go/v2/utils"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/mitchellh/copystructure"
|
||||
|
@ -215,9 +217,9 @@ func (p *Project) GetService(name string) (ServiceConfig, error) {
|
|||
if !ok {
|
||||
_, ok := p.DisabledServices[name]
|
||||
if ok {
|
||||
return ServiceConfig{}, fmt.Errorf("service %s is disabled", name)
|
||||
return ServiceConfig{}, fmt.Errorf("no such service: %s: %w", name, errdefs.ErrDisabled)
|
||||
}
|
||||
return ServiceConfig{}, fmt.Errorf("no such service: %s", name)
|
||||
return ServiceConfig{}, fmt.Errorf("no such service: %s: %w", name, errdefs.ErrNotFound)
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
@ -331,6 +333,9 @@ func (s ServiceConfig) HasProfile(profiles []string) bool {
|
|||
return true
|
||||
}
|
||||
for _, p := range profiles {
|
||||
if p == "*" {
|
||||
return true
|
||||
}
|
||||
for _, sp := range s.Profiles {
|
||||
if sp == p {
|
||||
return true
|
||||
|
@ -344,11 +349,6 @@ func (s ServiceConfig) HasProfile(profiles []string) bool {
|
|||
// It returns a new Project instance with the changes and keep the original Project unchanged
|
||||
func (p *Project) WithProfiles(profiles []string) (*Project, error) {
|
||||
newProject := p.deepCopy()
|
||||
for _, p := range profiles {
|
||||
if p == "*" {
|
||||
return newProject, nil
|
||||
}
|
||||
}
|
||||
enabled := Services{}
|
||||
disabled := Services{}
|
||||
for name, service := range newProject.AllServices() {
|
||||
|
@ -536,39 +536,29 @@ func (p *Project) WithServicesDisabled(names ...string) *Project {
|
|||
// WithImagesResolved updates services images to include digest computed by a resolver function
|
||||
// It returns a new Project instance with the changes and keep the original Project unchanged
|
||||
func (p *Project) WithImagesResolved(resolver func(named reference.Named) (godigest.Digest, error)) (*Project, error) {
|
||||
newProject := p.deepCopy()
|
||||
eg := errgroup.Group{}
|
||||
for i, s := range newProject.Services {
|
||||
idx := i
|
||||
service := s
|
||||
|
||||
return p.WithServicesTransform(func(name string, service ServiceConfig) (ServiceConfig, error) {
|
||||
if service.Image == "" {
|
||||
continue
|
||||
return service, nil
|
||||
}
|
||||
eg.Go(func() error {
|
||||
named, err := reference.ParseDockerRef(service.Image)
|
||||
named, err := reference.ParseDockerRef(service.Image)
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
if _, ok := named.(reference.Canonical); !ok {
|
||||
// image is named but not digested reference
|
||||
digest, err := resolver(named)
|
||||
if err != nil {
|
||||
return err
|
||||
return service, err
|
||||
}
|
||||
|
||||
if _, ok := named.(reference.Canonical); !ok {
|
||||
// image is named but not digested reference
|
||||
digest, err := resolver(named)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
named, err = reference.WithDigest(named, digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
named, err = reference.WithDigest(named, digest)
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
service.Image = named.String()
|
||||
newProject.Services[idx] = service
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return newProject, eg.Wait()
|
||||
}
|
||||
service.Image = named.String()
|
||||
return service, nil
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalYAML marshal Project into a yaml tree
|
||||
|
@ -606,7 +596,7 @@ func (p *Project) MarshalJSON() ([]byte, error) {
|
|||
for k, v := range p.Extensions {
|
||||
m[k] = v
|
||||
}
|
||||
return json.Marshal(m)
|
||||
return json.MarshalIndent(m, "", " ")
|
||||
}
|
||||
|
||||
// WithServicesEnvironmentResolved parses env_files set for services to resolve the actual environment map for services
|
||||
|
@ -662,3 +652,47 @@ func (p *Project) deepCopy() *Project {
|
|||
}
|
||||
return instance.(*Project)
|
||||
}
|
||||
|
||||
// WithServicesTransform applies a transformation to project services and return a new project with transformation results
|
||||
func (p *Project) WithServicesTransform(fn func(name string, s ServiceConfig) (ServiceConfig, error)) (*Project, error) {
|
||||
type result struct {
|
||||
name string
|
||||
service ServiceConfig
|
||||
}
|
||||
resultCh := make(chan result)
|
||||
newProject := p.deepCopy()
|
||||
|
||||
eg, ctx := errgroup.WithContext(context.Background())
|
||||
eg.Go(func() error {
|
||||
expect := len(newProject.Services)
|
||||
s := Services{}
|
||||
for expect > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// interrupted as some goroutine returned an error
|
||||
return nil
|
||||
case r := <-resultCh:
|
||||
s[r.name] = r.service
|
||||
expect--
|
||||
}
|
||||
}
|
||||
newProject.Services = s
|
||||
return nil
|
||||
})
|
||||
for n, s := range newProject.Services {
|
||||
name := n
|
||||
service := s
|
||||
eg.Go(func() error {
|
||||
updated, err := fn(name, service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resultCh <- result{
|
||||
name: name,
|
||||
service: updated,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return newProject, eg.Wait()
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/go-connections/nat"
|
||||
|
@ -365,7 +366,7 @@ type Resources struct {
|
|||
// Resource is a resource to be limited or reserved
|
||||
type Resource struct {
|
||||
// TODO: types to convert from units and ratios
|
||||
NanoCPUs string `yaml:"cpus,omitempty" json:"cpus,omitempty"`
|
||||
NanoCPUs NanoCPUs `yaml:"cpus,omitempty" json:"cpus,omitempty"`
|
||||
MemoryBytes UnitBytes `yaml:"memory,omitempty" json:"memory,omitempty"`
|
||||
Pids int64 `yaml:"pids,omitempty" json:"pids,omitempty"`
|
||||
Devices []DeviceRequest `yaml:"devices,omitempty" json:"devices,omitempty"`
|
||||
|
@ -374,6 +375,30 @@ type Resource struct {
|
|||
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
||||
}
|
||||
|
||||
type NanoCPUs float32
|
||||
|
||||
func (n *NanoCPUs) DecodeMapstructure(a any) error {
|
||||
switch v := a.(type) {
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*n = NanoCPUs(f)
|
||||
case float32:
|
||||
*n = NanoCPUs(v)
|
||||
case float64:
|
||||
*n = NanoCPUs(v)
|
||||
default:
|
||||
return fmt.Errorf("unexpected value type %T for cpus", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NanoCPUs) Value() float32 {
|
||||
return float32(*n)
|
||||
}
|
||||
|
||||
// GenericResource represents a "user defined" resource which can
|
||||
// only be an integer (e.g: SSD=3) for a service
|
||||
type GenericResource struct {
|
||||
|
@ -433,11 +458,13 @@ type ServiceNetworkConfig struct {
|
|||
|
||||
// ServicePortConfig is the port configuration for a service
|
||||
type ServicePortConfig struct {
|
||||
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
|
||||
HostIP string `yaml:"host_ip,omitempty" json:"host_ip,omitempty"`
|
||||
Target uint32 `yaml:"target,omitempty" json:"target,omitempty"`
|
||||
Published string `yaml:"published,omitempty" json:"published,omitempty"`
|
||||
Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"`
|
||||
Name string `yaml:"name,omitempty" json:"name,omitempty"`
|
||||
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
|
||||
HostIP string `yaml:"host_ip,omitempty" json:"host_ip,omitempty"`
|
||||
Target uint32 `yaml:"target,omitempty" json:"target,omitempty"`
|
||||
Published string `yaml:"published,omitempty" json:"published,omitempty"`
|
||||
Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"`
|
||||
AppProtocol string `yaml:"app_protocol,omitempty" json:"app_protocol,omitempty"`
|
||||
|
||||
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
||||
}
|
||||
|
@ -511,6 +538,9 @@ func (s ServiceVolumeConfig) String() string {
|
|||
if s.Volume != nil && s.Volume.NoCopy {
|
||||
options = append(options, "nocopy")
|
||||
}
|
||||
if s.Volume != nil && s.Volume.Subpath != "" {
|
||||
options = append(options, s.Volume.Subpath)
|
||||
}
|
||||
return fmt.Sprintf("%s:%s:%s", s.Source, s.Target, strings.Join(options, ","))
|
||||
}
|
||||
|
||||
|
@ -567,7 +597,8 @@ const (
|
|||
|
||||
// ServiceVolumeVolume are options for a service volume of type volume
|
||||
type ServiceVolumeVolume struct {
|
||||
NoCopy bool `yaml:"nocopy,omitempty" json:"nocopy,omitempty"`
|
||||
NoCopy bool `yaml:"nocopy,omitempty" json:"nocopy,omitempty"`
|
||||
Subpath string `yaml:"subpath,omitempty" json:"subpath,omitempty"`
|
||||
|
||||
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
||||
}
|
||||
|
|
|
@ -131,7 +131,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/v2 v2.0.0-rc.8
|
||||
# github.com/compose-spec/compose-go/v2 v2.0.2
|
||||
## explicit; go 1.21
|
||||
github.com/compose-spec/compose-go/v2/cli
|
||||
github.com/compose-spec/compose-go/v2/consts
|
||||
|
|
Loading…
Reference in New Issue