detached f009838

feat(container): wire AttachmentConfigProvider into container service

commit
f009838701796f90f6b3345b71507206794a5b1a
author
brice · · 2026-03-15 16:08 UTC
parent
ae29dfc
signature
unsigned
5 files · +109 -83
M internal/app/run.go +1 -1
@@ -1239,7 +1239,7 @@ func createContainerService(ctx context.Context, v *viper.Viper, cfg Config, svc
12391239 log.Warn().Msg("registry auth disabled; container image pulls will use unauthenticated mode")
12401240 }
12411241
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
12431243 }
12441244
12451245 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), nil1242+ 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) {
M internal/usecase/container/routes_test.go +5 -5
@@ -12,7 +12,7 @@ import (
1212
1313 func TestService_ListRoutesWithDetails(t *testing.T) {
1414 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)
1616 ctx := testContext()
1717
1818 svc.containers["app.example.com"] = &domain.Container{
@@ -80,7 +80,7 @@ func TestService_ListRoutesWithDetails(t *testing.T) {
8080
8181 func TestService_ListRoutesWithDetails_Empty(t *testing.T) {
8282 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)
8484 ctx := testContext()
8585
8686 // getAllAttachments is called even with empty containers
@@ -93,7 +93,7 @@ func TestService_ListRoutesWithDetails_Empty(t *testing.T) {
9393
9494 func TestService_ListRoutesWithDetails_NetworkError(t *testing.T) {
9595 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)
9797 ctx := testContext()
9898
9999 svc.containers["app.example.com"] = &domain.Container{
@@ -113,7 +113,7 @@ func TestService_ListRoutesWithDetails_NetworkError(t *testing.T) {
113113
114114 func TestService_ListAttachments(t *testing.T) {
115115 runtime := mocks.NewMockContainerRuntime(t)
116- svc := NewService(runtime, nil, nil, nil, Config{})
116+ svc := NewService(runtime, nil, nil, nil, Config{}, nil)
117117 ctx := testContext()
118118
119119 runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{
@@ -153,7 +153,7 @@ func TestService_ListAttachments(t *testing.T) {
153153
154154 func TestService_ListNetworks(t *testing.T) {
155155 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)
157157 ctx := testContext()
158158
159159 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 containers86 // 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 +10 -7
@@ -76,6 +76,7 @@ type Service struct {
7676 cacheInvalidator out.ProxyCacheInvalidator
7777 drainWaiter out.ProxyDrainWaiter
7878 config Config
79+ configProvider AttachmentConfigProvider // live config reads for attachments/networks (may be nil)
7980 metrics *telemetry.Metrics
8081 containers map[string]*domain.Container
8182 attachments map[string][]string // ownerDomain → []containerIDs
@@ -136,15 +137,17 @@ func NewService(
136137 eventBus out.EventPublisher,
137138 logWriter out.ContainerLogWriter,
138139 config Config,
140+ configProvider AttachmentConfigProvider,
139141 ) *Service {
140142 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),
148151 }
149152 }
150153
@@ -76,6 +76,7 @@ type Service struct {
76 cacheInvalidator out.ProxyCacheInvalidator76 cacheInvalidator out.ProxyCacheInvalidator
77 drainWaiter out.ProxyDrainWaiter77 drainWaiter out.ProxyDrainWaiter
78 config Config78 config Config
79+ configProvider AttachmentConfigProvider // live config reads for attachments/networks (may be nil)
79 metrics *telemetry.Metrics80 metrics *telemetry.Metrics
80 containers map[string]*domain.Container81 containers map[string]*domain.Container
81 attachments map[string][]string // ownerDomain → []containerIDs82 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
M internal/usecase/container/service_readiness_test.go +10 -10
@@ -31,7 +31,7 @@ func TestService_WaitForReady_AutoFallsBackToDelayWhenNoHealthcheck(t *testing.T
3131 svc := NewService(runtime, nil, nil, nil, Config{
3232 ReadinessMode: "auto",
3333 ReadinessDelay: time.Millisecond,
34- })
34+ }, nil)
3535
3636 ctx := testContext()
3737 containerID := "container-1"
@@ -52,7 +52,7 @@ func TestService_WaitForReady_AutoCascadeUsesDefaultHTTPAliveProbeWhenNoHealthch
5252 svc := NewService(runtime, nil, nil, nil, Config{
5353 ReadinessMode: "auto",
5454 HTTPProbeTimeout: 50 * time.Millisecond,
55- })
55+ }, nil)
5656
5757 ctx := testContext()
5858 containerID := "container-1"
@@ -87,7 +87,7 @@ func TestService_WaitForReady_AutoCascadeUsesHTTPProbeWhenHealthLabelSet(t *test
8787 svc := NewService(runtime, nil, nil, nil, Config{
8888 ReadinessMode: "auto",
8989 HTTPProbeTimeout: 50 * time.Millisecond,
90- })
90+ }, nil)
9191
9292 ctx := testContext()
9393 containerID := "container-1"
@@ -122,7 +122,7 @@ func TestService_WaitForReady_AutoCascadeUsesHealthcheckWhenPresent(t *testing.T
122122 svc := NewService(runtime, nil, nil, nil, Config{
123123 ReadinessMode: "auto",
124124 HealthTimeout: 50 * time.Millisecond,
125- })
125+ }, nil)
126126
127127 ctx := testContext()
128128 containerID := "container-1"
@@ -151,7 +151,7 @@ func TestService_WaitForReady_AutoCascadeFallsToDelayWhenNoEndpoint(t *testing.T
151151 svc := NewService(runtime, nil, nil, nil, Config{
152152 ReadinessMode: "auto",
153153 ReadinessDelay: time.Millisecond,
154- })
154+ }, nil)
155155
156156 ctx := testContext()
157157 containerID := "container-1"
@@ -180,7 +180,7 @@ func TestService_WaitForReady_ExplicitDelayModeIgnoresCascade(t *testing.T) {
180180 svc := NewService(runtime, nil, nil, nil, Config{
181181 ReadinessMode: "delay",
182182 ReadinessDelay: time.Millisecond,
183- })
183+ }, nil)
184184
185185 ctx := testContext()
186186 containerID := "container-1"
@@ -205,7 +205,7 @@ func TestService_WaitForReady_ExplicitDelayModeIgnoresCascade(t *testing.T) {
205205
206206 func TestService_WaitForHealthy_EmptyStatusIsReportedAsStarting(t *testing.T) {
207207 runtime := mocks.NewMockContainerRuntime(t)
208- svc := NewService(runtime, nil, nil, nil, Config{})
208+ svc := NewService(runtime, nil, nil, nil, Config{}, nil)
209209
210210 ctx := testContext()
211211 containerID := "container-1"
@@ -222,7 +222,7 @@ func TestService_WaitForReady_ProbeUsesGordonProxyPortLabel(t *testing.T) {
222222 svc := NewService(runtime, nil, nil, nil, Config{
223223 ReadinessMode: "auto",
224224 HTTPProbeTimeout: 50 * time.Millisecond,
225- })
225+ }, nil)
226226
227227 ctx := testContext()
228228 containerID := "container-port-label"
@@ -255,7 +255,7 @@ func TestService_WaitForReady_ProbeUsesDeprecatedPortLabel(t *testing.T) {
255255 svc := NewService(runtime, nil, nil, nil, Config{
256256 ReadinessMode: "auto",
257257 HTTPProbeTimeout: 50 * time.Millisecond,
258- })
258+ }, nil)
259259
260260 ctx := testContext()
261261 containerID := "container-proxy-port"
@@ -286,7 +286,7 @@ func TestService_WaitForReady_ProxyPortTakesPriorityOverPort(t *testing.T) {
286286 svc := NewService(runtime, nil, nil, nil, Config{
287287 ReadinessMode: "auto",
288288 HTTPProbeTimeout: 50 * time.Millisecond,
289- })
289+ }, nil)
290290
291291 ctx := testContext()
292292 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 +83 -60
@@ -21,6 +21,15 @@ import (
2121 "github.com/bnema/gordon/internal/domain"
2222 )
2323
24+// fakeConfigProvider implements AttachmentConfigProvider for tests.
25+type fakeConfigProvider struct {
26+ attachments map[string][]string
27+ networkGroups map[string][]string
28+}
29+
30+func (f *fakeConfigProvider) GetAttachments() map[string][]string { return f.attachments }
31+func (f *fakeConfigProvider) GetNetworkGroups() map[string][]string { return f.networkGroups }
32+
2433 func testContext() context.Context {
2534 return zerowrap.WithCtx(context.Background(), zerowrap.Default())
2635 }
@@ -80,12 +89,26 @@ func managedMetricState(t *testing.T, reader *sdkmetric.ManualReader) (int64, in
8089 return 0, 0, nil
8190 }
8291
92+func TestService_NewService_WithConfigProvider(t *testing.T) {
93+ runtime := mocks.NewMockContainerRuntime(t)
94+ envLoader := mocks.NewMockEnvLoader(t)
95+ eventBus := mocks.NewMockEventPublisher(t)
96+
97+ provider := &fakeConfigProvider{
98+ attachments: map[string][]string{"test.com": {"postgres:16"}},
99+ networkGroups: map[string][]string{"group1": {"test.com"}},
100+ }
101+
102+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider)
103+ assert.NotNil(t, svc)
104+}
105+
83106 func TestService_ManagedContainersMetric_GlobalSeriesOnDeployReplaceAndRemove(t *testing.T) {
84107 runtime := mocks.NewMockContainerRuntime(t)
85108 envLoader := mocks.NewMockEnvLoader(t)
86109 eventBus := mocks.NewMockEventPublisher(t)
87110
88- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
111+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
89112 ctx := testContext()
90113 metrics, reader := setupMetricsTest(t)
91114 svc.SetMetrics(metrics)
@@ -119,7 +142,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T)
119142 envLoader := mocks.NewMockEnvLoader(t)
120143 eventBus := mocks.NewMockEventPublisher(t)
121144
122- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
145+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
123146 ctx := testContext()
124147 metrics, reader := setupMetricsTest(t)
125148 svc.SetMetrics(metrics)
@@ -202,7 +225,7 @@ func TestService_Deploy_Success(t *testing.T) {
202225 DrainDelayConfigured: true,
203226 }
204227
205- svc := NewService(runtime, envLoader, eventBus, nil, config)
228+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
206229 ctx := testContext()
207230
208231 route := domain.Route{
@@ -284,7 +307,7 @@ func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T
284307 DrainDelayConfigured: true,
285308 }
286309
287- svc := NewService(runtime, envLoader, eventBus, nil, config)
310+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
288311 ctx := testContext()
289312
290313 route := domain.Route{
@@ -342,7 +365,7 @@ func TestService_Deploy_ImagePullFailure(t *testing.T) {
342365 eventBus := mocks.NewMockEventPublisher(t)
343366
344367 config := Config{}
345- svc := NewService(runtime, envLoader, eventBus, nil, config)
368+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
346369 ctx := testContext()
347370
348371 route := domain.Route{
@@ -373,7 +396,7 @@ func TestService_PullImage_UsesServiceTokenForExternalPull(t *testing.T) {
373396 ServiceToken: "service-token",
374397 }
375398
376- svc := NewService(runtime, nil, nil, nil, config)
399+ svc := NewService(runtime, nil, nil, nil, config, nil)
377400 ctx := testContext()
378401
379402 runtime.EXPECT().PullImageWithAuth(mock.Anything, "registry.example.com/myapp:latest", "gordon-service", "service-token").Return(nil)
@@ -390,7 +413,7 @@ func TestService_PullImage_RequiresServiceTokenForExternalPull(t *testing.T) {
390413 RegistryAuthEnabled: true,
391414 }
392415
393- svc := NewService(runtime, nil, nil, nil, config)
416+ svc := NewService(runtime, nil, nil, nil, config, nil)
394417 ctx := testContext()
395418
396419 err := svc.pullImage(ctx, "registry.example.com/myapp:latest", false)
@@ -408,7 +431,7 @@ func TestService_PullImage_InternalRetriesOnConnectionRefused(t *testing.T) {
408431 InternalRegistryPassword: "secret",
409432 }
410433
411- svc := NewService(runtime, nil, nil, nil, config)
434+ svc := NewService(runtime, nil, nil, nil, config, nil)
412435 ctx := testContext()
413436
414437 runtime.EXPECT().PullImageWithAuth(mock.Anything, "localhost:5000/myapp:latest", "internal", "secret").
@@ -435,7 +458,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) {
435458 DrainDelayConfigured: true,
436459 StabilizationDelay: time.Millisecond,
437460 }
438- svc := NewService(runtime, envLoader, eventBus, nil, config)
461+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
439462 svc.SetProxyCacheInvalidator(cacheInvalidator)
440463 ctx := testContext()
441464
@@ -511,7 +534,7 @@ func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T
511534 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
512535
513536 config := testMinDelayConfig()
514- svc := NewService(runtime, envLoader, eventBus, nil, config)
537+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
515538 svc.SetProxyCacheInvalidator(cacheInvalidator)
516539 ctx := testContext()
517540
@@ -608,7 +631,7 @@ func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) {
608631 eventBus := mocks.NewMockEventPublisher(t)
609632
610633 config := testMinDelayConfig()
611- svc := NewService(runtime, envLoader, eventBus, nil, config)
634+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
612635 ctx := testContext()
613636 expectedEnvHash := hashEnvironment([]string{})
614637
@@ -675,7 +698,7 @@ func TestService_Deploy_WithNetworkIsolation(t *testing.T) {
675698 DrainDelay: time.Millisecond,
676699 DrainDelayConfigured: true,
677700 }
678- svc := NewService(runtime, envLoader, eventBus, nil, config)
701+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
679702 ctx := testContext()
680703
681704 route := domain.Route{
@@ -732,7 +755,7 @@ func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) {
732755 DrainDelay: time.Millisecond,
733756 DrainDelayConfigured: true,
734757 }
735- svc := NewService(runtime, envLoader, eventBus, nil, config)
758+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
736759 ctx := testContext()
737760
738761 route := domain.Route{
@@ -784,7 +807,7 @@ func TestService_Stop(t *testing.T) {
784807 envLoader := mocks.NewMockEnvLoader(t)
785808 eventBus := mocks.NewMockEventPublisher(t)
786809
787- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
810+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
788811 ctx := testContext()
789812
790813 runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(nil)
@@ -799,7 +822,7 @@ func TestService_Stop_Error(t *testing.T) {
799822 envLoader := mocks.NewMockEnvLoader(t)
800823 eventBus := mocks.NewMockEventPublisher(t)
801824
802- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
825+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
803826 ctx := testContext()
804827
805828 runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(errors.New("container not found"))
@@ -815,7 +838,7 @@ func TestService_Remove(t *testing.T) {
815838 envLoader := mocks.NewMockEnvLoader(t)
816839 eventBus := mocks.NewMockEventPublisher(t)
817840
818- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
841+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
819842 ctx := testContext()
820843
821844 // Add tracked container
@@ -844,7 +867,7 @@ func TestService_Remove_WithNetworkCleanup(t *testing.T) {
844867 NetworkIsolation: true,
845868 NetworkPrefix: "gordon",
846869 }
847- svc := NewService(runtime, envLoader, eventBus, nil, config)
870+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
848871 ctx := testContext()
849872
850873 svc.containers["test.example.com"] = &domain.Container{
@@ -868,7 +891,7 @@ func TestService_Get(t *testing.T) {
868891 envLoader := mocks.NewMockEnvLoader(t)
869892 eventBus := mocks.NewMockEventPublisher(t)
870893
871- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
894+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
872895 ctx := testContext()
873896
874897 container := &domain.Container{ID: "container-123"}
@@ -885,7 +908,7 @@ func TestService_Get_NotFound(t *testing.T) {
885908 envLoader := mocks.NewMockEnvLoader(t)
886909 eventBus := mocks.NewMockEventPublisher(t)
887910
888- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
911+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
889912 ctx := testContext()
890913
891914 result, exists := svc.Get(ctx, "nonexistent.example.com")
@@ -899,7 +922,7 @@ func TestService_List(t *testing.T) {
899922 envLoader := mocks.NewMockEnvLoader(t)
900923 eventBus := mocks.NewMockEventPublisher(t)
901924
902- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
925+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
903926 ctx := testContext()
904927
905928 svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"}
@@ -917,7 +940,7 @@ func TestService_HealthCheck(t *testing.T) {
917940 envLoader := mocks.NewMockEnvLoader(t)
918941 eventBus := mocks.NewMockEventPublisher(t)
919942
920- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
943+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
921944 ctx := testContext()
922945
923946 svc.containers["healthy.example.com"] = &domain.Container{ID: "healthy-container"}
@@ -937,7 +960,7 @@ func TestService_SyncContainers(t *testing.T) {
937960 envLoader := mocks.NewMockEnvLoader(t)
938961 eventBus := mocks.NewMockEventPublisher(t)
939962
940- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
963+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
941964 ctx := testContext()
942965
943966 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
@@ -977,7 +1000,7 @@ func TestService_Shutdown(t *testing.T) {
9771000 envLoader := mocks.NewMockEnvLoader(t)
9781001 eventBus := mocks.NewMockEventPublisher(t)
9791002
980- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
1003+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
9811004 ctx := testContext()
9821005
9831006 svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"}
@@ -997,7 +1020,7 @@ func TestService_Restart_NotFound(t *testing.T) {
9971020 envLoader := mocks.NewMockEnvLoader(t)
9981021 eventBus := mocks.NewMockEventPublisher(t)
9991022
1000- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
1023+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
10011024 ctx := testContext()
10021025
10031026 err := svc.Restart(ctx, "nonexistent.example.com", false)
@@ -1011,7 +1034,7 @@ func TestService_Restart_Success(t *testing.T) {
10111034 envLoader := mocks.NewMockEnvLoader(t)
10121035 eventBus := mocks.NewMockEventPublisher(t)
10131036
1014- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
1037+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
10151038 ctx := testContext()
10161039
10171040 svc.containers["test.example.com"] = &domain.Container{
@@ -1032,7 +1055,7 @@ func TestService_Restart_Error(t *testing.T) {
10321055 envLoader := mocks.NewMockEnvLoader(t)
10331056 eventBus := mocks.NewMockEventPublisher(t)
10341057
1035- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
1058+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
10361059 ctx := testContext()
10371060
10381061 svc.containers["test.example.com"] = &domain.Container{
@@ -1054,7 +1077,7 @@ func TestService_Restart_ReconcilesStaleContainerID(t *testing.T) {
10541077 envLoader := mocks.NewMockEnvLoader(t)
10551078 eventBus := mocks.NewMockEventPublisher(t)
10561079
1057- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
1080+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
10581081 ctx := testContext()
10591082
10601083 svc.containers["test.example.com"] = &domain.Container{
@@ -1088,7 +1111,7 @@ func TestService_Restart_ReconcilesStaleContainerID_NotFoundAfterSync(t *testing
10881111 envLoader := mocks.NewMockEnvLoader(t)
10891112 eventBus := mocks.NewMockEventPublisher(t)
10901113
1091- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
1114+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
10921115 ctx := testContext()
10931116
10941117 svc.containers["test.example.com"] = &domain.Container{
@@ -1113,7 +1136,7 @@ func TestService_Restart_WithAttachments(t *testing.T) {
11131136 envLoader := mocks.NewMockEnvLoader(t)
11141137 eventBus := mocks.NewMockEventPublisher(t)
11151138
1116- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
1139+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
11171140 ctx := testContext()
11181141
11191142 svc.containers["test.example.com"] = &domain.Container{
@@ -1452,7 +1475,7 @@ func TestService_Deploy_InternalDeployForcesPull(t *testing.T) {
14521475 DrainDelayConfigured: true,
14531476 }
14541477
1455- svc := NewService(runtime, envLoader, eventBus, nil, config)
1478+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
14561479
14571480 // Use internal deploy context (simulating image push event)
14581481 ctx := domain.WithInternalDeploy(testContext())
@@ -1525,7 +1548,7 @@ func TestService_AutoStart_StartsNewContainers(t *testing.T) {
15251548 DrainDelay: time.Millisecond,
15261549 DrainDelayConfigured: true,
15271550 }
1528- svc := NewService(runtime, envLoader, eventBus, nil, config)
1551+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
15291552 ctx := testContext()
15301553
15311554 routes := []domain.Route{
@@ -1573,7 +1596,7 @@ func TestService_AutoStart_SkipsExistingContainers(t *testing.T) {
15731596 DrainDelay: time.Millisecond,
15741597 DrainDelayConfigured: true,
15751598 }
1576- svc := NewService(runtime, envLoader, eventBus, nil, config)
1599+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
15771600 ctx := testContext()
15781601
15791602 // Pre-populate with existing container
@@ -1617,7 +1640,7 @@ func TestService_AutoStart_HandlesDeployErrors(t *testing.T) {
16171640 eventBus := mocks.NewMockEventPublisher(t)
16181641
16191642 config := Config{}
1620- svc := NewService(runtime, envLoader, eventBus, nil, config)
1643+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
16211644 ctx := testContext()
16221645
16231646 routes := []domain.Route{
@@ -1651,7 +1674,7 @@ func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) {
16511674 DrainDelay: time.Millisecond,
16521675 DrainDelayConfigured: true,
16531676 }
1654- svc := NewService(runtime, envLoader, eventBus, nil, config)
1677+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
16551678
16561679 // Mark context as internal deploy — this is what syncAndAutoStart should do
16571680 ctx := domain.WithInternalDeploy(testContext())
@@ -1703,7 +1726,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) {
17031726 DrainDelayConfigured: true,
17041727 StabilizationDelay: time.Millisecond,
17051728 }
1706- svc := NewService(runtime, envLoader, eventBus, nil, config)
1729+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
17071730 svc.SetProxyCacheInvalidator(cacheInvalidator)
17081731 ctx := testContext()
17091732
@@ -1795,7 +1818,7 @@ func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) {
17951818 DrainDelay: time.Millisecond,
17961819 DrainDelayConfigured: true,
17971820 }
1798- svc := NewService(runtime, envLoader, eventBus, nil, config)
1821+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
17991822 ctx := testContext()
18001823
18011824 // NO tracked container - service is empty for this domain
@@ -1871,7 +1894,7 @@ func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) {
18711894 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
18721895
18731896 config := testMinDelayConfig()
1874- svc := NewService(runtime, envLoader, eventBus, nil, config)
1897+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
18751898 svc.SetProxyCacheInvalidator(cacheInvalidator)
18761899 ctx := testContext()
18771900
@@ -1942,7 +1965,7 @@ func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T)
19421965 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
19431966
19441967 config := testMinDelayConfig()
1945- svc := NewService(runtime, envLoader, eventBus, nil, config)
1968+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
19461969 svc.SetProxyCacheInvalidator(cacheInvalidator)
19471970 ctx := testContext()
19481971
@@ -2053,7 +2076,7 @@ func TestService_StripRegistryPrefix(t *testing.T) {
20532076 config := Config{
20542077 RegistryDomain: tt.registryDomain,
20552078 }
2056- svc := NewService(runtime, envLoader, eventBus, nil, config)
2079+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
20572080
20582081 result := svc.stripRegistryPrefix(tt.image)
20592082 assert.Equal(t, tt.expected, result)
@@ -2130,7 +2153,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) {
21302153 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
21312154
21322155 config := testMinDelayConfig()
2133- svc := NewService(runtime, envLoader, eventBus, nil, config)
2156+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
21342157 svc.SetProxyCacheInvalidator(cacheInvalidator)
21352158 ctx := testContext()
21362159
@@ -2242,7 +2265,7 @@ func TestService_Deploy_ContextCancellation(t *testing.T) {
22422265 DrainDelay: time.Millisecond,
22432266 DrainDelayConfigured: true,
22442267 }
2245- svc := NewService(runtime, envLoader, eventBus, nil, config)
2268+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
22462269
22472270 route := domain.Route{
22482271 Domain: "test.example.com",
@@ -2268,7 +2291,7 @@ func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) {
22682291 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
22692292
22702293 config := testMinDelayConfig()
2271- svc := NewService(runtime, envLoader, eventBus, nil, config)
2294+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
22722295 svc.SetProxyCacheInvalidator(cacheInvalidator)
22732296 ctx := testContext()
22742297
@@ -2347,7 +2370,7 @@ func TestService_Deploy_NilCacheInvalidator(t *testing.T) {
23472370
23482371 config := testMinDelayConfig()
23492372 // Intentionally NOT setting cache invalidator
2350- svc := NewService(runtime, envLoader, eventBus, nil, config)
2373+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
23512374 ctx := testContext()
23522375
23532376 // Pre-populate with existing container
@@ -2398,7 +2421,7 @@ func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) {
23982421 DrainDelay: time.Millisecond,
23992422 DrainDelayConfigured: true,
24002423 }
2401- svc := NewService(runtime, envLoader, eventBus, nil, config)
2424+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
24022425 ctx := testContext()
24032426 expectedEnvHash := hashEnvironment([]string{})
24042427
@@ -2457,7 +2480,7 @@ func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testin
24572480 DrainDelay: time.Millisecond,
24582481 DrainDelayConfigured: true,
24592482 }
2460- svc := NewService(runtime, envLoader, eventBus, nil, config)
2483+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
24612484 ctx := testContext()
24622485 expectedEnvHash := hashEnvironment([]string{})
24632486
@@ -2514,7 +2537,7 @@ func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) {
25142537 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
25152538
25162539 config := testMinDelayConfig()
2517- svc := NewService(runtime, envLoader, eventBus, nil, config)
2540+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
25182541 svc.SetProxyCacheInvalidator(cacheInvalidator)
25192542 ctx := testContext()
25202543
@@ -2570,7 +2593,7 @@ func TestSyncContainersRebuildsAttachmentMap(t *testing.T) {
25702593 envLoader := mocks.NewMockEnvLoader(t)
25712594 eventBus := mocks.NewMockEventPublisher(t)
25722595
2573- svc := NewService(runtime, envLoader, eventBus, nil, Config{})
2596+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
25742597 ctx := testContext()
25752598
25762599 // ListContainers returns one main container and one attachment container
@@ -2614,7 +2637,7 @@ func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) {
26142637 eventBus := mocks.NewMockEventPublisher(t)
26152638
26162639 config := testMinDelayConfig()
2617- svc := NewService(runtime, envLoader, eventBus, nil, config)
2640+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
26182641 ctx := testContext()
26192642 expectedEnvHash := hashEnvironment([]string{})
26202643
@@ -2676,7 +2699,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) {
26762699 eventBus := mocks.NewMockEventPublisher(t)
26772700
26782701 config := testMinDelayConfig()
2679- svc := NewService(runtime, envLoader, eventBus, nil, config)
2702+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
26802703 ctx := testContext()
26812704 expectedEnvHash := hashEnvironment([]string{})
26822705
@@ -2733,7 +2756,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) {
27332756 eventBus := mocks.NewMockEventPublisher(t)
27342757
27352758 config := testMinDelayConfig()
2736- svc := NewService(runtime, envLoader, eventBus, nil, config)
2759+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
27372760 ctx := testContext()
27382761 expectedEnvHash := hashEnvironment([]string{})
27392762
@@ -2787,7 +2810,7 @@ func TestService_attachmentEnvDrifted_ReturnsFalseWhenHashMatches(t *testing.T)
27872810 envLoader := mocks.NewMockEnvLoader(t)
27882811 eventBus := mocks.NewMockEventPublisher(t)
27892812
2790- svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig())
2813+ svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil)
27912814 ctx := testContext()
27922815 imageRef := svc.buildImageRef("postgres:16")
27932816 envVars := []string{"POSTGRES_PASSWORD=secret"}
@@ -2809,7 +2832,7 @@ func TestService_attachmentEnvDrifted_ReturnsTrueWhenHashDiffers(t *testing.T) {
28092832 envLoader := mocks.NewMockEnvLoader(t)
28102833 eventBus := mocks.NewMockEventPublisher(t)
28112834
2812- svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig())
2835+ svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil)
28132836 ctx := testContext()
28142837 imageRef := svc.buildImageRef("postgres:16")
28152838 envVars := []string{"POSTGRES_PASSWORD=secret"}
@@ -2830,7 +2853,7 @@ func TestService_attachmentEnvDrifted_ReturnsTrueWhenHashMissing(t *testing.T) {
28302853 envLoader := mocks.NewMockEnvLoader(t)
28312854 eventBus := mocks.NewMockEventPublisher(t)
28322855
2833- svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig())
2856+ svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil)
28342857 ctx := testContext()
28352858 imageRef := svc.buildImageRef("postgres:16")
28362859 envVars := []string{"POSTGRES_PASSWORD=secret"}
@@ -2858,7 +2881,7 @@ func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) {
28582881 eventBus := mocks.NewMockEventPublisher(t)
28592882
28602883 config := testMinDelayConfig()
2861- svc := NewService(runtime, envLoader, eventBus, nil, config)
2884+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
28622885 ctx := testContext()
28632886
28642887 // The canonical container is running and has the managed label — it will be
@@ -2959,7 +2982,7 @@ func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) {
29592982 DrainDelayConfigured: true,
29602983 StabilizationDelay: time.Millisecond,
29612984 }
2962- svc := NewService(runtime, envLoader, eventBus, nil, config)
2985+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
29632986 svc.SetProxyCacheInvalidator(cacheInvalidator)
29642987 ctx := testContext()
29652988
@@ -3052,7 +3075,7 @@ func TestService_Deploy_StabilizationSuccess(t *testing.T) {
30523075 DrainDelayConfigured: true,
30533076 StabilizationDelay: time.Millisecond,
30543077 }
3055- svc := NewService(runtime, envLoader, eventBus, nil, config)
3078+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
30563079 svc.SetProxyCacheInvalidator(cacheInvalidator)
30573080 ctx := testContext()
30583081
@@ -3138,7 +3161,7 @@ func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T)
31383161 DrainDelayConfigured: true,
31393162 StabilizationDelay: time.Millisecond,
31403163 }
3141- svc := NewService(runtime, envLoader, eventBus, nil, config)
3164+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
31423165 svc.SetProxyCacheInvalidator(cacheInvalidator)
31433166 ctx := testContext()
31443167
@@ -3231,7 +3254,7 @@ func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T)
32313254 DrainDelayConfigured: true,
32323255 StabilizationDelay: time.Millisecond,
32333256 }
3234- svc := NewService(runtime, envLoader, eventBus, nil, config)
3257+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
32353258 ctx := testContext()
32363259
32373260 // Pre-populate with existing tracked container
@@ -3312,7 +3335,7 @@ func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) {
33123335 DrainDelayConfigured: true,
33133336 StabilizationDelay: time.Millisecond,
33143337 }
3315- svc := NewService(runtime, envLoader, eventBus, nil, config)
3338+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
33163339 ctx := testContext()
33173340
33183341 // NO existing container — this is a first deploy
@@ -3389,7 +3412,7 @@ func TestService_Deploy_PropagatesImageLabelsToContainerConfig(t *testing.T) {
33893412 DrainDelayConfigured: true,
33903413 }
33913414
3392- svc := NewService(runtime, envLoader, eventBus, nil, config)
3415+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
33933416 ctx := testContext()
33943417
33953418 route := domain.Route{
@@ -21,6 +21,15 @@ import (
21 "github.com/bnema/gordon/internal/domain"21 "github.com/bnema/gordon/internal/domain"
22 )22 )
23 23
24+// fakeConfigProvider implements AttachmentConfigProvider for tests.
25+type fakeConfigProvider struct {
26+ attachments map[string][]string
27+ networkGroups map[string][]string
28+}
29+
30+func (f *fakeConfigProvider) GetAttachments() map[string][]string { return f.attachments }
31+func (f *fakeConfigProvider) GetNetworkGroups() map[string][]string { return f.networkGroups }
32+
24 func testContext() context.Context {33 func testContext() context.Context {
25 return zerowrap.WithCtx(context.Background(), zerowrap.Default())34 return zerowrap.WithCtx(context.Background(), zerowrap.Default())
26 }35 }
@@ -80,12 +89,26 @@ func managedMetricState(t *testing.T, reader *sdkmetric.ManualReader) (int64, in
80 return 0, 0, nil89 return 0, 0, nil
81 }90 }
82 91
92+func TestService_NewService_WithConfigProvider(t *testing.T) {
93+ runtime := mocks.NewMockContainerRuntime(t)
94+ envLoader := mocks.NewMockEnvLoader(t)
95+ eventBus := mocks.NewMockEventPublisher(t)
96+
97+ provider := &fakeConfigProvider{
98+ attachments: map[string][]string{"test.com": {"postgres:16"}},
99+ networkGroups: map[string][]string{"group1": {"test.com"}},
100+ }
101+
102+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, provider)
103+ assert.NotNil(t, svc)
104+}
105+
83 func TestService_ManagedContainersMetric_GlobalSeriesOnDeployReplaceAndRemove(t *testing.T) {106 func TestService_ManagedContainersMetric_GlobalSeriesOnDeployReplaceAndRemove(t *testing.T) {
84 runtime := mocks.NewMockContainerRuntime(t)107 runtime := mocks.NewMockContainerRuntime(t)
85 envLoader := mocks.NewMockEnvLoader(t)108 envLoader := mocks.NewMockEnvLoader(t)
86 eventBus := mocks.NewMockEventPublisher(t)109 eventBus := mocks.NewMockEventPublisher(t)
87 110
88- svc := NewService(runtime, envLoader, eventBus, nil, Config{})111+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
89 ctx := testContext()112 ctx := testContext()
90 metrics, reader := setupMetricsTest(t)113 metrics, reader := setupMetricsTest(t)
91 svc.SetMetrics(metrics)114 svc.SetMetrics(metrics)
@@ -119,7 +142,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T)
119 envLoader := mocks.NewMockEnvLoader(t)142 envLoader := mocks.NewMockEnvLoader(t)
120 eventBus := mocks.NewMockEventPublisher(t)143 eventBus := mocks.NewMockEventPublisher(t)
121 144
122- svc := NewService(runtime, envLoader, eventBus, nil, Config{})145+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
123 ctx := testContext()146 ctx := testContext()
124 metrics, reader := setupMetricsTest(t)147 metrics, reader := setupMetricsTest(t)
125 svc.SetMetrics(metrics)148 svc.SetMetrics(metrics)
@@ -202,7 +225,7 @@ func TestService_Deploy_Success(t *testing.T) {
202 DrainDelayConfigured: true,225 DrainDelayConfigured: true,
203 }226 }
204 227
205- svc := NewService(runtime, envLoader, eventBus, nil, config)228+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
206 ctx := testContext()229 ctx := testContext()
207 230
208 route := domain.Route{231 route := domain.Route{
@@ -284,7 +307,7 @@ func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T
284 DrainDelayConfigured: true,307 DrainDelayConfigured: true,
285 }308 }
286 309
287- svc := NewService(runtime, envLoader, eventBus, nil, config)310+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
288 ctx := testContext()311 ctx := testContext()
289 312
290 route := domain.Route{313 route := domain.Route{
@@ -342,7 +365,7 @@ func TestService_Deploy_ImagePullFailure(t *testing.T) {
342 eventBus := mocks.NewMockEventPublisher(t)365 eventBus := mocks.NewMockEventPublisher(t)
343 366
344 config := Config{}367 config := Config{}
345- svc := NewService(runtime, envLoader, eventBus, nil, config)368+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
346 ctx := testContext()369 ctx := testContext()
347 370
348 route := domain.Route{371 route := domain.Route{
@@ -373,7 +396,7 @@ func TestService_PullImage_UsesServiceTokenForExternalPull(t *testing.T) {
373 ServiceToken: "service-token",396 ServiceToken: "service-token",
374 }397 }
375 398
376- svc := NewService(runtime, nil, nil, nil, config)399+ svc := NewService(runtime, nil, nil, nil, config, nil)
377 ctx := testContext()400 ctx := testContext()
378 401
379 runtime.EXPECT().PullImageWithAuth(mock.Anything, "registry.example.com/myapp:latest", "gordon-service", "service-token").Return(nil)402 runtime.EXPECT().PullImageWithAuth(mock.Anything, "registry.example.com/myapp:latest", "gordon-service", "service-token").Return(nil)
@@ -390,7 +413,7 @@ func TestService_PullImage_RequiresServiceTokenForExternalPull(t *testing.T) {
390 RegistryAuthEnabled: true,413 RegistryAuthEnabled: true,
391 }414 }
392 415
393- svc := NewService(runtime, nil, nil, nil, config)416+ svc := NewService(runtime, nil, nil, nil, config, nil)
394 ctx := testContext()417 ctx := testContext()
395 418
396 err := svc.pullImage(ctx, "registry.example.com/myapp:latest", false)419 err := svc.pullImage(ctx, "registry.example.com/myapp:latest", false)
@@ -408,7 +431,7 @@ func TestService_PullImage_InternalRetriesOnConnectionRefused(t *testing.T) {
408 InternalRegistryPassword: "secret",431 InternalRegistryPassword: "secret",
409 }432 }
410 433
411- svc := NewService(runtime, nil, nil, nil, config)434+ svc := NewService(runtime, nil, nil, nil, config, nil)
412 ctx := testContext()435 ctx := testContext()
413 436
414 runtime.EXPECT().PullImageWithAuth(mock.Anything, "localhost:5000/myapp:latest", "internal", "secret").437 runtime.EXPECT().PullImageWithAuth(mock.Anything, "localhost:5000/myapp:latest", "internal", "secret").
@@ -435,7 +458,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) {
435 DrainDelayConfigured: true,458 DrainDelayConfigured: true,
436 StabilizationDelay: time.Millisecond,459 StabilizationDelay: time.Millisecond,
437 }460 }
438- svc := NewService(runtime, envLoader, eventBus, nil, config)461+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
439 svc.SetProxyCacheInvalidator(cacheInvalidator)462 svc.SetProxyCacheInvalidator(cacheInvalidator)
440 ctx := testContext()463 ctx := testContext()
441 464
@@ -511,7 +534,7 @@ func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T
511 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)534 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
512 535
513 config := testMinDelayConfig()536 config := testMinDelayConfig()
514- svc := NewService(runtime, envLoader, eventBus, nil, config)537+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
515 svc.SetProxyCacheInvalidator(cacheInvalidator)538 svc.SetProxyCacheInvalidator(cacheInvalidator)
516 ctx := testContext()539 ctx := testContext()
517 540
@@ -608,7 +631,7 @@ func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) {
608 eventBus := mocks.NewMockEventPublisher(t)631 eventBus := mocks.NewMockEventPublisher(t)
609 632
610 config := testMinDelayConfig()633 config := testMinDelayConfig()
611- svc := NewService(runtime, envLoader, eventBus, nil, config)634+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
612 ctx := testContext()635 ctx := testContext()
613 expectedEnvHash := hashEnvironment([]string{})636 expectedEnvHash := hashEnvironment([]string{})
614 637
@@ -675,7 +698,7 @@ func TestService_Deploy_WithNetworkIsolation(t *testing.T) {
675 DrainDelay: time.Millisecond,698 DrainDelay: time.Millisecond,
676 DrainDelayConfigured: true,699 DrainDelayConfigured: true,
677 }700 }
678- svc := NewService(runtime, envLoader, eventBus, nil, config)701+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
679 ctx := testContext()702 ctx := testContext()
680 703
681 route := domain.Route{704 route := domain.Route{
@@ -732,7 +755,7 @@ func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) {
732 DrainDelay: time.Millisecond,755 DrainDelay: time.Millisecond,
733 DrainDelayConfigured: true,756 DrainDelayConfigured: true,
734 }757 }
735- svc := NewService(runtime, envLoader, eventBus, nil, config)758+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
736 ctx := testContext()759 ctx := testContext()
737 760
738 route := domain.Route{761 route := domain.Route{
@@ -784,7 +807,7 @@ func TestService_Stop(t *testing.T) {
784 envLoader := mocks.NewMockEnvLoader(t)807 envLoader := mocks.NewMockEnvLoader(t)
785 eventBus := mocks.NewMockEventPublisher(t)808 eventBus := mocks.NewMockEventPublisher(t)
786 809
787- svc := NewService(runtime, envLoader, eventBus, nil, Config{})810+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
788 ctx := testContext()811 ctx := testContext()
789 812
790 runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(nil)813 runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(nil)
@@ -799,7 +822,7 @@ func TestService_Stop_Error(t *testing.T) {
799 envLoader := mocks.NewMockEnvLoader(t)822 envLoader := mocks.NewMockEnvLoader(t)
800 eventBus := mocks.NewMockEventPublisher(t)823 eventBus := mocks.NewMockEventPublisher(t)
801 824
802- svc := NewService(runtime, envLoader, eventBus, nil, Config{})825+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
803 ctx := testContext()826 ctx := testContext()
804 827
805 runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(errors.New("container not found"))828 runtime.EXPECT().StopContainer(mock.Anything, "container-123").Return(errors.New("container not found"))
@@ -815,7 +838,7 @@ func TestService_Remove(t *testing.T) {
815 envLoader := mocks.NewMockEnvLoader(t)838 envLoader := mocks.NewMockEnvLoader(t)
816 eventBus := mocks.NewMockEventPublisher(t)839 eventBus := mocks.NewMockEventPublisher(t)
817 840
818- svc := NewService(runtime, envLoader, eventBus, nil, Config{})841+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
819 ctx := testContext()842 ctx := testContext()
820 843
821 // Add tracked container844 // Add tracked container
@@ -844,7 +867,7 @@ func TestService_Remove_WithNetworkCleanup(t *testing.T) {
844 NetworkIsolation: true,867 NetworkIsolation: true,
845 NetworkPrefix: "gordon",868 NetworkPrefix: "gordon",
846 }869 }
847- svc := NewService(runtime, envLoader, eventBus, nil, config)870+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
848 ctx := testContext()871 ctx := testContext()
849 872
850 svc.containers["test.example.com"] = &domain.Container{873 svc.containers["test.example.com"] = &domain.Container{
@@ -868,7 +891,7 @@ func TestService_Get(t *testing.T) {
868 envLoader := mocks.NewMockEnvLoader(t)891 envLoader := mocks.NewMockEnvLoader(t)
869 eventBus := mocks.NewMockEventPublisher(t)892 eventBus := mocks.NewMockEventPublisher(t)
870 893
871- svc := NewService(runtime, envLoader, eventBus, nil, Config{})894+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
872 ctx := testContext()895 ctx := testContext()
873 896
874 container := &domain.Container{ID: "container-123"}897 container := &domain.Container{ID: "container-123"}
@@ -885,7 +908,7 @@ func TestService_Get_NotFound(t *testing.T) {
885 envLoader := mocks.NewMockEnvLoader(t)908 envLoader := mocks.NewMockEnvLoader(t)
886 eventBus := mocks.NewMockEventPublisher(t)909 eventBus := mocks.NewMockEventPublisher(t)
887 910
888- svc := NewService(runtime, envLoader, eventBus, nil, Config{})911+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
889 ctx := testContext()912 ctx := testContext()
890 913
891 result, exists := svc.Get(ctx, "nonexistent.example.com")914 result, exists := svc.Get(ctx, "nonexistent.example.com")
@@ -899,7 +922,7 @@ func TestService_List(t *testing.T) {
899 envLoader := mocks.NewMockEnvLoader(t)922 envLoader := mocks.NewMockEnvLoader(t)
900 eventBus := mocks.NewMockEventPublisher(t)923 eventBus := mocks.NewMockEventPublisher(t)
901 924
902- svc := NewService(runtime, envLoader, eventBus, nil, Config{})925+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
903 ctx := testContext()926 ctx := testContext()
904 927
905 svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"}928 svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"}
@@ -917,7 +940,7 @@ func TestService_HealthCheck(t *testing.T) {
917 envLoader := mocks.NewMockEnvLoader(t)940 envLoader := mocks.NewMockEnvLoader(t)
918 eventBus := mocks.NewMockEventPublisher(t)941 eventBus := mocks.NewMockEventPublisher(t)
919 942
920- svc := NewService(runtime, envLoader, eventBus, nil, Config{})943+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
921 ctx := testContext()944 ctx := testContext()
922 945
923 svc.containers["healthy.example.com"] = &domain.Container{ID: "healthy-container"}946 svc.containers["healthy.example.com"] = &domain.Container{ID: "healthy-container"}
@@ -937,7 +960,7 @@ func TestService_SyncContainers(t *testing.T) {
937 envLoader := mocks.NewMockEnvLoader(t)960 envLoader := mocks.NewMockEnvLoader(t)
938 eventBus := mocks.NewMockEventPublisher(t)961 eventBus := mocks.NewMockEventPublisher(t)
939 962
940- svc := NewService(runtime, envLoader, eventBus, nil, Config{})963+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
941 ctx := testContext()964 ctx := testContext()
942 965
943 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{966 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
@@ -977,7 +1000,7 @@ func TestService_Shutdown(t *testing.T) {
977 envLoader := mocks.NewMockEnvLoader(t)1000 envLoader := mocks.NewMockEnvLoader(t)
978 eventBus := mocks.NewMockEventPublisher(t)1001 eventBus := mocks.NewMockEventPublisher(t)
979 1002
980- svc := NewService(runtime, envLoader, eventBus, nil, Config{})1003+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
981 ctx := testContext()1004 ctx := testContext()
982 1005
983 svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"}1006 svc.containers["app1.example.com"] = &domain.Container{ID: "container-1"}
@@ -997,7 +1020,7 @@ func TestService_Restart_NotFound(t *testing.T) {
997 envLoader := mocks.NewMockEnvLoader(t)1020 envLoader := mocks.NewMockEnvLoader(t)
998 eventBus := mocks.NewMockEventPublisher(t)1021 eventBus := mocks.NewMockEventPublisher(t)
999 1022
1000- svc := NewService(runtime, envLoader, eventBus, nil, Config{})1023+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
1001 ctx := testContext()1024 ctx := testContext()
1002 1025
1003 err := svc.Restart(ctx, "nonexistent.example.com", false)1026 err := svc.Restart(ctx, "nonexistent.example.com", false)
@@ -1011,7 +1034,7 @@ func TestService_Restart_Success(t *testing.T) {
1011 envLoader := mocks.NewMockEnvLoader(t)1034 envLoader := mocks.NewMockEnvLoader(t)
1012 eventBus := mocks.NewMockEventPublisher(t)1035 eventBus := mocks.NewMockEventPublisher(t)
1013 1036
1014- svc := NewService(runtime, envLoader, eventBus, nil, Config{})1037+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
1015 ctx := testContext()1038 ctx := testContext()
1016 1039
1017 svc.containers["test.example.com"] = &domain.Container{1040 svc.containers["test.example.com"] = &domain.Container{
@@ -1032,7 +1055,7 @@ func TestService_Restart_Error(t *testing.T) {
1032 envLoader := mocks.NewMockEnvLoader(t)1055 envLoader := mocks.NewMockEnvLoader(t)
1033 eventBus := mocks.NewMockEventPublisher(t)1056 eventBus := mocks.NewMockEventPublisher(t)
1034 1057
1035- svc := NewService(runtime, envLoader, eventBus, nil, Config{})1058+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
1036 ctx := testContext()1059 ctx := testContext()
1037 1060
1038 svc.containers["test.example.com"] = &domain.Container{1061 svc.containers["test.example.com"] = &domain.Container{
@@ -1054,7 +1077,7 @@ func TestService_Restart_ReconcilesStaleContainerID(t *testing.T) {
1054 envLoader := mocks.NewMockEnvLoader(t)1077 envLoader := mocks.NewMockEnvLoader(t)
1055 eventBus := mocks.NewMockEventPublisher(t)1078 eventBus := mocks.NewMockEventPublisher(t)
1056 1079
1057- svc := NewService(runtime, envLoader, eventBus, nil, Config{})1080+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
1058 ctx := testContext()1081 ctx := testContext()
1059 1082
1060 svc.containers["test.example.com"] = &domain.Container{1083 svc.containers["test.example.com"] = &domain.Container{
@@ -1088,7 +1111,7 @@ func TestService_Restart_ReconcilesStaleContainerID_NotFoundAfterSync(t *testing
1088 envLoader := mocks.NewMockEnvLoader(t)1111 envLoader := mocks.NewMockEnvLoader(t)
1089 eventBus := mocks.NewMockEventPublisher(t)1112 eventBus := mocks.NewMockEventPublisher(t)
1090 1113
1091- svc := NewService(runtime, envLoader, eventBus, nil, Config{})1114+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
1092 ctx := testContext()1115 ctx := testContext()
1093 1116
1094 svc.containers["test.example.com"] = &domain.Container{1117 svc.containers["test.example.com"] = &domain.Container{
@@ -1113,7 +1136,7 @@ func TestService_Restart_WithAttachments(t *testing.T) {
1113 envLoader := mocks.NewMockEnvLoader(t)1136 envLoader := mocks.NewMockEnvLoader(t)
1114 eventBus := mocks.NewMockEventPublisher(t)1137 eventBus := mocks.NewMockEventPublisher(t)
1115 1138
1116- svc := NewService(runtime, envLoader, eventBus, nil, Config{})1139+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
1117 ctx := testContext()1140 ctx := testContext()
1118 1141
1119 svc.containers["test.example.com"] = &domain.Container{1142 svc.containers["test.example.com"] = &domain.Container{
@@ -1452,7 +1475,7 @@ func TestService_Deploy_InternalDeployForcesPull(t *testing.T) {
1452 DrainDelayConfigured: true,1475 DrainDelayConfigured: true,
1453 }1476 }
1454 1477
1455- svc := NewService(runtime, envLoader, eventBus, nil, config)1478+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
1456 1479
1457 // Use internal deploy context (simulating image push event)1480 // Use internal deploy context (simulating image push event)
1458 ctx := domain.WithInternalDeploy(testContext())1481 ctx := domain.WithInternalDeploy(testContext())
@@ -1525,7 +1548,7 @@ func TestService_AutoStart_StartsNewContainers(t *testing.T) {
1525 DrainDelay: time.Millisecond,1548 DrainDelay: time.Millisecond,
1526 DrainDelayConfigured: true,1549 DrainDelayConfigured: true,
1527 }1550 }
1528- svc := NewService(runtime, envLoader, eventBus, nil, config)1551+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
1529 ctx := testContext()1552 ctx := testContext()
1530 1553
1531 routes := []domain.Route{1554 routes := []domain.Route{
@@ -1573,7 +1596,7 @@ func TestService_AutoStart_SkipsExistingContainers(t *testing.T) {
1573 DrainDelay: time.Millisecond,1596 DrainDelay: time.Millisecond,
1574 DrainDelayConfigured: true,1597 DrainDelayConfigured: true,
1575 }1598 }
1576- svc := NewService(runtime, envLoader, eventBus, nil, config)1599+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
1577 ctx := testContext()1600 ctx := testContext()
1578 1601
1579 // Pre-populate with existing container1602 // Pre-populate with existing container
@@ -1617,7 +1640,7 @@ func TestService_AutoStart_HandlesDeployErrors(t *testing.T) {
1617 eventBus := mocks.NewMockEventPublisher(t)1640 eventBus := mocks.NewMockEventPublisher(t)
1618 1641
1619 config := Config{}1642 config := Config{}
1620- svc := NewService(runtime, envLoader, eventBus, nil, config)1643+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
1621 ctx := testContext()1644 ctx := testContext()
1622 1645
1623 routes := []domain.Route{1646 routes := []domain.Route{
@@ -1651,7 +1674,7 @@ func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) {
1651 DrainDelay: time.Millisecond,1674 DrainDelay: time.Millisecond,
1652 DrainDelayConfigured: true,1675 DrainDelayConfigured: true,
1653 }1676 }
1654- svc := NewService(runtime, envLoader, eventBus, nil, config)1677+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
1655 1678
1656 // Mark context as internal deploy — this is what syncAndAutoStart should do1679 // Mark context as internal deploy — this is what syncAndAutoStart should do
1657 ctx := domain.WithInternalDeploy(testContext())1680 ctx := domain.WithInternalDeploy(testContext())
@@ -1703,7 +1726,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) {
1703 DrainDelayConfigured: true,1726 DrainDelayConfigured: true,
1704 StabilizationDelay: time.Millisecond,1727 StabilizationDelay: time.Millisecond,
1705 }1728 }
1706- svc := NewService(runtime, envLoader, eventBus, nil, config)1729+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
1707 svc.SetProxyCacheInvalidator(cacheInvalidator)1730 svc.SetProxyCacheInvalidator(cacheInvalidator)
1708 ctx := testContext()1731 ctx := testContext()
1709 1732
@@ -1795,7 +1818,7 @@ func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) {
1795 DrainDelay: time.Millisecond,1818 DrainDelay: time.Millisecond,
1796 DrainDelayConfigured: true,1819 DrainDelayConfigured: true,
1797 }1820 }
1798- svc := NewService(runtime, envLoader, eventBus, nil, config)1821+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
1799 ctx := testContext()1822 ctx := testContext()
1800 1823
1801 // NO tracked container - service is empty for this domain1824 // NO tracked container - service is empty for this domain
@@ -1871,7 +1894,7 @@ func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) {
1871 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)1894 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
1872 1895
1873 config := testMinDelayConfig()1896 config := testMinDelayConfig()
1874- svc := NewService(runtime, envLoader, eventBus, nil, config)1897+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
1875 svc.SetProxyCacheInvalidator(cacheInvalidator)1898 svc.SetProxyCacheInvalidator(cacheInvalidator)
1876 ctx := testContext()1899 ctx := testContext()
1877 1900
@@ -1942,7 +1965,7 @@ func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T)
1942 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)1965 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
1943 1966
1944 config := testMinDelayConfig()1967 config := testMinDelayConfig()
1945- svc := NewService(runtime, envLoader, eventBus, nil, config)1968+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
1946 svc.SetProxyCacheInvalidator(cacheInvalidator)1969 svc.SetProxyCacheInvalidator(cacheInvalidator)
1947 ctx := testContext()1970 ctx := testContext()
1948 1971
@@ -2053,7 +2076,7 @@ func TestService_StripRegistryPrefix(t *testing.T) {
2053 config := Config{2076 config := Config{
2054 RegistryDomain: tt.registryDomain,2077 RegistryDomain: tt.registryDomain,
2055 }2078 }
2056- svc := NewService(runtime, envLoader, eventBus, nil, config)2079+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2057 2080
2058 result := svc.stripRegistryPrefix(tt.image)2081 result := svc.stripRegistryPrefix(tt.image)
2059 assert.Equal(t, tt.expected, result)2082 assert.Equal(t, tt.expected, result)
@@ -2130,7 +2153,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) {
2130 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)2153 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
2131 2154
2132 config := testMinDelayConfig()2155 config := testMinDelayConfig()
2133- svc := NewService(runtime, envLoader, eventBus, nil, config)2156+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2134 svc.SetProxyCacheInvalidator(cacheInvalidator)2157 svc.SetProxyCacheInvalidator(cacheInvalidator)
2135 ctx := testContext()2158 ctx := testContext()
2136 2159
@@ -2242,7 +2265,7 @@ func TestService_Deploy_ContextCancellation(t *testing.T) {
2242 DrainDelay: time.Millisecond,2265 DrainDelay: time.Millisecond,
2243 DrainDelayConfigured: true,2266 DrainDelayConfigured: true,
2244 }2267 }
2245- svc := NewService(runtime, envLoader, eventBus, nil, config)2268+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2246 2269
2247 route := domain.Route{2270 route := domain.Route{
2248 Domain: "test.example.com",2271 Domain: "test.example.com",
@@ -2268,7 +2291,7 @@ func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) {
2268 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)2291 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
2269 2292
2270 config := testMinDelayConfig()2293 config := testMinDelayConfig()
2271- svc := NewService(runtime, envLoader, eventBus, nil, config)2294+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2272 svc.SetProxyCacheInvalidator(cacheInvalidator)2295 svc.SetProxyCacheInvalidator(cacheInvalidator)
2273 ctx := testContext()2296 ctx := testContext()
2274 2297
@@ -2347,7 +2370,7 @@ func TestService_Deploy_NilCacheInvalidator(t *testing.T) {
2347 2370
2348 config := testMinDelayConfig()2371 config := testMinDelayConfig()
2349 // Intentionally NOT setting cache invalidator2372 // Intentionally NOT setting cache invalidator
2350- svc := NewService(runtime, envLoader, eventBus, nil, config)2373+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2351 ctx := testContext()2374 ctx := testContext()
2352 2375
2353 // Pre-populate with existing container2376 // Pre-populate with existing container
@@ -2398,7 +2421,7 @@ func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) {
2398 DrainDelay: time.Millisecond,2421 DrainDelay: time.Millisecond,
2399 DrainDelayConfigured: true,2422 DrainDelayConfigured: true,
2400 }2423 }
2401- svc := NewService(runtime, envLoader, eventBus, nil, config)2424+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2402 ctx := testContext()2425 ctx := testContext()
2403 expectedEnvHash := hashEnvironment([]string{})2426 expectedEnvHash := hashEnvironment([]string{})
2404 2427
@@ -2457,7 +2480,7 @@ func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testin
2457 DrainDelay: time.Millisecond,2480 DrainDelay: time.Millisecond,
2458 DrainDelayConfigured: true,2481 DrainDelayConfigured: true,
2459 }2482 }
2460- svc := NewService(runtime, envLoader, eventBus, nil, config)2483+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2461 ctx := testContext()2484 ctx := testContext()
2462 expectedEnvHash := hashEnvironment([]string{})2485 expectedEnvHash := hashEnvironment([]string{})
2463 2486
@@ -2514,7 +2537,7 @@ func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) {
2514 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)2537 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
2515 2538
2516 config := testMinDelayConfig()2539 config := testMinDelayConfig()
2517- svc := NewService(runtime, envLoader, eventBus, nil, config)2540+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2518 svc.SetProxyCacheInvalidator(cacheInvalidator)2541 svc.SetProxyCacheInvalidator(cacheInvalidator)
2519 ctx := testContext()2542 ctx := testContext()
2520 2543
@@ -2570,7 +2593,7 @@ func TestSyncContainersRebuildsAttachmentMap(t *testing.T) {
2570 envLoader := mocks.NewMockEnvLoader(t)2593 envLoader := mocks.NewMockEnvLoader(t)
2571 eventBus := mocks.NewMockEventPublisher(t)2594 eventBus := mocks.NewMockEventPublisher(t)
2572 2595
2573- svc := NewService(runtime, envLoader, eventBus, nil, Config{})2596+ svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
2574 ctx := testContext()2597 ctx := testContext()
2575 2598
2576 // ListContainers returns one main container and one attachment container2599 // ListContainers returns one main container and one attachment container
@@ -2614,7 +2637,7 @@ func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) {
2614 eventBus := mocks.NewMockEventPublisher(t)2637 eventBus := mocks.NewMockEventPublisher(t)
2615 2638
2616 config := testMinDelayConfig()2639 config := testMinDelayConfig()
2617- svc := NewService(runtime, envLoader, eventBus, nil, config)2640+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2618 ctx := testContext()2641 ctx := testContext()
2619 expectedEnvHash := hashEnvironment([]string{})2642 expectedEnvHash := hashEnvironment([]string{})
2620 2643
@@ -2676,7 +2699,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) {
2676 eventBus := mocks.NewMockEventPublisher(t)2699 eventBus := mocks.NewMockEventPublisher(t)
2677 2700
2678 config := testMinDelayConfig()2701 config := testMinDelayConfig()
2679- svc := NewService(runtime, envLoader, eventBus, nil, config)2702+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2680 ctx := testContext()2703 ctx := testContext()
2681 expectedEnvHash := hashEnvironment([]string{})2704 expectedEnvHash := hashEnvironment([]string{})
2682 2705
@@ -2733,7 +2756,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) {
2733 eventBus := mocks.NewMockEventPublisher(t)2756 eventBus := mocks.NewMockEventPublisher(t)
2734 2757
2735 config := testMinDelayConfig()2758 config := testMinDelayConfig()
2736- svc := NewService(runtime, envLoader, eventBus, nil, config)2759+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2737 ctx := testContext()2760 ctx := testContext()
2738 expectedEnvHash := hashEnvironment([]string{})2761 expectedEnvHash := hashEnvironment([]string{})
2739 2762
@@ -2787,7 +2810,7 @@ func TestService_attachmentEnvDrifted_ReturnsFalseWhenHashMatches(t *testing.T)
2787 envLoader := mocks.NewMockEnvLoader(t)2810 envLoader := mocks.NewMockEnvLoader(t)
2788 eventBus := mocks.NewMockEventPublisher(t)2811 eventBus := mocks.NewMockEventPublisher(t)
2789 2812
2790- svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig())2813+ svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil)
2791 ctx := testContext()2814 ctx := testContext()
2792 imageRef := svc.buildImageRef("postgres:16")2815 imageRef := svc.buildImageRef("postgres:16")
2793 envVars := []string{"POSTGRES_PASSWORD=secret"}2816 envVars := []string{"POSTGRES_PASSWORD=secret"}
@@ -2809,7 +2832,7 @@ func TestService_attachmentEnvDrifted_ReturnsTrueWhenHashDiffers(t *testing.T) {
2809 envLoader := mocks.NewMockEnvLoader(t)2832 envLoader := mocks.NewMockEnvLoader(t)
2810 eventBus := mocks.NewMockEventPublisher(t)2833 eventBus := mocks.NewMockEventPublisher(t)
2811 2834
2812- svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig())2835+ svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil)
2813 ctx := testContext()2836 ctx := testContext()
2814 imageRef := svc.buildImageRef("postgres:16")2837 imageRef := svc.buildImageRef("postgres:16")
2815 envVars := []string{"POSTGRES_PASSWORD=secret"}2838 envVars := []string{"POSTGRES_PASSWORD=secret"}
@@ -2830,7 +2853,7 @@ func TestService_attachmentEnvDrifted_ReturnsTrueWhenHashMissing(t *testing.T) {
2830 envLoader := mocks.NewMockEnvLoader(t)2853 envLoader := mocks.NewMockEnvLoader(t)
2831 eventBus := mocks.NewMockEventPublisher(t)2854 eventBus := mocks.NewMockEventPublisher(t)
2832 2855
2833- svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig())2856+ svc := NewService(runtime, envLoader, eventBus, nil, testMinDelayConfig(), nil)
2834 ctx := testContext()2857 ctx := testContext()
2835 imageRef := svc.buildImageRef("postgres:16")2858 imageRef := svc.buildImageRef("postgres:16")
2836 envVars := []string{"POSTGRES_PASSWORD=secret"}2859 envVars := []string{"POSTGRES_PASSWORD=secret"}
@@ -2858,7 +2881,7 @@ func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) {
2858 eventBus := mocks.NewMockEventPublisher(t)2881 eventBus := mocks.NewMockEventPublisher(t)
2859 2882
2860 config := testMinDelayConfig()2883 config := testMinDelayConfig()
2861- svc := NewService(runtime, envLoader, eventBus, nil, config)2884+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2862 ctx := testContext()2885 ctx := testContext()
2863 2886
2864 // The canonical container is running and has the managed label — it will be2887 // The canonical container is running and has the managed label — it will be
@@ -2959,7 +2982,7 @@ func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) {
2959 DrainDelayConfigured: true,2982 DrainDelayConfigured: true,
2960 StabilizationDelay: time.Millisecond,2983 StabilizationDelay: time.Millisecond,
2961 }2984 }
2962- svc := NewService(runtime, envLoader, eventBus, nil, config)2985+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
2963 svc.SetProxyCacheInvalidator(cacheInvalidator)2986 svc.SetProxyCacheInvalidator(cacheInvalidator)
2964 ctx := testContext()2987 ctx := testContext()
2965 2988
@@ -3052,7 +3075,7 @@ func TestService_Deploy_StabilizationSuccess(t *testing.T) {
3052 DrainDelayConfigured: true,3075 DrainDelayConfigured: true,
3053 StabilizationDelay: time.Millisecond,3076 StabilizationDelay: time.Millisecond,
3054 }3077 }
3055- svc := NewService(runtime, envLoader, eventBus, nil, config)3078+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
3056 svc.SetProxyCacheInvalidator(cacheInvalidator)3079 svc.SetProxyCacheInvalidator(cacheInvalidator)
3057 ctx := testContext()3080 ctx := testContext()
3058 3081
@@ -3138,7 +3161,7 @@ func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T)
3138 DrainDelayConfigured: true,3161 DrainDelayConfigured: true,
3139 StabilizationDelay: time.Millisecond,3162 StabilizationDelay: time.Millisecond,
3140 }3163 }
3141- svc := NewService(runtime, envLoader, eventBus, nil, config)3164+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
3142 svc.SetProxyCacheInvalidator(cacheInvalidator)3165 svc.SetProxyCacheInvalidator(cacheInvalidator)
3143 ctx := testContext()3166 ctx := testContext()
3144 3167
@@ -3231,7 +3254,7 @@ func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T)
3231 DrainDelayConfigured: true,3254 DrainDelayConfigured: true,
3232 StabilizationDelay: time.Millisecond,3255 StabilizationDelay: time.Millisecond,
3233 }3256 }
3234- svc := NewService(runtime, envLoader, eventBus, nil, config)3257+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
3235 ctx := testContext()3258 ctx := testContext()
3236 3259
3237 // Pre-populate with existing tracked container3260 // Pre-populate with existing tracked container
@@ -3312,7 +3335,7 @@ func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) {
3312 DrainDelayConfigured: true,3335 DrainDelayConfigured: true,
3313 StabilizationDelay: time.Millisecond,3336 StabilizationDelay: time.Millisecond,
3314 }3337 }
3315- svc := NewService(runtime, envLoader, eventBus, nil, config)3338+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
3316 ctx := testContext()3339 ctx := testContext()
3317 3340
3318 // NO existing container — this is a first deploy3341 // NO existing container — this is a first deploy
@@ -3389,7 +3412,7 @@ func TestService_Deploy_PropagatesImageLabelsToContainerConfig(t *testing.T) {
3389 DrainDelayConfigured: true,3412 DrainDelayConfigured: true,
3390 }3413 }
3391 3414
3392- svc := NewService(runtime, envLoader, eventBus, nil, config)3415+ svc := NewService(runtime, envLoader, eventBus, nil, config, nil)
3393 ctx := testContext()3416 ctx := testContext()
3394 3417
3395 route := domain.Route{3418 route := domain.Route{

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close