diff --git a/loader/full-struct_test.go b/loader/full-struct_test.go index 8c50ef7e..07e258d6 100644 --- a/loader/full-struct_test.go +++ b/loader/full-struct_test.go @@ -72,7 +72,7 @@ func services(workingDir, homeDir string) types.Services { Target: "my_secret", UID: "103", GID: "103", - Mode: uint32Ptr(0o440), + Mode: ptr(types.FileMode(0o440)), }, }, Tags: []string{"foo:v1.0.0", "docker.io/username/foo:my-other-tag", "full_example_project_name:1.0.0"}, @@ -91,7 +91,7 @@ func services(workingDir, homeDir string) types.Services { Target: "/my_config", UID: "103", GID: "103", - Mode: uint32Ptr(0o440), + Mode: ptr(types.FileMode(0o440)), }, }, ContainerName: "my-web-container", @@ -101,10 +101,10 @@ func services(workingDir, homeDir string) types.Services { }, Deploy: &types.DeployConfig{ Mode: "replicated", - Replicas: intPtr(6), + Replicas: ptr(6), Labels: map[string]string{"FOO": "BAR"}, RollbackConfig: &types.UpdateConfig{ - Parallelism: uint64Ptr(3), + Parallelism: ptr(uint64(3)), Delay: types.Duration(10 * time.Second), FailureAction: "continue", Monitor: types.Duration(60 * time.Second), @@ -112,7 +112,7 @@ func services(workingDir, homeDir string) types.Services { Order: "start-first", }, UpdateConfig: &types.UpdateConfig{ - Parallelism: uint64Ptr(3), + Parallelism: ptr(uint64(3)), Delay: types.Duration(10 * time.Second), FailureAction: "continue", Monitor: types.Duration(60 * time.Second), @@ -145,9 +145,9 @@ func services(workingDir, homeDir string) types.Services { }, RestartPolicy: &types.RestartPolicy{ Condition: types.RestartPolicyOnFailure, - Delay: durationPtr(5 * time.Second), - MaxAttempts: uint64Ptr(3), - Window: durationPtr(2 * time.Minute), + Delay: ptr(types.Duration(5 * time.Second)), + MaxAttempts: ptr(uint64(3)), + Window: ptr(types.Duration(2 * time.Minute)), }, Placement: types.Placement{ Constraints: []string{"node=foo"}, @@ -207,11 +207,11 @@ func services(workingDir, homeDir string) types.Services { }, HealthCheck: &types.HealthCheckConfig{ Test: types.HealthCheckTest([]string{"CMD-SHELL", "echo \"hello world\""}), - Interval: durationPtr(10 * time.Second), - Timeout: durationPtr(1 * time.Second), - Retries: uint64Ptr(5), - StartPeriod: durationPtr(15 * time.Second), - StartInterval: durationPtr(5 * time.Second), + Interval: ptr(types.Duration(10 * time.Second)), + Timeout: ptr(types.Duration(1 * time.Second)), + Retries: ptr(uint64(5)), + StartPeriod: ptr(types.Duration(15 * time.Second)), + StartInterval: ptr(types.Duration(5 * time.Second)), }, Hostname: "foo", Image: "redis", @@ -418,7 +418,7 @@ func services(workingDir, homeDir string) types.Services { Target: "my_secret", UID: "103", GID: "103", - Mode: uint32Ptr(0o440), + Mode: ptr(types.FileMode(0o440)), }, }, SecurityOpt: []string{ @@ -428,7 +428,7 @@ func services(workingDir, homeDir string) types.Services { StdinOpen: true, StopSignal: "SIGUSR1", StorageOpt: map[string]string{"size": "20G"}, - StopGracePeriod: durationPtr(20 * time.Second), + StopGracePeriod: ptr(types.Duration(20 * time.Second)), Sysctls: map[string]string{ "net.core.somaxconn": "1024", "net.ipv4.tcp_syncookies": "0", @@ -649,7 +649,7 @@ services: target: my_secret uid: "103" gid: "103" - mode: 288 + mode: "0440" tags: - foo:v1.0.0 - docker.io/username/foo:my-other-tag @@ -675,7 +675,7 @@ services: target: /my_config uid: "103" gid: "103" - mode: 288 + mode: "0440" container_name: my-web-container depends_on: db: @@ -919,7 +919,7 @@ services: target: my_secret uid: "103" gid: "103" - mode: 288 + mode: "0440" security_opt: - label=level:s0:c100,c200 - label=type:svirt_apache_t @@ -1220,7 +1220,7 @@ func fullExampleJSON(workingDir, homeDir string) string { "target": "my_secret", "uid": "103", "gid": "103", - "mode": 288 + "mode": "0440" } ], "tags": [ @@ -1257,7 +1257,7 @@ func fullExampleJSON(workingDir, homeDir string) string { "target": "/my_config", "uid": "103", "gid": "103", - "mode": 288 + "mode": "0440" } ], "container_name": "my-web-container", @@ -1599,7 +1599,7 @@ func fullExampleJSON(workingDir, homeDir string) string { "target": "my_secret", "uid": "103", "gid": "103", - "mode": 288 + "mode": "0440" } ], "security_opt": [ diff --git a/loader/interpolate.go b/loader/interpolate.go index 481c66b5..491de5bd 100644 --- a/loader/interpolate.go +++ b/loader/interpolate.go @@ -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, diff --git a/loader/loader_test.go b/loader/loader_test.go index b510aa8f..e2814f78 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -699,10 +699,10 @@ services: web: configs: - source: appconfig - mode: $theint + mode: "$theint" secrets: - source: super - mode: $theint + mode: "$theint" healthcheck: retries: ${theint} disable: $thebool @@ -789,32 +789,32 @@ networks: Configs: []types.ServiceConfigObjConfig{ { Source: "appconfig", - Mode: uint32Ptr(555), + Mode: ptr(types.FileMode(0o555)), }, }, Secrets: []types.ServiceSecretConfig{ { Source: "super", Target: "/run/secrets/super", - Mode: uint32Ptr(555), + Mode: ptr(types.FileMode(0o555)), }, }, HealthCheck: &types.HealthCheckConfig{ - Retries: uint64Ptr(555), + Retries: ptr(uint64(555)), Disable: true, }, Deploy: &types.DeployConfig{ - Replicas: intPtr(555), + Replicas: ptr(555), UpdateConfig: &types.UpdateConfig{ - Parallelism: uint64Ptr(555), + Parallelism: ptr(uint64(555)), MaxFailureRatio: 3.14, }, RollbackConfig: &types.UpdateConfig{ - Parallelism: uint64Ptr(555), + Parallelism: ptr(uint64(555)), MaxFailureRatio: 3.14, }, RestartPolicy: &types.RestartPolicy{ - MaxAttempts: uint64Ptr(555), + MaxAttempts: ptr(uint64(555)), }, Placement: types.Placement{ MaxReplicas: 555, @@ -1122,21 +1122,8 @@ services: assert.Equal(t, *foo.Scale, 2) } -func durationPtr(value time.Duration) *types.Duration { - result := types.Duration(value) - return &result -} - -func intPtr(value int) *int { - return &value -} - -func uint64Ptr(value uint64) *uint64 { - return &value -} - -func uint32Ptr(value uint32) *uint32 { - return &value +func ptr[T any](t T) *T { + return &t } func TestFullExample(t *testing.T) { @@ -3744,3 +3731,33 @@ services: `) assert.Check(t, strings.Contains(err.Error(), "'services[test].environment': environment variable DEBUG is declared with a trailing space")) } + +func TestFileModeNumber(t *testing.T) { + p, err := loadYAML(` +name: load-file-mode +services: + test: + secrets: + - source: server-certificate + target: server.cert + mode: 0o440 +`) + assert.NilError(t, err) + assert.Equal(t, len(p.Services["test"].Secrets), 1) + assert.Equal(t, *p.Services["test"].Secrets[0].Mode, types.FileMode(0o440)) +} + +func TestFileModeString(t *testing.T) { + p, err := loadYAML(` +name: load-file-mode +services: + test: + secrets: + - source: server-certificate + target: server.cert + mode: "0440" +`) + assert.NilError(t, err) + assert.Equal(t, len(p.Services["test"].Secrets), 1) + assert.Equal(t, *p.Services["test"].Secrets[0].Mode, types.FileMode(0o440)) +} diff --git a/types/derived.gen.go b/types/derived.gen.go index 17b22e66..fd8e059e 100644 --- a/types/derived.gen.go +++ b/types/derived.gen.go @@ -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 { @@ -1892,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 { diff --git a/types/types.go b/types/types.go index 1773f694..fe95444d 100644 --- a/types/types.go +++ b/types/types.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "sort" + "strconv" "strings" "time" @@ -626,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