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) | |||
| 1776 | 1776 | } | |
| 1777 | 1777 | ||
| 1778 | 1778 | 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 | + | } | |
| 1782 | 1793 | ||
| 1783 | - | if !cfg.NetworkIsolation { | |
| 1794 | + | if !networkIsolation { | |
| 1784 | 1795 | return "bridge" | |
| 1785 | 1796 | } | |
| 1786 | 1797 | ||
| 1787 | - | for groupName, domains := range cfg.NetworkGroups { | |
| 1798 | + | for groupName, domains := range networkGroups { | |
| 1788 | 1799 | if slices.Contains(domains, domainName) { | |
| 1789 | 1800 | return s.generateNetworkName(groupName) | |
| 1790 | 1801 | } | |
| @@ -1952,15 +1963,24 @@ func (s *Service) deployAttachments(ctx context.Context, domainName, networkName | |||
| 1952 | 1963 | // 1. Direct domain attachments (attachments[domain]) | |
| 1953 | 1964 | // 2. Network group attachments (attachments[group] where domain is in network_groups[group]) | |
| 1954 | 1965 | 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 | + | } | |
| 1958 | 1978 | ||
| 1959 | 1979 | seen := make(map[string]bool) | |
| 1960 | 1980 | var result []string | |
| 1961 | 1981 | ||
| 1962 | 1982 | // First, add domain-specific attachments | |
| 1963 | - | if domainAttachments, ok := cfg.Attachments[domainName]; ok { | |
| 1983 | + | if domainAttachments, ok := attachments[domainName]; ok { | |
| 1964 | 1984 | for _, img := range domainAttachments { | |
| 1965 | 1985 | if !seen[img] { | |
| 1966 | 1986 | seen[img] = true | |
| @@ -1970,9 +1990,9 @@ func (s *Service) resolveAttachmentsForDomain(domainName string) []string { | |||
| 1970 | 1990 | } | |
| 1971 | 1991 | ||
| 1972 | 1992 | // 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 | 1994 | if slices.Contains(domains, domainName) { | |
| 1975 | - | if groupAttachments, ok := cfg.Attachments[groupName]; ok { | |
| 1995 | + | if groupAttachments, ok := attachments[groupName]; ok { | |
| 1976 | 1996 | for _, img := range groupAttachments { | |
| 1977 | 1997 | if !seen[img] { | |
| 1978 | 1998 | 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.config | 1780 | + | 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.config | 1967 | + | 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 []string | 1980 | var result []string | ||
| 1961 | 1981 | ||||
| 1962 | // First, add domain-specific attachments | 1982 | // 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] = true | 1986 | 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 attachments | 1992 | // 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] = true | 1998 | seen[img] = true | ||
M internal/usecase/container/service_test.go +52 -0
| @@ -103,6 +103,58 @@ func TestService_NewService_WithConfigProvider(t *testing.T) { | |||
| 103 | 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 | 158 | func TestService_ManagedContainersMetric_GlobalSeriesOnDeployReplaceAndRemove(t *testing.T) { | |
| 107 | 159 | runtime := mocks.NewMockContainerRuntime(t) | |
| 108 | 160 | 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) | ||