From 67bdec0ae2d52fde64d8feb59136c77aea95ca6d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 13 Feb 2025 17:21:33 +0100 Subject: [PATCH] Manage secrets|configs.mode as an octal string||number Signed-off-by: Nicolas De Loof --- loader/full-struct_test.go | 42 ++++++++++++------------ loader/interpolate.go | 2 -- loader/loader_test.go | 65 ++++++++++++++++++++++++-------------- types/derived.gen.go | 5 +-- types/types.go | 45 +++++++++++++++++++++++--- 5 files changed, 105 insertions(+), 54 deletions(-) diff --git a/loader/full-struct_test.go b/loader/full-struct_test.go index 187eea59..361eb3f0 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"}, @@ -206,11 +206,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", @@ -417,7 +417,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{ @@ -427,7 +427,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", @@ -648,7 +648,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 @@ -674,7 +674,7 @@ services: target: /my_config uid: "103" gid: "103" - mode: 288 + mode: "0440" container_name: my-web-container depends_on: db: @@ -918,7 +918,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 @@ -1219,7 +1219,7 @@ func fullExampleJSON(workingDir, homeDir string) string { "target": "my_secret", "uid": "103", "gid": "103", - "mode": 288 + "mode": "0440" } ], "tags": [ @@ -1256,7 +1256,7 @@ func fullExampleJSON(workingDir, homeDir string) string { "target": "/my_config", "uid": "103", "gid": "103", - "mode": 288 + "mode": "0440" } ], "container_name": "my-web-container", @@ -1598,7 +1598,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 234107f4..b9ba0bea 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -745,10 +745,10 @@ services: web: configs: - source: appconfig - mode: $theint + mode: "$theint" secrets: - source: super - mode: $theint + mode: "$theint" healthcheck: retries: ${theint} disable: $thebool @@ -835,32 +835,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, @@ -1167,21 +1167,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) { @@ -3750,3 +3737,33 @@ services: assert.Equal(t, len(p.Services["test"].Gpus), 1) assert.Equal(t, p.Services["test"].Gpus[0].Count, types.DeviceCount(-1)) } + +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 445d1cd3..09c6b0b8 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 { @@ -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 { diff --git a/types/types.go b/types/types.go index 4c7baa46..a4c59392 100644 --- a/types/types.go +++ b/types/types.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "sort" + "strconv" "strings" "github.com/docker/go-connections/nat" @@ -604,17 +605,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