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

[droplets]: add support for droplet backup policy #1609

Merged
merged 35 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6dbdedf
[droplets]: add support for droplet backup policy
loosla Nov 5, 2024
71b3046
add internal droplets package to parse a policy
loosla Nov 5, 2024
fcd37de
fix tests, add a new test for droplet actions backup policy update
loosla Nov 5, 2024
7b9ee32
Merge branch 'main' into alushnikova/droplet-backup-policy
loosla Nov 5, 2024
869797d
add droplet backup policies into droplet create
loosla Nov 5, 2024
4e66a20
rename droplet-action command to change-backup-policy
loosla Nov 6, 2024
5a7da2c
fix tests after command renaming
loosla Nov 6, 2024
ccb5f29
add enable backups with policy to droplet actions
loosla Nov 6, 2024
a2d6ff7
add tests for EnableBackupsWithPolicy
loosla Nov 6, 2024
d9c1c1e
add enable-backups-with-policy to droplet-actions test
loosla Nov 6, 2024
cc27e0e
add get droplet backup policy
loosla Nov 6, 2024
7369111
add list droplet backup policies for all existing droplets
loosla Nov 7, 2024
0409737
add list supported droplet backup policies
loosla Nov 7, 2024
8a8a634
use a flag to apply a backup policy when enabling backups rather than…
loosla Nov 7, 2024
93be1fb
add a wait flag for a droplet change backup policy
loosla Nov 7, 2024
bc4ede2
renaming to clarify instance we refer in a loop
loosla Nov 13, 2024
3b4e2da
reduce naming for get backup policy
loosla Nov 13, 2024
b23966b
fix integration tests making backup policy optional in droplet action…
loosla Nov 13, 2024
10aabd3
group droplet backup-policies read commands under backup-policies sub…
loosla Nov 13, 2024
b796bb5
protect against panics on list for Droplets that do not have backups …
loosla Nov 13, 2024
4f6dfbd
pass droplet backup policies with the flags instead of a config file
loosla Nov 13, 2024
841b2f2
adding an empty backup policy to integration droplet action test
loosla Nov 13, 2024
8f9e0e0
add a key to the test
loosla Nov 13, 2024
d5a31a5
add a check for a default backup policy when it's missing on backup e…
loosla Nov 14, 2024
185e29b
add a comment and an integration test to enable droplet backups with …
loosla Nov 14, 2024
22c8684
add an integration test for change_backup_policy in droplet_action
loosla Nov 14, 2024
028b168
add an integration test for creating a droplet with backups enabled a…
loosla Nov 14, 2024
afd94a3
add template and format flags to droplet backup policies get; add int…
loosla Nov 14, 2024
adde2c9
rename integration tet file; add integration test for listing backup …
loosla Nov 15, 2024
4bbdfb0
add integration tests for listing droplet supported droplet backup po…
loosla Nov 15, 2024
a6c2f09
Merge branch 'main' into alushnikova/droplet-backup-policy
loosla Nov 15, 2024
4b6ac16
avoid using default values, use api defaults in droplet actions
loosla Nov 18, 2024
2aa4aa0
fix test: incorrect update in test
loosla Nov 18, 2024
614e5c7
avoid using defaults; use api defaults in droplet create
loosla Nov 18, 2024
b577a57
Merge branch 'main' into alushnikova/droplet-backup-policy
loosla Nov 18, 2024
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
6 changes: 6 additions & 0 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ const (
ArgResourceType = "resource"
// ArgBackups is an enable backups argument.
ArgBackups = "enable-backups"
// ArgDropletBackupPolicyPlan sets a frequency plan for backups.
ArgDropletBackupPolicyPlan = "backup-policy-plan"
// ArgDropletBackupPolicyWeekday sets backup policy day of the week.
ArgDropletBackupPolicyWeekday = "backup-policy-weekday"
// ArgDropletBackupPolicyHour sets backup policy hour.
ArgDropletBackupPolicyHour = "backup-policy-hour"
// ArgIPv6 is an enable IPv6 argument.
ArgIPv6 = "enable-ipv6"
// ArgPrivateNetworking is an enable private networking argument.
Expand Down
58 changes: 58 additions & 0 deletions commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package commands
import (
"io"
"testing"
"time"

"github.com/digitalocean/doctl"
"github.com/digitalocean/doctl/do"
Expand Down Expand Up @@ -123,6 +124,63 @@ var (
}

testSnapshotList = do.Snapshots{testSnapshot, testSnapshotSecondary}

testDropletBackupPolicy = do.DropletBackupPolicy{
DropletBackupPolicy: &godo.DropletBackupPolicy{
DropletID: 123,
BackupPolicy: &godo.DropletBackupPolicyConfig{
Plan: "weekly",
Weekday: "MON",
Hour: 0,
WindowLengthHours: 4,
RetentionPeriodDays: 28,
},
NextBackupWindow: &godo.BackupWindow{
Start: &godo.Timestamp{Time: time.Date(2024, time.January, 1, 12, 0, 0, 0, time.UTC)},
End: &godo.Timestamp{Time: time.Date(2024, time.February, 1, 12, 0, 0, 0, time.UTC)},
},
},
}

anotherTestDropletBackupPolicy = do.DropletBackupPolicy{
DropletBackupPolicy: &godo.DropletBackupPolicy{
DropletID: 123,
BackupPolicy: &godo.DropletBackupPolicyConfig{
Plan: "daily",
Hour: 12,
WindowLengthHours: 4,
RetentionPeriodDays: 7,
},
NextBackupWindow: &godo.BackupWindow{
Start: &godo.Timestamp{Time: time.Date(2024, time.January, 1, 12, 0, 0, 0, time.UTC)},
End: &godo.Timestamp{Time: time.Date(2024, time.February, 1, 12, 0, 0, 0, time.UTC)},
},
},
}

testDropletBackupPolicies = do.DropletBackupPolicies{testDropletBackupPolicy, anotherTestDropletBackupPolicy}

testDropletSupportedBackupPolicy = do.DropletSupportedBackupPolicy{
SupportedBackupPolicy: &godo.SupportedBackupPolicy{
Name: "daily",
PossibleWindowStarts: []int{0, 4, 8, 12, 16, 20},
WindowLengthHours: 4,
RetentionPeriodDays: 7,
PossibleDays: []string{},
},
}

anotherTestDropletSupportedBackupPolicy = do.DropletSupportedBackupPolicy{
SupportedBackupPolicy: &godo.SupportedBackupPolicy{
Name: "weekly",
PossibleWindowStarts: []int{0, 4, 8, 12, 16, 20},
WindowLengthHours: 4,
RetentionPeriodDays: 28,
PossibleDays: []string{"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"},
},
}

testDropletSupportedBackupPolicies = do.DropletSupportedBackupPolicies{testDropletSupportedBackupPolicy, anotherTestDropletSupportedBackupPolicy}
)

func assertCommandNames(t *testing.T, cmd *Command, expected ...string) {
Expand Down
53 changes: 53 additions & 0 deletions commands/displayers/droplet_backup_policies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package displayers

import (
"io"

"github.com/digitalocean/doctl/do"
)

type DropletBackupPolicy struct {
DropletBackupPolicies []do.DropletBackupPolicy
}

var _ Displayable = &DropletBackupPolicy{}

func (d *DropletBackupPolicy) JSON(out io.Writer) error {
return writeJSON(d.DropletBackupPolicies, out)
}

func (d *DropletBackupPolicy) Cols() []string {
cols := []string{
"DropletID", "BackupEnabled", "BackupPolicyPlan", "BackupPolicyWeekday", "BackupPolicyHour",
"BackupPolicyWindowLengthHours", "BackupPolicyRetentionPeriodDays",
"NextBackupWindowStart", "NextBackupWindowEnd",
}
return cols
}

func (d *DropletBackupPolicy) ColMap() map[string]string {
return map[string]string{
"DropletID": "Droplet ID", "BackupEnabled": "Enabled",
"BackupPolicyPlan": "Plan", "BackupPolicyWeekday": "Weekday", "BackupPolicyHour": "Hour",
"BackupPolicyWindowLengthHours": "Window Length Hours", "BackupPolicyRetentionPeriodDays": "Retention Period Days",
"NextBackupWindowStart": "Next Window Start", "NextBackupWindowEnd": "Next Window End",
}
}

func (d *DropletBackupPolicy) KV() []map[string]any {
out := make([]map[string]any, 0)
for _, policy := range d.DropletBackupPolicies {
if policy.BackupPolicy != nil && policy.NextBackupWindow != nil {
m := map[string]any{
"DropletID": policy.DropletID, "BackupEnabled": policy.BackupEnabled,
"BackupPolicyPlan": policy.BackupPolicy.Plan,
"BackupPolicyWeekday": policy.BackupPolicy.Weekday, "BackupPolicyHour": policy.BackupPolicy.Hour,
"BackupPolicyWindowLengthHours": policy.BackupPolicy.WindowLengthHours, "BackupPolicyRetentionPeriodDays": policy.BackupPolicy.RetentionPeriodDays,
"NextBackupWindowStart": policy.NextBackupWindow.Start, "NextBackupWindowEnd": policy.NextBackupWindow.End,
}
out = append(out, m)
}
}

return out
}
44 changes: 44 additions & 0 deletions commands/displayers/droplet_supported_backup_policies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package displayers

import (
"io"

"github.com/digitalocean/doctl/do"
)

type DropletSupportedBackupPolicy struct {
DropletSupportedBackupPolicies []do.DropletSupportedBackupPolicy
}

var _ Displayable = &DropletSupportedBackupPolicy{}

func (d *DropletSupportedBackupPolicy) JSON(out io.Writer) error {
return writeJSON(d.DropletSupportedBackupPolicies, out)
}

func (d *DropletSupportedBackupPolicy) Cols() []string {
cols := []string{
"Name", "PossibleWindowStarts", "WindowLengthHours", "RetentionPeriodDays", "PossibleDays",
}
return cols
}

func (d *DropletSupportedBackupPolicy) ColMap() map[string]string {
return map[string]string{
"Name": "Name", "PossibleWindowStarts": "Possible Window Starts",
"WindowLengthHours": "Window Length Hours", "RetentionPeriodDays": "Retention Period Days", "PossibleDays": "Possible Days",
}
}

func (d *DropletSupportedBackupPolicy) KV() []map[string]any {
out := make([]map[string]any, 0)
for _, supported := range d.DropletSupportedBackupPolicies {
m := map[string]any{
"Name": supported.Name, "PossibleWindowStarts": supported.PossibleWindowStarts, "WindowLengthHours": supported.WindowLengthHours,
"RetentionPeriodDays": supported.RetentionPeriodDays, "PossibleDays": supported.PossibleDays,
}
out = append(out, m)
}

return out
}
80 changes: 77 additions & 3 deletions commands/droplet_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/digitalocean/doctl"
"github.com/digitalocean/doctl/commands/displayers"
"github.com/digitalocean/doctl/do"
"github.com/digitalocean/godo"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -72,15 +73,27 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re
cmdDropletActionEnableBackups := CmdBuilder(cmd, RunDropletActionEnableBackups,
"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,
displayerType(&displayers.Action{}))
AddStringFlag(cmdDropletActionEnableBackups, doctl.ArgDropletBackupPolicyPlan, "", "", `Backup policy frequency plan.`)
AddStringFlag(cmdDropletActionEnableBackups, doctl.ArgDropletBackupPolicyWeekday, "", "", `Backup policy weekday.`)
AddIntFlag(cmdDropletActionEnableBackups, doctl.ArgDropletBackupPolicyHour, "", 0, `Backup policy hour.`)
AddBoolFlag(cmdDropletActionEnableBackups, doctl.ArgCommandWait, "", false, "Wait for action to complete")
cmdDropletActionEnableBackups.Example = `The following example enables backups on a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet-action enable-backups 386734086`
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`

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

cmdDropletActionChangeBackupPolicy := CmdBuilder(cmd, RunDropletActionChangeBackupPolicy,
"change-backup-policy <droplet-id>", "Change backup policy on a Droplet", `Changes backup policy for a Droplet with enabled backups.`, Writer,
displayerType(&displayers.Action{}))
AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyPlan, "", "", `Backup policy frequency plan.`, requiredOpt())
AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyWeekday, "", "", `Backup policy weekday.`)
AddIntFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyHour, "", 0, `Backup policy hour.`)
AddBoolFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgCommandWait, "", false, "Wait for action to complete")
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`

cmdDropletActionReboot := CmdBuilder(cmd, RunDropletActionReboot,
"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,
displayerType(&displayers.Action{}))
Expand Down Expand Up @@ -242,8 +255,12 @@ func RunDropletActionEnableBackups(c *CmdConfig) error {
return nil, err
}

a, err := das.EnableBackups(id)
return a, err
policy, err := readDropletBackupPolicy(c)
if err == nil && policy != nil {
return das.EnableBackupsWithPolicy(id, policy)
}

return das.EnableBackups(id)
}

return performAction(c, fn)
Expand All @@ -268,6 +285,63 @@ func RunDropletActionDisableBackups(c *CmdConfig) error {
return performAction(c, fn)
}

func readDropletBackupPolicy(c *CmdConfig) (*godo.DropletBackupPolicyRequest, error) {
loosla marked this conversation as resolved.
Show resolved Hide resolved
policyPlan, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicyPlan)
if err != nil {
return nil, err
}

// For cases when backup policy is not specified.
if policyPlan == "" {
return nil, nil
}

policyHour, err := c.Doit.GetInt(c.NS, doctl.ArgDropletBackupPolicyHour)
if err != nil {
return nil, err
}

policy := godo.DropletBackupPolicyRequest{
Plan: policyPlan,
Hour: &policyHour,
}

if policyPlan == "weekly" {
policyWeekday, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicyWeekday)
if err != nil {
return nil, err
}
policy.Weekday = policyWeekday
}

return &policy, nil
}

// RunDropletActionChangeBackupPolicy changes backup policy for a droplet.
func RunDropletActionChangeBackupPolicy(c *CmdConfig) error {
fn := func(das do.DropletActionsService) (*do.Action, error) {
err := ensureOneArg(c)
if err != nil {
return nil, err
}

id, err := ContextualAtoi(c.Args[0], dropletIDResource)
if err != nil {
return nil, err
}

policy, err := readDropletBackupPolicy(c)
if err != nil {
return nil, err
}

a, err := das.ChangeBackupPolicy(id, policy)
return a, err
}

return performAction(c, fn)
}

// RunDropletActionReboot reboots a droplet.
func RunDropletActionReboot(c *CmdConfig) error {
fn := func(das do.DropletActionsService) (*do.Action, error) {
Expand Down
42 changes: 41 additions & 1 deletion commands/droplet_actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ import (
"testing"

"github.com/digitalocean/doctl"
"github.com/digitalocean/godo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDropletActionCommand(t *testing.T) {
cmd := DropletAction()
assert.NotNil(t, cmd)
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")
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")
}

func TestDropletActionsChangeKernel(t *testing.T) {
Expand Down Expand Up @@ -59,6 +61,24 @@ func TestDropletActionsEnableBackups(t *testing.T) {
err := RunDropletActionEnableBackups(config)
assert.EqualError(t, err, `expected <droplet-id> to be a positive integer, got "my-test-id"`)
})
// Enable backups with a backup policy applied.
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
policy := &godo.DropletBackupPolicyRequest{
Plan: "weekly",
Weekday: "SAT",
Hour: godo.PtrTo(0),
}

tm.dropletActions.EXPECT().EnableBackupsWithPolicy(1, policy).Times(1).Return(&testAction, nil)

config.Args = append(config.Args, "1")
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyPlan, policy.Plan)
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyWeekday, policy.Weekday)
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyHour, policy.Hour)

err := RunDropletActionEnableBackups(config)
require.NoError(t, err)
})
}

func TestDropletActionsDisableBackups(t *testing.T) {
Expand All @@ -78,6 +98,26 @@ func TestDropletActionsDisableBackups(t *testing.T) {
})
}

func TestDropletActionsChangeBackupPolicy(t *testing.T) {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
policy := &godo.DropletBackupPolicyRequest{
Plan: "weekly",
Weekday: "SAT",
Hour: godo.PtrTo(0),
}

tm.dropletActions.EXPECT().ChangeBackupPolicy(1, policy).Times(1).Return(&testAction, nil)

config.Args = append(config.Args, "1")
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyPlan, policy.Plan)
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyWeekday, policy.Weekday)
config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyHour, policy.Hour)

err := RunDropletActionChangeBackupPolicy(config)
require.NoError(t, err)
})
}

func TestDropletActionsEnableIPv6(t *testing.T) {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
tm.dropletActions.EXPECT().EnableIPv6(1).Return(&testAction, nil)
Expand Down
Loading
Loading