|
| 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 | +} |
0 commit comments