feat(app): wire image prune config and scheduler job
- commit
- b5442e49e15ce4acc2ba09126f470bcf91fad34a
- author
- brice · · 2026-02-08 06:56 UTC
- committer
- brice · · 2026-02-08 09:40 UTC
- parent
- 352a4ea
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +141 -3
M internal/app/run.go +84 -1
| @@ -57,7 +57,9 @@ import ( | |||
| 57 | 57 | "github.com/bnema/gordon/internal/usecase/backup" | |
| 58 | 58 | "github.com/bnema/gordon/internal/usecase/config" | |
| 59 | 59 | "github.com/bnema/gordon/internal/usecase/container" | |
| 60 | + | cronSvc "github.com/bnema/gordon/internal/usecase/cron" | |
| 60 | 61 | "github.com/bnema/gordon/internal/usecase/health" | |
| 62 | + | "github.com/bnema/gordon/internal/usecase/images" | |
| 61 | 63 | "github.com/bnema/gordon/internal/usecase/logs" | |
| 62 | 64 | "github.com/bnema/gordon/internal/usecase/proxy" | |
| 63 | 65 | registrySvc "github.com/bnema/gordon/internal/usecase/registry" | |
| @@ -141,6 +143,14 @@ type Config struct { | |||
| 141 | 143 | Monthly int `mapstructure:"monthly"` | |
| 142 | 144 | } `mapstructure:"retention"` | |
| 143 | 145 | } `mapstructure:"backups"` | |
| 146 | + | ||
| 147 | + | Images struct { | |
| 148 | + | Prune struct { | |
| 149 | + | Enabled bool `mapstructure:"enabled"` | |
| 150 | + | Schedule string `mapstructure:"schedule"` | |
| 151 | + | KeepLast int `mapstructure:"keep_last"` | |
| 152 | + | } `mapstructure:"prune"` | |
| 153 | + | } `mapstructure:"images"` | |
| 144 | 154 | } | |
| 145 | 155 | ||
| 146 | 156 | // services holds all the services used by the application. | |
| @@ -157,6 +167,7 @@ type services struct { | |||
| 157 | 167 | containerSvc *container.Service | |
| 158 | 168 | backupSvc *backup.Service | |
| 159 | 169 | registrySvc *registrySvc.Service | |
| 170 | + | imageSvc *images.Service | |
| 160 | 171 | proxySvc *proxy.Service | |
| 161 | 172 | authSvc *auth.Service | |
| 162 | 173 | authHandler *authhandler.Handler | |
| @@ -452,6 +463,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||
| 452 | 463 | } | |
| 453 | 464 | ||
| 454 | 465 | svc.registrySvc = registrySvc.NewService(svc.blobStorage, svc.manifestStorage, svc.eventBus) | |
| 466 | + | svc.imageSvc = images.NewService(svc.runtime, svc.manifestStorage, svc.blobStorage, log) | |
| 455 | 467 | ||
| 456 | 468 | proxyCfg, err := buildProxyConfig(cfg, log) | |
| 457 | 469 | if err != nil { | |
| @@ -482,7 +494,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||
| 482 | 494 | logSvc := logs.NewService(resolveLogFilePath(cfg), svc.containerSvc, svc.runtime, log) | |
| 483 | 495 | ||
| 484 | 496 | // Create admin handler for admin API | |
| 485 | - | svc.adminHandler = admin.NewHandler(svc.configSvc, svc.authSvc, svc.containerSvc, healthSvc, secretSvc, logSvc, svc.registrySvc, svc.eventBus, log, svc.backupSvc) | |
| 497 | + | svc.adminHandler = admin.NewHandler(svc.configSvc, svc.authSvc, svc.containerSvc, healthSvc, secretSvc, logSvc, svc.registrySvc, svc.eventBus, log, svc.backupSvc, svc.imageSvc) | |
| 486 | 498 | ||
| 487 | 499 | return svc, nil | |
| 488 | 500 | } | |
| @@ -1434,6 +1446,14 @@ func runServers(ctx context.Context, cfg Config, registryHandler, proxyHandler h | |||
| 1434 | 1446 | } | |
| 1435 | 1447 | logEvent.Msg("Gordon is running") | |
| 1436 | 1448 | ||
| 1449 | + | imageScheduler, err := startImagePruneScheduler(ctx, cfg, svc, log) | |
| 1450 | + | if err != nil { | |
| 1451 | + | return err | |
| 1452 | + | } | |
| 1453 | + | if imageScheduler != nil { | |
| 1454 | + | defer imageScheduler.Stop() | |
| 1455 | + | } | |
| 1456 | + | ||
| 1437 | 1457 | // Auto-start after servers are listening (registry port is now bound). | |
| 1438 | 1458 | syncAndAutoStart(ctx, svc, log) | |
| 1439 | 1459 | ||
| @@ -1442,6 +1462,66 @@ func runServers(ctx context.Context, cfg Config, registryHandler, proxyHandler h | |||
| 1442 | 1462 | return nil | |
| 1443 | 1463 | } | |
| 1444 | 1464 | ||
| 1465 | + | func startImagePruneScheduler(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger) (*cronSvc.Scheduler, error) { | |
| 1466 | + | if !cfg.Images.Prune.Enabled || svc == nil || svc.imageSvc == nil { | |
| 1467 | + | return nil, nil | |
| 1468 | + | } | |
| 1469 | + | if cfg.Images.Prune.KeepLast < 0 { | |
| 1470 | + | return nil, fmt.Errorf("images.prune.keep_last must be >= 0") | |
| 1471 | + | } | |
| 1472 | + | ||
| 1473 | + | preset, err := resolveImagePruneSchedule(cfg.Images.Prune.Schedule) | |
| 1474 | + | if err != nil { | |
| 1475 | + | return nil, err | |
| 1476 | + | } | |
| 1477 | + | ||
| 1478 | + | scheduler := cronSvc.NewScheduler(log) | |
| 1479 | + | err = scheduler.Add( | |
| 1480 | + | "image-prune", | |
| 1481 | + | "Image prune", | |
| 1482 | + | domain.CronSchedule{Preset: preset}, | |
| 1483 | + | func(jobCtx context.Context) error { | |
| 1484 | + | report, err := svc.imageSvc.Prune(jobCtx, cfg.Images.Prune.KeepLast) | |
| 1485 | + | if err != nil { | |
| 1486 | + | return err | |
| 1487 | + | } | |
| 1488 | + | ||
| 1489 | + | log.Info(). | |
| 1490 | + | Int("runtime_deleted", report.Runtime.DeletedCount). | |
| 1491 | + | Int64("runtime_reclaimed_bytes", report.Runtime.SpaceReclaimed). | |
| 1492 | + | Int("registry_tags_removed", report.Registry.TagsRemoved). | |
| 1493 | + | Int("registry_blobs_removed", report.Registry.BlobsRemoved). | |
| 1494 | + | Msg("scheduled image prune complete") | |
| 1495 | + | return nil | |
| 1496 | + | }, | |
| 1497 | + | ) | |
| 1498 | + | if err != nil { | |
| 1499 | + | return nil, log.WrapErr(err, "failed to register image prune schedule") | |
| 1500 | + | } | |
| 1501 | + | ||
| 1502 | + | scheduler.Start(ctx) | |
| 1503 | + | log.Info(). | |
| 1504 | + | Str("schedule", string(preset)). | |
| 1505 | + | Int("keep_last", cfg.Images.Prune.KeepLast). | |
| 1506 | + | Msg("image prune scheduler enabled") | |
| 1507 | + | ||
| 1508 | + | return scheduler, nil | |
| 1509 | + | } | |
| 1510 | + | ||
| 1511 | + | func resolveImagePruneSchedule(raw string) (domain.BackupSchedule, error) { | |
| 1512 | + | schedule := domain.BackupSchedule(strings.ToLower(strings.TrimSpace(raw))) | |
| 1513 | + | if schedule == "" { | |
| 1514 | + | schedule = domain.ScheduleDaily | |
| 1515 | + | } | |
| 1516 | + | ||
| 1517 | + | switch schedule { | |
| 1518 | + | case domain.ScheduleHourly, domain.ScheduleDaily, domain.ScheduleWeekly, domain.ScheduleMonthly: | |
| 1519 | + | return schedule, nil | |
| 1520 | + | default: | |
| 1521 | + | return "", fmt.Errorf("images.prune.schedule must be one of: hourly, daily, weekly, monthly") | |
| 1522 | + | } | |
| 1523 | + | } | |
| 1524 | + | ||
| 1445 | 1525 | // waitForShutdown blocks on the event loop, handling server errors and | |
| 1446 | 1526 | // Unix signals (reload, deploy, shutdown) until the context is cancelled. | |
| 1447 | 1527 | func waitForShutdown(ctx context.Context, errChan <-chan error, reloadChan, deployChan <-chan os.Signal, eventBus out.EventBus, log zerowrap.Logger) { | |
| @@ -1861,6 +1941,9 @@ func loadConfig(v *viper.Viper, configPath string) error { | |||
| 1861 | 1941 | v.SetDefault("backups.retention.daily", 0) | |
| 1862 | 1942 | v.SetDefault("backups.retention.weekly", 0) | |
| 1863 | 1943 | v.SetDefault("backups.retention.monthly", 0) | |
| 1944 | + | v.SetDefault("images.prune.enabled", false) | |
| 1945 | + | v.SetDefault("images.prune.schedule", string(domain.ScheduleDaily)) | |
| 1946 | + | v.SetDefault("images.prune.keep_last", 3) | |
| 1864 | 1947 | v.SetDefault("server.max_concurrent_connections", -1) // -1 = use default (10000), 0 = no limit | |
| 1865 | 1948 | v.SetDefault("server.registry_allowed_ips", []string{}) | |
| 1866 | 1949 | v.SetDefault("deploy.readiness_delay", "5s") | |
| @@ -57,7 +57,9 @@ import ( | |||||
| 57 | "github.com/bnema/gordon/internal/usecase/backup" | 57 | "github.com/bnema/gordon/internal/usecase/backup" | ||
| 58 | "github.com/bnema/gordon/internal/usecase/config" | 58 | "github.com/bnema/gordon/internal/usecase/config" | ||
| 59 | "github.com/bnema/gordon/internal/usecase/container" | 59 | "github.com/bnema/gordon/internal/usecase/container" | ||
| 60 | + | cronSvc "github.com/bnema/gordon/internal/usecase/cron" | |||
| 60 | "github.com/bnema/gordon/internal/usecase/health" | 61 | "github.com/bnema/gordon/internal/usecase/health" | ||
| 62 | + | "github.com/bnema/gordon/internal/usecase/images" | |||
| 61 | "github.com/bnema/gordon/internal/usecase/logs" | 63 | "github.com/bnema/gordon/internal/usecase/logs" | ||
| 62 | "github.com/bnema/gordon/internal/usecase/proxy" | 64 | "github.com/bnema/gordon/internal/usecase/proxy" | ||
| 63 | registrySvc "github.com/bnema/gordon/internal/usecase/registry" | 65 | registrySvc "github.com/bnema/gordon/internal/usecase/registry" | ||
| @@ -141,6 +143,14 @@ type Config struct { | |||||
| 141 | Monthly int `mapstructure:"monthly"` | 143 | Monthly int `mapstructure:"monthly"` | ||
| 142 | } `mapstructure:"retention"` | 144 | } `mapstructure:"retention"` | ||
| 143 | } `mapstructure:"backups"` | 145 | } `mapstructure:"backups"` | ||
| 146 | + | ||||
| 147 | + | Images struct { | |||
| 148 | + | Prune struct { | |||
| 149 | + | Enabled bool `mapstructure:"enabled"` | |||
| 150 | + | Schedule string `mapstructure:"schedule"` | |||
| 151 | + | KeepLast int `mapstructure:"keep_last"` | |||
| 152 | + | } `mapstructure:"prune"` | |||
| 153 | + | } `mapstructure:"images"` | |||
| 144 | } | 154 | } | ||
| 145 | 155 | ||||
| 146 | // services holds all the services used by the application. | 156 | // services holds all the services used by the application. | ||
| @@ -157,6 +167,7 @@ type services struct { | |||||
| 157 | containerSvc *container.Service | 167 | containerSvc *container.Service | ||
| 158 | backupSvc *backup.Service | 168 | backupSvc *backup.Service | ||
| 159 | registrySvc *registrySvc.Service | 169 | registrySvc *registrySvc.Service | ||
| 170 | + | imageSvc *images.Service | |||
| 160 | proxySvc *proxy.Service | 171 | proxySvc *proxy.Service | ||
| 161 | authSvc *auth.Service | 172 | authSvc *auth.Service | ||
| 162 | authHandler *authhandler.Handler | 173 | authHandler *authhandler.Handler | ||
| @@ -452,6 +463,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||||
| 452 | } | 463 | } | ||
| 453 | 464 | ||||
| 454 | svc.registrySvc = registrySvc.NewService(svc.blobStorage, svc.manifestStorage, svc.eventBus) | 465 | svc.registrySvc = registrySvc.NewService(svc.blobStorage, svc.manifestStorage, svc.eventBus) | ||
| 466 | + | svc.imageSvc = images.NewService(svc.runtime, svc.manifestStorage, svc.blobStorage, log) | |||
| 455 | 467 | ||||
| 456 | proxyCfg, err := buildProxyConfig(cfg, log) | 468 | proxyCfg, err := buildProxyConfig(cfg, log) | ||
| 457 | if err != nil { | 469 | if err != nil { | ||
| @@ -482,7 +494,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||||
| 482 | logSvc := logs.NewService(resolveLogFilePath(cfg), svc.containerSvc, svc.runtime, log) | 494 | logSvc := logs.NewService(resolveLogFilePath(cfg), svc.containerSvc, svc.runtime, log) | ||
| 483 | 495 | ||||
| 484 | // Create admin handler for admin API | 496 | // Create admin handler for admin API | ||
| 485 | - | svc.adminHandler = admin.NewHandler(svc.configSvc, svc.authSvc, svc.containerSvc, healthSvc, secretSvc, logSvc, svc.registrySvc, svc.eventBus, log, svc.backupSvc) | 497 | + | svc.adminHandler = admin.NewHandler(svc.configSvc, svc.authSvc, svc.containerSvc, healthSvc, secretSvc, logSvc, svc.registrySvc, svc.eventBus, log, svc.backupSvc, svc.imageSvc) |
| 486 | 498 | ||||
| 487 | return svc, nil | 499 | return svc, nil | ||
| 488 | } | 500 | } | ||
| @@ -1434,6 +1446,14 @@ func runServers(ctx context.Context, cfg Config, registryHandler, proxyHandler h | |||||
| 1434 | } | 1446 | } | ||
| 1435 | logEvent.Msg("Gordon is running") | 1447 | logEvent.Msg("Gordon is running") | ||
| 1436 | 1448 | ||||
| 1449 | + | imageScheduler, err := startImagePruneScheduler(ctx, cfg, svc, log) | |||
| 1450 | + | if err != nil { | |||
| 1451 | + | return err | |||
| 1452 | + | } | |||
| 1453 | + | if imageScheduler != nil { | |||
| 1454 | + | defer imageScheduler.Stop() | |||
| 1455 | + | } | |||
| 1456 | + | ||||
| 1437 | // Auto-start after servers are listening (registry port is now bound). | 1457 | // Auto-start after servers are listening (registry port is now bound). | ||
| 1438 | syncAndAutoStart(ctx, svc, log) | 1458 | syncAndAutoStart(ctx, svc, log) | ||
| 1439 | 1459 | ||||
| @@ -1442,6 +1462,66 @@ func runServers(ctx context.Context, cfg Config, registryHandler, proxyHandler h | |||||
| 1442 | return nil | 1462 | return nil | ||
| 1443 | } | 1463 | } | ||
| 1444 | 1464 | ||||
| 1465 | + | func startImagePruneScheduler(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger) (*cronSvc.Scheduler, error) { | |||
| 1466 | + | if !cfg.Images.Prune.Enabled || svc == nil || svc.imageSvc == nil { | |||
| 1467 | + | return nil, nil | |||
| 1468 | + | } | |||
| 1469 | + | if cfg.Images.Prune.KeepLast < 0 { | |||
| 1470 | + | return nil, fmt.Errorf("images.prune.keep_last must be >= 0") | |||
| 1471 | + | } | |||
| 1472 | + | ||||
| 1473 | + | preset, err := resolveImagePruneSchedule(cfg.Images.Prune.Schedule) | |||
| 1474 | + | if err != nil { | |||
| 1475 | + | return nil, err | |||
| 1476 | + | } | |||
| 1477 | + | ||||
| 1478 | + | scheduler := cronSvc.NewScheduler(log) | |||
| 1479 | + | err = scheduler.Add( | |||
| 1480 | + | "image-prune", | |||
| 1481 | + | "Image prune", | |||
| 1482 | + | domain.CronSchedule{Preset: preset}, | |||
| 1483 | + | func(jobCtx context.Context) error { | |||
| 1484 | + | report, err := svc.imageSvc.Prune(jobCtx, cfg.Images.Prune.KeepLast) | |||
| 1485 | + | if err != nil { | |||
| 1486 | + | return err | |||
| 1487 | + | } | |||
| 1488 | + | ||||
| 1489 | + | log.Info(). | |||
| 1490 | + | Int("runtime_deleted", report.Runtime.DeletedCount). | |||
| 1491 | + | Int64("runtime_reclaimed_bytes", report.Runtime.SpaceReclaimed). | |||
| 1492 | + | Int("registry_tags_removed", report.Registry.TagsRemoved). | |||
| 1493 | + | Int("registry_blobs_removed", report.Registry.BlobsRemoved). | |||
| 1494 | + | Msg("scheduled image prune complete") | |||
| 1495 | + | return nil | |||
| 1496 | + | }, | |||
| 1497 | + | ) | |||
| 1498 | + | if err != nil { | |||
| 1499 | + | return nil, log.WrapErr(err, "failed to register image prune schedule") | |||
| 1500 | + | } | |||
| 1501 | + | ||||
| 1502 | + | scheduler.Start(ctx) | |||
| 1503 | + | log.Info(). | |||
| 1504 | + | Str("schedule", string(preset)). | |||
| 1505 | + | Int("keep_last", cfg.Images.Prune.KeepLast). | |||
| 1506 | + | Msg("image prune scheduler enabled") | |||
| 1507 | + | ||||
| 1508 | + | return scheduler, nil | |||
| 1509 | + | } | |||
| 1510 | + | ||||
| 1511 | + | func resolveImagePruneSchedule(raw string) (domain.BackupSchedule, error) { | |||
| 1512 | + | schedule := domain.BackupSchedule(strings.ToLower(strings.TrimSpace(raw))) | |||
| 1513 | + | if schedule == "" { | |||
| 1514 | + | schedule = domain.ScheduleDaily | |||
| 1515 | + | } | |||
| 1516 | + | ||||
| 1517 | + | switch schedule { | |||
| 1518 | + | case domain.ScheduleHourly, domain.ScheduleDaily, domain.ScheduleWeekly, domain.ScheduleMonthly: | |||
| 1519 | + | return schedule, nil | |||
| 1520 | + | default: | |||
| 1521 | + | return "", fmt.Errorf("images.prune.schedule must be one of: hourly, daily, weekly, monthly") | |||
| 1522 | + | } | |||
| 1523 | + | } | |||
| 1524 | + | ||||
| 1445 | // waitForShutdown blocks on the event loop, handling server errors and | 1525 | // waitForShutdown blocks on the event loop, handling server errors and | ||
| 1446 | // Unix signals (reload, deploy, shutdown) until the context is cancelled. | 1526 | // Unix signals (reload, deploy, shutdown) until the context is cancelled. | ||
| 1447 | func waitForShutdown(ctx context.Context, errChan <-chan error, reloadChan, deployChan <-chan os.Signal, eventBus out.EventBus, log zerowrap.Logger) { | 1527 | func waitForShutdown(ctx context.Context, errChan <-chan error, reloadChan, deployChan <-chan os.Signal, eventBus out.EventBus, log zerowrap.Logger) { | ||
| @@ -1861,6 +1941,9 @@ func loadConfig(v *viper.Viper, configPath string) error { | |||||
| 1861 | v.SetDefault("backups.retention.daily", 0) | 1941 | v.SetDefault("backups.retention.daily", 0) | ||
| 1862 | v.SetDefault("backups.retention.weekly", 0) | 1942 | v.SetDefault("backups.retention.weekly", 0) | ||
| 1863 | v.SetDefault("backups.retention.monthly", 0) | 1943 | v.SetDefault("backups.retention.monthly", 0) | ||
| 1944 | + | v.SetDefault("images.prune.enabled", false) | |||
| 1945 | + | v.SetDefault("images.prune.schedule", string(domain.ScheduleDaily)) | |||
| 1946 | + | v.SetDefault("images.prune.keep_last", 3) | |||
| 1864 | v.SetDefault("server.max_concurrent_connections", -1) // -1 = use default (10000), 0 = no limit | 1947 | v.SetDefault("server.max_concurrent_connections", -1) // -1 = use default (10000), 0 = no limit | ||
| 1865 | v.SetDefault("server.registry_allowed_ips", []string{}) | 1948 | v.SetDefault("server.registry_allowed_ips", []string{}) | ||
| 1866 | v.SetDefault("deploy.readiness_delay", "5s") | 1949 | v.SetDefault("deploy.readiness_delay", "5s") | ||
A internal/app/run_images_test.go +50 -0
| @@ -0,0 +1,50 @@ | |||
| 1 | + | package app | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/spf13/viper" | |
| 7 | + | "github.com/stretchr/testify/assert" | |
| 8 | + | "github.com/stretchr/testify/require" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func TestLoadConfigSetsImagePruneDefaults(t *testing.T) { | |
| 14 | + | v := viper.New() | |
| 15 | + | ||
| 16 | + | require.NoError(t, loadConfig(v, "")) | |
| 17 | + | ||
| 18 | + | assert.False(t, v.GetBool("images.prune.enabled")) | |
| 19 | + | assert.Equal(t, string(domain.ScheduleDaily), v.GetString("images.prune.schedule")) | |
| 20 | + | assert.Equal(t, 3, v.GetInt("images.prune.keep_last")) | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func TestResolveImagePruneSchedule(t *testing.T) { | |
| 24 | + | t.Run("valid values", func(t *testing.T) { | |
| 25 | + | tests := []struct { | |
| 26 | + | input string | |
| 27 | + | want domain.BackupSchedule | |
| 28 | + | }{ | |
| 29 | + | {input: "hourly", want: domain.ScheduleHourly}, | |
| 30 | + | {input: "daily", want: domain.ScheduleDaily}, | |
| 31 | + | {input: "weekly", want: domain.ScheduleWeekly}, | |
| 32 | + | {input: "monthly", want: domain.ScheduleMonthly}, | |
| 33 | + | {input: " DAILY ", want: domain.ScheduleDaily}, | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | for _, tt := range tests { | |
| 37 | + | t.Run(tt.input, func(t *testing.T) { | |
| 38 | + | got, err := resolveImagePruneSchedule(tt.input) | |
| 39 | + | require.NoError(t, err) | |
| 40 | + | assert.Equal(t, tt.want, got) | |
| 41 | + | }) | |
| 42 | + | } | |
| 43 | + | }) | |
| 44 | + | ||
| 45 | + | t.Run("invalid value", func(t *testing.T) { | |
| 46 | + | _, err := resolveImagePruneSchedule("every-minute") | |
| 47 | + | require.Error(t, err) | |
| 48 | + | assert.Contains(t, err.Error(), "images.prune.schedule") | |
| 49 | + | }) | |
| 50 | + | } | |
| @@ -0,0 +1,50 @@ | |||||
| 1 | + | package app | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/spf13/viper" | |||
| 7 | + | "github.com/stretchr/testify/assert" | |||
| 8 | + | "github.com/stretchr/testify/require" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func TestLoadConfigSetsImagePruneDefaults(t *testing.T) { | |||
| 14 | + | v := viper.New() | |||
| 15 | + | ||||
| 16 | + | require.NoError(t, loadConfig(v, "")) | |||
| 17 | + | ||||
| 18 | + | assert.False(t, v.GetBool("images.prune.enabled")) | |||
| 19 | + | assert.Equal(t, string(domain.ScheduleDaily), v.GetString("images.prune.schedule")) | |||
| 20 | + | assert.Equal(t, 3, v.GetInt("images.prune.keep_last")) | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func TestResolveImagePruneSchedule(t *testing.T) { | |||
| 24 | + | t.Run("valid values", func(t *testing.T) { | |||
| 25 | + | tests := []struct { | |||
| 26 | + | input string | |||
| 27 | + | want domain.BackupSchedule | |||
| 28 | + | }{ | |||
| 29 | + | {input: "hourly", want: domain.ScheduleHourly}, | |||
| 30 | + | {input: "daily", want: domain.ScheduleDaily}, | |||
| 31 | + | {input: "weekly", want: domain.ScheduleWeekly}, | |||
| 32 | + | {input: "monthly", want: domain.ScheduleMonthly}, | |||
| 33 | + | {input: " DAILY ", want: domain.ScheduleDaily}, | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | for _, tt := range tests { | |||
| 37 | + | t.Run(tt.input, func(t *testing.T) { | |||
| 38 | + | got, err := resolveImagePruneSchedule(tt.input) | |||
| 39 | + | require.NoError(t, err) | |||
| 40 | + | assert.Equal(t, tt.want, got) | |||
| 41 | + | }) | |||
| 42 | + | } | |||
| 43 | + | }) | |||
| 44 | + | ||||
| 45 | + | t.Run("invalid value", func(t *testing.T) { | |||
| 46 | + | _, err := resolveImagePruneSchedule("every-minute") | |||
| 47 | + | require.Error(t, err) | |||
| 48 | + | assert.Contains(t, err.Error(), "images.prune.schedule") | |||
| 49 | + | }) | |||
| 50 | + | } | |||
M internal/usecase/images/service.go +7 -2
| @@ -17,15 +17,20 @@ import ( | |||
| 17 | 17 | ||
| 18 | 18 | // Service implements image list and prune operations. | |
| 19 | 19 | type Service struct { | |
| 20 | - | runtime runtime.Runtime | |
| 20 | + | runtime imageRuntime | |
| 21 | 21 | manifestStorage out.ManifestStorage | |
| 22 | 22 | blobStorage out.BlobStorage | |
| 23 | 23 | log zerowrap.Logger | |
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | + | type imageRuntime interface { | |
| 27 | + | ListImagesDetailed(ctx context.Context) ([]runtime.ImageDetail, error) | |
| 28 | + | PruneImages(ctx context.Context, danglingOnly bool) (runtime.PruneReport, error) | |
| 29 | + | } | |
| 30 | + | ||
| 26 | 31 | // NewService creates a new images service. | |
| 27 | 32 | func NewService( | |
| 28 | - | rt runtime.Runtime, | |
| 33 | + | rt imageRuntime, | |
| 29 | 34 | manifestStorage out.ManifestStorage, | |
| 30 | 35 | blobStorage out.BlobStorage, | |
| 31 | 36 | log zerowrap.Logger, | |
| @@ -17,15 +17,20 @@ import ( | |||||
| 17 | 17 | ||||
| 18 | // Service implements image list and prune operations. | 18 | // Service implements image list and prune operations. | ||
| 19 | type Service struct { | 19 | type Service struct { | ||
| 20 | - | runtime runtime.Runtime | 20 | + | runtime imageRuntime |
| 21 | manifestStorage out.ManifestStorage | 21 | manifestStorage out.ManifestStorage | ||
| 22 | blobStorage out.BlobStorage | 22 | blobStorage out.BlobStorage | ||
| 23 | log zerowrap.Logger | 23 | log zerowrap.Logger | ||
| 24 | } | 24 | } | ||
| 25 | 25 | ||||
| 26 | + | type imageRuntime interface { | |||
| 27 | + | ListImagesDetailed(ctx context.Context) ([]runtime.ImageDetail, error) | |||
| 28 | + | PruneImages(ctx context.Context, danglingOnly bool) (runtime.PruneReport, error) | |||
| 29 | + | } | |||
| 30 | + | ||||
| 26 | // NewService creates a new images service. | 31 | // NewService creates a new images service. | ||
| 27 | func NewService( | 32 | func NewService( | ||
| 28 | - | rt runtime.Runtime, | 33 | + | rt imageRuntime, |
| 29 | manifestStorage out.ManifestStorage, | 34 | manifestStorage out.ManifestStorage, | ||
| 30 | blobStorage out.BlobStorage, | 35 | blobStorage out.BlobStorage, | ||
| 31 | log zerowrap.Logger, | 36 | log zerowrap.Logger, | ||