Merge pull request #121 from bnema/fix/attachment-deploy-lifecycle
fix: deploy/restart handles missing attachment containers (#120)
13 files · +755 -118
M .gitignore +2 -1
| @@ -1,4 +1,5 @@ | |||
| 1 | 1 | gordon | |
| 2 | 2 | .env | |
| 3 | 3 | dist/ | |
| 4 | - | docs/plans/⏎̸ | |
| 4 | + | docs/plans/ | |
| 5 | + | .worktrees/⏎̸ | |
| @@ -1,4 +1,5 @@ | |||||
| 1 | gordon | 1 | gordon | ||
| 2 | .env | 2 | .env | ||
| 3 | dist/ | 3 | dist/ | ||
| 4 | - | docs/plans/⏎̸ | 4 | + | docs/plans/ |
| 5 | + | .worktrees/⏎̸ | |||
M .mockery.yaml +1 -0
| @@ -19,6 +19,7 @@ packages: | |||
| 19 | 19 | SecretProvider: | |
| 20 | 20 | EnvLoader: | |
| 21 | 21 | ContainerLogWriter: | |
| 22 | + | AttachmentConfigProvider: | |
| 22 | 23 | TokenStore: | |
| 23 | 24 | DomainSecretStore: | |
| 24 | 25 | RateLimiter: | |
| @@ -19,6 +19,7 @@ packages: | |||||
| 19 | SecretProvider: | 19 | SecretProvider: | ||
| 20 | EnvLoader: | 20 | EnvLoader: | ||
| 21 | ContainerLogWriter: | 21 | ContainerLogWriter: | ||
| 22 | + | AttachmentConfigProvider: | |||
| 22 | TokenStore: | 23 | TokenStore: | ||
| 23 | DomainSecretStore: | 24 | DomainSecretStore: | ||
| 24 | RateLimiter: | 25 | RateLimiter: | ||
M internal/app/run.go +1 -1
| @@ -1239,7 +1239,7 @@ func createContainerService(ctx context.Context, v *viper.Viper, cfg Config, svc | |||
| 1239 | 1239 | log.Warn().Msg("registry auth disabled; container image pulls will use unauthenticated mode") | |
| 1240 | 1240 | } | |
| 1241 | 1241 | ||
| 1242 | - | return container.NewService(svc.runtime, svc.envLoader, svc.eventBus, svc.logWriter, containerConfig), nil | |
| 1242 | + | return container.NewService(svc.runtime, svc.envLoader, svc.eventBus, svc.logWriter, containerConfig, svc.configSvc), nil | |
| 1243 | 1243 | } | |
| 1244 | 1244 | ||
| 1245 | 1245 | func createBackupService(cfg Config, svc *services, log zerowrap.Logger) (*filesystem.BackupStorage, *backup.Service, error) { | |
| @@ -1239,7 +1239,7 @@ func createContainerService(ctx context.Context, v *viper.Viper, cfg Config, svc | |||||
| 1239 | log.Warn().Msg("registry auth disabled; container image pulls will use unauthenticated mode") | 1239 | log.Warn().Msg("registry auth disabled; container image pulls will use unauthenticated mode") | ||
| 1240 | } | 1240 | } | ||
| 1241 | 1241 | ||||
| 1242 | - | return container.NewService(svc.runtime, svc.envLoader, svc.eventBus, svc.logWriter, containerConfig), nil | 1242 | + | return container.NewService(svc.runtime, svc.envLoader, svc.eventBus, svc.logWriter, containerConfig, svc.configSvc), nil |
| 1243 | } | 1243 | } | ||
| 1244 | 1244 | ||||
| 1245 | func createBackupService(cfg Config, svc *services, log zerowrap.Logger) (*filesystem.BackupStorage, *backup.Service, error) { | 1245 | func createBackupService(cfg Config, svc *services, log zerowrap.Logger) (*filesystem.BackupStorage, *backup.Service, error) { | ||
A internal/boundaries/out/attachment_config.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | package out | |
| 2 | + | ||
| 3 | + | // AttachmentConfigSnapshot holds a consistent point-in-time snapshot of | |
| 4 | + | // attachment and network group configuration. | |
| 5 | + | type AttachmentConfigSnapshot struct { | |
| 6 | + | Attachments map[string][]string // domain/group -> []image | |
| 7 | + | NetworkGroups map[string][]string // group -> []domain | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | // AttachmentConfigProvider is a read-only view into the live config service | |
| 11 | + | // for attachment and network group resolution. It decouples the container | |
| 12 | + | // service from the full ConfigService interface while ensuring fresh reads | |
| 13 | + | // at decision time (deploy, restart). | |
| 14 | + | // | |
| 15 | + | // Implementations must return deep copies of the underlying data so that | |
| 16 | + | // callers cannot mutate provider state. Each call must produce an independent | |
| 17 | + | // map with independently allocated slices. | |
| 18 | + | type AttachmentConfigProvider interface { | |
| 19 | + | // GetAttachmentConfig returns a consistent snapshot of attachments and | |
| 20 | + | // network groups, read under a single lock to prevent cross-field races. | |
| 21 | + | GetAttachmentConfig() AttachmentConfigSnapshot | |
| 22 | + | ||
| 23 | + | // GetAttachments returns a deep copy of the current attachment config: domain/group -> []image. | |
| 24 | + | GetAttachments() map[string][]string | |
| 25 | + | ||
| 26 | + | // GetNetworkGroups returns a deep copy of the current network group config: group -> []domain. | |
| 27 | + | GetNetworkGroups() map[string][]string | |
| 28 | + | } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | package out | |||
| 2 | + | ||||
| 3 | + | // AttachmentConfigSnapshot holds a consistent point-in-time snapshot of | |||
| 4 | + | // attachment and network group configuration. | |||
| 5 | + | type AttachmentConfigSnapshot struct { | |||
| 6 | + | Attachments map[string][]string // domain/group -> []image | |||
| 7 | + | NetworkGroups map[string][]string // group -> []domain | |||
| 8 | + | } | |||
| 9 | + | ||||
| 10 | + | // AttachmentConfigProvider is a read-only view into the live config service | |||
| 11 | + | // for attachment and network group resolution. It decouples the container | |||
| 12 | + | // service from the full ConfigService interface while ensuring fresh reads | |||
| 13 | + | // at decision time (deploy, restart). | |||
| 14 | + | // | |||
| 15 | + | // Implementations must return deep copies of the underlying data so that | |||
| 16 | + | // callers cannot mutate provider state. Each call must produce an independent | |||
| 17 | + | // map with independently allocated slices. | |||
| 18 | + | type AttachmentConfigProvider interface { | |||
| 19 | + | // GetAttachmentConfig returns a consistent snapshot of attachments and | |||
| 20 | + | // network groups, read under a single lock to prevent cross-field races. | |||
| 21 | + | GetAttachmentConfig() AttachmentConfigSnapshot | |||
| 22 | + | ||||
| 23 | + | // GetAttachments returns a deep copy of the current attachment config: domain/group -> []image. | |||
| 24 | + | GetAttachments() map[string][]string | |||
| 25 | + | ||||
| 26 | + | // GetNetworkGroups returns a deep copy of the current network group config: group -> []domain. | |||
| 27 | + | GetNetworkGroups() map[string][]string | |||
| 28 | + | } | |||
A internal/boundaries/out/mocks/mock_attachment_config_provider.go +173 -0
| @@ -0,0 +1,173 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "github.com/bnema/gordon/internal/boundaries/out" | |
| 9 | + | mock "github.com/stretchr/testify/mock" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // NewMockAttachmentConfigProvider creates a new instance of MockAttachmentConfigProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 13 | + | // The first argument is typically a *testing.T value. | |
| 14 | + | func NewMockAttachmentConfigProvider(t interface { | |
| 15 | + | mock.TestingT | |
| 16 | + | Cleanup(func()) | |
| 17 | + | }) *MockAttachmentConfigProvider { | |
| 18 | + | mock := &MockAttachmentConfigProvider{} | |
| 19 | + | mock.Mock.Test(t) | |
| 20 | + | ||
| 21 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 22 | + | ||
| 23 | + | return mock | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // MockAttachmentConfigProvider is an autogenerated mock type for the AttachmentConfigProvider type | |
| 27 | + | type MockAttachmentConfigProvider struct { | |
| 28 | + | mock.Mock | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | type MockAttachmentConfigProvider_Expecter struct { | |
| 32 | + | mock *mock.Mock | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func (_m *MockAttachmentConfigProvider) EXPECT() *MockAttachmentConfigProvider_Expecter { | |
| 36 | + | return &MockAttachmentConfigProvider_Expecter{mock: &_m.Mock} | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // GetAttachmentConfig provides a mock function for the type MockAttachmentConfigProvider | |
| 40 | + | func (_mock *MockAttachmentConfigProvider) GetAttachmentConfig() out.AttachmentConfigSnapshot { | |
| 41 | + | ret := _mock.Called() | |
| 42 | + | ||
| 43 | + | if len(ret) == 0 { | |
| 44 | + | panic("no return value specified for GetAttachmentConfig") | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | var r0 out.AttachmentConfigSnapshot | |
| 48 | + | if returnFunc, ok := ret.Get(0).(func() out.AttachmentConfigSnapshot); ok { | |
| 49 | + | r0 = returnFunc() | |
| 50 | + | } else { | |
| 51 | + | r0 = ret.Get(0).(out.AttachmentConfigSnapshot) | |
| 52 | + | } | |
| 53 | + | return r0 | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // MockAttachmentConfigProvider_GetAttachmentConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAttachmentConfig' | |
| 57 | + | type MockAttachmentConfigProvider_GetAttachmentConfig_Call struct { | |
| 58 | + | *mock.Call | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // GetAttachmentConfig is a helper method to define mock.On call | |
| 62 | + | func (_e *MockAttachmentConfigProvider_Expecter) GetAttachmentConfig() *MockAttachmentConfigProvider_GetAttachmentConfig_Call { | |
| 63 | + | return &MockAttachmentConfigProvider_GetAttachmentConfig_Call{Call: _e.mock.On("GetAttachmentConfig")} | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func (_c *MockAttachmentConfigProvider_GetAttachmentConfig_Call) Run(run func()) *MockAttachmentConfigProvider_GetAttachmentConfig_Call { | |
| 67 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 68 | + | run() | |
| 69 | + | }) | |
| 70 | + | return _c | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | func (_c *MockAttachmentConfigProvider_GetAttachmentConfig_Call) Return(attachmentConfigSnapshot out.AttachmentConfigSnapshot) *MockAttachmentConfigProvider_GetAttachmentConfig_Call { | |
| 74 | + | _c.Call.Return(attachmentConfigSnapshot) | |
| 75 | + | return _c | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func (_c *MockAttachmentConfigProvider_GetAttachmentConfig_Call) RunAndReturn(run func() out.AttachmentConfigSnapshot) *MockAttachmentConfigProvider_GetAttachmentConfig_Call { | |
| 79 | + | _c.Call.Return(run) | |
| 80 | + | return _c | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // GetAttachments provides a mock function for the type MockAttachmentConfigProvider | |
| 84 | + | func (_mock *MockAttachmentConfigProvider) GetAttachments() map[string][]string { | |
| 85 | + | ret := _mock.Called() | |
| 86 | + | ||
| 87 | + | if len(ret) == 0 { | |
| 88 | + | panic("no return value specified for GetAttachments") | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | var r0 map[string][]string | |
| 92 | + | if returnFunc, ok := ret.Get(0).(func() map[string][]string); ok { | |
| 93 | + | r0 = returnFunc() | |
| 94 | + | } else { | |
| 95 | + | if ret.Get(0) != nil { | |
| 96 | + | r0 = ret.Get(0).(map[string][]string) | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | return r0 | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // MockAttachmentConfigProvider_GetAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAttachments' | |
| 103 | + | type MockAttachmentConfigProvider_GetAttachments_Call struct { | |
| 104 | + | *mock.Call | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // GetAttachments is a helper method to define mock.On call | |
| 108 | + | func (_e *MockAttachmentConfigProvider_Expecter) GetAttachments() *MockAttachmentConfigProvider_GetAttachments_Call { | |
| 109 | + | return &MockAttachmentConfigProvider_GetAttachments_Call{Call: _e.mock.On("GetAttachments")} | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | func (_c *MockAttachmentConfigProvider_GetAttachments_Call) Run(run func()) *MockAttachmentConfigProvider_GetAttachments_Call { | |
| 113 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 114 | + | run() | |
| 115 | + | }) | |
| 116 | + | return _c | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | func (_c *MockAttachmentConfigProvider_GetAttachments_Call) Return(stringToStrings map[string][]string) *MockAttachmentConfigProvider_GetAttachments_Call { | |
| 120 | + | _c.Call.Return(stringToStrings) | |
| 121 | + | return _c | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | func (_c *MockAttachmentConfigProvider_GetAttachments_Call) RunAndReturn(run func() map[string][]string) *MockAttachmentConfigProvider_GetAttachments_Call { | |
| 125 | + | _c.Call.Return(run) | |
| 126 | + | return _c | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | // GetNetworkGroups provides a mock function for the type MockAttachmentConfigProvider | |
| 130 | + | func (_mock *MockAttachmentConfigProvider) GetNetworkGroups() map[string][]string { | |
| 131 | + | ret := _mock.Called() | |
| 132 | + | ||
| 133 | + | if len(ret) == 0 { | |
| 134 | + | panic("no return value specified for GetNetworkGroups") | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | var r0 map[string][]string | |
| 138 | + | if returnFunc, ok := ret.Get(0).(func() map[string][]string); ok { | |
| 139 | + | r0 = returnFunc() | |
| 140 | + | } else { | |
| 141 | + | if ret.Get(0) != nil { | |
| 142 | + | r0 = ret.Get(0).(map[string][]string) | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | return r0 | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | // MockAttachmentConfigProvider_GetNetworkGroups_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetNetworkGroups' | |
| 149 | + | type MockAttachmentConfigProvider_GetNetworkGroups_Call struct { | |
| 150 | + | *mock.Call | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // GetNetworkGroups is a helper method to define mock.On call | |
| 154 | + | func (_e *MockAttachmentConfigProvider_Expecter) GetNetworkGroups() *MockAttachmentConfigProvider_GetNetworkGroups_Call { | |
| 155 | + | return &MockAttachmentConfigProvider_GetNetworkGroups_Call{Call: _e.mock.On("GetNetworkGroups")} | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | func (_c *MockAttachmentConfigProvider_GetNetworkGroups_Call) Run(run func()) *MockAttachmentConfigProvider_GetNetworkGroups_Call { | |
| 159 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 160 | + | run() | |
| 161 | + | }) | |
| 162 | + | return _c | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | func (_c *MockAttachmentConfigProvider_GetNetworkGroups_Call) Return(stringToStrings map[string][]string) *MockAttachmentConfigProvider_GetNetworkGroups_Call { | |
| 166 | + | _c.Call.Return(stringToStrings) | |
| 167 | + | return _c | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | func (_c *MockAttachmentConfigProvider_GetNetworkGroups_Call) RunAndReturn(run func() map[string][]string) *MockAttachmentConfigProvider_GetNetworkGroups_Call { | |
| 171 | + | _c.Call.Return(run) | |
| 172 | + | return _c | |
| 173 | + | } | |
| @@ -0,0 +1,173 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package mocks | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "github.com/bnema/gordon/internal/boundaries/out" | |||
| 9 | + | mock "github.com/stretchr/testify/mock" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // NewMockAttachmentConfigProvider creates a new instance of MockAttachmentConfigProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 13 | + | // The first argument is typically a *testing.T value. | |||
| 14 | + | func NewMockAttachmentConfigProvider(t interface { | |||
| 15 | + | mock.TestingT | |||
| 16 | + | Cleanup(func()) | |||
| 17 | + | }) *MockAttachmentConfigProvider { | |||
| 18 | + | mock := &MockAttachmentConfigProvider{} | |||
| 19 | + | mock.Mock.Test(t) | |||
| 20 | + | ||||
| 21 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |||
| 22 | + | ||||
| 23 | + | return mock | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // MockAttachmentConfigProvider is an autogenerated mock type for the AttachmentConfigProvider type | |||
| 27 | + | type MockAttachmentConfigProvider struct { | |||
| 28 | + | mock.Mock | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | type MockAttachmentConfigProvider_Expecter struct { | |||
| 32 | + | mock *mock.Mock | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func (_m *MockAttachmentConfigProvider) EXPECT() *MockAttachmentConfigProvider_Expecter { | |||
| 36 | + | return &MockAttachmentConfigProvider_Expecter{mock: &_m.Mock} | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // GetAttachmentConfig provides a mock function for the type MockAttachmentConfigProvider | |||
| 40 | + | func (_mock *MockAttachmentConfigProvider) GetAttachmentConfig() out.AttachmentConfigSnapshot { | |||
| 41 | + | ret := _mock.Called() | |||
| 42 | + | ||||
| 43 | + | if len(ret) == 0 { | |||
| 44 | + | panic("no return value specified for GetAttachmentConfig") | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | var r0 out.AttachmentConfigSnapshot | |||
| 48 | + | if returnFunc, ok := ret.Get(0).(func() out.AttachmentConfigSnapshot); ok { | |||
| 49 | + | r0 = returnFunc() | |||
| 50 | + | } else { | |||
| 51 | + | r0 = ret.Get(0).(out.AttachmentConfigSnapshot) | |||
| 52 | + | } | |||
| 53 | + | return r0 | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // MockAttachmentConfigProvider_GetAttachmentConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAttachmentConfig' | |||
| 57 | + | type MockAttachmentConfigProvider_GetAttachmentConfig_Call struct { | |||
| 58 | + | *mock.Call | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // GetAttachmentConfig is a helper method to define mock.On call | |||
| 62 | + | func (_e *MockAttachmentConfigProvider_Expecter) GetAttachmentConfig() *MockAttachmentConfigProvider_GetAttachmentConfig_Call { | |||
| 63 | + | return &MockAttachmentConfigProvider_GetAttachmentConfig_Call{Call: _e.mock.On("GetAttachmentConfig")} | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func (_c *MockAttachmentConfigProvider_GetAttachmentConfig_Call) Run(run func()) *MockAttachmentConfigProvider_GetAttachmentConfig_Call { | |||
| 67 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 68 | + | run() | |||
| 69 | + | }) | |||
| 70 | + | return _c | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func (_c *MockAttachmentConfigProvider_GetAttachmentConfig_Call) Return(attachmentConfigSnapshot out.AttachmentConfigSnapshot) *MockAttachmentConfigProvider_GetAttachmentConfig_Call { | |||
| 74 | + | _c.Call.Return(attachmentConfigSnapshot) | |||
| 75 | + | return _c | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func (_c *MockAttachmentConfigProvider_GetAttachmentConfig_Call) RunAndReturn(run func() out.AttachmentConfigSnapshot) *MockAttachmentConfigProvider_GetAttachmentConfig_Call { | |||
| 79 | + | _c.Call.Return(run) | |||
| 80 | + | return _c | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // GetAttachments provides a mock function for the type MockAttachmentConfigProvider | |||
| 84 | + | func (_mock *MockAttachmentConfigProvider) GetAttachments() map[string][]string { | |||
| 85 | + | ret := _mock.Called() | |||
| 86 | + | ||||
| 87 | + | if len(ret) == 0 { | |||
| 88 | + | panic("no return value specified for GetAttachments") | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | var r0 map[string][]string | |||
| 92 | + | if returnFunc, ok := ret.Get(0).(func() map[string][]string); ok { | |||
| 93 | + | r0 = returnFunc() | |||
| 94 | + | } else { | |||
| 95 | + | if ret.Get(0) != nil { | |||
| 96 | + | r0 = ret.Get(0).(map[string][]string) | |||
| 97 | + | } | |||
| 98 | + | } | |||
| 99 | + | return r0 | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // MockAttachmentConfigProvider_GetAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAttachments' | |||
| 103 | + | type MockAttachmentConfigProvider_GetAttachments_Call struct { | |||
| 104 | + | *mock.Call | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // GetAttachments is a helper method to define mock.On call | |||
| 108 | + | func (_e *MockAttachmentConfigProvider_Expecter) GetAttachments() *MockAttachmentConfigProvider_GetAttachments_Call { | |||
| 109 | + | return &MockAttachmentConfigProvider_GetAttachments_Call{Call: _e.mock.On("GetAttachments")} | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | func (_c *MockAttachmentConfigProvider_GetAttachments_Call) Run(run func()) *MockAttachmentConfigProvider_GetAttachments_Call { | |||
| 113 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 114 | + | run() | |||
| 115 | + | }) | |||
| 116 | + | return _c | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | func (_c *MockAttachmentConfigProvider_GetAttachments_Call) Return(stringToStrings map[string][]string) *MockAttachmentConfigProvider_GetAttachments_Call { | |||
| 120 | + | _c.Call.Return(stringToStrings) | |||
| 121 | + | return _c | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | func (_c *MockAttachmentConfigProvider_GetAttachments_Call) RunAndReturn(run func() map[string][]string) *MockAttachmentConfigProvider_GetAttachments_Call { | |||
| 125 | + | _c.Call.Return(run) | |||
| 126 | + | return _c | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | // GetNetworkGroups provides a mock function for the type MockAttachmentConfigProvider | |||
| 130 | + | func (_mock *MockAttachmentConfigProvider) GetNetworkGroups() map[string][]string { | |||
| 131 | + | ret := _mock.Called() | |||
| 132 | + | ||||
| 133 | + | if len(ret) == 0 { | |||
| 134 | + | panic("no return value specified for GetNetworkGroups") | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | var r0 map[string][]string | |||
| 138 | + | if returnFunc, ok := ret.Get(0).(func() map[string][]string); ok { | |||
| 139 | + | r0 = returnFunc() | |||
| 140 | + | } else { | |||
| 141 | + | if ret.Get(0) != nil { | |||
| 142 | + | r0 = ret.Get(0).(map[string][]string) | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 145 | + | return r0 | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | // MockAttachmentConfigProvider_GetNetworkGroups_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetNetworkGroups' | |||
| 149 | + | type MockAttachmentConfigProvider_GetNetworkGroups_Call struct { | |||
| 150 | + | *mock.Call | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // GetNetworkGroups is a helper method to define mock.On call | |||
| 154 | + | func (_e *MockAttachmentConfigProvider_Expecter) GetNetworkGroups() *MockAttachmentConfigProvider_GetNetworkGroups_Call { | |||
| 155 | + | return &MockAttachmentConfigProvider_GetNetworkGroups_Call{Call: _e.mock.On("GetNetworkGroups")} | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | func (_c *MockAttachmentConfigProvider_GetNetworkGroups_Call) Run(run func()) *MockAttachmentConfigProvider_GetNetworkGroups_Call { | |||
| 159 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 160 | + | run() | |||
| 161 | + | }) | |||
| 162 | + | return _c | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | func (_c *MockAttachmentConfigProvider_GetNetworkGroups_Call) Return(stringToStrings map[string][]string) *MockAttachmentConfigProvider_GetNetworkGroups_Call { | |||
| 166 | + | _c.Call.Return(stringToStrings) | |||
| 167 | + | return _c | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | func (_c *MockAttachmentConfigProvider_GetNetworkGroups_Call) RunAndReturn(run func() map[string][]string) *MockAttachmentConfigProvider_GetNetworkGroups_Call { | |||
| 171 | + | _c.Call.Return(run) | |||
| 172 | + | return _c | |||
| 173 | + | } | |||
M internal/domain/errors.go +1 -0
| @@ -71,4 +71,5 @@ var ( | |||
| 71 | 71 | ErrAttachmentExists = errors.New("attachment already exists") | |
| 72 | 72 | ErrAttachmentImageEmpty = errors.New("attachment image cannot be empty") | |
| 73 | 73 | ErrAttachmentTargetEmpty = errors.New("attachment target (domain or group) cannot be empty") | |
| 74 | + | ErrAttachmentNotDeployed = errors.New("configured attachment is not deployed; run gordon deploy to create it") | |
| 74 | 75 | ) | |
| @@ -71,4 +71,5 @@ var ( | |||||
| 71 | ErrAttachmentExists = errors.New("attachment already exists") | 71 | ErrAttachmentExists = errors.New("attachment already exists") | ||
| 72 | ErrAttachmentImageEmpty = errors.New("attachment image cannot be empty") | 72 | ErrAttachmentImageEmpty = errors.New("attachment image cannot be empty") | ||
| 73 | ErrAttachmentTargetEmpty = errors.New("attachment target (domain or group) cannot be empty") | 73 | ErrAttachmentTargetEmpty = errors.New("attachment target (domain or group) cannot be empty") | ||
| 74 | + | ErrAttachmentNotDeployed = errors.New("configured attachment is not deployed; run gordon deploy to create it") | |||
| 74 | ) | 75 | ) | ||
M internal/usecase/config/service.go +22 -0
| @@ -860,6 +860,28 @@ func (s *Service) GetNetworkGroups() map[string][]string { | |||
| 860 | 860 | return result | |
| 861 | 861 | } | |
| 862 | 862 | ||
| 863 | + | // GetAttachmentConfig returns a consistent snapshot of attachments and network | |
| 864 | + | // groups under a single lock, preventing cross-field races during config reloads. | |
| 865 | + | func (s *Service) GetAttachmentConfig() out.AttachmentConfigSnapshot { | |
| 866 | + | s.mu.RLock() | |
| 867 | + | defer s.mu.RUnlock() | |
| 868 | + | ||
| 869 | + | attachments := make(map[string][]string, len(s.config.Attachments)) | |
| 870 | + | for k, v := range s.config.Attachments { | |
| 871 | + | attachments[k] = append([]string{}, v...) | |
| 872 | + | } | |
| 873 | + | ||
| 874 | + | networkGroups := make(map[string][]string, len(s.config.NetworkGroups)) | |
| 875 | + | for k, v := range s.config.NetworkGroups { | |
| 876 | + | networkGroups[k] = append([]string{}, v...) | |
| 877 | + | } | |
| 878 | + | ||
| 879 | + | return out.AttachmentConfigSnapshot{ | |
| 880 | + | Attachments: attachments, | |
| 881 | + | NetworkGroups: networkGroups, | |
| 882 | + | } | |
| 883 | + | } | |
| 884 | + | ||
| 863 | 885 | // GetAttachments returns attachment configuration. | |
| 864 | 886 | // Deprecated: Use GetAllAttachments instead. | |
| 865 | 887 | func (s *Service) GetAttachments() map[string][]string { | |
| @@ -860,6 +860,28 @@ func (s *Service) GetNetworkGroups() map[string][]string { | |||||
| 860 | return result | 860 | return result | ||
| 861 | } | 861 | } | ||
| 862 | 862 | ||||
| 863 | + | // GetAttachmentConfig returns a consistent snapshot of attachments and network | |||
| 864 | + | // groups under a single lock, preventing cross-field races during config reloads. | |||
| 865 | + | func (s *Service) GetAttachmentConfig() out.AttachmentConfigSnapshot { | |||
| 866 | + | s.mu.RLock() | |||
| 867 | + | defer s.mu.RUnlock() | |||
| 868 | + | ||||
| 869 | + | attachments := make(map[string][]string, len(s.config.Attachments)) | |||
| 870 | + | for k, v := range s.config.Attachments { | |||
| 871 | + | attachments[k] = append([]string{}, v...) | |||
| 872 | + | } | |||
| 873 | + | ||||
| 874 | + | networkGroups := make(map[string][]string, len(s.config.NetworkGroups)) | |||
| 875 | + | for k, v := range s.config.NetworkGroups { | |||
| 876 | + | networkGroups[k] = append([]string{}, v...) | |||
| 877 | + | } | |||
| 878 | + | ||||
| 879 | + | return out.AttachmentConfigSnapshot{ | |||
| 880 | + | Attachments: attachments, | |||
| 881 | + | NetworkGroups: networkGroups, | |||
| 882 | + | } | |||
| 883 | + | } | |||
| 884 | + | ||||
| 863 | // GetAttachments returns attachment configuration. | 885 | // GetAttachments returns attachment configuration. | ||
| 864 | // Deprecated: Use GetAllAttachments instead. | 886 | // Deprecated: Use GetAllAttachments instead. | ||
| 865 | func (s *Service) GetAttachments() map[string][]string { | 887 | func (s *Service) GetAttachments() map[string][]string { | ||
M internal/usecase/config/service_test.go +30 -0
| @@ -487,6 +487,36 @@ func TestService_GetAttachments(t *testing.T) { | |||
| 487 | 487 | assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, attachments["app.example.com"]) | |
| 488 | 488 | } | |
| 489 | 489 | ||
| 490 | + | func TestService_GetAttachmentConfig(t *testing.T) { | |
| 491 | + | v := viper.New() | |
| 492 | + | v.Set("attachments", map[string]interface{}{ | |
| 493 | + | "app.example.com": []interface{}{"redis:latest", "postgres:18"}, | |
| 494 | + | }) | |
| 495 | + | v.Set("network_groups", map[string]interface{}{ | |
| 496 | + | "shared": []interface{}{"app.example.com", "api.example.com"}, | |
| 497 | + | }) | |
| 498 | + | ||
| 499 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 500 | + | svc := NewService(v, eventBus) | |
| 501 | + | ctx := testContext() | |
| 502 | + | ||
| 503 | + | _ = svc.Load(ctx) | |
| 504 | + | ||
| 505 | + | snapshot := svc.GetAttachmentConfig() | |
| 506 | + | ||
| 507 | + | assert.Len(t, snapshot.Attachments, 1) | |
| 508 | + | assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, snapshot.Attachments["app.example.com"]) | |
| 509 | + | assert.Len(t, snapshot.NetworkGroups, 1) | |
| 510 | + | assert.ElementsMatch(t, []string{"app.example.com", "api.example.com"}, snapshot.NetworkGroups["shared"]) | |
| 511 | + | ||
| 512 | + | snapshot.Attachments["app.example.com"][0] = "mutated" | |
| 513 | + | snapshot.NetworkGroups["shared"][0] = "mutated.example.com" | |
| 514 | + | ||
| 515 | + | freshSnapshot := svc.GetAttachmentConfig() | |
| 516 | + | assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, freshSnapshot.Attachments["app.example.com"]) | |
| 517 | + | assert.ElementsMatch(t, []string{"app.example.com", "api.example.com"}, freshSnapshot.NetworkGroups["shared"]) | |
| 518 | + | } | |
| 519 | + | ||
| 490 | 520 | func TestService_GetAllAttachments(t *testing.T) { | |
| 491 | 521 | t.Run("returns all attachments", func(t *testing.T) { | |
| 492 | 522 | v := viper.New() | |
| @@ -487,6 +487,36 @@ func TestService_GetAttachments(t *testing.T) { | |||||
| 487 | assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, attachments["app.example.com"]) | 487 | assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, attachments["app.example.com"]) | ||
| 488 | } | 488 | } | ||
| 489 | 489 | ||||
| 490 | + | func TestService_GetAttachmentConfig(t *testing.T) { | |||
| 491 | + | v := viper.New() | |||
| 492 | + | v.Set("attachments", map[string]interface{}{ | |||
| 493 | + | "app.example.com": []interface{}{"redis:latest", "postgres:18"}, | |||
| 494 | + | }) | |||
| 495 | + | v.Set("network_groups", map[string]interface{}{ | |||
| 496 | + | "shared": []interface{}{"app.example.com", "api.example.com"}, | |||
| 497 | + | }) | |||
| 498 | + | ||||
| 499 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 500 | + | svc := NewService(v, eventBus) | |||
| 501 | + | ctx := testContext() | |||
| 502 | + | ||||
| 503 | + | _ = svc.Load(ctx) | |||
| 504 | + | ||||
| 505 | + | snapshot := svc.GetAttachmentConfig() | |||
| 506 | + | ||||
| 507 | + | assert.Len(t, snapshot.Attachments, 1) | |||
| 508 | + | assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, snapshot.Attachments["app.example.com"]) | |||
| 509 | + | assert.Len(t, snapshot.NetworkGroups, 1) | |||
| 510 | + | assert.ElementsMatch(t, []string{"app.example.com", "api.example.com"}, snapshot.NetworkGroups["shared"]) | |||
| 511 | + | ||||
| 512 | + | snapshot.Attachments["app.example.com"][0] = "mutated" | |||
| 513 | + | snapshot.NetworkGroups["shared"][0] = "mutated.example.com" | |||
| 514 | + | ||||
| 515 | + | freshSnapshot := svc.GetAttachmentConfig() | |||
| 516 | + | assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, freshSnapshot.Attachments["app.example.com"]) | |||
| 517 | + | assert.ElementsMatch(t, []string{"app.example.com", "api.example.com"}, freshSnapshot.NetworkGroups["shared"]) | |||
| 518 | + | } | |||
| 519 | + | ||||
| 490 | func TestService_GetAllAttachments(t *testing.T) { | 520 | func TestService_GetAllAttachments(t *testing.T) { | ||
| 491 | t.Run("returns all attachments", func(t *testing.T) { | 521 | t.Run("returns all attachments", func(t *testing.T) { | ||
| 492 | v := viper.New() | 522 | v := viper.New() | ||
A internal/usecase/container/config_provider.go +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | package container | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/gordon/internal/boundaries/out" | |
| 4 | + | ||
| 5 | + | // AttachmentConfigProvider is an alias for the boundary interface, kept for | |
| 6 | + | // internal readability. | |
| 7 | + | type AttachmentConfigProvider = out.AttachmentConfigProvider | |
| @@ -0,0 +1,7 @@ | |||||
| 1 | + | package container | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/gordon/internal/boundaries/out" | |||
| 4 | + | ||||
| 5 | + | // AttachmentConfigProvider is an alias for the boundary interface, kept for | |||
| 6 | + | // internal readability. | |||
| 7 | + | type AttachmentConfigProvider = out.AttachmentConfigProvider | |||
M internal/usecase/container/routes_test.go +5 -5
| @@ -12,7 +12,7 @@ import ( | |||
| 12 | 12 | ||
| 13 | 13 | func TestService_ListRoutesWithDetails(t *testing.T) { | |
| 14 | 14 | runtime := mocks.NewMockContainerRuntime(t) | |
| 15 | - | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |
| 15 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}, nil) | |
| 16 | 16 | ctx := testContext() | |
| 17 | 17 | ||
| 18 | 18 | svc.containers["app.example.com"] = &domain.Container{ | |
| @@ -80,7 +80,7 @@ func TestService_ListRoutesWithDetails(t *testing.T) { | |||
| 80 | 80 | ||
| 81 | 81 | func TestService_ListRoutesWithDetails_Empty(t *testing.T) { | |
| 82 | 82 | runtime := mocks.NewMockContainerRuntime(t) | |
| 83 | - | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |
| 83 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}, nil) | |
| 84 | 84 | ctx := testContext() | |
| 85 | 85 | ||
| 86 | 86 | // getAllAttachments is called even with empty containers | |
| @@ -93,7 +93,7 @@ func TestService_ListRoutesWithDetails_Empty(t *testing.T) { | |||
| 93 | 93 | ||
| 94 | 94 | func TestService_ListRoutesWithDetails_NetworkError(t *testing.T) { | |
| 95 | 95 | runtime := mocks.NewMockContainerRuntime(t) | |
| 96 | - | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |
| 96 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}, nil) | |
| 97 | 97 | ctx := testContext() | |
| 98 | 98 | ||
| 99 | 99 | svc.containers["app.example.com"] = &domain.Container{ | |
| @@ -113,7 +113,7 @@ func TestService_ListRoutesWithDetails_NetworkError(t *testing.T) { | |||
| 113 | 113 | ||
| 114 | 114 | func TestService_ListAttachments(t *testing.T) { | |
| 115 | 115 | runtime := mocks.NewMockContainerRuntime(t) | |
| 116 | - | svc := NewService(runtime, nil, nil, nil, Config{}) | |
| 116 | + | svc := NewService(runtime, nil, nil, nil, Config{}, nil) | |
| 117 | 117 | ctx := testContext() | |
| 118 | 118 | ||
| 119 | 119 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{ | |
| @@ -153,7 +153,7 @@ func TestService_ListAttachments(t *testing.T) { | |||
| 153 | 153 | ||
| 154 | 154 | func TestService_ListNetworks(t *testing.T) { | |
| 155 | 155 | runtime := mocks.NewMockContainerRuntime(t) | |
| 156 | - | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |
| 156 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}, nil) | |
| 157 | 157 | ctx := testContext() | |
| 158 | 158 | ||
| 159 | 159 | runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{ | |
| @@ -12,7 +12,7 @@ import ( | |||||
| 12 | 12 | ||||
| 13 | func TestService_ListRoutesWithDetails(t *testing.T) { | 13 | func TestService_ListRoutesWithDetails(t *testing.T) { | ||
| 14 | runtime := mocks.NewMockContainerRuntime(t) | 14 | runtime := mocks.NewMockContainerRuntime(t) | ||
| 15 | - | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | 15 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}, nil) |
| 16 | ctx := testContext() | 16 | ctx := testContext() | ||
| 17 | 17 | ||||
| 18 | svc.containers["app.example.com"] = &domain.Container{ | 18 | svc.containers["app.example.com"] = &domain.Container{ | ||
| @@ -80,7 +80,7 @@ func TestService_ListRoutesWithDetails(t *testing.T) { | |||||
| 80 | 80 | ||||
| 81 | func TestService_ListRoutesWithDetails_Empty(t *testing.T) { | 81 | func TestService_ListRoutesWithDetails_Empty(t *testing.T) { | ||
| 82 | runtime := mocks.NewMockContainerRuntime(t) | 82 | runtime := mocks.NewMockContainerRuntime(t) | ||
| 83 | - | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | 83 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}, nil) |
| 84 | ctx := testContext() | 84 | ctx := testContext() | ||
| 85 | 85 | ||||
| 86 | // getAllAttachments is called even with empty containers | 86 | // getAllAttachments is called even with empty containers | ||
| @@ -93,7 +93,7 @@ func TestService_ListRoutesWithDetails_Empty(t *testing.T) { | |||||
| 93 | 93 | ||||
| 94 | func TestService_ListRoutesWithDetails_NetworkError(t *testing.T) { | 94 | func TestService_ListRoutesWithDetails_NetworkError(t *testing.T) { | ||
| 95 | runtime := mocks.NewMockContainerRuntime(t) | 95 | runtime := mocks.NewMockContainerRuntime(t) | ||
| 96 | - | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | 96 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}, nil) |
| 97 | ctx := testContext() | 97 | ctx := testContext() | ||
| 98 | 98 | ||||
| 99 | svc.containers["app.example.com"] = &domain.Container{ | 99 | svc.containers["app.example.com"] = &domain.Container{ | ||
| @@ -113,7 +113,7 @@ func TestService_ListRoutesWithDetails_NetworkError(t *testing.T) { | |||||
| 113 | 113 | ||||
| 114 | func TestService_ListAttachments(t *testing.T) { | 114 | func TestService_ListAttachments(t *testing.T) { | ||
| 115 | runtime := mocks.NewMockContainerRuntime(t) | 115 | runtime := mocks.NewMockContainerRuntime(t) | ||
| 116 | - | svc := NewService(runtime, nil, nil, nil, Config{}) | 116 | + | svc := NewService(runtime, nil, nil, nil, Config{}, nil) |
| 117 | ctx := testContext() | 117 | ctx := testContext() | ||
| 118 | 118 | ||||
| 119 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{ | 119 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{ | ||
| @@ -153,7 +153,7 @@ func TestService_ListAttachments(t *testing.T) { | |||||
| 153 | 153 | ||||
| 154 | func TestService_ListNetworks(t *testing.T) { | 154 | func TestService_ListNetworks(t *testing.T) { | ||
| 155 | runtime := mocks.NewMockContainerRuntime(t) | 155 | runtime := mocks.NewMockContainerRuntime(t) | ||
| 156 | - | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | 156 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}, nil) |
| 157 | ctx := testContext() | 157 | ctx := testContext() | ||
| 158 | 158 | ||||
| 159 | runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{ | 159 | runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{ | ||
M internal/usecase/container/service.go +106 -40
| @@ -76,6 +76,7 @@ type Service struct { | |||
| 76 | 76 | cacheInvalidator out.ProxyCacheInvalidator | |
| 77 | 77 | drainWaiter out.ProxyDrainWaiter | |
| 78 | 78 | config Config | |
| 79 | + | configProvider AttachmentConfigProvider // live config reads for attachments/networks (may be nil) | |
| 79 | 80 | metrics *telemetry.Metrics | |
| 80 | 81 | containers map[string]*domain.Container | |
| 81 | 82 | attachments map[string][]string // ownerDomain → []containerIDs | |
| @@ -136,15 +137,17 @@ func NewService( | |||
| 136 | 137 | eventBus out.EventPublisher, | |
| 137 | 138 | logWriter out.ContainerLogWriter, | |
| 138 | 139 | config Config, | |
| 140 | + | configProvider AttachmentConfigProvider, | |
| 139 | 141 | ) *Service { | |
| 140 | 142 | return &Service{ | |
| 141 | - | runtime: runtime, | |
| 142 | - | envLoader: envLoader, | |
| 143 | - | eventBus: eventBus, | |
| 144 | - | logWriter: logWriter, | |
| 145 | - | config: config, | |
| 146 | - | containers: make(map[string]*domain.Container), | |
| 147 | - | attachments: make(map[string][]string), | |
| 143 | + | runtime: runtime, | |
| 144 | + | envLoader: envLoader, | |
| 145 | + | eventBus: eventBus, | |
| 146 | + | logWriter: logWriter, | |
| 147 | + | config: config, | |
| 148 | + | configProvider: configProvider, | |
| 149 | + | containers: make(map[string]*domain.Container), | |
| 150 | + | attachments: make(map[string][]string), | |
| 148 | 151 | } | |
| 149 | 152 | } | |
| 150 | 153 | ||
| @@ -787,6 +790,44 @@ func (s *Service) waitForDrain(ctx context.Context, oldContainerID string) { | |||
| 787 | 790 | } | |
| 788 | 791 | } | |
| 789 | 792 | ||
| 793 | + | // restartContainerWithRecovery restarts a container, reconciling stale state if needed. | |
| 794 | + | // It returns the (possibly refreshed) container and updated attachment IDs. | |
| 795 | + | func (s *Service) restartContainerWithRecovery(ctx context.Context, domainName string, container *domain.Container, attachmentIDs []string) (*domain.Container, []string, error) { | |
| 796 | + | log := zerowrap.FromCtx(ctx) | |
| 797 | + | ||
| 798 | + | if err := s.runtime.RestartContainer(ctx, container.ID); err != nil { | |
| 799 | + | if !isContainerNotFoundError(err) { | |
| 800 | + | return nil, nil, log.WrapErr(err, "failed to restart container") | |
| 801 | + | } | |
| 802 | + | ||
| 803 | + | // In-memory container ID can become stale after external runtime changes. | |
| 804 | + | // Re-sync and retry once with the latest tracked container. | |
| 805 | + | log.Warn(). | |
| 806 | + | Err(err). | |
| 807 | + | Str(zerowrap.FieldEntityID, container.ID). | |
| 808 | + | Msg("tracked container missing during restart, attempting state reconciliation") | |
| 809 | + | ||
| 810 | + | if syncErr := s.SyncContainers(ctx); syncErr != nil { | |
| 811 | + | log.Warn().Err(syncErr).Msg("failed to sync container state during restart recovery") | |
| 812 | + | } | |
| 813 | + | ||
| 814 | + | s.mu.RLock() | |
| 815 | + | refreshed, refreshedExists := s.containers[domainName] | |
| 816 | + | attachmentIDs = append([]string{}, s.attachments[domainName]...) | |
| 817 | + | s.mu.RUnlock() | |
| 818 | + | ||
| 819 | + | if !refreshedExists || refreshed == nil { | |
| 820 | + | return nil, nil, domain.ErrContainerNotFound | |
| 821 | + | } | |
| 822 | + | if err := s.runtime.RestartContainer(ctx, refreshed.ID); err != nil { | |
| 823 | + | return nil, nil, log.WrapErr(err, "failed to restart container after state reconciliation") | |
| 824 | + | } | |
| 825 | + | return refreshed, attachmentIDs, nil | |
| 826 | + | } | |
| 827 | + | ||
| 828 | + | return container, attachmentIDs, nil | |
| 829 | + | } | |
| 830 | + | ||
| 790 | 831 | // Restart restarts a running container for the given domain. | |
| 791 | 832 | func (s *Service) Restart(ctx context.Context, domainName string, withAttachments bool) error { | |
| 792 | 833 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| @@ -810,35 +851,38 @@ func (s *Service) Restart(ctx context.Context, domainName string, withAttachment | |||
| 810 | 851 | return domain.ErrContainerNotFound | |
| 811 | 852 | } | |
| 812 | 853 | ||
| 813 | - | // Restart the main container | |
| 814 | - | if err := s.runtime.RestartContainer(ctx, container.ID); err != nil { | |
| 815 | - | if !isContainerNotFoundError(err) { | |
| 816 | - | return log.WrapErr(err, "failed to restart container") | |
| 817 | - | } | |
| 818 | - | ||
| 819 | - | // In-memory container ID can become stale after external runtime changes. | |
| 820 | - | // Re-sync and retry once with the latest tracked container. | |
| 821 | - | log.Warn(). | |
| 822 | - | Err(err). | |
| 823 | - | Str(zerowrap.FieldEntityID, container.ID). | |
| 824 | - | Msg("tracked container missing during restart, attempting state reconciliation") | |
| 825 | - | ||
| 854 | + | // When attachments are requested, sync runtime state first to get accurate | |
| 855 | + | // attachment tracking, then check if configured attachments are deployed. | |
| 856 | + | // If fewer attachment containers are tracked than configured, fail early with | |
| 857 | + | // guidance rather than restarting the main container into a broken state | |
| 858 | + | // (e.g., missing database hostname). | |
| 859 | + | if withAttachments { | |
| 826 | 860 | if syncErr := s.SyncContainers(ctx); syncErr != nil { | |
| 827 | - | log.Warn().Err(syncErr).Msg("failed to sync container state during restart recovery") | |
| 861 | + | log.Warn().Err(syncErr).Msg("failed to sync container state before attachment check") | |
| 828 | 862 | } | |
| 829 | 863 | ||
| 864 | + | // Re-read attachment IDs after sync | |
| 830 | 865 | s.mu.RLock() | |
| 831 | - | refreshed, refreshedExists := s.containers[domainName] | |
| 832 | - | attachmentIDs = append([]string{}, s.attachments[domainName]...) | |
| 866 | + | if freshAttachments := s.attachments[domainName]; len(freshAttachments) > 0 { | |
| 867 | + | attachmentIDs = make([]string, len(freshAttachments)) | |
| 868 | + | copy(attachmentIDs, freshAttachments) | |
| 869 | + | } else { | |
| 870 | + | attachmentIDs = nil | |
| 871 | + | } | |
| 833 | 872 | s.mu.RUnlock() | |
| 834 | 873 | ||
| 835 | - | if !refreshedExists || refreshed == nil { | |
| 836 | - | return domain.ErrContainerNotFound | |
| 874 | + | configuredAttachments := s.resolveAttachmentsForDomain(domainName) | |
| 875 | + | if len(configuredAttachments) > 0 && len(attachmentIDs) < len(configuredAttachments) { | |
| 876 | + | missing := len(configuredAttachments) - len(attachmentIDs) | |
| 877 | + | return fmt.Errorf("%w: domain %q has %d configured attachment(s) but only %d deployed (%d missing)", | |
| 878 | + | domain.ErrAttachmentNotDeployed, domainName, len(configuredAttachments), len(attachmentIDs), missing) | |
| 837 | 879 | } | |
| 838 | - | if err := s.runtime.RestartContainer(ctx, refreshed.ID); err != nil { | |
| 839 | - | return log.WrapErr(err, "failed to restart container after state reconciliation") | |
| 840 | - | } | |
| 841 | - | container = refreshed | |
| 880 | + | } | |
| 881 | + | ||
| 882 | + | // Restart the main container (with stale-state recovery) | |
| 883 | + | container, attachmentIDs, err := s.restartContainerWithRecovery(ctx, domainName, container, attachmentIDs) | |
| 884 | + | if err != nil { | |
| 885 | + | return err | |
| 842 | 886 | } | |
| 843 | 887 | log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("container restarted") | |
| 844 | 888 | ||
| @@ -1773,15 +1817,27 @@ func (s *Service) setupVolumes(ctx context.Context, domainName, imageRef string) | |||
| 1773 | 1817 | } | |
| 1774 | 1818 | ||
| 1775 | 1819 | func (s *Service) getNetworkForApp(domainName string) string { | |
| 1776 | - | s.mu.RLock() | |
| 1777 | - | cfg := s.config | |
| 1778 | - | s.mu.RUnlock() | |
| 1820 | + | var networkIsolation bool | |
| 1821 | + | var networkGroups map[string][]string | |
| 1822 | + | ||
| 1823 | + | if s.configProvider != nil { | |
| 1824 | + | snap := s.configProvider.GetAttachmentConfig() | |
| 1825 | + | networkGroups = snap.NetworkGroups | |
| 1826 | + | s.mu.RLock() | |
| 1827 | + | networkIsolation = s.config.NetworkIsolation | |
| 1828 | + | s.mu.RUnlock() | |
| 1829 | + | } else { | |
| 1830 | + | s.mu.RLock() | |
| 1831 | + | networkIsolation = s.config.NetworkIsolation | |
| 1832 | + | networkGroups = s.config.NetworkGroups | |
| 1833 | + | s.mu.RUnlock() | |
| 1834 | + | } | |
| 1779 | 1835 | ||
| 1780 | - | if !cfg.NetworkIsolation { | |
| 1836 | + | if !networkIsolation { | |
| 1781 | 1837 | return "bridge" | |
| 1782 | 1838 | } | |
| 1783 | 1839 | ||
| 1784 | - | for groupName, domains := range cfg.NetworkGroups { | |
| 1840 | + | for groupName, domains := range networkGroups { | |
| 1785 | 1841 | if slices.Contains(domains, domainName) { | |
| 1786 | 1842 | return s.generateNetworkName(groupName) | |
| 1787 | 1843 | } | |
| @@ -1949,15 +2005,25 @@ func (s *Service) deployAttachments(ctx context.Context, domainName, networkName | |||
| 1949 | 2005 | // 1. Direct domain attachments (attachments[domain]) | |
| 1950 | 2006 | // 2. Network group attachments (attachments[group] where domain is in network_groups[group]) | |
| 1951 | 2007 | func (s *Service) resolveAttachmentsForDomain(domainName string) []string { | |
| 1952 | - | s.mu.RLock() | |
| 1953 | - | cfg := s.config | |
| 1954 | - | s.mu.RUnlock() | |
| 2008 | + | var attachments map[string][]string | |
| 2009 | + | var networkGroups map[string][]string | |
| 2010 | + | ||
| 2011 | + | if s.configProvider != nil { | |
| 2012 | + | snap := s.configProvider.GetAttachmentConfig() | |
| 2013 | + | attachments = snap.Attachments | |
| 2014 | + | networkGroups = snap.NetworkGroups | |
| 2015 | + | } else { | |
| 2016 | + | s.mu.RLock() | |
| 2017 | + | attachments = s.config.Attachments | |
| 2018 | + | networkGroups = s.config.NetworkGroups | |
| 2019 | + | s.mu.RUnlock() | |
| 2020 | + | } | |
| 1955 | 2021 | ||
| 1956 | 2022 | seen := make(map[string]bool) | |
| 1957 | 2023 | var result []string | |
| 1958 | 2024 | ||
| 1959 | 2025 | // First, add domain-specific attachments | |
| 1960 | - | if domainAttachments, ok := cfg.Attachments[domainName]; ok { | |
| 2026 | + | if domainAttachments, ok := attachments[domainName]; ok { | |
| 1961 | 2027 | for _, img := range domainAttachments { | |
| 1962 | 2028 | if !seen[img] { | |
| 1963 | 2029 | seen[img] = true | |
| @@ -1967,9 +2033,9 @@ func (s *Service) resolveAttachmentsForDomain(domainName string) []string { | |||
| 1967 | 2033 | } | |
| 1968 | 2034 | ||
| 1969 | 2035 | // Then, find which network group this domain belongs to and add group attachments | |
| 1970 | - | for groupName, domains := range cfg.NetworkGroups { | |
| 2036 | + | for groupName, domains := range networkGroups { | |
| 1971 | 2037 | if slices.Contains(domains, domainName) { | |
| 1972 | - | if groupAttachments, ok := cfg.Attachments[groupName]; ok { | |
| 2038 | + | if groupAttachments, ok := attachments[groupName]; ok { | |
| 1973 | 2039 | for _, img := range groupAttachments { | |
| 1974 | 2040 | if !seen[img] { | |
| 1975 | 2041 | seen[img] = true | |
| @@ -76,6 +76,7 @@ type Service struct { | |||||
| 76 | cacheInvalidator out.ProxyCacheInvalidator | 76 | cacheInvalidator out.ProxyCacheInvalidator | ||
| 77 | drainWaiter out.ProxyDrainWaiter | 77 | drainWaiter out.ProxyDrainWaiter | ||
| 78 | config Config | 78 | config Config | ||
| 79 | + | configProvider AttachmentConfigProvider // live config reads for attachments/networks (may be nil) | |||
| 79 | metrics *telemetry.Metrics | 80 | metrics *telemetry.Metrics | ||
| 80 | containers map[string]*domain.Container | 81 | containers map[string]*domain.Container | ||
| 81 | attachments map[string][]string // ownerDomain → []containerIDs | 82 | attachments map[string][]string // ownerDomain → []containerIDs | ||
| @@ -136,15 +137,17 @@ func NewService( | |||||
| 136 | eventBus out.EventPublisher, | 137 | eventBus out.EventPublisher, | ||
| 137 | logWriter out.ContainerLogWriter, | 138 | logWriter out.ContainerLogWriter, | ||
| 138 | config Config, | 139 | config Config, | ||
| 140 | + | configProvider AttachmentConfigProvider, | |||
| 139 | ) *Service { | 141 | ) *Service { | ||
| 140 | return &Service{ | 142 | return &Service{ | ||
| 141 | - | runtime: runtime, | 143 | + | runtime: runtime, |
| 142 | - | envLoader: envLoader, | 144 | + | envLoader: envLoader, |
| 143 | - | eventBus: eventBus, | 145 | + | eventBus: eventBus, |
| 144 | - | logWriter: logWriter, | 146 | + | logWriter: logWriter, |
| 145 | - | config: config, | 147 | + | config: config, |
| 146 | - | containers: make(map[string]*domain.Container), | 148 | + | configProvider: configProvider, |
| 147 | - | attachments: make(map[string][]string), | 149 | + | containers: make(map[string]*domain.Container), |
| 150 | + | attachments: make(map[string][]string), | |||
| 148 | } | 151 | } | ||
| 149 | } | 152 | } | ||
| 150 | 153 | ||||
| @@ -787,6 +790,44 @@ func (s *Service) waitForDrain(ctx context.Context, oldContainerID string) { | |||||
| 787 | } | 790 | } | ||
| 788 | } | 791 | } | ||
| 789 | 792 | ||||
| 793 | + | // restartContainerWithRecovery restarts a container, reconciling stale state if needed. | |||
| 794 | + | // It returns the (possibly refreshed) container and updated attachment IDs. | |||
| 795 | + | func (s *Service) restartContainerWithRecovery(ctx context.Context, domainName string, container *domain.Container, attachmentIDs []string) (*domain.Container, []string, error) { | |||
| 796 | + | log := zerowrap.FromCtx(ctx) | |||
| 797 | + | ||||
| 798 | + | if err := s.runtime.RestartContainer(ctx, container.ID); err != nil { | |||
| 799 | + | if !isContainerNotFoundError(err) { | |||
| 800 | + | return nil, nil, log.WrapErr(err, "failed to restart container") | |||
| 801 | + | } | |||
| 802 | + | ||||
| 803 | + | // In-memory container ID can become stale after external runtime changes. | |||
| 804 | + | // Re-sync and retry once with the latest tracked container. | |||
| 805 | + | log.Warn(). | |||
| 806 | + | Err(err). | |||
| 807 | + | Str(zerowrap.FieldEntityID, container.ID). | |||
| 808 | + | Msg("tracked container missing during restart, attempting state reconciliation") | |||
| 809 | + | ||||
| 810 | + | if syncErr := s.SyncContainers(ctx); syncErr != nil { | |||
| 811 | + | log.Warn().Err(syncErr).Msg("failed to sync container state during restart recovery") | |||
| 812 | + | } | |||
| 813 | + | ||||
| 814 | + | s.mu.RLock() | |||
| 815 | + | refreshed, refreshedExists := s.containers[domainName] | |||
| 816 | + | attachmentIDs = append([]string{}, s.attachments[domainName]...) | |||
| 817 | + | s.mu.RUnlock() | |||
| 818 | + | ||||
| 819 | + | if !refreshedExists || refreshed == nil { | |||
| 820 | + | return nil, nil, domain.ErrContainerNotFound | |||
| 821 | + | } | |||
| 822 | + | if err := s.runtime.RestartContainer(ctx, refreshed.ID); err != nil { | |||
| 823 | + | return nil, nil, log.WrapErr(err, "failed to restart container after state reconciliation") | |||
| 824 | + | } | |||
| 825 | + | return refreshed, attachmentIDs, nil | |||
| 826 | + | } | |||
| 827 | + | ||||
| 828 | + | return container, attachmentIDs, nil | |||
| 829 | + | } | |||
| 830 | + | ||||
| 790 | // Restart restarts a running container for the given domain. | 831 | // Restart restarts a running container for the given domain. | ||
| 791 | func (s *Service) Restart(ctx context.Context, domainName string, withAttachments bool) error { | 832 | func (s *Service) Restart(ctx context.Context, domainName string, withAttachments bool) error { | ||
| 792 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | 833 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | ||
| @@ -810,35 +851,38 @@ func (s *Service) Restart(ctx context.Context, domainName string, withAttachment | |||||
| 810 | return domain.ErrContainerNotFound | 851 | return domain.ErrContainerNotFound | ||
| 811 | } | 852 | } | ||
| 812 | 853 | ||||
| 813 | - | // Restart the main container | 854 | + | // When attachments are requested, sync runtime state first to get accurate |
| 814 | - | if err := s.runtime.RestartContainer(ctx, container.ID); err != nil { | 855 | + | // attachment tracking, then check if configured attachments are deployed. |
| 815 | - | if !isContainerNotFoundError(err) { | 856 | + | // If fewer attachment containers are tracked than configured, fail early with |
| 816 | - | return log.WrapErr(err, "failed to restart container") | 857 | + | // guidance rather than restarting the main container into a broken state |
| 817 | - | } | 858 | + | // (e.g., missing database hostname). |
| 818 | - | 859 | + | if withAttachments { | |
| 819 | - | // In-memory container ID can become stale after external runtime changes. | |||
| 820 | - | // Re-sync and retry once with the latest tracked container. | |||
| 821 | - | log.Warn(). | |||
| 822 | - | Err(err). | |||
| 823 | - | Str(zerowrap.FieldEntityID, container.ID). | |||
| 824 | - | Msg("tracked container missing during restart, attempting state reconciliation") | |||
| 825 | - | ||||
| 826 | if syncErr := s.SyncContainers(ctx); syncErr != nil { | 860 | if syncErr := s.SyncContainers(ctx); syncErr != nil { | ||
| 827 | - | log.Warn().Err(syncErr).Msg("failed to sync container state during restart recovery") | 861 | + | log.Warn().Err(syncErr).Msg("failed to sync container state before attachment check") |
| 828 | } | 862 | } | ||
| 829 | 863 | ||||
| 864 | + | // Re-read attachment IDs after sync | |||
| 830 | s.mu.RLock() | 865 | s.mu.RLock() | ||
| 831 | - | refreshed, refreshedExists := s.containers[domainName] | 866 | + | if freshAttachments := s.attachments[domainName]; len(freshAttachments) > 0 { |
| 832 | - | attachmentIDs = append([]string{}, s.attachments[domainName]...) | 867 | + | attachmentIDs = make([]string, len(freshAttachments)) |
| 868 | + | copy(attachmentIDs, freshAttachments) | |||
| 869 | + | } else { | |||
| 870 | + | attachmentIDs = nil | |||
| 871 | + | } | |||
| 833 | s.mu.RUnlock() | 872 | s.mu.RUnlock() | ||
| 834 | 873 | ||||
| 835 | - | if !refreshedExists || refreshed == nil { | 874 | + | configuredAttachments := s.resolveAttachmentsForDomain(domainName) |
| 836 | - | return domain.ErrContainerNotFound | 875 | + | if len(configuredAttachments) > 0 && len(attachmentIDs) < len(configuredAttachments) { |
| 876 | + | missing := len(configuredAttachments) - len(attachmentIDs) | |||
| 877 | + | return fmt.Errorf("%w: domain %q has %d configured attachment(s) but only %d deployed (%d missing)", | |||
| 878 | + | domain.ErrAttachmentNotDeployed, domainName, len(configuredAttachments), len(attachmentIDs), missing) | |||
| 837 | } | 879 | } | ||
| 838 | - | if err := s.runtime.RestartContainer(ctx, refreshed.ID); err != nil { | 880 | + | } |
| 839 | - | return log.WrapErr(err, "failed to restart container after state reconciliation") | 881 | + | |
| 840 | - | } | 882 | + | // Restart the main container (with stale-state recovery) |
| 841 | - | container = refreshed | 883 | + | container, attachmentIDs, err := s.restartContainerWithRecovery(ctx, domainName, container, attachmentIDs) |
| 884 | + | if err != nil { | |||
| 885 | + | return err | |||
| 842 | } | 886 | } | ||
| 843 | log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("container restarted") | 887 | log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("container restarted") | ||
| 844 | 888 | ||||
| @@ -1773,15 +1817,27 @@ func (s *Service) setupVolumes(ctx context.Context, domainName, imageRef string) | |||||
| 1773 | } | 1817 | } | ||
| 1774 | 1818 | ||||
| 1775 | func (s *Service) getNetworkForApp(domainName string) string { | 1819 | func (s *Service) getNetworkForApp(domainName string) string { | ||
| 1776 | - | s.mu.RLock() | 1820 | + | var networkIsolation bool |
| 1777 | - | cfg := s.config | 1821 | + | var networkGroups map[string][]string |
| 1778 | - | s.mu.RUnlock() | 1822 | + | |
| 1823 | + | if s.configProvider != nil { | |||
| 1824 | + | snap := s.configProvider.GetAttachmentConfig() | |||
| 1825 | + | networkGroups = snap.NetworkGroups | |||
| 1826 | + | s.mu.RLock() | |||
| 1827 | + | networkIsolation = s.config.NetworkIsolation | |||
| 1828 | + | s.mu.RUnlock() | |||
| 1829 | + | } else { | |||
| 1830 | + | s.mu.RLock() | |||
| 1831 | + | networkIsolation = s.config.NetworkIsolation | |||
| 1832 | + | networkGroups = s.config.NetworkGroups | |||
| 1833 | + | s.mu.RUnlock() | |||
| 1834 | + | } | |||
| 1779 | 1835 | ||||
| 1780 | - | if !cfg.NetworkIsolation { | 1836 | + | if !networkIsolation { |
| 1781 | return "bridge" | 1837 | return "bridge" | ||
| 1782 | } | 1838 | } | ||
| 1783 | 1839 | ||||
| 1784 | - | for groupName, domains := range cfg.NetworkGroups { | 1840 | + | for groupName, domains := range networkGroups { |
| 1785 | if slices.Contains(domains, domainName) { | 1841 | if slices.Contains(domains, domainName) { | ||
| 1786 | return s.generateNetworkName(groupName) | 1842 | return s.generateNetworkName(groupName) | ||
| 1787 | } | 1843 | } | ||
| @@ -1949,15 +2005,25 @@ func (s *Service) deployAttachments(ctx context.Context, domainName, networkName | |||||
| 1949 | // 1. Direct domain attachments (attachments[domain]) | 2005 | // 1. Direct domain attachments (attachments[domain]) | ||
| 1950 | // 2. Network group attachments (attachments[group] where domain is in network_groups[group]) | 2006 | // 2. Network group attachments (attachments[group] where domain is in network_groups[group]) | ||
| 1951 | func (s *Service) resolveAttachmentsForDomain(domainName string) []string { | 2007 | func (s *Service) resolveAttachmentsForDomain(domainName string) []string { | ||
| 1952 | - | s.mu.RLock() | 2008 | + | var attachments map[string][]string |
| 1953 | - | cfg := s.config | 2009 | + | var networkGroups map[string][]string |
| 1954 | - | s.mu.RUnlock() | 2010 | + | |
| 2011 | + | if s.configProvider != nil { | |||
| 2012 | + | snap := s.configProvider.GetAttachmentConfig() | |||
| 2013 | + | attachments = snap.Attachments | |||
| 2014 | + | networkGroups = snap.NetworkGroups | |||
| 2015 | + | } else { | |||
| 2016 | + | s.mu.RLock() | |||
| 2017 | + | attachments = s.config.Attachments | |||
| 2018 | + | networkGroups = s.config.NetworkGroups | |||
| 2019 | + | s.mu.RUnlock() | |||
| 2020 | + | } | |||
| 1955 | 2021 | ||||
| 1956 | seen := make(map[string]bool) | 2022 | seen := make(map[string]bool) | ||
| 1957 | var result []string | 2023 | var result []string | ||
| 1958 | 2024 | ||||
| 1959 | // First, add domain-specific attachments | 2025 | // First, add domain-specific attachments | ||
| 1960 | - | if domainAttachments, ok := cfg.Attachments[domainName]; ok { | 2026 | + | if domainAttachments, ok := attachments[domainName]; ok { |
| 1961 | for _, img := range domainAttachments { | 2027 | for _, img := range domainAttachments { | ||
| 1962 | if !seen[img] { | 2028 | if !seen[img] { | ||
| 1963 | seen[img] = true | 2029 | seen[img] = true | ||
| @@ -1967,9 +2033,9 @@ func (s *Service) resolveAttachmentsForDomain(domainName string) []string { | |||||
| 1967 | } | 2033 | } | ||
| 1968 | 2034 | ||||
| 1969 | // Then, find which network group this domain belongs to and add group attachments | 2035 | // Then, find which network group this domain belongs to and add group attachments | ||
| 1970 | - | for groupName, domains := range cfg.NetworkGroups { | 2036 | + | for groupName, domains := range networkGroups { |
| 1971 | if slices.Contains(domains, domainName) { | 2037 | if slices.Contains(domains, domainName) { | ||
| 1972 | - | if groupAttachments, ok := cfg.Attachments[groupName]; ok { | 2038 | + | if groupAttachments, ok := attachments[groupName]; ok { |
| 1973 | for _, img := range groupAttachments { | 2039 | for _, img := range groupAttachments { | ||
| 1974 | if !seen[img] { | 2040 | if !seen[img] { | ||
| 1975 | seen[img] = true | 2041 | seen[img] = true | ||
M internal/usecase/container/service_readiness_test.go +10 -10
| @@ -31,7 +31,7 @@ func TestService_WaitForReady_AutoFallsBackToDelayWhenNoHealthcheck(t *testing.T | |||
| 31 | 31 | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 32 | 32 | ReadinessMode: "auto", | |
| 33 | 33 | ReadinessDelay: time.Millisecond, | |
| 34 | - | }) | |
| 34 | + | }, nil) | |
| 35 | 35 | ||
| 36 | 36 | ctx := testContext() | |
| 37 | 37 | containerID := "container-1" | |
| @@ -52,7 +52,7 @@ func TestService_WaitForReady_AutoCascadeUsesDefaultHTTPAliveProbeWhenNoHealthch | |||
| 52 | 52 | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 53 | 53 | ReadinessMode: "auto", | |
| 54 | 54 | HTTPProbeTimeout: 50 * time.Millisecond, | |
| 55 | - | }) | |
| 55 | + | }, nil) | |
| 56 | 56 | ||
| 57 | 57 | ctx := testContext() | |
| 58 | 58 | containerID := "container-1" | |
| @@ -87,7 +87,7 @@ func TestService_WaitForReady_AutoCascadeUsesHTTPProbeWhenHealthLabelSet(t *test | |||
| 87 | 87 | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 88 | 88 | ReadinessMode: "auto", | |
| 89 | 89 | HTTPProbeTimeout: 50 * time.Millisecond, | |
| 90 | - | }) | |
| 90 | + | }, nil) | |
| 91 | 91 | ||
| 92 | 92 | ctx := testContext() | |
| 93 | 93 | containerID := "container-1" | |
| @@ -122,7 +122,7 @@ func TestService_WaitForReady_AutoCascadeUsesHealthcheckWhenPresent(t *testing.T | |||
| 122 | 122 | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 123 | 123 | ReadinessMode: "auto", | |
| 124 | 124 | HealthTimeout: 50 * time.Millisecond, | |
| 125 | - | }) | |
| 125 | + | }, nil) | |
| 126 | 126 | ||
| 127 | 127 | ctx := testContext() | |
| 128 | 128 | containerID := "container-1" | |
| @@ -151,7 +151,7 @@ func TestService_WaitForReady_AutoCascadeFallsToDelayWhenNoEndpoint(t *testing.T | |||
| 151 | 151 | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 152 | 152 | ReadinessMode: "auto", | |
| 153 | 153 | ReadinessDelay: time.Millisecond, | |
| 154 | - | }) | |
| 154 | + | }, nil) | |
| 155 | 155 | ||
| 156 | 156 | ctx := testContext() | |
| 157 | 157 | containerID := "container-1" | |
| @@ -180,7 +180,7 @@ func TestService_WaitForReady_ExplicitDelayModeIgnoresCascade(t *testing.T) { | |||
| 180 | 180 | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 181 | 181 | ReadinessMode: "delay", | |
| 182 | 182 | ReadinessDelay: time.Millisecond, | |
| 183 | - | }) | |
| 183 | + | }, nil) | |
| 184 | 184 | ||
| 185 | 185 | ctx := testContext() | |
| 186 | 186 | containerID := "container-1" | |
| @@ -205,7 +205,7 @@ func TestService_WaitForReady_ExplicitDelayModeIgnoresCascade(t *testing.T) { | |||
| 205 | 205 | ||
| 206 | 206 | func TestService_WaitForHealthy_EmptyStatusIsReportedAsStarting(t *testing.T) { | |
| 207 | 207 | runtime := mocks.NewMockContainerRuntime(t) | |
| 208 | - | svc := NewService(runtime, nil, nil, nil, Config{}) | |
| 208 | + | svc := NewService(runtime, nil, nil, nil, Config{}, nil) | |
| 209 | 209 | ||
| 210 | 210 | ctx := testContext() | |
| 211 | 211 | containerID := "container-1" | |
| @@ -222,7 +222,7 @@ func TestService_WaitForReady_ProbeUsesGordonProxyPortLabel(t *testing.T) { | |||
| 222 | 222 | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 223 | 223 | ReadinessMode: "auto", | |
| 224 | 224 | HTTPProbeTimeout: 50 * time.Millisecond, | |
| 225 | - | }) | |
| 225 | + | }, nil) | |
| 226 | 226 | ||
| 227 | 227 | ctx := testContext() | |
| 228 | 228 | containerID := "container-port-label" | |
| @@ -255,7 +255,7 @@ func TestService_WaitForReady_ProbeUsesDeprecatedPortLabel(t *testing.T) { | |||
| 255 | 255 | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 256 | 256 | ReadinessMode: "auto", | |
| 257 | 257 | HTTPProbeTimeout: 50 * time.Millisecond, | |
| 258 | - | }) | |
| 258 | + | }, nil) | |
| 259 | 259 | ||
| 260 | 260 | ctx := testContext() | |
| 261 | 261 | containerID := "container-proxy-port" | |
| @@ -286,7 +286,7 @@ func TestService_WaitForReady_ProxyPortTakesPriorityOverPort(t *testing.T) { | |||
| 286 | 286 | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 287 | 287 | ReadinessMode: "auto", | |
| 288 | 288 | HTTPProbeTimeout: 50 * time.Millisecond, | |
| 289 | - | }) | |
| 289 | + | }, nil) | |
| 290 | 290 | ||
| 291 | 291 | ctx := testContext() | |
| 292 | 292 | containerID := "container-priority" | |
| @@ -31,7 +31,7 @@ func TestService_WaitForReady_AutoFallsBackToDelayWhenNoHealthcheck(t *testing.T | |||||
| 31 | svc := NewService(runtime, nil, nil, nil, Config{ | 31 | svc := NewService(runtime, nil, nil, nil, Config{ | ||
| 32 | ReadinessMode: "auto", | 32 | ReadinessMode: "auto", | ||
| 33 | ReadinessDelay: time.Millisecond, | 33 | ReadinessDelay: time.Millisecond, | ||
| 34 | - | }) | 34 | + | }, nil) |
| 35 | 35 | ||||
| 36 | ctx := testContext() | 36 | ctx := testContext() | ||
| 37 | containerID := "container-1" | 37 | containerID := "container-1" | ||
| @@ -52,7 +52,7 @@ func TestService_WaitForReady_AutoCascadeUsesDefaultHTTPAliveProbeWhenNoHealthch | |||||
| 52 | svc := NewService(runtime, nil, nil, nil, Config{ | 52 | svc := NewService(runtime, nil, nil, nil, Config{ | ||
| 53 | ReadinessMode: "auto", | 53 | ReadinessMode: "auto", | ||
| 54 | HTTPProbeTimeout: 50 * time.Millisecond, | 54 | HTTPProbeTimeout: 50 * time.Millisecond, | ||
| 55 | - | }) | 55 | + | }, nil) |
| 56 | 56 | ||||
| 57 | ctx := testContext() | 57 | ctx := testContext() | ||
| 58 | containerID := "container-1" | 58 | containerID := "container-1" | ||
| @@ -87,7 +87,7 @@ func TestService_WaitForReady_AutoCascadeUsesHTTPProbeWhenHealthLabelSet(t *test | |||||
| 87 | svc := NewService(runtime, nil, nil, nil, Config{ | 87 | svc := NewService(runtime, nil, nil, nil, Config{ | ||
| 88 | ReadinessMode: "auto", | 88 | ReadinessMode: "auto", | ||
| 89 | HTTPProbeTimeout: 50 * time.Millisecond, | 89 | HTTPProbeTimeout: 50 * time.Millisecond, | ||
| 90 | - | }) | 90 | + | }, nil) |
| 91 | 91 | ||||
| 92 | ctx := testContext() | 92 | ctx := testContext() | ||
| 93 | containerID := "container-1" | 93 | containerID := "container-1" | ||
| @@ -122,7 +122,7 @@ func TestService_WaitForReady_AutoCascadeUsesHealthcheckWhenPresent(t *testing.T | |||||
| 122 | svc := NewService(runtime, nil, nil, nil, Config{ | 122 | svc := NewService(runtime, nil, nil, nil, Config{ | ||
| 123 | ReadinessMode: "auto", | 123 | ReadinessMode: "auto", | ||
| 124 | HealthTimeout: 50 * time.Millisecond, | 124 | HealthTimeout: 50 * time.Millisecond, | ||
| 125 | - | }) | 125 | + | }, nil) |
| 126 | 126 | ||||
| 127 | ctx := testContext() | 127 | ctx := testContext() | ||
| 128 | containerID := "container-1" | 128 | containerID := "container-1" | ||
| @@ -151,7 +151,7 @@ func TestService_WaitForReady_AutoCascadeFallsToDelayWhenNoEndpoint(t *testing.T | |||||
| 151 | svc := NewService(runtime, nil, nil, nil, Config{ | 151 | svc := NewService(runtime, nil, nil, nil, Config{ | ||
| 152 | ReadinessMode: "auto", | 152 | ReadinessMode: "auto", | ||
| 153 | ReadinessDelay: time.Millisecond, | 153 | ReadinessDelay: time.Millisecond, | ||
| 154 | - | }) | 154 | + | }, nil) |
| 155 | 155 | ||||
| 156 | ctx := testContext() | 156 | ctx := testContext() | ||
| 157 | containerID := "container-1" | 157 | containerID := "container-1" | ||
| @@ -180,7 +180,7 @@ func TestService_WaitForReady_ExplicitDelayModeIgnoresCascade(t *testing.T) { | |||||
| 180 | svc := NewService(runtime, nil, nil, nil, Config{ | 180 | svc := NewService(runtime, nil, nil, nil, Config{ | ||
| 181 | ReadinessMode: "delay", | 181 | ReadinessMode: "delay", | ||
| 182 | ReadinessDelay: time.Millisecond, | 182 | ReadinessDelay: time.Millisecond, | ||
| 183 | - | }) | 183 | + | }, nil) |
| 184 | 184 | ||||
| 185 | ctx := testContext() | 185 | ctx := testContext() | ||
| 186 | containerID := "container-1" | 186 | containerID := "container-1" | ||
| @@ -205,7 +205,7 @@ func TestService_WaitForReady_ExplicitDelayModeIgnoresCascade(t *testing.T) { | |||||
| 205 | 205 | ||||
| 206 | func TestService_WaitForHealthy_EmptyStatusIsReportedAsStarting(t *testing.T) { | 206 | func TestService_WaitForHealthy_EmptyStatusIsReportedAsStarting(t *testing.T) { | ||
| 207 | runtime := mocks.NewMockContainerRuntime(t) | 207 | runtime := mocks.NewMockContainerRuntime(t) | ||
| 208 | - | svc := NewService(runtime, nil, nil, nil, Config{}) | 208 | + | svc := NewService(runtime, nil, nil, nil, Config{}, nil) |
| 209 | 209 | ||||
| 210 | ctx := testContext() | 210 | ctx := testContext() | ||
| 211 | containerID := "container-1" | 211 | containerID := "container-1" | ||
| @@ -222,7 +222,7 @@ func TestService_WaitForReady_ProbeUsesGordonProxyPortLabel(t *testing.T) { | |||||
| 222 | svc := NewService(runtime, nil, nil, nil, Config{ | 222 | svc := NewService(runtime, nil, nil, nil, Config{ | ||
| 223 | ReadinessMode: "auto", | 223 | ReadinessMode: "auto", | ||
| 224 | HTTPProbeTimeout: 50 * time.Millisecond, | 224 | HTTPProbeTimeout: 50 * time.Millisecond, | ||
| 225 | - | }) | 225 | + | }, nil) |
| 226 | 226 | ||||
| 227 | ctx := testContext() | 227 | ctx := testContext() | ||
| 228 | containerID := "container-port-label" | 228 | containerID := "container-port-label" | ||
| @@ -255,7 +255,7 @@ func TestService_WaitForReady_ProbeUsesDeprecatedPortLabel(t *testing.T) { | |||||
| 255 | svc := NewService(runtime, nil, nil, nil, Config{ | 255 | svc := NewService(runtime, nil, nil, nil, Config{ | ||
| 256 | ReadinessMode: "auto", | 256 | ReadinessMode: "auto", | ||
| 257 | HTTPProbeTimeout: 50 * time.Millisecond, | 257 | HTTPProbeTimeout: 50 * time.Millisecond, | ||
| 258 | - | }) | 258 | + | }, nil) |
| 259 | 259 | ||||
| 260 | ctx := testContext() | 260 | ctx := testContext() | ||
| 261 | containerID := "container-proxy-port" | 261 | containerID := "container-proxy-port" | ||
| @@ -286,7 +286,7 @@ func TestService_WaitForReady_ProxyPortTakesPriorityOverPort(t *testing.T) { | |||||
| 286 | svc := NewService(runtime, nil, nil, nil, Config{ | 286 | svc := NewService(runtime, nil, nil, nil, Config{ | ||
| 287 | ReadinessMode: "auto", | 287 | ReadinessMode: "auto", | ||
| 288 | HTTPProbeTimeout: 50 * time.Millisecond, | 288 | HTTPProbeTimeout: 50 * time.Millisecond, | ||
| 289 | - | }) | 289 | + | }, nil) |
| 290 | 290 | ||||
| 291 | ctx := testContext() | 291 | ctx := testContext() | ||
| 292 | containerID := "container-priority" | 292 | containerID := "container-priority" | ||
M internal/usecase/container/service_test.go +369 -61
| @@ -17,7 +17,8 @@ import ( | |||
| 17 | 17 | "go.opentelemetry.io/otel/sdk/metric/metricdata" | |
| 18 | 18 | ||
| 19 | 19 | "github.com/bnema/gordon/internal/adapters/out/telemetry" | |
| 20 | - | "github.com/bnema/gordon/internal/boundaries/out/mocks" | |
| 20 | + | "github.com/bnema/gordon/internal/boundaries/out" | |
| 21 | + | mocks "github.com/bnema/gordon/internal/boundaries/out/mocks" | |
| 21 | 22 | "github.com/bnema/gordon/internal/domain" | |
| 22 | 23 | ) | |
| 23 | 24 | ||
| @@ -80,12 +81,123 @@ func managedMetricState(t *testing.T, reader *sdkmetric.ManualReader) (int64, in | |||
| 80 | 81 | return 0, 0, nil | |
| 81 | 82 | } | |
| 82 | 83 | ||
| 84 | + | func TestService_NewService_WithConfigProvider(t *testing.T) { | |
| 85 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 86 | + | envLoader := mocks.NewMockEnvLoader(t) | |
| 87 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 88 | + | ||
| 89 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |
| 90 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 91 | + | Attachments: map[string][]string{"test.com": {"postgres:16"}}, | |
| 92 | + | NetworkGroups: map[string][]string{"group1": {"test.com"}}, | |
| 93 | + | }).Maybe() | |
| 94 | + | ||
| 95 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |
| 96 | + | assert.NotNil(t, svc) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func TestService_ResolveAttachments_UsesLiveConfigProvider(t *testing.T) { | |
| 100 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 101 | + | envLoader := mocks.NewMockEnvLoader(t) | |
| 102 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 103 | + | ||
| 104 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |
| 105 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 106 | + | Attachments: map[string][]string{"app.example.com": {"postgres:16"}}, | |
| 107 | + | NetworkGroups: map[string][]string{}, | |
| 108 | + | }).Once() | |
| 109 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 110 | + | Attachments: map[string][]string{"app.example.com": {"postgres:16", "redis:7"}}, | |
| 111 | + | NetworkGroups: map[string][]string{}, | |
| 112 | + | }).Once() | |
| 113 | + | ||
| 114 | + | // Create service with EMPTY static config but a live provider | |
| 115 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{ | |
| 116 | + | NetworkIsolation: true, | |
| 117 | + | NetworkPrefix: "gordon", | |
| 118 | + | }, provider) | |
| 119 | + | ||
| 120 | + | // resolveAttachmentsForDomain should return the provider's data, not the empty snapshot | |
| 121 | + | result := svc.resolveAttachmentsForDomain("app.example.com") | |
| 122 | + | assert.Equal(t, []string{"postgres:16"}, result) | |
| 123 | + | ||
| 124 | + | result = svc.resolveAttachmentsForDomain("app.example.com") | |
| 125 | + | assert.Equal(t, []string{"postgres:16", "redis:7"}, result) | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | func TestService_ResolveAttachments_GroupBasedViaProvider(t *testing.T) { | |
| 129 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |
| 130 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 131 | + | Attachments: map[string][]string{ | |
| 132 | + | "shared-group": {"postgres:16"}, | |
| 133 | + | }, | |
| 134 | + | NetworkGroups: map[string][]string{ | |
| 135 | + | "shared-group": {"app1.example.com", "app2.example.com"}, | |
| 136 | + | }, | |
| 137 | + | }).Twice() | |
| 138 | + | ||
| 139 | + | svc := NewService( | |
| 140 | + | mocks.NewMockContainerRuntime(t), | |
| 141 | + | mocks.NewMockEnvLoader(t), | |
| 142 | + | mocks.NewMockEventPublisher(t), | |
| 143 | + | nil, Config{NetworkIsolation: true, NetworkPrefix: "gordon"}, provider, | |
| 144 | + | ) | |
| 145 | + | ||
| 146 | + | // Domain in group should see group attachments | |
| 147 | + | result := svc.resolveAttachmentsForDomain("app1.example.com") | |
| 148 | + | assert.Equal(t, []string{"postgres:16"}, result) | |
| 149 | + | ||
| 150 | + | // Domain not in any group should see nothing | |
| 151 | + | result = svc.resolveAttachmentsForDomain("other.example.com") | |
| 152 | + | assert.Empty(t, result) | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | func TestService_GetNetworkForApp_UsesLiveConfigProvider(t *testing.T) { | |
| 156 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |
| 157 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 158 | + | Attachments: map[string][]string{}, | |
| 159 | + | NetworkGroups: map[string][]string{ | |
| 160 | + | "shared-group": {"app1.example.com", "app2.example.com"}, | |
| 161 | + | }, | |
| 162 | + | }).Once() | |
| 163 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 164 | + | Attachments: map[string][]string{}, | |
| 165 | + | NetworkGroups: map[string][]string{ | |
| 166 | + | "shared-group": {"app1.example.com", "app2.example.com", "app3.example.com"}, | |
| 167 | + | }, | |
| 168 | + | }).Once() | |
| 169 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 170 | + | Attachments: map[string][]string{}, | |
| 171 | + | NetworkGroups: map[string][]string{ | |
| 172 | + | "shared-group": {"app1.example.com", "app2.example.com", "app3.example.com"}, | |
| 173 | + | }, | |
| 174 | + | }).Once() | |
| 175 | + | ||
| 176 | + | svc := NewService( | |
| 177 | + | mocks.NewMockContainerRuntime(t), | |
| 178 | + | mocks.NewMockEnvLoader(t), | |
| 179 | + | mocks.NewMockEventPublisher(t), | |
| 180 | + | nil, Config{NetworkIsolation: true, NetworkPrefix: "gordon"}, provider, | |
| 181 | + | ) | |
| 182 | + | ||
| 183 | + | // Should use group network: generateNetworkName produces "gordon-shared-group" | |
| 184 | + | network := svc.getNetworkForApp("app1.example.com") | |
| 185 | + | assert.Equal(t, "gordon-shared-group", network) | |
| 186 | + | ||
| 187 | + | network = svc.getNetworkForApp("app3.example.com") | |
| 188 | + | assert.Equal(t, "gordon-shared-group", network) | |
| 189 | + | ||
| 190 | + | // Domain not in any group gets its own network: "gordon-solo-example-com" | |
| 191 | + | network = svc.getNetworkForApp("solo.example.com") | |
| 192 | + | assert.Equal(t, "gordon-solo-example-com", network) | |
| 193 | + | } | |
| 194 | + | ||
| 83 | 195 | func TestService_ManagedContainersMetric_GlobalSeriesOnDeployReplaceAndRemove(t *testing.T) { | |
| 84 | 196 | runtime := mocks.NewMockContainerRuntime(t) | |
| 85 | 197 | envLoader := mocks.NewMockEnvLoader(t) | |
| 86 | 198 | eventBus := mocks.NewMockEventPublisher(t) | |
| 87 | 199 | ||
| 88 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 200 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 89 | 201 | ctx := testContext() | |
| 90 | 202 | metrics, reader := setupMetricsTest(t) | |
| 91 | 203 | svc.SetMetrics(metrics) | |
| @@ -119,7 +231,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T) | |||
| 119 | 231 | envLoader := mocks.NewMockEnvLoader(t) | |
| 120 | 232 | eventBus := mocks.NewMockEventPublisher(t) | |
| 121 | 233 | ||
| 122 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 234 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 123 | 235 | ctx := testContext() | |
| 124 | 236 | metrics, reader := setupMetricsTest(t) | |
| 125 | 237 | svc.SetMetrics(metrics) | |
| @@ -202,7 +314,7 @@ func TestService_Deploy_Success(t *testing.T) { | |||
| 202 | 314 | DrainDelayConfigured: true, | |
| 203 | 315 | } | |
| 204 | 316 | ||
| 205 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 317 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 206 | 318 | ctx := testContext() | |
| 207 | 319 | ||
| 208 | 320 | route := domain.Route{ | |
| @@ -284,7 +396,7 @@ func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T | |||
| 284 | 396 | DrainDelayConfigured: true, | |
| 285 | 397 | } | |
| 286 | 398 | ||
| 287 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 399 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 288 | 400 | ctx := testContext() | |
| 289 | 401 | ||
| 290 | 402 | route := domain.Route{ | |
| @@ -342,7 +454,7 @@ func TestService_Deploy_ImagePullFailure(t *testing.T) { | |||
| 342 | 454 | eventBus := mocks.NewMockEventPublisher(t) | |
| 343 | 455 | ||
| 344 | 456 | config := Config{} | |
| 345 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 457 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 346 | 458 | ctx := testContext() | |
| 347 | 459 | ||
| 348 | 460 | route := domain.Route{ | |
| @@ -373,7 +485,7 @@ func TestService_PullImage_UsesServiceTokenForExternalPull(t *testing.T) { | |||
| 373 | 485 | ServiceToken: "service-token", | |
| 374 | 486 | } | |
| 375 | 487 | ||
| 376 | - | svc := NewService(runtime, nil, nil, nil, config) | |
| 488 | + | svc := NewService(runtime, nil, nil, nil, config, nil) | |
| 377 | 489 | ctx := testContext() | |
| 378 | 490 | ||
| 379 | 491 | runtime.EXPECT().PullImageWithAuth(mock.Anything, "registry.example.com/myapp:latest", "gordon-service", "service-token").Return(nil) | |
| @@ -390,7 +502,7 @@ func TestService_PullImage_RequiresServiceTokenForExternalPull(t *testing.T) { | |||
| 390 | 502 | RegistryAuthEnabled: true, | |
| 391 | 503 | } | |
| 392 | 504 | ||
| 393 | - | svc := NewService(runtime, nil, nil, nil, config) | |
| 505 | + | svc := NewService(runtime, nil, nil, nil, config, nil) | |
| 394 | 506 | ctx := testContext() | |
| 395 | 507 | ||
| 396 | 508 | err := svc.pullImage(ctx, "registry.example.com/myapp:latest", false) | |
| @@ -408,7 +520,7 @@ func TestService_PullImage_InternalRetriesOnConnectionRefused(t *testing.T) { | |||
| 408 | 520 | InternalRegistryPassword: "secret", | |
| 409 | 521 | } | |
| 410 | 522 | ||
| 411 | - | svc := NewService(runtime, nil, nil, nil, config) | |
| 523 | + | svc := NewService(runtime, nil, nil, nil, config, nil) | |
| 412 | 524 | ctx := testContext() | |
| 413 | 525 | ||
| 414 | 526 | runtime.EXPECT().PullImageWithAuth(mock.Anything, "localhost:5000/myapp:latest", "internal", "secret"). | |
| @@ -435,7 +547,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) { | |||
| 435 | 547 | DrainDelayConfigured: true, | |
| 436 | 548 | StabilizationDelay: time.Millisecond, | |
| 437 | 549 | } | |
| 438 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 550 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 439 | 551 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 440 | 552 | ctx := testContext() | |
| 441 | 553 | ||
| @@ -511,7 +623,7 @@ func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T | |||
| 511 | 623 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | |
| 512 | 624 | ||
| 513 | 625 | config := testMinDelayConfig() | |
| 514 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 626 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 515 | 627 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 516 | 628 | ctx := testContext() | |
| 517 | 629 | ||
| @@ -608,7 +720,7 @@ func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) { | |||
| 608 | 720 | eventBus := mocks.NewMockEventPublisher(t) | |
| 609 | 721 | ||
| 610 | 722 | config := testMinDelayConfig() | |
| 611 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 723 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 612 | 724 | ctx := testContext() | |
| 613 | 725 | expectedEnvHash := hashEnvironment([]string{}) | |
| 614 | 726 | ||
| @@ -675,7 +787,7 @@ func TestService_Deploy_WithNetworkIsolation(t *testing.T) { | |||
| 675 | 787 | DrainDelay: time.Millisecond, | |
| 676 | 788 | DrainDelayConfigured: true, | |
| 677 | 789 | } | |
| 678 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 790 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 679 | 791 | ctx := testContext() | |
| 680 | 792 | ||
| 681 | 793 | route := domain.Route{ | |
| @@ -732,7 +844,7 @@ func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) { | |||
| 732 | 844 | DrainDelay: time.Millisecond, | |
| 733 | 845 | DrainDelayConfigured: true, | |
| 734 | 846 | } | |
| 735 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 847 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 736 | 848 | ctx := testContext() | |
| 737 | 849 | ||
| 738 | 850 | route := domain.Route{ | |
| @@ -784,7 +896,7 @@ func TestService_Stop(t *testing.T) { | |||
| 784 | 896 | envLoader := mocks.NewMockEnvLoader(t) | |
| 785 | 897 | eventBus := mocks.NewMockEventPublisher(t) | |
| 786 | 898 | ||
| 787 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 899 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 788 | 900 | ctx := testContext() | |
| 789 | 901 | ||
| 790 | 902 | runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(nil) | |
| @@ -799,7 +911,7 @@ func TestService_Stop_Error(t *testing.T) { | |||
| 799 | 911 | envLoader := mocks.NewMockEnvLoader(t) | |
| 800 | 912 | eventBus := mocks.NewMockEventPublisher(t) | |
| 801 | 913 | ||
| 802 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 914 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 803 | 915 | ctx := testContext() | |
| 804 | 916 | ||
| 805 | 917 | runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(errors.New("container not found")) | |
| @@ -815,7 +927,7 @@ func TestService_Remove(t *testing.T) { | |||
| 815 | 927 | envLoader := mocks.NewMockEnvLoader(t) | |
| 816 | 928 | eventBus := mocks.NewMockEventPublisher(t) | |
| 817 | 929 | ||
| 818 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 930 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 819 | 931 | ctx := testContext() | |
| 820 | 932 | ||
| 821 | 933 | // Add tracked container | |
| @@ -844,7 +956,7 @@ func TestService_Remove_WithNetworkCleanup(t *testing.T) { | |||
| 844 | 956 | NetworkIsolation: true, | |
| 845 | 957 | NetworkPrefix: "gordon", | |
| 846 | 958 | } | |
| 847 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 959 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 848 | 960 | ctx := testContext() | |
| 849 | 961 | ||
| 850 | 962 | svc.containers["test.example.com"] = &domain.Container{ | |
| @@ -868,7 +980,7 @@ func TestService_Get(t *testing.T) { | |||
| 868 | 980 | envLoader := mocks.NewMockEnvLoader(t) | |
| 869 | 981 | eventBus := mocks.NewMockEventPublisher(t) | |
| 870 | 982 | ||
| 871 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 983 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 872 | 984 | ctx := testContext() | |
| 873 | 985 | ||
| 874 | 986 | container := &domain.Container{ID: "container-123"} | |
| @@ -885,7 +997,7 @@ func TestService_Get_NotFound(t *testing.T) { | |||
| 885 | 997 | envLoader := mocks.NewMockEnvLoader(t) | |
| 886 | 998 | eventBus := mocks.NewMockEventPublisher(t) | |
| 887 | 999 | ||
| 888 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1000 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 889 | 1001 | ctx := testContext() | |
| 890 | 1002 | ||
| 891 | 1003 | result, exists := svc.Get(ctx, "nonexistent.example.com") | |
| @@ -899,7 +1011,7 @@ func TestService_List(t *testing.T) { | |||
| 899 | 1011 | envLoader := mocks.NewMockEnvLoader(t) | |
| 900 | 1012 | eventBus := mocks.NewMockEventPublisher(t) | |
| 901 | 1013 | ||
| 902 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1014 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 903 | 1015 | ctx := testContext() | |
| 904 | 1016 | ||
| 905 | 1017 | svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"} | |
| @@ -917,7 +1029,7 @@ func TestService_HealthCheck(t *testing.T) { | |||
| 917 | 1029 | envLoader := mocks.NewMockEnvLoader(t) | |
| 918 | 1030 | eventBus := mocks.NewMockEventPublisher(t) | |
| 919 | 1031 | ||
| 920 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1032 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 921 | 1033 | ctx := testContext() | |
| 922 | 1034 | ||
| 923 | 1035 | svc.containers["healthy.example.com"] = &domain.Container{ID: "healthy-container"} | |
| @@ -937,7 +1049,7 @@ func TestService_SyncContainers(t *testing.T) { | |||
| 937 | 1049 | envLoader := mocks.NewMockEnvLoader(t) | |
| 938 | 1050 | eventBus := mocks.NewMockEventPublisher(t) | |
| 939 | 1051 | ||
| 940 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1052 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 941 | 1053 | ctx := testContext() | |
| 942 | 1054 | ||
| 943 | 1055 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| @@ -977,7 +1089,7 @@ func TestService_Shutdown(t *testing.T) { | |||
| 977 | 1089 | envLoader := mocks.NewMockEnvLoader(t) | |
| 978 | 1090 | eventBus := mocks.NewMockEventPublisher(t) | |
| 979 | 1091 | ||
| 980 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1092 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 981 | 1093 | ctx := testContext() | |
| 982 | 1094 | ||
| 983 | 1095 | svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"} | |
| @@ -997,7 +1109,7 @@ func TestService_Restart_NotFound(t *testing.T) { | |||
| 997 | 1109 | envLoader := mocks.NewMockEnvLoader(t) | |
| 998 | 1110 | eventBus := mocks.NewMockEventPublisher(t) | |
| 999 | 1111 | ||
| 1000 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1112 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 1001 | 1113 | ctx := testContext() | |
| 1002 | 1114 | ||
| 1003 | 1115 | err := svc.Restart(ctx, "nonexistent.example.com", false) | |
| @@ -1011,7 +1123,7 @@ func TestService_Restart_Success(t *testing.T) { | |||
| 1011 | 1123 | envLoader := mocks.NewMockEnvLoader(t) | |
| 1012 | 1124 | eventBus := mocks.NewMockEventPublisher(t) | |
| 1013 | 1125 | ||
| 1014 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1126 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 1015 | 1127 | ctx := testContext() | |
| 1016 | 1128 | ||
| 1017 | 1129 | svc.containers["test.example.com"] = &domain.Container{ | |
| @@ -1032,7 +1144,7 @@ func TestService_Restart_Error(t *testing.T) { | |||
| 1032 | 1144 | envLoader := mocks.NewMockEnvLoader(t) | |
| 1033 | 1145 | eventBus := mocks.NewMockEventPublisher(t) | |
| 1034 | 1146 | ||
| 1035 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1147 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 1036 | 1148 | ctx := testContext() | |
| 1037 | 1149 | ||
| 1038 | 1150 | svc.containers["test.example.com"] = &domain.Container{ | |
| @@ -1054,7 +1166,7 @@ func TestService_Restart_ReconcilesStaleContainerID(t *testing.T) { | |||
| 1054 | 1166 | envLoader := mocks.NewMockEnvLoader(t) | |
| 1055 | 1167 | eventBus := mocks.NewMockEventPublisher(t) | |
| 1056 | 1168 | ||
| 1057 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1169 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 1058 | 1170 | ctx := testContext() | |
| 1059 | 1171 | ||
| 1060 | 1172 | svc.containers["test.example.com"] = &domain.Container{ | |
| @@ -1088,7 +1200,7 @@ func TestService_Restart_ReconcilesStaleContainerID_NotFoundAfterSync(t *testing | |||
| 1088 | 1200 | envLoader := mocks.NewMockEnvLoader(t) | |
| 1089 | 1201 | eventBus := mocks.NewMockEventPublisher(t) | |
| 1090 | 1202 | ||
| 1091 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1203 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 1092 | 1204 | ctx := testContext() | |
| 1093 | 1205 | ||
| 1094 | 1206 | svc.containers["test.example.com"] = &domain.Container{ | |
| @@ -1113,7 +1225,7 @@ func TestService_Restart_WithAttachments(t *testing.T) { | |||
| 1113 | 1225 | envLoader := mocks.NewMockEnvLoader(t) | |
| 1114 | 1226 | eventBus := mocks.NewMockEventPublisher(t) | |
| 1115 | 1227 | ||
| 1116 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 1228 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 1117 | 1229 | ctx := testContext() | |
| 1118 | 1230 | ||
| 1119 | 1231 | svc.containers["test.example.com"] = &domain.Container{ | |
| @@ -1122,6 +1234,31 @@ func TestService_Restart_WithAttachments(t *testing.T) { | |||
| 1122 | 1234 | Status: "running", | |
| 1123 | 1235 | } | |
| 1124 | 1236 | svc.attachments["test.example.com"] = []string{"container-attach-1", "container-attach-2"} | |
| 1237 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 1238 | + | { | |
| 1239 | + | ID: "container-123", | |
| 1240 | + | Labels: map[string]string{ | |
| 1241 | + | domain.LabelDomain: "test.example.com", | |
| 1242 | + | domain.LabelManaged: "true", | |
| 1243 | + | }, | |
| 1244 | + | }, | |
| 1245 | + | { | |
| 1246 | + | ID: "container-attach-1", | |
| 1247 | + | Labels: map[string]string{ | |
| 1248 | + | domain.LabelManaged: "true", | |
| 1249 | + | domain.LabelAttachment: "true", | |
| 1250 | + | domain.LabelAttachedTo: "test.example.com", | |
| 1251 | + | }, | |
| 1252 | + | }, | |
| 1253 | + | { | |
| 1254 | + | ID: "container-attach-2", | |
| 1255 | + | Labels: map[string]string{ | |
| 1256 | + | domain.LabelManaged: "true", | |
| 1257 | + | domain.LabelAttachment: "true", | |
| 1258 | + | domain.LabelAttachedTo: "test.example.com", | |
| 1259 | + | }, | |
| 1260 | + | }, | |
| 1261 | + | }, nil).Maybe() | |
| 1125 | 1262 | ||
| 1126 | 1263 | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | |
| 1127 | 1264 | runtime.EXPECT().RestartContainer(mock.Anything, "container-attach-1").Return(nil) | |
| @@ -1132,6 +1269,177 @@ func TestService_Restart_WithAttachments(t *testing.T) { | |||
| 1132 | 1269 | assert.NoError(t, err) | |
| 1133 | 1270 | } | |
| 1134 | 1271 | ||
| 1272 | + | func TestService_Restart_WithAttachments_ErrorsWhenConfiguredButNotDeployed(t *testing.T) { | |
| 1273 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 1274 | + | envLoader := mocks.NewMockEnvLoader(t) | |
| 1275 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 1276 | + | ||
| 1277 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |
| 1278 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 1279 | + | Attachments: map[string][]string{"test.example.com": {"postgres:16"}}, | |
| 1280 | + | NetworkGroups: map[string][]string{}, | |
| 1281 | + | }).Maybe() | |
| 1282 | + | ||
| 1283 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |
| 1284 | + | ||
| 1285 | + | svc.containers["test.example.com"] = &domain.Container{ | |
| 1286 | + | ID: "container-123", | |
| 1287 | + | Name: "gordon-test.example.com", | |
| 1288 | + | Status: "running", | |
| 1289 | + | } | |
| 1290 | + | // No attachment containers tracked — s.attachments is empty | |
| 1291 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe() | |
| 1292 | + | ||
| 1293 | + | err := svc.Restart(testContext(), "test.example.com", true) | |
| 1294 | + | ||
| 1295 | + | assert.Error(t, err) | |
| 1296 | + | assert.True(t, errors.Is(err, domain.ErrAttachmentNotDeployed)) | |
| 1297 | + | // Main container should NOT have been restarted | |
| 1298 | + | runtime.AssertNotCalled(t, "RestartContainer", mock.Anything, mock.Anything) | |
| 1299 | + | } | |
| 1300 | + | ||
| 1301 | + | func TestService_Restart_WithAttachments_ErrorsWhenPartiallyDeployed(t *testing.T) { | |
| 1302 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 1303 | + | envLoader := mocks.NewMockEnvLoader(t) | |
| 1304 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 1305 | + | ||
| 1306 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |
| 1307 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 1308 | + | Attachments: map[string][]string{"test.example.com": {"postgres:16", "redis:7"}}, | |
| 1309 | + | NetworkGroups: map[string][]string{}, | |
| 1310 | + | }).Maybe() | |
| 1311 | + | ||
| 1312 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |
| 1313 | + | ||
| 1314 | + | svc.containers["test.example.com"] = &domain.Container{ | |
| 1315 | + | ID: "container-123", | |
| 1316 | + | Name: "gordon-test.example.com", | |
| 1317 | + | Status: "running", | |
| 1318 | + | } | |
| 1319 | + | // Only 1 of 2 attachments deployed | |
| 1320 | + | svc.attachments["test.example.com"] = []string{"attach-postgres"} | |
| 1321 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 1322 | + | { | |
| 1323 | + | ID: "container-123", | |
| 1324 | + | Labels: map[string]string{ | |
| 1325 | + | domain.LabelDomain: "test.example.com", | |
| 1326 | + | domain.LabelManaged: "true", | |
| 1327 | + | }, | |
| 1328 | + | }, | |
| 1329 | + | { | |
| 1330 | + | ID: "attach-postgres", | |
| 1331 | + | Labels: map[string]string{ | |
| 1332 | + | domain.LabelManaged: "true", | |
| 1333 | + | domain.LabelAttachment: "true", | |
| 1334 | + | domain.LabelAttachedTo: "test.example.com", | |
| 1335 | + | }, | |
| 1336 | + | }, | |
| 1337 | + | }, nil).Maybe() | |
| 1338 | + | ||
| 1339 | + | err := svc.Restart(testContext(), "test.example.com", true) | |
| 1340 | + | ||
| 1341 | + | assert.Error(t, err) | |
| 1342 | + | assert.True(t, errors.Is(err, domain.ErrAttachmentNotDeployed)) | |
| 1343 | + | assert.Contains(t, err.Error(), "1 missing") | |
| 1344 | + | // Main container should NOT have been restarted | |
| 1345 | + | runtime.AssertNotCalled(t, "RestartContainer", mock.Anything, mock.Anything) | |
| 1346 | + | } | |
| 1347 | + | ||
| 1348 | + | func TestService_Restart_WithAttachments_SucceedsWhenAllDeployed(t *testing.T) { | |
| 1349 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 1350 | + | envLoader := mocks.NewMockEnvLoader(t) | |
| 1351 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 1352 | + | ||
| 1353 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |
| 1354 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 1355 | + | Attachments: map[string][]string{"test.example.com": {"postgres:16"}}, | |
| 1356 | + | NetworkGroups: map[string][]string{}, | |
| 1357 | + | }).Maybe() | |
| 1358 | + | ||
| 1359 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |
| 1360 | + | ||
| 1361 | + | svc.containers["test.example.com"] = &domain.Container{ | |
| 1362 | + | ID: "container-123", | |
| 1363 | + | Name: "gordon-test.example.com", | |
| 1364 | + | Status: "running", | |
| 1365 | + | } | |
| 1366 | + | svc.attachments["test.example.com"] = []string{"attach-456"} | |
| 1367 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 1368 | + | { | |
| 1369 | + | ID: "container-123", | |
| 1370 | + | Labels: map[string]string{ | |
| 1371 | + | domain.LabelDomain: "test.example.com", | |
| 1372 | + | domain.LabelManaged: "true", | |
| 1373 | + | }, | |
| 1374 | + | }, | |
| 1375 | + | { | |
| 1376 | + | ID: "attach-456", | |
| 1377 | + | Labels: map[string]string{ | |
| 1378 | + | domain.LabelManaged: "true", | |
| 1379 | + | domain.LabelAttachment: "true", | |
| 1380 | + | domain.LabelAttachedTo: "test.example.com", | |
| 1381 | + | }, | |
| 1382 | + | }, | |
| 1383 | + | }, nil).Maybe() | |
| 1384 | + | ||
| 1385 | + | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | |
| 1386 | + | runtime.EXPECT().RestartContainer(mock.Anything, "attach-456").Return(nil) | |
| 1387 | + | ||
| 1388 | + | err := svc.Restart(testContext(), "test.example.com", true) | |
| 1389 | + | assert.NoError(t, err) | |
| 1390 | + | } | |
| 1391 | + | ||
| 1392 | + | func TestService_Restart_WithoutAttachmentFlag_IgnoresMissingAttachments(t *testing.T) { | |
| 1393 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 1394 | + | envLoader := mocks.NewMockEnvLoader(t) | |
| 1395 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 1396 | + | ||
| 1397 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |
| 1398 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 1399 | + | Attachments: map[string][]string{"test.example.com": {"postgres:16"}}, | |
| 1400 | + | NetworkGroups: map[string][]string{}, | |
| 1401 | + | }).Maybe() | |
| 1402 | + | ||
| 1403 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |
| 1404 | + | ||
| 1405 | + | svc.containers["test.example.com"] = &domain.Container{ | |
| 1406 | + | ID: "container-123", | |
| 1407 | + | Name: "gordon-test.example.com", | |
| 1408 | + | Status: "running", | |
| 1409 | + | } | |
| 1410 | + | ||
| 1411 | + | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | |
| 1412 | + | ||
| 1413 | + | err := svc.Restart(testContext(), "test.example.com", false) | |
| 1414 | + | assert.NoError(t, err) | |
| 1415 | + | } | |
| 1416 | + | ||
| 1417 | + | func TestService_Restart_WithAttachments_NoConfiguredNoDeployed_Succeeds(t *testing.T) { | |
| 1418 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 1419 | + | envLoader := mocks.NewMockEnvLoader(t) | |
| 1420 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 1421 | + | ||
| 1422 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |
| 1423 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |
| 1424 | + | Attachments: map[string][]string{}, | |
| 1425 | + | NetworkGroups: map[string][]string{}, | |
| 1426 | + | }).Maybe() | |
| 1427 | + | ||
| 1428 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |
| 1429 | + | ||
| 1430 | + | svc.containers["test.example.com"] = &domain.Container{ | |
| 1431 | + | ID: "container-123", | |
| 1432 | + | Name: "gordon-test.example.com", | |
| 1433 | + | Status: "running", | |
| 1434 | + | } | |
| 1435 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe() | |
| 1436 | + | ||
| 1437 | + | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | |
| 1438 | + | ||
| 1439 | + | err := svc.Restart(testContext(), "test.example.com", true) | |
| 1440 | + | assert.NoError(t, err) | |
| 1441 | + | } | |
| 1442 | + | ||
| 1135 | 1443 | func TestNormalizeImageRef(t *testing.T) { | |
| 1136 | 1444 | tests := []struct { | |
| 1137 | 1445 | name string | |
| @@ -1452,7 +1760,7 @@ func TestService_Deploy_InternalDeployForcesPull(t *testing.T) { | |||
| 1452 | 1760 | DrainDelayConfigured: true, | |
| 1453 | 1761 | } | |
| 1454 | 1762 | ||
| 1455 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 1763 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 1456 | 1764 | ||
| 1457 | 1765 | // Use internal deploy context (simulating image push event) | |
| 1458 | 1766 | ctx := domain.WithInternalDeploy(testContext()) | |
| @@ -1525,7 +1833,7 @@ func TestService_AutoStart_StartsNewContainers(t *testing.T) { | |||
| 1525 | 1833 | DrainDelay: time.Millisecond, | |
| 1526 | 1834 | DrainDelayConfigured: true, | |
| 1527 | 1835 | } | |
| 1528 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 1836 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 1529 | 1837 | ctx := testContext() | |
| 1530 | 1838 | ||
| 1531 | 1839 | routes := []domain.Route{ | |
| @@ -1573,7 +1881,7 @@ func TestService_AutoStart_SkipsExistingContainers(t *testing.T) { | |||
| 1573 | 1881 | DrainDelay: time.Millisecond, | |
| 1574 | 1882 | DrainDelayConfigured: true, | |
| 1575 | 1883 | } | |
| 1576 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 1884 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 1577 | 1885 | ctx := testContext() | |
| 1578 | 1886 | ||
| 1579 | 1887 | // Pre-populate with existing container | |
| @@ -1617,7 +1925,7 @@ func TestService_AutoStart_HandlesDeployErrors(t *testing.T) { | |||
| 1617 | 1925 | eventBus := mocks.NewMockEventPublisher(t) | |
| 1618 | 1926 | ||
| 1619 | 1927 | config := Config{} | |
| 1620 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 1928 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 1621 | 1929 | ctx := testContext() | |
| 1622 | 1930 | ||
| 1623 | 1931 | routes := []domain.Route{ | |
| @@ -1651,7 +1959,7 @@ func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) { | |||
| 1651 | 1959 | DrainDelay: time.Millisecond, | |
| 1652 | 1960 | DrainDelayConfigured: true, | |
| 1653 | 1961 | } | |
| 1654 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 1962 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 1655 | 1963 | ||
| 1656 | 1964 | // Mark context as internal deploy — this is what syncAndAutoStart should do | |
| 1657 | 1965 | ctx := domain.WithInternalDeploy(testContext()) | |
| @@ -1703,7 +2011,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) { | |||
| 1703 | 2011 | DrainDelayConfigured: true, | |
| 1704 | 2012 | StabilizationDelay: time.Millisecond, | |
| 1705 | 2013 | } | |
| 1706 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2014 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 1707 | 2015 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 1708 | 2016 | ctx := testContext() | |
| 1709 | 2017 | ||
| @@ -1795,7 +2103,7 @@ func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) { | |||
| 1795 | 2103 | DrainDelay: time.Millisecond, | |
| 1796 | 2104 | DrainDelayConfigured: true, | |
| 1797 | 2105 | } | |
| 1798 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2106 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 1799 | 2107 | ctx := testContext() | |
| 1800 | 2108 | ||
| 1801 | 2109 | // NO tracked container - service is empty for this domain | |
| @@ -1871,7 +2179,7 @@ func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) { | |||
| 1871 | 2179 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | |
| 1872 | 2180 | ||
| 1873 | 2181 | config := testMinDelayConfig() | |
| 1874 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2182 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 1875 | 2183 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 1876 | 2184 | ctx := testContext() | |
| 1877 | 2185 | ||
| @@ -1942,7 +2250,7 @@ func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T) | |||
| 1942 | 2250 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | |
| 1943 | 2251 | ||
| 1944 | 2252 | config := testMinDelayConfig() | |
| 1945 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2253 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 1946 | 2254 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 1947 | 2255 | ctx := testContext() | |
| 1948 | 2256 | ||
| @@ -2053,7 +2361,7 @@ func TestService_StripRegistryPrefix(t *testing.T) { | |||
| 2053 | 2361 | config := Config{ | |
| 2054 | 2362 | RegistryDomain: tt.registryDomain, | |
| 2055 | 2363 | } | |
| 2056 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2364 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2057 | 2365 | ||
| 2058 | 2366 | result := svc.stripRegistryPrefix(tt.image) | |
| 2059 | 2367 | assert.Equal(t, tt.expected, result) | |
| @@ -2130,7 +2438,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) { | |||
| 2130 | 2438 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | |
| 2131 | 2439 | ||
| 2132 | 2440 | config := testMinDelayConfig() | |
| 2133 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2441 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2134 | 2442 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 2135 | 2443 | ctx := testContext() | |
| 2136 | 2444 | ||
| @@ -2242,7 +2550,7 @@ func TestService_Deploy_ContextCancellation(t *testing.T) { | |||
| 2242 | 2550 | DrainDelay: time.Millisecond, | |
| 2243 | 2551 | DrainDelayConfigured: true, | |
| 2244 | 2552 | } | |
| 2245 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2553 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2246 | 2554 | ||
| 2247 | 2555 | route := domain.Route{ | |
| 2248 | 2556 | Domain: "test.example.com", | |
| @@ -2268,7 +2576,7 @@ func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) { | |||
| 2268 | 2576 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | |
| 2269 | 2577 | ||
| 2270 | 2578 | config := testMinDelayConfig() | |
| 2271 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2579 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2272 | 2580 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 2273 | 2581 | ctx := testContext() | |
| 2274 | 2582 | ||
| @@ -2347,7 +2655,7 @@ func TestService_Deploy_NilCacheInvalidator(t *testing.T) { | |||
| 2347 | 2655 | ||
| 2348 | 2656 | config := testMinDelayConfig() | |
| 2349 | 2657 | // Intentionally NOT setting cache invalidator | |
| 2350 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2658 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2351 | 2659 | ctx := testContext() | |
| 2352 | 2660 | ||
| 2353 | 2661 | // Pre-populate with existing container | |
| @@ -2398,7 +2706,7 @@ func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) { | |||
| 2398 | 2706 | DrainDelay: time.Millisecond, | |
| 2399 | 2707 | DrainDelayConfigured: true, | |
| 2400 | 2708 | } | |
| 2401 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2709 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2402 | 2710 | ctx := testContext() | |
| 2403 | 2711 | expectedEnvHash := hashEnvironment([]string{}) | |
| 2404 | 2712 | ||
| @@ -2457,7 +2765,7 @@ func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testin | |||
| 2457 | 2765 | DrainDelay: time.Millisecond, | |
| 2458 | 2766 | DrainDelayConfigured: true, | |
| 2459 | 2767 | } | |
| 2460 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2768 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2461 | 2769 | ctx := testContext() | |
| 2462 | 2770 | expectedEnvHash := hashEnvironment([]string{}) | |
| 2463 | 2771 | ||
| @@ -2514,7 +2822,7 @@ func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) { | |||
| 2514 | 2822 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | |
| 2515 | 2823 | ||
| 2516 | 2824 | config := testMinDelayConfig() | |
| 2517 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2825 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2518 | 2826 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 2519 | 2827 | ctx := testContext() | |
| 2520 | 2828 | ||
| @@ -2570,7 +2878,7 @@ func TestSyncContainersRebuildsAttachmentMap(t *testing.T) { | |||
| 2570 | 2878 | envLoader := mocks.NewMockEnvLoader(t) | |
| 2571 | 2879 | eventBus := mocks.NewMockEventPublisher(t) | |
| 2572 | 2880 | ||
| 2573 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | |
| 2881 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 2574 | 2882 | ctx := testContext() | |
| 2575 | 2883 | ||
| 2576 | 2884 | // ListContainers returns one main container and one attachment container | |
| @@ -2614,7 +2922,7 @@ func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) { | |||
| 2614 | 2922 | eventBus := mocks.NewMockEventPublisher(t) | |
| 2615 | 2923 | ||
| 2616 | 2924 | config := testMinDelayConfig() | |
| 2617 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2925 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2618 | 2926 | ctx := testContext() | |
| 2619 | 2927 | expectedEnvHash := hashEnvironment([]string{}) | |
| 2620 | 2928 | ||
| @@ -2676,7 +2984,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) { | |||
| 2676 | 2984 | eventBus := mocks.NewMockEventPublisher(t) | |
| 2677 | 2985 | ||
| 2678 | 2986 | config := testMinDelayConfig() | |
| 2679 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 2987 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2680 | 2988 | ctx := testContext() | |
| 2681 | 2989 | expectedEnvHash := hashEnvironment([]string{}) | |
| 2682 | 2990 | ||
| @@ -2733,7 +3041,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) { | |||
| 2733 | 3041 | eventBus := mocks.NewMockEventPublisher(t) | |
| 2734 | 3042 | ||
| 2735 | 3043 | config := testMinDelayConfig() | |
| 2736 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 3044 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2737 | 3045 | ctx := testContext() | |
| 2738 | 3046 | expectedEnvHash := hashEnvironment([]string{}) | |
| 2739 | 3047 | ||
| @@ -2787,7 +3095,7 @@ func TestService_attachmentEnvDrifted_ReturnsFalseWhenHashMatches(t *testing.T) | |||
| 2787 | 3095 | envLoader := mocks.NewMockEnvLoader(t) | |
| 2788 | 3096 | eventBus := mocks.NewMockEventPublisher(t) | |
| 2789 | 3097 | ||
| 2790 | - | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig()) | |
| 3098 | + | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil) | |
| 2791 | 3099 | ctx := testContext() | |
| 2792 | 3100 | imageRef := svc.buildImageRef("postgres:16") | |
| 2793 | 3101 | envVars := []string{"POSTGRES_PASSWORD=secret"} | |
| @@ -2809,7 +3117,7 @@ func TestService_attachmentEnvDrifted_ReturnsTrueWhenHashDiffers(t *testing.T) { | |||
| 2809 | 3117 | envLoader := mocks.NewMockEnvLoader(t) | |
| 2810 | 3118 | eventBus := mocks.NewMockEventPublisher(t) | |
| 2811 | 3119 | ||
| 2812 | - | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig()) | |
| 3120 | + | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil) | |
| 2813 | 3121 | ctx := testContext() | |
| 2814 | 3122 | imageRef := svc.buildImageRef("postgres:16") | |
| 2815 | 3123 | envVars := []string{"POSTGRES_PASSWORD=secret"} | |
| @@ -2830,7 +3138,7 @@ func TestService_attachmentEnvDrifted_ReturnsTrueWhenHashMissing(t *testing.T) { | |||
| 2830 | 3138 | envLoader := mocks.NewMockEnvLoader(t) | |
| 2831 | 3139 | eventBus := mocks.NewMockEventPublisher(t) | |
| 2832 | 3140 | ||
| 2833 | - | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig()) | |
| 3141 | + | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil) | |
| 2834 | 3142 | ctx := testContext() | |
| 2835 | 3143 | imageRef := svc.buildImageRef("postgres:16") | |
| 2836 | 3144 | envVars := []string{"POSTGRES_PASSWORD=secret"} | |
| @@ -2858,7 +3166,7 @@ func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) { | |||
| 2858 | 3166 | eventBus := mocks.NewMockEventPublisher(t) | |
| 2859 | 3167 | ||
| 2860 | 3168 | config := testMinDelayConfig() | |
| 2861 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 3169 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2862 | 3170 | ctx := testContext() | |
| 2863 | 3171 | ||
| 2864 | 3172 | // The canonical container is running and has the managed label — it will be | |
| @@ -2959,7 +3267,7 @@ func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) { | |||
| 2959 | 3267 | DrainDelayConfigured: true, | |
| 2960 | 3268 | StabilizationDelay: time.Millisecond, | |
| 2961 | 3269 | } | |
| 2962 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 3270 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 2963 | 3271 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 2964 | 3272 | ctx := testContext() | |
| 2965 | 3273 | ||
| @@ -3052,7 +3360,7 @@ func TestService_Deploy_StabilizationSuccess(t *testing.T) { | |||
| 3052 | 3360 | DrainDelayConfigured: true, | |
| 3053 | 3361 | StabilizationDelay: time.Millisecond, | |
| 3054 | 3362 | } | |
| 3055 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 3363 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 3056 | 3364 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 3057 | 3365 | ctx := testContext() | |
| 3058 | 3366 | ||
| @@ -3138,7 +3446,7 @@ func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T) | |||
| 3138 | 3446 | DrainDelayConfigured: true, | |
| 3139 | 3447 | StabilizationDelay: time.Millisecond, | |
| 3140 | 3448 | } | |
| 3141 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 3449 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 3142 | 3450 | svc.SetProxyCacheInvalidator(cacheInvalidator) | |
| 3143 | 3451 | ctx := testContext() | |
| 3144 | 3452 | ||
| @@ -3231,7 +3539,7 @@ func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T) | |||
| 3231 | 3539 | DrainDelayConfigured: true, | |
| 3232 | 3540 | StabilizationDelay: time.Millisecond, | |
| 3233 | 3541 | } | |
| 3234 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 3542 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 3235 | 3543 | ctx := testContext() | |
| 3236 | 3544 | ||
| 3237 | 3545 | // Pre-populate with existing tracked container | |
| @@ -3312,7 +3620,7 @@ func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) { | |||
| 3312 | 3620 | DrainDelayConfigured: true, | |
| 3313 | 3621 | StabilizationDelay: time.Millisecond, | |
| 3314 | 3622 | } | |
| 3315 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 3623 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 3316 | 3624 | ctx := testContext() | |
| 3317 | 3625 | ||
| 3318 | 3626 | // NO existing container — this is a first deploy | |
| @@ -3389,7 +3697,7 @@ func TestService_Deploy_PropagatesImageLabelsToContainerConfig(t *testing.T) { | |||
| 3389 | 3697 | DrainDelayConfigured: true, | |
| 3390 | 3698 | } | |
| 3391 | 3699 | ||
| 3392 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 3700 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) | |
| 3393 | 3701 | ctx := testContext() | |
| 3394 | 3702 | ||
| 3395 | 3703 | route := domain.Route{ | |
| @@ -17,7 +17,8 @@ import ( | |||||
| 17 | "go.opentelemetry.io/otel/sdk/metric/metricdata" | 17 | "go.opentelemetry.io/otel/sdk/metric/metricdata" | ||
| 18 | 18 | ||||
| 19 | "github.com/bnema/gordon/internal/adapters/out/telemetry" | 19 | "github.com/bnema/gordon/internal/adapters/out/telemetry" | ||
| 20 | - | "github.com/bnema/gordon/internal/boundaries/out/mocks" | 20 | + | "github.com/bnema/gordon/internal/boundaries/out" |
| 21 | + | mocks "github.com/bnema/gordon/internal/boundaries/out/mocks" | |||
| 21 | "github.com/bnema/gordon/internal/domain" | 22 | "github.com/bnema/gordon/internal/domain" | ||
| 22 | ) | 23 | ) | ||
| 23 | 24 | ||||
| @@ -80,12 +81,123 @@ func managedMetricState(t *testing.T, reader *sdkmetric.ManualReader) (int64, in | |||||
| 80 | return 0, 0, nil | 81 | return 0, 0, nil | ||
| 81 | } | 82 | } | ||
| 82 | 83 | ||||
| 84 | + | func TestService_NewService_WithConfigProvider(t *testing.T) { | |||
| 85 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 86 | + | envLoader := mocks.NewMockEnvLoader(t) | |||
| 87 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 88 | + | ||||
| 89 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |||
| 90 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 91 | + | Attachments: map[string][]string{"test.com": {"postgres:16"}}, | |||
| 92 | + | NetworkGroups: map[string][]string{"group1": {"test.com"}}, | |||
| 93 | + | }).Maybe() | |||
| 94 | + | ||||
| 95 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |||
| 96 | + | assert.NotNil(t, svc) | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func TestService_ResolveAttachments_UsesLiveConfigProvider(t *testing.T) { | |||
| 100 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 101 | + | envLoader := mocks.NewMockEnvLoader(t) | |||
| 102 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 103 | + | ||||
| 104 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |||
| 105 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 106 | + | Attachments: map[string][]string{"app.example.com": {"postgres:16"}}, | |||
| 107 | + | NetworkGroups: map[string][]string{}, | |||
| 108 | + | }).Once() | |||
| 109 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 110 | + | Attachments: map[string][]string{"app.example.com": {"postgres:16", "redis:7"}}, | |||
| 111 | + | NetworkGroups: map[string][]string{}, | |||
| 112 | + | }).Once() | |||
| 113 | + | ||||
| 114 | + | // Create service with EMPTY static config but a live provider | |||
| 115 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{ | |||
| 116 | + | NetworkIsolation: true, | |||
| 117 | + | NetworkPrefix: "gordon", | |||
| 118 | + | }, provider) | |||
| 119 | + | ||||
| 120 | + | // resolveAttachmentsForDomain should return the provider's data, not the empty snapshot | |||
| 121 | + | result := svc.resolveAttachmentsForDomain("app.example.com") | |||
| 122 | + | assert.Equal(t, []string{"postgres:16"}, result) | |||
| 123 | + | ||||
| 124 | + | result = svc.resolveAttachmentsForDomain("app.example.com") | |||
| 125 | + | assert.Equal(t, []string{"postgres:16", "redis:7"}, result) | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | func TestService_ResolveAttachments_GroupBasedViaProvider(t *testing.T) { | |||
| 129 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |||
| 130 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 131 | + | Attachments: map[string][]string{ | |||
| 132 | + | "shared-group": {"postgres:16"}, | |||
| 133 | + | }, | |||
| 134 | + | NetworkGroups: map[string][]string{ | |||
| 135 | + | "shared-group": {"app1.example.com", "app2.example.com"}, | |||
| 136 | + | }, | |||
| 137 | + | }).Twice() | |||
| 138 | + | ||||
| 139 | + | svc := NewService( | |||
| 140 | + | mocks.NewMockContainerRuntime(t), | |||
| 141 | + | mocks.NewMockEnvLoader(t), | |||
| 142 | + | mocks.NewMockEventPublisher(t), | |||
| 143 | + | nil, Config{NetworkIsolation: true, NetworkPrefix: "gordon"}, provider, | |||
| 144 | + | ) | |||
| 145 | + | ||||
| 146 | + | // Domain in group should see group attachments | |||
| 147 | + | result := svc.resolveAttachmentsForDomain("app1.example.com") | |||
| 148 | + | assert.Equal(t, []string{"postgres:16"}, result) | |||
| 149 | + | ||||
| 150 | + | // Domain not in any group should see nothing | |||
| 151 | + | result = svc.resolveAttachmentsForDomain("other.example.com") | |||
| 152 | + | assert.Empty(t, result) | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | func TestService_GetNetworkForApp_UsesLiveConfigProvider(t *testing.T) { | |||
| 156 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |||
| 157 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 158 | + | Attachments: map[string][]string{}, | |||
| 159 | + | NetworkGroups: map[string][]string{ | |||
| 160 | + | "shared-group": {"app1.example.com", "app2.example.com"}, | |||
| 161 | + | }, | |||
| 162 | + | }).Once() | |||
| 163 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 164 | + | Attachments: map[string][]string{}, | |||
| 165 | + | NetworkGroups: map[string][]string{ | |||
| 166 | + | "shared-group": {"app1.example.com", "app2.example.com", "app3.example.com"}, | |||
| 167 | + | }, | |||
| 168 | + | }).Once() | |||
| 169 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 170 | + | Attachments: map[string][]string{}, | |||
| 171 | + | NetworkGroups: map[string][]string{ | |||
| 172 | + | "shared-group": {"app1.example.com", "app2.example.com", "app3.example.com"}, | |||
| 173 | + | }, | |||
| 174 | + | }).Once() | |||
| 175 | + | ||||
| 176 | + | svc := NewService( | |||
| 177 | + | mocks.NewMockContainerRuntime(t), | |||
| 178 | + | mocks.NewMockEnvLoader(t), | |||
| 179 | + | mocks.NewMockEventPublisher(t), | |||
| 180 | + | nil, Config{NetworkIsolation: true, NetworkPrefix: "gordon"}, provider, | |||
| 181 | + | ) | |||
| 182 | + | ||||
| 183 | + | // Should use group network: generateNetworkName produces "gordon-shared-group" | |||
| 184 | + | network := svc.getNetworkForApp("app1.example.com") | |||
| 185 | + | assert.Equal(t, "gordon-shared-group", network) | |||
| 186 | + | ||||
| 187 | + | network = svc.getNetworkForApp("app3.example.com") | |||
| 188 | + | assert.Equal(t, "gordon-shared-group", network) | |||
| 189 | + | ||||
| 190 | + | // Domain not in any group gets its own network: "gordon-solo-example-com" | |||
| 191 | + | network = svc.getNetworkForApp("solo.example.com") | |||
| 192 | + | assert.Equal(t, "gordon-solo-example-com", network) | |||
| 193 | + | } | |||
| 194 | + | ||||
| 83 | func TestService_ManagedContainersMetric_GlobalSeriesOnDeployReplaceAndRemove(t *testing.T) { | 195 | func TestService_ManagedContainersMetric_GlobalSeriesOnDeployReplaceAndRemove(t *testing.T) { | ||
| 84 | runtime := mocks.NewMockContainerRuntime(t) | 196 | runtime := mocks.NewMockContainerRuntime(t) | ||
| 85 | envLoader := mocks.NewMockEnvLoader(t) | 197 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 86 | eventBus := mocks.NewMockEventPublisher(t) | 198 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 87 | 199 | ||||
| 88 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 200 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 89 | ctx := testContext() | 201 | ctx := testContext() | ||
| 90 | metrics, reader := setupMetricsTest(t) | 202 | metrics, reader := setupMetricsTest(t) | ||
| 91 | svc.SetMetrics(metrics) | 203 | svc.SetMetrics(metrics) | ||
| @@ -119,7 +231,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T) | |||||
| 119 | envLoader := mocks.NewMockEnvLoader(t) | 231 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 120 | eventBus := mocks.NewMockEventPublisher(t) | 232 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 121 | 233 | ||||
| 122 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 234 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 123 | ctx := testContext() | 235 | ctx := testContext() | ||
| 124 | metrics, reader := setupMetricsTest(t) | 236 | metrics, reader := setupMetricsTest(t) | ||
| 125 | svc.SetMetrics(metrics) | 237 | svc.SetMetrics(metrics) | ||
| @@ -202,7 +314,7 @@ func TestService_Deploy_Success(t *testing.T) { | |||||
| 202 | DrainDelayConfigured: true, | 314 | DrainDelayConfigured: true, | ||
| 203 | } | 315 | } | ||
| 204 | 316 | ||||
| 205 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 317 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 206 | ctx := testContext() | 318 | ctx := testContext() | ||
| 207 | 319 | ||||
| 208 | route := domain.Route{ | 320 | route := domain.Route{ | ||
| @@ -284,7 +396,7 @@ func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T | |||||
| 284 | DrainDelayConfigured: true, | 396 | DrainDelayConfigured: true, | ||
| 285 | } | 397 | } | ||
| 286 | 398 | ||||
| 287 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 399 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 288 | ctx := testContext() | 400 | ctx := testContext() | ||
| 289 | 401 | ||||
| 290 | route := domain.Route{ | 402 | route := domain.Route{ | ||
| @@ -342,7 +454,7 @@ func TestService_Deploy_ImagePullFailure(t *testing.T) { | |||||
| 342 | eventBus := mocks.NewMockEventPublisher(t) | 454 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 343 | 455 | ||||
| 344 | config := Config{} | 456 | config := Config{} | ||
| 345 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 457 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 346 | ctx := testContext() | 458 | ctx := testContext() | ||
| 347 | 459 | ||||
| 348 | route := domain.Route{ | 460 | route := domain.Route{ | ||
| @@ -373,7 +485,7 @@ func TestService_PullImage_UsesServiceTokenForExternalPull(t *testing.T) { | |||||
| 373 | ServiceToken: "service-token", | 485 | ServiceToken: "service-token", | ||
| 374 | } | 486 | } | ||
| 375 | 487 | ||||
| 376 | - | svc := NewService(runtime, nil, nil, nil, config) | 488 | + | svc := NewService(runtime, nil, nil, nil, config, nil) |
| 377 | ctx := testContext() | 489 | ctx := testContext() | ||
| 378 | 490 | ||||
| 379 | runtime.EXPECT().PullImageWithAuth(mock.Anything, "registry.example.com/myapp:latest", "gordon-service", "service-token").Return(nil) | 491 | runtime.EXPECT().PullImageWithAuth(mock.Anything, "registry.example.com/myapp:latest", "gordon-service", "service-token").Return(nil) | ||
| @@ -390,7 +502,7 @@ func TestService_PullImage_RequiresServiceTokenForExternalPull(t *testing.T) { | |||||
| 390 | RegistryAuthEnabled: true, | 502 | RegistryAuthEnabled: true, | ||
| 391 | } | 503 | } | ||
| 392 | 504 | ||||
| 393 | - | svc := NewService(runtime, nil, nil, nil, config) | 505 | + | svc := NewService(runtime, nil, nil, nil, config, nil) |
| 394 | ctx := testContext() | 506 | ctx := testContext() | ||
| 395 | 507 | ||||
| 396 | err := svc.pullImage(ctx, "registry.example.com/myapp:latest", false) | 508 | err := svc.pullImage(ctx, "registry.example.com/myapp:latest", false) | ||
| @@ -408,7 +520,7 @@ func TestService_PullImage_InternalRetriesOnConnectionRefused(t *testing.T) { | |||||
| 408 | InternalRegistryPassword: "secret", | 520 | InternalRegistryPassword: "secret", | ||
| 409 | } | 521 | } | ||
| 410 | 522 | ||||
| 411 | - | svc := NewService(runtime, nil, nil, nil, config) | 523 | + | svc := NewService(runtime, nil, nil, nil, config, nil) |
| 412 | ctx := testContext() | 524 | ctx := testContext() | ||
| 413 | 525 | ||||
| 414 | runtime.EXPECT().PullImageWithAuth(mock.Anything, "localhost:5000/myapp:latest", "internal", "secret"). | 526 | runtime.EXPECT().PullImageWithAuth(mock.Anything, "localhost:5000/myapp:latest", "internal", "secret"). | ||
| @@ -435,7 +547,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) { | |||||
| 435 | DrainDelayConfigured: true, | 547 | DrainDelayConfigured: true, | ||
| 436 | StabilizationDelay: time.Millisecond, | 548 | StabilizationDelay: time.Millisecond, | ||
| 437 | } | 549 | } | ||
| 438 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 550 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 439 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 551 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 440 | ctx := testContext() | 552 | ctx := testContext() | ||
| 441 | 553 | ||||
| @@ -511,7 +623,7 @@ func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T | |||||
| 511 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | 623 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | ||
| 512 | 624 | ||||
| 513 | config := testMinDelayConfig() | 625 | config := testMinDelayConfig() | ||
| 514 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 626 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 515 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 627 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 516 | ctx := testContext() | 628 | ctx := testContext() | ||
| 517 | 629 | ||||
| @@ -608,7 +720,7 @@ func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) { | |||||
| 608 | eventBus := mocks.NewMockEventPublisher(t) | 720 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 609 | 721 | ||||
| 610 | config := testMinDelayConfig() | 722 | config := testMinDelayConfig() | ||
| 611 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 723 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 612 | ctx := testContext() | 724 | ctx := testContext() | ||
| 613 | expectedEnvHash := hashEnvironment([]string{}) | 725 | expectedEnvHash := hashEnvironment([]string{}) | ||
| 614 | 726 | ||||
| @@ -675,7 +787,7 @@ func TestService_Deploy_WithNetworkIsolation(t *testing.T) { | |||||
| 675 | DrainDelay: time.Millisecond, | 787 | DrainDelay: time.Millisecond, | ||
| 676 | DrainDelayConfigured: true, | 788 | DrainDelayConfigured: true, | ||
| 677 | } | 789 | } | ||
| 678 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 790 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 679 | ctx := testContext() | 791 | ctx := testContext() | ||
| 680 | 792 | ||||
| 681 | route := domain.Route{ | 793 | route := domain.Route{ | ||
| @@ -732,7 +844,7 @@ func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) { | |||||
| 732 | DrainDelay: time.Millisecond, | 844 | DrainDelay: time.Millisecond, | ||
| 733 | DrainDelayConfigured: true, | 845 | DrainDelayConfigured: true, | ||
| 734 | } | 846 | } | ||
| 735 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 847 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 736 | ctx := testContext() | 848 | ctx := testContext() | ||
| 737 | 849 | ||||
| 738 | route := domain.Route{ | 850 | route := domain.Route{ | ||
| @@ -784,7 +896,7 @@ func TestService_Stop(t *testing.T) { | |||||
| 784 | envLoader := mocks.NewMockEnvLoader(t) | 896 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 785 | eventBus := mocks.NewMockEventPublisher(t) | 897 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 786 | 898 | ||||
| 787 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 899 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 788 | ctx := testContext() | 900 | ctx := testContext() | ||
| 789 | 901 | ||||
| 790 | runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(nil) | 902 | runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(nil) | ||
| @@ -799,7 +911,7 @@ func TestService_Stop_Error(t *testing.T) { | |||||
| 799 | envLoader := mocks.NewMockEnvLoader(t) | 911 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 800 | eventBus := mocks.NewMockEventPublisher(t) | 912 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 801 | 913 | ||||
| 802 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 914 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 803 | ctx := testContext() | 915 | ctx := testContext() | ||
| 804 | 916 | ||||
| 805 | runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(errors.New("container not found")) | 917 | runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(errors.New("container not found")) | ||
| @@ -815,7 +927,7 @@ func TestService_Remove(t *testing.T) { | |||||
| 815 | envLoader := mocks.NewMockEnvLoader(t) | 927 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 816 | eventBus := mocks.NewMockEventPublisher(t) | 928 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 817 | 929 | ||||
| 818 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 930 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 819 | ctx := testContext() | 931 | ctx := testContext() | ||
| 820 | 932 | ||||
| 821 | // Add tracked container | 933 | // Add tracked container | ||
| @@ -844,7 +956,7 @@ func TestService_Remove_WithNetworkCleanup(t *testing.T) { | |||||
| 844 | NetworkIsolation: true, | 956 | NetworkIsolation: true, | ||
| 845 | NetworkPrefix: "gordon", | 957 | NetworkPrefix: "gordon", | ||
| 846 | } | 958 | } | ||
| 847 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 959 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 848 | ctx := testContext() | 960 | ctx := testContext() | ||
| 849 | 961 | ||||
| 850 | svc.containers["test.example.com"] = &domain.Container{ | 962 | svc.containers["test.example.com"] = &domain.Container{ | ||
| @@ -868,7 +980,7 @@ func TestService_Get(t *testing.T) { | |||||
| 868 | envLoader := mocks.NewMockEnvLoader(t) | 980 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 869 | eventBus := mocks.NewMockEventPublisher(t) | 981 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 870 | 982 | ||||
| 871 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 983 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 872 | ctx := testContext() | 984 | ctx := testContext() | ||
| 873 | 985 | ||||
| 874 | container := &domain.Container{ID: "container-123"} | 986 | container := &domain.Container{ID: "container-123"} | ||
| @@ -885,7 +997,7 @@ func TestService_Get_NotFound(t *testing.T) { | |||||
| 885 | envLoader := mocks.NewMockEnvLoader(t) | 997 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 886 | eventBus := mocks.NewMockEventPublisher(t) | 998 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 887 | 999 | ||||
| 888 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1000 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 889 | ctx := testContext() | 1001 | ctx := testContext() | ||
| 890 | 1002 | ||||
| 891 | result, exists := svc.Get(ctx, "nonexistent.example.com") | 1003 | result, exists := svc.Get(ctx, "nonexistent.example.com") | ||
| @@ -899,7 +1011,7 @@ func TestService_List(t *testing.T) { | |||||
| 899 | envLoader := mocks.NewMockEnvLoader(t) | 1011 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 900 | eventBus := mocks.NewMockEventPublisher(t) | 1012 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 901 | 1013 | ||||
| 902 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1014 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 903 | ctx := testContext() | 1015 | ctx := testContext() | ||
| 904 | 1016 | ||||
| 905 | svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"} | 1017 | svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"} | ||
| @@ -917,7 +1029,7 @@ func TestService_HealthCheck(t *testing.T) { | |||||
| 917 | envLoader := mocks.NewMockEnvLoader(t) | 1029 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 918 | eventBus := mocks.NewMockEventPublisher(t) | 1030 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 919 | 1031 | ||||
| 920 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1032 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 921 | ctx := testContext() | 1033 | ctx := testContext() | ||
| 922 | 1034 | ||||
| 923 | svc.containers["healthy.example.com"] = &domain.Container{ID: "healthy-container"} | 1035 | svc.containers["healthy.example.com"] = &domain.Container{ID: "healthy-container"} | ||
| @@ -937,7 +1049,7 @@ func TestService_SyncContainers(t *testing.T) { | |||||
| 937 | envLoader := mocks.NewMockEnvLoader(t) | 1049 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 938 | eventBus := mocks.NewMockEventPublisher(t) | 1050 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 939 | 1051 | ||||
| 940 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1052 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 941 | ctx := testContext() | 1053 | ctx := testContext() | ||
| 942 | 1054 | ||||
| 943 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | 1055 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | ||
| @@ -977,7 +1089,7 @@ func TestService_Shutdown(t *testing.T) { | |||||
| 977 | envLoader := mocks.NewMockEnvLoader(t) | 1089 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 978 | eventBus := mocks.NewMockEventPublisher(t) | 1090 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 979 | 1091 | ||||
| 980 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1092 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 981 | ctx := testContext() | 1093 | ctx := testContext() | ||
| 982 | 1094 | ||||
| 983 | svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"} | 1095 | svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"} | ||
| @@ -997,7 +1109,7 @@ func TestService_Restart_NotFound(t *testing.T) { | |||||
| 997 | envLoader := mocks.NewMockEnvLoader(t) | 1109 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 998 | eventBus := mocks.NewMockEventPublisher(t) | 1110 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 999 | 1111 | ||||
| 1000 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1112 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 1001 | ctx := testContext() | 1113 | ctx := testContext() | ||
| 1002 | 1114 | ||||
| 1003 | err := svc.Restart(ctx, "nonexistent.example.com", false) | 1115 | err := svc.Restart(ctx, "nonexistent.example.com", false) | ||
| @@ -1011,7 +1123,7 @@ func TestService_Restart_Success(t *testing.T) { | |||||
| 1011 | envLoader := mocks.NewMockEnvLoader(t) | 1123 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 1012 | eventBus := mocks.NewMockEventPublisher(t) | 1124 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 1013 | 1125 | ||||
| 1014 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1126 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 1015 | ctx := testContext() | 1127 | ctx := testContext() | ||
| 1016 | 1128 | ||||
| 1017 | svc.containers["test.example.com"] = &domain.Container{ | 1129 | svc.containers["test.example.com"] = &domain.Container{ | ||
| @@ -1032,7 +1144,7 @@ func TestService_Restart_Error(t *testing.T) { | |||||
| 1032 | envLoader := mocks.NewMockEnvLoader(t) | 1144 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 1033 | eventBus := mocks.NewMockEventPublisher(t) | 1145 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 1034 | 1146 | ||||
| 1035 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1147 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 1036 | ctx := testContext() | 1148 | ctx := testContext() | ||
| 1037 | 1149 | ||||
| 1038 | svc.containers["test.example.com"] = &domain.Container{ | 1150 | svc.containers["test.example.com"] = &domain.Container{ | ||
| @@ -1054,7 +1166,7 @@ func TestService_Restart_ReconcilesStaleContainerID(t *testing.T) { | |||||
| 1054 | envLoader := mocks.NewMockEnvLoader(t) | 1166 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 1055 | eventBus := mocks.NewMockEventPublisher(t) | 1167 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 1056 | 1168 | ||||
| 1057 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1169 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 1058 | ctx := testContext() | 1170 | ctx := testContext() | ||
| 1059 | 1171 | ||||
| 1060 | svc.containers["test.example.com"] = &domain.Container{ | 1172 | svc.containers["test.example.com"] = &domain.Container{ | ||
| @@ -1088,7 +1200,7 @@ func TestService_Restart_ReconcilesStaleContainerID_NotFoundAfterSync(t *testing | |||||
| 1088 | envLoader := mocks.NewMockEnvLoader(t) | 1200 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 1089 | eventBus := mocks.NewMockEventPublisher(t) | 1201 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 1090 | 1202 | ||||
| 1091 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1203 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 1092 | ctx := testContext() | 1204 | ctx := testContext() | ||
| 1093 | 1205 | ||||
| 1094 | svc.containers["test.example.com"] = &domain.Container{ | 1206 | svc.containers["test.example.com"] = &domain.Container{ | ||
| @@ -1113,7 +1225,7 @@ func TestService_Restart_WithAttachments(t *testing.T) { | |||||
| 1113 | envLoader := mocks.NewMockEnvLoader(t) | 1225 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 1114 | eventBus := mocks.NewMockEventPublisher(t) | 1226 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 1115 | 1227 | ||||
| 1116 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 1228 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 1117 | ctx := testContext() | 1229 | ctx := testContext() | ||
| 1118 | 1230 | ||||
| 1119 | svc.containers["test.example.com"] = &domain.Container{ | 1231 | svc.containers["test.example.com"] = &domain.Container{ | ||
| @@ -1122,6 +1234,31 @@ func TestService_Restart_WithAttachments(t *testing.T) { | |||||
| 1122 | Status: "running", | 1234 | Status: "running", | ||
| 1123 | } | 1235 | } | ||
| 1124 | svc.attachments["test.example.com"] = []string{"container-attach-1", "container-attach-2"} | 1236 | svc.attachments["test.example.com"] = []string{"container-attach-1", "container-attach-2"} | ||
| 1237 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |||
| 1238 | + | { | |||
| 1239 | + | ID: "container-123", | |||
| 1240 | + | Labels: map[string]string{ | |||
| 1241 | + | domain.LabelDomain: "test.example.com", | |||
| 1242 | + | domain.LabelManaged: "true", | |||
| 1243 | + | }, | |||
| 1244 | + | }, | |||
| 1245 | + | { | |||
| 1246 | + | ID: "container-attach-1", | |||
| 1247 | + | Labels: map[string]string{ | |||
| 1248 | + | domain.LabelManaged: "true", | |||
| 1249 | + | domain.LabelAttachment: "true", | |||
| 1250 | + | domain.LabelAttachedTo: "test.example.com", | |||
| 1251 | + | }, | |||
| 1252 | + | }, | |||
| 1253 | + | { | |||
| 1254 | + | ID: "container-attach-2", | |||
| 1255 | + | Labels: map[string]string{ | |||
| 1256 | + | domain.LabelManaged: "true", | |||
| 1257 | + | domain.LabelAttachment: "true", | |||
| 1258 | + | domain.LabelAttachedTo: "test.example.com", | |||
| 1259 | + | }, | |||
| 1260 | + | }, | |||
| 1261 | + | }, nil).Maybe() | |||
| 1125 | 1262 | ||||
| 1126 | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | 1263 | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | ||
| 1127 | runtime.EXPECT().RestartContainer(mock.Anything, "container-attach-1").Return(nil) | 1264 | runtime.EXPECT().RestartContainer(mock.Anything, "container-attach-1").Return(nil) | ||
| @@ -1132,6 +1269,177 @@ func TestService_Restart_WithAttachments(t *testing.T) { | |||||
| 1132 | assert.NoError(t, err) | 1269 | assert.NoError(t, err) | ||
| 1133 | } | 1270 | } | ||
| 1134 | 1271 | ||||
| 1272 | + | func TestService_Restart_WithAttachments_ErrorsWhenConfiguredButNotDeployed(t *testing.T) { | |||
| 1273 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 1274 | + | envLoader := mocks.NewMockEnvLoader(t) | |||
| 1275 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 1276 | + | ||||
| 1277 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |||
| 1278 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 1279 | + | Attachments: map[string][]string{"test.example.com": {"postgres:16"}}, | |||
| 1280 | + | NetworkGroups: map[string][]string{}, | |||
| 1281 | + | }).Maybe() | |||
| 1282 | + | ||||
| 1283 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |||
| 1284 | + | ||||
| 1285 | + | svc.containers["test.example.com"] = &domain.Container{ | |||
| 1286 | + | ID: "container-123", | |||
| 1287 | + | Name: "gordon-test.example.com", | |||
| 1288 | + | Status: "running", | |||
| 1289 | + | } | |||
| 1290 | + | // No attachment containers tracked — s.attachments is empty | |||
| 1291 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe() | |||
| 1292 | + | ||||
| 1293 | + | err := svc.Restart(testContext(), "test.example.com", true) | |||
| 1294 | + | ||||
| 1295 | + | assert.Error(t, err) | |||
| 1296 | + | assert.True(t, errors.Is(err, domain.ErrAttachmentNotDeployed)) | |||
| 1297 | + | // Main container should NOT have been restarted | |||
| 1298 | + | runtime.AssertNotCalled(t, "RestartContainer", mock.Anything, mock.Anything) | |||
| 1299 | + | } | |||
| 1300 | + | ||||
| 1301 | + | func TestService_Restart_WithAttachments_ErrorsWhenPartiallyDeployed(t *testing.T) { | |||
| 1302 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 1303 | + | envLoader := mocks.NewMockEnvLoader(t) | |||
| 1304 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 1305 | + | ||||
| 1306 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |||
| 1307 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 1308 | + | Attachments: map[string][]string{"test.example.com": {"postgres:16", "redis:7"}}, | |||
| 1309 | + | NetworkGroups: map[string][]string{}, | |||
| 1310 | + | }).Maybe() | |||
| 1311 | + | ||||
| 1312 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |||
| 1313 | + | ||||
| 1314 | + | svc.containers["test.example.com"] = &domain.Container{ | |||
| 1315 | + | ID: "container-123", | |||
| 1316 | + | Name: "gordon-test.example.com", | |||
| 1317 | + | Status: "running", | |||
| 1318 | + | } | |||
| 1319 | + | // Only 1 of 2 attachments deployed | |||
| 1320 | + | svc.attachments["test.example.com"] = []string{"attach-postgres"} | |||
| 1321 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |||
| 1322 | + | { | |||
| 1323 | + | ID: "container-123", | |||
| 1324 | + | Labels: map[string]string{ | |||
| 1325 | + | domain.LabelDomain: "test.example.com", | |||
| 1326 | + | domain.LabelManaged: "true", | |||
| 1327 | + | }, | |||
| 1328 | + | }, | |||
| 1329 | + | { | |||
| 1330 | + | ID: "attach-postgres", | |||
| 1331 | + | Labels: map[string]string{ | |||
| 1332 | + | domain.LabelManaged: "true", | |||
| 1333 | + | domain.LabelAttachment: "true", | |||
| 1334 | + | domain.LabelAttachedTo: "test.example.com", | |||
| 1335 | + | }, | |||
| 1336 | + | }, | |||
| 1337 | + | }, nil).Maybe() | |||
| 1338 | + | ||||
| 1339 | + | err := svc.Restart(testContext(), "test.example.com", true) | |||
| 1340 | + | ||||
| 1341 | + | assert.Error(t, err) | |||
| 1342 | + | assert.True(t, errors.Is(err, domain.ErrAttachmentNotDeployed)) | |||
| 1343 | + | assert.Contains(t, err.Error(), "1 missing") | |||
| 1344 | + | // Main container should NOT have been restarted | |||
| 1345 | + | runtime.AssertNotCalled(t, "RestartContainer", mock.Anything, mock.Anything) | |||
| 1346 | + | } | |||
| 1347 | + | ||||
| 1348 | + | func TestService_Restart_WithAttachments_SucceedsWhenAllDeployed(t *testing.T) { | |||
| 1349 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 1350 | + | envLoader := mocks.NewMockEnvLoader(t) | |||
| 1351 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 1352 | + | ||||
| 1353 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |||
| 1354 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 1355 | + | Attachments: map[string][]string{"test.example.com": {"postgres:16"}}, | |||
| 1356 | + | NetworkGroups: map[string][]string{}, | |||
| 1357 | + | }).Maybe() | |||
| 1358 | + | ||||
| 1359 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |||
| 1360 | + | ||||
| 1361 | + | svc.containers["test.example.com"] = &domain.Container{ | |||
| 1362 | + | ID: "container-123", | |||
| 1363 | + | Name: "gordon-test.example.com", | |||
| 1364 | + | Status: "running", | |||
| 1365 | + | } | |||
| 1366 | + | svc.attachments["test.example.com"] = []string{"attach-456"} | |||
| 1367 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |||
| 1368 | + | { | |||
| 1369 | + | ID: "container-123", | |||
| 1370 | + | Labels: map[string]string{ | |||
| 1371 | + | domain.LabelDomain: "test.example.com", | |||
| 1372 | + | domain.LabelManaged: "true", | |||
| 1373 | + | }, | |||
| 1374 | + | }, | |||
| 1375 | + | { | |||
| 1376 | + | ID: "attach-456", | |||
| 1377 | + | Labels: map[string]string{ | |||
| 1378 | + | domain.LabelManaged: "true", | |||
| 1379 | + | domain.LabelAttachment: "true", | |||
| 1380 | + | domain.LabelAttachedTo: "test.example.com", | |||
| 1381 | + | }, | |||
| 1382 | + | }, | |||
| 1383 | + | }, nil).Maybe() | |||
| 1384 | + | ||||
| 1385 | + | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | |||
| 1386 | + | runtime.EXPECT().RestartContainer(mock.Anything, "attach-456").Return(nil) | |||
| 1387 | + | ||||
| 1388 | + | err := svc.Restart(testContext(), "test.example.com", true) | |||
| 1389 | + | assert.NoError(t, err) | |||
| 1390 | + | } | |||
| 1391 | + | ||||
| 1392 | + | func TestService_Restart_WithoutAttachmentFlag_IgnoresMissingAttachments(t *testing.T) { | |||
| 1393 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 1394 | + | envLoader := mocks.NewMockEnvLoader(t) | |||
| 1395 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 1396 | + | ||||
| 1397 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |||
| 1398 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 1399 | + | Attachments: map[string][]string{"test.example.com": {"postgres:16"}}, | |||
| 1400 | + | NetworkGroups: map[string][]string{}, | |||
| 1401 | + | }).Maybe() | |||
| 1402 | + | ||||
| 1403 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |||
| 1404 | + | ||||
| 1405 | + | svc.containers["test.example.com"] = &domain.Container{ | |||
| 1406 | + | ID: "container-123", | |||
| 1407 | + | Name: "gordon-test.example.com", | |||
| 1408 | + | Status: "running", | |||
| 1409 | + | } | |||
| 1410 | + | ||||
| 1411 | + | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | |||
| 1412 | + | ||||
| 1413 | + | err := svc.Restart(testContext(), "test.example.com", false) | |||
| 1414 | + | assert.NoError(t, err) | |||
| 1415 | + | } | |||
| 1416 | + | ||||
| 1417 | + | func TestService_Restart_WithAttachments_NoConfiguredNoDeployed_Succeeds(t *testing.T) { | |||
| 1418 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 1419 | + | envLoader := mocks.NewMockEnvLoader(t) | |||
| 1420 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 1421 | + | ||||
| 1422 | + | provider := mocks.NewMockAttachmentConfigProvider(t) | |||
| 1423 | + | provider.EXPECT().GetAttachmentConfig().Return(out.AttachmentConfigSnapshot{ | |||
| 1424 | + | Attachments: map[string][]string{}, | |||
| 1425 | + | NetworkGroups: map[string][]string{}, | |||
| 1426 | + | }).Maybe() | |||
| 1427 | + | ||||
| 1428 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider) | |||
| 1429 | + | ||||
| 1430 | + | svc.containers["test.example.com"] = &domain.Container{ | |||
| 1431 | + | ID: "container-123", | |||
| 1432 | + | Name: "gordon-test.example.com", | |||
| 1433 | + | Status: "running", | |||
| 1434 | + | } | |||
| 1435 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe() | |||
| 1436 | + | ||||
| 1437 | + | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | |||
| 1438 | + | ||||
| 1439 | + | err := svc.Restart(testContext(), "test.example.com", true) | |||
| 1440 | + | assert.NoError(t, err) | |||
| 1441 | + | } | |||
| 1442 | + | ||||
| 1135 | func TestNormalizeImageRef(t *testing.T) { | 1443 | func TestNormalizeImageRef(t *testing.T) { | ||
| 1136 | tests := []struct { | 1444 | tests := []struct { | ||
| 1137 | name string | 1445 | name string | ||
| @@ -1452,7 +1760,7 @@ func TestService_Deploy_InternalDeployForcesPull(t *testing.T) { | |||||
| 1452 | DrainDelayConfigured: true, | 1760 | DrainDelayConfigured: true, | ||
| 1453 | } | 1761 | } | ||
| 1454 | 1762 | ||||
| 1455 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 1763 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 1456 | 1764 | ||||
| 1457 | // Use internal deploy context (simulating image push event) | 1765 | // Use internal deploy context (simulating image push event) | ||
| 1458 | ctx := domain.WithInternalDeploy(testContext()) | 1766 | ctx := domain.WithInternalDeploy(testContext()) | ||
| @@ -1525,7 +1833,7 @@ func TestService_AutoStart_StartsNewContainers(t *testing.T) { | |||||
| 1525 | DrainDelay: time.Millisecond, | 1833 | DrainDelay: time.Millisecond, | ||
| 1526 | DrainDelayConfigured: true, | 1834 | DrainDelayConfigured: true, | ||
| 1527 | } | 1835 | } | ||
| 1528 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 1836 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 1529 | ctx := testContext() | 1837 | ctx := testContext() | ||
| 1530 | 1838 | ||||
| 1531 | routes := []domain.Route{ | 1839 | routes := []domain.Route{ | ||
| @@ -1573,7 +1881,7 @@ func TestService_AutoStart_SkipsExistingContainers(t *testing.T) { | |||||
| 1573 | DrainDelay: time.Millisecond, | 1881 | DrainDelay: time.Millisecond, | ||
| 1574 | DrainDelayConfigured: true, | 1882 | DrainDelayConfigured: true, | ||
| 1575 | } | 1883 | } | ||
| 1576 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 1884 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 1577 | ctx := testContext() | 1885 | ctx := testContext() | ||
| 1578 | 1886 | ||||
| 1579 | // Pre-populate with existing container | 1887 | // Pre-populate with existing container | ||
| @@ -1617,7 +1925,7 @@ func TestService_AutoStart_HandlesDeployErrors(t *testing.T) { | |||||
| 1617 | eventBus := mocks.NewMockEventPublisher(t) | 1925 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 1618 | 1926 | ||||
| 1619 | config := Config{} | 1927 | config := Config{} | ||
| 1620 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 1928 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 1621 | ctx := testContext() | 1929 | ctx := testContext() | ||
| 1622 | 1930 | ||||
| 1623 | routes := []domain.Route{ | 1931 | routes := []domain.Route{ | ||
| @@ -1651,7 +1959,7 @@ func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) { | |||||
| 1651 | DrainDelay: time.Millisecond, | 1959 | DrainDelay: time.Millisecond, | ||
| 1652 | DrainDelayConfigured: true, | 1960 | DrainDelayConfigured: true, | ||
| 1653 | } | 1961 | } | ||
| 1654 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 1962 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 1655 | 1963 | ||||
| 1656 | // Mark context as internal deploy — this is what syncAndAutoStart should do | 1964 | // Mark context as internal deploy — this is what syncAndAutoStart should do | ||
| 1657 | ctx := domain.WithInternalDeploy(testContext()) | 1965 | ctx := domain.WithInternalDeploy(testContext()) | ||
| @@ -1703,7 +2011,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) { | |||||
| 1703 | DrainDelayConfigured: true, | 2011 | DrainDelayConfigured: true, | ||
| 1704 | StabilizationDelay: time.Millisecond, | 2012 | StabilizationDelay: time.Millisecond, | ||
| 1705 | } | 2013 | } | ||
| 1706 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2014 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 1707 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 2015 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 1708 | ctx := testContext() | 2016 | ctx := testContext() | ||
| 1709 | 2017 | ||||
| @@ -1795,7 +2103,7 @@ func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) { | |||||
| 1795 | DrainDelay: time.Millisecond, | 2103 | DrainDelay: time.Millisecond, | ||
| 1796 | DrainDelayConfigured: true, | 2104 | DrainDelayConfigured: true, | ||
| 1797 | } | 2105 | } | ||
| 1798 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2106 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 1799 | ctx := testContext() | 2107 | ctx := testContext() | ||
| 1800 | 2108 | ||||
| 1801 | // NO tracked container - service is empty for this domain | 2109 | // NO tracked container - service is empty for this domain | ||
| @@ -1871,7 +2179,7 @@ func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) { | |||||
| 1871 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | 2179 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | ||
| 1872 | 2180 | ||||
| 1873 | config := testMinDelayConfig() | 2181 | config := testMinDelayConfig() | ||
| 1874 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2182 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 1875 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 2183 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 1876 | ctx := testContext() | 2184 | ctx := testContext() | ||
| 1877 | 2185 | ||||
| @@ -1942,7 +2250,7 @@ func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T) | |||||
| 1942 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | 2250 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | ||
| 1943 | 2251 | ||||
| 1944 | config := testMinDelayConfig() | 2252 | config := testMinDelayConfig() | ||
| 1945 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2253 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 1946 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 2254 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 1947 | ctx := testContext() | 2255 | ctx := testContext() | ||
| 1948 | 2256 | ||||
| @@ -2053,7 +2361,7 @@ func TestService_StripRegistryPrefix(t *testing.T) { | |||||
| 2053 | config := Config{ | 2361 | config := Config{ | ||
| 2054 | RegistryDomain: tt.registryDomain, | 2362 | RegistryDomain: tt.registryDomain, | ||
| 2055 | } | 2363 | } | ||
| 2056 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2364 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2057 | 2365 | ||||
| 2058 | result := svc.stripRegistryPrefix(tt.image) | 2366 | result := svc.stripRegistryPrefix(tt.image) | ||
| 2059 | assert.Equal(t, tt.expected, result) | 2367 | assert.Equal(t, tt.expected, result) | ||
| @@ -2130,7 +2438,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) { | |||||
| 2130 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | 2438 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | ||
| 2131 | 2439 | ||||
| 2132 | config := testMinDelayConfig() | 2440 | config := testMinDelayConfig() | ||
| 2133 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2441 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2134 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 2442 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 2135 | ctx := testContext() | 2443 | ctx := testContext() | ||
| 2136 | 2444 | ||||
| @@ -2242,7 +2550,7 @@ func TestService_Deploy_ContextCancellation(t *testing.T) { | |||||
| 2242 | DrainDelay: time.Millisecond, | 2550 | DrainDelay: time.Millisecond, | ||
| 2243 | DrainDelayConfigured: true, | 2551 | DrainDelayConfigured: true, | ||
| 2244 | } | 2552 | } | ||
| 2245 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2553 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2246 | 2554 | ||||
| 2247 | route := domain.Route{ | 2555 | route := domain.Route{ | ||
| 2248 | Domain: "test.example.com", | 2556 | Domain: "test.example.com", | ||
| @@ -2268,7 +2576,7 @@ func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) { | |||||
| 2268 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | 2576 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | ||
| 2269 | 2577 | ||||
| 2270 | config := testMinDelayConfig() | 2578 | config := testMinDelayConfig() | ||
| 2271 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2579 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2272 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 2580 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 2273 | ctx := testContext() | 2581 | ctx := testContext() | ||
| 2274 | 2582 | ||||
| @@ -2347,7 +2655,7 @@ func TestService_Deploy_NilCacheInvalidator(t *testing.T) { | |||||
| 2347 | 2655 | ||||
| 2348 | config := testMinDelayConfig() | 2656 | config := testMinDelayConfig() | ||
| 2349 | // Intentionally NOT setting cache invalidator | 2657 | // Intentionally NOT setting cache invalidator | ||
| 2350 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2658 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2351 | ctx := testContext() | 2659 | ctx := testContext() | ||
| 2352 | 2660 | ||||
| 2353 | // Pre-populate with existing container | 2661 | // Pre-populate with existing container | ||
| @@ -2398,7 +2706,7 @@ func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) { | |||||
| 2398 | DrainDelay: time.Millisecond, | 2706 | DrainDelay: time.Millisecond, | ||
| 2399 | DrainDelayConfigured: true, | 2707 | DrainDelayConfigured: true, | ||
| 2400 | } | 2708 | } | ||
| 2401 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2709 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2402 | ctx := testContext() | 2710 | ctx := testContext() | ||
| 2403 | expectedEnvHash := hashEnvironment([]string{}) | 2711 | expectedEnvHash := hashEnvironment([]string{}) | ||
| 2404 | 2712 | ||||
| @@ -2457,7 +2765,7 @@ func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testin | |||||
| 2457 | DrainDelay: time.Millisecond, | 2765 | DrainDelay: time.Millisecond, | ||
| 2458 | DrainDelayConfigured: true, | 2766 | DrainDelayConfigured: true, | ||
| 2459 | } | 2767 | } | ||
| 2460 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2768 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2461 | ctx := testContext() | 2769 | ctx := testContext() | ||
| 2462 | expectedEnvHash := hashEnvironment([]string{}) | 2770 | expectedEnvHash := hashEnvironment([]string{}) | ||
| 2463 | 2771 | ||||
| @@ -2514,7 +2822,7 @@ func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) { | |||||
| 2514 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | 2822 | cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t) | ||
| 2515 | 2823 | ||||
| 2516 | config := testMinDelayConfig() | 2824 | config := testMinDelayConfig() | ||
| 2517 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2825 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2518 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 2826 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 2519 | ctx := testContext() | 2827 | ctx := testContext() | ||
| 2520 | 2828 | ||||
| @@ -2570,7 +2878,7 @@ func TestSyncContainersRebuildsAttachmentMap(t *testing.T) { | |||||
| 2570 | envLoader := mocks.NewMockEnvLoader(t) | 2878 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 2571 | eventBus := mocks.NewMockEventPublisher(t) | 2879 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 2572 | 2880 | ||||
| 2573 | - | svc := NewService(runtime, envLoader, eventBus, nil, Config{}) | 2881 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) |
| 2574 | ctx := testContext() | 2882 | ctx := testContext() | ||
| 2575 | 2883 | ||||
| 2576 | // ListContainers returns one main container and one attachment container | 2884 | // ListContainers returns one main container and one attachment container | ||
| @@ -2614,7 +2922,7 @@ func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) { | |||||
| 2614 | eventBus := mocks.NewMockEventPublisher(t) | 2922 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 2615 | 2923 | ||||
| 2616 | config := testMinDelayConfig() | 2924 | config := testMinDelayConfig() | ||
| 2617 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2925 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2618 | ctx := testContext() | 2926 | ctx := testContext() | ||
| 2619 | expectedEnvHash := hashEnvironment([]string{}) | 2927 | expectedEnvHash := hashEnvironment([]string{}) | ||
| 2620 | 2928 | ||||
| @@ -2676,7 +2984,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) { | |||||
| 2676 | eventBus := mocks.NewMockEventPublisher(t) | 2984 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 2677 | 2985 | ||||
| 2678 | config := testMinDelayConfig() | 2986 | config := testMinDelayConfig() | ||
| 2679 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 2987 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2680 | ctx := testContext() | 2988 | ctx := testContext() | ||
| 2681 | expectedEnvHash := hashEnvironment([]string{}) | 2989 | expectedEnvHash := hashEnvironment([]string{}) | ||
| 2682 | 2990 | ||||
| @@ -2733,7 +3041,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) { | |||||
| 2733 | eventBus := mocks.NewMockEventPublisher(t) | 3041 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 2734 | 3042 | ||||
| 2735 | config := testMinDelayConfig() | 3043 | config := testMinDelayConfig() | ||
| 2736 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 3044 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2737 | ctx := testContext() | 3045 | ctx := testContext() | ||
| 2738 | expectedEnvHash := hashEnvironment([]string{}) | 3046 | expectedEnvHash := hashEnvironment([]string{}) | ||
| 2739 | 3047 | ||||
| @@ -2787,7 +3095,7 @@ func TestService_attachmentEnvDrifted_ReturnsFalseWhenHashMatches(t *testing.T) | |||||
| 2787 | envLoader := mocks.NewMockEnvLoader(t) | 3095 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 2788 | eventBus := mocks.NewMockEventPublisher(t) | 3096 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 2789 | 3097 | ||||
| 2790 | - | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig()) | 3098 | + | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil) |
| 2791 | ctx := testContext() | 3099 | ctx := testContext() | ||
| 2792 | imageRef := svc.buildImageRef("postgres:16") | 3100 | imageRef := svc.buildImageRef("postgres:16") | ||
| 2793 | envVars := []string{"POSTGRES_PASSWORD=secret"} | 3101 | envVars := []string{"POSTGRES_PASSWORD=secret"} | ||
| @@ -2809,7 +3117,7 @@ func TestService_attachmentEnvDrifted_ReturnsTrueWhenHashDiffers(t *testing.T) { | |||||
| 2809 | envLoader := mocks.NewMockEnvLoader(t) | 3117 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 2810 | eventBus := mocks.NewMockEventPublisher(t) | 3118 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 2811 | 3119 | ||||
| 2812 | - | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig()) | 3120 | + | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil) |
| 2813 | ctx := testContext() | 3121 | ctx := testContext() | ||
| 2814 | imageRef := svc.buildImageRef("postgres:16") | 3122 | imageRef := svc.buildImageRef("postgres:16") | ||
| 2815 | envVars := []string{"POSTGRES_PASSWORD=secret"} | 3123 | envVars := []string{"POSTGRES_PASSWORD=secret"} | ||
| @@ -2830,7 +3138,7 @@ func TestService_attachmentEnvDrifted_ReturnsTrueWhenHashMissing(t *testing.T) { | |||||
| 2830 | envLoader := mocks.NewMockEnvLoader(t) | 3138 | envLoader := mocks.NewMockEnvLoader(t) | ||
| 2831 | eventBus := mocks.NewMockEventPublisher(t) | 3139 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 2832 | 3140 | ||||
| 2833 | - | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig()) | 3141 | + | svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil) |
| 2834 | ctx := testContext() | 3142 | ctx := testContext() | ||
| 2835 | imageRef := svc.buildImageRef("postgres:16") | 3143 | imageRef := svc.buildImageRef("postgres:16") | ||
| 2836 | envVars := []string{"POSTGRES_PASSWORD=secret"} | 3144 | envVars := []string{"POSTGRES_PASSWORD=secret"} | ||
| @@ -2858,7 +3166,7 @@ func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) { | |||||
| 2858 | eventBus := mocks.NewMockEventPublisher(t) | 3166 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 2859 | 3167 | ||||
| 2860 | config := testMinDelayConfig() | 3168 | config := testMinDelayConfig() | ||
| 2861 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 3169 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2862 | ctx := testContext() | 3170 | ctx := testContext() | ||
| 2863 | 3171 | ||||
| 2864 | // The canonical container is running and has the managed label — it will be | 3172 | // The canonical container is running and has the managed label — it will be | ||
| @@ -2959,7 +3267,7 @@ func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) { | |||||
| 2959 | DrainDelayConfigured: true, | 3267 | DrainDelayConfigured: true, | ||
| 2960 | StabilizationDelay: time.Millisecond, | 3268 | StabilizationDelay: time.Millisecond, | ||
| 2961 | } | 3269 | } | ||
| 2962 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 3270 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 2963 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 3271 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 2964 | ctx := testContext() | 3272 | ctx := testContext() | ||
| 2965 | 3273 | ||||
| @@ -3052,7 +3360,7 @@ func TestService_Deploy_StabilizationSuccess(t *testing.T) { | |||||
| 3052 | DrainDelayConfigured: true, | 3360 | DrainDelayConfigured: true, | ||
| 3053 | StabilizationDelay: time.Millisecond, | 3361 | StabilizationDelay: time.Millisecond, | ||
| 3054 | } | 3362 | } | ||
| 3055 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 3363 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 3056 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 3364 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 3057 | ctx := testContext() | 3365 | ctx := testContext() | ||
| 3058 | 3366 | ||||
| @@ -3138,7 +3446,7 @@ func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T) | |||||
| 3138 | DrainDelayConfigured: true, | 3446 | DrainDelayConfigured: true, | ||
| 3139 | StabilizationDelay: time.Millisecond, | 3447 | StabilizationDelay: time.Millisecond, | ||
| 3140 | } | 3448 | } | ||
| 3141 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 3449 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 3142 | svc.SetProxyCacheInvalidator(cacheInvalidator) | 3450 | svc.SetProxyCacheInvalidator(cacheInvalidator) | ||
| 3143 | ctx := testContext() | 3451 | ctx := testContext() | ||
| 3144 | 3452 | ||||
| @@ -3231,7 +3539,7 @@ func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T) | |||||
| 3231 | DrainDelayConfigured: true, | 3539 | DrainDelayConfigured: true, | ||
| 3232 | StabilizationDelay: time.Millisecond, | 3540 | StabilizationDelay: time.Millisecond, | ||
| 3233 | } | 3541 | } | ||
| 3234 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 3542 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 3235 | ctx := testContext() | 3543 | ctx := testContext() | ||
| 3236 | 3544 | ||||
| 3237 | // Pre-populate with existing tracked container | 3545 | // Pre-populate with existing tracked container | ||
| @@ -3312,7 +3620,7 @@ func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) { | |||||
| 3312 | DrainDelayConfigured: true, | 3620 | DrainDelayConfigured: true, | ||
| 3313 | StabilizationDelay: time.Millisecond, | 3621 | StabilizationDelay: time.Millisecond, | ||
| 3314 | } | 3622 | } | ||
| 3315 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 3623 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 3316 | ctx := testContext() | 3624 | ctx := testContext() | ||
| 3317 | 3625 | ||||
| 3318 | // NO existing container — this is a first deploy | 3626 | // NO existing container — this is a first deploy | ||
| @@ -3389,7 +3697,7 @@ func TestService_Deploy_PropagatesImageLabelsToContainerConfig(t *testing.T) { | |||||
| 3389 | DrainDelayConfigured: true, | 3697 | DrainDelayConfigured: true, | ||
| 3390 | } | 3698 | } | ||
| 3391 | 3699 | ||||
| 3392 | - | svc := NewService(runtime, envLoader, eventBus, nil, config) | 3700 | + | svc := NewService(runtime, envLoader, eventBus, nil, config, nil) |
| 3393 | ctx := testContext() | 3701 | ctx := testContext() | ||
| 3394 | 3702 | ||||
| 3395 | route := domain.Route{ | 3703 | route := domain.Route{ | ||