494 lines
11 KiB
Go
494 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
type TaskConfig struct {
|
|
BackupPaths []string
|
|
Env map[string]string
|
|
Logger *log.Logger
|
|
Restic *Restic
|
|
}
|
|
|
|
// 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 {
|
|
OnBackup string `hcl:"on_backup,optional"`
|
|
OnRestore string `hcl:"on_restore,optional"`
|
|
Cwd string `hcl:"cwd,optional"`
|
|
env map[string]string
|
|
name string
|
|
}
|
|
|
|
func (t JobTaskScript) run(script string, cfg TaskConfig) error {
|
|
if script == "" {
|
|
return nil
|
|
}
|
|
|
|
env := MergeEnvMap(cfg.Env, t.env)
|
|
if env == nil {
|
|
env = map[string]string{}
|
|
}
|
|
|
|
if err := RunShell(script, t.Cwd, env, cfg.Logger); err != nil {
|
|
return fmt.Errorf("failed running task script %s: %w", t.Name(), err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RunBackup runs script on backup.
|
|
func (t JobTaskScript) RunBackup(cfg TaskConfig) error {
|
|
return t.run(t.OnBackup, cfg)
|
|
}
|
|
|
|
// RunRestore script on restore.
|
|
func (t JobTaskScript) RunRestore(cfg TaskConfig) error {
|
|
return t.run(t.OnRestore, cfg)
|
|
}
|
|
|
|
func (t JobTaskScript) Name() string {
|
|
return t.name
|
|
}
|
|
|
|
func (t *JobTaskScript) SetName(name string) {
|
|
t.name = name
|
|
}
|
|
|
|
// JobTaskMySQL is a MySQL backup task that performs required pre and post tasks.
|
|
type JobTaskMySQL struct {
|
|
Port int `hcl:"port,optional"`
|
|
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"`
|
|
Tables []string `hcl:"tables,optional"`
|
|
NoTablespaces bool `hcl:"no_tablespaces,optional"`
|
|
DumpToPath string `hcl:"dump_to"`
|
|
}
|
|
|
|
func (t JobTaskMySQL) Paths() []string {
|
|
return []string{t.DumpToPath}
|
|
}
|
|
|
|
func (t JobTaskMySQL) Validate() error {
|
|
if t.DumpToPath == "" {
|
|
return fmt.Errorf("task %s is missing dump_to path: %w", t.Name, ErrMissingField)
|
|
}
|
|
|
|
if stat, err := os.Stat(t.DumpToPath); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf(
|
|
"task %s: invalid dump_to: could not stat path: %s: %w",
|
|
t.Name,
|
|
t.DumpToPath,
|
|
ErrInvalidConfigValue,
|
|
)
|
|
}
|
|
} else if stat.Mode().IsDir() {
|
|
return fmt.Errorf("task %s: dump_to cannot be a directory: %w", t.Name, ErrInvalidConfigValue)
|
|
}
|
|
|
|
if len(t.Tables) > 0 && t.Database == "" {
|
|
return fmt.Errorf(
|
|
"task %s is invalid. Must specify a database to use tables: %w",
|
|
t.Name,
|
|
ErrMissingField,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t JobTaskMySQL) GetPreTask() ExecutableTask {
|
|
command := []string{"mysqldump", "--result-file", t.DumpToPath}
|
|
|
|
if t.Hostname != "" {
|
|
command = append(command, "--host", t.Hostname)
|
|
}
|
|
|
|
if t.Port != 0 {
|
|
command = append(command, "--port", fmt.Sprintf("%d", t.Port))
|
|
}
|
|
|
|
if t.Username != "" {
|
|
command = append(command, "--user", t.Username)
|
|
}
|
|
|
|
if t.Password != "" {
|
|
command = append(command, fmt.Sprintf("--password=%s", t.Password))
|
|
}
|
|
|
|
if t.NoTablespaces {
|
|
command = append(command, "--no-tablespaces")
|
|
}
|
|
|
|
if t.Database != "" {
|
|
command = append(command, t.Database)
|
|
} else {
|
|
command = append(command, "--all-databases")
|
|
}
|
|
|
|
command = append(command, t.Tables...)
|
|
|
|
return JobTaskScript{
|
|
name: t.Name,
|
|
env: nil,
|
|
Cwd: ".",
|
|
OnBackup: strings.Join(command, " "),
|
|
OnRestore: "",
|
|
}
|
|
}
|
|
|
|
func (t JobTaskMySQL) GetPostTask() ExecutableTask {
|
|
command := []string{"mysql"}
|
|
|
|
if t.Hostname != "" {
|
|
command = append(command, "--host", t.Hostname)
|
|
}
|
|
|
|
if t.Port != 0 {
|
|
command = append(command, "--port", fmt.Sprintf("%d", t.Port))
|
|
}
|
|
|
|
if t.Username != "" {
|
|
command = append(command, "--user", t.Username)
|
|
}
|
|
|
|
if t.Password != "" {
|
|
command = append(command, fmt.Sprintf("--password=%s", t.Password))
|
|
}
|
|
|
|
if t.Database != "" {
|
|
command = append(command, t.Database)
|
|
}
|
|
|
|
command = append(command, "<", t.DumpToPath)
|
|
|
|
return JobTaskScript{
|
|
name: t.Name,
|
|
env: nil,
|
|
Cwd: ".",
|
|
OnBackup: "",
|
|
OnRestore: strings.Join(command, " "),
|
|
}
|
|
}
|
|
|
|
// JobTaskPostgres is a postgres backup task that performs required pre and post tasks.
|
|
type JobTaskPostgres struct {
|
|
Port int `hcl:"port,optional"`
|
|
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"`
|
|
Tables []string `hcl:"tables,optional"`
|
|
DumpToPath string `hcl:"dump_to"`
|
|
NoTablespaces bool `hcl:"no_tablespaces,optional"`
|
|
Clean bool `hcl:"clean,optional"`
|
|
Create bool `hcl:"create,optional"`
|
|
}
|
|
|
|
func (t JobTaskPostgres) Paths() []string {
|
|
return []string{t.DumpToPath}
|
|
}
|
|
|
|
func (t JobTaskPostgres) Validate() error {
|
|
if t.DumpToPath == "" {
|
|
return fmt.Errorf("task %s is missing dump_to path: %w", t.Name, ErrMissingField)
|
|
}
|
|
|
|
if stat, err := os.Stat(t.DumpToPath); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf(
|
|
"task %s: invalid dump_to: could not stat path: %s: %w",
|
|
t.Name,
|
|
t.DumpToPath,
|
|
ErrInvalidConfigValue,
|
|
)
|
|
}
|
|
} else if stat.Mode().IsDir() {
|
|
return fmt.Errorf("task %s: dump_to cannot be a directory: %w", t.Name, ErrInvalidConfigValue)
|
|
}
|
|
|
|
if len(t.Tables) > 0 && t.Database == "" {
|
|
return fmt.Errorf(
|
|
"task %s is invalid. Must specify a database to use tables: %w",
|
|
t.Name,
|
|
ErrMissingField,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//nolint:cyclop
|
|
func (t JobTaskPostgres) GetPreTask() ExecutableTask {
|
|
command := []string{"pg_dump"}
|
|
if t.Database == "" {
|
|
command = []string{"pg_dumpall"}
|
|
}
|
|
|
|
command = append(command, "--file", t.DumpToPath)
|
|
|
|
if t.Hostname != "" {
|
|
command = append(command, "--host", t.Hostname)
|
|
}
|
|
|
|
if t.Port != 0 {
|
|
command = append(command, "--port", fmt.Sprintf("%d", t.Port))
|
|
}
|
|
|
|
if t.Username != "" {
|
|
command = append(command, "--username", t.Username)
|
|
}
|
|
|
|
if t.NoTablespaces {
|
|
command = append(command, "--no-tablespaces")
|
|
}
|
|
|
|
if t.Clean {
|
|
command = append(command, "--clean")
|
|
}
|
|
|
|
if t.Create {
|
|
command = append(command, "--create")
|
|
}
|
|
|
|
for _, table := range t.Tables {
|
|
command = append(command, "--table", table)
|
|
}
|
|
|
|
if t.Database != "" {
|
|
command = append(command, t.Database)
|
|
}
|
|
|
|
env := map[string]string{}
|
|
if t.Password != "" {
|
|
env["PGPASSWORD"] = t.Password
|
|
}
|
|
|
|
return JobTaskScript{
|
|
name: t.Name,
|
|
env: env,
|
|
Cwd: ".",
|
|
OnBackup: strings.Join(command, " "),
|
|
OnRestore: "",
|
|
}
|
|
}
|
|
|
|
func (t JobTaskPostgres) GetPostTask() ExecutableTask {
|
|
command := []string{"psql"}
|
|
|
|
if t.Hostname != "" {
|
|
command = append(command, "--host", t.Hostname)
|
|
}
|
|
|
|
if t.Port != 0 {
|
|
command = append(command, "--port", fmt.Sprintf("%d", t.Port))
|
|
}
|
|
|
|
if t.Username != "" {
|
|
command = append(command, "--username", t.Username)
|
|
}
|
|
|
|
if t.Database != "" {
|
|
command = append(command, t.Database)
|
|
}
|
|
|
|
command = append(command, "<", t.DumpToPath)
|
|
|
|
env := map[string]string{}
|
|
if t.Password != "" {
|
|
env["PGPASSWORD"] = t.Password
|
|
}
|
|
|
|
return JobTaskScript{
|
|
name: t.Name,
|
|
env: env,
|
|
Cwd: ".",
|
|
OnBackup: "",
|
|
OnRestore: strings.Join(command, " "),
|
|
}
|
|
}
|
|
|
|
// JobTaskSqlite is a sqlite backup task that performs required pre and post tasks.
|
|
type JobTaskSqlite struct {
|
|
Name string `hcl:"name,label"`
|
|
Path string `hcl:"path"`
|
|
DumpToPath string `hcl:"dump_to"`
|
|
}
|
|
|
|
func (t JobTaskSqlite) Paths() []string {
|
|
return []string{t.DumpToPath}
|
|
}
|
|
|
|
func (t JobTaskSqlite) Validate() error {
|
|
if t.DumpToPath == "" {
|
|
return fmt.Errorf("task %s is missing dump_to path: %w", t.Name, ErrMissingField)
|
|
}
|
|
|
|
if stat, err := os.Stat(t.DumpToPath); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf(
|
|
"task %s: invalid dump_to: could not stat path: %s: %w",
|
|
t.Name,
|
|
t.DumpToPath,
|
|
ErrInvalidConfigValue,
|
|
)
|
|
}
|
|
} else if stat.Mode().IsDir() {
|
|
return fmt.Errorf("task %s: dump_to cannot be a directory: %w", t.Name, ErrInvalidConfigValue)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t JobTaskSqlite) GetPreTask() ExecutableTask {
|
|
return JobTaskScript{
|
|
name: t.Name,
|
|
env: nil,
|
|
Cwd: ".",
|
|
OnBackup: fmt.Sprintf("sqlite3 '%s' '.backup %s'", t.Path, t.DumpToPath),
|
|
OnRestore: "",
|
|
}
|
|
}
|
|
|
|
func (t JobTaskSqlite) GetPostTask() ExecutableTask {
|
|
return JobTaskScript{
|
|
name: t.Name,
|
|
env: nil,
|
|
Cwd: ".",
|
|
OnBackup: "",
|
|
OnRestore: fmt.Sprintf("cp '%s' '%s'", t.DumpToPath, t.Path),
|
|
}
|
|
}
|
|
|
|
type BackupFilesTask struct {
|
|
Paths []string `hcl:"paths"`
|
|
BackupOpts *BackupOpts `hcl:"backup_opts,block"`
|
|
RestoreOpts *RestoreOpts `hcl:"restore_opts,block"`
|
|
name string
|
|
snapshot string
|
|
}
|
|
|
|
func (t BackupFilesTask) RunBackup(cfg TaskConfig) error {
|
|
if t.BackupOpts == nil {
|
|
t.BackupOpts = &BackupOpts{} //nolint:exhaustruct
|
|
}
|
|
|
|
if err := cfg.Restic.Backup(cfg.BackupPaths, *t.BackupOpts); err != nil {
|
|
err = fmt.Errorf("failed backing up paths: %w", err)
|
|
cfg.Logger.Print(err)
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t BackupFilesTask) RunRestore(cfg TaskConfig) error {
|
|
if t.RestoreOpts == nil {
|
|
t.RestoreOpts = &RestoreOpts{} //nolint:exhaustruct
|
|
}
|
|
|
|
if t.snapshot == "" {
|
|
t.snapshot = "latest"
|
|
}
|
|
|
|
if err := cfg.Restic.Restore(t.snapshot, *t.RestoreOpts); err != nil {
|
|
err = fmt.Errorf("failed restoring paths: %w", err)
|
|
cfg.Logger.Print(err)
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t BackupFilesTask) Name() string {
|
|
return t.name
|
|
}
|
|
|
|
func (t *BackupFilesTask) SetName(name string) {
|
|
t.name = name
|
|
}
|
|
|
|
func (t *BackupFilesTask) Validate() error {
|
|
if len(t.Paths) == 0 {
|
|
return fmt.Errorf("backup config doesn't include any paths: %w", ErrInvalidConfigValue)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// JobTask represents a single task within a backup job.
|
|
type JobTask struct {
|
|
Name string `hcl:"name,label"`
|
|
PreScripts []JobTaskScript `hcl:"pre_script,block"`
|
|
PostScripts []JobTaskScript `hcl:"post_script,block"`
|
|
MySQL []JobTaskMySQL `hcl:"mysql,block"`
|
|
Postgres []JobTaskPostgres `hcl:"postgres,block"`
|
|
Sqlite []JobTaskSqlite `hcl:"sqlite,block"`
|
|
}
|
|
|
|
func (t JobTask) Validate() error {
|
|
// NOTE: Might make task types mutually exclusive because order is confusing even if deterministic
|
|
if t.Name == "" {
|
|
return fmt.Errorf("task is missing a name: %w", ErrMissingField)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t JobTask) GetPreTasks() []ExecutableTask {
|
|
allTasks := []ExecutableTask{}
|
|
|
|
for _, task := range t.MySQL {
|
|
allTasks = append(allTasks, task.GetPreTask())
|
|
}
|
|
|
|
for _, task := range t.Sqlite {
|
|
allTasks = append(allTasks, task.GetPreTask())
|
|
}
|
|
|
|
for _, exTask := range t.PreScripts {
|
|
exTask.SetName(t.Name)
|
|
allTasks = append(allTasks, exTask)
|
|
}
|
|
|
|
return allTasks
|
|
}
|
|
|
|
func (t JobTask) GetPostTasks() []ExecutableTask {
|
|
allTasks := []ExecutableTask{}
|
|
|
|
for _, exTask := range t.PostScripts {
|
|
exTask.SetName(t.Name)
|
|
allTasks = append(allTasks, exTask)
|
|
}
|
|
|
|
for _, task := range t.MySQL {
|
|
allTasks = append(allTasks, task.GetPostTask())
|
|
}
|
|
|
|
for _, task := range t.Sqlite {
|
|
allTasks = append(allTasks, task.GetPostTask())
|
|
}
|
|
|
|
return allTasks
|
|
}
|