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
33 changed files with 637 additions and 164 deletions

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