Compare commits

..

14 Commits
v0.2.0 ... main

Author SHA1 Message Date
4b16dea34e Log when attempting to push metrics to gateway
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-01-23 12:26:10 -08:00
7b313b8f9b Move log line to the proper location
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-22 08:50:49 -08:00
bea338c27a Add log when tasks are stopped
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-22 08:49:46 -08:00
1d0d6b3fe6 More loosly pin tzdata
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-01-06 15:34:02 -08:00
985572d737 Pin pre-commit plugin to specific sha
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2024-01-06 15:12:27 -08:00
cff06cd1c6 Update linters and fumpt all files
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-01-06 15:10:29 -08:00
90cd0ec9e0 Update versions to fix golangci-lint on drone
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-06 14:55:44 -08:00
a0db27be1a Add ability to unlock repos that may have stale locks
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
Defaults to remove all locks, even non-stale
2024-01-06 14:29:14 -08:00
cddc290ee0 Fix exhaustive structs 2024-01-06 14:28:52 -08:00
d049228980 Fix index out of range when reading snapshots list
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is passing
2023-11-07 11:03:18 -08:00
390074e048 Add insecure-tls global option
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-11-06 15:02:07 -08:00
e0542a68e5 Add docker healthcheck checking default address
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-25 20:17:18 -07:00
4ddcea9f7d Bump to alpine 3.18 and bump versions
All checks were successful
continuous-integration/drone/push Build is passing
Also, more loosely pinning some dependencies with stable apis.
2023-10-25 20:16:26 -07:00
a2823e09ad Add tzdata
All checks were successful
continuous-integration/drone/push Build is passing
Allows setting container timezone using TZ env variable
2023-08-11 05:53:17 -07:00
12 changed files with 130 additions and 59 deletions

View File

@ -4,7 +4,7 @@ name: test
steps: steps:
- name: test - name: test
image: golang:1.20 image: golang:1.21
environment: environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}} VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands: commands:
@ -13,7 +13,7 @@ steps:
- make test - make test
- name: check - name: check
image: iamthefij/drone-pre-commit:personal image: iamthefij/drone-pre-commit@sha256:30fa17489b86d7a4c3ad9c3ce2e152c25d82b8671e5609d322c6cae0baed89cd
--- ---
kind: pipeline kind: pipeline
@ -32,7 +32,7 @@ trigger:
steps: steps:
- name: build all binaries - name: build all binaries
image: golang:1.17 image: golang:1.21
environment: environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}} VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands: commands:

View File

@ -31,10 +31,8 @@ linters:
- gocognit - gocognit
- goconst - goconst
- gocritic - gocritic
# - gocyclo # Using cyclop
- godot - godot
# - goerr113 # Using errorlint - gofumpt
- gofmt
- goheader - goheader
- goimports - goimports
- gomnd - gomnd
@ -44,7 +42,6 @@ linters:
- gosec - gosec
- grouper - grouper
- importas - importas
# - ireturn
- lll - lll
- maintidx - maintidx
- makezero - makezero
@ -59,11 +56,9 @@ linters:
- paralleltest - paralleltest
- prealloc - prealloc
- predeclared - predeclared
# - promlinter # Not common enough
- revive - revive
- rowserrcheck - rowserrcheck
- sqlclosecheck - sqlclosecheck
# - stylecheck # Using revive
- tagliatelle - tagliatelle
- tenv - tenv
- testpackage - testpackage
@ -71,27 +66,11 @@ linters:
- tparallel - tparallel
- unconvert - unconvert
- unparam - unparam
- varnamelen
- wastedassign - wastedassign
- whitespace - whitespace
- wrapcheck - wrapcheck
- wsl - wsl
disable:
- gochecknoglobals
- godox
- forbidigo
# Deprecated
- golint
- interfacer
- maligned
- scopelint
- ifshort
- varcheck
- structcheck
- deadcode
- exhaustivestruct
linters-settings: linters-settings:
gomnd: gomnd:
settings: settings:

View File

@ -1,7 +1,7 @@
--- ---
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0 rev: v4.5.0
hooks: hooks:
- id: check-added-large-files - id: check-added-large-files
- id: check-yaml - id: check-yaml
@ -11,10 +11,8 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-merge-conflict - id: check-merge-conflict
- repo: https://github.com/dnephin/pre-commit-golang - repo: https://github.com/dnephin/pre-commit-golang
rev: v0.4.0 rev: v0.5.1
hooks: hooks:
- id: go-fmt
- id: go-imports
- id: golangci-lint - id: golangci-lint
args: args:
- --timeout=3m - --timeout=3m

View File

@ -1,20 +1,23 @@
FROM alpine:3.17 FROM alpine:3.18
RUN apk add --no-cache \ RUN apk add --no-cache \
bash~=5 \ bash~=5 \
consul~=1.14 \ consul~=1 \
mariadb-client~=10.6 \ mariadb-client~=10 \
mariadb-connector-c~=3.3 \ mariadb-connector-c~=3 \
nomad~=1.4 \ nomad~=1 \
postgresql15-client~=15.3 \ postgresql15-client~=15 \
rclone~=1.60 \ rclone~=1.62 \
redis~=7.0 \ redis~=7 \
restic~=0.14 \ restic~=0.15 \
sqlite~=3 \ sqlite~=3 \
tzdata~=2023 \
; ;
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
COPY ./dist/resticscheduler-$TARGETOS-$TARGETARCH /bin/resticscheduler COPY ./dist/resticscheduler-$TARGETOS-$TARGETARCH /bin/resticscheduler
HEALTHCHECK CMD ["wget", "-O", "-", "http://localhost:8080/health"]
ENTRYPOINT [ "/bin/resticscheduler" ] ENTRYPOINT [ "/bin/resticscheduler" ]

6
job.go
View File

@ -280,8 +280,10 @@ func (j Job) Run() {
result.LastError = err result.LastError = err
} else { } else {
Metrics.SnapshotCurrentCount.WithLabelValues(j.Name).Set(float64(len(snapshots))) Metrics.SnapshotCurrentCount.WithLabelValues(j.Name).Set(float64(len(snapshots)))
latestSnapshot := snapshots[len(snapshots)-1] if len(snapshots) > 0 {
Metrics.SnapshotLatestTime.WithLabelValues(j.Name).Set(float64(latestSnapshot.Time.Unix())) latestSnapshot := snapshots[len(snapshots)-1]
Metrics.SnapshotLatestTime.WithLabelValues(j.Name).Set(float64(latestSnapshot.Time.Unix()))
}
} }
if result.Success { if result.Success {

View File

@ -148,7 +148,9 @@ func TestJobValidation(t *testing.T) {
Name: "Test job", Name: "Test job",
Schedule: "@daily", Schedule: "@daily",
Config: ValidResticConfig(), Config: ValidResticConfig(),
Tasks: []main.JobTask{{}}, Tasks: []main.JobTask{
{}, //nolint:exhaustruct
},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil, Forget: nil,
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
@ -166,7 +168,9 @@ func TestJobValidation(t *testing.T) {
Tasks: []main.JobTask{}, Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil, Forget: nil,
MySQL: []main.JobTaskMySQL{{}}, MySQL: []main.JobTaskMySQL{
{}, //nolint:exhaustruct
},
Postgres: []main.JobTaskPostgres{}, Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{}, Sqlite: []main.JobTaskSqlite{},
}, },
@ -183,7 +187,9 @@ func TestJobValidation(t *testing.T) {
Forget: nil, Forget: nil,
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{}, Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{{}}, Sqlite: []main.JobTaskSqlite{
{}, //nolint:exhaustruct
},
}, },
expectedErr: main.ErrMissingField, expectedErr: main.ErrMissingField,
}, },
@ -222,6 +228,7 @@ func TestConfigValidation(t *testing.T) {
Config: ValidResticConfig(), Config: ValidResticConfig(),
Tasks: []main.JobTask{}, Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil,
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{}, Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{}, Sqlite: []main.JobTaskSqlite{},
@ -239,6 +246,7 @@ func TestConfigValidation(t *testing.T) {
Config: nil, Config: nil,
Tasks: []main.JobTask{}, Tasks: []main.JobTask{},
Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct Backup: main.BackupFilesTask{Paths: []string{"/test"}}, //nolint:exhaustruct
Forget: nil,
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{}, Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{}, Sqlite: []main.JobTaskSqlite{},
@ -286,7 +294,8 @@ func TestConfigValidation(t *testing.T) {
MySQL: []main.JobTaskMySQL{}, MySQL: []main.JobTaskMySQL{},
Postgres: []main.JobTaskPostgres{}, Postgres: []main.JobTaskPostgres{},
Sqlite: []main.JobTaskSqlite{}, Sqlite: []main.JobTaskSqlite{},
}}}, }},
},
expectedErr: main.ErrMissingField, expectedErr: main.ErrMissingField,
}, },
} }

50
main.go
View File

@ -29,8 +29,12 @@ func ParseConfig(path string) ([]Job, error) {
Functions: map[string]function.Function{ Functions: map[string]function.Function{
"env": function.New(&function.Spec{ "env": function.New(&function.Spec{
Params: []function.Parameter{{ Params: []function.Parameter{{
Name: "var", Name: "var",
Type: cty.String, Type: cty.String,
AllowNull: false,
AllowUnknown: false,
AllowDynamicType: false,
AllowMarked: false,
}}, }},
VarParam: nil, VarParam: nil,
Type: function.StaticReturnType(cty.String), Type: function.StaticReturnType(cty.String),
@ -40,8 +44,12 @@ func ParseConfig(path string) ([]Job, error) {
}), }),
"readfile": function.New(&function.Spec{ "readfile": function.New(&function.Spec{
Params: []function.Parameter{{ Params: []function.Parameter{{
Name: "path", Name: "path",
Type: cty.String, Type: cty.String,
AllowNull: false,
AllowUnknown: false,
AllowDynamicType: false,
AllowMarked: false,
}}, }},
VarParam: nil, VarParam: nil,
Type: function.StaticReturnType(cty.String), Type: function.StaticReturnType(cty.String),
@ -181,10 +189,32 @@ func runRestoreJobs(jobs []Job, names string, snapshot string) error {
return filterJobErr return filterJobErr
} }
func runUnlockJobs(jobs []Job, names string) error {
if names == "" {
return nil
}
namesSlice := strings.Split(names, ",")
if len(namesSlice) == 0 {
return nil
}
jobs, filterJobErr := FilterJobs(jobs, namesSlice)
for _, job := range jobs {
if err := job.NewRestic().Unlock(UnlockOpts{RemoveAll: true}); err != nil {
return err
}
}
return filterJobErr
}
type Flags struct { type Flags struct {
showVersion bool showVersion bool
backup string backup string
restore string restore string
unlock string
restoreSnapshot string restoreSnapshot string
once bool once bool
healthCheckAddr string healthCheckAddr string
@ -196,6 +226,7 @@ func readFlags() Flags {
flag.BoolVar(&flags.showVersion, "version", false, "Display the version and exit") flag.BoolVar(&flags.showVersion, "version", false, "Display the version and exit")
flag.StringVar(&flags.backup, "backup", "", "Run backup jobs now. Names are comma separated. `all` will run all.") flag.StringVar(&flags.backup, "backup", "", "Run backup jobs now. Names are comma separated. `all` will run all.")
flag.StringVar(&flags.restore, "restore", "", "Run restore jobs now. Names are comma separated. `all` will run all.") flag.StringVar(&flags.restore, "restore", "", "Run restore jobs now. Names are comma separated. `all` will run all.")
flag.StringVar(&flags.unlock, "unlock", "", "Unlock job repos now. Names are comma separated. `all` will run all.")
flag.BoolVar(&flags.once, "once", false, "Run jobs specified using -backup and -restore once and exit") flag.BoolVar(&flags.once, "once", false, "Run jobs specified using -backup and -restore once and exit")
flag.StringVar(&flags.healthCheckAddr, "addr", "0.0.0.0:8080", "address to bind health check API") flag.StringVar(&flags.healthCheckAddr, "addr", "0.0.0.0:8080", "address to bind health check API")
flag.StringVar(&flags.metricsPushGateway, "push-gateway", "", "url of push gateway service for batch runs (optional)") flag.StringVar(&flags.metricsPushGateway, "push-gateway", "", "url of push gateway service for batch runs (optional)")
@ -206,7 +237,12 @@ func readFlags() Flags {
return flags return flags
} }
func runSpecifiedJobs(jobs []Job, backupJobs, restoreJobs, snapshot string) error { func runSpecifiedJobs(jobs []Job, backupJobs, restoreJobs, unlockJobs, snapshot string) error {
// Run specified job unlocks
if err := runUnlockJobs(jobs, unlockJobs); err != nil {
return fmt.Errorf("Failed running unlock for jobs: %w", err)
}
// Run specified backup jobs // Run specified backup jobs
if err := runBackupJobs(jobs, backupJobs); err != nil { if err := runBackupJobs(jobs, backupJobs); err != nil {
return fmt.Errorf("Failed running backup jobs: %w", err) return fmt.Errorf("Failed running backup jobs: %w", err)
@ -222,6 +258,8 @@ func runSpecifiedJobs(jobs []Job, backupJobs, restoreJobs, snapshot string) erro
func maybePushMetrics(metricsPushGateway string) error { func maybePushMetrics(metricsPushGateway string) error {
if metricsPushGateway != "" { if metricsPushGateway != "" {
fmt.Println("Pushing metrics to push gateway")
if err := Metrics.PushToGateway(metricsPushGateway); err != nil { if err := Metrics.PushToGateway(metricsPushGateway); err != nil {
return fmt.Errorf("Failed pushing metrics after jobs run: %w", err) return fmt.Errorf("Failed pushing metrics after jobs run: %w", err)
} }
@ -253,7 +291,7 @@ func main() {
log.Fatalf("Failed to read jobs from files: %v", err) log.Fatalf("Failed to read jobs from files: %v", err)
} }
if err := runSpecifiedJobs(jobs, flags.backup, flags.restore, flags.restoreSnapshot); err != nil { if err := runSpecifiedJobs(jobs, flags.backup, flags.restore, flags.unlock, flags.restoreSnapshot); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -31,7 +31,6 @@ func TestReadJobs(t *testing.T) {
t.Parallel() t.Parallel()
jobs, err := main.ReadJobs([]string{"./test/sample.hcl"}) jobs, err := main.ReadJobs([]string{"./test/sample.hcl"})
if err != nil { if err != nil {
t.Errorf("Unexpected error reading jobs: %v", err) t.Errorf("Unexpected error reading jobs: %v", err)
} }

View File

@ -19,7 +19,6 @@ func (m ResticMetrics) PushToGateway(url string) error {
err := push.New(url, "batch"). err := push.New(url, "batch").
Gatherer(m.Registry). Gatherer(m.Registry).
Add() Add()
if err != nil { if err != nil {
return fmt.Errorf("error pushing to registry %s: %w", url, err) return fmt.Errorf("error pushing to registry %s: %w", url, err)
} }

View File

@ -11,8 +11,10 @@ import (
"time" "time"
) )
var ErrRestic = errors.New("restic error") var (
var ErrRepoNotFound = errors.New("repository not found or uninitialized") ErrRestic = errors.New("restic error")
ErrRepoNotFound = errors.New("repository not found or uninitialized")
)
func lineIn(needle string, haystack []string) bool { func lineIn(needle string, haystack []string) bool {
for _, line := range haystack { for _, line := range haystack {
@ -72,6 +74,16 @@ func (NoOpts) ToArgs() []string {
return []string{} return []string{}
} }
type UnlockOpts struct {
RemoveAll bool `hcl:"RemoveAll,optional"`
}
func (uo UnlockOpts) ToArgs() (args []string) {
args = maybeAddArgBool(args, "--remove-all", uo.RemoveAll)
return
}
type BackupOpts struct { type BackupOpts struct {
Exclude []string `hcl:"Exclude,optional"` Exclude []string `hcl:"Exclude,optional"`
Include []string `hcl:"Include,optional"` Include []string `hcl:"Include,optional"`
@ -193,9 +205,10 @@ type ResticGlobalOpts struct {
TLSClientCertFile string `hcl:"TlsClientCertFile,optional"` TLSClientCertFile string `hcl:"TlsClientCertFile,optional"`
LimitDownload int `hcl:"LimitDownload,optional"` LimitDownload int `hcl:"LimitDownload,optional"`
LimitUpload int `hcl:"LimitUpload,optional"` LimitUpload int `hcl:"LimitUpload,optional"`
Options map[string]string `hcl:"Options,optional"`
VerboseLevel int `hcl:"VerboseLevel,optional"` VerboseLevel int `hcl:"VerboseLevel,optional"`
Options map[string]string `hcl:"Options,optional"`
CleanupCache bool `hcl:"CleanupCache,optional"` CleanupCache bool `hcl:"CleanupCache,optional"`
InsecureTLS bool `hcl:"InsecureTls,optional"`
NoCache bool `hcl:"NoCache,optional"` NoCache bool `hcl:"NoCache,optional"`
NoLock bool `hcl:"NoLock,optional"` NoLock bool `hcl:"NoLock,optional"`
} }
@ -209,6 +222,7 @@ func (glo ResticGlobalOpts) ToArgs() (args []string) {
args = maybeAddArgInt(args, "--limit-upload", glo.LimitUpload) args = maybeAddArgInt(args, "--limit-upload", glo.LimitUpload)
args = maybeAddArgInt(args, "--verbose", glo.VerboseLevel) args = maybeAddArgInt(args, "--verbose", glo.VerboseLevel)
args = maybeAddArgBool(args, "--cleanup-cache", glo.CleanupCache) args = maybeAddArgBool(args, "--cleanup-cache", glo.CleanupCache)
args = maybeAddArgBool(args, "--insecure-tls", glo.InsecureTLS)
args = maybeAddArgBool(args, "--no-cache", glo.NoCache) args = maybeAddArgBool(args, "--no-cache", glo.NoCache)
args = maybeAddArgBool(args, "--no-lock", glo.NoLock) args = maybeAddArgBool(args, "--no-lock", glo.NoLock)
@ -331,6 +345,12 @@ func (rcmd Restic) Check() error {
return err return err
} }
func (rcmd Restic) Unlock(unlockOpts UnlockOpts) error {
_, err := rcmd.RunRestic("unlock", unlockOpts)
return err
}
type Snapshot struct { type Snapshot struct {
UID int `json:"uid"` UID int `json:"uid"`
GID int `json:"gid"` GID int `json:"gid"`

View File

@ -32,6 +32,7 @@ func TestGlobalOptions(t *testing.T) {
LimitUpload: 1, LimitUpload: 1,
VerboseLevel: 1, VerboseLevel: 1,
CleanupCache: true, CleanupCache: true,
InsecureTLS: true,
NoCache: true, NoCache: true,
NoLock: true, NoLock: true,
Options: map[string]string{ Options: map[string]string{
@ -48,6 +49,7 @@ func TestGlobalOptions(t *testing.T) {
"--limit-upload", "1", "--limit-upload", "1",
"--verbose", "1", "--verbose", "1",
"--cleanup-cache", "--cleanup-cache",
"--insecure-tls",
"--no-cache", "--no-cache",
"--no-lock", "--no-lock",
"--option", "key='a long value'", "--option", "key='a long value'",
@ -150,6 +152,20 @@ func TestForgetOpts(t *testing.T) {
AssertEqual(t, "args didn't match", expected, args) AssertEqual(t, "args didn't match", expected, args)
} }
func TestUnlockOpts(t *testing.T) {
t.Parallel()
args := main.UnlockOpts{
RemoveAll: true,
}.ToArgs()
expected := []string{
"--remove-all",
}
AssertEqual(t, "args didn't match", expected, args)
}
func TestBuildEnv(t *testing.T) { func TestBuildEnv(t *testing.T) {
t.Parallel() t.Parallel()
@ -221,7 +237,7 @@ func TestResticInterface(t *testing.T) {
} }
// Write test file to the data dir // Write test file to the data dir
err := os.WriteFile(dataFile, []byte("testing"), 0644) err := os.WriteFile(dataFile, []byte("testing"), 0o644)
AssertEqualFail(t, "unexpected error writing to test file", nil, err) AssertEqualFail(t, "unexpected error writing to test file", nil, err)
// Make sure no existing repo is found // Make sure no existing repo is found
@ -281,7 +297,7 @@ func TestResticInterface(t *testing.T) {
AssertEqualFail(t, "unexpected error checking repo", nil, err) AssertEqualFail(t, "unexpected error checking repo", nil, err)
// Change the data file // Change the data file
err = os.WriteFile(dataFile, []byte("unexpected"), 0644) err = os.WriteFile(dataFile, []byte("unexpected"), 0o644)
AssertEqualFail(t, "unexpected error writing to test file", nil, err) AssertEqualFail(t, "unexpected error writing to test file", nil, err)
// Check that data wrote // Check that data wrote
@ -297,4 +313,8 @@ func TestResticInterface(t *testing.T) {
value, err = os.ReadFile(restoredDataFile) value, err = os.ReadFile(restoredDataFile)
AssertEqualFail(t, "unexpected error reading from test file", nil, err) AssertEqualFail(t, "unexpected error reading from test file", nil, err)
AssertEqualFail(t, "incorrect value in test file", "testing", string(value)) AssertEqualFail(t, "incorrect value in test file", "testing", string(value))
// Try to unlock the repo (repo shouldn't really be locked, but this should still run without error
err = restic.Unlock(main.UnlockOpts{}) //nolint:exhaustruct
AssertEqualFail(t, "unexpected error unlocking repo", nil, err)
} }

View File

@ -13,8 +13,10 @@ import (
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
) )
var jobResultsLock = sync.Mutex{} var (
var jobResults = map[string]JobResult{} jobResultsLock = sync.Mutex{}
jobResults = map[string]JobResult{}
)
type JobResult struct { type JobResult struct {
JobName string JobName string
@ -113,6 +115,8 @@ func ScheduleAndRunJobs(jobs []Job) error {
defer func() { defer func() {
ctx := scheduler.Stop() ctx := scheduler.Stop()
<-ctx.Done() <-ctx.Done()
fmt.Println("All jobs successfully stopped")
}() }()
return nil return nil