bump compose-go to v2.4.9

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
This commit is contained in:
Guillaume Lours 2025-03-14 14:40:03 +01:00 committed by CrazyMax
parent 00fdcd38ab
commit bf95aa3dfa
No known key found for this signature in database
GPG Key ID: ADE44D8C9D44FBE4
33 changed files with 637 additions and 164 deletions

View File

@ -214,7 +214,7 @@ func validateComposeFile(dt []byte, fn string) (bool, error) {
}
func validateCompose(dt []byte, envs map[string]string) error {
_, err := loader.Load(composetypes.ConfigDetails{
_, err := loader.LoadWithContext(context.Background(), composetypes.ConfigDetails{
ConfigFiles: []composetypes.ConfigFile{
{
Content: dt,

7
go.mod
View File

@ -1,12 +1,14 @@
module github.com/docker/buildx
go 1.22.0
go 1.23
toolchain go1.23.7
require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/Microsoft/go-winio v0.6.2
github.com/aws/aws-sdk-go-v2/config v1.27.27
github.com/compose-spec/compose-go/v2 v2.4.8
github.com/compose-spec/compose-go/v2 v2.4.9
github.com/containerd/console v1.0.4
github.com/containerd/containerd/v2 v2.0.3
github.com/containerd/continuity v0.4.5
@ -159,6 +161,7 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect

6
go.sum
View File

@ -77,8 +77,8 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
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.4.8 h1:7Myl8wDRl/4mRz77S+eyDJymGGEHu0diQdGSSeyq90A=
github.com/compose-spec/compose-go/v2 v2.4.8/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
github.com/compose-spec/compose-go/v2 v2.4.9 h1:2K4TDw+1ba2idiR6empXHKRXvWYpnvAKoNQy93/sSOs=
github.com/compose-spec/compose-go/v2 v2.4.9/go.mod h1:6k5l/0TxCg0/2uLEhRVEsoBWBprS2uvZi32J7xub3lo=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
@ -459,6 +459,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zclconf/go-cty v1.4.0/go.mod h1:nHzOclRkoj++EU9ZjSrZvRG0BXIWt8c7loYc0qXAFGQ=

View File

@ -18,7 +18,6 @@ package cli
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
@ -30,7 +29,6 @@ import (
"github.com/compose-spec/compose-go/v2/consts"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/errdefs"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/compose-spec/compose-go/v2/utils"
@ -551,14 +549,6 @@ func withListeners(options *ProjectOptions) func(*loader.Options) {
}
}
// getConfigPaths retrieves the config files for project based on project options
func (o *ProjectOptions) getConfigPaths() ([]string, error) {
if len(o.ConfigPaths) != 0 {
return absolutePaths(o.ConfigPaths)
}
return nil, fmt.Errorf("no configuration file provided: %w", errdefs.ErrNotFound)
}
func findFiles(names []string, pwd string) []string {
candidates := []string{}
for _, n := range names {

View File

@ -21,7 +21,17 @@ import (
"io"
)
var formats = map[string]Parser{}
const DotEnv = ".env"
var formats = map[string]Parser{
DotEnv: func(r io.Reader, filename string, lookup func(key string) (string, bool)) (map[string]string, error) {
m, err := ParseWithLookup(r, lookup)
if err != nil {
return nil, fmt.Errorf("failed to read %s: %w", filename, err)
}
return m, nil
},
}
type Parser func(r io.Reader, filename string, lookup func(key string) (string, bool)) (map[string]string, error)
@ -30,9 +40,12 @@ func RegisterFormat(format string, p Parser) {
}
func ParseWithFormat(r io.Reader, filename string, resolve LookupFn, format string) (map[string]string, error) {
parser, ok := formats[format]
if format == "" {
format = DotEnv
}
fn, ok := formats[format]
if !ok {
return nil, fmt.Errorf("unsupported env_file format %q", format)
}
return parser(r, filename, resolve)
return fn(r, filename, resolve)
}

View File

@ -30,7 +30,7 @@ var startsWithDigitRegex = regexp.MustCompile(`^\s*\d.*`) // Keys starting with
// LookupFn represents a lookup function to resolve variables from
type LookupFn func(string) (string, bool)
var noLookupFn = func(s string) (string, bool) {
var noLookupFn = func(_ string) (string, bool) {
return "", false
}

View File

@ -115,7 +115,7 @@ loop:
switch rune {
case '=', ':', '\n':
// library also supports yaml-style value declaration
key = string(src[0:i])
key = src[0:i]
offset = i + 1
inherited = rune == '\n'
break loop
@ -157,7 +157,7 @@ func (p *parser) extractVarValue(src string, envMap map[string]string, lookupFn
// Remove inline comments on unquoted lines
value, _, _ = strings.Cut(value, " #")
value = strings.TrimRightFunc(value, unicode.IsSpace)
retVal, err := expandVariables(string(value), envMap, lookupFn)
retVal, err := expandVariables(value, envMap, lookupFn)
return retVal, rest, err
}

View File

@ -63,9 +63,9 @@ func newTraversal[S, T any](fn CollectorFn[S, T]) *traversal[S, T] {
}
// WithMaxConcurrency configure traversal to limit concurrency walking graph nodes
func WithMaxConcurrency(max int) func(*Options) {
func WithMaxConcurrency(concurrency int) func(*Options) {
return func(o *Options) {
o.maxConcurrency = max
o.maxConcurrency = concurrency
}
}

View File

@ -113,11 +113,14 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
source := deepClone(base).(map[string]any)
for _, processor := range post {
processor.Apply(map[string]any{
err = processor.Apply(map[string]any{
"services": map[string]any{
name: source,
},
})
if err != nil {
return nil, err
}
}
merged, err := override.ExtendService(source, service)
if err != nil {

View File

@ -27,7 +27,6 @@ import (
)
var interpolateTypeCastMapping = map[tree.Path]interp.Cast{
servicePath("configs", tree.PathMatchList, "mode"): toInt,
servicePath("cpu_count"): toInt64,
servicePath("cpu_percent"): toFloat,
servicePath("cpu_period"): toInt64,
@ -53,7 +52,6 @@ var interpolateTypeCastMapping = map[tree.Path]interp.Cast{
servicePath("privileged"): toBoolean,
servicePath("read_only"): toBoolean,
servicePath("scale"): toInt,
servicePath("secrets", tree.PathMatchList, "mode"): toInt,
servicePath("stdin_open"): toBoolean,
servicePath("tty"): toBoolean,
servicePath("ulimits", tree.PathMatchAll): toInt,

View File

@ -257,15 +257,6 @@ 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) {
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 {
@ -275,32 +266,6 @@ type PostProcessor interface {
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
}
// LoadConfigFiles ingests config files with ResourceLoader and returns config details with paths to local copies
func LoadConfigFiles(ctx context.Context, configFiles []string, workingDir string, options ...func(*Options)) (*types.ConfigDetails, error) {
if len(configFiles) < 1 {
@ -353,12 +318,6 @@ func LoadConfigFiles(ctx context.Context, configFiles []string, workingDir strin
return config, 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 as a compose-go Project
func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
opts := toOptions(&configDetails, options)
@ -448,7 +407,15 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
return dict, nil
}
func loadYamlFile(ctx context.Context, file types.ConfigFile, opts *Options, workingDir string, environment types.Mapping, ct *cycleTracker, dict map[string]interface{}, included []string) (map[string]interface{}, PostProcessor, error) {
func loadYamlFile(ctx context.Context,
file types.ConfigFile,
opts *Options,
workingDir string,
environment types.Mapping,
ct *cycleTracker,
dict map[string]interface{},
included []string,
) (map[string]interface{}, PostProcessor, error) {
ctx = context.WithValue(ctx, consts.ComposeFileKey{}, file.Filename)
if file.Content == nil && file.Config == nil {
content, err := os.ReadFile(file.Filename)
@ -565,7 +532,6 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
return nil, fmt.Errorf("include cycle detected:\n%s\n include %s", loaded[0], strings.Join(loaded[1:], "\n include "))
}
}
loaded = append(loaded, mainFile)
dict, err := loadYamlModel(ctx, configDetails, opts, &cycleTracker{}, nil)
if err != nil {
@ -576,7 +542,7 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
return nil, errors.New("empty compose file")
}
if opts.projectName == "" {
if !opts.SkipValidation && opts.projectName == "" {
return nil, errors.New("project name must not be empty")
}

View File

@ -19,7 +19,8 @@ package loader
import "github.com/compose-spec/compose-go/v2/tree"
var omitempty = []tree.Path{
"services.*.dns"}
"services.*.dns",
}
// OmitEmpty removes empty attributes which are irrelevant when unset
func OmitEmpty(yaml map[string]any) map[string]any {

View File

@ -17,9 +17,7 @@
package loader
import (
"os"
"path/filepath"
"strings"
"github.com/compose-spec/compose-go/v2/types"
)
@ -40,17 +38,6 @@ func ResolveRelativePaths(project *types.Project) error {
return nil
}
func absPath(workingDir string, filePath string) string {
if strings.HasPrefix(filePath, "~") {
home, _ := os.UserHomeDir()
return filepath.Join(home, filePath[1:])
}
if filepath.IsAbs(filePath) {
return filePath
}
return filepath.Join(workingDir, filePath)
}
func absComposeFiles(composeFiles []string) ([]string, error) {
for i, composeFile := range composeFiles {
absComposefile, err := filepath.Abs(composeFile)
@ -61,14 +48,3 @@ func absComposeFiles(composeFiles []string) ([]string, error) {
}
return composeFiles, nil
}
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
}

View File

@ -27,7 +27,7 @@ import (
)
// checkConsistency validate a compose model is consistent
func checkConsistency(project *types.Project) error {
func checkConsistency(project *types.Project) error { //nolint:gocyclo
for name, s := range project.Services {
if s.Build == nil && s.Image == "" {
return fmt.Errorf("service %q has neither an image nor a build context specified: %w", s.Name, errdefs.ErrInvalid)
@ -171,7 +171,6 @@ func checkConsistency(project *types.Project) error {
return fmt.Errorf("services.%s.develop.watch: target is required for non-rebuild actions: %w", s.Name, errdefs.ErrInvalid)
}
}
}
}

View File

@ -44,7 +44,6 @@ func isWindowsAbs(path string) (b bool) {
// volumeNameLen returns length of the leading volume name on Windows.
// It returns 0 elsewhere.
// nolint: gocyclo
func volumeNameLen(path string) int {
if len(path) < 2 {
return 0

View File

@ -370,9 +370,10 @@
"pre_stop": {"type": "array", "items": {"$ref": "#/definitions/service_hook"}},
"privileged": {"type": ["boolean", "string"]},
"profiles": {"$ref": "#/definitions/list_of_strings"},
"pull_policy": {"type": "string", "enum": [
"always", "never", "if_not_present", "build", "missing"
]},
"pull_policy": {"type": "string",
"pattern": "always|never|build|if_not_present|missing|refresh|daily|weekly|every_([0-9]+[wdhms])+"
},
"pull_refresh_after": {"type": "string"},
"read_only": {"type": ["boolean", "string"]},
"restart": {"type": "string"},
"runtime": {
@ -490,7 +491,8 @@
"type": "object",
"required": ["path", "action"],
"properties": {
"ignore": {"type": "array", "items": {"type": "string"}},
"ignore": {"$ref": "#/definitions/string_or_list"},
"include": {"$ref": "#/definitions/string_or_list"},
"path": {"type": "string"},
"action": {"type": "string", "enum": ["rebuild", "sync", "restart", "sync+restart", "sync+exec"]},
"target": {"type": "string"},
@ -837,7 +839,8 @@
"environment": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
"patternProperties": {"^x-": {}},
"required": ["command"]
},
"env_file": {

View File

@ -26,25 +26,28 @@ import (
"github.com/sirupsen/logrus"
)
var delimiter = "\\$"
var substitutionNamed = "[_a-z][_a-z0-9]*"
var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-+?](.*))?"
var groupEscaped = "escaped"
var groupNamed = "named"
var groupBraced = "braced"
var groupInvalid = "invalid"
var patternString = fmt.Sprintf(
"%s(?i:(?P<%s>%s)|(?P<%s>%s)|{(?:(?P<%s>%s)}|(?P<%s>)))",
delimiter,
groupEscaped, delimiter,
groupNamed, substitutionNamed,
groupBraced, substitutionBraced,
groupInvalid,
const (
delimiter = "\\$"
substitutionNamed = "[_a-z][_a-z0-9]*"
substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-+?](.*))?"
groupEscaped = "escaped"
groupNamed = "named"
groupBraced = "braced"
groupInvalid = "invalid"
)
var DefaultPattern = regexp.MustCompile(patternString)
var (
patternString = fmt.Sprintf(
"%s(?i:(?P<%s>%s)|(?P<%s>%s)|{(?:(?P<%s>%s)}|(?P<%s>)))",
delimiter,
groupEscaped, delimiter,
groupNamed, substitutionNamed,
groupBraced, substitutionBraced,
groupInvalid,
)
DefaultPattern = regexp.MustCompile(patternString)
)
// InvalidTemplateError is returned when a variable template is not in a valid
// format

View File

@ -44,6 +44,8 @@ func init() {
transformers["services.*.build.ssh"] = transformSSH
transformers["services.*.ulimits.*"] = transformUlimits
transformers["services.*.build.ulimits.*"] = transformUlimits
transformers["services.*.develop.watch.*.ignore"] = transformStringOrList
transformers["services.*.develop.watch.*.include"] = transformStringOrList
transformers["volumes.*"] = transformMaybeExternal
transformers["networks.*"] = transformMaybeExternal
transformers["secrets.*"] = transformMaybeExternal

View File

@ -28,8 +28,8 @@ func deviceRequestDefaults(data any, p tree.Path, _ bool) (any, error) {
return data, fmt.Errorf("%s: invalid type %T for device request", p, v)
}
_, hasCount := v["count"]
_, hasIds := v["device_ids"]
if !hasCount && !hasIds {
_, hasIDs := v["device_ids"]
if !hasCount && !hasIDs {
v["count"] = "all"
}
return v, nil

View File

@ -24,10 +24,8 @@ import (
"github.com/go-viper/mapstructure/v2"
)
var (
// isCaseInsensitiveEnvVars is true on platforms where environment variable names are treated case-insensitively.
isCaseInsensitiveEnvVars = (runtime.GOOS == "windows")
)
// isCaseInsensitiveEnvVars is true on platforms where environment variable names are treated case-insensitively.
var isCaseInsensitiveEnvVars = (runtime.GOOS == "windows")
// ConfigDetails are the details about a group of ConfigFiles
type ConfigDetails struct {

View File

@ -1605,7 +1605,7 @@ func deriveDeepCopy_31(dst, src *ServiceConfigObjConfig) {
if src.Mode == nil {
dst.Mode = nil
} else {
dst.Mode = new(uint32)
dst.Mode = new(FileMode)
*dst.Mode = *src.Mode
}
if src.Extensions != nil {
@ -1812,6 +1812,7 @@ func deriveDeepCopy_38(dst, src *DeviceRequest) {
// deriveDeepCopy_39 recursively copies the contents of src into dst.
func deriveDeepCopy_39(dst, src *ServiceNetworkConfig) {
dst.Priority = src.Priority
dst.GatewayPriority = src.GatewayPriority
if src.Aliases == nil {
dst.Aliases = nil
} else {
@ -1891,7 +1892,7 @@ func deriveDeepCopy_41(dst, src *ServiceSecretConfig) {
if src.Mode == nil {
dst.Mode = nil
} else {
dst.Mode = new(uint32)
dst.Mode = new(FileMode)
*dst.Mode = *src.Mode
}
if src.Extensions != nil {
@ -2024,6 +2025,24 @@ func deriveDeepCopy_46(dst, src *Trigger) {
deriveDeepCopy_44(field, &src.Exec)
dst.Exec = *field
}()
if src.Include == nil {
dst.Include = nil
} else {
if dst.Include != nil {
if len(src.Include) > len(dst.Include) {
if cap(dst.Include) >= len(src.Include) {
dst.Include = (dst.Include)[:len(src.Include)]
} else {
dst.Include = make([]string, len(src.Include))
}
} else if len(src.Include) < len(dst.Include) {
dst.Include = (dst.Include)[:len(src.Include)]
}
} else {
dst.Include = make([]string, len(src.Include))
}
copy(dst.Include, src.Include)
}
if src.Ignore == nil {
dst.Ignore = nil
} else {

View File

@ -37,6 +37,7 @@ type Trigger struct {
Action WatchAction `yaml:"action" json:"action"`
Target string `yaml:"target,omitempty" json:"target,omitempty"`
Exec ServiceHook `yaml:"exec,omitempty" json:"exec,omitempty"`
Include []string `yaml:"include,omitempty" json:"include,omitempty"`
Ignore []string `yaml:"ignore,omitempty" json:"ignore,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
}

View File

@ -21,6 +21,8 @@ import (
"fmt"
"strings"
"time"
"github.com/xhit/go-str2duration/v2"
)
// Duration is a thin wrapper around time.Duration with improved JSON marshalling
@ -31,7 +33,7 @@ func (d Duration) String() string {
}
func (d *Duration) DecodeMapstructure(value interface{}) error {
v, err := time.ParseDuration(fmt.Sprint(value))
v, err := str2duration.ParseDuration(fmt.Sprint(value))
if err != nil {
return err
}

View File

@ -55,7 +55,6 @@ func (l Labels) AsList() []string {
func (l Labels) ToMappingWithEquals() MappingWithEquals {
mapping := MappingWithEquals{}
for k, v := range l {
v := v
mapping[k] = &v
}
return mapping

View File

@ -20,6 +20,7 @@ import (
"fmt"
"sort"
"strings"
"unicode"
)
// MappingWithEquals is a mapping type that can be converted from a list of
@ -94,6 +95,9 @@ func (m *MappingWithEquals) DecodeMapstructure(value interface{}) error {
mapping := make(MappingWithEquals, len(v))
for _, s := range v {
k, e, ok := strings.Cut(fmt.Sprint(s), "=")
if unicode.IsSpace(rune(k[len(k)-1])) {
return fmt.Errorf("environment variable %s is declared with a trailing space", k)
}
if !ok {
mapping[k] = nil
} else {
@ -157,7 +161,6 @@ func (m Mapping) Values() []string {
func (m Mapping) ToMappingWithEquals() MappingWithEquals {
mapping := MappingWithEquals{}
for k, v := range m {
v := v
mapping[k] = &v
}
return mapping

View File

@ -380,12 +380,7 @@ func (p *Project) WithServicesEnabled(names ...string) (*Project, error) {
service := p.DisabledServices[name]
profiles = append(profiles, service.Profiles...)
}
newProject, err := newProject.WithProfiles(profiles)
if err != nil {
return newProject, err
}
return newProject.WithServicesEnvironmentResolved(true)
return newProject.WithProfiles(profiles)
}
// WithoutUnnecessaryResources drops networks/volumes/secrets/configs that are not referenced by active services
@ -477,7 +472,7 @@ func (p *Project) WithSelectedServices(names []string, options ...DependencyOpti
}
set := utils.NewSet[string]()
err := p.ForEachService(names, func(name string, service *ServiceConfig) error {
err := p.ForEachService(names, func(name string, _ *ServiceConfig) error {
set.Add(name)
return nil
}, options...)
@ -535,7 +530,7 @@ 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) {
return p.WithServicesTransform(func(name string, service ServiceConfig) (ServiceConfig, error) {
return p.WithServicesTransform(func(_ string, service ServiceConfig) (ServiceConfig, error) {
if service.Image == "" {
return service, nil
}
@ -725,14 +720,9 @@ func loadMappingFile(path string, format string, resolve dotenv.LookupFn) (Mappi
if err != nil {
return nil, err
}
defer file.Close() //nolint:errcheck
defer file.Close()
var fileVars map[string]string
if format != "" {
fileVars, err = dotenv.ParseWithFormat(file, path, resolve, format)
} else {
fileVars, err = dotenv.ParseWithLookup(file, resolve)
}
fileVars, err := dotenv.ParseWithFormat(file, path, resolve, format)
if err != nil {
return nil, err
}
@ -746,7 +736,6 @@ func (p *Project) deepCopy() *Project {
n := &Project{}
deriveDeepCopyProject(n, p)
return n
}
// WithServicesTransform applies a transformation to project services and return a new project with transformation results

View File

@ -20,9 +20,12 @@ import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/docker/go-connections/nat"
"github.com/xhit/go-str2duration/v2"
)
// ServiceConfig is the configuration of one service
@ -215,6 +218,8 @@ const (
PullPolicyMissing = "missing"
// PullPolicyBuild force building images
PullPolicyBuild = "build"
// PullPolicyRefresh checks if image needs to be updated
PullPolicyRefresh = "refresh"
)
const (
@ -268,6 +273,27 @@ func (s ServiceConfig) GetDependents(p *Project) []string {
return dependent
}
func (s ServiceConfig) GetPullPolicy() (string, time.Duration, error) {
switch s.PullPolicy {
case PullPolicyAlways, PullPolicyNever, PullPolicyIfNotPresent, PullPolicyMissing, PullPolicyBuild:
return s.PullPolicy, 0, nil
case "daily":
return PullPolicyRefresh, 24 * time.Hour, nil
case "weekly":
return PullPolicyRefresh, 7 * 24 * time.Hour, nil
default:
if strings.HasPrefix(s.PullPolicy, "every_") {
delay := s.PullPolicy[6:]
duration, err := str2duration.ParseDuration(delay)
if err != nil {
return "", 0, err
}
return PullPolicyRefresh, duration, nil
}
return PullPolicyMissing, 0, nil
}
}
// BuildConfig is a type for build
type BuildConfig struct {
Context string `yaml:"context,omitempty" json:"context,omitempty"`
@ -479,16 +505,13 @@ func ParsePortConfig(value string) ([]ServicePortConfig, error) {
for _, key := range keys {
port := nat.Port(key)
converted, err := convertPortToPortConfig(port, portBindings)
if err != nil {
return nil, err
}
converted := convertPortToPortConfig(port, portBindings)
portConfigs = append(portConfigs, converted...)
}
return portConfigs, nil
}
func convertPortToPortConfig(port nat.Port, portBindings map[nat.Port][]nat.PortBinding) ([]ServicePortConfig, error) {
func convertPortToPortConfig(port nat.Port, portBindings map[nat.Port][]nat.PortBinding) []ServicePortConfig {
var portConfigs []ServicePortConfig
for _, binding := range portBindings[port] {
portConfigs = append(portConfigs, ServicePortConfig{
@ -499,7 +522,7 @@ func convertPortToPortConfig(port nat.Port, portBindings map[nat.Port][]nat.Port
Mode: "ingress",
})
}
return portConfigs, nil
return portConfigs
}
// ServiceVolumeConfig are references to a volume used by a service
@ -604,17 +627,51 @@ type ServiceVolumeTmpfs struct {
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
}
type FileMode int64
// FileReferenceConfig for a reference to a swarm file object
type FileReferenceConfig struct {
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Target string `yaml:"target,omitempty" json:"target,omitempty"`
UID string `yaml:"uid,omitempty" json:"uid,omitempty"`
GID string `yaml:"gid,omitempty" json:"gid,omitempty"`
Mode *uint32 `yaml:"mode,omitempty" json:"mode,omitempty"`
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Target string `yaml:"target,omitempty" json:"target,omitempty"`
UID string `yaml:"uid,omitempty" json:"uid,omitempty"`
GID string `yaml:"gid,omitempty" json:"gid,omitempty"`
Mode *FileMode `yaml:"mode,omitempty" json:"mode,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
}
func (f *FileMode) DecodeMapstructure(value interface{}) error {
switch v := value.(type) {
case *FileMode:
return nil
case string:
i, err := strconv.ParseInt(v, 8, 64)
if err != nil {
return err
}
*f = FileMode(i)
case int:
*f = FileMode(v)
default:
return fmt.Errorf("unexpected value type %T for mode", value)
}
return nil
}
// MarshalYAML makes FileMode implement yaml.Marshaller
func (f *FileMode) MarshalYAML() (interface{}, error) {
return f.String(), nil
}
// MarshalJSON makes FileMode implement json.Marshaller
func (f *FileMode) MarshalJSON() ([]byte, error) {
return []byte("\"" + f.String() + "\""), nil
}
func (f *FileMode) String() string {
return fmt.Sprintf("0%o", int64(*f))
}
// ServiceConfigObjConfig is the config obj configuration for a service
type ServiceConfigObjConfig FileReferenceConfig

View File

@ -41,7 +41,6 @@ func ResolveSymbolicLink(path string) (string, error) {
return path, nil
}
return strings.Replace(path, part, sym, 1), nil
}
// getSymbolinkLink parses all parts of the path and returns the

View File

@ -65,7 +65,6 @@ func check(value any, p tree.Path) error {
func checkFileObject(keys ...string) checkerFunc {
return func(value any, p tree.Path) error {
v := value.(map[string]any)
count := 0
for _, s := range keys {
@ -100,8 +99,8 @@ func checkPath(value any, p tree.Path) error {
func checkDeviceRequest(value any, p tree.Path) error {
v := value.(map[string]any)
_, hasCount := v["count"]
_, hasIds := v["device_ids"]
if hasCount && hasIds {
_, hasIDs := v["device_ids"]
if hasCount && hasIDs {
return fmt.Errorf(`%s: "count" and "device_ids" attributes are exclusive`, p)
}
return nil

27
vendor/github.com/xhit/go-str2duration/v2/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

88
vendor/github.com/xhit/go-str2duration/v2/README.md generated vendored Normal file
View File

@ -0,0 +1,88 @@
# Go String To Duration (go-str2duration)
This package allows to get a time.Duration from a string. The string can be a string retorned for time.Duration or a similar string with weeks or days too!.
<a href="https://goreportcard.com/report/github.com/xhit/go-str2duration/v2"><img src="https://goreportcard.com/badge/github.com/xhit/go-str2duration" alt="Go Report Card"></a>
<a href="https://pkg.go.dev/github.com/xhit/go-str2duration/v2?tab=doc"><img src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white" alt="go.dev"></a>
## Download
```bash
go get github.com/xhit/go-str2duration/v2
```
## Features
Go String To Duration supports this strings conversions to duration:
- All strings returned in time.Duration String.
- A string more readable like 1w2d6h3ns (1 week 2 days 6 hours and 3 nanoseconds).
- `µs` and `us` are microsecond.
It's the same `time.ParseDuration` standard function in Go, but with days and week support.
**Note**: a day is 24 hour.
If you don't need days and weeks, use [`time.ParseDuration`](https://golang.org/pkg/time/#ParseDuration).
## Usage
```go
package main
import (
"fmt"
str2duration "github.com/xhit/go-str2duration/v2"
"os"
"time"
)
func main() {
for i, tt := range []struct {
dur string
expected time.Duration
}{
//This times are returned with time.Duration string
{"1h", time.Duration(time.Hour)},
{"1m", time.Duration(time.Minute)},
{"1s", time.Duration(time.Second)},
{"1ms", time.Duration(time.Millisecond)},
{"1µs", time.Duration(time.Microsecond)},
{"1us", time.Duration(time.Microsecond)},
{"1ns", time.Duration(time.Nanosecond)},
{"4.000000001s", time.Duration(4*time.Second + time.Nanosecond)},
{"1h0m4.000000001s", time.Duration(time.Hour + 4*time.Second + time.Nanosecond)},
{"1h1m0.01s", time.Duration(61*time.Minute + 10*time.Millisecond)},
{"1h1m0.123456789s", time.Duration(61*time.Minute + 123456789*time.Nanosecond)},
{"1.00002ms", time.Duration(time.Millisecond + 20*time.Nanosecond)},
{"1.00000002s", time.Duration(time.Second + 20*time.Nanosecond)},
{"693ns", time.Duration(693 * time.Nanosecond)},
//This times aren't returned with time.Duration string, but are easily readable and can be parsed too!
{"1ms1ns", time.Duration(time.Millisecond + 1*time.Nanosecond)},
{"1s20ns", time.Duration(time.Second + 20*time.Nanosecond)},
{"60h8ms", time.Duration(60*time.Hour + 8*time.Millisecond)},
{"96h63s", time.Duration(96*time.Hour + 63*time.Second)},
//And works with days and weeks!
{"2d3s96ns", time.Duration(48*time.Hour + 3*time.Second + 96*time.Nanosecond)},
{"1w2d3s96ns", time.Duration(168*time.Hour + 48*time.Hour + 3*time.Second + 96*time.Nanosecond)},
{"10s1us693ns", time.Duration(10*time.Second + time.Microsecond + 693*time.Nanosecond)},
} {
durationFromString, err := str2duration.ParseDuration(tt.dur)
if err != nil {
panic(err)
//Check if expected time is the time returned by the parser
} else if tt.expected != durationFromString {
fmt.Println(fmt.Sprintf("index %d -> in: %s returned: %s\tnot equal to %s", i, tt.dur, durationFromString.String(), tt.expected.String()))
}else{
fmt.Println(fmt.Sprintf("index %d -> in: %s parsed succesfully", i, tt.dur))
}
}
}
```
Also, you can convert to string the duration using `String(t time.Duration)` function. This support weeks and days and not return the ugly decimals from golang standard `t.String()` function. Units with 0 values aren't returned. For example: `1d1ms` means 1 day 1 millisecond.

View File

@ -0,0 +1,331 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in https://raw.githubusercontent.com/golang/go/master/LICENSE
package str2duration
import (
"errors"
"time"
)
var unitMap = map[string]int64{
"ns": int64(time.Nanosecond),
"us": int64(time.Microsecond),
"µs": int64(time.Microsecond), // U+00B5 = micro symbol
"μs": int64(time.Microsecond), // U+03BC = Greek letter mu
"ms": int64(time.Millisecond),
"s": int64(time.Second),
"m": int64(time.Minute),
"h": int64(time.Hour),
"d": int64(time.Hour) * 24,
"w": int64(time.Hour) * 168,
}
// ParseDuration parses a duration string.
// A duration string is a possibly signed sequence of
// decimal numbers, each with optional fraction and a unit suffix,
// such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h", "d", "w".
func ParseDuration(s string) (time.Duration, error) {
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
orig := s
var d int64
neg := false
// Consume [-+]?
if s != "" {
c := s[0]
if c == '-' || c == '+' {
neg = c == '-'
s = s[1:]
}
}
// Special case: if all that is left is "0", this is zero.
if s == "0" {
return 0, nil
}
if s == "" {
return 0, errors.New("time: invalid duration " + quote(orig))
}
for s != "" {
var (
v, f int64 // integers before, after decimal point
scale float64 = 1 // value = v + f/scale
)
var err error
// The next character must be [0-9.]
if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') {
return 0, errors.New("time: invalid duration " + quote(orig))
}
// Consume [0-9]*
pl := len(s)
v, s, err = leadingInt(s)
if err != nil {
return 0, errors.New("time: invalid duration " + quote(orig))
}
pre := pl != len(s) // whether we consumed anything before a period
// Consume (\.[0-9]*)?
post := false
if s != "" && s[0] == '.' {
s = s[1:]
pl := len(s)
f, scale, s = leadingFraction(s)
post = pl != len(s)
}
if !pre && !post {
// no digits (e.g. ".s" or "-.s")
return 0, errors.New("time: invalid duration " + quote(orig))
}
// Consume unit.
i := 0
for ; i < len(s); i++ {
c := s[i]
if c == '.' || '0' <= c && c <= '9' {
break
}
}
if i == 0 {
return 0, errors.New("time: missing unit in duration " + quote(orig))
}
u := s[:i]
s = s[i:]
unit, ok := unitMap[u]
if !ok {
return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig))
}
if v > (1<<63-1)/unit {
// overflow
return 0, errors.New("time: invalid duration " + quote(orig))
}
v *= unit
if f > 0 {
// float64 is needed to be nanosecond accurate for fractions of hours.
// v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
v += int64(float64(f) * (float64(unit) / scale))
if v < 0 {
// overflow
return 0, errors.New("time: invalid duration " + quote(orig))
}
}
d += v
if d < 0 {
// overflow
return 0, errors.New("time: invalid duration " + quote(orig))
}
}
if neg {
d = -d
}
return time.Duration(d), nil
}
func quote(s string) string {
return "\"" + s + "\""
}
var errLeadingInt = errors.New("time: bad [0-9]*") // never printed
// leadingInt consumes the leading [0-9]* from s.
func leadingInt(s string) (x int64, rem string, err error) {
i := 0
for ; i < len(s); i++ {
c := s[i]
if c < '0' || c > '9' {
break
}
if x > (1<<63-1)/10 {
// overflow
return 0, "", errLeadingInt
}
x = x*10 + int64(c) - '0'
if x < 0 {
// overflow
return 0, "", errLeadingInt
}
}
return x, s[i:], nil
}
// leadingFraction consumes the leading [0-9]* from s.
// It is used only for fractions, so does not return an error on overflow,
// it just stops accumulating precision.
func leadingFraction(s string) (x int64, scale float64, rem string) {
i := 0
scale = 1
overflow := false
for ; i < len(s); i++ {
c := s[i]
if c < '0' || c > '9' {
break
}
if overflow {
continue
}
if x > (1<<63-1)/10 {
// It's possible for overflow to give a positive number, so take care.
overflow = true
continue
}
y := x*10 + int64(c) - '0'
if y < 0 {
overflow = true
continue
}
x = y
scale *= 10
}
return x, scale, s[i:]
}
// String returns a string representing the duration in the form "1w4d2h3m5s".
// Units with 0 values aren't returned, for example: 1d1ms is 1 day 1 milliseconds
func String(d time.Duration) string {
if d == 0 {
return "0s"
}
// Largest time is 15250w1d23h47m16s854ms775us807ns
var buf [32]byte
w := len(buf)
var sign string
u := uint64(d)
neg := d < 0
if neg {
u = -u
sign = "-"
}
// u is nanoseconds (ns)
if u > 0 {
w--
if u%1000 > 0 {
buf[w] = 's'
w--
buf[w] = 'n'
w = fmtInt(buf[:w], u%1000)
} else {
w++
}
u /= 1000
// u is now integer microseconds (us)
if u > 0 {
w--
if u%1000 > 0 {
buf[w] = 's'
w--
buf[w] = 'u'
w = fmtInt(buf[:w], u%1000)
} else {
w++
}
u /= 1000
// u is now integer milliseconds (ms)
if u > 0 {
w--
if u%1000 > 0 {
buf[w] = 's'
w--
buf[w] = 'm'
w = fmtInt(buf[:w], u%1000)
} else {
w++
}
u /= 1000
// u is now integer seconds (s)
if u > 0 {
w--
if u%60 > 0 {
buf[w] = 's'
w = fmtInt(buf[:w], u%60)
} else {
w++
}
u /= 60
// u is now integer minutes (m)
if u > 0 {
w--
if u%60 > 0 {
buf[w] = 'm'
w = fmtInt(buf[:w], u%60)
} else {
w++
}
u /= 60
// u is now integer hours (h)
if u > 0 {
w--
if u%24 > 0 {
buf[w] = 'h'
w = fmtInt(buf[:w], u%24)
} else {
w++
}
u /= 24
// u is now integer days (d)
if u > 0 {
w--
if u%7 > 0 {
buf[w] = 'd'
w = fmtInt(buf[:w], u%7)
} else {
w++
}
u /= 7
// u is now integer weeks (w)
if u > 0 {
w--
buf[w] = 'w'
w = fmtInt(buf[:w], u)
}
}
}
}
}
}
}
}
return sign + string(buf[w:])
}
// fmtInt formats v into the tail of buf.
// It returns the index where the output begins.
func fmtInt(buf []byte, v uint64) int {
w := len(buf)
if v == 0 {
w--
buf[w] = '0'
} else {
for v > 0 {
w--
buf[w] = byte(v%10) + '0'
v /= 10
}
}
return w
}

7
vendor/modules.txt vendored
View File

@ -130,8 +130,8 @@ github.com/cenkalti/backoff/v4
# github.com/cespare/xxhash/v2 v2.3.0
## explicit; go 1.11
github.com/cespare/xxhash/v2
# github.com/compose-spec/compose-go/v2 v2.4.8
## explicit; go 1.21
# github.com/compose-spec/compose-go/v2 v2.4.9
## explicit; go 1.23
github.com/compose-spec/compose-go/v2/cli
github.com/compose-spec/compose-go/v2/consts
github.com/compose-spec/compose-go/v2/dotenv
@ -761,6 +761,9 @@ github.com/xeipuuv/gojsonreference
# github.com/xeipuuv/gojsonschema v1.2.0
## explicit
github.com/xeipuuv/gojsonschema
# github.com/xhit/go-str2duration/v2 v2.1.0
## explicit; go 1.13
github.com/xhit/go-str2duration/v2
# github.com/zclconf/go-cty v1.16.0
## explicit; go 1.18
github.com/zclconf/go-cty/cty