detached ef44fd8

feat(container): read attachments and network groups from live config provider

commit
ef44fd883f07a2122eb79561a25961c5381321ab
author
brice · · 2026-03-15 16:10 UTC
parent
f009838
signature
unsigned
2 files · +83 -11
M internal/usecase/container/service.go +31 -11
@@ -1776,15 +1776,26 @@ func (s *Service) setupVolumes(ctx context.Context, domainName, imageRef string)
17761776 }
17771777
17781778 func (s *Service) getNetworkForApp(domainName string) string {
1779- s.mu.RLock()
1780- cfg := s.config
1781- s.mu.RUnlock()
1779+ var networkIsolation bool
1780+ var networkGroups map[string][]string
1781+
1782+ if s.configProvider != nil {
1783+ networkGroups = s.configProvider.GetNetworkGroups()
1784+ s.mu.RLock()
1785+ networkIsolation = s.config.NetworkIsolation
1786+ s.mu.RUnlock()
1787+ } else {
1788+ s.mu.RLock()
1789+ networkIsolation = s.config.NetworkIsolation
1790+ networkGroups = s.config.NetworkGroups
1791+ s.mu.RUnlock()
1792+ }
17821793
1783- if !cfg.NetworkIsolation {
1794+ if !networkIsolation {
17841795 return "bridge"
17851796 }
17861797
1787- for groupName, domains := range cfg.NetworkGroups {
1798+ for groupName, domains := range networkGroups {
17881799 if slices.Contains(domains, domainName) {
17891800 return s.generateNetworkName(groupName)
17901801 }
@@ -1952,15 +1963,24 @@ func (s *Service) deployAttachments(ctx context.Context, domainName, networkName
19521963 // 1. Direct domain attachments (attachments[domain])
19531964 // 2. Network group attachments (attachments[group] where domain is in network_groups[group])
19541965 func (s *Service) resolveAttachmentsForDomain(domainName string) []string {
1955- s.mu.RLock()
1956- cfg := s.config
1957- s.mu.RUnlock()
1966+ var attachments map[string][]string
1967+ var networkGroups map[string][]string
1968+
1969+ if s.configProvider != nil {
1970+ attachments = s.configProvider.GetAttachments()
1971+ networkGroups = s.configProvider.GetNetworkGroups()
1972+ } else {
1973+ s.mu.RLock()
1974+ attachments = s.config.Attachments
1975+ networkGroups = s.config.NetworkGroups
1976+ s.mu.RUnlock()
1977+ }
19581978
19591979 seen := make(map[string]bool)
19601980 var result []string
19611981
19621982 // First, add domain-specific attachments
1963- if domainAttachments, ok := cfg.Attachments[domainName]; ok {
1983+ if domainAttachments, ok := attachments[domainName]; ok {
19641984 for _, img := range domainAttachments {
19651985 if !seen[img] {
19661986 seen[img] = true
@@ -1970,9 +1990,9 @@ func (s *Service) resolveAttachmentsForDomain(domainName string) []string {
19701990 }
19711991
19721992 // Then, find which network group this domain belongs to and add group attachments
1973- for groupName, domains := range cfg.NetworkGroups {
1993+ for groupName, domains := range networkGroups {
19741994 if slices.Contains(domains, domainName) {
1975- if groupAttachments, ok := cfg.Attachments[groupName]; ok {
1995+ if groupAttachments, ok := attachments[groupName]; ok {
19761996 for _, img := range groupAttachments {
19771997 if !seen[img] {
19781998 seen[img] = true
@@ -1776,15 +1776,26 @@ func (s *Service) setupVolumes(ctx context.Context, domainName, imageRef string)
1776 }1776 }
1777 1777
1778 func (s *Service) getNetworkForApp(domainName string) string {1778 func (s *Service) getNetworkForApp(domainName string) string {
1779- s.mu.RLock()1779+ var networkIsolation bool
1780- cfg := s.config1780+ var networkGroups map[string][]string
1781- s.mu.RUnlock()1781+
1782+ if s.configProvider != nil {
1783+ networkGroups = s.configProvider.GetNetworkGroups()
1784+ s.mu.RLock()
1785+ networkIsolation = s.config.NetworkIsolation
1786+ s.mu.RUnlock()
1787+ } else {
1788+ s.mu.RLock()
1789+ networkIsolation = s.config.NetworkIsolation
1790+ networkGroups = s.config.NetworkGroups
1791+ s.mu.RUnlock()
1792+ }
1782 1793
1783- if !cfg.NetworkIsolation {1794+ if !networkIsolation {
1784 return "bridge"1795 return "bridge"
1785 }1796 }
1786 1797
1787- for groupName, domains := range cfg.NetworkGroups {1798+ for groupName, domains := range networkGroups {
1788 if slices.Contains(domains, domainName) {1799 if slices.Contains(domains, domainName) {
1789 return s.generateNetworkName(groupName)1800 return s.generateNetworkName(groupName)
1790 }1801 }
@@ -1952,15 +1963,24 @@ func (s *Service) deployAttachments(ctx context.Context, domainName, networkName
1952 // 1. Direct domain attachments (attachments[domain])1963 // 1. Direct domain attachments (attachments[domain])
1953 // 2. Network group attachments (attachments[group] where domain is in network_groups[group])1964 // 2. Network group attachments (attachments[group] where domain is in network_groups[group])
1954 func (s *Service) resolveAttachmentsForDomain(domainName string) []string {1965 func (s *Service) resolveAttachmentsForDomain(domainName string) []string {
1955- s.mu.RLock()1966+ var attachments map[string][]string
1956- cfg := s.config1967+ var networkGroups map[string][]string
1957- s.mu.RUnlock()1968+
1969+ if s.configProvider != nil {
1970+ attachments = s.configProvider.GetAttachments()
1971+ networkGroups = s.configProvider.GetNetworkGroups()
1972+ } else {
1973+ s.mu.RLock()
1974+ attachments = s.config.Attachments
1975+ networkGroups = s.config.NetworkGroups
1976+ s.mu.RUnlock()
1977+ }
1958 1978
1959 seen := make(map[string]bool)1979 seen := make(map[string]bool)
1960 var result []string1980 var result []string
1961 1981
1962 // First, add domain-specific attachments1982 // First, add domain-specific attachments
1963- if domainAttachments, ok := cfg.Attachments[domainName]; ok {1983+ if domainAttachments, ok := attachments[domainName]; ok {
1964 for _, img := range domainAttachments {1984 for _, img := range domainAttachments {
1965 if !seen[img] {1985 if !seen[img] {
1966 seen[img] = true1986 seen[img] = true
@@ -1970,9 +1990,9 @@ func (s *Service) resolveAttachmentsForDomain(domainName string) []string {
1970 }1990 }
1971 1991
1972 // Then, find which network group this domain belongs to and add group attachments1992 // Then, find which network group this domain belongs to and add group attachments
1973- for groupName, domains := range cfg.NetworkGroups {1993+ for groupName, domains := range networkGroups {
1974 if slices.Contains(domains, domainName) {1994 if slices.Contains(domains, domainName) {
1975- if groupAttachments, ok := cfg.Attachments[groupName]; ok {1995+ if groupAttachments, ok := attachments[groupName]; ok {
1976 for _, img := range groupAttachments {1996 for _, img := range groupAttachments {
1977 if !seen[img] {1997 if !seen[img] {
1978 seen[img] = true1998 seen[img] = true
M internal/usecase/container/service_test.go +52 -0
@@ -103,6 +103,58 @@ func TestService_NewService_WithConfigProvider(t *testing.T) {
103103 assert.NotNil(t, svc)
104104 }
105105
106+func TestService_ResolveAttachments_UsesLiveConfigProvider(t *testing.T) {
107+ runtime := mocks.NewMockContainerRuntime(t)
108+ envLoader := mocks.NewMockEnvLoader(t)
109+ eventBus := mocks.NewMockEventPublisher(t)
110+
111+ provider := &fakeConfigProvider{
112+ attachments: map[string][]string{"app.example.com": {"postgres:16"}},
113+ networkGroups: map[string][]string{},
114+ }
115+
116+ // Create service with EMPTY static config but a live provider
117+ svc := NewService(runtime, envLoader, eventBus, nil, Config{
118+ NetworkIsolation: true,
119+ NetworkPrefix: "gordon",
120+ }, provider)
121+
122+ // resolveAttachmentsForDomain should return the provider's data, not the empty snapshot
123+ result := svc.resolveAttachmentsForDomain("app.example.com")
124+ assert.Equal(t, []string{"postgres:16"}, result)
125+
126+ // Simulate adding a new attachment after construction
127+ provider.attachments["app.example.com"] = append(provider.attachments["app.example.com"], "redis:7")
128+ result = svc.resolveAttachmentsForDomain("app.example.com")
129+ assert.Equal(t, []string{"postgres:16", "redis:7"}, result)
130+}
131+
132+func TestService_ResolveAttachments_GroupBasedViaProvider(t *testing.T) {
133+ provider := &fakeConfigProvider{
134+ attachments: map[string][]string{
135+ "shared-group": {"postgres:16"},
136+ },
137+ networkGroups: map[string][]string{
138+ "shared-group": {"app1.example.com", "app2.example.com"},
139+ },
140+ }
141+
142+ svc := NewService(
143+ mocks.NewMockContainerRuntime(t),
144+ mocks.NewMockEnvLoader(t),
145+ mocks.NewMockEventPublisher(t),
146+ nil, Config{NetworkIsolation: true, NetworkPrefix: "gordon"}, provider,
147+ )
148+
149+ // Domain in group should see group attachments
150+ result := svc.resolveAttachmentsForDomain("app1.example.com")
151+ assert.Equal(t, []string{"postgres:16"}, result)
152+
153+ // Domain not in any group should see nothing
154+ result = svc.resolveAttachmentsForDomain("other.example.com")
155+ assert.Empty(t, result)
156+}
157+
106158 func TestService_ManagedContainersMetric_GlobalSeriesOnDeployReplaceAndRemove(t *testing.T) {
107159 runtime := mocks.NewMockContainerRuntime(t)
108160 envLoader := mocks.NewMockEnvLoader(t)
@@ -103,6 +103,58 @@ func TestService_NewService_WithConfigProvider(t *testing.T) {
103 assert.NotNil(t, svc)103 assert.NotNil(t, svc)
104 }104 }
105 105
106+func TestService_ResolveAttachments_UsesLiveConfigProvider(t *testing.T) {
107+ runtime := mocks.NewMockContainerRuntime(t)
108+ envLoader := mocks.NewMockEnvLoader(t)
109+ eventBus := mocks.NewMockEventPublisher(t)
110+
111+ provider := &fakeConfigProvider{
112+ attachments: map[string][]string{"app.example.com": {"postgres:16"}},
113+ networkGroups: map[string][]string{},
114+ }
115+
116+ // Create service with EMPTY static config but a live provider
117+ svc := NewService(runtime, envLoader, eventBus, nil, Config{
118+ NetworkIsolation: true,
119+ NetworkPrefix: "gordon",
120+ }, provider)
121+
122+ // resolveAttachmentsForDomain should return the provider's data, not the empty snapshot
123+ result := svc.resolveAttachmentsForDomain("app.example.com")
124+ assert.Equal(t, []string{"postgres:16"}, result)
125+
126+ // Simulate adding a new attachment after construction
127+ provider.attachments["app.example.com"] = append(provider.attachments["app.example.com"], "redis:7")
128+ result = svc.resolveAttachmentsForDomain("app.example.com")
129+ assert.Equal(t, []string{"postgres:16", "redis:7"}, result)
130+}
131+
132+func TestService_ResolveAttachments_GroupBasedViaProvider(t *testing.T) {
133+ provider := &fakeConfigProvider{
134+ attachments: map[string][]string{
135+ "shared-group": {"postgres:16"},
136+ },
137+ networkGroups: map[string][]string{
138+ "shared-group": {"app1.example.com", "app2.example.com"},
139+ },
140+ }
141+
142+ svc := NewService(
143+ mocks.NewMockContainerRuntime(t),
144+ mocks.NewMockEnvLoader(t),
145+ mocks.NewMockEventPublisher(t),
146+ nil, Config{NetworkIsolation: true, NetworkPrefix: "gordon"}, provider,
147+ )
148+
149+ // Domain in group should see group attachments
150+ result := svc.resolveAttachmentsForDomain("app1.example.com")
151+ assert.Equal(t, []string{"postgres:16"}, result)
152+
153+ // Domain not in any group should see nothing
154+ result = svc.resolveAttachmentsForDomain("other.example.com")
155+ assert.Empty(t, result)
156+}
157+
106 func TestService_ManagedContainersMetric_GlobalSeriesOnDeployReplaceAndRemove(t *testing.T) {158 func TestService_ManagedContainersMetric_GlobalSeriesOnDeployReplaceAndRemove(t *testing.T) {
107 runtime := mocks.NewMockContainerRuntime(t)159 runtime := mocks.NewMockContainerRuntime(t)
108 envLoader := mocks.NewMockEnvLoader(t)160 envLoader := mocks.NewMockEnvLoader(t)

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