Skip to content

Commit 3e89d32

Browse files
Dipta Dastamalsaha
authored andcommitted
Backup and restore Postgres DB (#695)
- [x] merge #694 - [x] merge #691 - [x] Additional arguments can be passed as a single string using `pgArgs` param in Backup Config Ref: https://github.com/kubedb/postgres/tree/master/hack/docker/postgres-tools/10.2
1 parent 03b95f3 commit 3e89d32

File tree

3 files changed

+319
-3
lines changed

3 files changed

+319
-3
lines changed

backup_pg.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package cmds
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"path/filepath"
7+
"time"
8+
9+
"github.com/appscode/go/flags"
10+
"github.com/appscode/go/log"
11+
"github.com/appscode/stash/pkg/restic"
12+
"github.com/spf13/cobra"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/util/errors"
15+
"k8s.io/client-go/kubernetes"
16+
"k8s.io/client-go/tools/clientcmd"
17+
appcatalog_cs "kmodules.xyz/custom-resources/client/clientset/versioned"
18+
)
19+
20+
const (
21+
JobPGBackup = "stash-pg-backup"
22+
PostgresUser = "POSTGRES_USER"
23+
PostgresPassword = "POSTGRES_PASSWORD"
24+
EnvPgPassword = "PGPASSWORD"
25+
PgDumpFile = "dumpfile.sql"
26+
PgDumpCMD = "pg_dumpall"
27+
PgRestoreCMD = "psql"
28+
)
29+
30+
func NewCmdBackupPG() *cobra.Command {
31+
var (
32+
masterURL string
33+
kubeconfigPath string
34+
namespace string
35+
appBindingName string
36+
pgArgs string
37+
outputDir string
38+
setupOpt = restic.SetupOptions{
39+
ScratchDir: restic.DefaultScratchDir,
40+
EnableCache: false,
41+
}
42+
backupOpt = restic.BackupOptions{
43+
StdinFileName: PgDumpFile,
44+
}
45+
metrics = restic.MetricsOptions{
46+
JobName: JobPGBackup,
47+
}
48+
)
49+
50+
cmd := &cobra.Command{
51+
Use: "backup-pg",
52+
Short: "Takes a backup of Postgres DB",
53+
DisableAutoGenTag: true,
54+
RunE: func(cmd *cobra.Command, args []string) error {
55+
flags.EnsureRequiredFlags(cmd, "app-binding", "provider", "secret-dir")
56+
57+
// prepare client
58+
config, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfigPath)
59+
if err != nil {
60+
return err
61+
}
62+
kubeClient, err := kubernetes.NewForConfig(config)
63+
if err != nil {
64+
return err
65+
}
66+
appCatalogClient, err := appcatalog_cs.NewForConfig(config)
67+
if err != nil {
68+
return err
69+
}
70+
71+
// get app binding
72+
appBinding, err := appCatalogClient.AppcatalogV1alpha1().AppBindings(namespace).Get(appBindingName, metav1.GetOptions{})
73+
if err != nil {
74+
return err
75+
}
76+
// get secret
77+
appBindingSecret, err := kubeClient.CoreV1().Secrets(namespace).Get(appBinding.Spec.Secret.Name, metav1.GetOptions{})
78+
if err != nil {
79+
return err
80+
}
81+
82+
// init restic wrapper
83+
resticWrapper, err := restic.NewResticWrapper(setupOpt)
84+
if err != nil {
85+
return err
86+
}
87+
88+
// set env for pg_dump
89+
resticWrapper.SetEnv(EnvPgPassword, string(appBindingSecret.Data[PostgresPassword]))
90+
// setup pipe command
91+
backupOpt.StdinPipeCommand = restic.Command{
92+
Name: PgDumpCMD,
93+
Args: []interface{}{
94+
"-U", string(appBindingSecret.Data[PostgresUser]),
95+
"-h", appBinding.Spec.ClientConfig.Service.Name,
96+
},
97+
}
98+
if pgArgs != "" {
99+
backupOpt.StdinPipeCommand.Args = append(backupOpt.StdinPipeCommand.Args, pgArgs)
100+
}
101+
102+
// wait for DB ready
103+
waitForDBReady(appBinding.Spec.ClientConfig.Service.Name, appBinding.Spec.ClientConfig.Service.Port)
104+
105+
// Run backup
106+
backupOutput, backupErr := resticWrapper.RunBackup(backupOpt)
107+
// If metrics are enabled then generate metrics
108+
if metrics.Enabled {
109+
err := backupOutput.HandleMetrics(&metrics, backupErr)
110+
if err != nil {
111+
return errors.NewAggregate([]error{backupErr, err})
112+
}
113+
}
114+
// If output directory specified, then write the output in "output.json" file in the specified directory
115+
if backupErr == nil && outputDir != "" {
116+
err := backupOutput.WriteOutput(filepath.Join(outputDir, restic.DefaultOutputFileName))
117+
if err != nil {
118+
return err
119+
}
120+
}
121+
return backupErr
122+
},
123+
}
124+
125+
cmd.Flags().StringVar(&pgArgs, "pg-args", pgArgs, "Additional arguments")
126+
127+
cmd.Flags().StringVar(&masterURL, "master", masterURL, "The address of the Kubernetes API server (overrides any value in kubeconfig)")
128+
cmd.Flags().StringVar(&kubeconfigPath, "kubeconfig", kubeconfigPath, "Path to kubeconfig file with authorization information (the master location is set by the master flag).")
129+
cmd.Flags().StringVar(&namespace, "namespace", "default", "Namespace of Backup/Restore Session")
130+
cmd.Flags().StringVar(&appBindingName, "app-binding", appBindingName, "Name of the app binding")
131+
132+
cmd.Flags().StringVar(&setupOpt.Provider, "provider", setupOpt.Provider, "Backend provider (i.e. gcs, s3, azure etc)")
133+
cmd.Flags().StringVar(&setupOpt.Bucket, "bucket", setupOpt.Bucket, "Name of the cloud bucket/container (keep empty for local backend)")
134+
cmd.Flags().StringVar(&setupOpt.Endpoint, "endpoint", setupOpt.Endpoint, "Endpoint for s3/s3 compatible backend")
135+
cmd.Flags().StringVar(&setupOpt.Path, "path", setupOpt.Path, "Directory inside the bucket where backup will be stored")
136+
cmd.Flags().StringVar(&setupOpt.SecretDir, "secret-dir", setupOpt.SecretDir, "Directory where storage secret has been mounted")
137+
cmd.Flags().StringVar(&setupOpt.ScratchDir, "scratch-dir", setupOpt.ScratchDir, "Temporary directory")
138+
cmd.Flags().BoolVar(&setupOpt.EnableCache, "enable-cache", setupOpt.EnableCache, "Specify weather to enable caching for restic")
139+
140+
cmd.Flags().StringVar(&backupOpt.Host, "hostname", backupOpt.Host, "Name of the host machine")
141+
142+
cmd.Flags().IntVar(&backupOpt.RetentionPolicy.KeepLast, "retention-keep-last", backupOpt.RetentionPolicy.KeepLast, "Specify value for retention strategy")
143+
cmd.Flags().IntVar(&backupOpt.RetentionPolicy.KeepHourly, "retention-keep-hourly", backupOpt.RetentionPolicy.KeepHourly, "Specify value for retention strategy")
144+
cmd.Flags().IntVar(&backupOpt.RetentionPolicy.KeepDaily, "retention-keep-daily", backupOpt.RetentionPolicy.KeepDaily, "Specify value for retention strategy")
145+
cmd.Flags().IntVar(&backupOpt.RetentionPolicy.KeepWeekly, "retention-keep-weekly", backupOpt.RetentionPolicy.KeepWeekly, "Specify value for retention strategy")
146+
cmd.Flags().IntVar(&backupOpt.RetentionPolicy.KeepMonthly, "retention-keep-monthly", backupOpt.RetentionPolicy.KeepMonthly, "Specify value for retention strategy")
147+
cmd.Flags().IntVar(&backupOpt.RetentionPolicy.KeepYearly, "retention-keep-yearly", backupOpt.RetentionPolicy.KeepYearly, "Specify value for retention strategy")
148+
cmd.Flags().StringSliceVar(&backupOpt.RetentionPolicy.KeepTags, "retention-keep-tags", backupOpt.RetentionPolicy.KeepTags, "Specify value for retention strategy")
149+
cmd.Flags().BoolVar(&backupOpt.RetentionPolicy.Prune, "retention-prune", backupOpt.RetentionPolicy.Prune, "Specify weather to prune old snapshot data")
150+
cmd.Flags().BoolVar(&backupOpt.RetentionPolicy.DryRun, "retention-dry-run", backupOpt.RetentionPolicy.DryRun, "Specify weather to test retention policy without deleting actual data")
151+
152+
cmd.Flags().StringVar(&outputDir, "output-dir", outputDir, "Directory where output.json file will be written (keep empty if you don't need to write output in file)")
153+
154+
cmd.Flags().BoolVar(&metrics.Enabled, "metrics-enabled", metrics.Enabled, "Specify weather to export Prometheus metrics")
155+
cmd.Flags().StringVar(&metrics.PushgatewayURL, "metrics-pushgateway-url", metrics.PushgatewayURL, "Pushgateway URL where the metrics will be pushed")
156+
cmd.Flags().StringVar(&metrics.MetricFileDir, "metrics-dir", metrics.MetricFileDir, "Directory where to write metric.prom file (keep empty if you don't want to write metric in a text file)")
157+
cmd.Flags().StringSliceVar(&metrics.Labels, "metrics-labels", metrics.Labels, "Labels to apply in exported metrics")
158+
159+
return cmd
160+
}
161+
162+
func waitForDBReady(host string, port int32) {
163+
log.Infoln("Checking database connection")
164+
cmd := fmt.Sprintf(`nc "%s" "%d" -w 30`, host, port)
165+
for {
166+
if err := exec.Command(cmd).Run(); err != nil {
167+
break
168+
}
169+
log.Infoln("Waiting... database is not ready yet")
170+
time.Sleep(5 * time.Second)
171+
}
172+
}

restore_pg.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package cmds
2+
3+
import (
4+
"path/filepath"
5+
6+
"github.com/appscode/go/flags"
7+
"github.com/appscode/stash/pkg/restic"
8+
"github.com/spf13/cobra"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/util/errors"
11+
"k8s.io/client-go/kubernetes"
12+
"k8s.io/client-go/tools/clientcmd"
13+
appcatalog_cs "kmodules.xyz/custom-resources/client/clientset/versioned"
14+
)
15+
16+
func NewCmdRestorePG() *cobra.Command {
17+
var (
18+
masterURL string
19+
kubeconfigPath string
20+
namespace string
21+
appBindingName string
22+
outputDir string
23+
pgArgs string
24+
setupOpt = restic.SetupOptions{
25+
ScratchDir: restic.DefaultScratchDir,
26+
EnableCache: false,
27+
}
28+
dumpOpt = restic.DumpOptions{
29+
FileName: PgDumpFile,
30+
}
31+
metrics = restic.MetricsOptions{
32+
JobName: JobPGBackup,
33+
}
34+
)
35+
36+
cmd := &cobra.Command{
37+
Use: "restore-pg",
38+
Short: "Restores Postgres DB Backup",
39+
DisableAutoGenTag: true,
40+
RunE: func(cmd *cobra.Command, args []string) error {
41+
flags.EnsureRequiredFlags(cmd, "app-binding", "provider", "secret-dir")
42+
43+
// prepare client
44+
config, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfigPath)
45+
if err != nil {
46+
return err
47+
}
48+
kubeClient, err := kubernetes.NewForConfig(config)
49+
if err != nil {
50+
return err
51+
}
52+
appCatalogClient, err := appcatalog_cs.NewForConfig(config)
53+
if err != nil {
54+
return err
55+
}
56+
57+
// get app binding
58+
appBinding, err := appCatalogClient.AppcatalogV1alpha1().AppBindings(namespace).Get(appBindingName, metav1.GetOptions{})
59+
if err != nil {
60+
return err
61+
}
62+
// get secret
63+
appBindingSecret, err := kubeClient.CoreV1().Secrets(namespace).Get(appBinding.Spec.Secret.Name, metav1.GetOptions{})
64+
if err != nil {
65+
return err
66+
}
67+
68+
// init restic wrapper
69+
resticWrapper, err := restic.NewResticWrapper(setupOpt)
70+
if err != nil {
71+
return err
72+
}
73+
74+
// set env for psql
75+
resticWrapper.SetEnv(EnvPgPassword, string(appBindingSecret.Data[PostgresPassword]))
76+
// setup pipe command
77+
dumpOpt.StdoutPipeCommand = restic.Command{
78+
Name: PgRestoreCMD,
79+
Args: []interface{}{
80+
"-U", string(appBindingSecret.Data[PostgresUser]),
81+
"-h", appBinding.Spec.ClientConfig.Service.Name,
82+
},
83+
}
84+
if pgArgs != "" {
85+
dumpOpt.StdoutPipeCommand.Args = append(dumpOpt.StdoutPipeCommand.Args, pgArgs)
86+
}
87+
88+
// wait for DB ready
89+
waitForDBReady(appBinding.Spec.ClientConfig.Service.Name, appBinding.Spec.ClientConfig.Service.Port)
90+
91+
// Run dump
92+
dumpOutput, backupErr := resticWrapper.Dump(dumpOpt)
93+
// If metrics are enabled then generate metrics
94+
if metrics.Enabled {
95+
err := dumpOutput.HandleMetrics(&metrics, backupErr)
96+
if err != nil {
97+
return errors.NewAggregate([]error{backupErr, err})
98+
}
99+
}
100+
// If output directory specified, then write the output in "output.json" file in the specified directory
101+
if backupErr == nil && outputDir != "" {
102+
err := dumpOutput.WriteOutput(filepath.Join(outputDir, restic.DefaultOutputFileName))
103+
if err != nil {
104+
return err
105+
}
106+
}
107+
return backupErr
108+
},
109+
}
110+
111+
cmd.Flags().StringVar(&pgArgs, "pg-args", pgArgs, "Additional arguments")
112+
113+
cmd.Flags().StringVar(&masterURL, "master", masterURL, "The address of the Kubernetes API server (overrides any value in kubeconfig)")
114+
cmd.Flags().StringVar(&kubeconfigPath, "kubeconfig", kubeconfigPath, "Path to kubeconfig file with authorization information (the master location is set by the master flag).")
115+
cmd.Flags().StringVar(&namespace, "namespace", "default", "Namespace of Backup/Restore Session")
116+
cmd.Flags().StringVar(&appBindingName, "app-binding", appBindingName, "Name of the app binding")
117+
118+
cmd.Flags().StringVar(&setupOpt.Provider, "provider", setupOpt.Provider, "Backend provider (i.e. gcs, s3, azure etc)")
119+
cmd.Flags().StringVar(&setupOpt.Bucket, "bucket", setupOpt.Bucket, "Name of the cloud bucket/container (keep empty for local backend)")
120+
cmd.Flags().StringVar(&setupOpt.Endpoint, "endpoint", setupOpt.Endpoint, "Endpoint for s3/s3 compatible backend")
121+
cmd.Flags().StringVar(&setupOpt.Path, "path", setupOpt.Path, "Directory inside the bucket where backup will be stored")
122+
cmd.Flags().StringVar(&setupOpt.SecretDir, "secret-dir", setupOpt.SecretDir, "Directory where storage secret has been mounted")
123+
cmd.Flags().StringVar(&setupOpt.ScratchDir, "scratch-dir", setupOpt.ScratchDir, "Temporary directory")
124+
cmd.Flags().BoolVar(&setupOpt.EnableCache, "enable-cache", setupOpt.EnableCache, "Specify weather to enable caching for restic")
125+
126+
cmd.Flags().StringVar(&dumpOpt.Host, "hostname", dumpOpt.Host, "Name of the host machine")
127+
// TODO: sliceVar
128+
cmd.Flags().StringVar(&dumpOpt.Snapshot, "snapshot", dumpOpt.Snapshot, "Snapshot to dump")
129+
130+
cmd.Flags().StringVar(&outputDir, "output-dir", outputDir, "Directory where output.json file will be written (keep empty if you don't need to write output in file)")
131+
132+
cmd.Flags().BoolVar(&metrics.Enabled, "metrics-enabled", metrics.Enabled, "Specify weather to export Prometheus metrics")
133+
cmd.Flags().StringVar(&metrics.PushgatewayURL, "metrics-pushgateway-url", metrics.PushgatewayURL, "Pushgateway URL where the metrics will be pushed")
134+
cmd.Flags().StringVar(&metrics.MetricFileDir, "metrics-dir", metrics.MetricFileDir, "Directory where to write metric.prom file (keep empty if you don't want to write metric in a text file)")
135+
cmd.Flags().StringSliceVar(&metrics.Labels, "metrics-labels", metrics.Labels, "Labels to apply in exported metrics")
136+
137+
return cmd
138+
}

root.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,8 @@ func NewRootCmd() *cobra.Command {
4242
rootCmd.AddCommand(v.NewCmdVersion())
4343
stopCh := genericapiserver.SetupSignalHandler()
4444
rootCmd.AddCommand(NewCmdRun(os.Stdout, os.Stderr, stopCh))
45+
4546
rootCmd.AddCommand(NewCmdBackup())
46-
rootCmd.AddCommand(NewCmdBackupPVC())
47-
rootCmd.AddCommand(NewCmdRestorePVC())
48-
rootCmd.AddCommand(NewCmdUpdateStatus())
4947
rootCmd.AddCommand(NewCmdRecover())
5048
rootCmd.AddCommand(NewCmdCheck())
5149
rootCmd.AddCommand(NewCmdScaleDown())
@@ -55,5 +53,13 @@ func NewRootCmd() *cobra.Command {
5553
rootCmd.AddCommand(NewCmdRestore())
5654
rootCmd.AddCommand(NewCmdRunBackup())
5755

56+
rootCmd.AddCommand(NewCmdBackupPVC())
57+
rootCmd.AddCommand(NewCmdRestorePVC())
58+
59+
rootCmd.AddCommand(NewCmdBackupPG())
60+
rootCmd.AddCommand(NewCmdRestorePG())
61+
62+
rootCmd.AddCommand(NewCmdUpdateStatus())
63+
5864
return rootCmd
5965
}

0 commit comments

Comments
 (0)