Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

import github.com/databacker/api instead of defining locally #370

Merged
merged 1 commit into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.21'
go-version: '1.22'
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.21
go-version: 1.22
- name: Build for all platforms
run: |
make build-all
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# mysql backup image
FROM golang:1.21.13-alpine3.20 AS build
FROM golang:1.22.9-alpine3.20 AS build

COPY . /src/mysql-backup
WORKDIR /src/mysql-backup
Expand Down
7 changes: 4 additions & 3 deletions cmd/common_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"reflect"

"github.com/databacker/mysql-backup/pkg/core"
Expand All @@ -19,17 +20,17 @@ func newMockExecs() *mockExecs {
return m
}

func (m *mockExecs) Dump(opts core.DumpOptions) (core.DumpResults, error) {
func (m *mockExecs) Dump(ctx context.Context, opts core.DumpOptions) (core.DumpResults, error) {
args := m.Called(opts)
return core.DumpResults{}, args.Error(0)
}

func (m *mockExecs) Restore(opts core.RestoreOptions) error {
func (m *mockExecs) Restore(ctx context.Context, opts core.RestoreOptions) error {
args := m.Called(opts)
return args.Error(0)
}

func (m *mockExecs) Prune(opts core.PruneOptions) error {
func (m *mockExecs) Prune(ctx context.Context, opts core.PruneOptions) error {
args := m.Called(opts)
return args.Error(0)
}
Expand Down
211 changes: 137 additions & 74 deletions cmd/dump.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package cmd

import (
"context"
"fmt"
"strings"

"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.opentelemetry.io/otel/codes"

"github.com/databacker/api/go/api"
"github.com/databacker/mysql-backup/pkg/compression"
"github.com/databacker/mysql-backup/pkg/core"
"github.com/databacker/mysql-backup/pkg/storage"
"github.com/databacker/mysql-backup/pkg/util"
)

const (
Expand Down Expand Up @@ -38,92 +42,84 @@ func dumpCmd(passedExecs execs, cmdConfig *cmdConfiguration) (*cobra.Command, er
bindFlags(cmd, v)
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// this is the tracer that we will use throughout the entire run
tracer := getTracer("dump")
ctx = util.ContextWithTracer(ctx, tracer)
_, startupSpan := tracer.Start(ctx, "startup")
cmdConfig.logger.Debug("starting dump")
defer func() {
tp := getTracerProvider()
tp.ForceFlush(ctx)
_ = tp.Shutdown(ctx)
}()
// check targets
targetURLs := v.GetStringSlice("target")
var (
targets []storage.Storage
err error
dumpConfig *api.Dump
scriptsConfig *api.Scripts
)
if len(targetURLs) > 0 {
for _, t := range targetURLs {
store, err := storage.ParseURL(t, cmdConfig.creds)
if err != nil {
return fmt.Errorf("invalid target url: %v", err)
}
targets = append(targets, store)
}
} else {
// try the config file
if cmdConfig.configuration != nil {
// parse the target objects, then the ones listed for the backup
targetStructures := cmdConfig.configuration.Targets
dumpTargets := cmdConfig.configuration.Dump.Targets
for _, t := range dumpTargets {
var store storage.Storage
if target, ok := targetStructures[t]; !ok {
return fmt.Errorf("target %s from dump configuration not found in targets configuration", t)
} else {
store, err = target.Storage.Storage()
if err != nil {
return fmt.Errorf("target %s from dump configuration has invalid URL: %v", t, err)
}
}
targets = append(targets, store)
}
if cmdConfig.configuration != nil {
dumpConfig = cmdConfig.configuration.Dump
if dumpConfig != nil {
scriptsConfig = dumpConfig.Scripts
}
}
targets, err := parseTargets(targetURLs, cmdConfig)
if err != nil {
return fmt.Errorf("error parsing targets: %v", err)
}
if len(targets) == 0 {
return fmt.Errorf("no targets specified")
}
safechars := v.GetBool("safechars")
if !v.IsSet("safechars") && cmdConfig.configuration != nil {
safechars = cmdConfig.configuration.Dump.Safechars
if !v.IsSet("safechars") && dumpConfig != nil && dumpConfig.Safechars != nil {
safechars = *dumpConfig.Safechars
}
include := v.GetStringSlice("include")
if len(include) == 0 && cmdConfig.configuration != nil {
include = cmdConfig.configuration.Dump.Include
if len(include) == 0 && dumpConfig != nil && dumpConfig.Include != nil {
include = *dumpConfig.Include
}
// make this slice nil if it's empty, so it is consistent; used mainly for test consistency
if len(include) == 0 {
include = nil
}
exclude := v.GetStringSlice("exclude")
if len(exclude) == 0 && cmdConfig.configuration != nil {
exclude = cmdConfig.configuration.Dump.Exclude
if len(exclude) == 0 && dumpConfig != nil && dumpConfig.Exclude != nil {
exclude = *dumpConfig.Exclude
}
// make this slice nil if it's empty, so it is consistent; used mainly for test consistency
if len(exclude) == 0 {
exclude = nil
}
preBackupScripts := v.GetString("pre-backup-scripts")
if preBackupScripts == "" && cmdConfig.configuration != nil {
preBackupScripts = cmdConfig.configuration.Dump.Scripts.PreBackup
if preBackupScripts == "" && scriptsConfig != nil && scriptsConfig.PreBackup != nil {
preBackupScripts = *scriptsConfig.PreBackup
}
postBackupScripts := v.GetString("post-backup-scripts")
if postBackupScripts == "" && cmdConfig.configuration != nil {
postBackupScripts = cmdConfig.configuration.Dump.Scripts.PostBackup
if postBackupScripts == "" && scriptsConfig != nil && scriptsConfig.PostBackup != nil {
postBackupScripts = *scriptsConfig.PostBackup
}
noDatabaseName := v.GetBool("no-database-name")
if !v.IsSet("no-database-name") && cmdConfig.configuration != nil {
noDatabaseName = cmdConfig.configuration.Dump.NoDatabaseName
if !v.IsSet("no-database-name") && dumpConfig != nil && dumpConfig.NoDatabaseName != nil {
noDatabaseName = *dumpConfig.NoDatabaseName
}
compact := v.GetBool("compact")
if !v.IsSet("compact") && cmdConfig.configuration != nil {
compact = cmdConfig.configuration.Dump.Compact
if !v.IsSet("compact") && dumpConfig != nil && dumpConfig.Compact != nil {
compact = *dumpConfig.Compact
}
maxAllowedPacket := v.GetInt("max-allowed-packet")
if !v.IsSet("max-allowed-packet") && cmdConfig.configuration != nil && cmdConfig.configuration.Dump.MaxAllowedPacket != 0 {
maxAllowedPacket = cmdConfig.configuration.Dump.MaxAllowedPacket
if !v.IsSet("max-allowed-packet") && dumpConfig != nil && dumpConfig.MaxAllowedPacket != nil && *dumpConfig.MaxAllowedPacket != 0 {
maxAllowedPacket = *dumpConfig.MaxAllowedPacket
}

// compression algorithm: check config, then CLI/env var overrides
var (
compressionAlgo string
compressor compression.Compressor
)
if cmdConfig.configuration != nil {
compressionAlgo = cmdConfig.configuration.Dump.Compression
if cmdConfig.configuration != nil && dumpConfig.Compression != nil {
compressionAlgo = *dumpConfig.Compression
}
compressionVar := v.GetString("compression")
if compressionVar != "" {
Expand All @@ -138,41 +134,21 @@ func dumpCmd(passedExecs execs, cmdConfig *cmdConfiguration) (*cobra.Command, er

// retention, if enabled
retention := v.GetString("retention")
if retention == "" && cmdConfig.configuration != nil {
retention = cmdConfig.configuration.Prune.Retention
if retention == "" && cmdConfig.configuration != nil && cmdConfig.configuration.Prune.Retention != nil {
retention = *cmdConfig.configuration.Prune.Retention
}
filenamePattern := v.GetString("filename-pattern")

if !v.IsSet("filename-pattern") && cmdConfig.configuration != nil {
filenamePattern = cmdConfig.configuration.Dump.FilenamePattern
if !v.IsSet("filename-pattern") && dumpConfig != nil && dumpConfig.FilenamePattern != nil {
filenamePattern = *dumpConfig.FilenamePattern
}
if filenamePattern == "" {
filenamePattern = defaultFilenamePattern
}

// timer options
once := v.GetBool("once")
if !v.IsSet("once") && cmdConfig.configuration != nil {
once = cmdConfig.configuration.Dump.Schedule.Once
}
cron := v.GetString("cron")
if cron == "" && cmdConfig.configuration != nil {
cron = cmdConfig.configuration.Dump.Schedule.Cron
}
begin := v.GetString("begin")
if begin == "" && cmdConfig.configuration != nil {
begin = cmdConfig.configuration.Dump.Schedule.Begin
}
frequency := v.GetInt("frequency")
if frequency == 0 && cmdConfig.configuration != nil {
frequency = cmdConfig.configuration.Dump.Schedule.Frequency
}
timerOpts := core.TimerOptions{
Once: once,
Cron: cron,
Begin: begin,
Frequency: frequency,
}
timerOpts := parseTimerOptions(v, cmdConfig.configuration)

var executor execs
executor = &core.Executor{}
if passedExecs != nil {
Expand All @@ -182,7 +158,14 @@ func dumpCmd(passedExecs execs, cmdConfig *cmdConfiguration) (*cobra.Command, er

// at this point, any errors should not have usage
cmd.SilenceUsage = true

// done with the startup
startupSpan.End()

if err := executor.Timer(timerOpts, func() error {
// start a new span for the dump, should not be a child of the startup one
tracerCtx, dumpSpan := tracer.Start(ctx, "run")
defer dumpSpan.End()
uid := uuid.New()
dumpOpts := core.DumpOptions{
Targets: targets,
Expand All @@ -199,15 +182,18 @@ func dumpCmd(passedExecs execs, cmdConfig *cmdConfiguration) (*cobra.Command, er
Run: uid,
FilenamePattern: filenamePattern,
}
_, err := executor.Dump(dumpOpts)
_, err := executor.Dump(tracerCtx, dumpOpts)
if err != nil {
dumpSpan.SetStatus(codes.Error, fmt.Sprintf("error running dump: %v", err))
return fmt.Errorf("error running dump: %w", err)
}
if retention != "" {
if err := executor.Prune(core.PruneOptions{Targets: targets, Retention: retention}); err != nil {
if err := executor.Prune(tracerCtx, core.PruneOptions{Targets: targets, Retention: retention}); err != nil {
dumpSpan.SetStatus(codes.Error, fmt.Sprintf("error running prune: %v", err))
return fmt.Errorf("error running prune: %w", err)
}
}
dumpSpan.SetStatus(codes.Ok, "dump complete")
return nil
}); err != nil {
return fmt.Errorf("error running command: %w", err)
Expand Down Expand Up @@ -278,3 +264,80 @@ S3: If it is a URL of the format s3://bucketname/path then it will connect via S

return cmd, nil
}

func parseTimerOptions(v *viper.Viper, config *api.ConfigSpec) core.TimerOptions {
var scheduleConfig *api.Schedule
if config != nil {
dumpConfig := config.Dump
if dumpConfig != nil {
scheduleConfig = dumpConfig.Schedule
}
}
once := v.GetBool("once")
if !v.IsSet("once") && scheduleConfig != nil && scheduleConfig.Once != nil {
once = *scheduleConfig.Once
}
cron := v.GetString("cron")
if cron == "" && scheduleConfig != nil && scheduleConfig.Cron != nil {
cron = *scheduleConfig.Cron
}
begin := v.GetString("begin")
if begin == "" && scheduleConfig != nil && scheduleConfig.Begin != nil {
begin = fmt.Sprintf("%d", *scheduleConfig.Begin)
}
frequency := v.GetInt("frequency")
if frequency == 0 && scheduleConfig != nil && scheduleConfig.Frequency != nil {
frequency = *scheduleConfig.Frequency
}
return core.TimerOptions{
Once: once,
Cron: cron,
Begin: begin,
Frequency: frequency,
}

}

func parseTargets(urls []string, cmdConfig *cmdConfiguration) ([]storage.Storage, error) {
var targets []storage.Storage
if len(urls) > 0 {
for _, t := range urls {
store, err := storage.ParseURL(t, cmdConfig.creds)
if err != nil {
return nil, fmt.Errorf("invalid target url: %v", err)
}
targets = append(targets, store)
}
} else {
// try the config file
if cmdConfig.configuration != nil {
// parse the target objects, then the ones listed for the backup
var (
targetStructures map[string]api.Target
dumpTargets []string
)
if cmdConfig.configuration.Targets != nil {
targetStructures = *cmdConfig.configuration.Targets
}
if cmdConfig.configuration != nil && cmdConfig.configuration.Dump != nil && cmdConfig.configuration.Dump.Targets != nil {
dumpTargets = *cmdConfig.configuration.Dump.Targets
}
for _, t := range dumpTargets {
var (
store storage.Storage
err error
)
if target, ok := targetStructures[t]; !ok {
return nil, fmt.Errorf("target %s from dump configuration not found in targets configuration", t)
} else {
store, err = storage.FromTarget(target)
if err != nil {
return nil, fmt.Errorf("target %s from dump configuration has invalid URL: %v", t, err)
}
}
targets = append(targets, store)
}
}
}
return targets, nil
}
Loading