Compare commits
No commits in common. "main" and "docker-healthcheck" have entirely different histories.
main
...
docker-hea
@ -4,7 +4,7 @@ name: test
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: test
|
- name: test
|
||||||
image: golang:1.21
|
image: golang:1.20
|
||||||
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@sha256:30fa17489b86d7a4c3ad9c3ce2e152c25d82b8671e5609d322c6cae0baed89cd
|
image: iamthefij/drone-pre-commit:personal
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
@ -32,7 +32,7 @@ trigger:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build all binaries
|
- name: build all binaries
|
||||||
image: golang:1.21
|
image: golang:1.17
|
||||||
environment:
|
environment:
|
||||||
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
|
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
|
||||||
commands:
|
commands:
|
||||||
@ -65,7 +65,7 @@ steps:
|
|||||||
- name: push images
|
- name: push images
|
||||||
image: thegeeklab/drone-docker-buildx
|
image: thegeeklab/drone-docker-buildx
|
||||||
settings:
|
settings:
|
||||||
repo: iamthefij/restic-scheduler
|
repo: iamthefij/resticscheduler
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
platforms:
|
platforms:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
|
@ -31,8 +31,10 @@ linters:
|
|||||||
- gocognit
|
- gocognit
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
|
# - gocyclo # Using cyclop
|
||||||
- godot
|
- godot
|
||||||
- gofumpt
|
# - goerr113 # Using errorlint
|
||||||
|
- gofmt
|
||||||
- goheader
|
- goheader
|
||||||
- goimports
|
- goimports
|
||||||
- gomnd
|
- gomnd
|
||||||
@ -42,6 +44,7 @@ linters:
|
|||||||
- gosec
|
- gosec
|
||||||
- grouper
|
- grouper
|
||||||
- importas
|
- importas
|
||||||
|
# - ireturn
|
||||||
- lll
|
- lll
|
||||||
- maintidx
|
- maintidx
|
||||||
- makezero
|
- makezero
|
||||||
@ -56,9 +59,11 @@ 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
|
||||||
@ -66,11 +71,27 @@ 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:
|
||||||
|
@ -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: v4.5.0
|
rev: v3.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
@ -11,8 +11,10 @@ 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.5.1
|
rev: v0.4.0
|
||||||
hooks:
|
hooks:
|
||||||
|
- id: go-fmt
|
||||||
|
- id: go-imports
|
||||||
- id: golangci-lint
|
- id: golangci-lint
|
||||||
args:
|
args:
|
||||||
- --timeout=3m
|
- --timeout=3m
|
||||||
|
@ -11,13 +11,13 @@ RUN apk add --no-cache \
|
|||||||
redis~=7 \
|
redis~=7 \
|
||||||
restic~=0.15 \
|
restic~=0.15 \
|
||||||
sqlite~=3 \
|
sqlite~=3 \
|
||||||
tzdata~=2024 \
|
tzdata~=2023c \
|
||||||
;
|
;
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
COPY ./dist/restic-scheduler-$TARGETOS-$TARGETARCH /bin/restic-scheduler
|
COPY ./dist/resticscheduler-$TARGETOS-$TARGETARCH /bin/resticscheduler
|
||||||
|
|
||||||
HEALTHCHECK CMD ["wget", "-O", "-", "http://localhost:8080/health"]
|
HEALTHCHECK CMD ["wget", "-O", "-", "http://localhost:8080/health"]
|
||||||
|
|
||||||
ENTRYPOINT [ "/bin/restic-scheduler" ]
|
ENTRYPOINT [ "/bin/resticscheduler" ]
|
||||||
|
7
Makefile
7
Makefile
@ -1,10 +1,11 @@
|
|||||||
APP_NAME = restic-scheduler
|
APP_NAME = resticscheduler
|
||||||
VERSION ?= $(shell git describe --tags --dirty)
|
VERSION ?= $(shell git describe --tags --dirty)
|
||||||
GOFILES = *.go
|
GOFILES = *.go
|
||||||
# Multi-arch targets are generated from this
|
# Multi-arch targets are generated from this
|
||||||
TARGET_ALIAS = $(APP_NAME)-linux-amd64 $(APP_NAME)-linux-arm $(APP_NAME)-linux-arm64
|
TARGET_ALIAS = $(APP_NAME)-linux-amd64 $(APP_NAME)-linux-arm $(APP_NAME)-linux-arm64
|
||||||
TARGETS = $(addprefix dist/,$(TARGET_ALIAS))
|
TARGETS = $(addprefix dist/,$(TARGET_ALIAS))
|
||||||
CURRENT_GOARCH = $(shell go env GOARCH)
|
.QUOTE = "
|
||||||
|
CURRENT_GOARCH = $(shell go env | awk -F "=" '/GOARCH/ { gsub(/$(.QUOTE)/,"", $$2); print $$2}')
|
||||||
|
|
||||||
# Default make target will run tests
|
# Default make target will run tests
|
||||||
.DEFAULT_GOAL = test
|
.DEFAULT_GOAL = test
|
||||||
@ -54,7 +55,7 @@ clean:
|
|||||||
## Multi-arch targets
|
## Multi-arch targets
|
||||||
$(TARGETS): $(GOFILES)
|
$(TARGETS): $(GOFILES)
|
||||||
mkdir -p ./dist
|
mkdir -p ./dist
|
||||||
GOOS=$(word 3, $(subst -, ,$(@))) GOARCH=$(word 4, $(subst -, ,$(@))) CGO_ENABLED=0 \
|
GOOS=$(word 2, $(subst -, ,$(@))) GOARCH=$(word 3, $(subst -, ,$(@))) CGO_ENABLED=0 \
|
||||||
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
|
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
|
||||||
-o $@
|
-o $@
|
||||||
|
|
||||||
|
210
README.md
210
README.md
@ -1,211 +1,3 @@
|
|||||||
# [restic-scheduler](/iamthefij/restic-scheduler)
|
# [restic-scheduler](/iamthefij/restic-scheduler)
|
||||||
|
|
||||||
## About
|
Job scheduler for Restic backups
|
||||||
|
|
||||||
`restic-scheduler` is a tool designed to allow declarative scheduling of restic backups using HCL (HashiCorp Configuration Language). This tool simplifies the process of managing and automating backups by defining jobs in a configuration file.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
You can install `restic-scheduler` using the following command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go install git.iamthefij.com/iamthefij/restic-scheduler@latest
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also download the latest release from the [releases page](https://git.iamthefij.com/iamthefij/restic-scheduler/releases).
|
|
||||||
|
|
||||||
Finally, if you prefer to use Docker, you can run something like the following command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker run -v /path/to/config:/config -v /path/to/data:/data iamthefij/restic-scheduler -config /config/jobs.hcl
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
If you're not using Docker, you'll need to ensure that `restic` is installed and available in your system's PATH. You can download and install restic from [here](https://restic.net/).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Command Line Interface
|
|
||||||
|
|
||||||
The `restic-scheduler` command line interface provides several options for managing backup, restore, and unlock jobs. Below are some examples of how to use this tool.
|
|
||||||
|
|
||||||
#### Display Version
|
|
||||||
|
|
||||||
To display the version of `restic-scheduler`, use the `-version` flag:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
restic-scheduler -version
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Run Backup Jobs
|
|
||||||
|
|
||||||
To run backup jobs, use the `-backup` flag followed by a comma-separated list of job names. Use `all` to run all backup jobs:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
restic-scheduler -backup job1,job2
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Run Restore Jobs
|
|
||||||
|
|
||||||
To run restore jobs, use the `-restore` flag followed by a comma-separated list of job names. Use `all` to run all restore jobs:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
restic-scheduler -restore job1,job2
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Unlock Job Repositories
|
|
||||||
|
|
||||||
To unlock job repositories, use the `-unlock` flag followed by a comma-separated list of job names. Use `all` to unlock all job repositories:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
restic-scheduler -unlock job1,job2
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Run Jobs Once and Exit
|
|
||||||
|
|
||||||
To run specified backup and restore jobs once and exit, use the `-once` flag:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
restic-scheduler -backup job1 -restore job2 -once
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Health Check and metrics API
|
|
||||||
|
|
||||||
To bind the health check and Prometheus metrics API to a specific address, use the `-addr` flag:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
restic-scheduler -addr 0.0.0.0:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Metrics Push Gateway
|
|
||||||
|
|
||||||
To specify the URL of a Prometheus push gateway service for batch runs, use the `-push-gateway` flag:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
restic-scheduler -push-gateway http://example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## HCL Configuration
|
|
||||||
|
|
||||||
The configuration for `restic-scheduler` is defined using HCL. Below is a description and example of how to define a backup job in the configuration file.
|
|
||||||
|
|
||||||
### Job Configuration
|
|
||||||
|
|
||||||
A job in the configuration file is defined using the `job` block. Each job must have a unique name, a schedule, and a configuration for restic. Additionally, tasks can be defined to perform specific actions before and after the backup.
|
|
||||||
|
|
||||||
#### Fields
|
|
||||||
|
|
||||||
- `name`: The name of the job.
|
|
||||||
- `schedule`: The cron schedule for the job.
|
|
||||||
- `config`: The restic configuration block.
|
|
||||||
- `repo`: The restic repository.
|
|
||||||
- `passphrase`: (Optional) The passphrase for the repository.
|
|
||||||
- `env`: (Optional) Environment variables for restic.
|
|
||||||
- `options`: (Optional) Global options for restic. See the `restic` command for details.
|
|
||||||
- `task`: (Optional) A list of tasks to run before and after the backup.
|
|
||||||
- `mysql`, `postgres`, `sqlite`: (Optional) Database-specific tasks.
|
|
||||||
- `backup`: The backup configuration block.
|
|
||||||
- `forget`: (Optional) Options for forgetting old snapshots.
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
Below is an example of a job configuration in HCL:
|
|
||||||
|
|
||||||
```hcl
|
|
||||||
// Example job file
|
|
||||||
job "MyApp" {
|
|
||||||
schedule = "* * * * *"
|
|
||||||
|
|
||||||
config {
|
|
||||||
repo = "s3://..."
|
|
||||||
passphrase = "foo"
|
|
||||||
# Some alternate ways to pass the passphrase to restic
|
|
||||||
# passphrase = env("RESTIC_PASSWORD")
|
|
||||||
# passphrase = readfile("/path/to/passphrase")
|
|
||||||
env = {
|
|
||||||
"foo" = "bar",
|
|
||||||
}
|
|
||||||
options {
|
|
||||||
VerboseLevel = 3
|
|
||||||
# Another alternate way to pass the passphrase to restic
|
|
||||||
# PasswordFile = "/path/to/passphrase"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mysql "DumpMainDB" {
|
|
||||||
hostname = "foo"
|
|
||||||
username = "bar"
|
|
||||||
dump_to = "/data/main.sql"
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlite "DumpSqlite" {
|
|
||||||
path = "/db/sqlite.db"
|
|
||||||
dump_to = "/data/sqlite.db.bak"
|
|
||||||
}
|
|
||||||
|
|
||||||
task "Create biz file" {
|
|
||||||
|
|
||||||
pre_script {
|
|
||||||
on_backup = <<EOF
|
|
||||||
echo bar >> /biz.txt
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
post_script {
|
|
||||||
on_backup = <<EOF
|
|
||||||
rm /biz.txt
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task "Run restore shell script" {
|
|
||||||
pre_script {
|
|
||||||
on_restore = "/foo/bar.sh"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
backup {
|
|
||||||
files =[
|
|
||||||
"/data",
|
|
||||||
"/biz.txt",
|
|
||||||
]
|
|
||||||
|
|
||||||
backup_opts {
|
|
||||||
Tags = ["service"]
|
|
||||||
}
|
|
||||||
|
|
||||||
restore_opts {
|
|
||||||
Verify = true
|
|
||||||
# Since paths are absolute, restore to root
|
|
||||||
Target = "/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
forget {
|
|
||||||
KeepLast = 3
|
|
||||||
KeepWeekly = 2
|
|
||||||
KeepMonthly = 2
|
|
||||||
KeepYearly = 2
|
|
||||||
Prune = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
restic-scheduler jobs.hcl
|
|
||||||
```
|
|
||||||
|
|
||||||
This will read the job definitions from `jobs.hcl` and execute the specified jobs.
|
|
||||||
|
|
||||||
For more examples, check out `./config.hcl` or some of the example integration test configs in `./test/`.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Please open an issue or submit a pull request on the [GitHub repository](https://git.iamthefij.com/iamthefij/restic-scheduler).
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
||||||
|
@ -15,7 +15,7 @@ services:
|
|||||||
POSTGRES_DB: main
|
POSTGRES_DB: main
|
||||||
|
|
||||||
bootstrap:
|
bootstrap:
|
||||||
image: restic-scheduler
|
image: resticscheduler
|
||||||
entrypoint: /bootstrap-tests.sh
|
entrypoint: /bootstrap-tests.sh
|
||||||
environment:
|
environment:
|
||||||
MYSQL_HOST: mysql
|
MYSQL_HOST: mysql
|
||||||
@ -29,7 +29,7 @@ services:
|
|||||||
- ./data:/data
|
- ./data:/data
|
||||||
|
|
||||||
main:
|
main:
|
||||||
image: restic-scheduler
|
image: resticscheduler
|
||||||
environment:
|
environment:
|
||||||
MYSQL_HOST: mysql
|
MYSQL_HOST: mysql
|
||||||
MYSQL_USER: root
|
MYSQL_USER: root
|
||||||
@ -43,7 +43,7 @@ services:
|
|||||||
- ./test-backup.hcl:/test-backup.hcl
|
- ./test-backup.hcl:/test-backup.hcl
|
||||||
|
|
||||||
validate:
|
validate:
|
||||||
image: restic-scheduler
|
image: resticscheduler
|
||||||
entrypoint: /validate-tests.sh
|
entrypoint: /validate-tests.sh
|
||||||
environment:
|
environment:
|
||||||
MYSQL_HOST: mysql
|
MYSQL_HOST: mysql
|
||||||
|
6
job.go
6
job.go
@ -280,10 +280,8 @@ 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)))
|
||||||
if len(snapshots) > 0 {
|
latestSnapshot := snapshots[len(snapshots)-1]
|
||||||
latestSnapshot := snapshots[len(snapshots)-1]
|
Metrics.SnapshotLatestTime.WithLabelValues(j.Name).Set(float64(latestSnapshot.Time.Unix()))
|
||||||
Metrics.SnapshotLatestTime.WithLabelValues(j.Name).Set(float64(latestSnapshot.Time.Unix()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Success {
|
if result.Success {
|
||||||
|
17
job_test.go
17
job_test.go
@ -148,9 +148,7 @@ 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{},
|
||||||
@ -168,9 +166,7 @@ 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{},
|
||||||
},
|
},
|
||||||
@ -187,9 +183,7 @@ 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,
|
||||||
},
|
},
|
||||||
@ -228,7 +222,6 @@ 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{},
|
||||||
@ -246,7 +239,6 @@ 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{},
|
||||||
@ -294,8 +286,7 @@ 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
50
main.go
@ -29,12 +29,8 @@ 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),
|
||||||
@ -44,12 +40,8 @@ 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),
|
||||||
@ -189,32 +181,10 @@ 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
|
||||||
@ -226,7 +196,6 @@ 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)")
|
||||||
@ -237,12 +206,7 @@ func readFlags() Flags {
|
|||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSpecifiedJobs(jobs []Job, backupJobs, restoreJobs, unlockJobs, snapshot string) error {
|
func runSpecifiedJobs(jobs []Job, backupJobs, restoreJobs, 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)
|
||||||
@ -258,8 +222,6 @@ func runSpecifiedJobs(jobs []Job, backupJobs, restoreJobs, unlockJobs, snapshot
|
|||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -291,7 +253,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.unlock, flags.restoreSnapshot); err != nil {
|
if err := runSpecifiedJobs(jobs, flags.backup, flags.restore, flags.restoreSnapshot); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
51
restic.go
51
restic.go
@ -11,10 +11,18 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ErrRestic = errors.New("restic error")
|
||||||
ErrRestic = errors.New("restic error")
|
var ErrRepoNotFound = errors.New("repository not found or uninitialized")
|
||||||
ErrRepoNotFound = errors.Join(errors.New("repository not found or uninitialized"), ErrRestic)
|
|
||||||
)
|
func lineIn(needle string, haystack []string) bool {
|
||||||
|
for _, line := range haystack {
|
||||||
|
if line == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func maybeAddArgString(args []string, name, value string) []string {
|
func maybeAddArgString(args []string, name, value string) []string {
|
||||||
if value != "" {
|
if value != "" {
|
||||||
@ -48,41 +56,22 @@ func maybeAddArgsList(args []string, name string, value []string) []string {
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandOptions interface dictates a ToArgs() method should return each commandline arg as a string slice.
|
|
||||||
type CommandOptions interface {
|
type CommandOptions interface {
|
||||||
// ToArgs returns the structs arguments as a slice of strings.
|
|
||||||
ToArgs() []string
|
ToArgs() []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenericOpts allows passing an arbitrary string slice as a set of command line options compatible with CommandOptions.
|
|
||||||
type GenericOpts []string
|
type GenericOpts []string
|
||||||
|
|
||||||
// ToArgs returns the structs arguments as a slice of strings.
|
|
||||||
func (o GenericOpts) ToArgs() []string {
|
func (o GenericOpts) ToArgs() []string {
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoOpts is a struct that fulfils the CommandOptions interface but provides no arguments.
|
|
||||||
type NoOpts struct{}
|
type NoOpts struct{}
|
||||||
|
|
||||||
// ToArgs returns the structs arguments as a slice of strings.
|
|
||||||
func (NoOpts) ToArgs() []string {
|
func (NoOpts) ToArgs() []string {
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnlockOpts holds optional arguments for unlock command.
|
|
||||||
type UnlockOpts struct {
|
|
||||||
RemoveAll bool `hcl:"RemoveAll,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToArgs returns the structs arguments as a slice of strings.
|
|
||||||
func (uo UnlockOpts) ToArgs() (args []string) {
|
|
||||||
args = maybeAddArgBool(args, "--remove-all", uo.RemoveAll)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackupOpts holds optional arguments for the Restic backup command.
|
|
||||||
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"`
|
||||||
@ -90,7 +79,6 @@ type BackupOpts struct {
|
|||||||
Host string `hcl:"Host,optional"`
|
Host string `hcl:"Host,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToArgs returns the structs arguments as a slice of strings.
|
|
||||||
func (bo BackupOpts) ToArgs() (args []string) {
|
func (bo BackupOpts) ToArgs() (args []string) {
|
||||||
args = maybeAddArgsList(args, "--exclude", bo.Exclude)
|
args = maybeAddArgsList(args, "--exclude", bo.Exclude)
|
||||||
args = maybeAddArgsList(args, "--include", bo.Include)
|
args = maybeAddArgsList(args, "--include", bo.Include)
|
||||||
@ -110,7 +98,6 @@ type RestoreOpts struct {
|
|||||||
Verify bool `hcl:"Verify,optional"`
|
Verify bool `hcl:"Verify,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToArgs returns the structs arguments as a slice of strings.
|
|
||||||
func (ro RestoreOpts) ToArgs() (args []string) {
|
func (ro RestoreOpts) ToArgs() (args []string) {
|
||||||
args = maybeAddArgsList(args, "--exclude", ro.Exclude)
|
args = maybeAddArgsList(args, "--exclude", ro.Exclude)
|
||||||
args = maybeAddArgsList(args, "--include", ro.Include)
|
args = maybeAddArgsList(args, "--include", ro.Include)
|
||||||
@ -150,7 +137,6 @@ type ForgetOpts struct {
|
|||||||
Prune bool `hcl:"Prune,optional"`
|
Prune bool `hcl:"Prune,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToArgs returns the structs arguments as a slice of strings.
|
|
||||||
func (fo ForgetOpts) ToArgs() (args []string) {
|
func (fo ForgetOpts) ToArgs() (args []string) {
|
||||||
args = maybeAddArgInt(args, "--keep-last", fo.KeepLast)
|
args = maybeAddArgInt(args, "--keep-last", fo.KeepLast)
|
||||||
args = maybeAddArgInt(args, "--keep-hourly", fo.KeepHourly)
|
args = maybeAddArgInt(args, "--keep-hourly", fo.KeepHourly)
|
||||||
@ -207,15 +193,13 @@ 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"`
|
||||||
VerboseLevel int `hcl:"VerboseLevel,optional"`
|
|
||||||
Options map[string]string `hcl:"Options,optional"`
|
Options map[string]string `hcl:"Options,optional"`
|
||||||
|
VerboseLevel int `hcl:"VerboseLevel,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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToArgs returns the structs arguments as a slice of strings.
|
|
||||||
func (glo ResticGlobalOpts) ToArgs() (args []string) {
|
func (glo ResticGlobalOpts) ToArgs() (args []string) {
|
||||||
args = maybeAddArgString(args, "--cacert", glo.CaCertFile)
|
args = maybeAddArgString(args, "--cacert", glo.CaCertFile)
|
||||||
args = maybeAddArgString(args, "--cache-dir", glo.CacheDir)
|
args = maybeAddArgString(args, "--cache-dir", glo.CacheDir)
|
||||||
@ -225,7 +209,6 @@ 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)
|
||||||
|
|
||||||
@ -318,7 +301,7 @@ func (rcmd Restic) RunRestic(
|
|||||||
responseErr = ErrRepoNotFound
|
responseErr = ErrRepoNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return output, NewResticError(command, output.AllLines(), errors.Join(err, responseErr))
|
return output, NewResticError(command, output.AllLines(), responseErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return output, nil
|
return output, nil
|
||||||
@ -348,12 +331,6 @@ 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"`
|
||||||
|
@ -32,7 +32,6 @@ 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{
|
||||||
@ -49,7 +48,6 @@ 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'",
|
||||||
@ -152,20 +150,6 @@ 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()
|
||||||
|
|
||||||
@ -237,7 +221,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"), 0o644)
|
err := os.WriteFile(dataFile, []byte("testing"), 0644)
|
||||||
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
|
||||||
@ -297,7 +281,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"), 0o644)
|
err = os.WriteFile(dataFile, []byte("unexpected"), 0644)
|
||||||
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
|
||||||
@ -313,8 +297,4 @@ 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)
|
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,8 @@ import (
|
|||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var jobResultsLock = sync.Mutex{}
|
||||||
jobResultsLock = sync.Mutex{}
|
var jobResults = map[string]JobResult{}
|
||||||
jobResults = map[string]JobResult{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type JobResult struct {
|
type JobResult struct {
|
||||||
JobName string
|
JobName string
|
||||||
@ -115,8 +113,6 @@ 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
|
||||||
|
10
utils.go
10
utils.go
@ -2,16 +2,6 @@ package main
|
|||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
func lineIn(needle string, haystack []string) bool {
|
|
||||||
for _, line := range haystack {
|
|
||||||
if line == needle {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeEnvMap(parent, child map[string]string) map[string]string {
|
func MergeEnvMap(parent, child map[string]string) map[string]string {
|
||||||
result := map[string]string{}
|
result := map[string]string{}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user