fix(runtime): harden reload and cleanup reporting
- commit
- 93fc81411d174d081912a5d12b70f655a0a63303
- author
- brice · · 2026-05-23 10:10 UTC
- parent
- 78bf949
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +42 -16
M docs/config/server.md +1 -0
| @@ -438,6 +438,7 @@ gordon_domain = "gordon.mydomain.com" | |||
| 438 | 438 | ``` | |
| 439 | 439 | ||
| 440 | 440 | With firewall port forwarding: | |
| 441 | + | ||
| 441 | 442 | ```bash | |
| 442 | 443 | sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8088 | |
| 443 | 444 | sudo firewall-cmd --reload | |
| @@ -438,6 +438,7 @@ gordon_domain = "gordon.mydomain.com" | |||||
| 438 | ``` | 438 | ``` | ||
| 439 | 439 | ||||
| 440 | With firewall port forwarding: | 440 | With firewall port forwarding: | ||
| 441 | + | ||||
| 441 | ```bash | 442 | ```bash | ||
| 442 | sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8088 | 443 | sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8088 | ||
| 443 | sudo firewall-cmd --reload | 444 | sudo firewall-cmd --reload | ||
M docs/index.md +0 -1
| @@ -78,7 +78,6 @@ Gordon runs on your VPS and provides: | |||
| 78 | 78 | - Linux VPS (Ubuntu/Debian recommended) | |
| 79 | 79 | - Docker or Podman runtime | |
| 80 | 80 | - Domain pointing to your server | |
| 81 | - | - Domain pointing to your server | |
| 82 | 81 | - Optional Cloudflare account or other reverse proxy for edge TLS | |
| 83 | 82 | ||
| 84 | 83 | > **Note:** Gordon can run behind Cloudflare/nginx, or terminate TLS directly via its internal CA (enabled by default on `server.tls_port`). Set `tls_port = 0` to disable. | |
| @@ -78,7 +78,6 @@ Gordon runs on your VPS and provides: | |||||
| 78 | - Linux VPS (Ubuntu/Debian recommended) | 78 | - Linux VPS (Ubuntu/Debian recommended) | ||
| 79 | - Docker or Podman runtime | 79 | - Docker or Podman runtime | ||
| 80 | - Domain pointing to your server | 80 | - Domain pointing to your server | ||
| 81 | - | - Domain pointing to your server | |||
| 82 | - Optional Cloudflare account or other reverse proxy for edge TLS | 81 | - Optional Cloudflare account or other reverse proxy for edge TLS | ||
| 83 | 82 | ||||
| 84 | > **Note:** Gordon can run behind Cloudflare/nginx, or terminate TLS directly via its internal CA (enabled by default on `server.tls_port`). Set `tls_port = 0` to disable. | 83 | > **Note:** Gordon can run behind Cloudflare/nginx, or terminate TLS directly via its internal CA (enabled by default on `server.tls_port`). Set `tls_port = 0` to disable. | ||
M internal/adapters/in/cli/auth.go +1 -1
| @@ -238,7 +238,7 @@ Examples: | |||
| 238 | 238 | func resolveRequiredRemote(flagToken string) (*remote.ResolvedRemote, error) { | |
| 239 | 239 | resolved, isRemote, err := remote.ResolveStrict(remoteFlag, flagToken, insecureTLSFlag) | |
| 240 | 240 | if err != nil { | |
| 241 | - | return nil, err | |
| 241 | + | return nil, fmt.Errorf("failed to resolve remote %q: %w", remoteFlag, err) | |
| 242 | 242 | } | |
| 243 | 243 | if !isRemote { | |
| 244 | 244 | return nil, fmt.Errorf("no remote specified. Use --remote or 'gordon remotes use <name>'") | |
| @@ -238,7 +238,7 @@ Examples: | |||||
| 238 | func resolveRequiredRemote(flagToken string) (*remote.ResolvedRemote, error) { | 238 | func resolveRequiredRemote(flagToken string) (*remote.ResolvedRemote, error) { | ||
| 239 | resolved, isRemote, err := remote.ResolveStrict(remoteFlag, flagToken, insecureTLSFlag) | 239 | resolved, isRemote, err := remote.ResolveStrict(remoteFlag, flagToken, insecureTLSFlag) | ||
| 240 | if err != nil { | 240 | if err != nil { | ||
| 241 | - | return nil, err | 241 | + | return nil, fmt.Errorf("failed to resolve remote %q: %w", remoteFlag, err) |
| 242 | } | 242 | } | ||
| 243 | if !isRemote { | 243 | if !isRemote { | ||
| 244 | return nil, fmt.Errorf("no remote specified. Use --remote or 'gordon remotes use <name>'") | 244 | return nil, fmt.Errorf("no remote specified. Use --remote or 'gordon remotes use <name>'") | ||
M internal/adapters/in/cli/routes.go +21 -1
| @@ -1479,7 +1479,7 @@ func runRoutePurge(ctx context.Context, cp ControlPlane, domainName string, opts | |||
| 1479 | 1479 | if diag.Runtime != nil && diag.Runtime.ContainerID != "" { | |
| 1480 | 1480 | report.OrphanedEntities = append(report.OrphanedEntities, domain.CleanupOrphanedEntity{Kind: "route_container", ID: diag.Runtime.ContainerID, Status: diag.Runtime.ContainerStatus, Reason: "route runtime state matched purge target"}) | |
| 1481 | 1481 | } | |
| 1482 | - | report.OrphanedEntities = append(report.OrphanedEntities, diag.OrphanedEntities...) | |
| 1482 | + | report.OrphanedEntities = appendUniqueCleanupEntities(report.OrphanedEntities, diag.OrphanedEntities...) | |
| 1483 | 1483 | if opts.Volumes { | |
| 1484 | 1484 | for _, volume := range diag.Volumes { | |
| 1485 | 1485 | report.PreservedVolumes = append(report.PreservedVolumes, domain.CleanupVolume{Name: volume.Name, Reason: "volume purge requires explicit --volumes and runtime support"}) | |
| @@ -1516,6 +1516,26 @@ func runRoutePurge(ctx context.Context, cp ControlPlane, domainName string, opts | |||
| 1516 | 1516 | return report, nil | |
| 1517 | 1517 | } | |
| 1518 | 1518 | ||
| 1519 | + | func appendUniqueCleanupEntities(existing []domain.CleanupOrphanedEntity, candidates ...domain.CleanupOrphanedEntity) []domain.CleanupOrphanedEntity { | |
| 1520 | + | seen := make(map[string]struct{}, len(existing)) | |
| 1521 | + | for _, entity := range existing { | |
| 1522 | + | seen[cleanupEntityKey(entity)] = struct{}{} | |
| 1523 | + | } | |
| 1524 | + | for _, entity := range candidates { | |
| 1525 | + | key := cleanupEntityKey(entity) | |
| 1526 | + | if _, ok := seen[key]; ok { | |
| 1527 | + | continue | |
| 1528 | + | } | |
| 1529 | + | existing = append(existing, entity) | |
| 1530 | + | seen[key] = struct{}{} | |
| 1531 | + | } | |
| 1532 | + | return existing | |
| 1533 | + | } | |
| 1534 | + | ||
| 1535 | + | func cleanupEntityKey(entity domain.CleanupOrphanedEntity) string { | |
| 1536 | + | return entity.Kind + "|" + entity.ID + "|" + entity.Name | |
| 1537 | + | } | |
| 1538 | + | ||
| 1519 | 1539 | func writeRoutePurgeText(out io.Writer, report *domain.CleanupReport, force bool) error { | |
| 1520 | 1540 | mode := "Purge dry-run" | |
| 1521 | 1541 | if force { | |
| @@ -1479,7 +1479,7 @@ func runRoutePurge(ctx context.Context, cp ControlPlane, domainName string, opts | |||||
| 1479 | if diag.Runtime != nil && diag.Runtime.ContainerID != "" { | 1479 | if diag.Runtime != nil && diag.Runtime.ContainerID != "" { | ||
| 1480 | report.OrphanedEntities = append(report.OrphanedEntities, domain.CleanupOrphanedEntity{Kind: "route_container", ID: diag.Runtime.ContainerID, Status: diag.Runtime.ContainerStatus, Reason: "route runtime state matched purge target"}) | 1480 | report.OrphanedEntities = append(report.OrphanedEntities, domain.CleanupOrphanedEntity{Kind: "route_container", ID: diag.Runtime.ContainerID, Status: diag.Runtime.ContainerStatus, Reason: "route runtime state matched purge target"}) | ||
| 1481 | } | 1481 | } | ||
| 1482 | - | report.OrphanedEntities = append(report.OrphanedEntities, diag.OrphanedEntities...) | 1482 | + | report.OrphanedEntities = appendUniqueCleanupEntities(report.OrphanedEntities, diag.OrphanedEntities...) |
| 1483 | if opts.Volumes { | 1483 | if opts.Volumes { | ||
| 1484 | for _, volume := range diag.Volumes { | 1484 | for _, volume := range diag.Volumes { | ||
| 1485 | report.PreservedVolumes = append(report.PreservedVolumes, domain.CleanupVolume{Name: volume.Name, Reason: "volume purge requires explicit --volumes and runtime support"}) | 1485 | report.PreservedVolumes = append(report.PreservedVolumes, domain.CleanupVolume{Name: volume.Name, Reason: "volume purge requires explicit --volumes and runtime support"}) | ||
| @@ -1516,6 +1516,26 @@ func runRoutePurge(ctx context.Context, cp ControlPlane, domainName string, opts | |||||
| 1516 | return report, nil | 1516 | return report, nil | ||
| 1517 | } | 1517 | } | ||
| 1518 | 1518 | ||||
| 1519 | + | func appendUniqueCleanupEntities(existing []domain.CleanupOrphanedEntity, candidates ...domain.CleanupOrphanedEntity) []domain.CleanupOrphanedEntity { | |||
| 1520 | + | seen := make(map[string]struct{}, len(existing)) | |||
| 1521 | + | for _, entity := range existing { | |||
| 1522 | + | seen[cleanupEntityKey(entity)] = struct{}{} | |||
| 1523 | + | } | |||
| 1524 | + | for _, entity := range candidates { | |||
| 1525 | + | key := cleanupEntityKey(entity) | |||
| 1526 | + | if _, ok := seen[key]; ok { | |||
| 1527 | + | continue | |||
| 1528 | + | } | |||
| 1529 | + | existing = append(existing, entity) | |||
| 1530 | + | seen[key] = struct{}{} | |||
| 1531 | + | } | |||
| 1532 | + | return existing | |||
| 1533 | + | } | |||
| 1534 | + | ||||
| 1535 | + | func cleanupEntityKey(entity domain.CleanupOrphanedEntity) string { | |||
| 1536 | + | return entity.Kind + "|" + entity.ID + "|" + entity.Name | |||
| 1537 | + | } | |||
| 1538 | + | ||||
| 1519 | func writeRoutePurgeText(out io.Writer, report *domain.CleanupReport, force bool) error { | 1539 | func writeRoutePurgeText(out io.Writer, report *domain.CleanupReport, force bool) error { | ||
| 1520 | mode := "Purge dry-run" | 1540 | mode := "Purge dry-run" | ||
| 1521 | if force { | 1541 | if force { | ||
M internal/app/run.go +9 -9
| @@ -701,8 +701,8 @@ func (si *serviceInit) registerReloadCoordinatorHooks() { | |||
| 701 | 701 | return | |
| 702 | 702 | } | |
| 703 | 703 | ||
| 704 | - | si.svc.reloadCoordinator.SetContainerConfigApplier(func(reloadCfg Config) error { | |
| 705 | - | containerCfg, err := buildContainerServiceConfig(si.ctx, si.v, reloadCfg, si.svc, si.log) | |
| 704 | + | si.svc.reloadCoordinator.SetContainerConfigApplier(func(reloadCtx context.Context, reloadCfg Config) error { | |
| 705 | + | containerCfg, err := buildContainerServiceConfig(reloadCtx, si.v, reloadCfg, si.svc, si.log) | |
| 706 | 706 | if err != nil { | |
| 707 | 707 | return err | |
| 708 | 708 | } | |
| @@ -1627,7 +1627,7 @@ type reloadCoordinator struct { | |||
| 1627 | 1627 | configSvc configReloader | |
| 1628 | 1628 | v *viper.Viper | |
| 1629 | 1629 | proxySvc proxyConfigUpdater | |
| 1630 | - | applyContainerConfig func(Config) error | |
| 1630 | + | applyContainerConfig func(context.Context, Config) error | |
| 1631 | 1631 | registryLimits interface { | |
| 1632 | 1632 | UpdateBlobLimits(maxBlobChunkSize, maxBlobSize int64) | |
| 1633 | 1633 | } | |
| @@ -1659,7 +1659,7 @@ func (c *reloadCoordinator) SetRegistryLimits(limits interface { | |||
| 1659 | 1659 | c.registryLimits = limits | |
| 1660 | 1660 | } | |
| 1661 | 1661 | ||
| 1662 | - | func (c *reloadCoordinator) SetContainerConfigApplier(apply func(Config) error) { | |
| 1662 | + | func (c *reloadCoordinator) SetContainerConfigApplier(apply func(context.Context, Config) error) { | |
| 1663 | 1663 | c.mu.Lock() | |
| 1664 | 1664 | defer c.mu.Unlock() | |
| 1665 | 1665 | c.applyContainerConfig = apply | |
| @@ -1713,16 +1713,16 @@ func (c *reloadCoordinator) applyLoadedConfig(ctx context.Context, now time.Time | |||
| 1713 | 1713 | return fmt.Errorf("failed to parse proxy config on reload: %w", err) | |
| 1714 | 1714 | } | |
| 1715 | 1715 | ||
| 1716 | - | c.proxySvc.UpdateConfig(reloadedProxy.proxyConfig) | |
| 1717 | - | if c.registryLimits != nil { | |
| 1718 | - | c.registryLimits.UpdateBlobLimits(reloadedProxy.maxBlobChunkSize, reloadedProxy.maxBlobSize) | |
| 1719 | - | } | |
| 1720 | 1716 | if c.applyContainerConfig != nil { | |
| 1721 | - | if err := c.applyContainerConfig(reloadCfg); err != nil { | |
| 1717 | + | if err := c.applyContainerConfig(ctx, reloadCfg); err != nil { | |
| 1722 | 1718 | c.log.Error().Err(err).Msg("failed to apply container config on reload") | |
| 1723 | 1719 | return fmt.Errorf("failed to apply container config on reload: %w", err) | |
| 1724 | 1720 | } | |
| 1725 | 1721 | } | |
| 1722 | + | c.proxySvc.UpdateConfig(reloadedProxy.proxyConfig) | |
| 1723 | + | if c.registryLimits != nil { | |
| 1724 | + | c.registryLimits.UpdateBlobLimits(reloadedProxy.maxBlobChunkSize, reloadedProxy.maxBlobSize) | |
| 1725 | + | } | |
| 1726 | 1726 | ||
| 1727 | 1727 | if c.eventBus != nil { | |
| 1728 | 1728 | if err := c.eventBus.Publish(domain.EventConfigReload, nil); err != nil { | |
| @@ -701,8 +701,8 @@ func (si *serviceInit) registerReloadCoordinatorHooks() { | |||||
| 701 | return | 701 | return | ||
| 702 | } | 702 | } | ||
| 703 | 703 | ||||
| 704 | - | si.svc.reloadCoordinator.SetContainerConfigApplier(func(reloadCfg Config) error { | 704 | + | si.svc.reloadCoordinator.SetContainerConfigApplier(func(reloadCtx context.Context, reloadCfg Config) error { |
| 705 | - | containerCfg, err := buildContainerServiceConfig(si.ctx, si.v, reloadCfg, si.svc, si.log) | 705 | + | containerCfg, err := buildContainerServiceConfig(reloadCtx, si.v, reloadCfg, si.svc, si.log) |
| 706 | if err != nil { | 706 | if err != nil { | ||
| 707 | return err | 707 | return err | ||
| 708 | } | 708 | } | ||
| @@ -1627,7 +1627,7 @@ type reloadCoordinator struct { | |||||
| 1627 | configSvc configReloader | 1627 | configSvc configReloader | ||
| 1628 | v *viper.Viper | 1628 | v *viper.Viper | ||
| 1629 | proxySvc proxyConfigUpdater | 1629 | proxySvc proxyConfigUpdater | ||
| 1630 | - | applyContainerConfig func(Config) error | 1630 | + | applyContainerConfig func(context.Context, Config) error |
| 1631 | registryLimits interface { | 1631 | registryLimits interface { | ||
| 1632 | UpdateBlobLimits(maxBlobChunkSize, maxBlobSize int64) | 1632 | UpdateBlobLimits(maxBlobChunkSize, maxBlobSize int64) | ||
| 1633 | } | 1633 | } | ||
| @@ -1659,7 +1659,7 @@ func (c *reloadCoordinator) SetRegistryLimits(limits interface { | |||||
| 1659 | c.registryLimits = limits | 1659 | c.registryLimits = limits | ||
| 1660 | } | 1660 | } | ||
| 1661 | 1661 | ||||
| 1662 | - | func (c *reloadCoordinator) SetContainerConfigApplier(apply func(Config) error) { | 1662 | + | func (c *reloadCoordinator) SetContainerConfigApplier(apply func(context.Context, Config) error) { |
| 1663 | c.mu.Lock() | 1663 | c.mu.Lock() | ||
| 1664 | defer c.mu.Unlock() | 1664 | defer c.mu.Unlock() | ||
| 1665 | c.applyContainerConfig = apply | 1665 | c.applyContainerConfig = apply | ||
| @@ -1713,16 +1713,16 @@ func (c *reloadCoordinator) applyLoadedConfig(ctx context.Context, now time.Time | |||||
| 1713 | return fmt.Errorf("failed to parse proxy config on reload: %w", err) | 1713 | return fmt.Errorf("failed to parse proxy config on reload: %w", err) | ||
| 1714 | } | 1714 | } | ||
| 1715 | 1715 | ||||
| 1716 | - | c.proxySvc.UpdateConfig(reloadedProxy.proxyConfig) | |||
| 1717 | - | if c.registryLimits != nil { | |||
| 1718 | - | c.registryLimits.UpdateBlobLimits(reloadedProxy.maxBlobChunkSize, reloadedProxy.maxBlobSize) | |||
| 1719 | - | } | |||
| 1720 | if c.applyContainerConfig != nil { | 1716 | if c.applyContainerConfig != nil { | ||
| 1721 | - | if err := c.applyContainerConfig(reloadCfg); err != nil { | 1717 | + | if err := c.applyContainerConfig(ctx, reloadCfg); err != nil { |
| 1722 | c.log.Error().Err(err).Msg("failed to apply container config on reload") | 1718 | c.log.Error().Err(err).Msg("failed to apply container config on reload") | ||
| 1723 | return fmt.Errorf("failed to apply container config on reload: %w", err) | 1719 | return fmt.Errorf("failed to apply container config on reload: %w", err) | ||
| 1724 | } | 1720 | } | ||
| 1725 | } | 1721 | } | ||
| 1722 | + | c.proxySvc.UpdateConfig(reloadedProxy.proxyConfig) | |||
| 1723 | + | if c.registryLimits != nil { | |||
| 1724 | + | c.registryLimits.UpdateBlobLimits(reloadedProxy.maxBlobChunkSize, reloadedProxy.maxBlobSize) | |||
| 1725 | + | } | |||
| 1726 | 1726 | ||||
| 1727 | if c.eventBus != nil { | 1727 | if c.eventBus != nil { | ||
| 1728 | if err := c.eventBus.Publish(domain.EventConfigReload, nil); err != nil { | 1728 | if err := c.eventBus.Publish(domain.EventConfigReload, nil); err != nil { | ||
M internal/app/run_reload_test.go +4 -2
| @@ -116,11 +116,13 @@ func (r *registryLimitsRecorder) UpdateBlobLimits(maxBlobChunkSize, maxBlobSize | |||
| 116 | 116 | ||
| 117 | 117 | type containerConfigApplyRecorder struct { | |
| 118 | 118 | calls int | |
| 119 | + | ctx context.Context | |
| 119 | 120 | cfg Config | |
| 120 | 121 | } | |
| 121 | 122 | ||
| 122 | - | func (r *containerConfigApplyRecorder) Apply(cfg Config) error { | |
| 123 | + | func (r *containerConfigApplyRecorder) Apply(ctx context.Context, cfg Config) error { | |
| 123 | 124 | r.calls++ | |
| 125 | + | r.ctx = ctx | |
| 124 | 126 | r.cfg = cfg | |
| 125 | 127 | return nil | |
| 126 | 128 | } | |
| @@ -256,7 +258,7 @@ func TestServiceInit_RegisterReloadCoordinatorHooks_WiresContainerConfigApplier( | |||
| 256 | 258 | reloadCfg.Server.RegistryPort = 5000 | |
| 257 | 259 | reloadCfg.Images.AllowedRegistries = []string{"docker.io"} | |
| 258 | 260 | ||
| 259 | - | require.NoError(t, si.svc.reloadCoordinator.applyContainerConfig(reloadCfg)) | |
| 261 | + | require.NoError(t, si.svc.reloadCoordinator.applyContainerConfig(ctx, reloadCfg)) | |
| 260 | 262 | } | |
| 261 | 263 | func TestReloadCoordinator_DebouncesRepeatedWatchCallbacks(t *testing.T) { | |
| 262 | 264 | ctx := context.Background() | |
| @@ -116,11 +116,13 @@ func (r *registryLimitsRecorder) UpdateBlobLimits(maxBlobChunkSize, maxBlobSize | |||||
| 116 | 116 | ||||
| 117 | type containerConfigApplyRecorder struct { | 117 | type containerConfigApplyRecorder struct { | ||
| 118 | calls int | 118 | calls int | ||
| 119 | + | ctx context.Context | |||
| 119 | cfg Config | 120 | cfg Config | ||
| 120 | } | 121 | } | ||
| 121 | 122 | ||||
| 122 | - | func (r *containerConfigApplyRecorder) Apply(cfg Config) error { | 123 | + | func (r *containerConfigApplyRecorder) Apply(ctx context.Context, cfg Config) error { |
| 123 | r.calls++ | 124 | r.calls++ | ||
| 125 | + | r.ctx = ctx | |||
| 124 | r.cfg = cfg | 126 | r.cfg = cfg | ||
| 125 | return nil | 127 | return nil | ||
| 126 | } | 128 | } | ||
| @@ -256,7 +258,7 @@ func TestServiceInit_RegisterReloadCoordinatorHooks_WiresContainerConfigApplier( | |||||
| 256 | reloadCfg.Server.RegistryPort = 5000 | 258 | reloadCfg.Server.RegistryPort = 5000 | ||
| 257 | reloadCfg.Images.AllowedRegistries = []string{"docker.io"} | 259 | reloadCfg.Images.AllowedRegistries = []string{"docker.io"} | ||
| 258 | 260 | ||||
| 259 | - | require.NoError(t, si.svc.reloadCoordinator.applyContainerConfig(reloadCfg)) | 261 | + | require.NoError(t, si.svc.reloadCoordinator.applyContainerConfig(ctx, reloadCfg)) |
| 260 | } | 262 | } | ||
| 261 | func TestReloadCoordinator_DebouncesRepeatedWatchCallbacks(t *testing.T) { | 263 | func TestReloadCoordinator_DebouncesRepeatedWatchCallbacks(t *testing.T) { | ||
| 262 | ctx := context.Background() | 264 | ctx := context.Background() | ||
M internal/usecase/publictls/service_test.go +6 -2
| @@ -663,8 +663,12 @@ func TestServiceReconcile_SerializesConcurrentRuns(t *testing.T) { | |||
| 663 | 663 | t.Fatal("first reconcile did not start certificate obtain") | |
| 664 | 664 | } | |
| 665 | 665 | ||
| 666 | - | go func() { errCh <- svc.Reconcile(ctx) }() | |
| 667 | - | time.Sleep(50 * time.Millisecond) | |
| 666 | + | secondStarted := make(chan struct{}) | |
| 667 | + | go func() { | |
| 668 | + | close(secondStarted) | |
| 669 | + | errCh <- svc.Reconcile(ctx) | |
| 670 | + | }() | |
| 671 | + | <-secondStarted | |
| 668 | 672 | close(releaseObtain) | |
| 669 | 673 | ||
| 670 | 674 | require.NoError(t, <-errCh) | |
| @@ -663,8 +663,12 @@ func TestServiceReconcile_SerializesConcurrentRuns(t *testing.T) { | |||||
| 663 | t.Fatal("first reconcile did not start certificate obtain") | 663 | t.Fatal("first reconcile did not start certificate obtain") | ||
| 664 | } | 664 | } | ||
| 665 | 665 | ||||
| 666 | - | go func() { errCh <- svc.Reconcile(ctx) }() | 666 | + | secondStarted := make(chan struct{}) |
| 667 | - | time.Sleep(50 * time.Millisecond) | 667 | + | go func() { |
| 668 | + | close(secondStarted) | |||
| 669 | + | errCh <- svc.Reconcile(ctx) | |||
| 670 | + | }() | |||
| 671 | + | <-secondStarted | |||
| 668 | close(releaseObtain) | 672 | close(releaseObtain) | ||
| 669 | 673 | ||||
| 670 | require.NoError(t, <-errCh) | 674 | require.NoError(t, <-errCh) | ||