detached 2b371d2

Refresh auto-route domains and dedupe attachments

commit
2b371d2c265c680b6928fc162b75fd70a6ed3540
author
brice · · 2026-05-10 13:59 UTC
parent
637b278
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +175 -12
M internal/usecase/config/service.go +18 -4
@@ -886,9 +886,15 @@ func canonicalAttachmentsForSave(attachments map[string][]string, registryDomain
886886 continue
887887 }
888888
889- canonicalImages := make([]string, len(images))
890- for i, image := range images {
891- canonicalImages[i] = domain.CanonicalizeGordonImageRef(image, registryDomain, legacyRegistryDomains)
889+ canonicalImages := make([]string, 0, len(images))
890+ seen := make(map[string]struct{}, len(images))
891+ for _, image := range images {
892+ canonicalImage := domain.CanonicalizeGordonImageRef(image, registryDomain, legacyRegistryDomains)
893+ if _, ok := seen[canonicalImage]; ok {
894+ continue
895+ }
896+ seen[canonicalImage] = struct{}{}
897+ canonicalImages = append(canonicalImages, canonicalImage)
892898 }
893899 result[target] = canonicalImages
894900 }
@@ -1143,6 +1149,13 @@ func (s *Service) GetRegistryDomain() string {
11431149 return s.config.RegistryDomain
11441150 }
11451151
1152+// GetLegacyRegistryDomains returns the configured legacy registry domains.
1153+func (s *Service) GetLegacyRegistryDomains() []string {
1154+ s.mu.RLock()
1155+ defer s.mu.RUnlock()
1156+ return append([]string{}, s.config.LegacyRegistryDomains...)
1157+}
1158+
11461159 // GetDataDir returns the configured data directory.
11471160 func (s *Service) GetDataDir() string {
11481161 s.mu.RLock()
@@ -1271,8 +1284,9 @@ func (s *Service) AddAttachment(ctx context.Context, domainOrGroup, image string
12711284
12721285 // Check if already exists
12731286 existing := s.config.Attachments[domainOrGroup]
1287+ canonicalImage := domain.CanonicalizeGordonImageRef(image, s.config.RegistryDomain, s.config.LegacyRegistryDomains)
12741288 for _, img := range existing {
1275- if img == image {
1289+ if img == image || domain.CanonicalizeGordonImageRef(img, s.config.RegistryDomain, s.config.LegacyRegistryDomains) == canonicalImage {
12761290 s.mu.Unlock()
12771291 return domain.ErrAttachmentExists
12781292 }
@@ -886,9 +886,15 @@ func canonicalAttachmentsForSave(attachments map[string][]string, registryDomain
886 continue886 continue
887 }887 }
888 888
889- canonicalImages := make([]string, len(images))889+ canonicalImages := make([]string, 0, len(images))
890- for i, image := range images {890+ seen := make(map[string]struct{}, len(images))
891- canonicalImages[i] = domain.CanonicalizeGordonImageRef(image, registryDomain, legacyRegistryDomains)891+ for _, image := range images {
892+ canonicalImage := domain.CanonicalizeGordonImageRef(image, registryDomain, legacyRegistryDomains)
893+ if _, ok := seen[canonicalImage]; ok {
894+ continue
895+ }
896+ seen[canonicalImage] = struct{}{}
897+ canonicalImages = append(canonicalImages, canonicalImage)
892 }898 }
893 result[target] = canonicalImages899 result[target] = canonicalImages
894 }900 }
@@ -1143,6 +1149,13 @@ func (s *Service) GetRegistryDomain() string {
1143 return s.config.RegistryDomain1149 return s.config.RegistryDomain
1144 }1150 }
1145 1151
1152+// GetLegacyRegistryDomains returns the configured legacy registry domains.
1153+func (s *Service) GetLegacyRegistryDomains() []string {
1154+ s.mu.RLock()
1155+ defer s.mu.RUnlock()
1156+ return append([]string{}, s.config.LegacyRegistryDomains...)
1157+}
1158+
1146 // GetDataDir returns the configured data directory.1159 // GetDataDir returns the configured data directory.
1147 func (s *Service) GetDataDir() string {1160 func (s *Service) GetDataDir() string {
1148 s.mu.RLock()1161 s.mu.RLock()
@@ -1271,8 +1284,9 @@ func (s *Service) AddAttachment(ctx context.Context, domainOrGroup, image string
1271 1284
1272 // Check if already exists1285 // Check if already exists
1273 existing := s.config.Attachments[domainOrGroup]1286 existing := s.config.Attachments[domainOrGroup]
1287+ canonicalImage := domain.CanonicalizeGordonImageRef(image, s.config.RegistryDomain, s.config.LegacyRegistryDomains)
1274 for _, img := range existing {1288 for _, img := range existing {
1275- if img == image {1289+ if img == image || domain.CanonicalizeGordonImageRef(img, s.config.RegistryDomain, s.config.LegacyRegistryDomains) == canonicalImage {
1276 s.mu.Unlock()1290 s.mu.Unlock()
1277 return domain.ErrAttachmentExists1291 return domain.ErrAttachmentExists
1278 }1292 }
M internal/usecase/config/service_test.go +86 -0
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "os"
77 "path/filepath"
8+ "strings"
89 "testing"
910
1011 "github.com/bnema/zerowrap"
@@ -339,6 +340,31 @@ legacy_registry_domains = ["old-registry.example.com", "old-registry.example.com
339340 assert.Contains(t, text, `"external.example.com" = { image = "docker.io/library/nginx:latest", https = true }`)
340341 }
341342
343+func TestCanonicalAttachmentsForSave(t *testing.T) {
344+ attachments := map[string][]string{
345+ "app.example.com": {
346+ "old-registry.example.com/postgres:18",
347+ "registry.example.com/postgres:18",
348+ "rabbitmq:3",
349+ "docker.io/library/nginx:latest",
350+ },
351+ }
352+
353+ saved := canonicalAttachmentsForSave(attachments, "registry.example.com", []string{"old-registry.example.com"})
354+
355+ assert.Equal(t, []string{
356+ "registry.example.com/postgres:18",
357+ "rabbitmq:3",
358+ "docker.io/library/nginx:latest",
359+ }, saved["app.example.com"])
360+ assert.Equal(t, []string{
361+ "old-registry.example.com/postgres:18",
362+ "registry.example.com/postgres:18",
363+ "rabbitmq:3",
364+ "docker.io/library/nginx:latest",
365+ }, attachments["app.example.com"])
366+}
367+
342368 func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {
343369 tmpDir := t.TempDir()
344370 configFile := filepath.Join(tmpDir, "gordon.toml")
@@ -349,6 +375,7 @@ func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {
349375 [attachments]
350376 "app.example.com" = [
351377 "old-registry.example.com/postgres:18",
378+ "registry.example.com/postgres:18",
352379 "registry.example.com/redis:7",
353380 "rabbitmq:3",
354381 "docker.io/library/nginx:latest",
@@ -368,6 +395,7 @@ func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {
368395 require.NoError(t, svc.Load(ctx))
369396 require.Equal(t, []string{
370397 "old-registry.example.com/postgres:18",
398+ "registry.example.com/postgres:18",
371399 "registry.example.com/redis:7",
372400 "rabbitmq:3",
373401 "docker.io/library/nginx:latest",
@@ -376,6 +404,7 @@ func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {
376404 require.NoError(t, svc.Save(ctx))
377405 require.Equal(t, []string{
378406 "old-registry.example.com/postgres:18",
407+ "registry.example.com/postgres:18",
379408 "registry.example.com/redis:7",
380409 "rabbitmq:3",
381410 "docker.io/library/nginx:latest",
@@ -398,6 +427,7 @@ func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {
398427 require.NoError(t, err)
399428 text := string(content)
400429 assert.Contains(t, text, `registry.example.com/postgres:18`)
430+ assert.Equal(t, 1, strings.Count(text, `registry.example.com/postgres:18`))
401431 assert.Contains(t, text, `registry.example.com/redis:7`)
402432 assert.Contains(t, text, `rabbitmq:3`)
403433 assert.Contains(t, text, `docker.io/library/nginx:latest`)
@@ -1554,6 +1584,62 @@ func TestService_AddAttachment(t *testing.T) {
15541584 assert.NotContains(t, text, `old-registry.example.com/postgres:18`)
15551585 })
15561586
1587+ t.Run("rejects canonical duplicate when existing legacy ref matches current ref", func(t *testing.T) {
1588+ tmpDir := t.TempDir()
1589+ configFile := filepath.Join(tmpDir, "gordon.toml")
1590+ initialConfig := `[server]
1591+ gordon_domain = "registry.example.com"
1592+ legacy_registry_domains = ["old-registry.example.com"]
1593+
1594+ [attachments]
1595+ "app.example.com" = ["old-registry.example.com/postgres:18"]
1596+ `
1597+ err := os.WriteFile(configFile, []byte(initialConfig), 0600)
1598+ require.NoError(t, err)
1599+
1600+ v := viper.New()
1601+ v.SetConfigFile(configFile)
1602+ require.NoError(t, v.ReadInConfig())
1603+
1604+ eventBus := mocks.NewMockEventPublisher(t)
1605+ svc := NewService(v, eventBus)
1606+ ctx := testContext()
1607+ require.NoError(t, svc.Load(ctx))
1608+
1609+ err = svc.AddAttachment(ctx, "app.example.com", "registry.example.com/postgres:18")
1610+
1611+ assert.ErrorIs(t, err, domain.ErrAttachmentExists)
1612+ assert.Equal(t, []string{"old-registry.example.com/postgres:18"}, svc.GetConfig().Attachments["app.example.com"])
1613+ })
1614+
1615+ t.Run("rejects canonical duplicate when existing current ref matches legacy ref", func(t *testing.T) {
1616+ tmpDir := t.TempDir()
1617+ configFile := filepath.Join(tmpDir, "gordon.toml")
1618+ initialConfig := `[server]
1619+ gordon_domain = "registry.example.com"
1620+ legacy_registry_domains = ["old-registry.example.com"]
1621+
1622+ [attachments]
1623+ "app.example.com" = ["registry.example.com/postgres:18"]
1624+ `
1625+ err := os.WriteFile(configFile, []byte(initialConfig), 0600)
1626+ require.NoError(t, err)
1627+
1628+ v := viper.New()
1629+ v.SetConfigFile(configFile)
1630+ require.NoError(t, v.ReadInConfig())
1631+
1632+ eventBus := mocks.NewMockEventPublisher(t)
1633+ svc := NewService(v, eventBus)
1634+ ctx := testContext()
1635+ require.NoError(t, svc.Load(ctx))
1636+
1637+ err = svc.AddAttachment(ctx, "app.example.com", "old-registry.example.com/postgres:18")
1638+
1639+ assert.ErrorIs(t, err, domain.ErrAttachmentExists)
1640+ assert.Equal(t, []string{"registry.example.com/postgres:18"}, svc.GetConfig().Attachments["app.example.com"])
1641+ })
1642+
15571643 t.Run("duplicate attachment", func(t *testing.T) {
15581644 v := viper.New()
15591645 v.Set("attachments", map[string]interface{}{
@@ -5,6 +5,7 @@ import (
5 "fmt"5 "fmt"
6 "os"6 "os"
7 "path/filepath"7 "path/filepath"
8+ "strings"
8 "testing"9 "testing"
9 10
10 "github.com/bnema/zerowrap"11 "github.com/bnema/zerowrap"
@@ -339,6 +340,31 @@ legacy_registry_domains = ["old-registry.example.com", "old-registry.example.com
339 assert.Contains(t, text, `"external.example.com" = { image = "docker.io/library/nginx:latest", https = true }`)340 assert.Contains(t, text, `"external.example.com" = { image = "docker.io/library/nginx:latest", https = true }`)
340 }341 }
341 342
343+func TestCanonicalAttachmentsForSave(t *testing.T) {
344+ attachments := map[string][]string{
345+ "app.example.com": {
346+ "old-registry.example.com/postgres:18",
347+ "registry.example.com/postgres:18",
348+ "rabbitmq:3",
349+ "docker.io/library/nginx:latest",
350+ },
351+ }
352+
353+ saved := canonicalAttachmentsForSave(attachments, "registry.example.com", []string{"old-registry.example.com"})
354+
355+ assert.Equal(t, []string{
356+ "registry.example.com/postgres:18",
357+ "rabbitmq:3",
358+ "docker.io/library/nginx:latest",
359+ }, saved["app.example.com"])
360+ assert.Equal(t, []string{
361+ "old-registry.example.com/postgres:18",
362+ "registry.example.com/postgres:18",
363+ "rabbitmq:3",
364+ "docker.io/library/nginx:latest",
365+ }, attachments["app.example.com"])
366+}
367+
342 func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {368 func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {
343 tmpDir := t.TempDir()369 tmpDir := t.TempDir()
344 configFile := filepath.Join(tmpDir, "gordon.toml")370 configFile := filepath.Join(tmpDir, "gordon.toml")
@@ -349,6 +375,7 @@ func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {
349 [attachments]375 [attachments]
350 "app.example.com" = [376 "app.example.com" = [
351 "old-registry.example.com/postgres:18",377 "old-registry.example.com/postgres:18",
378+ "registry.example.com/postgres:18",
352 "registry.example.com/redis:7",379 "registry.example.com/redis:7",
353 "rabbitmq:3",380 "rabbitmq:3",
354 "docker.io/library/nginx:latest",381 "docker.io/library/nginx:latest",
@@ -368,6 +395,7 @@ func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {
368 require.NoError(t, svc.Load(ctx))395 require.NoError(t, svc.Load(ctx))
369 require.Equal(t, []string{396 require.Equal(t, []string{
370 "old-registry.example.com/postgres:18",397 "old-registry.example.com/postgres:18",
398+ "registry.example.com/postgres:18",
371 "registry.example.com/redis:7",399 "registry.example.com/redis:7",
372 "rabbitmq:3",400 "rabbitmq:3",
373 "docker.io/library/nginx:latest",401 "docker.io/library/nginx:latest",
@@ -376,6 +404,7 @@ func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {
376 require.NoError(t, svc.Save(ctx))404 require.NoError(t, svc.Save(ctx))
377 require.Equal(t, []string{405 require.Equal(t, []string{
378 "old-registry.example.com/postgres:18",406 "old-registry.example.com/postgres:18",
407+ "registry.example.com/postgres:18",
379 "registry.example.com/redis:7",408 "registry.example.com/redis:7",
380 "rabbitmq:3",409 "rabbitmq:3",
381 "docker.io/library/nginx:latest",410 "docker.io/library/nginx:latest",
@@ -398,6 +427,7 @@ func TestService_SaveCanonicalizesAttachmentImages(t *testing.T) {
398 require.NoError(t, err)427 require.NoError(t, err)
399 text := string(content)428 text := string(content)
400 assert.Contains(t, text, `registry.example.com/postgres:18`)429 assert.Contains(t, text, `registry.example.com/postgres:18`)
430+ assert.Equal(t, 1, strings.Count(text, `registry.example.com/postgres:18`))
401 assert.Contains(t, text, `registry.example.com/redis:7`)431 assert.Contains(t, text, `registry.example.com/redis:7`)
402 assert.Contains(t, text, `rabbitmq:3`)432 assert.Contains(t, text, `rabbitmq:3`)
403 assert.Contains(t, text, `docker.io/library/nginx:latest`)433 assert.Contains(t, text, `docker.io/library/nginx:latest`)
@@ -1554,6 +1584,62 @@ func TestService_AddAttachment(t *testing.T) {
1554 assert.NotContains(t, text, `old-registry.example.com/postgres:18`)1584 assert.NotContains(t, text, `old-registry.example.com/postgres:18`)
1555 })1585 })
1556 1586
1587+ t.Run("rejects canonical duplicate when existing legacy ref matches current ref", func(t *testing.T) {
1588+ tmpDir := t.TempDir()
1589+ configFile := filepath.Join(tmpDir, "gordon.toml")
1590+ initialConfig := `[server]
1591+ gordon_domain = "registry.example.com"
1592+ legacy_registry_domains = ["old-registry.example.com"]
1593+
1594+ [attachments]
1595+ "app.example.com" = ["old-registry.example.com/postgres:18"]
1596+ `
1597+ err := os.WriteFile(configFile, []byte(initialConfig), 0600)
1598+ require.NoError(t, err)
1599+
1600+ v := viper.New()
1601+ v.SetConfigFile(configFile)
1602+ require.NoError(t, v.ReadInConfig())
1603+
1604+ eventBus := mocks.NewMockEventPublisher(t)
1605+ svc := NewService(v, eventBus)
1606+ ctx := testContext()
1607+ require.NoError(t, svc.Load(ctx))
1608+
1609+ err = svc.AddAttachment(ctx, "app.example.com", "registry.example.com/postgres:18")
1610+
1611+ assert.ErrorIs(t, err, domain.ErrAttachmentExists)
1612+ assert.Equal(t, []string{"old-registry.example.com/postgres:18"}, svc.GetConfig().Attachments["app.example.com"])
1613+ })
1614+
1615+ t.Run("rejects canonical duplicate when existing current ref matches legacy ref", func(t *testing.T) {
1616+ tmpDir := t.TempDir()
1617+ configFile := filepath.Join(tmpDir, "gordon.toml")
1618+ initialConfig := `[server]
1619+ gordon_domain = "registry.example.com"
1620+ legacy_registry_domains = ["old-registry.example.com"]
1621+
1622+ [attachments]
1623+ "app.example.com" = ["registry.example.com/postgres:18"]
1624+ `
1625+ err := os.WriteFile(configFile, []byte(initialConfig), 0600)
1626+ require.NoError(t, err)
1627+
1628+ v := viper.New()
1629+ v.SetConfigFile(configFile)
1630+ require.NoError(t, v.ReadInConfig())
1631+
1632+ eventBus := mocks.NewMockEventPublisher(t)
1633+ svc := NewService(v, eventBus)
1634+ ctx := testContext()
1635+ require.NoError(t, svc.Load(ctx))
1636+
1637+ err = svc.AddAttachment(ctx, "app.example.com", "old-registry.example.com/postgres:18")
1638+
1639+ assert.ErrorIs(t, err, domain.ErrAttachmentExists)
1640+ assert.Equal(t, []string{"registry.example.com/postgres:18"}, svc.GetConfig().Attachments["app.example.com"])
1641+ })
1642+
1557 t.Run("duplicate attachment", func(t *testing.T) {1643 t.Run("duplicate attachment", func(t *testing.T) {
1558 v := viper.New()1644 v := viper.New()
1559 v.Set("attachments", map[string]interface{}{1645 v.Set("attachments", map[string]interface{}{
M internal/usecase/container/autoroute.go +19 -4
@@ -22,6 +22,11 @@ type EnvFileExtractor interface {
2222 ExtractEnvFileFromImage(ctx context.Context, imageRef, envFilePath string) ([]byte, error)
2323 }
2424
25+type registryDomainsProvider interface {
26+ GetRegistryDomain() string
27+ GetLegacyRegistryDomains() []string
28+}
29+
2530 // AutoRouteHandler handles image.pushed events for auto-route from labels.
2631 type AutoRouteHandler struct {
2732 configSvc in.ConfigService
@@ -133,15 +138,24 @@ func (h *AutoRouteHandler) buildImageName(name, reference string) string {
133138 return fmt.Sprintf("%s:%s", name, reference)
134139 }
135140
141+func (h *AutoRouteHandler) registryDomains() (string, []string) {
142+ provider, ok := h.configSvc.(registryDomainsProvider)
143+ if !ok {
144+ return h.registryDomain, append([]string(nil), h.legacyRegistryDomains...)
145+ }
146+ return provider.GetRegistryDomain(), provider.GetLegacyRegistryDomains()
147+}
148+
136149 // buildFullImageRef constructs a fully qualified image reference with registry domain.
137150 // This is needed for Docker operations since images are stored with the registry prefix.
138151 func (h *AutoRouteHandler) buildFullImageRef(imageName string) string {
139- if h.registryDomain == "" {
152+ registryDomain, _ := h.registryDomains()
153+ if registryDomain == "" {
140154 return imageName
141155 }
142156
143157 // Don't prefix if already has registry domain
144- if strings.HasPrefix(imageName, h.registryDomain+"/") {
158+ if strings.HasPrefix(imageName, registryDomain+"/") {
145159 return imageName
146160 }
147161
@@ -154,7 +168,7 @@ func (h *AutoRouteHandler) buildFullImageRef(imageName string) string {
154168 return imageName
155169 }
156170
157- return fmt.Sprintf("%s/%s", h.registryDomain, imageName)
171+ return fmt.Sprintf("%s/%s", registryDomain, imageName)
158172 }
159173
160174 // processRoutes processes each domain and creates/updates routes.
@@ -211,11 +225,12 @@ func (h *AutoRouteHandler) createOrUpdateRoute(ctx context.Context, routeDomain,
211225 HTTPS: true,
212226 }
213227
228+ registryDomain, legacyRegistryDomains := h.registryDomains()
214229 existingRoutes := h.configSvc.GetRoutes(ctx)
215230 for _, existing := range existingRoutes {
216231 if existing.Domain == routeDomain {
217232 route.HTTPS = existing.HTTPS
218- if auto.ExtractRepoName(existing.Image, h.registryDomain, h.legacyRegistryDomains...) != auto.ExtractRepoName(imageName, h.registryDomain, h.legacyRegistryDomains...) {
233+ if auto.ExtractRepoName(existing.Image, registryDomain, legacyRegistryDomains...) != auto.ExtractRepoName(imageName, registryDomain, legacyRegistryDomains...) {
219234 log.Warn().Str("domain", routeDomain).Str("existing_image", existing.Image).Str("image", imageName).Msg("auto-route update rejected due to repository ownership mismatch")
220235 return false
221236 }
@@ -22,6 +22,11 @@ type EnvFileExtractor interface {
22 ExtractEnvFileFromImage(ctx context.Context, imageRef, envFilePath string) ([]byte, error)22 ExtractEnvFileFromImage(ctx context.Context, imageRef, envFilePath string) ([]byte, error)
23 }23 }
24 24
25+type registryDomainsProvider interface {
26+ GetRegistryDomain() string
27+ GetLegacyRegistryDomains() []string
28+}
29+
25 // AutoRouteHandler handles image.pushed events for auto-route from labels.30 // AutoRouteHandler handles image.pushed events for auto-route from labels.
26 type AutoRouteHandler struct {31 type AutoRouteHandler struct {
27 configSvc in.ConfigService32 configSvc in.ConfigService
@@ -133,15 +138,24 @@ func (h *AutoRouteHandler) buildImageName(name, reference string) string {
133 return fmt.Sprintf("%s:%s", name, reference)138 return fmt.Sprintf("%s:%s", name, reference)
134 }139 }
135 140
141+func (h *AutoRouteHandler) registryDomains() (string, []string) {
142+ provider, ok := h.configSvc.(registryDomainsProvider)
143+ if !ok {
144+ return h.registryDomain, append([]string(nil), h.legacyRegistryDomains...)
145+ }
146+ return provider.GetRegistryDomain(), provider.GetLegacyRegistryDomains()
147+}
148+
136 // buildFullImageRef constructs a fully qualified image reference with registry domain.149 // buildFullImageRef constructs a fully qualified image reference with registry domain.
137 // This is needed for Docker operations since images are stored with the registry prefix.150 // This is needed for Docker operations since images are stored with the registry prefix.
138 func (h *AutoRouteHandler) buildFullImageRef(imageName string) string {151 func (h *AutoRouteHandler) buildFullImageRef(imageName string) string {
139- if h.registryDomain == "" {152+ registryDomain, _ := h.registryDomains()
153+ if registryDomain == "" {
140 return imageName154 return imageName
141 }155 }
142 156
143 // Don't prefix if already has registry domain157 // Don't prefix if already has registry domain
144- if strings.HasPrefix(imageName, h.registryDomain+"/") {158+ if strings.HasPrefix(imageName, registryDomain+"/") {
145 return imageName159 return imageName
146 }160 }
147 161
@@ -154,7 +168,7 @@ func (h *AutoRouteHandler) buildFullImageRef(imageName string) string {
154 return imageName168 return imageName
155 }169 }
156 170
157- return fmt.Sprintf("%s/%s", h.registryDomain, imageName)171+ return fmt.Sprintf("%s/%s", registryDomain, imageName)
158 }172 }
159 173
160 // processRoutes processes each domain and creates/updates routes.174 // processRoutes processes each domain and creates/updates routes.
@@ -211,11 +225,12 @@ func (h *AutoRouteHandler) createOrUpdateRoute(ctx context.Context, routeDomain,
211 HTTPS: true,225 HTTPS: true,
212 }226 }
213 227
228+ registryDomain, legacyRegistryDomains := h.registryDomains()
214 existingRoutes := h.configSvc.GetRoutes(ctx)229 existingRoutes := h.configSvc.GetRoutes(ctx)
215 for _, existing := range existingRoutes {230 for _, existing := range existingRoutes {
216 if existing.Domain == routeDomain {231 if existing.Domain == routeDomain {
217 route.HTTPS = existing.HTTPS232 route.HTTPS = existing.HTTPS
218- if auto.ExtractRepoName(existing.Image, h.registryDomain, h.legacyRegistryDomains...) != auto.ExtractRepoName(imageName, h.registryDomain, h.legacyRegistryDomains...) {233+ if auto.ExtractRepoName(existing.Image, registryDomain, legacyRegistryDomains...) != auto.ExtractRepoName(imageName, registryDomain, legacyRegistryDomains...) {
219 log.Warn().Str("domain", routeDomain).Str("existing_image", existing.Image).Str("image", imageName).Msg("auto-route update rejected due to repository ownership mismatch")234 log.Warn().Str("domain", routeDomain).Str("existing_image", existing.Image).Str("image", imageName).Msg("auto-route update rejected due to repository ownership mismatch")
220 return false235 return false
221 }236 }
M internal/usecase/container/autoroute_test.go +46 -0
@@ -10,6 +10,7 @@ import (
1010 "testing"
1111
1212 "github.com/bnema/zerowrap"
13+ "github.com/spf13/viper"
1314 "github.com/stretchr/testify/assert"
1415 "github.com/stretchr/testify/mock"
1516 "github.com/stretchr/testify/require"
@@ -18,6 +19,7 @@ import (
1819 "github.com/bnema/gordon/internal/boundaries/out/mocks"
1920 "github.com/bnema/gordon/internal/domain"
2021 "github.com/bnema/gordon/internal/usecase/auto"
22+ configusecase "github.com/bnema/gordon/internal/usecase/config"
2123 )
2224
2325 // domainToEnvFileName tests
@@ -856,6 +858,50 @@ func TestAutoRouteHandler_ExistingDomain_SameRepoAcrossLegacyAndCurrentRegistryH
856858 assert.False(t, created)
857859 }
858860
861+func TestAutoRouteHandler_UsesRefreshedRegistryDomainsAfterReload(t *testing.T) {
862+ ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
863+ tmpDir := t.TempDir()
864+ configFile := filepath.Join(tmpDir, "gordon.toml")
865+ require.NoError(t, os.WriteFile(configFile, []byte(`[server]
866+ gordon_domain = "old-registry.example.com"
867+
868+ [routes]
869+ "app.example.com" = "old-registry.example.com/app:v1"
870+ `), 0600))
871+
872+ v := viper.New()
873+ v.SetConfigFile(configFile)
874+ require.NoError(t, v.ReadInConfig())
875+
876+ configSvc := configusecase.NewService(v, mocks.NewMockEventPublisher(t))
877+ require.NoError(t, configSvc.Load(ctx))
878+
879+ containerSvc := inmocks.NewMockContainerService(t)
880+ handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, mocks.NewMockBlobStorage(t), "old-registry.example.com")
881+
882+ require.NoError(t, os.WriteFile(configFile, []byte(`[server]
883+ gordon_domain = "new-registry.example.com"
884+ legacy_registry_domains = ["old-registry.example.com"]
885+
886+ [routes]
887+ "app.example.com" = "old-registry.example.com/app:v1"
888+ `), 0600))
889+ require.NoError(t, configSvc.Reload(ctx))
890+
891+ containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
892+ Domain: "app.example.com",
893+ Image: "new-registry.example.com/app:v2",
894+ HTTPS: true,
895+ }).Return(&domain.Container{ID: "c1"}, nil)
896+
897+ created := handler.createOrUpdateRoute(context.Background(), "app.example.com", "new-registry.example.com/app:v2", []string{"*.example.com"})
898+ assert.False(t, created)
899+
900+ route, err := configSvc.GetRoute(ctx, "app.example.com")
901+ require.NoError(t, err)
902+ assert.Equal(t, "new-registry.example.com/app:v2", route.Image)
903+}
904+
859905 func TestAutoRouteHandler_ExistingDomain_DifferentRepo(t *testing.T) {
860906 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
861907 configSvc := inmocks.NewMockConfigService(t)
@@ -10,6 +10,7 @@ import (
10 "testing"10 "testing"
11 11
12 "github.com/bnema/zerowrap"12 "github.com/bnema/zerowrap"
13+ "github.com/spf13/viper"
13 "github.com/stretchr/testify/assert"14 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/mock"15 "github.com/stretchr/testify/mock"
15 "github.com/stretchr/testify/require"16 "github.com/stretchr/testify/require"
@@ -18,6 +19,7 @@ import (
18 "github.com/bnema/gordon/internal/boundaries/out/mocks"19 "github.com/bnema/gordon/internal/boundaries/out/mocks"
19 "github.com/bnema/gordon/internal/domain"20 "github.com/bnema/gordon/internal/domain"
20 "github.com/bnema/gordon/internal/usecase/auto"21 "github.com/bnema/gordon/internal/usecase/auto"
22+ configusecase "github.com/bnema/gordon/internal/usecase/config"
21 )23 )
22 24
23 // domainToEnvFileName tests25 // domainToEnvFileName tests
@@ -856,6 +858,50 @@ func TestAutoRouteHandler_ExistingDomain_SameRepoAcrossLegacyAndCurrentRegistryH
856 assert.False(t, created)858 assert.False(t, created)
857 }859 }
858 860
861+func TestAutoRouteHandler_UsesRefreshedRegistryDomainsAfterReload(t *testing.T) {
862+ ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
863+ tmpDir := t.TempDir()
864+ configFile := filepath.Join(tmpDir, "gordon.toml")
865+ require.NoError(t, os.WriteFile(configFile, []byte(`[server]
866+ gordon_domain = "old-registry.example.com"
867+
868+ [routes]
869+ "app.example.com" = "old-registry.example.com/app:v1"
870+ `), 0600))
871+
872+ v := viper.New()
873+ v.SetConfigFile(configFile)
874+ require.NoError(t, v.ReadInConfig())
875+
876+ configSvc := configusecase.NewService(v, mocks.NewMockEventPublisher(t))
877+ require.NoError(t, configSvc.Load(ctx))
878+
879+ containerSvc := inmocks.NewMockContainerService(t)
880+ handler := NewAutoRouteHandler(ctx, configSvc, containerSvc, mocks.NewMockBlobStorage(t), "old-registry.example.com")
881+
882+ require.NoError(t, os.WriteFile(configFile, []byte(`[server]
883+ gordon_domain = "new-registry.example.com"
884+ legacy_registry_domains = ["old-registry.example.com"]
885+
886+ [routes]
887+ "app.example.com" = "old-registry.example.com/app:v1"
888+ `), 0600))
889+ require.NoError(t, configSvc.Reload(ctx))
890+
891+ containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
892+ Domain: "app.example.com",
893+ Image: "new-registry.example.com/app:v2",
894+ HTTPS: true,
895+ }).Return(&domain.Container{ID: "c1"}, nil)
896+
897+ created := handler.createOrUpdateRoute(context.Background(), "app.example.com", "new-registry.example.com/app:v2", []string{"*.example.com"})
898+ assert.False(t, created)
899+
900+ route, err := configSvc.GetRoute(ctx, "app.example.com")
901+ require.NoError(t, err)
902+ assert.Equal(t, "new-registry.example.com/app:v2", route.Image)
903+}
904+
859 func TestAutoRouteHandler_ExistingDomain_DifferentRepo(t *testing.T) {905 func TestAutoRouteHandler_ExistingDomain_DifferentRepo(t *testing.T) {
860 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())906 ctx := zerowrap.WithCtx(context.Background(), zerowrap.Default())
861 configSvc := inmocks.NewMockConfigService(t)907 configSvc := inmocks.NewMockConfigService(t)
M internal/usecase/container/service.go +6 -4
@@ -1334,8 +1334,10 @@ func (s *Service) ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo
13341334 return results
13351335 }
13361336
1337-// stripRegistryPrefix removes the configured registry domain prefix from an image reference.
1338-// For example, "reg.example.com/myapp:latest" becomes "myapp:latest" if registry domain is "reg.example.com".
1337+// stripRegistryPrefix removes the configured current or legacy Gordon registry
1338+// domain prefix from an image reference. For example,
1339+// "reg.example.com/myapp:latest" becomes "myapp:latest" when
1340+// "reg.example.com" is a current or legacy Gordon registry domain.
13391341 func (s *Service) stripRegistryPrefix(image string) string {
13401342 s.mu.RLock()
13411343 cfg := s.config
@@ -3365,8 +3367,8 @@ func rewriteToLocalRegistry(imageRef, registryDomain string, legacyRegistryDomai
33653367
33663368 localRegistry := fmt.Sprintf("localhost:%d", registryPort)
33673369 localPrefix := localRegistry + "/"
3368- if remainder, ok := strings.CutPrefix(imageRef, localPrefix); ok {
3369- return localPrefix + remainder
3370+ if strings.HasPrefix(imageRef, localPrefix) {
3371+ return imageRef
33703372 }
33713373
33723374 if domain.IsGordonRegistryImageRef(imageRef, registryDomain, legacyRegistryDomains) {
@@ -1334,8 +1334,10 @@ func (s *Service) ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo
1334 return results1334 return results
1335 }1335 }
1336 1336
1337-// stripRegistryPrefix removes the configured registry domain prefix from an image reference.1337+// stripRegistryPrefix removes the configured current or legacy Gordon registry
1338-// For example, "reg.example.com/myapp:latest" becomes "myapp:latest" if registry domain is "reg.example.com".1338+// domain prefix from an image reference. For example,
1339+// "reg.example.com/myapp:latest" becomes "myapp:latest" when
1340+// "reg.example.com" is a current or legacy Gordon registry domain.
1339 func (s *Service) stripRegistryPrefix(image string) string {1341 func (s *Service) stripRegistryPrefix(image string) string {
1340 s.mu.RLock()1342 s.mu.RLock()
1341 cfg := s.config1343 cfg := s.config
@@ -3365,8 +3367,8 @@ func rewriteToLocalRegistry(imageRef, registryDomain string, legacyRegistryDomai
3365 3367
3366 localRegistry := fmt.Sprintf("localhost:%d", registryPort)3368 localRegistry := fmt.Sprintf("localhost:%d", registryPort)
3367 localPrefix := localRegistry + "/"3369 localPrefix := localRegistry + "/"
3368- if remainder, ok := strings.CutPrefix(imageRef, localPrefix); ok {3370+ if strings.HasPrefix(imageRef, localPrefix) {
3369- return localPrefix + remainder3371+ return imageRef
3370 }3372 }
3371 3373
3372 if domain.IsGordonRegistryImageRef(imageRef, registryDomain, legacyRegistryDomains) {3374 if domain.IsGordonRegistryImageRef(imageRef, registryDomain, legacyRegistryDomains) {

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