Compare commits

...

14 Commits
v0.2.0 ... main

Author SHA1 Message Date
IamTheFij 4b16dea34e Log when attempting to push metrics to gateway
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-01-23 12:26:10 -08:00
IamTheFij 7b313b8f9b Move log line to the proper location
continuous-integration/drone/push Build is passing Details
2024-01-22 08:50:49 -08:00
IamTheFij bea338c27a Add log when tasks are stopped
continuous-integration/drone/push Build is passing Details
2024-01-22 08:49:46 -08:00
IamTheFij 1d0d6b3fe6 More loosly pin tzdata
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-01-06 15:34:02 -08:00
IamTheFij 985572d737 Pin pre-commit plugin to specific sha
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is failing Details
2024-01-06 15:12:27 -08:00
IamTheFij cff06cd1c6 Update linters and fumpt all files
continuous-integration/drone/push Build was killed Details
2024-01-06 15:10:29 -08:00
IamTheFij 90cd0ec9e0 Update versions to fix golangci-lint on drone
continuous-integration/drone/push Build is failing Details
2024-01-06 14:55:44 -08:00
IamTheFij a0db27be1a Add ability to unlock repos that may have stale locks
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is failing Details
Defaults to remove all locks, even non-stale
2024-01-06 14:29:14 -08:00
IamTheFij cddc290ee0 Fix exhaustive structs 2024-01-06 14:28:52 -08:00
IamTheFij d049228980 Fix index out of range when reading snapshots list
continuous-integration/drone/push Build was killed Details
continuous-integration/drone/tag Build is passing Details
2023-11-07 11:03:18 -08:00
IamTheFij 390074e048 Add insecure-tls global option
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-11-06 15:02:07 -08:00
IamTheFij e0542a68e5 Add docker healthcheck checking default address
continuous-integration/drone/push Build is passing Details
2023-10-25 20:17:18 -07:00
IamTheFij 4ddcea9f7d Bump to alpine 3.18 and bump versions
continuous-integration/drone/push Build is passing Details
Also, more loosely pinning some dependencies with stable apis.
2023-10-25 20:16:26 -07:00
IamTheFij a2823e09ad Add tzdata
continuous-integration/drone/push Build is passing Details
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:
- name: test
image: golang:1.20
image: golang:1.21
environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands:
@ -13,7 +13,7 @@ steps:
- make test
- name: check
image: iamthefij/drone-pre-commit:personal
image: iamthefij/drone-pre-commit@sha256:30fa17489b86d7a4c3ad9c3ce2e152c25d82b8671e5609d322c6cae0baed89cd
---
kind: pipeline
@ -32,7 +32,7 @@ trigger:
steps:
- name: build all binaries
image: golang:1.17
image: golang:1.21
environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands:

View File

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

View File

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

View File

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

6
job.go
View File

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

View File

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

50
main.go
View File

@ -29,8 +29,12 @@ func ParseConfig(path string) ([]Job, error) {
Functions: map[string]function.Function{
"env": function.New(&function.Spec{
Params: []function.Parameter{{
Name: "var",
Type: cty.String,
Name: "var",
Type: cty.String,
AllowNull: false,
AllowUnknown: false,
AllowDynamicType: false,
AllowMarked: false,
}},
VarParam: nil,
Type: function.StaticReturnType(cty.String),
@ -40,8 +44,12 @@ func ParseConfig(path string) ([]Job, error) {
}),
"readfile": function.New(&function.Spec{
Params: []function.Parameter{{
Name: "path",
Type: cty.String,
Name: "path",
Type: cty.String,
AllowNull: false,
AllowUnknown: false,
AllowDynamicType: false,
AllowMarked: false,
}},
VarParam: nil,
Type: function.StaticReturnType(cty.String),
@ -181,10 +189,32 @@ func runRestoreJobs(jobs []Job, names string, snapshot string) error {
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 {
showVersion bool
backup string
restore string
unlock string
restoreSnapshot string
once bool
healthCheckAddr string
@ -196,6 +226,7 @@ func readFlags() Flags {
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.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.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)")
@ -206,7 +237,12 @@ func readFlags() 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
if err := runBackupJobs(jobs, backupJobs); err != nil {
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 {
if metricsPushGateway != "" {
fmt.Println("Pushing metrics to push gateway")
if err := Metrics.PushToGateway(metricsPushGateway); err != nil {
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)
}
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)
}

View File

@ -31,7 +31,6 @@ func TestReadJobs(t *testing.T) {
t.Parallel()
jobs, err := main.ReadJobs([]string{"./test/sample.hcl"})
if err != nil {
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").
Gatherer(m.Registry).
Add()
if err != nil {
return fmt.Errorf("error pushing to registry %s: %w", url, err)
}

View File

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

View File

@ -32,6 +32,7 @@ func TestGlobalOptions(t *testing.T) {
LimitUpload: 1,
VerboseLevel: 1,
CleanupCache: true,
InsecureTLS: true,
NoCache: true,
NoLock: true,
Options: map[string]string{
@ -48,6 +49,7 @@ func TestGlobalOptions(t *testing.T) {
"--limit-upload", "1",
"--verbose", "1",
"--cleanup-cache",
"--insecure-tls",
"--no-cache",
"--no-lock",
"--option", "key='a long value'",
@ -150,6 +152,20 @@ func TestForgetOpts(t *testing.T) {
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) {
t.Parallel()
@ -221,7 +237,7 @@ func TestResticInterface(t *testing.T) {
}
// 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)
// Make sure no existing repo is found
@ -281,7 +297,7 @@ func TestResticInterface(t *testing.T) {
AssertEqualFail(t, "unexpected error checking repo", nil, err)
// 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)
// Check that data wrote
@ -297,4 +313,8 @@ func TestResticInterface(t *testing.T) {
value, err = os.ReadFile(restoredDataFile)
AssertEqualFail(t, "unexpected error reading from test file", nil, err)
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"
)
var jobResultsLock = sync.Mutex{}
var jobResults = map[string]JobResult{}
var (
jobResultsLock = sync.Mutex{}
jobResults = map[string]JobResult{}
)
type JobResult struct {
JobName string
@ -113,6 +115,8 @@ func ScheduleAndRunJobs(jobs []Job) error {
defer func() {
ctx := scheduler.Stop()
<-ctx.Done()
fmt.Println("All jobs successfully stopped")
}()
return nil