feat(app): wire admin API and auto-route handler
- commit
- 5954557ac99b685f7b6b5b8586bb9fc833f665ad
- author
- bnema · · 2026-01-16 20:52 UTC
- parent
- 70b6ab6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +52 -11
M internal/adapters/in/http/middleware/logging.go +5 -0
| @@ -47,6 +47,7 @@ func (rw *ResponseWriter) BytesWritten() int { | |||
| 47 | 47 | } | |
| 48 | 48 | ||
| 49 | 49 | // RequestLogger is a middleware that logs HTTP requests using zerowrap. | |
| 50 | + | // It also attaches the logger to the request context for downstream handlers. | |
| 50 | 51 | func RequestLogger(log zerowrap.Logger) func(http.Handler) http.Handler { | |
| 51 | 52 | return func(next http.Handler) http.Handler { | |
| 52 | 53 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| @@ -58,6 +59,10 @@ func RequestLogger(log zerowrap.Logger) func(http.Handler) http.Handler { | |||
| 58 | 59 | // Get client IP (handle X-Forwarded-For and X-Real-IP headers) | |
| 59 | 60 | clientIP := getClientIP(r) | |
| 60 | 61 | ||
| 62 | + | // Attach the logger to the request context for downstream handlers | |
| 63 | + | ctx := zerowrap.WithCtx(r.Context(), log) | |
| 64 | + | r = r.WithContext(ctx) | |
| 65 | + | ||
| 61 | 66 | // Call the next handler | |
| 62 | 67 | next.ServeHTTP(rw, r) | |
| 63 | 68 | ||
| @@ -47,6 +47,7 @@ func (rw *ResponseWriter) BytesWritten() int { | |||||
| 47 | } | 47 | } | ||
| 48 | 48 | ||||
| 49 | // RequestLogger is a middleware that logs HTTP requests using zerowrap. | 49 | // RequestLogger is a middleware that logs HTTP requests using zerowrap. | ||
| 50 | + | // It also attaches the logger to the request context for downstream handlers. | |||
| 50 | func RequestLogger(log zerowrap.Logger) func(http.Handler) http.Handler { | 51 | func RequestLogger(log zerowrap.Logger) func(http.Handler) http.Handler { | ||
| 51 | return func(next http.Handler) http.Handler { | 52 | return func(next http.Handler) http.Handler { | ||
| 52 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 53 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| @@ -58,6 +59,10 @@ func RequestLogger(log zerowrap.Logger) func(http.Handler) http.Handler { | |||||
| 58 | // Get client IP (handle X-Forwarded-For and X-Real-IP headers) | 59 | // Get client IP (handle X-Forwarded-For and X-Real-IP headers) | ||
| 59 | clientIP := getClientIP(r) | 60 | clientIP := getClientIP(r) | ||
| 60 | 61 | ||||
| 62 | + | // Attach the logger to the request context for downstream handlers | |||
| 63 | + | ctx := zerowrap.WithCtx(r.Context(), log) | |||
| 64 | + | r = r.WithContext(ctx) | |||
| 65 | + | ||||
| 61 | // Call the next handler | 66 | // Call the next handler | ||
| 62 | next.ServeHTTP(rw, r) | 67 | next.ServeHTTP(rw, r) | ||
| 63 | 68 | ||||
M internal/app/run.go +47 -11
| @@ -29,6 +29,7 @@ import ( | |||
| 29 | 29 | "gordon/internal/adapters/out/tokenstore" | |
| 30 | 30 | ||
| 31 | 31 | // Adapters - Input | |
| 32 | + | "gordon/internal/adapters/in/http/admin" | |
| 32 | 33 | "gordon/internal/adapters/in/http/middleware" | |
| 33 | 34 | "gordon/internal/adapters/in/http/registry" | |
| 34 | 35 | ||
| @@ -108,8 +109,10 @@ type services struct { | |||
| 108 | 109 | proxySvc *proxy.Service | |
| 109 | 110 | authSvc *auth.Service | |
| 110 | 111 | tokenHandler *registry.TokenHandler | |
| 112 | + | adminHandler *admin.Handler | |
| 111 | 113 | internalRegUser string | |
| 112 | 114 | internalRegPass string | |
| 115 | + | envDir string | |
| 113 | 116 | } | |
| 114 | 117 | ||
| 115 | 118 | // Run initializes and starts the Gordon application. | |
| @@ -145,7 +148,7 @@ func Run(ctx context.Context, configPath string) error { | |||
| 145 | 148 | } | |
| 146 | 149 | ||
| 147 | 150 | // Register event handlers | |
| 148 | - | if err := registerEventHandlers(ctx, svc); err != nil { | |
| 151 | + | if err := registerEventHandlers(ctx, svc, cfg); err != nil { | |
| 149 | 152 | return err | |
| 150 | 153 | } | |
| 151 | 154 | ||
| @@ -275,6 +278,19 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||
| 275 | 278 | svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, internalAuth, log) | |
| 276 | 279 | } | |
| 277 | 280 | ||
| 281 | + | // Determine env directory for admin API | |
| 282 | + | dataDir := cfg.Server.DataDir | |
| 283 | + | if dataDir == "" { | |
| 284 | + | dataDir = DefaultDataDir() | |
| 285 | + | } | |
| 286 | + | svc.envDir = cfg.Env.Dir | |
| 287 | + | if svc.envDir == "" { | |
| 288 | + | svc.envDir = filepath.Join(dataDir, "env") | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | // Create admin handler for admin API | |
| 292 | + | svc.adminHandler = admin.NewHandler(svc.configSvc, svc.authSvc, svc.containerSvc, svc.eventBus, svc.envDir, log) | |
| 293 | + | ||
| 278 | 294 | return svc, nil | |
| 279 | 295 | } | |
| 280 | 296 | ||
| @@ -636,6 +652,7 @@ func createContainerService(v *viper.Viper, cfg Config, svc *services) *containe | |||
| 636 | 652 | RegistryPassword: cfg.RegistryAuth.Password, | |
| 637 | 653 | InternalRegistryUsername: svc.internalRegUser, | |
| 638 | 654 | InternalRegistryPassword: svc.internalRegPass, | |
| 655 | + | PullPolicy: v.GetString("deploy.pull_policy"), | |
| 639 | 656 | VolumeAutoCreate: v.GetBool("volumes.auto_create"), | |
| 640 | 657 | VolumePrefix: v.GetString("volumes.prefix"), | |
| 641 | 658 | VolumePreserve: v.GetBool("volumes.preserve"), | |
| @@ -649,12 +666,19 @@ func createContainerService(v *viper.Viper, cfg Config, svc *services) *containe | |||
| 649 | 666 | } | |
| 650 | 667 | ||
| 651 | 668 | // registerEventHandlers registers all event handlers. | |
| 652 | - | func registerEventHandlers(ctx context.Context, svc *services) error { | |
| 669 | + | func registerEventHandlers(ctx context.Context, svc *services, cfg Config) error { | |
| 653 | 670 | imagePushedHandler := container.NewImagePushedHandler(ctx, svc.containerSvc, svc.configSvc) | |
| 654 | 671 | if err := svc.eventBus.Subscribe(imagePushedHandler); err != nil { | |
| 655 | 672 | return fmt.Errorf("failed to subscribe image pushed handler: %w", err) | |
| 656 | 673 | } | |
| 657 | 674 | ||
| 675 | + | // Auto-route handler for creating routes from image labels | |
| 676 | + | autoRouteHandler := container.NewAutoRouteHandler(ctx, svc.configSvc, svc.containerSvc, svc.blobStorage, cfg.Server.RegistryDomain). | |
| 677 | + | WithEnvExtractor(svc.runtime, svc.envDir) | |
| 678 | + | if err := svc.eventBus.Subscribe(autoRouteHandler); err != nil { | |
| 679 | + | return fmt.Errorf("failed to subscribe auto-route handler: %w", err) | |
| 680 | + | } | |
| 681 | + | ||
| 658 | 682 | configReloadHandler := container.NewConfigReloadHandler(ctx, svc.containerSvc, svc.configSvc) | |
| 659 | 683 | if err := svc.eventBus.Subscribe(configReloadHandler); err != nil { | |
| 660 | 684 | return fmt.Errorf("failed to subscribe config reload handler: %w", err) | |
| @@ -680,6 +704,10 @@ func registerEventHandlers(ctx context.Context, svc *services) error { | |||
| 680 | 704 | } | |
| 681 | 705 | ||
| 682 | 706 | // setupConfigHotReload sets up Viper config hot reload. | |
| 707 | + | // NOTE: This does NOT reload routes into memory. Routes are managed via API | |
| 708 | + | // (AddRoute/UpdateRoute/RemoveRoute) and memory is the source of truth. | |
| 709 | + | // The file watcher only updates proxy config and refreshes targets. | |
| 710 | + | // Manual config file edits to routes require a server restart. | |
| 683 | 711 | func setupConfigHotReload(ctx context.Context, v *viper.Viper, svc *services, log zerowrap.Logger) { | |
| 684 | 712 | v.OnConfigChange(func(e fsnotify.Event) { | |
| 685 | 713 | log.Info().Str("file", e.Name).Msg("config file changed") | |
| @@ -689,11 +717,7 @@ func setupConfigHotReload(ctx context.Context, v *viper.Viper, svc *services, lo | |||
| 689 | 717 | return | |
| 690 | 718 | } | |
| 691 | 719 | ||
| 692 | - | if err := svc.configSvc.Load(ctx); err != nil { | |
| 693 | - | log.Error().Err(err).Msg("failed to reload configuration") | |
| 694 | - | return | |
| 695 | - | } | |
| 696 | - | ||
| 720 | + | // Update proxy config from viper (reads directly from viper, not memory) | |
| 697 | 721 | svc.proxySvc.UpdateConfig(proxy.Config{ | |
| 698 | 722 | RegistryDomain: v.GetString("server.registry_domain"), | |
| 699 | 723 | RegistryPort: v.GetInt("server.registry_port"), | |
| @@ -704,10 +728,7 @@ func setupConfigHotReload(ctx context.Context, v *viper.Viper, svc *services, lo | |||
| 704 | 728 | log.Warn().Err(err).Msg("failed to refresh proxy targets") | |
| 705 | 729 | } | |
| 706 | 730 | ||
| 707 | - | // Publish config reload event to trigger container updates | |
| 708 | - | if err := svc.eventBus.Publish(domain.EventConfigReload, nil); err != nil { | |
| 709 | - | log.Error().Err(err).Msg("failed to publish config reload event") | |
| 710 | - | } | |
| 731 | + | log.Debug().Msg("config hot reload complete (routes unchanged, memory is source of truth)") | |
| 711 | 732 | }) | |
| 712 | 733 | v.WatchConfig() | |
| 713 | 734 | } | |
| @@ -759,6 +780,20 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||
| 759 | 780 | } | |
| 760 | 781 | registryMux.Handle("/v2/", registryWithMiddleware) | |
| 761 | 782 | ||
| 783 | + | // Add admin API handler with auth middleware | |
| 784 | + | if svc.adminHandler != nil { | |
| 785 | + | adminMiddlewares := []func(http.Handler) http.Handler{ | |
| 786 | + | middleware.PanicRecovery(log), | |
| 787 | + | middleware.RequestLogger(log), | |
| 788 | + | } | |
| 789 | + | // Add admin auth middleware if auth is enabled | |
| 790 | + | if svc.authSvc != nil { | |
| 791 | + | adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, log)) | |
| 792 | + | } | |
| 793 | + | adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler) | |
| 794 | + | registryMux.Handle("/admin/", adminWithMiddleware) | |
| 795 | + | } | |
| 796 | + | ||
| 762 | 797 | proxyMiddlewares := []func(http.Handler) http.Handler{ | |
| 763 | 798 | middleware.PanicRecovery(log), | |
| 764 | 799 | middleware.RequestLogger(log), | |
| @@ -1063,6 +1098,7 @@ func loadConfig(v *viper.Viper, configPath string) error { | |||
| 1063 | 1098 | v.SetDefault("registry_auth.enabled", false) | |
| 1064 | 1099 | v.SetDefault("registry_auth.type", "password") | |
| 1065 | 1100 | v.SetDefault("registry_auth.token_expiry", "720h") | |
| 1101 | + | v.SetDefault("deploy.pull_policy", container.PullPolicyIfTagChanged) | |
| 1066 | 1102 | ||
| 1067 | 1103 | ConfigureViper(v, configPath) | |
| 1068 | 1104 | ||
| @@ -29,6 +29,7 @@ import ( | |||||
| 29 | "gordon/internal/adapters/out/tokenstore" | 29 | "gordon/internal/adapters/out/tokenstore" | ||
| 30 | 30 | ||||
| 31 | // Adapters - Input | 31 | // Adapters - Input | ||
| 32 | + | "gordon/internal/adapters/in/http/admin" | |||
| 32 | "gordon/internal/adapters/in/http/middleware" | 33 | "gordon/internal/adapters/in/http/middleware" | ||
| 33 | "gordon/internal/adapters/in/http/registry" | 34 | "gordon/internal/adapters/in/http/registry" | ||
| 34 | 35 | ||||
| @@ -108,8 +109,10 @@ type services struct { | |||||
| 108 | proxySvc *proxy.Service | 109 | proxySvc *proxy.Service | ||
| 109 | authSvc *auth.Service | 110 | authSvc *auth.Service | ||
| 110 | tokenHandler *registry.TokenHandler | 111 | tokenHandler *registry.TokenHandler | ||
| 112 | + | adminHandler *admin.Handler | |||
| 111 | internalRegUser string | 113 | internalRegUser string | ||
| 112 | internalRegPass string | 114 | internalRegPass string | ||
| 115 | + | envDir string | |||
| 113 | } | 116 | } | ||
| 114 | 117 | ||||
| 115 | // Run initializes and starts the Gordon application. | 118 | // Run initializes and starts the Gordon application. | ||
| @@ -145,7 +148,7 @@ func Run(ctx context.Context, configPath string) error { | |||||
| 145 | } | 148 | } | ||
| 146 | 149 | ||||
| 147 | // Register event handlers | 150 | // Register event handlers | ||
| 148 | - | if err := registerEventHandlers(ctx, svc); err != nil { | 151 | + | if err := registerEventHandlers(ctx, svc, cfg); err != nil { |
| 149 | return err | 152 | return err | ||
| 150 | } | 153 | } | ||
| 151 | 154 | ||||
| @@ -275,6 +278,19 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||||
| 275 | svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, internalAuth, log) | 278 | svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, internalAuth, log) | ||
| 276 | } | 279 | } | ||
| 277 | 280 | ||||
| 281 | + | // Determine env directory for admin API | |||
| 282 | + | dataDir := cfg.Server.DataDir | |||
| 283 | + | if dataDir == "" { | |||
| 284 | + | dataDir = DefaultDataDir() | |||
| 285 | + | } | |||
| 286 | + | svc.envDir = cfg.Env.Dir | |||
| 287 | + | if svc.envDir == "" { | |||
| 288 | + | svc.envDir = filepath.Join(dataDir, "env") | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | // Create admin handler for admin API | |||
| 292 | + | svc.adminHandler = admin.NewHandler(svc.configSvc, svc.authSvc, svc.containerSvc, svc.eventBus, svc.envDir, log) | |||
| 293 | + | ||||
| 278 | return svc, nil | 294 | return svc, nil | ||
| 279 | } | 295 | } | ||
| 280 | 296 | ||||
| @@ -636,6 +652,7 @@ func createContainerService(v *viper.Viper, cfg Config, svc *services) *containe | |||||
| 636 | RegistryPassword: cfg.RegistryAuth.Password, | 652 | RegistryPassword: cfg.RegistryAuth.Password, | ||
| 637 | InternalRegistryUsername: svc.internalRegUser, | 653 | InternalRegistryUsername: svc.internalRegUser, | ||
| 638 | InternalRegistryPassword: svc.internalRegPass, | 654 | InternalRegistryPassword: svc.internalRegPass, | ||
| 655 | + | PullPolicy: v.GetString("deploy.pull_policy"), | |||
| 639 | VolumeAutoCreate: v.GetBool("volumes.auto_create"), | 656 | VolumeAutoCreate: v.GetBool("volumes.auto_create"), | ||
| 640 | VolumePrefix: v.GetString("volumes.prefix"), | 657 | VolumePrefix: v.GetString("volumes.prefix"), | ||
| 641 | VolumePreserve: v.GetBool("volumes.preserve"), | 658 | VolumePreserve: v.GetBool("volumes.preserve"), | ||
| @@ -649,12 +666,19 @@ func createContainerService(v *viper.Viper, cfg Config, svc *services) *containe | |||||
| 649 | } | 666 | } | ||
| 650 | 667 | ||||
| 651 | // registerEventHandlers registers all event handlers. | 668 | // registerEventHandlers registers all event handlers. | ||
| 652 | - | func registerEventHandlers(ctx context.Context, svc *services) error { | 669 | + | func registerEventHandlers(ctx context.Context, svc *services, cfg Config) error { |
| 653 | imagePushedHandler := container.NewImagePushedHandler(ctx, svc.containerSvc, svc.configSvc) | 670 | imagePushedHandler := container.NewImagePushedHandler(ctx, svc.containerSvc, svc.configSvc) | ||
| 654 | if err := svc.eventBus.Subscribe(imagePushedHandler); err != nil { | 671 | if err := svc.eventBus.Subscribe(imagePushedHandler); err != nil { | ||
| 655 | return fmt.Errorf("failed to subscribe image pushed handler: %w", err) | 672 | return fmt.Errorf("failed to subscribe image pushed handler: %w", err) | ||
| 656 | } | 673 | } | ||
| 657 | 674 | ||||
| 675 | + | // Auto-route handler for creating routes from image labels | |||
| 676 | + | autoRouteHandler := container.NewAutoRouteHandler(ctx, svc.configSvc, svc.containerSvc, svc.blobStorage, cfg.Server.RegistryDomain). | |||
| 677 | + | WithEnvExtractor(svc.runtime, svc.envDir) | |||
| 678 | + | if err := svc.eventBus.Subscribe(autoRouteHandler); err != nil { | |||
| 679 | + | return fmt.Errorf("failed to subscribe auto-route handler: %w", err) | |||
| 680 | + | } | |||
| 681 | + | ||||
| 658 | configReloadHandler := container.NewConfigReloadHandler(ctx, svc.containerSvc, svc.configSvc) | 682 | configReloadHandler := container.NewConfigReloadHandler(ctx, svc.containerSvc, svc.configSvc) | ||
| 659 | if err := svc.eventBus.Subscribe(configReloadHandler); err != nil { | 683 | if err := svc.eventBus.Subscribe(configReloadHandler); err != nil { | ||
| 660 | return fmt.Errorf("failed to subscribe config reload handler: %w", err) | 684 | return fmt.Errorf("failed to subscribe config reload handler: %w", err) | ||
| @@ -680,6 +704,10 @@ func registerEventHandlers(ctx context.Context, svc *services) error { | |||||
| 680 | } | 704 | } | ||
| 681 | 705 | ||||
| 682 | // setupConfigHotReload sets up Viper config hot reload. | 706 | // setupConfigHotReload sets up Viper config hot reload. | ||
| 707 | + | // NOTE: This does NOT reload routes into memory. Routes are managed via API | |||
| 708 | + | // (AddRoute/UpdateRoute/RemoveRoute) and memory is the source of truth. | |||
| 709 | + | // The file watcher only updates proxy config and refreshes targets. | |||
| 710 | + | // Manual config file edits to routes require a server restart. | |||
| 683 | func setupConfigHotReload(ctx context.Context, v *viper.Viper, svc *services, log zerowrap.Logger) { | 711 | func setupConfigHotReload(ctx context.Context, v *viper.Viper, svc *services, log zerowrap.Logger) { | ||
| 684 | v.OnConfigChange(func(e fsnotify.Event) { | 712 | v.OnConfigChange(func(e fsnotify.Event) { | ||
| 685 | log.Info().Str("file", e.Name).Msg("config file changed") | 713 | log.Info().Str("file", e.Name).Msg("config file changed") | ||
| @@ -689,11 +717,7 @@ func setupConfigHotReload(ctx context.Context, v *viper.Viper, svc *services, lo | |||||
| 689 | return | 717 | return | ||
| 690 | } | 718 | } | ||
| 691 | 719 | ||||
| 692 | - | if err := svc.configSvc.Load(ctx); err != nil { | 720 | + | // Update proxy config from viper (reads directly from viper, not memory) |
| 693 | - | log.Error().Err(err).Msg("failed to reload configuration") | |||
| 694 | - | return | |||
| 695 | - | } | |||
| 696 | - | ||||
| 697 | svc.proxySvc.UpdateConfig(proxy.Config{ | 721 | svc.proxySvc.UpdateConfig(proxy.Config{ | ||
| 698 | RegistryDomain: v.GetString("server.registry_domain"), | 722 | RegistryDomain: v.GetString("server.registry_domain"), | ||
| 699 | RegistryPort: v.GetInt("server.registry_port"), | 723 | RegistryPort: v.GetInt("server.registry_port"), | ||
| @@ -704,10 +728,7 @@ func setupConfigHotReload(ctx context.Context, v *viper.Viper, svc *services, lo | |||||
| 704 | log.Warn().Err(err).Msg("failed to refresh proxy targets") | 728 | log.Warn().Err(err).Msg("failed to refresh proxy targets") | ||
| 705 | } | 729 | } | ||
| 706 | 730 | ||||
| 707 | - | // Publish config reload event to trigger container updates | 731 | + | log.Debug().Msg("config hot reload complete (routes unchanged, memory is source of truth)") |
| 708 | - | if err := svc.eventBus.Publish(domain.EventConfigReload, nil); err != nil { | |||
| 709 | - | log.Error().Err(err).Msg("failed to publish config reload event") | |||
| 710 | - | } | |||
| 711 | }) | 732 | }) | ||
| 712 | v.WatchConfig() | 733 | v.WatchConfig() | ||
| 713 | } | 734 | } | ||
| @@ -759,6 +780,20 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||||
| 759 | } | 780 | } | ||
| 760 | registryMux.Handle("/v2/", registryWithMiddleware) | 781 | registryMux.Handle("/v2/", registryWithMiddleware) | ||
| 761 | 782 | ||||
| 783 | + | // Add admin API handler with auth middleware | |||
| 784 | + | if svc.adminHandler != nil { | |||
| 785 | + | adminMiddlewares := []func(http.Handler) http.Handler{ | |||
| 786 | + | middleware.PanicRecovery(log), | |||
| 787 | + | middleware.RequestLogger(log), | |||
| 788 | + | } | |||
| 789 | + | // Add admin auth middleware if auth is enabled | |||
| 790 | + | if svc.authSvc != nil { | |||
| 791 | + | adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, log)) | |||
| 792 | + | } | |||
| 793 | + | adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler) | |||
| 794 | + | registryMux.Handle("/admin/", adminWithMiddleware) | |||
| 795 | + | } | |||
| 796 | + | ||||
| 762 | proxyMiddlewares := []func(http.Handler) http.Handler{ | 797 | proxyMiddlewares := []func(http.Handler) http.Handler{ | ||
| 763 | middleware.PanicRecovery(log), | 798 | middleware.PanicRecovery(log), | ||
| 764 | middleware.RequestLogger(log), | 799 | middleware.RequestLogger(log), | ||
| @@ -1063,6 +1098,7 @@ func loadConfig(v *viper.Viper, configPath string) error { | |||||
| 1063 | v.SetDefault("registry_auth.enabled", false) | 1098 | v.SetDefault("registry_auth.enabled", false) | ||
| 1064 | v.SetDefault("registry_auth.type", "password") | 1099 | v.SetDefault("registry_auth.type", "password") | ||
| 1065 | v.SetDefault("registry_auth.token_expiry", "720h") | 1100 | v.SetDefault("registry_auth.token_expiry", "720h") | ||
| 1101 | + | v.SetDefault("deploy.pull_policy", container.PullPolicyIfTagChanged) | |||
| 1066 | 1102 | ||||
| 1067 | ConfigureViper(v, configPath) | 1103 | ConfigureViper(v, configPath) | ||
| 1068 | 1104 | ||||