Skip to content

Commit aa50506

Browse files
authored
Separate backup (#87)
* Separate backup * separates out the backup logic from the cobra command * adds a basic test to the backup command Note: `backup` updates the cluster spec, which updates the job pod, so this command is best tested with an e2e test with a running PGO. Issue: PGO-663
1 parent 9b3a84f commit aa50506

File tree

4 files changed

+142
-73
lines changed

4 files changed

+142
-73
lines changed

internal/cmd/backup.go

Lines changed: 81 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2424
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2525
"k8s.io/apimachinery/pkg/types"
26+
"k8s.io/client-go/dynamic"
2627

2728
"github.com/crunchydata/postgres-operator-client/internal"
2829
"github.com/crunchydata/postgres-operator-client/internal/apis/postgres-operator.crunchydata.com/v1beta1"
@@ -47,6 +48,8 @@ the --force-conflicts flag.
4748
postgresclusters.postgres-operator.crunchydata.com [get patch]
4849
4950
### Usage`,
51+
// Limit the number of args, that is, only one cluster name
52+
Args: cobra.ExactArgs(1),
5053
}
5154

5255
cmdBackup.Example = internal.FormatExample(`# Trigger a backup on the 'hippo' postgrescluster using the current spec options
@@ -63,13 +66,10 @@ pgo backup hippo --force-conflicts
6366
### Example output
6467
postgresclusters/hippo backup initiated`)
6568

66-
// Limit the number of args, that is, only one cluster name
67-
cmdBackup.Args = cobra.ExactArgs(1)
68-
6969
// `backup` command accepts `repoName`, `force-conflicts` and `options` flags;
7070
// multiple options flags can be used, with each becoming a new line
7171
// in the options array on the spec
72-
backup := pgBackRestBackup{}
72+
backup := pgBackRestBackupArgs{}
7373
cmdBackup.Flags().BoolVar(&backup.ForceConflicts, "force-conflicts", false, "take ownership and overwrite the backup settings")
7474
cmdBackup.Flags().StringVar(&backup.RepoName, "repoName", "", "repoName to backup to")
7575
cmdBackup.Flags().StringArrayVar(&backup.Options, "options", []string{},
@@ -79,88 +79,47 @@ postgresclusters/hippo backup initiated`)
7979
cmdBackup.RunE = func(cmd *cobra.Command, args []string) error {
8080

8181
// configure client
82-
ctx := context.Background()
8382
mapping, client, err := v1beta1.NewPostgresClusterClient(config)
8483
if err != nil {
8584
return err
8685
}
8786

88-
// Get the namespace. This will either be from the Kubernetes configuration
89-
// or from the --namespace (-n) flag.
90-
configNamespace, err := config.Namespace()
91-
if err != nil {
92-
return err
93-
}
94-
95-
cluster, err := client.Namespace(configNamespace).Get(ctx,
96-
args[0], // the name of the cluster object, limited to one name through `ExactArgs(1)`
97-
metav1.GetOptions{},
98-
)
99-
if err != nil {
100-
return err
101-
}
102-
103-
intent := new(unstructured.Unstructured)
104-
if err := internal.ExtractFieldsInto(cluster, intent, config.Patch.FieldManager); err != nil {
105-
return err
106-
}
107-
if err := backup.modifyIntent(intent, time.Now()); err != nil {
108-
return err
109-
}
110-
111-
patch, err := intent.MarshalJSON()
112-
if err != nil {
113-
cmd.Printf("\nError packaging payload: %s\n", err)
114-
return err
115-
}
87+
// Pass args[0] as the name of the cluster object, limited to one through `ExactArgs(1)`
88+
backup.ClusterName = args[0]
11689

117-
// Update the spec/annotate
118-
// TODO(benjaminjb): Would we want to allow a dry-run option here?
119-
patchOptions := metav1.PatchOptions{}
120-
if backup.ForceConflicts {
121-
b := true
122-
patchOptions.Force = &b
90+
msg, err := backup.Run(client, config)
91+
if msg != "" {
92+
cmd.Println(msg)
12393
}
124-
_, err = client.Namespace(configNamespace).Patch(ctx,
125-
args[0], // the name of the cluster object, limited to one name through `ExactArgs(1)`
126-
types.ApplyPatchType,
127-
patch,
128-
config.Patch.PatchOptions(patchOptions),
129-
)
130-
131-
if err != nil {
132-
if apierrors.IsConflict(err) {
133-
cmd.Printf("SUGGESTION: The --force-conflicts flag may help in performing this operation.")
134-
}
135-
cmd.Printf("\nError requesting update: %s\n", err)
136-
return err
94+
if err == nil {
95+
// Our `backup` command initiates a job, but does not signal to the user
96+
// that a backup has finished; consider a `--wait` flag to wait until the
97+
// backup is done.
98+
cmd.Printf("%s/%s backup initiated\n", mapping.Resource.Resource, backup.ClusterName)
13799
}
138100

139-
// Print the output received.
140-
// TODO(benjaminjb): consider a more informative output
141-
cmd.Printf("%s/%s backup initiated\n", mapping.Resource.Resource, args[0])
142-
143-
return nil
101+
return err
144102
}
145103

146104
return cmdBackup
147105
}
148106

149-
type pgBackRestBackup struct {
107+
type pgBackRestBackupArgs struct {
108+
ClusterName string
109+
ForceConflicts bool
150110
Options []string
151111
RepoName string
152-
ForceConflicts bool
153112
}
154113

155-
func (config pgBackRestBackup) modifyIntent(
114+
func (backup pgBackRestBackupArgs) modifyIntent(
156115
intent *unstructured.Unstructured, now time.Time,
157116
) error {
158117
intent.SetAnnotations(internal.MergeStringMaps(
159118
intent.GetAnnotations(), map[string]string{
160119
"postgres-operator.crunchydata.com/pgbackrest-backup": now.UTC().Format(time.RFC3339),
161120
}))
162121

163-
if value, path := config.Options, []string{
122+
if value, path := backup.Options, []string{
164123
"spec", "backups", "pgbackrest", "manual", "options",
165124
}; len(value) == 0 {
166125
unstructured.RemoveNestedField(intent.Object, path...)
@@ -170,7 +129,7 @@ func (config pgBackRestBackup) modifyIntent(
170129
return err
171130
}
172131

173-
if value, path := config.RepoName, []string{
132+
if value, path := backup.RepoName, []string{
174133
"spec", "backups", "pgbackrest", "manual", "repoName",
175134
}; len(value) == 0 {
176135
unstructured.RemoveNestedField(intent.Object, path...)
@@ -185,3 +144,63 @@ func (config pgBackRestBackup) modifyIntent(
185144

186145
return nil
187146
}
147+
148+
func (backup pgBackRestBackupArgs) Run(client dynamic.NamespaceableResourceInterface,
149+
config *internal.Config) (string, error) {
150+
151+
var (
152+
cluster *unstructured.Unstructured
153+
err error
154+
namespace string
155+
patch []byte
156+
)
157+
158+
ctx := context.Background()
159+
160+
// Get the namespace. This will either be from the Kubernetes configuration
161+
// or from the --namespace (-n) flag.
162+
if namespace, err = config.Namespace(); err != nil {
163+
return "", err
164+
}
165+
166+
if cluster, err = client.Namespace(namespace).Get(ctx,
167+
backup.ClusterName,
168+
metav1.GetOptions{},
169+
); err != nil {
170+
return "", err
171+
}
172+
173+
intent := new(unstructured.Unstructured)
174+
if err = internal.ExtractFieldsInto(
175+
cluster, intent, config.Patch.FieldManager); err != nil {
176+
return "", err
177+
}
178+
if err = backup.modifyIntent(intent, time.Now()); err != nil {
179+
return "", err
180+
}
181+
182+
if patch, err = intent.MarshalJSON(); err != nil {
183+
return "Error packaging payload", err
184+
}
185+
186+
// Update the spec/annotate
187+
// TODO(benjaminjb): Would we want to allow a dry-run option here?
188+
patchOptions := metav1.PatchOptions{}
189+
if backup.ForceConflicts {
190+
b := true
191+
patchOptions.Force = &b
192+
}
193+
if _, err = client.Namespace(namespace).Patch(ctx,
194+
backup.ClusterName,
195+
types.ApplyPatchType,
196+
patch,
197+
config.Patch.PatchOptions(patchOptions),
198+
); err != nil {
199+
if apierrors.IsConflict(err) {
200+
return "SUGGESTION: The --force-conflicts flag may help in performing this operation.", err
201+
}
202+
return "Error requesting update", err
203+
}
204+
205+
return "", err
206+
}

internal/cmd/backup_test.go

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,33 @@
1515
package cmd
1616

1717
import (
18+
"fmt"
19+
"os"
20+
"path/filepath"
1821
"strings"
1922
"testing"
2023
"time"
2124

2225
"gotest.tools/v3/assert"
2326
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27+
"k8s.io/apimachinery/pkg/runtime"
28+
"k8s.io/apimachinery/pkg/runtime/schema"
29+
"k8s.io/cli-runtime/pkg/genericclioptions"
30+
"k8s.io/client-go/dynamic/fake"
31+
k8stesting "k8s.io/client-go/testing"
2432
"sigs.k8s.io/yaml"
2533

34+
"github.com/crunchydata/postgres-operator-client/internal"
35+
"github.com/crunchydata/postgres-operator-client/internal/apis/postgres-operator.crunchydata.com/v1beta1"
2636
"github.com/crunchydata/postgres-operator-client/internal/testing/cmp"
2737
)
2838

29-
func TestPGBackRestBackupModifyIntent(t *testing.T) {
39+
func TestPGBackRestBackupArgsModifyIntent(t *testing.T) {
3040
now := time.Date(2020, 4, 5, 6, 7, 8, 99, time.FixedZone("ZONE", -11))
3141

3242
for _, tt := range []struct {
3343
Name, Before, After string
34-
Backup pgBackRestBackup
44+
Backup pgBackRestBackupArgs
3545
}{
3646
{
3747
Name: "Zero",
@@ -43,7 +53,7 @@ metadata:
4353
},
4454
{
4555
Name: "Options",
46-
Backup: pgBackRestBackup{
56+
Backup: pgBackRestBackupArgs{
4757
Options: []string{"--quoth=raven --midnight=dreary", "--ever=never"},
4858
},
4959
After: strings.TrimSpace(`
@@ -61,7 +71,7 @@ spec:
6171
},
6272
{
6373
Name: "RepoName",
64-
Backup: pgBackRestBackup{RepoName: "testRepo"},
74+
Backup: pgBackRestBackupArgs{RepoName: "testRepo"},
6575
After: strings.TrimSpace(`
6676
metadata:
6777
annotations:
@@ -94,7 +104,7 @@ metadata:
94104
},
95105
{
96106
Name: "NewRepoButOptions",
97-
Backup: pgBackRestBackup{RepoName: "testRepo"},
107+
Backup: pgBackRestBackupArgs{RepoName: "testRepo"},
98108
Before: strings.TrimSpace(`
99109
metadata:
100110
annotations:
@@ -119,7 +129,7 @@ spec:
119129
},
120130
{
121131
Name: "NewOptionsButRepo",
122-
Backup: pgBackRestBackup{Options: []string{"a", "b c"}},
132+
Backup: pgBackRestBackupArgs{Options: []string{"a", "b c"}},
123133
Before: strings.TrimSpace(`
124134
metadata:
125135
annotations:
@@ -160,12 +170,51 @@ spec:
160170
[]byte(`{ spec: { backups: 1234 } }`), &intent.Object,
161171
))
162172

163-
err := pgBackRestBackup{Options: []string{"a"}}.modifyIntent(&intent, now)
173+
err := pgBackRestBackupArgs{Options: []string{"a"}}.modifyIntent(&intent, now)
164174
assert.ErrorContains(t, err, ".spec.backups")
165175
assert.ErrorContains(t, err, "is not a map")
166176

167-
err = pgBackRestBackup{RepoName: "b"}.modifyIntent(&intent, now)
177+
err = pgBackRestBackupArgs{RepoName: "b"}.modifyIntent(&intent, now)
168178
assert.ErrorContains(t, err, ".spec.backups")
169179
assert.ErrorContains(t, err, "is not a map")
170180
})
171181
}
182+
183+
func TestBackupRun(t *testing.T) {
184+
cf := genericclioptions.NewConfigFlags(true)
185+
nsd := "test"
186+
cf.Namespace = &nsd
187+
config := &internal.Config{
188+
ConfigFlags: cf,
189+
IOStreams: genericclioptions.IOStreams{
190+
In: os.Stdin,
191+
Out: os.Stdout,
192+
ErrOut: os.Stderr},
193+
Patch: internal.PatchConfig{FieldManager: filepath.Base(os.Args[0])},
194+
}
195+
196+
scheme := runtime.NewScheme()
197+
client := fake.NewSimpleDynamicClient(scheme)
198+
// Set up dynamicResourceClient with `fake` client
199+
gvk := v1beta1.GroupVersion.WithKind("PostgresCluster")
200+
gvr := schema.GroupVersionResource{Group: gvk.Group, Version: gvk.Version, Resource: "postgresclusters"}
201+
drc := client.Resource(gvr)
202+
203+
t.Run("PassesThroughError", func(t *testing.T) {
204+
// Have the client return an error on get
205+
client.PrependReactor("get",
206+
"postgresclusters",
207+
func(action k8stesting.Action) (bool, runtime.Object, error) {
208+
return true, nil, fmt.Errorf("whoops")
209+
})
210+
211+
backup := pgBackRestBackupArgs{
212+
ClusterName: "name",
213+
}
214+
215+
msg, err := backup.Run(drc, config)
216+
assert.Equal(t, "", msg) // No special message is passed through on get fails
217+
assert.Error(t, err, "whoops", "Error from PGO API should be passed through")
218+
})
219+
220+
}

internal/cmd/restore_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,11 @@ spec:
174174
[]byte(`{ spec: { backups: 1234 } }`), &intent.Object,
175175
))
176176

177-
err := pgBackRestBackup{Options: []string{"a"}}.modifyIntent(&intent, now)
177+
err := pgBackRestBackupArgs{Options: []string{"a"}}.modifyIntent(&intent, now)
178178
assert.ErrorContains(t, err, ".spec.backups")
179179
assert.ErrorContains(t, err, "is not a map")
180180

181-
err = pgBackRestBackup{RepoName: "b"}.modifyIntent(&intent, now)
181+
err = pgBackRestBackupArgs{RepoName: "b"}.modifyIntent(&intent, now)
182182
assert.ErrorContains(t, err, ".spec.backups")
183183
assert.ErrorContains(t, err, "is not a map")
184184
})

testing/kuttl/e2e/backup/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* Check the annotation on the cluster
2222
* No backup occurs
2323

24-
(6) 10-11
24+
(6) 10-12
2525
* Update the spec through KUTTL, changing the ownership of that field
2626
* Call the backup CLI with different flags, and see a conflict
27+
* Call the backup CLI with different flags and the `--force-conflicts` flag, see no conflict

0 commit comments

Comments
 (0)