Merge pull request #3064 from glours/bump-compose-v2.34.0

bump compose-go to v2.4.9
This commit is contained in:
Tõnis Tiigi 2025-03-20 20:27:32 -07:00 committed by GitHub
commit 646df6d4a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 663 additions and 205 deletions

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding" "encoding"
"io" "io"
"maps"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -1104,9 +1105,7 @@ func (t *Target) GetEvalContexts(ectx *hcl.EvalContext, block *hcl.Block, loadDe
e2 := ectx.NewChild() e2 := ectx.NewChild()
e2.Variables = make(map[string]cty.Value) e2.Variables = make(map[string]cty.Value)
if e != ectx { if e != ectx {
for k, v := range e.Variables { maps.Copy(e2.Variables, e.Variables)
e2.Variables[k] = v
}
} }
e2.Variables[k] = v e2.Variables[k] = v
ectxs2 = append(ectxs2, e2) ectxs2 = append(ectxs2, e2)

View File

@ -3,6 +3,7 @@ package bake
import ( import (
"context" "context"
"fmt" "fmt"
"maps"
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
@ -91,9 +92,7 @@ func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Conf
var additionalContexts map[string]string var additionalContexts map[string]string
if s.Build.AdditionalContexts != nil { if s.Build.AdditionalContexts != nil {
additionalContexts = map[string]string{} additionalContexts = map[string]string{}
for k, v := range s.Build.AdditionalContexts { maps.Copy(additionalContexts, s.Build.AdditionalContexts)
additionalContexts[k] = v
}
} }
var shmSize *string var shmSize *string
@ -214,7 +213,7 @@ func validateComposeFile(dt []byte, fn string) (bool, error) {
} }
func validateCompose(dt []byte, envs map[string]string) 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{ ConfigFiles: []composetypes.ConfigFile{
{ {
Content: dt, Content: dt,

View File

@ -8,6 +8,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"maps"
"os" "os"
"slices" "slices"
"strconv" "strconv"
@ -431,9 +432,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
FrontendInputs: frontendInputs, FrontendInputs: frontendInputs,
FrontendOpt: make(map[string]string), FrontendOpt: make(map[string]string),
} }
for k, v := range so.FrontendAttrs { maps.Copy(req.FrontendOpt, so.FrontendAttrs)
req.FrontendOpt[k] = v
}
so.Frontend = "" so.Frontend = ""
so.FrontendInputs = nil so.FrontendInputs = nil

View File

@ -2,6 +2,7 @@ package build
import ( import (
"context" "context"
"maps"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -127,9 +128,7 @@ func getGitAttributes(ctx context.Context, contextPath, dockerfilePath string) (
if so.FrontendAttrs == nil { if so.FrontendAttrs == nil {
so.FrontendAttrs = make(map[string]string) so.FrontendAttrs = make(map[string]string)
} }
for k, v := range res { maps.Copy(so.FrontendAttrs, res)
so.FrontendAttrs[k] = v
}
if !setGitInfo || root == "" { if !setGitInfo || root == "" {
return return

View File

@ -5,6 +5,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"io" "io"
"maps"
"strings" "strings"
"sync" "sync"
@ -40,9 +41,7 @@ func setRecordProvenance(ctx context.Context, c *client.Client, sr *client.Solve
if err != nil { if err != nil {
return err return err
} }
for k, v := range res { maps.Copy(sr.ExporterResponse, res)
sr.ExporterResponse[k] = v
}
return nil return nil
}) })
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -409,9 +410,7 @@ func truncPlatforms(pfs []string, max int) truncatedPlatforms {
left[ppf] = append(left[ppf], pf) left[ppf] = append(left[ppf], pf)
} }
} }
for k, v := range left { maps.Copy(res, left)
res[k] = v
}
return truncatedPlatforms{ return truncatedPlatforms{
res: res, res: res,
input: pfs, input: pfs,

View File

@ -1,6 +1,10 @@
package pb package pb
import "github.com/moby/buildkit/client" import (
"maps"
"github.com/moby/buildkit/client"
)
func CreateCaches(entries []*CacheOptionsEntry) []client.CacheOptionsEntry { func CreateCaches(entries []*CacheOptionsEntry) []client.CacheOptionsEntry {
var outs []client.CacheOptionsEntry var outs []client.CacheOptionsEntry
@ -12,9 +16,7 @@ func CreateCaches(entries []*CacheOptionsEntry) []client.CacheOptionsEntry {
Type: entry.Type, Type: entry.Type,
Attrs: map[string]string{}, Attrs: map[string]string{},
} }
for k, v := range entry.Attrs { maps.Copy(out.Attrs, entry.Attrs)
out.Attrs[k] = v
}
outs = append(outs, out) outs = append(outs, out)
} }
return outs return outs

View File

@ -2,6 +2,7 @@ package pb
import ( import (
"io" "io"
"maps"
"os" "os"
"strconv" "strconv"
@ -26,9 +27,7 @@ func CreateExports(entries []*ExportEntry) ([]client.ExportEntry, []string, erro
Type: entry.Type, Type: entry.Type,
Attrs: map[string]string{}, Attrs: map[string]string{},
} }
for k, v := range entry.Attrs { maps.Copy(out.Attrs, entry.Attrs)
out.Attrs[k] = v
}
supportFile := false supportFile := false
supportDir := false supportDir := false

5
go.mod
View File

@ -1,12 +1,12 @@
module github.com/docker/buildx module github.com/docker/buildx
go 1.22.0 go 1.23.0
require ( require (
github.com/Masterminds/semver/v3 v3.2.1 github.com/Masterminds/semver/v3 v3.2.1
github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/go-winio v0.6.2
github.com/aws/aws-sdk-go-v2/config v1.27.27 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/console v1.0.4
github.com/containerd/containerd/v2 v2.0.4 github.com/containerd/containerd/v2 v2.0.4
github.com/containerd/continuity v0.4.5 github.com/containerd/continuity v0.4.5
@ -159,6 +159,7 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // 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/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/httptrace/otelhttptrace v0.56.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp 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/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 h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= 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.9 h1:2K4TDw+1ba2idiR6empXHKRXvWYpnvAKoNQy93/sSOs=
github.com/compose-spec/compose-go/v2 v2.4.8/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= 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 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= 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/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 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/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= github.com/zclconf/go-cty v1.4.0/go.mod h1:nHzOclRkoj++EU9ZjSrZvRG0BXIWt8c7loYc0qXAFGQ=

View File

@ -2,6 +2,7 @@ package store
import ( import (
"fmt" "fmt"
"maps"
"slices" "slices"
"time" "time"
@ -93,9 +94,7 @@ func (ng *NodeGroup) Update(name, endpoint string, platforms []string, endpoints
needsRestart = true needsRestart = true
} }
if buildkitdConfigFile != "" { if buildkitdConfigFile != "" {
for k, v := range files { maps.Copy(n.Files, files)
n.Files[k] = v
}
needsRestart = true needsRestart = true
} }
if needsRestart { if needsRestart {
@ -147,9 +146,7 @@ func (n *Node) Copy() *Node {
buildkitdFlags := []string{} buildkitdFlags := []string{}
copy(buildkitdFlags, n.BuildkitdFlags) copy(buildkitdFlags, n.BuildkitdFlags)
driverOpts := map[string]string{} driverOpts := map[string]string{}
for k, v := range n.DriverOpts { maps.Copy(driverOpts, n.DriverOpts)
driverOpts[k] = v
}
files := map[string][]byte{} files := map[string][]byte{}
for k, v := range n.Files { for k, v := range n.Files {
vv := []byte{} vv := []byte{}

View File

@ -107,9 +107,7 @@ func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann map[exptypes
if old.Annotations == nil { if old.Annotations == nil {
old.Annotations = map[string]string{} old.Annotations = map[string]string{}
} }
for k, v := range d.Annotations { maps.Copy(old.Annotations, d.Annotations)
old.Annotations[k] = v
}
newDescs[idx] = old newDescs[idx] = old
} else { } else {
m[d.Digest] = len(newDescs) m[d.Digest] = len(newDescs)

View File

@ -6,6 +6,7 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"maps"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
@ -126,13 +127,9 @@ func (l *loader) Load(ctx context.Context, ref string) (*result, error) {
} }
var a asset var a asset
annotations := make(map[string]string, len(mfst.manifest.Annotations)+len(mfst.desc.Annotations)) annotations := map[string]string{}
for k, v := range mfst.desc.Annotations { maps.Copy(annotations, mfst.desc.Annotations)
annotations[k] = v maps.Copy(annotations, mfst.manifest.Annotations)
}
for k, v := range mfst.manifest.Annotations {
annotations[k] = v
}
if err := l.scanConfig(ctx, fetcher, mfst.manifest.Config, &a); err != nil { if err := l.scanConfig(ctx, fetcher, mfst.manifest.Config, &a); err != nil {
return nil, err return nil, err

View File

@ -18,7 +18,6 @@ package cli
import ( import (
"context" "context"
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@ -30,7 +29,6 @@ import (
"github.com/compose-spec/compose-go/v2/consts" "github.com/compose-spec/compose-go/v2/consts"
"github.com/compose-spec/compose-go/v2/dotenv" "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/loader"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/compose-spec/compose-go/v2/utils" "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 { func findFiles(names []string, pwd string) []string {
candidates := []string{} candidates := []string{}
for _, n := range names { for _, n := range names {

View File

@ -21,7 +21,17 @@ import (
"io" "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) 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) { 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 { if !ok {
return nil, fmt.Errorf("unsupported env_file format %q", format) 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 // LookupFn represents a lookup function to resolve variables from
type LookupFn func(string) (string, bool) type LookupFn func(string) (string, bool)
var noLookupFn = func(s string) (string, bool) { var noLookupFn = func(_ string) (string, bool) {
return "", false return "", false
} }

View File

@ -115,7 +115,7 @@ loop:
switch rune { switch rune {
case '=', ':', '\n': case '=', ':', '\n':
// library also supports yaml-style value declaration // library also supports yaml-style value declaration
key = string(src[0:i]) key = src[0:i]
offset = i + 1 offset = i + 1
inherited = rune == '\n' inherited = rune == '\n'
break loop break loop
@ -157,7 +157,7 @@ func (p *parser) extractVarValue(src string, envMap map[string]string, lookupFn
// Remove inline comments on unquoted lines // Remove inline comments on unquoted lines
value, _, _ = strings.Cut(value, " #") value, _, _ = strings.Cut(value, " #")
value = strings.TrimRightFunc(value, unicode.IsSpace) value = strings.TrimRightFunc(value, unicode.IsSpace)
retVal, err := expandVariables(string(value), envMap, lookupFn) retVal, err := expandVariables(value, envMap, lookupFn)
return retVal, rest, err 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 // 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) { 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) source := deepClone(base).(map[string]any)
for _, processor := range post { for _, processor := range post {
processor.Apply(map[string]any{ err = processor.Apply(map[string]any{
"services": map[string]any{ "services": map[string]any{
name: source, name: source,
}, },
}) })
if err != nil {
return nil, err
}
} }
merged, err := override.ExtendService(source, service) merged, err := override.ExtendService(source, service)
if err != nil { if err != nil {

View File

@ -27,7 +27,6 @@ import (
) )
var interpolateTypeCastMapping = map[tree.Path]interp.Cast{ var interpolateTypeCastMapping = map[tree.Path]interp.Cast{
servicePath("configs", tree.PathMatchList, "mode"): toInt,
servicePath("cpu_count"): toInt64, servicePath("cpu_count"): toInt64,
servicePath("cpu_percent"): toFloat, servicePath("cpu_percent"): toFloat,
servicePath("cpu_period"): toInt64, servicePath("cpu_period"): toInt64,
@ -53,7 +52,6 @@ var interpolateTypeCastMapping = map[tree.Path]interp.Cast{
servicePath("privileged"): toBoolean, servicePath("privileged"): toBoolean,
servicePath("read_only"): toBoolean, servicePath("read_only"): toBoolean,
servicePath("scale"): toInt, servicePath("scale"): toInt,
servicePath("secrets", tree.PathMatchList, "mode"): toInt,
servicePath("stdin_open"): toBoolean, servicePath("stdin_open"): toBoolean,
servicePath("tty"): toBoolean, servicePath("tty"): toBoolean,
servicePath("ulimits", tree.PathMatchAll): toInt, 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 // 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 // that hardly can be implemented using go-yaml and mapstructure
type PostProcessor interface { type PostProcessor interface {
@ -275,32 +266,6 @@ type PostProcessor interface {
Apply(interface{}) error 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 // 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) { func LoadConfigFiles(ctx context.Context, configFiles []string, workingDir string, options ...func(*Options)) (*types.ConfigDetails, error) {
if len(configFiles) < 1 { if len(configFiles) < 1 {
@ -353,12 +318,6 @@ func LoadConfigFiles(ctx context.Context, configFiles []string, workingDir strin
return config, nil 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 // 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) { func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
opts := toOptions(&configDetails, options) opts := toOptions(&configDetails, options)
@ -448,7 +407,15 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
return dict, nil 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) ctx = context.WithValue(ctx, consts.ComposeFileKey{}, file.Filename)
if file.Content == nil && file.Config == nil { if file.Content == nil && file.Config == nil {
content, err := os.ReadFile(file.Filename) 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 ")) 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) dict, err := loadYamlModel(ctx, configDetails, opts, &cycleTracker{}, nil)
if err != 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") 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") 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" import "github.com/compose-spec/compose-go/v2/tree"
var omitempty = []tree.Path{ var omitempty = []tree.Path{
"services.*.dns"} "services.*.dns",
}
// OmitEmpty removes empty attributes which are irrelevant when unset // OmitEmpty removes empty attributes which are irrelevant when unset
func OmitEmpty(yaml map[string]any) map[string]any { func OmitEmpty(yaml map[string]any) map[string]any {

View File

@ -17,9 +17,7 @@
package loader package loader
import ( import (
"os"
"path/filepath" "path/filepath"
"strings"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
) )
@ -40,17 +38,6 @@ func ResolveRelativePaths(project *types.Project) error {
return nil 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) { func absComposeFiles(composeFiles []string) ([]string, error) {
for i, composeFile := range composeFiles { for i, composeFile := range composeFiles {
absComposefile, err := filepath.Abs(composeFile) absComposefile, err := filepath.Abs(composeFile)
@ -61,14 +48,3 @@ func absComposeFiles(composeFiles []string) ([]string, error) {
} }
return composeFiles, nil 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 // 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 { for name, s := range project.Services {
if s.Build == nil && s.Image == "" { 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) 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) 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. // volumeNameLen returns length of the leading volume name on Windows.
// It returns 0 elsewhere. // It returns 0 elsewhere.
// nolint: gocyclo
func volumeNameLen(path string) int { func volumeNameLen(path string) int {
if len(path) < 2 { if len(path) < 2 {
return 0 return 0

View File

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

View File

@ -26,25 +26,28 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var delimiter = "\\$" const (
var substitutionNamed = "[_a-z][_a-z0-9]*" delimiter = "\\$"
var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-+?](.*))?" substitutionNamed = "[_a-z][_a-z0-9]*"
substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-+?](.*))?"
var groupEscaped = "escaped" groupEscaped = "escaped"
var groupNamed = "named" groupNamed = "named"
var groupBraced = "braced" groupBraced = "braced"
var groupInvalid = "invalid" 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,
) )
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 // InvalidTemplateError is returned when a variable template is not in a valid
// format // format

View File

@ -44,6 +44,8 @@ func init() {
transformers["services.*.build.ssh"] = transformSSH transformers["services.*.build.ssh"] = transformSSH
transformers["services.*.ulimits.*"] = transformUlimits transformers["services.*.ulimits.*"] = transformUlimits
transformers["services.*.build.ulimits.*"] = transformUlimits transformers["services.*.build.ulimits.*"] = transformUlimits
transformers["services.*.develop.watch.*.ignore"] = transformStringOrList
transformers["services.*.develop.watch.*.include"] = transformStringOrList
transformers["volumes.*"] = transformMaybeExternal transformers["volumes.*"] = transformMaybeExternal
transformers["networks.*"] = transformMaybeExternal transformers["networks.*"] = transformMaybeExternal
transformers["secrets.*"] = 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) return data, fmt.Errorf("%s: invalid type %T for device request", p, v)
} }
_, hasCount := v["count"] _, hasCount := v["count"]
_, hasIds := v["device_ids"] _, hasIDs := v["device_ids"]
if !hasCount && !hasIds { if !hasCount && !hasIDs {
v["count"] = "all" v["count"] = "all"
} }
return v, nil return v, nil

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"unicode"
) )
// MappingWithEquals is a mapping type that can be converted from a list of // 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)) mapping := make(MappingWithEquals, len(v))
for _, s := range v { for _, s := range v {
k, e, ok := strings.Cut(fmt.Sprint(s), "=") 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 { if !ok {
mapping[k] = nil mapping[k] = nil
} else { } else {
@ -157,7 +161,6 @@ func (m Mapping) Values() []string {
func (m Mapping) ToMappingWithEquals() MappingWithEquals { func (m Mapping) ToMappingWithEquals() MappingWithEquals {
mapping := MappingWithEquals{} mapping := MappingWithEquals{}
for k, v := range m { for k, v := range m {
v := v
mapping[k] = &v mapping[k] = &v
} }
return mapping return mapping

View File

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

View File

@ -20,9 +20,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"sort" "sort"
"strconv"
"strings" "strings"
"time"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/xhit/go-str2duration/v2"
) )
// ServiceConfig is the configuration of one service // ServiceConfig is the configuration of one service
@ -215,6 +218,8 @@ const (
PullPolicyMissing = "missing" PullPolicyMissing = "missing"
// PullPolicyBuild force building images // PullPolicyBuild force building images
PullPolicyBuild = "build" PullPolicyBuild = "build"
// PullPolicyRefresh checks if image needs to be updated
PullPolicyRefresh = "refresh"
) )
const ( const (
@ -268,6 +273,27 @@ func (s ServiceConfig) GetDependents(p *Project) []string {
return dependent 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 // BuildConfig is a type for build
type BuildConfig struct { type BuildConfig struct {
Context string `yaml:"context,omitempty" json:"context,omitempty"` Context string `yaml:"context,omitempty" json:"context,omitempty"`
@ -479,16 +505,13 @@ func ParsePortConfig(value string) ([]ServicePortConfig, error) {
for _, key := range keys { for _, key := range keys {
port := nat.Port(key) port := nat.Port(key)
converted, err := convertPortToPortConfig(port, portBindings) converted := convertPortToPortConfig(port, portBindings)
if err != nil {
return nil, err
}
portConfigs = append(portConfigs, converted...) portConfigs = append(portConfigs, converted...)
} }
return portConfigs, nil 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 var portConfigs []ServicePortConfig
for _, binding := range portBindings[port] { for _, binding := range portBindings[port] {
portConfigs = append(portConfigs, ServicePortConfig{ portConfigs = append(portConfigs, ServicePortConfig{
@ -499,7 +522,7 @@ func convertPortToPortConfig(port nat.Port, portBindings map[nat.Port][]nat.Port
Mode: "ingress", Mode: "ingress",
}) })
} }
return portConfigs, nil return portConfigs
} }
// ServiceVolumeConfig are references to a volume used by a service // ServiceVolumeConfig are references to a volume used by a service
@ -604,17 +627,51 @@ type ServiceVolumeTmpfs struct {
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
} }
type FileMode int64
// FileReferenceConfig for a reference to a swarm file object // FileReferenceConfig for a reference to a swarm file object
type FileReferenceConfig struct { type FileReferenceConfig struct {
Source string `yaml:"source,omitempty" json:"source,omitempty"` Source string `yaml:"source,omitempty" json:"source,omitempty"`
Target string `yaml:"target,omitempty" json:"target,omitempty"` Target string `yaml:"target,omitempty" json:"target,omitempty"`
UID string `yaml:"uid,omitempty" json:"uid,omitempty"` UID string `yaml:"uid,omitempty" json:"uid,omitempty"`
GID string `yaml:"gid,omitempty" json:"gid,omitempty"` GID string `yaml:"gid,omitempty" json:"gid,omitempty"`
Mode *uint32 `yaml:"mode,omitempty" json:"mode,omitempty"` Mode *FileMode `yaml:"mode,omitempty" json:"mode,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` 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 // ServiceConfigObjConfig is the config obj configuration for a service
type ServiceConfigObjConfig FileReferenceConfig type ServiceConfigObjConfig FileReferenceConfig

View File

@ -41,7 +41,6 @@ func ResolveSymbolicLink(path string) (string, error) {
return path, nil return path, nil
} }
return strings.Replace(path, part, sym, 1), nil return strings.Replace(path, part, sym, 1), nil
} }
// getSymbolinkLink parses all parts of the path and returns the // 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 { func checkFileObject(keys ...string) checkerFunc {
return func(value any, p tree.Path) error { return func(value any, p tree.Path) error {
v := value.(map[string]any) v := value.(map[string]any)
count := 0 count := 0
for _, s := range keys { for _, s := range keys {
@ -100,8 +99,8 @@ func checkPath(value any, p tree.Path) error {
func checkDeviceRequest(value any, p tree.Path) error { func checkDeviceRequest(value any, p tree.Path) error {
v := value.(map[string]any) v := value.(map[string]any)
_, hasCount := v["count"] _, hasCount := v["count"]
_, hasIds := v["device_ids"] _, hasIDs := v["device_ids"]
if hasCount && hasIds { if hasCount && hasIDs {
return fmt.Errorf(`%s: "count" and "device_ids" attributes are exclusive`, p) return fmt.Errorf(`%s: "count" and "device_ids" attributes are exclusive`, p)
} }
return nil 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 # github.com/cespare/xxhash/v2 v2.3.0
## explicit; go 1.11 ## explicit; go 1.11
github.com/cespare/xxhash/v2 github.com/cespare/xxhash/v2
# github.com/compose-spec/compose-go/v2 v2.4.8 # github.com/compose-spec/compose-go/v2 v2.4.9
## explicit; go 1.21 ## explicit; go 1.23
github.com/compose-spec/compose-go/v2/cli github.com/compose-spec/compose-go/v2/cli
github.com/compose-spec/compose-go/v2/consts github.com/compose-spec/compose-go/v2/consts
github.com/compose-spec/compose-go/v2/dotenv github.com/compose-spec/compose-go/v2/dotenv
@ -762,6 +762,9 @@ github.com/xeipuuv/gojsonreference
# github.com/xeipuuv/gojsonschema v1.2.0 # github.com/xeipuuv/gojsonschema v1.2.0
## explicit ## explicit
github.com/xeipuuv/gojsonschema 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 # github.com/zclconf/go-cty v1.16.0
## explicit; go 1.18 ## explicit; go 1.18
github.com/zclconf/go-cty/cty github.com/zclconf/go-cty/cty