Skip to content

Commit 5d268d5

Browse files
authored
[droplets]: add support for droplet backup policy (#1609)
* [droplets]: add support for droplet backup policy * add internal droplets package to parse a policy * fix tests, add a new test for droplet actions backup policy update * add droplet backup policies into droplet create * rename droplet-action command to change-backup-policy * fix tests after command renaming * add enable backups with policy to droplet actions * add tests for EnableBackupsWithPolicy * add enable-backups-with-policy to droplet-actions test * add get droplet backup policy * add list droplet backup policies for all existing droplets * add list supported droplet backup policies * use a flag to apply a backup policy when enabling backups rather than use a separate droplet action for that * add a wait flag for a droplet change backup policy * renaming to clarify instance we refer in a loop * reduce naming for get backup policy * fix integration tests making backup policy optional in droplet actions enable backups * group droplet backup-policies read commands under backup-policies sub command. * protect against panics on list for Droplets that do not have backups enabled * pass droplet backup policies with the flags instead of a config file * adding an empty backup policy to integration droplet action test * add a key to the test * add a check for a default backup policy when it's missing on backup enabling; revert changes in integration tests * add a comment and an integration test to enable droplet backups with backup policy * add an integration test for change_backup_policy in droplet_action * add an integration test for creating a droplet with backups enabled and backup policy applied * add template and format flags to droplet backup policies get; add integration tests for droplet backup policies get * rename integration tet file; add integration test for listing backup policies for all droplets * add integration tests for listing droplet supported droplet backup policies * avoid using default values, use api defaults in droplet actions * fix test: incorrect update in test * avoid using defaults; use api defaults in droplet create
1 parent bd65bcc commit 5d268d5

17 files changed

+1101
-7
lines changed

args.go

+6
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ const (
168168
ArgResourceType = "resource"
169169
// ArgBackups is an enable backups argument.
170170
ArgBackups = "enable-backups"
171+
// ArgDropletBackupPolicyPlan sets a frequency plan for backups.
172+
ArgDropletBackupPolicyPlan = "backup-policy-plan"
173+
// ArgDropletBackupPolicyWeekday sets backup policy day of the week.
174+
ArgDropletBackupPolicyWeekday = "backup-policy-weekday"
175+
// ArgDropletBackupPolicyHour sets backup policy hour.
176+
ArgDropletBackupPolicyHour = "backup-policy-hour"
171177
// ArgIPv6 is an enable IPv6 argument.
172178
ArgIPv6 = "enable-ipv6"
173179
// ArgPrivateNetworking is an enable private networking argument.

commands/commands_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package commands
1616
import (
1717
"io"
1818
"testing"
19+
"time"
1920

2021
"github.com/digitalocean/doctl"
2122
"github.com/digitalocean/doctl/do"
@@ -123,6 +124,63 @@ var (
123124
}
124125

125126
testSnapshotList = do.Snapshots{testSnapshot, testSnapshotSecondary}
127+
128+
testDropletBackupPolicy = do.DropletBackupPolicy{
129+
DropletBackupPolicy: &godo.DropletBackupPolicy{
130+
DropletID: 123,
131+
BackupPolicy: &godo.DropletBackupPolicyConfig{
132+
Plan: "weekly",
133+
Weekday: "MON",
134+
Hour: 0,
135+
WindowLengthHours: 4,
136+
RetentionPeriodDays: 28,
137+
},
138+
NextBackupWindow: &godo.BackupWindow{
139+
Start: &godo.Timestamp{Time: time.Date(2024, time.January, 1, 12, 0, 0, 0, time.UTC)},
140+
End: &godo.Timestamp{Time: time.Date(2024, time.February, 1, 12, 0, 0, 0, time.UTC)},
141+
},
142+
},
143+
}
144+
145+
anotherTestDropletBackupPolicy = do.DropletBackupPolicy{
146+
DropletBackupPolicy: &godo.DropletBackupPolicy{
147+
DropletID: 123,
148+
BackupPolicy: &godo.DropletBackupPolicyConfig{
149+
Plan: "daily",
150+
Hour: 12,
151+
WindowLengthHours: 4,
152+
RetentionPeriodDays: 7,
153+
},
154+
NextBackupWindow: &godo.BackupWindow{
155+
Start: &godo.Timestamp{Time: time.Date(2024, time.January, 1, 12, 0, 0, 0, time.UTC)},
156+
End: &godo.Timestamp{Time: time.Date(2024, time.February, 1, 12, 0, 0, 0, time.UTC)},
157+
},
158+
},
159+
}
160+
161+
testDropletBackupPolicies = do.DropletBackupPolicies{testDropletBackupPolicy, anotherTestDropletBackupPolicy}
162+
163+
testDropletSupportedBackupPolicy = do.DropletSupportedBackupPolicy{
164+
SupportedBackupPolicy: &godo.SupportedBackupPolicy{
165+
Name: "daily",
166+
PossibleWindowStarts: []int{0, 4, 8, 12, 16, 20},
167+
WindowLengthHours: 4,
168+
RetentionPeriodDays: 7,
169+
PossibleDays: []string{},
170+
},
171+
}
172+
173+
anotherTestDropletSupportedBackupPolicy = do.DropletSupportedBackupPolicy{
174+
SupportedBackupPolicy: &godo.SupportedBackupPolicy{
175+
Name: "weekly",
176+
PossibleWindowStarts: []int{0, 4, 8, 12, 16, 20},
177+
WindowLengthHours: 4,
178+
RetentionPeriodDays: 28,
179+
PossibleDays: []string{"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"},
180+
},
181+
}
182+
183+
testDropletSupportedBackupPolicies = do.DropletSupportedBackupPolicies{testDropletSupportedBackupPolicy, anotherTestDropletSupportedBackupPolicy}
126184
)
127185

128186
func assertCommandNames(t *testing.T, cmd *Command, expected ...string) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package displayers
2+
3+
import (
4+
"io"
5+
6+
"github.com/digitalocean/doctl/do"
7+
)
8+
9+
type DropletBackupPolicy struct {
10+
DropletBackupPolicies []do.DropletBackupPolicy
11+
}
12+
13+
var _ Displayable = &DropletBackupPolicy{}
14+
15+
func (d *DropletBackupPolicy) JSON(out io.Writer) error {
16+
return writeJSON(d.DropletBackupPolicies, out)
17+
}
18+
19+
func (d *DropletBackupPolicy) Cols() []string {
20+
cols := []string{
21+
"DropletID", "BackupEnabled", "BackupPolicyPlan", "BackupPolicyWeekday", "BackupPolicyHour",
22+
"BackupPolicyWindowLengthHours", "BackupPolicyRetentionPeriodDays",
23+
"NextBackupWindowStart", "NextBackupWindowEnd",
24+
}
25+
return cols
26+
}
27+
28+
func (d *DropletBackupPolicy) ColMap() map[string]string {
29+
return map[string]string{
30+
"DropletID": "Droplet ID", "BackupEnabled": "Enabled",
31+
"BackupPolicyPlan": "Plan", "BackupPolicyWeekday": "Weekday", "BackupPolicyHour": "Hour",
32+
"BackupPolicyWindowLengthHours": "Window Length Hours", "BackupPolicyRetentionPeriodDays": "Retention Period Days",
33+
"NextBackupWindowStart": "Next Window Start", "NextBackupWindowEnd": "Next Window End",
34+
}
35+
}
36+
37+
func (d *DropletBackupPolicy) KV() []map[string]any {
38+
out := make([]map[string]any, 0)
39+
for _, policy := range d.DropletBackupPolicies {
40+
if policy.BackupPolicy != nil && policy.NextBackupWindow != nil {
41+
m := map[string]any{
42+
"DropletID": policy.DropletID, "BackupEnabled": policy.BackupEnabled,
43+
"BackupPolicyPlan": policy.BackupPolicy.Plan,
44+
"BackupPolicyWeekday": policy.BackupPolicy.Weekday, "BackupPolicyHour": policy.BackupPolicy.Hour,
45+
"BackupPolicyWindowLengthHours": policy.BackupPolicy.WindowLengthHours, "BackupPolicyRetentionPeriodDays": policy.BackupPolicy.RetentionPeriodDays,
46+
"NextBackupWindowStart": policy.NextBackupWindow.Start, "NextBackupWindowEnd": policy.NextBackupWindow.End,
47+
}
48+
out = append(out, m)
49+
}
50+
}
51+
52+
return out
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package displayers
2+
3+
import (
4+
"io"
5+
6+
"github.com/digitalocean/doctl/do"
7+
)
8+
9+
type DropletSupportedBackupPolicy struct {
10+
DropletSupportedBackupPolicies []do.DropletSupportedBackupPolicy
11+
}
12+
13+
var _ Displayable = &DropletSupportedBackupPolicy{}
14+
15+
func (d *DropletSupportedBackupPolicy) JSON(out io.Writer) error {
16+
return writeJSON(d.DropletSupportedBackupPolicies, out)
17+
}
18+
19+
func (d *DropletSupportedBackupPolicy) Cols() []string {
20+
cols := []string{
21+
"Name", "PossibleWindowStarts", "WindowLengthHours", "RetentionPeriodDays", "PossibleDays",
22+
}
23+
return cols
24+
}
25+
26+
func (d *DropletSupportedBackupPolicy) ColMap() map[string]string {
27+
return map[string]string{
28+
"Name": "Name", "PossibleWindowStarts": "Possible Window Starts",
29+
"WindowLengthHours": "Window Length Hours", "RetentionPeriodDays": "Retention Period Days", "PossibleDays": "Possible Days",
30+
}
31+
}
32+
33+
func (d *DropletSupportedBackupPolicy) KV() []map[string]any {
34+
out := make([]map[string]any, 0)
35+
for _, supported := range d.DropletSupportedBackupPolicies {
36+
m := map[string]any{
37+
"Name": supported.Name, "PossibleWindowStarts": supported.PossibleWindowStarts, "WindowLengthHours": supported.WindowLengthHours,
38+
"RetentionPeriodDays": supported.RetentionPeriodDays, "PossibleDays": supported.PossibleDays,
39+
}
40+
out = append(out, m)
41+
}
42+
43+
return out
44+
}

commands/droplet_actions.go

+77-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/digitalocean/doctl"
1818
"github.com/digitalocean/doctl/commands/displayers"
1919
"github.com/digitalocean/doctl/do"
20+
"github.com/digitalocean/godo"
2021
"github.com/spf13/cobra"
2122
)
2223

@@ -72,15 +73,27 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re
7273
cmdDropletActionEnableBackups := CmdBuilder(cmd, RunDropletActionEnableBackups,
7374
"enable-backups <droplet-id>", "Enable backups on a Droplet", `Enables backups on a Droplet. This automatically creates and stores a disk image of the Droplet. By default, backups happen daily.`, Writer,
7475
displayerType(&displayers.Action{}))
76+
AddStringFlag(cmdDropletActionEnableBackups, doctl.ArgDropletBackupPolicyPlan, "", "", `Backup policy frequency plan.`)
77+
AddStringFlag(cmdDropletActionEnableBackups, doctl.ArgDropletBackupPolicyWeekday, "", "", `Backup policy weekday.`)
78+
AddIntFlag(cmdDropletActionEnableBackups, doctl.ArgDropletBackupPolicyHour, "", 0, `Backup policy hour.`)
7579
AddBoolFlag(cmdDropletActionEnableBackups, doctl.ArgCommandWait, "", false, "Wait for action to complete")
76-
cmdDropletActionEnableBackups.Example = `The following example enables backups on a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet-action enable-backups 386734086`
80+
cmdDropletActionEnableBackups.Example = `The following example enables backups on a Droplet with the ID ` + "`" + `386734086` + "` with a backup policy flag" + `: doctl compute droplet-action enable-backups 386734086 --backup-policy-plan weekly --backup-policy-weekday SUN --backup-policy-hour 4`
7781

7882
cmdDropletActionDisableBackups := CmdBuilder(cmd, RunDropletActionDisableBackups,
7983
"disable-backups <droplet-id>", "Disable backups on a Droplet", `Disables backups on a Droplet. This does not delete existing backups.`, Writer,
8084
displayerType(&displayers.Action{}))
8185
AddBoolFlag(cmdDropletActionDisableBackups, doctl.ArgCommandWait, "", false, "Instruct the terminal to wait for the action to complete before returning access to the user")
8286
cmdDropletActionDisableBackups.Example = `The following example disables backups on a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet-action disable-backups 386734086`
8387

88+
cmdDropletActionChangeBackupPolicy := CmdBuilder(cmd, RunDropletActionChangeBackupPolicy,
89+
"change-backup-policy <droplet-id>", "Change backup policy on a Droplet", `Changes backup policy for a Droplet with enabled backups.`, Writer,
90+
displayerType(&displayers.Action{}))
91+
AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyPlan, "", "", `Backup policy frequency plan.`, requiredOpt())
92+
AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyWeekday, "", "", `Backup policy weekday.`)
93+
AddIntFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyHour, "", 0, `Backup policy hour.`)
94+
AddBoolFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgCommandWait, "", false, "Wait for action to complete")
95+
cmdDropletActionChangeBackupPolicy.Example = `The following example changes backup policy on a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet-action change-backup-policy 386734086 --backup-policy-plan weekly --backup-policy-weekday SUN --backup-policy-hour 4`
96+
8497
cmdDropletActionReboot := CmdBuilder(cmd, RunDropletActionReboot,
8598
"reboot <droplet-id>", "Reboot a Droplet", `Reboots a Droplet. A reboot action is an attempt to reboot the Droplet in a graceful way, similar to using the reboot command from the Droplet's console.`, Writer,
8699
displayerType(&displayers.Action{}))
@@ -242,8 +255,12 @@ func RunDropletActionEnableBackups(c *CmdConfig) error {
242255
return nil, err
243256
}
244257

245-
a, err := das.EnableBackups(id)
246-
return a, err
258+
policy, err := readDropletBackupPolicy(c)
259+
if err == nil && policy != nil {
260+
return das.EnableBackupsWithPolicy(id, policy)
261+
}
262+
263+
return das.EnableBackups(id)
247264
}
248265

249266
return performAction(c, fn)
@@ -268,6 +285,63 @@ func RunDropletActionDisableBackups(c *CmdConfig) error {
268285
return performAction(c, fn)
269286
}
270287

288+
func readDropletBackupPolicy(c *CmdConfig) (*godo.DropletBackupPolicyRequest, error) {
289+
policyPlan, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicyPlan)
290+
if err != nil {
291+
return nil, err
292+
}
293+
294+
// For cases when backup policy is not specified.
295+
if policyPlan == "" {
296+
return nil, nil
297+
}
298+
299+
policyHour, err := c.Doit.GetInt(c.NS, doctl.ArgDropletBackupPolicyHour)
300+
if err != nil {
301+
return nil, err
302+
}
303+
304+
policy := godo.DropletBackupPolicyRequest{
305+
Plan: policyPlan,
306+
Hour: &policyHour,
307+
}
308+
309+
if policyPlan == "weekly" {
310+
policyWeekday, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicyWeekday)
311+
if err != nil {
312+
return nil, err
313+
}
314+
policy.Weekday = policyWeekday
315+
}
316+
317+
return &policy, nil
318+
}
319+
320+
// RunDropletActionChangeBackupPolicy changes backup policy for a droplet.
321+
func RunDropletActionChangeBackupPolicy(c *CmdConfig) error {
322+
fn := func(das do.DropletActionsService) (*do.Action, error) {
323+
err := ensureOneArg(c)
324+
if err != nil {
325+
return nil, err
326+
}
327+
328+
id, err := ContextualAtoi(c.Args[0], dropletIDResource)
329+
if err != nil {
330+
return nil, err
331+
}
332+
333+
policy, err := readDropletBackupPolicy(c)
334+
if err != nil {
335+
return nil, err
336+
}
337+
338+
a, err := das.ChangeBackupPolicy(id, policy)
339+
return a, err
340+
}
341+
342+
return performAction(c, fn)
343+
}
344+
271345
// RunDropletActionReboot reboots a droplet.
272346
func RunDropletActionReboot(c *CmdConfig) error {
273347
fn := func(das do.DropletActionsService) (*do.Action, error) {

commands/droplet_actions_test.go

+41-1
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ import (
1717
"testing"
1818

1919
"github.com/digitalocean/doctl"
20+
"github.com/digitalocean/godo"
2021
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
2123
)
2224

2325
func TestDropletActionCommand(t *testing.T) {
2426
cmd := DropletAction()
2527
assert.NotNil(t, cmd)
26-
assertCommandNames(t, cmd, "change-kernel", "enable-backups", "disable-backups", "enable-ipv6", "enable-private-networking", "get", "power-cycle", "power-off", "power-on", "password-reset", "reboot", "rebuild", "rename", "resize", "restore", "shutdown", "snapshot")
28+
assertCommandNames(t, cmd, "change-kernel", "change-backup-policy", "enable-backups", "disable-backups", "enable-ipv6", "enable-private-networking", "get", "power-cycle", "power-off", "power-on", "password-reset", "reboot", "rebuild", "rename", "resize", "restore", "shutdown", "snapshot")
2729
}
2830

2931
func TestDropletActionsChangeKernel(t *testing.T) {
@@ -59,6 +61,24 @@ func TestDropletActionsEnableBackups(t *testing.T) {
5961
err := RunDropletActionEnableBackups(config)
6062
assert.EqualError(t, err, `expected <droplet-id> to be a positive integer, got "my-test-id"`)
6163
})
64+
// Enable backups with a backup policy applied.
65+
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
66+
policy := &godo.DropletBackupPolicyRequest{
67+
Plan: "weekly",
68+
Weekday: "SAT",
69+
Hour: godo.PtrTo(0),
70+
}
71+
72+
tm.dropletActions.EXPECT().EnableBackupsWithPolicy(1, policy).Times(1).Return(&testAction, nil)
73+
74+
config.Args = append(config.Args, "1")
75+
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyPlan, policy.Plan)
76+
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyWeekday, policy.Weekday)
77+
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyHour, policy.Hour)
78+
79+
err := RunDropletActionEnableBackups(config)
80+
require.NoError(t, err)
81+
})
6282
}
6383

6484
func TestDropletActionsDisableBackups(t *testing.T) {
@@ -78,6 +98,26 @@ func TestDropletActionsDisableBackups(t *testing.T) {
7898
})
7999
}
80100

101+
func TestDropletActionsChangeBackupPolicy(t *testing.T) {
102+
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
103+
policy := &godo.DropletBackupPolicyRequest{
104+
Plan: "weekly",
105+
Weekday: "SAT",
106+
Hour: godo.PtrTo(0),
107+
}
108+
109+
tm.dropletActions.EXPECT().ChangeBackupPolicy(1, policy).Times(1).Return(&testAction, nil)
110+
111+
config.Args = append(config.Args, "1")
112+
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyPlan, policy.Plan)
113+
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyWeekday, policy.Weekday)
114+
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyHour, policy.Hour)
115+
116+
err := RunDropletActionChangeBackupPolicy(config)
117+
require.NoError(t, err)
118+
})
119+
}
120+
81121
func TestDropletActionsEnableIPv6(t *testing.T) {
82122
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
83123
tm.dropletActions.EXPECT().EnableIPv6(1).Return(&testAction, nil)

0 commit comments

Comments
 (0)