Skip to content

Commit

Permalink
[droplets]: add support for droplet backup policy (digitalocean#1609)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
loosla authored Nov 18, 2024
1 parent bd65bcc commit 5d268d5
Show file tree
Hide file tree
Showing 17 changed files with 1,101 additions and 7 deletions.
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) {
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

0 comments on commit 5d268d5

Please sign in to comment.