v3.3.1 31f1eec

schedulers.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"fmt"
6	"strings"
7
8	"github.com/bnema/zerowrap"
9	"github.com/spf13/viper"
10
11	"github.com/bnema/gordon/internal/domain"
12	cronSvc "github.com/bnema/gordon/internal/usecase/cron"
13)
14
15func startOptionalSchedulers(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger, v *viper.Viper) (func(), error) {
16	schedulers := make([]*cronSvc.Scheduler, 0, 3)
17	stopAll := func() {
18		for i := len(schedulers) - 1; i >= 0; i-- {
19			schedulers[i].Stop()
20		}
21	}
22
23	backupScheduler, err := startBackupScheduler(ctx, cfg, svc, log)
24	if err != nil {
25		return nil, err
26	}
27	if backupScheduler != nil {
28		schedulers = append(schedulers, backupScheduler)
29	}
30
31	volumeBackupScheduler, err := startVolumeBackupScheduler(ctx, cfg, svc, log)
32	if err != nil {
33		stopAll()
34		return nil, err
35	}
36	if volumeBackupScheduler != nil {
37		schedulers = append(schedulers, volumeBackupScheduler)
38	}
39
40	imageScheduler, err := startImagePruneScheduler(ctx, cfg, svc, log, func() int {
41		return v.GetInt("images.prune.keep_last")
42	})
43	if err != nil {
44		stopAll()
45		return nil, err
46	}
47	if imageScheduler != nil {
48		schedulers = append(schedulers, imageScheduler)
49	}
50
51	if len(schedulers) == 0 {
52		return nil, nil
53	}
54
55	return stopAll, nil
56}
57
58func startBackupScheduler(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger) (*cronSvc.Scheduler, error) {
59	dbCfg := databaseBackupSettings(cfg)
60	if !dbCfg.Enabled || svc == nil || svc.backupSvc == nil {
61		return nil, nil
62	}
63
64	preset, err := resolveBackupSchedule(dbCfg.Schedule)
65	if err != nil {
66		return nil, err
67	}
68
69	scheduler := cronSvc.NewScheduler(log)
70	err = scheduler.Add(
71		"backup-scheduler",
72		"Backups",
73		domain.CronSchedule{Preset: preset},
74		func(jobCtx context.Context) error {
75			if err := svc.backupSvc.RunForSchedule(jobCtx, preset); err != nil {
76				return err
77			}
78			log.Info().
79				Str("schedule", string(preset)).
80				Msg("scheduled backup run complete")
81			return nil
82		},
83	)
84	if err != nil {
85		return nil, log.WrapErr(err, "failed to register backup schedule")
86	}
87
88	scheduler.Start(ctx)
89	log.Info().
90		Str("schedule", string(preset)).
91		Msg("backup scheduler enabled")
92
93	return scheduler, nil
94}
95
96func resolveBackupSchedule(raw string) (domain.BackupSchedule, error) {
97	return resolveSchedulePreset(raw, "backups.databases.schedule", domain.ScheduleDaily)
98}
99
100func startVolumeBackupScheduler(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger) (*cronSvc.Scheduler, error) {
101	if !cfg.Backups.Volumes.Enabled || svc == nil || svc.volumeBackupSvc == nil {
102		return nil, nil
103	}
104
105	volumeCfg, err := validateVolumeBackupConfig(cfg)
106	if err != nil {
107		return nil, err
108	}
109
110	scheduler := cronSvc.NewScheduler(log)
111	err = scheduler.Add(
112		"volume-backup-scheduler",
113		"Volume Backups",
114		domain.CronSchedule{Interval: volumeCfg.Interval},
115		func(jobCtx context.Context) error {
116			if err := svc.volumeBackupSvc.RunVolumeBackupsForSchedule(jobCtx, ""); err != nil {
117				return err
118			}
119			log.Info().
120				Dur("interval", volumeCfg.Interval).
121				Msg("scheduled volume backup run complete")
122			return nil
123		},
124	)
125	if err != nil {
126		return nil, log.WrapErr(err, "failed to register volume backup schedule")
127	}
128
129	scheduler.Start(ctx)
130	log.Info().
131		Dur("interval", volumeCfg.Interval).
132		Msg("volume backup scheduler enabled")
133
134	return scheduler, nil
135}
136
137func startImagePruneScheduler(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger, keepLastGetter func() int) (*cronSvc.Scheduler, error) {
138	if !cfg.Images.Prune.Enabled || svc == nil || svc.imageSvc == nil {
139		return nil, nil
140	}
141	// The scheduler runs the same use case and planner as manual
142	// execution. App presence is normal: apps exist on every real
143	// server, and prune decides per candidate.
144	if keepLastGetter == nil {
145		keepLastGetter = func() int { return cfg.Images.Prune.KeepLast }
146	}
147	if keepLastGetter() < 0 {
148		return nil, fmt.Errorf("images.prune.keep_last must be >= 0")
149	}
150
151	preset, err := resolveImagePruneSchedule(cfg.Images.Prune.Schedule)
152	if err != nil {
153		return nil, err
154	}
155
156	scheduler := cronSvc.NewScheduler(log)
157	err = scheduler.Add(
158		"image-prune",
159		"Image prune",
160		domain.CronSchedule{Preset: preset},
161		func(jobCtx context.Context) error {
162			keepLast := keepLastGetter()
163			if keepLast < 0 {
164				log.Warn().
165					Int("configured_keep_last", keepLast).
166					Int("fallback_keep_last", domain.DefaultImagePruneKeepLast).
167					Msg("invalid images.prune.keep_last; using default")
168				keepLast = domain.DefaultImagePruneKeepLast
169			}
170
171			report, err := svc.imageSvc.Prune(jobCtx, domain.ImagePruneOptions{
172				KeepLast:      keepLast,
173				PruneDangling: true,
174				PruneRegistry: true,
175			})
176			if err != nil {
177				return err
178			}
179
180			log.Info().
181				Int("keep_last", keepLast).
182				Int("eligible", report.Plan.CountByVerdict(domain.PruneVerdictEligible)).
183				Int("protected", report.Plan.CountByVerdict(domain.PruneVerdictProtected)).
184				Int("unknown", report.Plan.CountByVerdict(domain.PruneVerdictUnknown)).
185				Int("deleted", len(report.Plan.Deleted)).
186				Int("failures", len(report.Plan.Failures)).
187				Int("inventory_gaps", len(report.Plan.Gaps)).
188				Int("runtime_deleted", report.Runtime.DeletedCount).
189				Int("registry_tags_removed", report.Registry.TagsRemoved).
190				Int("registry_blobs_removed", report.Registry.BlobsRemoved).
191				Msg("scheduled image prune complete")
192			return nil
193		},
194	)
195	if err != nil {
196		return nil, log.WrapErr(err, "failed to register image prune schedule")
197	}
198
199	scheduler.Start(ctx)
200	log.Info().
201		Str("schedule", string(preset)).
202		Int("keep_last", keepLastGetter()).
203		Msg("image prune scheduler enabled")
204
205	return scheduler, nil
206}
207
208func resolveImagePruneSchedule(raw string) (domain.BackupSchedule, error) {
209	return resolveSchedulePreset(raw, "images.prune.schedule", domain.ScheduleDaily)
210}
211
212func resolveSchedulePreset(raw, name string, defaultVal domain.BackupSchedule) (domain.BackupSchedule, error) {
213	schedule := domain.BackupSchedule(strings.ToLower(strings.TrimSpace(raw)))
214	if schedule == "" {
215		schedule = defaultVal
216	}
217
218	switch schedule {
219	case domain.ScheduleHourly, domain.ScheduleDaily, domain.ScheduleWeekly, domain.ScheduleMonthly:
220		return schedule, nil
221	default:
222		return "", fmt.Errorf("%s must be one of: hourly, daily, weekly, monthly", name)
223	}
224}

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