Some basic functionality

This commit is contained in:
IamTheFij 2022-02-22 16:39:01 -08:00
parent c6201750a7
commit a1969b681a
8 changed files with 819 additions and 233 deletions

View File

@ -1,30 +1,73 @@
// Example job file
job "MyApp" {
schedule = "* * * * *"
config {
repo = "s3://..."
passphrase = "foo"
env = {
"foo" = "bar",
}
options {
VerboseLevel = 3
}
}
task "Dump mysql" {
mysql {
mysql "DumpMainDB" {
hostname = "foo"
username = "bar"
}
sqlite "DumpSqlite" {
path = "/db/path"
}
task "Create biz file" {
on_backup {
body = <<EOF
task "RunSomePreScripts" {
script {
on_backup = <<EOF
echo foo > /biz.txt
EOF
on_restore = "/foo/bar.sh"
}
script {
on_backup = <<EOF
echo bar >> /biz.txt
EOF
}
}
task "Backup data files" {
task "ActuallyBackupSomeStuff" {
backup {
files =[
"/foo/bar",
"/biz.txt",
]
backup_opts {
Tags = ["service"]
}
restore_opts {
Verify = true
}
}
}
task "RunSomePostScripts" {
script {
on_backup = <<EOF
rm /biz.txt
EOF
}
}
forget {
KeepLast = 3
KeepWeekly = 2
KeepMonthly = 2
KeepYearly = 2
Prune = true
}
}

283
job.go
View File

@ -1,40 +1,215 @@
package main
// JobConfig is all configuration to be sent to Restic
type JobConfig struct {
import (
"fmt"
"log"
"os"
"path/filepath"
)
const WorkDirPerms = 0o666
type TaskConfig struct {
JobDir string
Env map[string]string
Logger *log.Logger
Restic *ResticCmd
}
// ResticConfig is all configuration to be sent to Restic
type ResticConfig struct {
Repo string `hcl:"repo"`
Passphrase string `hcl:"passphrase"`
Passphrase string `hcl:"passphrase,optional"`
Env map[string]string `hcl:"env,optional"`
Args []string `hcl:"args"`
GlobalOpts *ResticGlobalOpts `hcl:"options,block"`
}
// ExecutableTask is a task to be run before or after backup/retore
type ExecutableTask interface {
RunBackup(cfg TaskConfig) error
RunRestore(cfg TaskConfig) error
Name() string
}
// JobTaskScript is a sript to be executed as part of a job task
type JobTaskScript struct {
ScriptPath string `hcl:"path,optional"`
Body string `hcl:"body,optional"`
OnBackup string `hcl:"on_backup,optional"`
OnRestore string `hcl:"on_restore,optional"`
FromJobDir bool `hcl:"from_job_dir,optional"`
env map[string]string
name string
}
// RunBackup runs script on backup
func (t JobTaskScript) RunBackup(cfg TaskConfig) error {
env := MergeEnv(cfg.Env, t.env)
if env == nil {
env = map[string]string{}
}
env["RESTIC_JOB_DIR"] = cfg.JobDir
cwd := ""
if t.FromJobDir {
cwd = cfg.JobDir
}
if err := RunShell(t.OnBackup, cwd, env, cfg.Logger); err != nil {
return fmt.Errorf("failed running task script %s: %w", t.Name(), err)
}
return nil
}
// RunRestore script on restore
func (t JobTaskScript) RunRestore(cfg TaskConfig) error {
env := MergeEnv(cfg.Env, t.env)
if env == nil {
env = map[string]string{}
}
env["RESTIC_JOB_DIR"] = cfg.JobDir
cwd := ""
if t.FromJobDir {
cwd = cfg.JobDir
}
if err := RunShell(t.OnRestore, cwd, env, cfg.Logger); err != nil {
return fmt.Errorf("failed running task script %s: %w", t.Name(), err)
}
return nil
}
func (t JobTaskScript) Name() string {
return t.name
}
func (t *JobTaskScript) SetName(name string) {
t.name = name
}
// JobTaskMySQL is a sqlite backup task that performs required pre and post tasks
type JobTaskMySQL struct {
Name string `hcl:"name,label"`
Hostname string `hcl:"hostname,optional"`
Database string `hcl:"database,optional"`
Username string `hcl:"username,optional"`
Password string `hcl:"password,optional"`
}
func (t JobTaskMySQL) GetPreTask() ExecutableTask {
return JobTaskScript{
name: t.Name,
OnBackup: fmt.Sprintf(
"mysqldump -h '%s' -u '%s' -p '%s' '%s' > './%s.sql'",
t.Hostname,
t.Username,
t.Password,
t.Database,
t.Name,
),
FromJobDir: true,
}
}
func (t JobTaskMySQL) GetPostTask() ExecutableTask {
return JobTaskScript{
name: t.Name,
OnRestore: fmt.Sprintf(
"mysql -h '%s' -u '%s' -p '%s' '%s' << './%s.sql'",
t.Hostname,
t.Username,
t.Password,
t.Database,
t.Name,
),
FromJobDir: true,
}
}
// JobTaskSqlite is a sqlite backup task that performs required pre and post tasks
type JobTaskSqlite struct {
Path string `hcl:"path,label"`
Name string `hcl:"name,label"`
Path string `hcl:"path"`
}
func (t JobTaskSqlite) GetPreTask() ExecutableTask {
return JobTaskScript{
name: t.Name,
OnBackup: fmt.Sprintf(
"sqlite3 %s '.backup $RESTIC_JOB_DIR/%s.bak'",
t.Path, t.Name,
),
}
}
func (t JobTaskSqlite) GetPostTask() ExecutableTask {
return JobTaskScript{
name: t.Name,
OnRestore: fmt.Sprintf("cp '$RESTIC_JOB_DIR/%s.bak' '%s'", t.Name, t.Path),
}
}
type BackupFilesTask struct {
Files []string `hcl:"files"`
BackupOpts *BackupOpts `hcl:"backup_opts,block"`
RestoreOpts *RestoreOpts `hcl:"restore_opts,block"`
name string
}
func (t BackupFilesTask) RunBackup(cfg TaskConfig) error {
if err := cfg.Restic.Backup(t.Files, t.BackupOpts); err != nil {
err = fmt.Errorf("failed backing up files: %w", err)
cfg.Logger.Fatal(err)
return err
}
return nil
}
func (t BackupFilesTask) RunRestore(cfg TaskConfig) error {
if err := cfg.Restic.Restore("latest", t.RestoreOpts); err != nil {
err = fmt.Errorf("failed restoring files: %w", err)
cfg.Logger.Fatal(err)
return err
}
return nil
}
func (t BackupFilesTask) Name() string {
return t.name
}
func (t *BackupFilesTask) SetName(name string) {
t.name = name
}
// JobTask represents a single task within a backup job
type JobTask struct {
Name string `hcl:"name,label"`
OnBackup []JobTaskScript `hcl:"on_backup,block"`
OnRestore []JobTaskScript `hcl:"on_restore,block"`
MySql []JobTaskMySQL `hcl:"mysql,block"`
Sqlite []JobTaskSqlite `hcl:"sqlite,block"`
Files []string `hcl:"files,optional"`
Scripts []JobTaskScript `hcl:"script,block"`
Backup *BackupFilesTask `hcl:"backup,block"`
}
func (t JobTask) GetTasks() []ExecutableTask {
allTasks := []ExecutableTask{}
for _, exTask := range t.Scripts {
exTask.SetName(t.Name)
allTasks = append(allTasks, exTask)
}
if t.Backup != nil {
t.Backup.SetName(t.Name)
allTasks = append(allTasks, t.Backup)
}
return allTasks
}
// Job contains all configuration required to construct and run a backup
@ -42,17 +217,87 @@ type JobTask struct {
type Job struct {
Name string `hcl:"name,label"`
Schedule string `hcl:"schedule"`
Config JobConfig `hcl:"config,block"`
Config ResticConfig `hcl:"config,block"`
Tasks []JobTask `hcl:"task,block"`
Validate bool `hcl:"validate,optional"`
Forget *ForgetOpts `hcl:"forget,block"`
// Meta Tasks
MySql []JobTaskMySQL `hcl:"mysql,block"`
Sqlite []JobTaskSqlite `hcl:"sqlite,block"`
}
func (job Job) NewRestic() ResticCmd {
return ResticCmd{
LogPrefix: job.Name,
Repo: job.Config.Repo,
Env: job.Config.Env,
Passphrase: job.Config.Passphrase,
func (j Job) AllTasks() []ExecutableTask {
allTasks := []ExecutableTask{}
// Pre tasks
for _, mysql := range j.MySql {
allTasks = append(allTasks, mysql.GetPreTask())
}
for _, sqlite := range j.Sqlite {
allTasks = append(allTasks, sqlite.GetPreTask())
}
// Get ordered tasks
for _, jobTask := range j.Tasks {
allTasks = append(allTasks, jobTask.GetTasks()...)
}
// Post tasks
for _, mysql := range j.MySql {
allTasks = append(allTasks, mysql.GetPreTask())
}
for _, sqlite := range j.Sqlite {
allTasks = append(allTasks, sqlite.GetPreTask())
}
return allTasks
}
func (j Job) JobDir() string {
cwd := filepath.Join("/restic_backup", j.Name)
_ = os.MkdirAll(cwd, WorkDirPerms)
return cwd
}
func (j Job) RunTasks() error {
logger := GetLogger(j.Name)
restic := j.NewRestic()
jobDir := j.JobDir()
if err := restic.EnsureInit(); err != nil {
return fmt.Errorf("failed to init restic for job %s: %w", j.Name, err)
}
for _, exTask := range j.AllTasks() {
taskCfg := TaskConfig{
JobDir: jobDir,
Logger: GetChildLogger(logger, exTask.Name()),
Restic: restic,
}
if err := exTask.RunBackup(taskCfg); err != nil {
return fmt.Errorf("failed running job %s: %w", j.Name, err)
}
}
if j.Forget != nil {
restic.Forget(j.Forget)
}
return nil
}
func (j Job) NewRestic() *ResticCmd {
return &ResticCmd{
Logger: GetLogger(j.Name),
Repo: j.Config.Repo,
Env: j.Config.Env,
Passphrase: j.Config.Passphrase,
GlobalOpts: j.Config.GlobalOpts,
}
}

17
main.go
View File

@ -26,9 +26,24 @@ func main() {
var config Config
if err := hclsimple.DecodeFile("config.hcl", nil, &config); err != nil {
args := flag.Args()
if len(args) == 0 {
log.Fatalf("Requires a path to a job file, but found none")
}
if err := hclsimple.DecodeFile(args[0], nil, &config); err != nil {
log.Fatalf("Failed to load configuration: %s", err)
}
log.Printf("Configuration is %#v", config)
if len(config.Jobs) == 0 {
log.Fatalf("No jobs defined in config")
}
for _, job := range config.Jobs {
if err := job.RunTasks(); err != nil {
log.Fatalf("%v", err)
}
}
}

321
restic.go Normal file
View File

@ -0,0 +1,321 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
"time"
)
type CommandOptions interface {
ToArgs() []string
}
type NoOpts struct{}
func (_ NoOpts) ToArgs() []string {
return []string{}
}
type ResticGlobalOpts struct {
CaCertFile string `hcl:"CaCertFile,optional"`
CacheDir string `hcl:"CacheDir,optional"`
PasswordFile string `hcl:"PasswordFile,optional"`
TlsClientCertFile string `hcl:"TlsClientCertFile,optional"`
LimitDownload int `hcl:"LimitDownload,optional"`
LimitUpload int `hcl:"LimitUpload,optional"`
VerboseLevel int `hcl:"VerboseLevel,optional"`
CleanupCache bool `hcl:"CleanupCache,optional"`
NoCache bool `hcl:"NoCache,optional"`
NoLock bool `hcl:"NoLock,optional"`
}
func (glo ResticGlobalOpts) ToArgs() (args []string) {
if glo.CaCertFile != "" {
args = append(args, "--cacert", glo.CaCertFile)
}
if glo.CacheDir != "" {
args = append(args, "--cache-dir", glo.CacheDir)
}
if glo.CleanupCache {
args = append(args, "--cleanup-cache")
}
if glo.LimitDownload > 0 {
args = append(args, "--limit-download", fmt.Sprint(glo.LimitDownload))
}
if glo.LimitUpload > 0 {
args = append(args, "--limit-upload", fmt.Sprint(glo.LimitUpload))
}
if glo.NoCache {
args = append(args, "--no-cache")
}
if glo.NoLock {
args = append(args, "--no-lock")
}
if glo.PasswordFile != "" {
args = append(args, "--password-file", glo.PasswordFile)
}
if glo.TlsClientCertFile != "" {
args = append(args, "--tls-client-cert", glo.TlsClientCertFile)
}
if glo.VerboseLevel > 0 {
args = append(args, "--verbose", fmt.Sprint(glo.VerboseLevel))
}
return args
}
type ResticCmd struct {
Logger *log.Logger
Repo string
Env map[string]string
Passphrase string
GlobalOpts *ResticGlobalOpts
Cwd string
}
func (rcmd ResticCmd) BuildEnv() []string {
if rcmd.Env == nil {
rcmd.Env = map[string]string{}
}
if rcmd.Passphrase != "" {
rcmd.Env["RESTIC_PASSWORD"] = rcmd.Passphrase
}
envList := os.Environ()
for name, value := range rcmd.Env {
envList = append(envList, fmt.Sprintf("%s=%s", name, value))
}
return envList
}
func (rcmd ResticCmd) RunRestic(command string, options CommandOptions, commandArgs ...string) error {
args := []string{}
if rcmd.GlobalOpts != nil {
args = rcmd.GlobalOpts.ToArgs()
}
args = append(args, "--repo", rcmd.Repo, command)
args = append(args, options.ToArgs()...)
args = append(args, commandArgs...)
cmd := exec.Command("restic", args...)
cmd.Stdout = NewLogWriter(rcmd.Logger)
cmd.Stderr = cmd.Stdout
cmd.Env = rcmd.BuildEnv()
cmd.Dir = rcmd.Cwd
err := cmd.Run()
return err
}
type BackupOpts struct {
Exclude []string `hcl:"Exclude,optional"`
Include []string `hcl:"Include,optional"`
Tags []string `hcl:"Tags,optional"`
Host string `hcl:"Host,optional"`
}
func (bo BackupOpts) ToArgs() (args []string) {
for _, tag := range bo.Tags {
args = append(args, "--tag", tag)
}
if bo.Host != "" {
args = append(args, "--host", bo.Host)
}
return
}
func (rcmd ResticCmd) Backup(files []string, options *BackupOpts) error {
if options == nil {
options = &BackupOpts{}
}
err := rcmd.RunRestic("backup", options, files...)
return err
}
type RestoreOpts struct {
Exclude []string `hcl:"Exclude,optional"`
Host []string `hcl:"Host,optional"`
Include []string `hcl:"Include,optional"`
Path string `hcl:"Path,optional"`
Tags []string `hcl:"Tags,optional"`
Target string `hcl:"Target,optional"`
Verify bool `hcl:"Verify,optional"`
}
func (ro RestoreOpts) ToArgs() (args []string) {
for _, exclude := range ro.Exclude {
args = append(args, "--exclude", exclude)
}
for _, include := range ro.Include {
args = append(args, "--include", include)
}
for _, host := range ro.Host {
args = append(args, "--host", host)
}
if ro.Path != "" {
args = append(args, "--path", ro.Path)
}
for _, tag := range ro.Tags {
args = append(args, "--tag", tag)
}
if ro.Target != "" {
args = append(args, "--target", ro.Target)
}
if ro.Verify {
args = append(args, "--verify")
}
return
}
func (rcmd ResticCmd) Restore(snapshot string, opts *RestoreOpts) error {
if opts == nil {
opts = &RestoreOpts{}
}
err := rcmd.RunRestic("restore", opts, snapshot)
return err
}
type ForgetOpts struct {
KeepLast int `hcl:"KeepLast,optional"`
KeepHourly int `hcl:"KeepHourly,optional"`
KeepDaily int `hcl:"KeepDaily,optional"`
KeepWeekly int `hcl:"KeepWeekly,optional"`
KeepMonthly int `hcl:"KeepMonthly,optional"`
KeepYearly int `hcl:"KeepYearly,optional"`
KeepWithin time.Duration `hcl:"KeepWithin,optional"`
KeepWithinHourly time.Duration `hcl:"KeepWithinHourly,optional"`
KeepWithinDaily time.Duration `hcl:"KeepWithinDaily,optional"`
KeepWithinWeekly time.Duration `hcl:"KeepWithinWeekly,optional"`
KeepWithinMonthly time.Duration `hcl:"KeepWithinMonthly,optional"`
KeepWithinYearly time.Duration `hcl:"KeepWithinYearly,optional"`
Tags []string `hcl:"Tags,optional"`
KeepTags []string `hcl:"KeepTags,optional"`
Prune bool `hcl:"Prune,optional"`
}
func (fo ForgetOpts) ToArgs() (args []string) {
// Add keep-*
if fo.KeepLast > 0 {
args = append(args, "--keep-last", fmt.Sprint(fo.KeepLast))
}
if fo.KeepHourly > 0 {
args = append(args, "--keep-hourly", fmt.Sprint(fo.KeepHourly))
}
if fo.KeepDaily > 0 {
args = append(args, "--keep-daily", fmt.Sprint(fo.KeepDaily))
}
if fo.KeepWeekly > 0 {
args = append(args, "--keep-weekly", fmt.Sprint(fo.KeepWeekly))
}
if fo.KeepMonthly > 0 {
args = append(args, "--keep-monthly", fmt.Sprint(fo.KeepMonthly))
}
if fo.KeepYearly > 0 {
args = append(args, "--keep-yearly", fmt.Sprint(fo.KeepYearly))
}
if fo.KeepWithin > 0 {
args = append(args, "--keep-within", fmt.Sprint(fo.KeepWithin))
}
// Add keep-within-*
if fo.KeepWithinHourly > 0 {
args = append(args, "--keep-within-hourly", fo.KeepWithinHourly.String())
}
if fo.KeepWithinDaily > 0 {
args = append(args, "--keep-within-daily", fo.KeepWithinDaily.String())
}
if fo.KeepWithinWeekly > 0 {
args = append(args, "--keep-within-weekly", fo.KeepWithinWeekly.String())
}
if fo.KeepWithinMonthly > 0 {
args = append(args, "--keep-within-monthly", fo.KeepWithinMonthly.String())
}
if fo.KeepWithinYearly > 0 {
args = append(args, "--keep-within-yearly", fo.KeepWithinYearly.String())
}
// Add tags
if len(fo.Tags) > 0 {
args = append(args, "--tag", strings.Join(fo.Tags, ","))
}
if len(fo.KeepTags) > 0 {
args = append(args, "--keep-tag", strings.Join(fo.Tags, ","))
}
// Add prune options
if fo.Prune {
args = append(args, "--prune")
}
return args
}
func (rcmd ResticCmd) Forget(forgetOpts *ForgetOpts) error {
if forgetOpts == nil {
forgetOpts = &ForgetOpts{}
}
err := rcmd.RunRestic("forget", forgetOpts)
return err
}
func (rcmd ResticCmd) Check() error {
err := rcmd.RunRestic("check", NoOpts{})
return err
}
func (rcmd ResticCmd) EnsureInit() error {
if err := rcmd.RunRestic("snapshots", NoOpts{}); err != nil {
return rcmd.RunRestic("init", NoOpts{})
}
return nil
}

169
run.go
View File

@ -1,169 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
"time"
)
var defaultFlags = log.LstdFlags | log.Lmsgprefix
type ResticCmd struct {
LogPrefix string
Repo string
Env map[string]string
Passphrase string
}
func (rcmd ResticCmd) BuildEnv() []string {
rcmd.Env["RESTIC_PASSWORD"] = rcmd.Passphrase
envList := []string{}
for name, value := range rcmd.Env {
envList = append(envList, fmt.Sprintf("%s=%s", name, value))
}
return envList
}
func (rcmd ResticCmd) RunRestic(args []string) error {
cmd := exec.Command("restic", args...)
cmd.Stdout = rcmd.Logger().Writer()
cmd.Stderr = cmd.Stdout
cmd.Env = rcmd.BuildEnv()
err := cmd.Run()
return err
}
func (rcmd ResticCmd) Logger() *log.Logger {
logger := log.New(os.Stderr, rcmd.LogPrefix, defaultFlags)
return logger
}
func (rcmd ResticCmd) Backup(path string, args []string) error {
args = append([]string{"--repo", rcmd.Repo, "backup"}, args...)
args = append(args, path)
err := rcmd.RunRestic(args)
return err
}
type ForgetOpts struct {
KeepLast int
KeepHourly int
KeepDaily int
KeepWeekly int
KeepMonthly int
KeepYearly int
KeepWithin time.Duration
KeepWithinHourly time.Duration
KeepWithinDaily time.Duration
KeepWithinWeekly time.Duration
KeepWithinMonthly time.Duration
KeepWithinYearly time.Duration
Tags []string
KeepTags []string
Prune bool
}
func (fo ForgetOpts) ToArgs() []string {
args := []string{}
// Add keep-*
if fo.KeepLast > 0 {
args = append(args, "--keep-last", fmt.Sprint(fo.KeepLast))
}
if fo.KeepHourly > 0 {
args = append(args, "--keep-hourly", fmt.Sprint(fo.KeepHourly))
}
if fo.KeepDaily > 0 {
args = append(args, "--keep-daily", fmt.Sprint(fo.KeepDaily))
}
if fo.KeepWeekly > 0 {
args = append(args, "--keep-weekly", fmt.Sprint(fo.KeepWeekly))
}
if fo.KeepMonthly > 0 {
args = append(args, "--keep-monthly", fmt.Sprint(fo.KeepMonthly))
}
if fo.KeepYearly > 0 {
args = append(args, "--keep-yearly", fmt.Sprint(fo.KeepYearly))
}
if fo.KeepWithin > 0 {
args = append(args, "--keep-within", fmt.Sprint(fo.KeepWithin))
}
// Add keep-within-*
if fo.KeepWithinHourly > 0 {
args = append(args, "--keep-within-hourly", fo.KeepWithinHourly.String())
}
if fo.KeepWithinDaily > 0 {
args = append(args, "--keep-within-daily", fo.KeepWithinDaily.String())
}
if fo.KeepWithinWeekly > 0 {
args = append(args, "--keep-within-weekly", fo.KeepWithinWeekly.String())
}
if fo.KeepWithinMonthly > 0 {
args = append(args, "--keep-within-monthly", fo.KeepWithinMonthly.String())
}
if fo.KeepWithinYearly > 0 {
args = append(args, "--keep-within-yearly", fo.KeepWithinYearly.String())
}
// Add tags
if len(fo.Tags) > 0 {
args = append(args, "--tag", strings.Join(fo.Tags, ","))
}
if len(fo.KeepTags) > 0 {
args = append(args, "--keep-tag", strings.Join(fo.Tags, ","))
}
// Add prune options
if fo.Prune {
args = append(args, "--prune")
}
return args
}
func (rcmd ResticCmd) Cleanup(forgetOpts ForgetOpts) error {
args := append([]string{"--repo", rcmd.Repo, "forget"}, forgetOpts.ToArgs()...)
err := rcmd.RunRestic(args)
return err
}
func (rcmd ResticCmd) Check() error {
args := []string{"--repo", rcmd.Repo, "check"}
err := rcmd.RunRestic(args)
return err
}

78
shell.go Normal file
View File

@ -0,0 +1,78 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
)
var (
loggerFlags = log.LstdFlags | log.Lmsgprefix
loggers = map[string]*log.Logger{}
)
func GetLogger(name string) *log.Logger {
if logger, ok := loggers[name]; ok {
return logger
}
logger := log.New(os.Stderr, name+":", loggerFlags)
loggers[name] = logger
return logger
}
func GetChildLogger(parent *log.Logger, name string) *log.Logger {
childName := fmt.Sprintf("%s%s", parent.Prefix(), name)
return GetLogger(childName)
}
type logWriter struct {
logger *log.Logger
}
func NewLogWriter(logger *log.Logger) *logWriter {
return &logWriter{logger}
}
func (w logWriter) Write(p []byte) (n int, err error) {
message := fmt.Sprintf("%s", p)
for _, line := range strings.Split(message, "\n") {
w.logger.Printf(" %s", line)
}
return len(p), nil
}
func RunShell(script string, cwd string, env map[string]string, logger *log.Logger) error {
cmd := exec.Command("sh", "-c", strings.TrimSpace(script))
// Make both stderr and stdout go to logger
// fmt.Println("LOGGER PREFIX", logger.Prefix())
// logger.Println("From logger")
cmd.Stdout = NewLogWriter(logger)
cmd.Stderr = cmd.Stdout
// Set working directory
cmd.Dir = cwd
// Convert env to list if values provided
if len(env) > 0 {
envList := os.Environ()
for name, value := range env {
envList = append(envList, fmt.Sprintf("%s=%s", name, value))
}
cmd.Env = envList
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("shell execution failed: %w", err)
}
return nil
}

40
test.hcl Normal file
View File

@ -0,0 +1,40 @@
job "TestBackup" {
schedule = "1 * * * *"
config {
repo = "./backups"
passphrase = "supersecret"
options {
CacheDir = "./cache"
}
}
task "before script" {
script {
on_backup = "echo before backup!"
}
}
task "backup" {
backup {
files = [
"./data"
]
backup_opts {
Tags = ["foo"]
}
}
}
task "after script" {
script {
on_backup = "echo after backup!"
}
}
forget {
KeepLast = 2
}
}

13
utils.go Normal file
View File

@ -0,0 +1,13 @@
package main
func MergeEnv(parent, child map[string]string) (result map[string]string) {
for key, value := range parent {
result[key] = value
}
for key, value := range child {
result[key] = value
}
return
}