fix(container): enforce deep-copy contract on AttachmentConfigProvider
- commit
- 6a0be67c77f6d7f5d4286adcfbe4e6025a980630
- author
- brice · · 2026-03-15 16:28 UTC
- parent
- eb2edd1
- signature
- unsigned
2 files · +25 -4
M internal/usecase/container/config_provider.go +18 -2
| @@ -4,10 +4,26 @@ package container | |||
| 4 | 4 | // for attachment and network group resolution. It decouples the container | |
| 5 | 5 | // service from the full ConfigService interface while ensuring fresh reads | |
| 6 | 6 | // at decision time (deploy, restart). | |
| 7 | + | // | |
| 8 | + | // Implementations must return deep copies of the underlying data so that | |
| 9 | + | // callers cannot mutate provider state. Each call must produce an independent | |
| 10 | + | // map with independently allocated slices. | |
| 7 | 11 | type AttachmentConfigProvider interface { | |
| 8 | - | // GetAttachments returns the current attachment config: domain/group → []image. | |
| 12 | + | // GetAttachments returns a deep copy of the current attachment config: domain/group → []image. | |
| 9 | 13 | GetAttachments() map[string][]string | |
| 10 | 14 | ||
| 11 | - | // GetNetworkGroups returns the current network group config: group → []domain. | |
| 15 | + | // GetNetworkGroups returns a deep copy of the current network group config: group → []domain. | |
| 12 | 16 | GetNetworkGroups() map[string][]string | |
| 13 | 17 | } | |
| 18 | + | ||
| 19 | + | // deepCopyStringMap returns a deep copy of a map[string][]string. | |
| 20 | + | func deepCopyStringMap(m map[string][]string) map[string][]string { | |
| 21 | + | if m == nil { | |
| 22 | + | return nil | |
| 23 | + | } | |
| 24 | + | result := make(map[string][]string, len(m)) | |
| 25 | + | for k, v := range m { | |
| 26 | + | result[k] = append([]string{}, v...) | |
| 27 | + | } | |
| 28 | + | return result | |
| 29 | + | } | |
| @@ -4,10 +4,26 @@ package container | |||||
| 4 | // for attachment and network group resolution. It decouples the container | 4 | // for attachment and network group resolution. It decouples the container | ||
| 5 | // service from the full ConfigService interface while ensuring fresh reads | 5 | // service from the full ConfigService interface while ensuring fresh reads | ||
| 6 | // at decision time (deploy, restart). | 6 | // at decision time (deploy, restart). | ||
| 7 | + | // | |||
| 8 | + | // Implementations must return deep copies of the underlying data so that | |||
| 9 | + | // callers cannot mutate provider state. Each call must produce an independent | |||
| 10 | + | // map with independently allocated slices. | |||
| 7 | type AttachmentConfigProvider interface { | 11 | type AttachmentConfigProvider interface { | ||
| 8 | - | // GetAttachments returns the current attachment config: domain/group → []image. | 12 | + | // GetAttachments returns a deep copy of the current attachment config: domain/group → []image. |
| 9 | GetAttachments() map[string][]string | 13 | GetAttachments() map[string][]string | ||
| 10 | 14 | ||||
| 11 | - | // GetNetworkGroups returns the current network group config: group → []domain. | 15 | + | // GetNetworkGroups returns a deep copy of the current network group config: group → []domain. |
| 12 | GetNetworkGroups() map[string][]string | 16 | GetNetworkGroups() map[string][]string | ||
| 13 | } | 17 | } | ||
| 18 | + | ||||
| 19 | + | // deepCopyStringMap returns a deep copy of a map[string][]string. | |||
| 20 | + | func deepCopyStringMap(m map[string][]string) map[string][]string { | |||
| 21 | + | if m == nil { | |||
| 22 | + | return nil | |||
| 23 | + | } | |||
| 24 | + | result := make(map[string][]string, len(m)) | |||
| 25 | + | for k, v := range m { | |||
| 26 | + | result[k] = append([]string{}, v...) | |||
| 27 | + | } | |||
| 28 | + | return result | |||
| 29 | + | } | |||
M internal/usecase/container/service_test.go +7 -2
| @@ -27,8 +27,13 @@ type fakeConfigProvider struct { | |||
| 27 | 27 | networkGroups map[string][]string | |
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | - | func (f *fakeConfigProvider) GetAttachments() map[string][]string { return f.attachments } | |
| 31 | - | func (f *fakeConfigProvider) GetNetworkGroups() map[string][]string { return f.networkGroups } | |
| 30 | + | func (f *fakeConfigProvider) GetAttachments() map[string][]string { | |
| 31 | + | return deepCopyStringMap(f.attachments) | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func (f *fakeConfigProvider) GetNetworkGroups() map[string][]string { | |
| 35 | + | return deepCopyStringMap(f.networkGroups) | |
| 36 | + | } | |
| 32 | 37 | ||
| 33 | 38 | func testContext() context.Context { | |
| 34 | 39 | return zerowrap.WithCtx(context.Background(), zerowrap.Default()) | |
| @@ -27,8 +27,13 @@ type fakeConfigProvider struct { | |||||
| 27 | networkGroups map[string][]string | 27 | networkGroups map[string][]string | ||
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | - | func (f *fakeConfigProvider) GetAttachments() map[string][]string { return f.attachments } | 30 | + | func (f *fakeConfigProvider) GetAttachments() map[string][]string { |
| 31 | - | func (f *fakeConfigProvider) GetNetworkGroups() map[string][]string { return f.networkGroups } | 31 | + | return deepCopyStringMap(f.attachments) |
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func (f *fakeConfigProvider) GetNetworkGroups() map[string][]string { | |||
| 35 | + | return deepCopyStringMap(f.networkGroups) | |||
| 36 | + | } | |||
| 32 | 37 | ||||
| 33 | func testContext() context.Context { | 38 | func testContext() context.Context { | ||
| 34 | return zerowrap.WithCtx(context.Background(), zerowrap.Default()) | 39 | return zerowrap.WithCtx(context.Background(), zerowrap.Default()) | ||