From 6dbdedf075ff43c779ad1bf10a8b0cf062e15d7b Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Tue, 5 Nov 2024 14:06:11 -0500 Subject: [PATCH 01/32] [droplets]: add support for droplet backup policy --- args.go | 2 + commands/droplet_actions.go | 35 ++++ do/droplet_actions.go | 6 + do/mocks/AccountService.go | 1 + do/mocks/ActionService.go | 1 + do/mocks/AppsService.go | 1 + do/mocks/BalanceService.go | 1 + do/mocks/BillingHistoryService.go | 1 + do/mocks/CDNsService.go | 1 + do/mocks/CertificatesService.go | 1 + do/mocks/DatabasesService.go | 1 + do/mocks/DomainService.go | 1 + do/mocks/DropletActionService.go | 17 ++ do/mocks/FirewallsService.go | 1 + do/mocks/ImageActionsService.go | 1 + do/mocks/ImageService.go | 1 + do/mocks/InvoicesService.go | 1 + do/mocks/KeysService.go | 1 + do/mocks/KubernetesService.go | 1 + do/mocks/Listen.go | 1 + do/mocks/LoadBalancersService.go | 1 + do/mocks/MonitoringService.go | 1 + do/mocks/OAuthService.go | 1 + do/mocks/OneClickService.go | 1 + do/mocks/ProjectsService.go | 1 + do/mocks/RegionsService.go | 1 + do/mocks/RegistryService.go | 1 + do/mocks/ReservedIPActionsService.go | 1 + do/mocks/ReservedIPsService.go | 1 + do/mocks/Runner.go | 1 + do/mocks/ServerlessService.go | 1 + do/mocks/SizesService.go | 1 + do/mocks/SnapshotsService.go | 1 + do/mocks/TagsService.go | 1 + do/mocks/UptimeChecksService.go | 1 + do/mocks/VPCsService.go | 1 + do/mocks/VolumeActionsService.go | 1 + do/mocks/VolumesService.go | 1 + go.mod | 2 +- go.sum | 4 +- .../digitalocean/godo/droplet_actions.go | 38 ++++ .../github.com/digitalocean/godo/droplets.go | 172 +++++++++++++++--- vendor/github.com/digitalocean/godo/godo.go | 2 + .../github.com/digitalocean/godo/registry.go | 120 ++++++++++++ vendor/modules.txt | 2 +- 45 files changed, 403 insertions(+), 31 deletions(-) diff --git a/args.go b/args.go index 4b5290bb1..b8d6e5a15 100644 --- a/args.go +++ b/args.go @@ -150,6 +150,8 @@ const ( ArgResourceType = "resource" // ArgBackups is an enable backups argument. ArgBackups = "enable-backups" + // ArgDropletBackupPolicy is a path to a droplet backup policy. + ArgDropletBackupPolicy = "backup-policy" // ArgIPv6 is an enable IPv6 argument. ArgIPv6 = "enable-ipv6" // ArgPrivateNetworking is an enable private networking argument. diff --git a/commands/droplet_actions.go b/commands/droplet_actions.go index af9077f42..36f54bbd4 100644 --- a/commands/droplet_actions.go +++ b/commands/droplet_actions.go @@ -14,9 +14,12 @@ limitations under the License. package commands import ( + "os" + "github.com/digitalocean/doctl" "github.com/digitalocean/doctl/commands/displayers" "github.com/digitalocean/doctl/do" + "github.com/digitalocean/doctl/internal/droplets" "github.com/spf13/cobra" ) @@ -81,6 +84,13 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re 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 ", "Change backup policy on a Droplet", `Changes backup policy for a Droplet with enabled backups.`, Writer, + displayerType(&displayers.Action{})) + AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicy, "", "", `Path to a new backup policy in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) + // AddBoolFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgCommandWait, "", false, "Wait for action to complete") // TODO: Add this flag when the doctl supports reading policy. + 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 src/your-backup-policy.yaml` + cmdDropletActionReboot := CmdBuilder(cmd, RunDropletActionReboot, "reboot ", "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{})) @@ -268,6 +278,31 @@ func RunDropletActionDisableBackups(c *CmdConfig) error { return performAction(c, fn) } +// RunDropletActionChangeBackupPolicy changes backup policy for a droplet. +func RunDropletActionChangeBackupPolicy(c *CmdConfig) error { + fn := func(das do.DropletActionsService) (*do.Action, error) { + id, err := ContextualAtoi(c.Args[0], dropletIDResource) + if err != nil { + return nil, err + } + + policyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) + if err != nil { + return nil, err + } + + policy, err := droplets.ReadDropletBackupPolicy(os.Stdin, policyPath) + 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) { diff --git a/do/droplet_actions.go b/do/droplet_actions.go index 45d6ef1cd..213388b2e 100644 --- a/do/droplet_actions.go +++ b/do/droplet_actions.go @@ -39,6 +39,7 @@ type DropletActionsService interface { EnableBackupsByTag(string) (Actions, error) DisableBackups(int) (*Action, error) DisableBackupsByTag(string) (Actions, error) + ChangeBackupPolicy(int, *godo.DropletBackupPolicyRequest) (*Action, error) PasswordReset(int) (*Action, error) RebuildByImageID(int, int) (*Action, error) RebuildByImageSlug(int, string) (*Action, error) @@ -176,6 +177,11 @@ func (das *dropletActionsService) DisableBackupsByTag(tag string) (Actions, erro return das.handleTagActionResponse(a, err) } +func (das *dropletActionsService) ChangeBackupPolicy(id int, policy *godo.DropletBackupPolicyRequest) (*Action, error) { + a, _, err := das.client.DropletActions.ChangeBackupPolicy(context.TODO(), id, policy) + return das.handleActionResponse(a, err) +} + func (das *dropletActionsService) PasswordReset(id int) (*Action, error) { a, _, err := das.client.DropletActions.PasswordReset(context.TODO(), id) return das.handleActionResponse(a, err) diff --git a/do/mocks/AccountService.go b/do/mocks/AccountService.go index 2a98fcd08..53bbf828c 100644 --- a/do/mocks/AccountService.go +++ b/do/mocks/AccountService.go @@ -20,6 +20,7 @@ import ( type MockAccountService struct { ctrl *gomock.Controller recorder *MockAccountServiceMockRecorder + isgomock struct{} } // MockAccountServiceMockRecorder is the mock recorder for MockAccountService. diff --git a/do/mocks/ActionService.go b/do/mocks/ActionService.go index 8dbf872ff..d571cc942 100644 --- a/do/mocks/ActionService.go +++ b/do/mocks/ActionService.go @@ -20,6 +20,7 @@ import ( type MockActionsService struct { ctrl *gomock.Controller recorder *MockActionsServiceMockRecorder + isgomock struct{} } // MockActionsServiceMockRecorder is the mock recorder for MockActionsService. diff --git a/do/mocks/AppsService.go b/do/mocks/AppsService.go index c1f9421f6..f0774d7df 100644 --- a/do/mocks/AppsService.go +++ b/do/mocks/AppsService.go @@ -20,6 +20,7 @@ import ( type MockAppsService struct { ctrl *gomock.Controller recorder *MockAppsServiceMockRecorder + isgomock struct{} } // MockAppsServiceMockRecorder is the mock recorder for MockAppsService. diff --git a/do/mocks/BalanceService.go b/do/mocks/BalanceService.go index ea9a08ff4..81384906e 100644 --- a/do/mocks/BalanceService.go +++ b/do/mocks/BalanceService.go @@ -20,6 +20,7 @@ import ( type MockBalanceService struct { ctrl *gomock.Controller recorder *MockBalanceServiceMockRecorder + isgomock struct{} } // MockBalanceServiceMockRecorder is the mock recorder for MockBalanceService. diff --git a/do/mocks/BillingHistoryService.go b/do/mocks/BillingHistoryService.go index 79203d17b..65cf25228 100644 --- a/do/mocks/BillingHistoryService.go +++ b/do/mocks/BillingHistoryService.go @@ -20,6 +20,7 @@ import ( type MockBillingHistoryService struct { ctrl *gomock.Controller recorder *MockBillingHistoryServiceMockRecorder + isgomock struct{} } // MockBillingHistoryServiceMockRecorder is the mock recorder for MockBillingHistoryService. diff --git a/do/mocks/CDNsService.go b/do/mocks/CDNsService.go index d2aaf34cf..72cdc4514 100644 --- a/do/mocks/CDNsService.go +++ b/do/mocks/CDNsService.go @@ -21,6 +21,7 @@ import ( type MockCDNsService struct { ctrl *gomock.Controller recorder *MockCDNsServiceMockRecorder + isgomock struct{} } // MockCDNsServiceMockRecorder is the mock recorder for MockCDNsService. diff --git a/do/mocks/CertificatesService.go b/do/mocks/CertificatesService.go index 33cecdf0b..b4d42f4ad 100644 --- a/do/mocks/CertificatesService.go +++ b/do/mocks/CertificatesService.go @@ -21,6 +21,7 @@ import ( type MockCertificatesService struct { ctrl *gomock.Controller recorder *MockCertificatesServiceMockRecorder + isgomock struct{} } // MockCertificatesServiceMockRecorder is the mock recorder for MockCertificatesService. diff --git a/do/mocks/DatabasesService.go b/do/mocks/DatabasesService.go index b9f2a99c0..b36985a5c 100644 --- a/do/mocks/DatabasesService.go +++ b/do/mocks/DatabasesService.go @@ -21,6 +21,7 @@ import ( type MockDatabasesService struct { ctrl *gomock.Controller recorder *MockDatabasesServiceMockRecorder + isgomock struct{} } // MockDatabasesServiceMockRecorder is the mock recorder for MockDatabasesService. diff --git a/do/mocks/DomainService.go b/do/mocks/DomainService.go index ae1c24f49..1528b28d8 100644 --- a/do/mocks/DomainService.go +++ b/do/mocks/DomainService.go @@ -21,6 +21,7 @@ import ( type MockDomainsService struct { ctrl *gomock.Controller recorder *MockDomainsServiceMockRecorder + isgomock struct{} } // MockDomainsServiceMockRecorder is the mock recorder for MockDomainsService. diff --git a/do/mocks/DropletActionService.go b/do/mocks/DropletActionService.go index 0aacf090d..4e6cac46b 100644 --- a/do/mocks/DropletActionService.go +++ b/do/mocks/DropletActionService.go @@ -13,6 +13,7 @@ import ( reflect "reflect" do "github.com/digitalocean/doctl/do" + godo "github.com/digitalocean/godo" gomock "go.uber.org/mock/gomock" ) @@ -20,6 +21,7 @@ import ( type MockDropletActionsService struct { ctrl *gomock.Controller recorder *MockDropletActionsServiceMockRecorder + isgomock struct{} } // MockDropletActionsServiceMockRecorder is the mock recorder for MockDropletActionsService. @@ -39,6 +41,21 @@ func (m *MockDropletActionsService) EXPECT() *MockDropletActionsServiceMockRecor return m.recorder } +// ChangeBackupPolicy mocks base method. +func (m *MockDropletActionsService) ChangeBackupPolicy(arg0 int, arg1 *godo.DropletBackupPolicyRequest) (*do.Action, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChangeBackupPolicy", arg0, arg1) + ret0, _ := ret[0].(*do.Action) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChangeBackupPolicy indicates an expected call of ChangeBackupPolicy. +func (mr *MockDropletActionsServiceMockRecorder) ChangeBackupPolicy(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeBackupPolicy", reflect.TypeOf((*MockDropletActionsService)(nil).ChangeBackupPolicy), arg0, arg1) +} + // ChangeKernel mocks base method. func (m *MockDropletActionsService) ChangeKernel(arg0, arg1 int) (*do.Action, error) { m.ctrl.T.Helper() diff --git a/do/mocks/FirewallsService.go b/do/mocks/FirewallsService.go index 8a318691c..28046e32d 100644 --- a/do/mocks/FirewallsService.go +++ b/do/mocks/FirewallsService.go @@ -21,6 +21,7 @@ import ( type MockFirewallsService struct { ctrl *gomock.Controller recorder *MockFirewallsServiceMockRecorder + isgomock struct{} } // MockFirewallsServiceMockRecorder is the mock recorder for MockFirewallsService. diff --git a/do/mocks/ImageActionsService.go b/do/mocks/ImageActionsService.go index 0ca645aa0..733c536f8 100644 --- a/do/mocks/ImageActionsService.go +++ b/do/mocks/ImageActionsService.go @@ -21,6 +21,7 @@ import ( type MockImageActionsService struct { ctrl *gomock.Controller recorder *MockImageActionsServiceMockRecorder + isgomock struct{} } // MockImageActionsServiceMockRecorder is the mock recorder for MockImageActionsService. diff --git a/do/mocks/ImageService.go b/do/mocks/ImageService.go index 4dacd4e2d..d8aa1ee5f 100644 --- a/do/mocks/ImageService.go +++ b/do/mocks/ImageService.go @@ -21,6 +21,7 @@ import ( type MockImagesService struct { ctrl *gomock.Controller recorder *MockImagesServiceMockRecorder + isgomock struct{} } // MockImagesServiceMockRecorder is the mock recorder for MockImagesService. diff --git a/do/mocks/InvoicesService.go b/do/mocks/InvoicesService.go index f7e2625f8..827f587d3 100644 --- a/do/mocks/InvoicesService.go +++ b/do/mocks/InvoicesService.go @@ -20,6 +20,7 @@ import ( type MockInvoicesService struct { ctrl *gomock.Controller recorder *MockInvoicesServiceMockRecorder + isgomock struct{} } // MockInvoicesServiceMockRecorder is the mock recorder for MockInvoicesService. diff --git a/do/mocks/KeysService.go b/do/mocks/KeysService.go index a4fd9002a..1a16868fa 100644 --- a/do/mocks/KeysService.go +++ b/do/mocks/KeysService.go @@ -21,6 +21,7 @@ import ( type MockKeysService struct { ctrl *gomock.Controller recorder *MockKeysServiceMockRecorder + isgomock struct{} } // MockKeysServiceMockRecorder is the mock recorder for MockKeysService. diff --git a/do/mocks/KubernetesService.go b/do/mocks/KubernetesService.go index 5ec1063c7..d4b44e985 100644 --- a/do/mocks/KubernetesService.go +++ b/do/mocks/KubernetesService.go @@ -21,6 +21,7 @@ import ( type MockKubernetesService struct { ctrl *gomock.Controller recorder *MockKubernetesServiceMockRecorder + isgomock struct{} } // MockKubernetesServiceMockRecorder is the mock recorder for MockKubernetesService. diff --git a/do/mocks/Listen.go b/do/mocks/Listen.go index 49e600c1c..94882a4ea 100644 --- a/do/mocks/Listen.go +++ b/do/mocks/Listen.go @@ -19,6 +19,7 @@ import ( type MockListenerService struct { ctrl *gomock.Controller recorder *MockListenerServiceMockRecorder + isgomock struct{} } // MockListenerServiceMockRecorder is the mock recorder for MockListenerService. diff --git a/do/mocks/LoadBalancersService.go b/do/mocks/LoadBalancersService.go index ac9644046..d58affbfe 100644 --- a/do/mocks/LoadBalancersService.go +++ b/do/mocks/LoadBalancersService.go @@ -21,6 +21,7 @@ import ( type MockLoadBalancersService struct { ctrl *gomock.Controller recorder *MockLoadBalancersServiceMockRecorder + isgomock struct{} } // MockLoadBalancersServiceMockRecorder is the mock recorder for MockLoadBalancersService. diff --git a/do/mocks/MonitoringService.go b/do/mocks/MonitoringService.go index 42d33f7e4..a2c29be3c 100644 --- a/do/mocks/MonitoringService.go +++ b/do/mocks/MonitoringService.go @@ -21,6 +21,7 @@ import ( type MockMonitoringService struct { ctrl *gomock.Controller recorder *MockMonitoringServiceMockRecorder + isgomock struct{} } // MockMonitoringServiceMockRecorder is the mock recorder for MockMonitoringService. diff --git a/do/mocks/OAuthService.go b/do/mocks/OAuthService.go index eba9c703a..56c5c8179 100644 --- a/do/mocks/OAuthService.go +++ b/do/mocks/OAuthService.go @@ -20,6 +20,7 @@ import ( type MockOAuthService struct { ctrl *gomock.Controller recorder *MockOAuthServiceMockRecorder + isgomock struct{} } // MockOAuthServiceMockRecorder is the mock recorder for MockOAuthService. diff --git a/do/mocks/OneClickService.go b/do/mocks/OneClickService.go index 825b19ea6..7dd68384f 100644 --- a/do/mocks/OneClickService.go +++ b/do/mocks/OneClickService.go @@ -20,6 +20,7 @@ import ( type MockOneClickService struct { ctrl *gomock.Controller recorder *MockOneClickServiceMockRecorder + isgomock struct{} } // MockOneClickServiceMockRecorder is the mock recorder for MockOneClickService. diff --git a/do/mocks/ProjectsService.go b/do/mocks/ProjectsService.go index f50b07e76..28005dee5 100644 --- a/do/mocks/ProjectsService.go +++ b/do/mocks/ProjectsService.go @@ -21,6 +21,7 @@ import ( type MockProjectsService struct { ctrl *gomock.Controller recorder *MockProjectsServiceMockRecorder + isgomock struct{} } // MockProjectsServiceMockRecorder is the mock recorder for MockProjectsService. diff --git a/do/mocks/RegionsService.go b/do/mocks/RegionsService.go index 8c6d2165b..28b1b41a8 100644 --- a/do/mocks/RegionsService.go +++ b/do/mocks/RegionsService.go @@ -20,6 +20,7 @@ import ( type MockRegionsService struct { ctrl *gomock.Controller recorder *MockRegionsServiceMockRecorder + isgomock struct{} } // MockRegionsServiceMockRecorder is the mock recorder for MockRegionsService. diff --git a/do/mocks/RegistryService.go b/do/mocks/RegistryService.go index 74d41b9b4..ddb9165d5 100644 --- a/do/mocks/RegistryService.go +++ b/do/mocks/RegistryService.go @@ -21,6 +21,7 @@ import ( type MockRegistryService struct { ctrl *gomock.Controller recorder *MockRegistryServiceMockRecorder + isgomock struct{} } // MockRegistryServiceMockRecorder is the mock recorder for MockRegistryService. diff --git a/do/mocks/ReservedIPActionsService.go b/do/mocks/ReservedIPActionsService.go index 5725b1ca0..0b835976d 100644 --- a/do/mocks/ReservedIPActionsService.go +++ b/do/mocks/ReservedIPActionsService.go @@ -21,6 +21,7 @@ import ( type MockReservedIPActionsService struct { ctrl *gomock.Controller recorder *MockReservedIPActionsServiceMockRecorder + isgomock struct{} } // MockReservedIPActionsServiceMockRecorder is the mock recorder for MockReservedIPActionsService. diff --git a/do/mocks/ReservedIPsService.go b/do/mocks/ReservedIPsService.go index 1a16d115c..fb828b94f 100644 --- a/do/mocks/ReservedIPsService.go +++ b/do/mocks/ReservedIPsService.go @@ -21,6 +21,7 @@ import ( type MockReservedIPsService struct { ctrl *gomock.Controller recorder *MockReservedIPsServiceMockRecorder + isgomock struct{} } // MockReservedIPsServiceMockRecorder is the mock recorder for MockReservedIPsService. diff --git a/do/mocks/Runner.go b/do/mocks/Runner.go index 55a32a8e1..8bfcb48f4 100644 --- a/do/mocks/Runner.go +++ b/do/mocks/Runner.go @@ -19,6 +19,7 @@ import ( type MockRunner struct { ctrl *gomock.Controller recorder *MockRunnerMockRecorder + isgomock struct{} } // MockRunnerMockRecorder is the mock recorder for MockRunner. diff --git a/do/mocks/ServerlessService.go b/do/mocks/ServerlessService.go index 9a14a899b..f26c5c7ad 100644 --- a/do/mocks/ServerlessService.go +++ b/do/mocks/ServerlessService.go @@ -23,6 +23,7 @@ import ( type MockServerlessService struct { ctrl *gomock.Controller recorder *MockServerlessServiceMockRecorder + isgomock struct{} } // MockServerlessServiceMockRecorder is the mock recorder for MockServerlessService. diff --git a/do/mocks/SizesService.go b/do/mocks/SizesService.go index c9ffe8057..a2c57b032 100644 --- a/do/mocks/SizesService.go +++ b/do/mocks/SizesService.go @@ -20,6 +20,7 @@ import ( type MockSizesService struct { ctrl *gomock.Controller recorder *MockSizesServiceMockRecorder + isgomock struct{} } // MockSizesServiceMockRecorder is the mock recorder for MockSizesService. diff --git a/do/mocks/SnapshotsService.go b/do/mocks/SnapshotsService.go index 09086ae82..cb69ec7ac 100644 --- a/do/mocks/SnapshotsService.go +++ b/do/mocks/SnapshotsService.go @@ -20,6 +20,7 @@ import ( type MockSnapshotsService struct { ctrl *gomock.Controller recorder *MockSnapshotsServiceMockRecorder + isgomock struct{} } // MockSnapshotsServiceMockRecorder is the mock recorder for MockSnapshotsService. diff --git a/do/mocks/TagsService.go b/do/mocks/TagsService.go index 5a6ed17a8..5f31ce663 100644 --- a/do/mocks/TagsService.go +++ b/do/mocks/TagsService.go @@ -21,6 +21,7 @@ import ( type MockTagsService struct { ctrl *gomock.Controller recorder *MockTagsServiceMockRecorder + isgomock struct{} } // MockTagsServiceMockRecorder is the mock recorder for MockTagsService. diff --git a/do/mocks/UptimeChecksService.go b/do/mocks/UptimeChecksService.go index 926c34e4d..b4baf99b7 100644 --- a/do/mocks/UptimeChecksService.go +++ b/do/mocks/UptimeChecksService.go @@ -21,6 +21,7 @@ import ( type MockUptimeChecksService struct { ctrl *gomock.Controller recorder *MockUptimeChecksServiceMockRecorder + isgomock struct{} } // MockUptimeChecksServiceMockRecorder is the mock recorder for MockUptimeChecksService. diff --git a/do/mocks/VPCsService.go b/do/mocks/VPCsService.go index 33d86691f..a4f4873f7 100644 --- a/do/mocks/VPCsService.go +++ b/do/mocks/VPCsService.go @@ -21,6 +21,7 @@ import ( type MockVPCsService struct { ctrl *gomock.Controller recorder *MockVPCsServiceMockRecorder + isgomock struct{} } // MockVPCsServiceMockRecorder is the mock recorder for MockVPCsService. diff --git a/do/mocks/VolumeActionsService.go b/do/mocks/VolumeActionsService.go index 60a22b67f..423dc5d88 100644 --- a/do/mocks/VolumeActionsService.go +++ b/do/mocks/VolumeActionsService.go @@ -20,6 +20,7 @@ import ( type MockVolumeActionsService struct { ctrl *gomock.Controller recorder *MockVolumeActionsServiceMockRecorder + isgomock struct{} } // MockVolumeActionsServiceMockRecorder is the mock recorder for MockVolumeActionsService. diff --git a/do/mocks/VolumesService.go b/do/mocks/VolumesService.go index 48920d665..bad84e71c 100644 --- a/do/mocks/VolumesService.go +++ b/do/mocks/VolumesService.go @@ -21,6 +21,7 @@ import ( type MockVolumesService struct { ctrl *gomock.Controller recorder *MockVolumesServiceMockRecorder + isgomock struct{} } // MockVolumesServiceMockRecorder is the mock recorder for MockVolumesService. diff --git a/go.mod b/go.mod index 91b3ac287..11c716ec9 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/blang/semver v3.5.1+incompatible github.com/creack/pty v1.1.21 - github.com/digitalocean/godo v1.128.1-0.20241025145008-2654a9d1e887 + github.com/digitalocean/godo v1.128.1-0.20241105142035-a379869c286c github.com/docker/cli v24.0.5+incompatible github.com/docker/docker v25.0.6+incompatible github.com/docker/docker-credential-helpers v0.7.0 // indirect diff --git a/go.sum b/go.sum index 04b75b62f..0267ad795 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/digitalocean/godo v1.128.1-0.20241025145008-2654a9d1e887 h1:kdXNbMfHEDbQilcqllKkNrJ85ftyJSvSDpsQvzrhHbg= -github.com/digitalocean/godo v1.128.1-0.20241025145008-2654a9d1e887/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc= +github.com/digitalocean/godo v1.128.1-0.20241105142035-a379869c286c h1:Z1Osn3VmAR5FtPgrdi2VTxa069CnKc/DCXX1U2qpNpM= +github.com/digitalocean/godo v1.128.1-0.20241105142035-a379869c286c/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc= diff --git a/vendor/github.com/digitalocean/godo/droplet_actions.go b/vendor/github.com/digitalocean/godo/droplet_actions.go index 2e09d0c59..ed0f583c9 100644 --- a/vendor/github.com/digitalocean/godo/droplet_actions.go +++ b/vendor/github.com/digitalocean/godo/droplet_actions.go @@ -30,6 +30,8 @@ type DropletActionsService interface { SnapshotByTag(context.Context, string, string) ([]Action, *Response, error) EnableBackups(context.Context, int) (*Action, *Response, error) EnableBackupsByTag(context.Context, string) ([]Action, *Response, error) + EnableBackupsWithPolicy(context.Context, int, *DropletBackupPolicyRequest) (*Action, *Response, error) + ChangeBackupPolicy(context.Context, int, *DropletBackupPolicyRequest) (*Action, *Response, error) DisableBackups(context.Context, int) (*Action, *Response, error) DisableBackupsByTag(context.Context, string) ([]Action, *Response, error) PasswordReset(context.Context, int) (*Action, *Response, error) @@ -169,6 +171,42 @@ func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag st return s.doActionByTag(ctx, tag, request) } +// EnableBackupsWithPolicy enables droplet's backup with a backup policy applied. +func (s *DropletActionsServiceOp) EnableBackupsWithPolicy(ctx context.Context, id int, policy *DropletBackupPolicyRequest) (*Action, *Response, error) { + if policy == nil { + return nil, nil, NewArgError("policy", "policy can't be nil") + } + + policyMap := map[string]interface{}{ + "plan": policy.Plan, + "weekday": policy.Weekday, + } + if policy.Hour != nil { + policyMap["hour"] = policy.Hour + } + + request := &ActionRequest{"type": "enable_backups", "backup_policy": policyMap} + return s.doAction(ctx, id, request) +} + +// ChangeBackupPolicy updates a backup policy when backups are enabled. +func (s *DropletActionsServiceOp) ChangeBackupPolicy(ctx context.Context, id int, policy *DropletBackupPolicyRequest) (*Action, *Response, error) { + if policy == nil { + return nil, nil, NewArgError("policy", "policy can't be nil") + } + + policyMap := map[string]interface{}{ + "plan": policy.Plan, + "weekday": policy.Weekday, + } + if policy.Hour != nil { + policyMap["hour"] = policy.Hour + } + + request := &ActionRequest{"type": "change_backup_policy", "backup_policy": policyMap} + return s.doAction(ctx, id, request) +} + // DisableBackups disables backups for a Droplet. func (s *DropletActionsServiceOp) DisableBackups(ctx context.Context, id int) (*Action, *Response, error) { request := &ActionRequest{"type": "disable_backups"} diff --git a/vendor/github.com/digitalocean/godo/droplets.go b/vendor/github.com/digitalocean/godo/droplets.go index 1ed09ec8c..2ddd7d6b7 100644 --- a/vendor/github.com/digitalocean/godo/droplets.go +++ b/vendor/github.com/digitalocean/godo/droplets.go @@ -30,6 +30,9 @@ type DropletsService interface { Backups(context.Context, int, *ListOptions) ([]Image, *Response, error) Actions(context.Context, int, *ListOptions) ([]Action, *Response, error) Neighbors(context.Context, int) ([]Droplet, *Response, error) + GetBackupPolicy(context.Context, int) (*DropletBackupPolicy, *Response, error) + ListBackupPolicies(context.Context, *ListOptions) (map[int]*DropletBackupPolicy, *Response, error) + ListSupportedBackupPolicies(context.Context) ([]*SupportedBackupPolicy, *Response, error) } // DropletsServiceOp handles communication with the Droplet related methods of the @@ -218,37 +221,46 @@ func (d DropletCreateSSHKey) MarshalJSON() ([]byte, error) { // DropletCreateRequest represents a request to create a Droplet. type DropletCreateRequest struct { - Name string `json:"name"` - Region string `json:"region"` - Size string `json:"size"` - Image DropletCreateImage `json:"image"` - SSHKeys []DropletCreateSSHKey `json:"ssh_keys"` - Backups bool `json:"backups"` - IPv6 bool `json:"ipv6"` - PrivateNetworking bool `json:"private_networking"` - Monitoring bool `json:"monitoring"` - UserData string `json:"user_data,omitempty"` - Volumes []DropletCreateVolume `json:"volumes,omitempty"` - Tags []string `json:"tags"` - VPCUUID string `json:"vpc_uuid,omitempty"` - WithDropletAgent *bool `json:"with_droplet_agent,omitempty"` + Name string `json:"name"` + Region string `json:"region"` + Size string `json:"size"` + Image DropletCreateImage `json:"image"` + SSHKeys []DropletCreateSSHKey `json:"ssh_keys"` + Backups bool `json:"backups"` + IPv6 bool `json:"ipv6"` + PrivateNetworking bool `json:"private_networking"` + Monitoring bool `json:"monitoring"` + UserData string `json:"user_data,omitempty"` + Volumes []DropletCreateVolume `json:"volumes,omitempty"` + Tags []string `json:"tags"` + VPCUUID string `json:"vpc_uuid,omitempty"` + WithDropletAgent *bool `json:"with_droplet_agent,omitempty"` + BackupPolicy *DropletBackupPolicyRequest `json:"backup_policy,omitempty"` } // DropletMultiCreateRequest is a request to create multiple Droplets. type DropletMultiCreateRequest struct { - Names []string `json:"names"` - Region string `json:"region"` - Size string `json:"size"` - Image DropletCreateImage `json:"image"` - SSHKeys []DropletCreateSSHKey `json:"ssh_keys"` - Backups bool `json:"backups"` - IPv6 bool `json:"ipv6"` - PrivateNetworking bool `json:"private_networking"` - Monitoring bool `json:"monitoring"` - UserData string `json:"user_data,omitempty"` - Tags []string `json:"tags"` - VPCUUID string `json:"vpc_uuid,omitempty"` - WithDropletAgent *bool `json:"with_droplet_agent,omitempty"` + Names []string `json:"names"` + Region string `json:"region"` + Size string `json:"size"` + Image DropletCreateImage `json:"image"` + SSHKeys []DropletCreateSSHKey `json:"ssh_keys"` + Backups bool `json:"backups"` + IPv6 bool `json:"ipv6"` + PrivateNetworking bool `json:"private_networking"` + Monitoring bool `json:"monitoring"` + UserData string `json:"user_data,omitempty"` + Tags []string `json:"tags"` + VPCUUID string `json:"vpc_uuid,omitempty"` + WithDropletAgent *bool `json:"with_droplet_agent,omitempty"` + BackupPolicy *DropletBackupPolicyRequest `json:"backup_policy,omitempty"` +} + +// DropletBackupPolicyRequest defines the backup policy when creating a Droplet. +type DropletBackupPolicyRequest struct { + Plan string `json:"plan,omitempty"` + Weekday string `json:"weekday,omitempty"` + Hour *int `json:"hour,omitempty"` } func (d DropletCreateRequest) String() string { @@ -618,3 +630,109 @@ func (s *DropletsServiceOp) dropletActionStatus(ctx context.Context, uri string) return action.Status, nil } + +// DropletBackupPolicy defines the information about a droplet's backup policy. +type DropletBackupPolicy struct { + DropletID int `json:"droplet_id,omitempty"` + BackupEnabled bool `json:"backup_enabled,omitempty"` + BackupPolicy *DropletBackupPolicyConfig `json:"backup_policy,omitempty"` + NextBackupWindow *BackupWindow `json:"next_backup_window,omitempty"` +} + +// DropletBackupPolicyConfig defines the backup policy for a Droplet. +type DropletBackupPolicyConfig struct { + Plan string `json:"plan,omitempty"` + Weekday string `json:"weekday,omitempty"` + Hour int `json:"hour,omitempty"` + WindowLengthHours int `json:"window_length_hours,omitempty"` + RetentionPeriodDays int `json:"retention_period_days,omitempty"` +} + +// dropletBackupPolicyRoot represents a DropletBackupPolicy root +type dropletBackupPolicyRoot struct { + DropletBackupPolicy *DropletBackupPolicy `json:"policy,omitempty"` +} + +type dropletBackupPoliciesRoot struct { + DropletBackupPolicies map[int]*DropletBackupPolicy `json:"policies,omitempty"` + Links *Links `json:"links,omitempty"` + Meta *Meta `json:"meta"` +} + +// Get individual droplet backup policy. +func (s *DropletsServiceOp) GetBackupPolicy(ctx context.Context, dropletID int) (*DropletBackupPolicy, *Response, error) { + if dropletID < 1 { + return nil, nil, NewArgError("dropletID", "cannot be less than 1") + } + + path := fmt.Sprintf("%s/%d/backups/policy", dropletBasePath, dropletID) + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(dropletBackupPolicyRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.DropletBackupPolicy, resp, err +} + +// List all droplet backup policies. +func (s *DropletsServiceOp) ListBackupPolicies(ctx context.Context, opt *ListOptions) (map[int]*DropletBackupPolicy, *Response, error) { + path := fmt.Sprintf("%s/backups/policies", dropletBasePath) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(dropletBackupPoliciesRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + if m := root.Meta; m != nil { + resp.Meta = m + } + + return root.DropletBackupPolicies, resp, nil +} + +type SupportedBackupPolicy struct { + Name string `json:"name,omitempty"` + PossibleWindowStarts []int `json:"possible_window_starts,omitempty"` + WindowLengthHours int `json:"window_length_hours,omitempty"` + RetentionPeriodDays int `json:"retention_period_days,omitempty"` + PossibleDays []string `json:"possible_days,omitempty"` +} + +type dropletSupportedBackupPoliciesRoot struct { + SupportedBackupPolicies []*SupportedBackupPolicy `json:"supported_policies,omitempty"` +} + +// List supported droplet backup policies. +func (s *DropletsServiceOp) ListSupportedBackupPolicies(ctx context.Context) ([]*SupportedBackupPolicy, *Response, error) { + path := fmt.Sprintf("%s/backups/supported_policies", dropletBasePath) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(dropletSupportedBackupPoliciesRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.SupportedBackupPolicies, resp, nil +} diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go index edf0f6d46..efa1bd616 100644 --- a/vendor/github.com/digitalocean/godo/godo.go +++ b/vendor/github.com/digitalocean/godo/godo.go @@ -81,6 +81,7 @@ type Client struct { Projects ProjectsService Regions RegionsService Registry RegistryService + Registries RegistriesService ReservedIPs ReservedIPsService ReservedIPActions ReservedIPActionsService Sizes SizesService @@ -292,6 +293,7 @@ func NewClient(httpClient *http.Client) *Client { c.Projects = &ProjectsServiceOp{client: c} c.Regions = &RegionsServiceOp{client: c} c.Registry = &RegistryServiceOp{client: c} + c.Registries = &RegistriesServiceOp{client: c} c.ReservedIPs = &ReservedIPsServiceOp{client: c} c.ReservedIPActions = &ReservedIPActionsServiceOp{client: c} c.Sizes = &SizesServiceOp{client: c} diff --git a/vendor/github.com/digitalocean/godo/registry.go b/vendor/github.com/digitalocean/godo/registry.go index b0c243281..e64822682 100644 --- a/vendor/github.com/digitalocean/godo/registry.go +++ b/vendor/github.com/digitalocean/godo/registry.go @@ -14,6 +14,9 @@ const ( registryPath = "/v2/registry" // RegistryServer is the hostname of the DigitalOcean registry service RegistryServer = "registry.digitalocean.com" + + // Multi-registry Open Beta API constants + registriesPath = "/v2/registries" ) // RegistryService is an interface for interfacing with the Registry endpoints @@ -240,6 +243,19 @@ type RegistryValidateNameRequest struct { Name string `json:"name"` } +// Multi-registry Open Beta API structs + +type registriesRoot struct { + Registries []*Registry `json:"registries,omitempty"` + TotalStorageUsageBytes uint64 `json:"total_storage_usage_bytes,omitempty"` +} + +// RegistriesCreateRequest represents a request to create a secondary registry. +type RegistriesCreateRequest struct { + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` +} + // Get retrieves the details of a Registry. func (svc *RegistryServiceOp) Get(ctx context.Context) (*Registry, *Response, error) { req, err := svc.client.NewRequest(ctx, http.MethodGet, registryPath, nil) @@ -610,3 +626,107 @@ func (svc *RegistryServiceOp) ValidateName(ctx context.Context, request *Registr } return resp, nil } + +// RegistriesService is an interface for interfacing with the new multiple-registry beta endpoints +// of the DigitalOcean API. +// +// We are creating a separate Service in alignment with the new /v2/registries endpoints. +type RegistriesService interface { + Get(context.Context, string) (*Registry, *Response, error) + List(context.Context) ([]*Registry, *Response, error) + Create(context.Context, *RegistriesCreateRequest) (*Registry, *Response, error) + Delete(context.Context, string) (*Response, error) + DockerCredentials(context.Context, string, *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) +} + +var _ RegistriesService = &RegistriesServiceOp{} + +// RegistriesServiceOp handles communication with the multiple-registry beta methods. +type RegistriesServiceOp struct { + client *Client +} + +// Get returns the details of a named Registry. +func (svc *RegistriesServiceOp) Get(ctx context.Context, registry string) (*Registry, *Response, error) { + path := fmt.Sprintf("%s/%s", registriesPath, registry) + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(registryRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Registry, resp, nil +} + +// List returns a list of the named Registries. +func (svc *RegistriesServiceOp) List(ctx context.Context) ([]*Registry, *Response, error) { + req, err := svc.client.NewRequest(ctx, http.MethodGet, registriesPath, nil) + if err != nil { + return nil, nil, err + } + root := new(registriesRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Registries, resp, nil +} + +// Create creates a named Registry. +func (svc *RegistriesServiceOp) Create(ctx context.Context, create *RegistriesCreateRequest) (*Registry, *Response, error) { + req, err := svc.client.NewRequest(ctx, http.MethodPost, registriesPath, create) + if err != nil { + return nil, nil, err + } + root := new(registryRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Registry, resp, nil +} + +// Delete deletes a named Registry. There is no way to recover a Registry once it has +// been destroyed. +func (svc *RegistriesServiceOp) Delete(ctx context.Context, registry string) (*Response, error) { + path := fmt.Sprintf("%s/%s", registriesPath, registry) + req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + resp, err := svc.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + return resp, nil +} + +// DockerCredentials retrieves a Docker config file containing named Registry's credentials. +func (svc *RegistriesServiceOp) DockerCredentials(ctx context.Context, registry string, request *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) { + path := fmt.Sprintf("%s/%s/%s", registriesPath, registry, "docker-credentials") + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + q := req.URL.Query() + q.Add("read_write", strconv.FormatBool(request.ReadWrite)) + if request.ExpirySeconds != nil { + q.Add("expiry_seconds", strconv.Itoa(*request.ExpirySeconds)) + } + req.URL.RawQuery = q.Encode() + + var buf bytes.Buffer + resp, err := svc.client.Do(ctx, req, &buf) + if err != nil { + return nil, resp, err + } + + dc := &DockerCredentials{ + DockerConfigJSON: buf.Bytes(), + } + return dc, resp, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1ee0dde8a..2b69c553b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -61,7 +61,7 @@ github.com/creack/pty # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc ## explicit github.com/davecgh/go-spew/spew -# github.com/digitalocean/godo v1.128.1-0.20241025145008-2654a9d1e887 +# github.com/digitalocean/godo v1.128.1-0.20241105142035-a379869c286c ## explicit; go 1.22 github.com/digitalocean/godo github.com/digitalocean/godo/metrics From 71b3046c78cd6de977052a870fb5d27d6a0f3fcd Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Tue, 5 Nov 2024 14:09:21 -0500 Subject: [PATCH 02/32] add internal droplets package to parse a policy --- internal/droplets/droplets.go | 58 ++++++++++++++++++++++++++++++ internal/droplets/droplets_test.go | 1 + 2 files changed, 59 insertions(+) create mode 100644 internal/droplets/droplets.go create mode 100644 internal/droplets/droplets_test.go diff --git a/internal/droplets/droplets.go b/internal/droplets/droplets.go new file mode 100644 index 000000000..cb5cea6ae --- /dev/null +++ b/internal/droplets/droplets.go @@ -0,0 +1,58 @@ +package droplets + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + + "github.com/digitalocean/godo" + "sigs.k8s.io/yaml" +) + +func ReadDropletBackupPolicy(stdin io.Reader, path string) (*godo.DropletBackupPolicyRequest, error) { + var policy io.Reader + if path == "-" && stdin != nil { + policy = stdin + } else { + specFile, err := os.Open(path) // guardrails-disable-line + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("opening droplet backup policy: %s does not exist", path) + } + return nil, fmt.Errorf("opening droplet backup policy: %w", err) + } + defer specFile.Close() + policy = specFile + } + + byt, err := io.ReadAll(policy) + if err != nil { + return nil, fmt.Errorf("reading droplet backup policy: %w", err) + } + + s, err := ParseDropletBackupPolicy(byt) + if err != nil { + return nil, fmt.Errorf("parsing droplet backup policy: %w", err) + } + + return s, nil +} + +func ParseDropletBackupPolicy(spec []byte) (*godo.DropletBackupPolicyRequest, error) { + jsonSpec, err := yaml.YAMLToJSON(spec) + if err != nil { + return nil, err + } + + dec := json.NewDecoder(bytes.NewReader(jsonSpec)) + dec.DisallowUnknownFields() + + var policy godo.DropletBackupPolicyRequest + if err := dec.Decode(&policy); err != nil { + return nil, err + } + + return &policy, nil +} diff --git a/internal/droplets/droplets_test.go b/internal/droplets/droplets_test.go new file mode 100644 index 000000000..2eadb6532 --- /dev/null +++ b/internal/droplets/droplets_test.go @@ -0,0 +1 @@ +package droplets From fcd37de5b48abda865468ce5bf9a0f4133bb1fdd Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Tue, 5 Nov 2024 15:37:52 -0500 Subject: [PATCH 03/32] fix tests, add a new test for droplet actions backup policy update --- commands/droplet_actions_test.go | 37 +++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/commands/droplet_actions_test.go b/commands/droplet_actions_test.go index 4aa76ef9b..75bc092e9 100644 --- a/commands/droplet_actions_test.go +++ b/commands/droplet_actions_test.go @@ -14,16 +14,20 @@ limitations under the License. package commands import ( + "encoding/json" + "os" "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) { @@ -78,6 +82,37 @@ func TestDropletActionsDisableBackups(t *testing.T) { }) } +func TestDropletChangeBackupPolicy(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + dropletPolicy := godo.DropletBackupPolicyRequest{ + Plan: "weekly", + Weekday: "SAT", + Hour: godo.PtrTo(0), + } + + policyFile, err := os.CreateTemp(t.TempDir(), "policy-cfg") + require.NoError(t, err) + defer policyFile.Close() + + err = json.NewEncoder(policyFile).Encode(&dropletPolicy) + require.NoError(t, err) + + policyReq := &godo.DropletBackupPolicyRequest{ + Plan: dropletPolicy.Plan, + Weekday: dropletPolicy.Weekday, + Hour: dropletPolicy.Hour, + } + + tm.dropletActions.EXPECT().ChangeBackupPolicy(1, policyReq).Times(1).Return(&testAction, nil) + + config.Args = append(config.Args, "1") + config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicy, policyFile.Name()) + + 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) From 869797d95c7e63c841a474825f185428f29eb153 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Tue, 5 Nov 2024 17:06:13 -0500 Subject: [PATCH 04/32] add droplet backup policies into droplet create --- commands/droplets.go | 19 +++++++++++++ commands/droplets_test.go | 57 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/commands/droplets.go b/commands/droplets.go index 139eda2ff..725efc83d 100644 --- a/commands/droplets.go +++ b/commands/droplets.go @@ -25,6 +25,7 @@ import ( "github.com/digitalocean/doctl" "github.com/digitalocean/doctl/commands/displayers" "github.com/digitalocean/doctl/do" + "github.com/digitalocean/doctl/internal/droplets" "github.com/digitalocean/godo" "github.com/gobwas/glob" "github.com/spf13/cobra" @@ -81,6 +82,9 @@ If you do not specify a region, the Droplet is created in the default region for AddStringFlag(cmdDropletCreate, doctl.ArgSizeSlug, "", "", "A `slug` indicating the Droplet's number of vCPUs, RAM, and disk size. For example, `s-1vcpu-1gb` specifies a Droplet with one vCPU and 1 GiB of RAM. The disk size is defined by the slug's plan. Run `doctl compute size list` for a list of valid size slugs and their disk sizes.", requiredOpt()) AddBoolFlag(cmdDropletCreate, doctl.ArgBackups, "", false, "Enables backups for the Droplet. By default, backups are created on a daily basis.") + AddStringFlag(cmdDropletCreate, doctl.ArgDropletBackupPolicy, "", "", + `Path to a droplet backup policy in JSON or YAML format. Set to "-" to read from stdin. Backups must be enabled for the Droplet to apply the backup policy. + Example: doctl compute droplet create my-droplet --size s-1vcpu-1gb --image ubuntu-20-04-x64 --region nyc1 --enable-backups --backup-policy path-to-policy/policy-cfg.yaml`) AddBoolFlag(cmdDropletCreate, doctl.ArgIPv6, "", false, "Enables IPv6 support and assigns an IPv6 address to the Droplet") AddBoolFlag(cmdDropletCreate, doctl.ArgPrivateNetworking, "", false, "Enables private networking for the Droplet by provisioning it inside of your account's default VPC for the region") AddBoolFlag(cmdDropletCreate, doctl.ArgMonitoring, "", false, "Installs the DigitalOcean agent for additional monitoring") @@ -197,6 +201,20 @@ func RunDropletCreate(c *CmdConfig) error { return err } + backupPolicyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) + if err != nil { + return err + } + + var backupPolicy *godo.DropletBackupPolicyRequest + + if backupPolicyPath != "" { + backupPolicy, err = droplets.ReadDropletBackupPolicy(os.Stdin, backupPolicyPath) + if err != nil { + return err + } + } + ipv6, err := c.Doit.GetBool(c.NS, doctl.ArgIPv6) if err != nil { return err @@ -298,6 +316,7 @@ func RunDropletCreate(c *CmdConfig) error { Image: createImage, Volumes: volumes, Backups: backups, + BackupPolicy: backupPolicy, IPv6: ipv6, PrivateNetworking: privateNetworking, Monitoring: monitoring, diff --git a/commands/droplets_test.go b/commands/droplets_test.go index 2cc7ceeda..ca78c587c 100644 --- a/commands/droplets_test.go +++ b/commands/droplets_test.go @@ -15,6 +15,7 @@ package commands import ( "bytes" + "encoding/json" "os" "strconv" "testing" @@ -23,6 +24,7 @@ import ( "github.com/digitalocean/doctl/do" "github.com/digitalocean/godo" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var ( @@ -117,6 +119,61 @@ func TestDropletCreate(t *testing.T) { }) } +func TestDropletCreateWithBackupPolicy(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + dropletPolicy := godo.DropletBackupPolicyRequest{ + Plan: "weekly", + Weekday: "SAT", + Hour: godo.PtrTo(0), + } + volumeUUID := "00000000-0000-4000-8000-000000000000" + vpcUUID := "00000000-0000-4000-8000-000000000000" + dcr := &godo.DropletCreateRequest{ + Name: "droplet", + Region: "dev0", + Size: "1gb", + Image: godo.DropletCreateImage{ID: 0, Slug: "image"}, + SSHKeys: []godo.DropletCreateSSHKey{}, + Volumes: []godo.DropletCreateVolume{ + {Name: "test-volume"}, + {ID: volumeUUID}, + }, + Backups: true, + IPv6: false, + PrivateNetworking: false, + Monitoring: false, + VPCUUID: vpcUUID, + UserData: "#cloud-config", + Tags: []string{"one", "two"}, + BackupPolicy: &dropletPolicy, + } + + policyFile, err := os.CreateTemp(t.TempDir(), "policy-cfg") + require.NoError(t, err) + defer policyFile.Close() + + err = json.NewEncoder(policyFile).Encode(&dropletPolicy) + require.NoError(t, err) + + tm.droplets.EXPECT().Create(dcr, false).Return(&testDroplet, nil) + + config.Args = append(config.Args, "droplet") + + config.Doit.Set(config.NS, doctl.ArgRegionSlug, "dev0") + config.Doit.Set(config.NS, doctl.ArgSizeSlug, "1gb") + config.Doit.Set(config.NS, doctl.ArgImage, "image") + config.Doit.Set(config.NS, doctl.ArgUserData, "#cloud-config") + config.Doit.Set(config.NS, doctl.ArgVPCUUID, vpcUUID) + config.Doit.Set(config.NS, doctl.ArgVolumeList, []string{"test-volume", volumeUUID}) + config.Doit.Set(config.NS, doctl.ArgTagNames, []string{"one", "two"}) + config.Doit.Set(config.NS, doctl.ArgBackups, true) + config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicy, policyFile.Name()) + + err = RunDropletCreate(config) + assert.NoError(t, err) + }) +} + func TestDropletCreateWithTag(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { dcr := &godo.DropletCreateRequest{ From 4e66a20efff328f09d9be44861ce62d097facda1 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 6 Nov 2024 13:52:53 -0500 Subject: [PATCH 05/32] rename droplet-action command to change-backup-policy --- commands/droplet_actions.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/droplet_actions.go b/commands/droplet_actions.go index 36f54bbd4..436b938cd 100644 --- a/commands/droplet_actions.go +++ b/commands/droplet_actions.go @@ -85,11 +85,11 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re 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 ", "Change backup policy on a Droplet", `Changes backup policy for a Droplet with enabled backups.`, Writer, + "change-backup-policy ", "Change backup policy on a Droplet", `Changes backup policy for a Droplet with enabled backups.`, Writer, displayerType(&displayers.Action{})) AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicy, "", "", `Path to a new backup policy in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) // AddBoolFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgCommandWait, "", false, "Wait for action to complete") // TODO: Add this flag when the doctl supports reading policy. - 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 src/your-backup-policy.yaml` + 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 src/your-backup-policy.yaml` cmdDropletActionReboot := CmdBuilder(cmd, RunDropletActionReboot, "reboot ", "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, From 5a7da2c2c1814890b1ed199fb22aa7a6c1889363 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 6 Nov 2024 13:56:01 -0500 Subject: [PATCH 06/32] fix tests after command renaming --- commands/droplet_actions_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/droplet_actions_test.go b/commands/droplet_actions_test.go index 75bc092e9..d9c8481fb 100644 --- a/commands/droplet_actions_test.go +++ b/commands/droplet_actions_test.go @@ -27,7 +27,7 @@ import ( func TestDropletActionCommand(t *testing.T) { cmd := DropletAction() assert.NotNil(t, cmd) - 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") + 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) { From ccb5f2912af8bd608bbd80d7458d92a44f99a4e2 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 6 Nov 2024 14:09:26 -0500 Subject: [PATCH 07/32] add enable backups with policy to droplet actions --- commands/droplet_actions.go | 32 ++++++++++++++++++++++++++++++++ do/droplet_actions.go | 6 ++++++ 2 files changed, 38 insertions(+) diff --git a/commands/droplet_actions.go b/commands/droplet_actions.go index 436b938cd..04c99cb6a 100644 --- a/commands/droplet_actions.go +++ b/commands/droplet_actions.go @@ -91,6 +91,13 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re // AddBoolFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgCommandWait, "", false, "Wait for action to complete") // TODO: Add this flag when the doctl supports reading policy. 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 src/your-backup-policy.yaml` + cmdDropletActionEnableBackupsWithPolicy := CmdBuilder(cmd, RunDropletActionEnableBackupsWithPolicy, + "enable-backups-with-policy ", "Enable backups with a policy on a Droplet", `Enables backups and apply a backup policy for a Droplet.`, Writer, + displayerType(&displayers.Action{})) + AddStringFlag(cmdDropletActionEnableBackupsWithPolicy, doctl.ArgDropletBackupPolicy, "", "", `Path to a new backup policy in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) + // AddBoolFlag(cmdDropletActionEnableBackupsWithPolicy, doctl.ArgCommandWait, "", false, "Wait for action to complete") // TODO: Add this flag when the doctl supports reading policy. + cmdDropletActionEnableBackupsWithPolicy.Example = `The following example changes backup policy on a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet-action enable-backups-with-policy 386734086 --backup-policy src/your-backup-policy.yaml` + cmdDropletActionReboot := CmdBuilder(cmd, RunDropletActionReboot, "reboot ", "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{})) @@ -303,6 +310,31 @@ func RunDropletActionChangeBackupPolicy(c *CmdConfig) error { return performAction(c, fn) } +// RunDropletActionEnableBackupsWithPolicy enables backups with a policy for a droplet. +func RunDropletActionEnableBackupsWithPolicy(c *CmdConfig) error { + fn := func(das do.DropletActionsService) (*do.Action, error) { + id, err := ContextualAtoi(c.Args[0], dropletIDResource) + if err != nil { + return nil, err + } + + policyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) + if err != nil { + return nil, err + } + + policy, err := droplets.ReadDropletBackupPolicy(os.Stdin, policyPath) + if err != nil { + return nil, err + } + + a, err := das.EnableBackupsWithPolicy(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) { diff --git a/do/droplet_actions.go b/do/droplet_actions.go index 213388b2e..76de2a459 100644 --- a/do/droplet_actions.go +++ b/do/droplet_actions.go @@ -40,6 +40,7 @@ type DropletActionsService interface { DisableBackups(int) (*Action, error) DisableBackupsByTag(string) (Actions, error) ChangeBackupPolicy(int, *godo.DropletBackupPolicyRequest) (*Action, error) + EnableBackupsWithPolicy(int, *godo.DropletBackupPolicyRequest) (*Action, error) PasswordReset(int) (*Action, error) RebuildByImageID(int, int) (*Action, error) RebuildByImageSlug(int, string) (*Action, error) @@ -182,6 +183,11 @@ func (das *dropletActionsService) ChangeBackupPolicy(id int, policy *godo.Drople return das.handleActionResponse(a, err) } +func (das *dropletActionsService) EnableBackupsWithPolicy(id int, policy *godo.DropletBackupPolicyRequest) (*Action, error) { + a, _, err := das.client.DropletActions.EnableBackupsWithPolicy(context.TODO(), id, policy) + return das.handleActionResponse(a, err) +} + func (das *dropletActionsService) PasswordReset(id int) (*Action, error) { a, _, err := das.client.DropletActions.PasswordReset(context.TODO(), id) return das.handleActionResponse(a, err) From a2d6ff7b46e44bf5b7228bd65b6936cf049c1de5 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 6 Nov 2024 14:13:57 -0500 Subject: [PATCH 08/32] add tests for EnableBackupsWithPolicy --- commands/droplet_actions_test.go | 33 +++++++++++++++++++++++++++++++- do/mocks/DropletActionService.go | 15 +++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/commands/droplet_actions_test.go b/commands/droplet_actions_test.go index d9c8481fb..5b2f1465e 100644 --- a/commands/droplet_actions_test.go +++ b/commands/droplet_actions_test.go @@ -82,7 +82,7 @@ func TestDropletActionsDisableBackups(t *testing.T) { }) } -func TestDropletChangeBackupPolicy(t *testing.T) { +func TestDropletActionsChangeBackupPolicy(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { dropletPolicy := godo.DropletBackupPolicyRequest{ Plan: "weekly", @@ -113,6 +113,37 @@ func TestDropletChangeBackupPolicy(t *testing.T) { }) } +func TestDropletActionsEnableBackupsWithPolicy(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + dropletPolicy := godo.DropletBackupPolicyRequest{ + Plan: "weekly", + Weekday: "SAT", + Hour: godo.PtrTo(0), + } + + policyFile, err := os.CreateTemp(t.TempDir(), "policy-cfg") + require.NoError(t, err) + defer policyFile.Close() + + err = json.NewEncoder(policyFile).Encode(&dropletPolicy) + require.NoError(t, err) + + policyReq := &godo.DropletBackupPolicyRequest{ + Plan: dropletPolicy.Plan, + Weekday: dropletPolicy.Weekday, + Hour: dropletPolicy.Hour, + } + + tm.dropletActions.EXPECT().EnableBackupsWithPolicy(1, policyReq).Times(1).Return(&testAction, nil) + + config.Args = append(config.Args, "1") + config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicy, policyFile.Name()) + + err = RunDropletActionEnableBackupsWithPolicy(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) diff --git a/do/mocks/DropletActionService.go b/do/mocks/DropletActionService.go index 4e6cac46b..93a51cb46 100644 --- a/do/mocks/DropletActionService.go +++ b/do/mocks/DropletActionService.go @@ -131,6 +131,21 @@ func (mr *MockDropletActionsServiceMockRecorder) EnableBackupsByTag(arg0 any) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableBackupsByTag", reflect.TypeOf((*MockDropletActionsService)(nil).EnableBackupsByTag), arg0) } +// EnableBackupsWithPolicy mocks base method. +func (m *MockDropletActionsService) EnableBackupsWithPolicy(arg0 int, arg1 *godo.DropletBackupPolicyRequest) (*do.Action, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnableBackupsWithPolicy", arg0, arg1) + ret0, _ := ret[0].(*do.Action) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EnableBackupsWithPolicy indicates an expected call of EnableBackupsWithPolicy. +func (mr *MockDropletActionsServiceMockRecorder) EnableBackupsWithPolicy(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableBackupsWithPolicy", reflect.TypeOf((*MockDropletActionsService)(nil).EnableBackupsWithPolicy), arg0, arg1) +} + // EnableIPv6 mocks base method. func (m *MockDropletActionsService) EnableIPv6(arg0 int) (*do.Action, error) { m.ctrl.T.Helper() From d9c1c1e2626db133b6e806379d175b1e76a984df Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 6 Nov 2024 14:16:59 -0500 Subject: [PATCH 09/32] add enable-backups-with-policy to droplet-actions test --- commands/droplet_actions_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/droplet_actions_test.go b/commands/droplet_actions_test.go index 5b2f1465e..f07ce1895 100644 --- a/commands/droplet_actions_test.go +++ b/commands/droplet_actions_test.go @@ -27,7 +27,7 @@ import ( func TestDropletActionCommand(t *testing.T) { cmd := DropletAction() assert.NotNil(t, cmd) - 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") + assertCommandNames(t, cmd, "change-kernel", "change-backup-policy", "enable-backups", "enable-backups-with-policy", "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) { From cc27e0ed8586b82b1833175bb9a39efc81078d9c Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 6 Nov 2024 16:35:24 -0500 Subject: [PATCH 10/32] add get droplet backup policy --- commands/commands_test.go | 18 +++++++ .../displayers/droplet_backup_policies.go | 50 +++++++++++++++++++ commands/droplets.go | 21 ++++++++ commands/droplets_test.go | 13 ++++- do/droplets.go | 15 ++++++ do/mocks/DropletsService.go | 15 ++++++ 6 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 commands/displayers/droplet_backup_policies.go diff --git a/commands/commands_test.go b/commands/commands_test.go index 0991468ec..87588b59e 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -16,6 +16,7 @@ package commands import ( "io" "testing" + "time" "github.com/digitalocean/doctl" "github.com/digitalocean/doctl/do" @@ -123,6 +124,23 @@ var ( } testSnapshotList = do.Snapshots{testSnapshot, testSnapshotSecondary} + + testDropletBackupPolicy = do.DropletBackupPolicy{ + DropletBackupPolicy: &godo.DropletBackupPolicy{ + DropletID: 123, + BackupPolicy: &godo.DropletBackupPolicyConfig{ + Plan: "weekly", + Weekday: "monday", + Hour: 1, + WindowLengthHours: 2, + RetentionPeriodDays: 3, + }, + 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)}, + }, + }, + } ) func assertCommandNames(t *testing.T, cmd *Command, expected ...string) { diff --git a/commands/displayers/droplet_backup_policies.go b/commands/displayers/droplet_backup_policies.go new file mode 100644 index 000000000..5e5fa7a5f --- /dev/null +++ b/commands/displayers/droplet_backup_policies.go @@ -0,0 +1,50 @@ +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": "Backup Enabled", + "BackupPolicyPlan": "Backup Policy Plan", "BackupPolicyWeekday": "Backup Policy Weekday", "BackupPolicyHour": "Backup Policy Hour", + "BackupPolicyWindowLengthHours": "Backup Policy Window Length Hours", "BackupPolicyRetentionPeriodDays": "Backup Policy Retention Period Days", + "NextBackupWindowStart": "Next Backup Window Start", "NextBackupWindowEnd": "Next Backup Window End", + } +} + +func (d *DropletBackupPolicy) KV() []map[string]any { + out := make([]map[string]any, 0) + for _, d := range d.DropletBackupPolicies { + m := map[string]any{ + "DropletID": d.DropletID, "BackupEnabled": d.BackupEnabled, "BackupPolicyPlan": d.BackupPolicy.Plan, + "BackupPolicyWeekday": d.BackupPolicy.Weekday, "BackupPolicyHour": d.BackupPolicy.Hour, + "BackupPolicyWindowLengthHours": d.BackupPolicy.WindowLengthHours, "BackupPolicyRetentionPeriodDays": d.BackupPolicy.RetentionPeriodDays, + "NextBackupWindowStart": d.NextBackupWindow.Start, "NextBackupWindowEnd": d.NextBackupWindow.End, + } + out = append(out, m) + } + + return out +} diff --git a/commands/droplets.go b/commands/droplets.go index 725efc83d..84e8fec05 100644 --- a/commands/droplets.go +++ b/commands/droplets.go @@ -137,6 +137,9 @@ If you do not specify a region, the Droplet is created in the default region for AddStringSliceFlag(cmdRunDropletUntag, doctl.ArgTagName, "", []string{}, "The tag name to remove from Droplet") cmdRunDropletUntag.Example = `The following example removes the tag ` + "`" + `frontend` + "`" + ` from a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet untag 386734086 --tag-name frontend` + cmdDropletGetBackupPolicy := CmdBuilder(cmd, RunDropletGetBackupPolicy, "get-backup-policy ", "Get droplet's backup policy", `Retrieves a backup policy of a Droplet.`, Writer) + cmdDropletGetBackupPolicy.Example = `The following example retrieves a backup policy for a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet get-backup-policy 386734086` + cmd.AddCommand(dropletOneClicks()) return cmd @@ -838,3 +841,21 @@ func RunDropletOneClickList(c *CmdConfig) error { return c.Display(items) } + +// RunDropletGetBackupPolicy retrieves a backup policy for a Droplet. +func RunDropletGetBackupPolicy(c *CmdConfig) error { + ds := c.Droplets() + + id, err := getDropletIDArg(c.NS, c.Args) + if err != nil { + return err + } + + policy, err := ds.GetBackupPolicy(id) + if err != nil { + return err + } + + item := &displayers.DropletBackupPolicy{DropletBackupPolicies: []do.DropletBackupPolicy{*policy}} + return c.Display(item) +} diff --git a/commands/droplets_test.go b/commands/droplets_test.go index ca78c587c..008f7e11d 100644 --- a/commands/droplets_test.go +++ b/commands/droplets_test.go @@ -55,7 +55,7 @@ var ( func TestDropletCommand(t *testing.T) { cmd := Droplet() assert.NotNil(t, cmd) - assertCommandNames(t, cmd, "1-click", "actions", "backups", "create", "delete", "get", "kernels", "list", "neighbors", "snapshots", "tag", "untag") + assertCommandNames(t, cmd, "1-click", "actions", "backups", "create", "delete", "get", "get-backup-policy", "kernels", "list", "neighbors", "snapshots", "tag", "untag") } func TestDropletActionList(t *testing.T) { @@ -658,3 +658,14 @@ func TestDropletCreateWithAgent(t *testing.T) { }) } } + +func TestDropletGetBackupPolicy(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.droplets.EXPECT().GetBackupPolicy(testDropletBackupPolicy.DropletID).Return(&testDropletBackupPolicy, nil) + + config.Args = append(config.Args, strconv.Itoa(testDropletBackupPolicy.DropletID)) + + err := RunDropletGetBackupPolicy(config) + assert.NoError(t, err) + }) +} diff --git a/do/droplets.go b/do/droplets.go index e3e325a15..9b7dac200 100644 --- a/do/droplets.go +++ b/do/droplets.go @@ -49,6 +49,10 @@ type Kernel struct { // Kernels is a slice of Kernel. type Kernels []Kernel +type DropletBackupPolicy struct { + *godo.DropletBackupPolicy +} + // DropletsService is an interface for interacting with DigitalOcean's droplet api. type DropletsService interface { List() (Droplets, error) @@ -64,6 +68,7 @@ type DropletsService interface { Backups(int) (Images, error) Actions(int) (Actions, error) Neighbors(int) (Droplets, error) + GetBackupPolicy(int) (*DropletBackupPolicy, error) } type dropletsService struct { @@ -338,3 +343,13 @@ func (ds *dropletsService) Neighbors(id int) (Droplets, error) { return droplets, nil } + +func (ds *dropletsService) GetBackupPolicy(id int) (*DropletBackupPolicy, error) { + policy, _, err := ds.client.Droplets.GetBackupPolicy(context.TODO(), id) + if err != nil { + return nil, err + } + + return &DropletBackupPolicy{policy}, nil + +} diff --git a/do/mocks/DropletsService.go b/do/mocks/DropletsService.go index 3f8162f1f..e87b2d09a 100644 --- a/do/mocks/DropletsService.go +++ b/do/mocks/DropletsService.go @@ -144,6 +144,21 @@ func (mr *MockDropletsServiceMockRecorder) Get(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDropletsService)(nil).Get), arg0) } +// GetBackupPolicy mocks base method. +func (m *MockDropletsService) GetBackupPolicy(arg0 int) (*do.DropletBackupPolicy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBackupPolicy", arg0) + ret0, _ := ret[0].(*do.DropletBackupPolicy) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBackupPolicy indicates an expected call of GetBackupPolicy. +func (mr *MockDropletsServiceMockRecorder) GetBackupPolicy(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBackupPolicy", reflect.TypeOf((*MockDropletsService)(nil).GetBackupPolicy), arg0) +} + // Kernels mocks base method. func (m *MockDropletsService) Kernels(arg0 int) (do.Kernels, error) { m.ctrl.T.Helper() From 7369111d3ac8a9dc7b3aab60c03e828ad78990e0 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 7 Nov 2024 08:47:48 -0500 Subject: [PATCH 11/32] add list droplet backup policies for all existing droplets --- commands/commands_test.go | 26 ++++++++++++++++++++++---- commands/droplets.go | 16 ++++++++++++++++ commands/droplets_test.go | 10 +++++++++- do/droplets.go | 37 +++++++++++++++++++++++++++++++++++++ do/mocks/DropletsService.go | 15 +++++++++++++++ 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/commands/commands_test.go b/commands/commands_test.go index 87588b59e..d0dc3db5a 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -130,10 +130,10 @@ var ( DropletID: 123, BackupPolicy: &godo.DropletBackupPolicyConfig{ Plan: "weekly", - Weekday: "monday", - Hour: 1, - WindowLengthHours: 2, - RetentionPeriodDays: 3, + 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)}, @@ -141,6 +141,24 @@ var ( }, }, } + + 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} ) func assertCommandNames(t *testing.T, cmd *Command, expected ...string) { diff --git a/commands/droplets.go b/commands/droplets.go index 84e8fec05..fad9c4d45 100644 --- a/commands/droplets.go +++ b/commands/droplets.go @@ -140,6 +140,9 @@ If you do not specify a region, the Droplet is created in the default region for cmdDropletGetBackupPolicy := CmdBuilder(cmd, RunDropletGetBackupPolicy, "get-backup-policy ", "Get droplet's backup policy", `Retrieves a backup policy of a Droplet.`, Writer) cmdDropletGetBackupPolicy.Example = `The following example retrieves a backup policy for a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet get-backup-policy 386734086` + cmdDropletListBackupPolicies := CmdBuilder(cmd, RunDropletListBackupPolicies, "list-backup-policies", "List backup policies for all Droplets", `List droplet backup policies for all existing Droplets.`, Writer) + cmdDropletListBackupPolicies.Example = `The following example list backup policies for all existing Droplets: doctl compute droplet list-backup-policies` + cmd.AddCommand(dropletOneClicks()) return cmd @@ -859,3 +862,16 @@ func RunDropletGetBackupPolicy(c *CmdConfig) error { item := &displayers.DropletBackupPolicy{DropletBackupPolicies: []do.DropletBackupPolicy{*policy}} return c.Display(item) } + +// RunDropletListBackupPolicies list backup policies for all existing Droplets. +func RunDropletListBackupPolicies(c *CmdConfig) error { + ds := c.Droplets() + + policies, err := ds.ListBackupPolicies() + if err != nil { + return err + } + + items := &displayers.DropletBackupPolicy{DropletBackupPolicies: policies} + return c.Display(items) +} diff --git a/commands/droplets_test.go b/commands/droplets_test.go index 008f7e11d..aceea3e71 100644 --- a/commands/droplets_test.go +++ b/commands/droplets_test.go @@ -55,7 +55,7 @@ var ( func TestDropletCommand(t *testing.T) { cmd := Droplet() assert.NotNil(t, cmd) - assertCommandNames(t, cmd, "1-click", "actions", "backups", "create", "delete", "get", "get-backup-policy", "kernels", "list", "neighbors", "snapshots", "tag", "untag") + assertCommandNames(t, cmd, "1-click", "actions", "backups", "create", "delete", "get", "get-backup-policy", "kernels", "list", "list-backup-policies", "neighbors", "snapshots", "tag", "untag") } func TestDropletActionList(t *testing.T) { @@ -669,3 +669,11 @@ func TestDropletGetBackupPolicy(t *testing.T) { assert.NoError(t, err) }) } + +func TestDropletListBackupPolicies(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.droplets.EXPECT().ListBackupPolicies().Return(testDropletBackupPolicies, nil) + err := RunDropletListBackupPolicies(config) + assert.NoError(t, err) + }) +} diff --git a/do/droplets.go b/do/droplets.go index 9b7dac200..bb0b525c4 100644 --- a/do/droplets.go +++ b/do/droplets.go @@ -15,6 +15,7 @@ package do import ( "context" + "fmt" "github.com/digitalocean/godo" "github.com/digitalocean/godo/util" @@ -49,10 +50,14 @@ type Kernel struct { // Kernels is a slice of Kernel. type Kernels []Kernel +// DropletBackupPolicy is a wrapper for godo.DropletBackupPolicy. type DropletBackupPolicy struct { *godo.DropletBackupPolicy } +// DropletBackupPolicies is a slice of DropletBackupPolicy. +type DropletBackupPolicies []DropletBackupPolicy + // DropletsService is an interface for interacting with DigitalOcean's droplet api. type DropletsService interface { List() (Droplets, error) @@ -69,6 +74,7 @@ type DropletsService interface { Actions(int) (Actions, error) Neighbors(int) (Droplets, error) GetBackupPolicy(int) (*DropletBackupPolicy, error) + ListBackupPolicies() (DropletBackupPolicies, error) } type dropletsService struct { @@ -351,5 +357,36 @@ func (ds *dropletsService) GetBackupPolicy(id int) (*DropletBackupPolicy, error) } return &DropletBackupPolicy{policy}, nil +} + +func (ds *dropletsService) ListBackupPolicies() (DropletBackupPolicies, error) { + f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) { + policies, resp, err := ds.client.Droplets.ListBackupPolicies(context.TODO(), opt) + if err != nil { + return nil, nil, err + } + + pl := make([]any, len(policies)) + i := 0 + for _, value := range policies { + pl[i] = value + i++ + } + + return pl, resp, err + } + + si, err := PaginateResp(f) + if err != nil { + return nil, err + } + list := make(DropletBackupPolicies, len(si)) + fmt.Println("si: ", si) + for i := range si { + p := si[i].(*godo.DropletBackupPolicy) + list[i] = DropletBackupPolicy{DropletBackupPolicy: p} + } + + return list, nil } diff --git a/do/mocks/DropletsService.go b/do/mocks/DropletsService.go index e87b2d09a..a65fb1769 100644 --- a/do/mocks/DropletsService.go +++ b/do/mocks/DropletsService.go @@ -189,6 +189,21 @@ func (mr *MockDropletsServiceMockRecorder) List() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockDropletsService)(nil).List)) } +// ListBackupPolicies mocks base method. +func (m *MockDropletsService) ListBackupPolicies() (do.DropletBackupPolicies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListBackupPolicies") + ret0, _ := ret[0].(do.DropletBackupPolicies) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListBackupPolicies indicates an expected call of ListBackupPolicies. +func (mr *MockDropletsServiceMockRecorder) ListBackupPolicies() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBackupPolicies", reflect.TypeOf((*MockDropletsService)(nil).ListBackupPolicies)) +} + // ListByTag mocks base method. func (m *MockDropletsService) ListByTag(arg0 string) (do.Droplets, error) { m.ctrl.T.Helper() From 040973774165b74f38a31a56cff3ddf84add189c Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 7 Nov 2024 09:51:59 -0500 Subject: [PATCH 12/32] add list supported droplet backup policies --- commands/commands_test.go | 22 ++++++++++ .../droplet_supported_backup_policies.go | 44 +++++++++++++++++++ commands/droplets.go | 16 +++++++ commands/droplets_test.go | 10 ++++- do/droplets.go | 23 ++++++++++ do/mocks/DropletsService.go | 15 +++++++ 6 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 commands/displayers/droplet_supported_backup_policies.go diff --git a/commands/commands_test.go b/commands/commands_test.go index d0dc3db5a..23ee58561 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -159,6 +159,28 @@ var ( } 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) { diff --git a/commands/displayers/droplet_supported_backup_policies.go b/commands/displayers/droplet_supported_backup_policies.go new file mode 100644 index 000000000..cd6bea59a --- /dev/null +++ b/commands/displayers/droplet_supported_backup_policies.go @@ -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 _, d := range d.DropletSupportedBackupPolicies { + m := map[string]any{ + "Name": d.Name, "PossibleWindowStarts": d.PossibleWindowStarts, "WindowLengthHours": d.WindowLengthHours, + "RetentionPeriodDays": d.RetentionPeriodDays, "PossibleDays": d.PossibleDays, + } + out = append(out, m) + } + + return out +} diff --git a/commands/droplets.go b/commands/droplets.go index fad9c4d45..4d758319d 100644 --- a/commands/droplets.go +++ b/commands/droplets.go @@ -143,6 +143,9 @@ If you do not specify a region, the Droplet is created in the default region for cmdDropletListBackupPolicies := CmdBuilder(cmd, RunDropletListBackupPolicies, "list-backup-policies", "List backup policies for all Droplets", `List droplet backup policies for all existing Droplets.`, Writer) cmdDropletListBackupPolicies.Example = `The following example list backup policies for all existing Droplets: doctl compute droplet list-backup-policies` + cmdDropletListSupportedBackupPolicies := CmdBuilder(cmd, RunDropletListSupportedBackupPolicies, "list-supported-backup-policies", "List of all supported droplet backup policies", `List of all supported droplet backup policies.`, Writer) + cmdDropletListSupportedBackupPolicies.Example = `The following example list all supported backup policies for Droplets: doctl compute droplet list-supported-backup-policies` + cmd.AddCommand(dropletOneClicks()) return cmd @@ -875,3 +878,16 @@ func RunDropletListBackupPolicies(c *CmdConfig) error { items := &displayers.DropletBackupPolicy{DropletBackupPolicies: policies} return c.Display(items) } + +// RunDropletListSupportedBackupPolicies list all supported backup policies for Droplets. +func RunDropletListSupportedBackupPolicies(c *CmdConfig) error { + ds := c.Droplets() + + policies, err := ds.ListSupportedBackupPolicies() + if err != nil { + return err + } + + items := &displayers.DropletSupportedBackupPolicy{DropletSupportedBackupPolicies: policies} + return c.Display(items) +} diff --git a/commands/droplets_test.go b/commands/droplets_test.go index aceea3e71..d0b8d8843 100644 --- a/commands/droplets_test.go +++ b/commands/droplets_test.go @@ -55,7 +55,7 @@ var ( func TestDropletCommand(t *testing.T) { cmd := Droplet() assert.NotNil(t, cmd) - assertCommandNames(t, cmd, "1-click", "actions", "backups", "create", "delete", "get", "get-backup-policy", "kernels", "list", "list-backup-policies", "neighbors", "snapshots", "tag", "untag") + assertCommandNames(t, cmd, "1-click", "actions", "backups", "create", "delete", "get", "get-backup-policy", "kernels", "list", "list-backup-policies", "list-supported-backup-policies", "neighbors", "snapshots", "tag", "untag") } func TestDropletActionList(t *testing.T) { @@ -677,3 +677,11 @@ func TestDropletListBackupPolicies(t *testing.T) { assert.NoError(t, err) }) } + +func TestDropletListSupportedBackupPolicies(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.droplets.EXPECT().ListSupportedBackupPolicies().Return(testDropletSupportedBackupPolicies, nil) + err := RunDropletListSupportedBackupPolicies(config) + assert.NoError(t, err) + }) +} diff --git a/do/droplets.go b/do/droplets.go index bb0b525c4..a162cf4e6 100644 --- a/do/droplets.go +++ b/do/droplets.go @@ -58,6 +58,14 @@ type DropletBackupPolicy struct { // DropletBackupPolicies is a slice of DropletBackupPolicy. type DropletBackupPolicies []DropletBackupPolicy +// DropletSupportedBackupPolicy is a wrapper for godo.SupportedBackupPolicy. +type DropletSupportedBackupPolicy struct { + *godo.SupportedBackupPolicy +} + +// DropletSupportedBackupPolicies is a slice of DropletSupportedBackupPolicy. +type DropletSupportedBackupPolicies []DropletSupportedBackupPolicy + // DropletsService is an interface for interacting with DigitalOcean's droplet api. type DropletsService interface { List() (Droplets, error) @@ -75,6 +83,7 @@ type DropletsService interface { Neighbors(int) (Droplets, error) GetBackupPolicy(int) (*DropletBackupPolicy, error) ListBackupPolicies() (DropletBackupPolicies, error) + ListSupportedBackupPolicies() (DropletSupportedBackupPolicies, error) } type dropletsService struct { @@ -390,3 +399,17 @@ func (ds *dropletsService) ListBackupPolicies() (DropletBackupPolicies, error) { return list, nil } + +func (ds *dropletsService) ListSupportedBackupPolicies() (DropletSupportedBackupPolicies, error) { + policies, _, err := ds.client.Droplets.ListSupportedBackupPolicies(context.TODO()) + if err != nil { + return nil, err + } + + list := make(DropletSupportedBackupPolicies, len(policies)) + for i := range policies { + list[i] = DropletSupportedBackupPolicy{SupportedBackupPolicy: policies[i]} + } + + return list, nil +} diff --git a/do/mocks/DropletsService.go b/do/mocks/DropletsService.go index a65fb1769..ca6fe87de 100644 --- a/do/mocks/DropletsService.go +++ b/do/mocks/DropletsService.go @@ -219,6 +219,21 @@ func (mr *MockDropletsServiceMockRecorder) ListByTag(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByTag", reflect.TypeOf((*MockDropletsService)(nil).ListByTag), arg0) } +// ListSupportedBackupPolicies mocks base method. +func (m *MockDropletsService) ListSupportedBackupPolicies() (do.DropletSupportedBackupPolicies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSupportedBackupPolicies") + ret0, _ := ret[0].(do.DropletSupportedBackupPolicies) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSupportedBackupPolicies indicates an expected call of ListSupportedBackupPolicies. +func (mr *MockDropletsServiceMockRecorder) ListSupportedBackupPolicies() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSupportedBackupPolicies", reflect.TypeOf((*MockDropletsService)(nil).ListSupportedBackupPolicies)) +} + // ListWithGPUs mocks base method. func (m *MockDropletsService) ListWithGPUs() (do.Droplets, error) { m.ctrl.T.Helper() From 8a8a634fc40ac8bc7a771ce714ee2a4773138009 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 7 Nov 2024 15:26:38 -0500 Subject: [PATCH 13/32] use a flag to apply a backup policy when enabling backups rather than use a separate droplet action for that --- commands/droplet_actions.go | 67 +++++++++++++++++--------------- commands/droplet_actions_test.go | 50 ++++++++++++------------ 2 files changed, 60 insertions(+), 57 deletions(-) diff --git a/commands/droplet_actions.go b/commands/droplet_actions.go index 04c99cb6a..6d6c50bfe 100644 --- a/commands/droplet_actions.go +++ b/commands/droplet_actions.go @@ -75,8 +75,9 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re cmdDropletActionEnableBackups := CmdBuilder(cmd, RunDropletActionEnableBackups, "enable-backups ", "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.ArgDropletBackupPolicy, "", "", `Path to a new backup policy in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) 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`" + `: doctl compute droplet-action enable-backups 386734086 --backup-policy src/your-backup-policy.yaml` cmdDropletActionDisableBackups := CmdBuilder(cmd, RunDropletActionDisableBackups, "disable-backups ", "Disable backups on a Droplet", `Disables backups on a Droplet. This does not delete existing backups.`, Writer, @@ -91,13 +92,6 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re // AddBoolFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgCommandWait, "", false, "Wait for action to complete") // TODO: Add this flag when the doctl supports reading policy. 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 src/your-backup-policy.yaml` - cmdDropletActionEnableBackupsWithPolicy := CmdBuilder(cmd, RunDropletActionEnableBackupsWithPolicy, - "enable-backups-with-policy ", "Enable backups with a policy on a Droplet", `Enables backups and apply a backup policy for a Droplet.`, Writer, - displayerType(&displayers.Action{})) - AddStringFlag(cmdDropletActionEnableBackupsWithPolicy, doctl.ArgDropletBackupPolicy, "", "", `Path to a new backup policy in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) - // AddBoolFlag(cmdDropletActionEnableBackupsWithPolicy, doctl.ArgCommandWait, "", false, "Wait for action to complete") // TODO: Add this flag when the doctl supports reading policy. - cmdDropletActionEnableBackupsWithPolicy.Example = `The following example changes backup policy on a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet-action enable-backups-with-policy 386734086 --backup-policy src/your-backup-policy.yaml` - cmdDropletActionReboot := CmdBuilder(cmd, RunDropletActionReboot, "reboot ", "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{})) @@ -247,27 +241,33 @@ func RunDropletActionGet(c *CmdConfig) error { return performAction(c, fn) } -// RunDropletActionEnableBackups disables backups for a droplet. -func RunDropletActionEnableBackups(c *CmdConfig) error { +// RunDropletActionEnableBackupsWithPolicy enables backups with a policy for a droplet. +func RunDropletActionEnableBackupsWithPolicy(c *CmdConfig) error { fn := func(das do.DropletActionsService) (*do.Action, error) { - err := ensureOneArg(c) + id, err := ContextualAtoi(c.Args[0], dropletIDResource) if err != nil { return nil, err } - id, err := ContextualAtoi(c.Args[0], dropletIDResource) + + policyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) + if err != nil { + return nil, err + } + + policy, err := droplets.ReadDropletBackupPolicy(os.Stdin, policyPath) if err != nil { return nil, err } - a, err := das.EnableBackups(id) + a, err := das.EnableBackupsWithPolicy(id, policy) return a, err } return performAction(c, fn) } -// RunDropletActionDisableBackups disables backups for a droplet. -func RunDropletActionDisableBackups(c *CmdConfig) error { +// RunDropletActionEnableBackups disables backups for a droplet. +func RunDropletActionEnableBackups(c *CmdConfig) error { fn := func(das do.DropletActionsService) (*do.Action, error) { err := ensureOneArg(c) if err != nil { @@ -278,40 +278,45 @@ func RunDropletActionDisableBackups(c *CmdConfig) error { return nil, err } - a, err := das.DisableBackups(id) - return a, err + policyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) + if err != nil { + return nil, err + } + if policyPath != "" { + policy, err := droplets.ReadDropletBackupPolicy(os.Stdin, policyPath) + if err != nil { + return nil, err + } + return das.EnableBackupsWithPolicy(id, policy) + } else { + return das.EnableBackups(id) + } } return performAction(c, fn) } -// RunDropletActionChangeBackupPolicy changes backup policy for a droplet. -func RunDropletActionChangeBackupPolicy(c *CmdConfig) error { +// RunDropletActionDisableBackups disables backups for a droplet. +func RunDropletActionDisableBackups(c *CmdConfig) error { fn := func(das do.DropletActionsService) (*do.Action, error) { - id, err := ContextualAtoi(c.Args[0], dropletIDResource) - if err != nil { - return nil, err - } - - policyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) + err := ensureOneArg(c) if err != nil { return nil, err } - - policy, err := droplets.ReadDropletBackupPolicy(os.Stdin, policyPath) + id, err := ContextualAtoi(c.Args[0], dropletIDResource) if err != nil { return nil, err } - a, err := das.ChangeBackupPolicy(id, policy) + a, err := das.DisableBackups(id) return a, err } return performAction(c, fn) } -// RunDropletActionEnableBackupsWithPolicy enables backups with a policy for a droplet. -func RunDropletActionEnableBackupsWithPolicy(c *CmdConfig) error { +// RunDropletActionChangeBackupPolicy changes backup policy for a droplet. +func RunDropletActionChangeBackupPolicy(c *CmdConfig) error { fn := func(das do.DropletActionsService) (*do.Action, error) { id, err := ContextualAtoi(c.Args[0], dropletIDResource) if err != nil { @@ -328,7 +333,7 @@ func RunDropletActionEnableBackupsWithPolicy(c *CmdConfig) error { return nil, err } - a, err := das.EnableBackupsWithPolicy(id, policy) + a, err := das.ChangeBackupPolicy(id, policy) return a, err } diff --git a/commands/droplet_actions_test.go b/commands/droplet_actions_test.go index f07ce1895..611a9fca7 100644 --- a/commands/droplet_actions_test.go +++ b/commands/droplet_actions_test.go @@ -27,7 +27,7 @@ import ( func TestDropletActionCommand(t *testing.T) { cmd := DropletAction() assert.NotNil(t, cmd) - assertCommandNames(t, cmd, "change-kernel", "change-backup-policy", "enable-backups", "enable-backups-with-policy", "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) { @@ -63,26 +63,7 @@ func TestDropletActionsEnableBackups(t *testing.T) { err := RunDropletActionEnableBackups(config) assert.EqualError(t, err, `expected to be a positive integer, got "my-test-id"`) }) -} - -func TestDropletActionsDisableBackups(t *testing.T) { - withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - tm.dropletActions.EXPECT().DisableBackups(1).Return(&testAction, nil) - - config.Args = append(config.Args, "1") - - err := RunDropletActionDisableBackups(config) - assert.NoError(t, err) - }) - withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - config.Args = append(config.Args, "my-test-id") - - err := RunDropletActionDisableBackups(config) - assert.EqualError(t, err, `expected to be a positive integer, got "my-test-id"`) - }) -} - -func TestDropletActionsChangeBackupPolicy(t *testing.T) { + // Enable backups with a backup policy applied. withTestClient(t, func(config *CmdConfig, tm *tcMocks) { dropletPolicy := godo.DropletBackupPolicyRequest{ Plan: "weekly", @@ -103,17 +84,34 @@ func TestDropletActionsChangeBackupPolicy(t *testing.T) { Hour: dropletPolicy.Hour, } - tm.dropletActions.EXPECT().ChangeBackupPolicy(1, policyReq).Times(1).Return(&testAction, nil) + tm.dropletActions.EXPECT().EnableBackupsWithPolicy(1, policyReq).Times(1).Return(&testAction, nil) config.Args = append(config.Args, "1") config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicy, policyFile.Name()) - err = RunDropletActionChangeBackupPolicy(config) + err = RunDropletActionEnableBackups(config) require.NoError(t, err) }) } -func TestDropletActionsEnableBackupsWithPolicy(t *testing.T) { +func TestDropletActionsDisableBackups(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.dropletActions.EXPECT().DisableBackups(1).Return(&testAction, nil) + + config.Args = append(config.Args, "1") + + err := RunDropletActionDisableBackups(config) + assert.NoError(t, err) + }) + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + config.Args = append(config.Args, "my-test-id") + + err := RunDropletActionDisableBackups(config) + assert.EqualError(t, err, `expected to be a positive integer, got "my-test-id"`) + }) +} + +func TestDropletActionsChangeBackupPolicy(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { dropletPolicy := godo.DropletBackupPolicyRequest{ Plan: "weekly", @@ -134,12 +132,12 @@ func TestDropletActionsEnableBackupsWithPolicy(t *testing.T) { Hour: dropletPolicy.Hour, } - tm.dropletActions.EXPECT().EnableBackupsWithPolicy(1, policyReq).Times(1).Return(&testAction, nil) + tm.dropletActions.EXPECT().ChangeBackupPolicy(1, policyReq).Times(1).Return(&testAction, nil) config.Args = append(config.Args, "1") config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicy, policyFile.Name()) - err = RunDropletActionEnableBackupsWithPolicy(config) + err = RunDropletActionChangeBackupPolicy(config) require.NoError(t, err) }) } From 93be1fb12b517f9dbaf9bfcccb76f789231ed38b Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 7 Nov 2024 15:42:04 -0500 Subject: [PATCH 14/32] add a wait flag for a droplet change backup policy --- commands/droplet_actions.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/commands/droplet_actions.go b/commands/droplet_actions.go index 6d6c50bfe..3193fd0b1 100644 --- a/commands/droplet_actions.go +++ b/commands/droplet_actions.go @@ -77,7 +77,7 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re displayerType(&displayers.Action{})) AddStringFlag(cmdDropletActionEnableBackups, doctl.ArgDropletBackupPolicy, "", "", `Path to a new backup policy in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) AddBoolFlag(cmdDropletActionEnableBackups, doctl.ArgCommandWait, "", false, "Wait for action to complete") - cmdDropletActionEnableBackups.Example = `The following example enables backups on a Droplet with the ID ` + "`" + `386734086` + " with a backup policy`" + `: doctl compute droplet-action enable-backups 386734086 --backup-policy src/your-backup-policy.yaml` + 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 src/your-backup-policy.yaml` cmdDropletActionDisableBackups := CmdBuilder(cmd, RunDropletActionDisableBackups, "disable-backups ", "Disable backups on a Droplet", `Disables backups on a Droplet. This does not delete existing backups.`, Writer, @@ -89,7 +89,7 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re "change-backup-policy ", "Change backup policy on a Droplet", `Changes backup policy for a Droplet with enabled backups.`, Writer, displayerType(&displayers.Action{})) AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicy, "", "", `Path to a new backup policy in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) - // AddBoolFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgCommandWait, "", false, "Wait for action to complete") // TODO: Add this flag when the doctl supports reading policy. + 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 src/your-backup-policy.yaml` cmdDropletActionReboot := CmdBuilder(cmd, RunDropletActionReboot, @@ -318,6 +318,11 @@ func RunDropletActionDisableBackups(c *CmdConfig) error { // 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 From bc4ede23d4ca84d1ce6e2cd3361c0cbb095f24c7 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 13 Nov 2024 08:09:24 -0500 Subject: [PATCH 15/32] renaming to clarify instance we refer in a loop --- commands/displayers/droplet_backup_policies.go | 10 +++++----- .../displayers/droplet_supported_backup_policies.go | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/commands/displayers/droplet_backup_policies.go b/commands/displayers/droplet_backup_policies.go index 5e5fa7a5f..fa78b589d 100644 --- a/commands/displayers/droplet_backup_policies.go +++ b/commands/displayers/droplet_backup_policies.go @@ -36,12 +36,12 @@ func (d *DropletBackupPolicy) ColMap() map[string]string { func (d *DropletBackupPolicy) KV() []map[string]any { out := make([]map[string]any, 0) - for _, d := range d.DropletBackupPolicies { + for _, policy := range d.DropletBackupPolicies { m := map[string]any{ - "DropletID": d.DropletID, "BackupEnabled": d.BackupEnabled, "BackupPolicyPlan": d.BackupPolicy.Plan, - "BackupPolicyWeekday": d.BackupPolicy.Weekday, "BackupPolicyHour": d.BackupPolicy.Hour, - "BackupPolicyWindowLengthHours": d.BackupPolicy.WindowLengthHours, "BackupPolicyRetentionPeriodDays": d.BackupPolicy.RetentionPeriodDays, - "NextBackupWindowStart": d.NextBackupWindow.Start, "NextBackupWindowEnd": d.NextBackupWindow.End, + "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) } diff --git a/commands/displayers/droplet_supported_backup_policies.go b/commands/displayers/droplet_supported_backup_policies.go index cd6bea59a..c617047b0 100644 --- a/commands/displayers/droplet_supported_backup_policies.go +++ b/commands/displayers/droplet_supported_backup_policies.go @@ -32,10 +32,10 @@ func (d *DropletSupportedBackupPolicy) ColMap() map[string]string { func (d *DropletSupportedBackupPolicy) KV() []map[string]any { out := make([]map[string]any, 0) - for _, d := range d.DropletSupportedBackupPolicies { + for _, supported := range d.DropletSupportedBackupPolicies { m := map[string]any{ - "Name": d.Name, "PossibleWindowStarts": d.PossibleWindowStarts, "WindowLengthHours": d.WindowLengthHours, - "RetentionPeriodDays": d.RetentionPeriodDays, "PossibleDays": d.PossibleDays, + "Name": supported.Name, "PossibleWindowStarts": supported.PossibleWindowStarts, "WindowLengthHours": supported.WindowLengthHours, + "RetentionPeriodDays": supported.RetentionPeriodDays, "PossibleDays": supported.PossibleDays, } out = append(out, m) } From 3b4e2da6b2cb6688c1f26ae307194eca3c8e9f70 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 13 Nov 2024 08:24:31 -0500 Subject: [PATCH 16/32] reduce naming for get backup policy --- commands/displayers/droplet_backup_policies.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/commands/displayers/droplet_backup_policies.go b/commands/displayers/droplet_backup_policies.go index fa78b589d..90b6589c0 100644 --- a/commands/displayers/droplet_backup_policies.go +++ b/commands/displayers/droplet_backup_policies.go @@ -27,10 +27,10 @@ func (d *DropletBackupPolicy) Cols() []string { func (d *DropletBackupPolicy) ColMap() map[string]string { return map[string]string{ - "DropletID": "Droplet ID", "BackupEnabled": "Backup Enabled", - "BackupPolicyPlan": "Backup Policy Plan", "BackupPolicyWeekday": "Backup Policy Weekday", "BackupPolicyHour": "Backup Policy Hour", - "BackupPolicyWindowLengthHours": "Backup Policy Window Length Hours", "BackupPolicyRetentionPeriodDays": "Backup Policy Retention Period Days", - "NextBackupWindowStart": "Next Backup Window Start", "NextBackupWindowEnd": "Next Backup Window End", + "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", } } From b23966bb9f42959eb697de7ec3922c10c586c91e Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 13 Nov 2024 09:07:34 -0500 Subject: [PATCH 17/32] fix integration tests making backup policy optional in droplet actions enable backups --- commands/droplet_actions.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/commands/droplet_actions.go b/commands/droplet_actions.go index 3193fd0b1..8c3c11572 100644 --- a/commands/droplet_actions.go +++ b/commands/droplet_actions.go @@ -279,18 +279,16 @@ func RunDropletActionEnableBackups(c *CmdConfig) error { } policyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) - if err != nil { - return nil, err - } - if policyPath != "" { + if err == nil && policyPath != "" { policy, err := droplets.ReadDropletBackupPolicy(os.Stdin, policyPath) if err != nil { return nil, err } + return das.EnableBackupsWithPolicy(id, policy) - } else { - return das.EnableBackups(id) } + + return das.EnableBackups(id) } return performAction(c, fn) From 10aabd3e57cd915ab4770bc35e4c607d8d381b7f Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 13 Nov 2024 10:11:10 -0500 Subject: [PATCH 18/32] group droplet backup-policies read commands under backup-policies sub command. --- commands/droplets.go | 32 +++++++++++++++++++++++--------- commands/droplets_test.go | 2 +- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/commands/droplets.go b/commands/droplets.go index 4d758319d..79f6a4ef6 100644 --- a/commands/droplets.go +++ b/commands/droplets.go @@ -137,16 +137,8 @@ If you do not specify a region, the Droplet is created in the default region for AddStringSliceFlag(cmdRunDropletUntag, doctl.ArgTagName, "", []string{}, "The tag name to remove from Droplet") cmdRunDropletUntag.Example = `The following example removes the tag ` + "`" + `frontend` + "`" + ` from a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet untag 386734086 --tag-name frontend` - cmdDropletGetBackupPolicy := CmdBuilder(cmd, RunDropletGetBackupPolicy, "get-backup-policy ", "Get droplet's backup policy", `Retrieves a backup policy of a Droplet.`, Writer) - cmdDropletGetBackupPolicy.Example = `The following example retrieves a backup policy for a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet get-backup-policy 386734086` - - cmdDropletListBackupPolicies := CmdBuilder(cmd, RunDropletListBackupPolicies, "list-backup-policies", "List backup policies for all Droplets", `List droplet backup policies for all existing Droplets.`, Writer) - cmdDropletListBackupPolicies.Example = `The following example list backup policies for all existing Droplets: doctl compute droplet list-backup-policies` - - cmdDropletListSupportedBackupPolicies := CmdBuilder(cmd, RunDropletListSupportedBackupPolicies, "list-supported-backup-policies", "List of all supported droplet backup policies", `List of all supported droplet backup policies.`, Writer) - cmdDropletListSupportedBackupPolicies.Example = `The following example list all supported backup policies for Droplets: doctl compute droplet list-supported-backup-policies` - cmd.AddCommand(dropletOneClicks()) + cmd.AddCommand(dropletBackupPolicies()) return cmd } @@ -816,6 +808,28 @@ func buildDropletSummary(ds do.DropletsService) (*dropletSummary, error) { return &sum, nil } +// dropletBackupPolicies creates the backup-policies command subtree. +func dropletBackupPolicies() *Command { + cmd := &Command{ + Command: &cobra.Command{ + Use: "backup-policies", + Short: "Display commands for Droplet's backup policies.", + Long: "The commands under `doctl compute droplet backup-policies` are for displaying the commands for Droplet's backup policies.", + }, + } + + cmdDropletGetBackupPolicy := CmdBuilder(cmd, RunDropletGetBackupPolicy, "get ", "Get droplet's backup policy", `Retrieves a backup policy of a Droplet.`, Writer) + cmdDropletGetBackupPolicy.Example = `The following example retrieves a backup policy for a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet backup-policies get 386734086` + + cmdDropletListBackupPolicies := CmdBuilder(cmd, RunDropletListBackupPolicies, "list", "List backup policies for all Droplets", `List droplet backup policies for all existing Droplets.`, Writer, aliasOpt("ls")) + cmdDropletListBackupPolicies.Example = `The following example list backup policies for all existing Droplets: doctl compute droplet backup-policies list` + + cmdDropletListSupportedBackupPolicies := CmdBuilder(cmd, RunDropletListSupportedBackupPolicies, "list-supported", "List of all supported droplet backup policies", `List of all supported droplet backup policies.`, Writer) + cmdDropletListSupportedBackupPolicies.Example = `The following example list all supported backup policies for Droplets: doctl compute droplet backup-policies list-supported` + + return cmd +} + // kubernetesOneClicks creates the 1-click command. func dropletOneClicks() *Command { cmd := &Command{ diff --git a/commands/droplets_test.go b/commands/droplets_test.go index d0b8d8843..98bac1cb4 100644 --- a/commands/droplets_test.go +++ b/commands/droplets_test.go @@ -55,7 +55,7 @@ var ( func TestDropletCommand(t *testing.T) { cmd := Droplet() assert.NotNil(t, cmd) - assertCommandNames(t, cmd, "1-click", "actions", "backups", "create", "delete", "get", "get-backup-policy", "kernels", "list", "list-backup-policies", "list-supported-backup-policies", "neighbors", "snapshots", "tag", "untag") + assertCommandNames(t, cmd, "1-click", "actions", "backups", "backup-policies", "create", "delete", "get", "kernels", "list", "neighbors", "snapshots", "tag", "untag") } func TestDropletActionList(t *testing.T) { From b796bb581a1a3a6ed97f07ae038beccaebce3abc Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 13 Nov 2024 10:37:45 -0500 Subject: [PATCH 19/32] protect against panics on list for Droplets that do not have backups enabled --- commands/displayers/droplet_backup_policies.go | 15 +++++++++------ do/droplets.go | 2 -- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/commands/displayers/droplet_backup_policies.go b/commands/displayers/droplet_backup_policies.go index 90b6589c0..366f77add 100644 --- a/commands/displayers/droplet_backup_policies.go +++ b/commands/displayers/droplet_backup_policies.go @@ -37,13 +37,16 @@ func (d *DropletBackupPolicy) ColMap() map[string]string { func (d *DropletBackupPolicy) KV() []map[string]any { out := make([]map[string]any, 0) for _, policy := range d.DropletBackupPolicies { - 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, + 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) } - out = append(out, m) } return out diff --git a/do/droplets.go b/do/droplets.go index a162cf4e6..999364a48 100644 --- a/do/droplets.go +++ b/do/droplets.go @@ -15,7 +15,6 @@ package do import ( "context" - "fmt" "github.com/digitalocean/godo" "github.com/digitalocean/godo/util" @@ -391,7 +390,6 @@ func (ds *dropletsService) ListBackupPolicies() (DropletBackupPolicies, error) { } list := make(DropletBackupPolicies, len(si)) - fmt.Println("si: ", si) for i := range si { p := si[i].(*godo.DropletBackupPolicy) list[i] = DropletBackupPolicy{DropletBackupPolicy: p} From 4f6dfbde3d120dd11df6311f8ab1ce978a98ecec Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 13 Nov 2024 15:10:16 -0500 Subject: [PATCH 20/32] pass droplet backup policies with the flags instead of a config file --- args.go | 8 ++- commands/droplet_actions.go | 88 +++++++++++++++--------------- commands/droplet_actions_test.go | 48 ++++------------ commands/droplets.go | 18 ++---- commands/droplets_test.go | 15 ++--- internal/droplets/droplets.go | 58 -------------------- internal/droplets/droplets_test.go | 1 - 7 files changed, 69 insertions(+), 167 deletions(-) delete mode 100644 internal/droplets/droplets.go delete mode 100644 internal/droplets/droplets_test.go diff --git a/args.go b/args.go index b8d6e5a15..36e5c75ba 100644 --- a/args.go +++ b/args.go @@ -150,8 +150,12 @@ const ( ArgResourceType = "resource" // ArgBackups is an enable backups argument. ArgBackups = "enable-backups" - // ArgDropletBackupPolicy is a path to a droplet backup policy. - ArgDropletBackupPolicy = "backup-policy" + // 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. diff --git a/commands/droplet_actions.go b/commands/droplet_actions.go index 8c3c11572..abbd5c286 100644 --- a/commands/droplet_actions.go +++ b/commands/droplet_actions.go @@ -14,12 +14,10 @@ limitations under the License. package commands import ( - "os" - "github.com/digitalocean/doctl" "github.com/digitalocean/doctl/commands/displayers" "github.com/digitalocean/doctl/do" - "github.com/digitalocean/doctl/internal/droplets" + "github.com/digitalocean/godo" "github.com/spf13/cobra" ) @@ -75,9 +73,11 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re cmdDropletActionEnableBackups := CmdBuilder(cmd, RunDropletActionEnableBackups, "enable-backups ", "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.ArgDropletBackupPolicy, "", "", `Path to a new backup policy in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) + AddStringFlag(cmdDropletActionEnableBackups, doctl.ArgDropletBackupPolicyPlan, "", "daily", `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` + "` with a backup policy flag" + `: doctl compute droplet-action enable-backups 386734086 --backup-policy src/your-backup-policy.yaml` + 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 ", "Disable backups on a Droplet", `Disables backups on a Droplet. This does not delete existing backups.`, Writer, @@ -88,9 +88,11 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re cmdDropletActionChangeBackupPolicy := CmdBuilder(cmd, RunDropletActionChangeBackupPolicy, "change-backup-policy ", "Change backup policy on a Droplet", `Changes backup policy for a Droplet with enabled backups.`, Writer, displayerType(&displayers.Action{})) - AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicy, "", "", `Path to a new backup policy in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) + AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyPlan, "", "daily", `Backup policy frequency plan.`, requiredOpt()) + AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyWeekday, "", "", `Backup policy weekday.`) + AddIntFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyHour, "", 0, `Backup policy hour.`, requiredOpt()) 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 src/your-backup-policy.yaml` + 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 ", "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, @@ -241,31 +243,6 @@ func RunDropletActionGet(c *CmdConfig) error { return performAction(c, fn) } -// RunDropletActionEnableBackupsWithPolicy enables backups with a policy for a droplet. -func RunDropletActionEnableBackupsWithPolicy(c *CmdConfig) error { - fn := func(das do.DropletActionsService) (*do.Action, error) { - id, err := ContextualAtoi(c.Args[0], dropletIDResource) - if err != nil { - return nil, err - } - - policyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) - if err != nil { - return nil, err - } - - policy, err := droplets.ReadDropletBackupPolicy(os.Stdin, policyPath) - if err != nil { - return nil, err - } - - a, err := das.EnableBackupsWithPolicy(id, policy) - return a, err - } - - return performAction(c, fn) -} - // RunDropletActionEnableBackups disables backups for a droplet. func RunDropletActionEnableBackups(c *CmdConfig) error { fn := func(das do.DropletActionsService) (*do.Action, error) { @@ -278,13 +255,8 @@ func RunDropletActionEnableBackups(c *CmdConfig) error { return nil, err } - policyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) - if err == nil && policyPath != "" { - policy, err := droplets.ReadDropletBackupPolicy(os.Stdin, policyPath) - if err != nil { - return nil, err - } - + policy, err := readDropletBackupPolicy(c) + if err == nil && policy != nil { return das.EnableBackupsWithPolicy(id, policy) } @@ -313,6 +285,37 @@ 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 + } + + 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) { @@ -326,12 +329,7 @@ func RunDropletActionChangeBackupPolicy(c *CmdConfig) error { return nil, err } - policyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) - if err != nil { - return nil, err - } - - policy, err := droplets.ReadDropletBackupPolicy(os.Stdin, policyPath) + policy, err := readDropletBackupPolicy(c) if err != nil { return nil, err } diff --git a/commands/droplet_actions_test.go b/commands/droplet_actions_test.go index 611a9fca7..ceba2dec8 100644 --- a/commands/droplet_actions_test.go +++ b/commands/droplet_actions_test.go @@ -14,8 +14,6 @@ limitations under the License. package commands import ( - "encoding/json" - "os" "testing" "github.com/digitalocean/doctl" @@ -65,31 +63,20 @@ func TestDropletActionsEnableBackups(t *testing.T) { }) // Enable backups with a backup policy applied. withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - dropletPolicy := godo.DropletBackupPolicyRequest{ + policy := &godo.DropletBackupPolicyRequest{ Plan: "weekly", Weekday: "SAT", Hour: godo.PtrTo(0), } - policyFile, err := os.CreateTemp(t.TempDir(), "policy-cfg") - require.NoError(t, err) - defer policyFile.Close() - - err = json.NewEncoder(policyFile).Encode(&dropletPolicy) - require.NoError(t, err) - - policyReq := &godo.DropletBackupPolicyRequest{ - Plan: dropletPolicy.Plan, - Weekday: dropletPolicy.Weekday, - Hour: dropletPolicy.Hour, - } - - tm.dropletActions.EXPECT().EnableBackupsWithPolicy(1, policyReq).Times(1).Return(&testAction, nil) + tm.dropletActions.EXPECT().EnableBackupsWithPolicy(1, policy).Times(1).Return(&testAction, nil) config.Args = append(config.Args, "1") - config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicy, policyFile.Name()) + 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) + err := RunDropletActionEnableBackups(config) require.NoError(t, err) }) } @@ -113,31 +100,20 @@ func TestDropletActionsDisableBackups(t *testing.T) { func TestDropletActionsChangeBackupPolicy(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - dropletPolicy := godo.DropletBackupPolicyRequest{ + policy := &godo.DropletBackupPolicyRequest{ Plan: "weekly", Weekday: "SAT", Hour: godo.PtrTo(0), } - policyFile, err := os.CreateTemp(t.TempDir(), "policy-cfg") - require.NoError(t, err) - defer policyFile.Close() - - err = json.NewEncoder(policyFile).Encode(&dropletPolicy) - require.NoError(t, err) - - policyReq := &godo.DropletBackupPolicyRequest{ - Plan: dropletPolicy.Plan, - Weekday: dropletPolicy.Weekday, - Hour: dropletPolicy.Hour, - } - - tm.dropletActions.EXPECT().ChangeBackupPolicy(1, policyReq).Times(1).Return(&testAction, nil) + tm.dropletActions.EXPECT().ChangeBackupPolicy(1, policy).Times(1).Return(&testAction, nil) config.Args = append(config.Args, "1") - config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicy, policyFile.Name()) + 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) + err := RunDropletActionChangeBackupPolicy(config) require.NoError(t, err) }) } diff --git a/commands/droplets.go b/commands/droplets.go index 79f6a4ef6..f314b1b64 100644 --- a/commands/droplets.go +++ b/commands/droplets.go @@ -25,7 +25,6 @@ import ( "github.com/digitalocean/doctl" "github.com/digitalocean/doctl/commands/displayers" "github.com/digitalocean/doctl/do" - "github.com/digitalocean/doctl/internal/droplets" "github.com/digitalocean/godo" "github.com/gobwas/glob" "github.com/spf13/cobra" @@ -82,9 +81,9 @@ If you do not specify a region, the Droplet is created in the default region for AddStringFlag(cmdDropletCreate, doctl.ArgSizeSlug, "", "", "A `slug` indicating the Droplet's number of vCPUs, RAM, and disk size. For example, `s-1vcpu-1gb` specifies a Droplet with one vCPU and 1 GiB of RAM. The disk size is defined by the slug's plan. Run `doctl compute size list` for a list of valid size slugs and their disk sizes.", requiredOpt()) AddBoolFlag(cmdDropletCreate, doctl.ArgBackups, "", false, "Enables backups for the Droplet. By default, backups are created on a daily basis.") - AddStringFlag(cmdDropletCreate, doctl.ArgDropletBackupPolicy, "", "", - `Path to a droplet backup policy in JSON or YAML format. Set to "-" to read from stdin. Backups must be enabled for the Droplet to apply the backup policy. - Example: doctl compute droplet create my-droplet --size s-1vcpu-1gb --image ubuntu-20-04-x64 --region nyc1 --enable-backups --backup-policy path-to-policy/policy-cfg.yaml`) + AddStringFlag(cmdDropletCreate, doctl.ArgDropletBackupPolicyPlan, "", "daily", `Backup policy frequency plan.`) + AddStringFlag(cmdDropletCreate, doctl.ArgDropletBackupPolicyWeekday, "", "", `Backup policy weekday.`) + AddIntFlag(cmdDropletCreate, doctl.ArgDropletBackupPolicyHour, "", 0, `Backup policy hour.`) AddBoolFlag(cmdDropletCreate, doctl.ArgIPv6, "", false, "Enables IPv6 support and assigns an IPv6 address to the Droplet") AddBoolFlag(cmdDropletCreate, doctl.ArgPrivateNetworking, "", false, "Enables private networking for the Droplet by provisioning it inside of your account's default VPC for the region") AddBoolFlag(cmdDropletCreate, doctl.ArgMonitoring, "", false, "Installs the DigitalOcean agent for additional monitoring") @@ -202,20 +201,11 @@ func RunDropletCreate(c *CmdConfig) error { return err } - backupPolicyPath, err := c.Doit.GetString(c.NS, doctl.ArgDropletBackupPolicy) + backupPolicy, err := readDropletBackupPolicy(c) if err != nil { return err } - var backupPolicy *godo.DropletBackupPolicyRequest - - if backupPolicyPath != "" { - backupPolicy, err = droplets.ReadDropletBackupPolicy(os.Stdin, backupPolicyPath) - if err != nil { - return err - } - } - ipv6, err := c.Doit.GetBool(c.NS, doctl.ArgIPv6) if err != nil { return err diff --git a/commands/droplets_test.go b/commands/droplets_test.go index 98bac1cb4..aadfb9714 100644 --- a/commands/droplets_test.go +++ b/commands/droplets_test.go @@ -15,7 +15,6 @@ package commands import ( "bytes" - "encoding/json" "os" "strconv" "testing" @@ -24,7 +23,6 @@ import ( "github.com/digitalocean/doctl/do" "github.com/digitalocean/godo" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) var ( @@ -148,13 +146,6 @@ func TestDropletCreateWithBackupPolicy(t *testing.T) { BackupPolicy: &dropletPolicy, } - policyFile, err := os.CreateTemp(t.TempDir(), "policy-cfg") - require.NoError(t, err) - defer policyFile.Close() - - err = json.NewEncoder(policyFile).Encode(&dropletPolicy) - require.NoError(t, err) - tm.droplets.EXPECT().Create(dcr, false).Return(&testDroplet, nil) config.Args = append(config.Args, "droplet") @@ -167,9 +158,11 @@ func TestDropletCreateWithBackupPolicy(t *testing.T) { config.Doit.Set(config.NS, doctl.ArgVolumeList, []string{"test-volume", volumeUUID}) config.Doit.Set(config.NS, doctl.ArgTagNames, []string{"one", "two"}) config.Doit.Set(config.NS, doctl.ArgBackups, true) - config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicy, policyFile.Name()) + config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyPlan, dropletPolicy.Plan) + config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyWeekday, dropletPolicy.Weekday) + config.Doit.Set(config.NS, doctl.ArgDropletBackupPolicyHour, dropletPolicy.Hour) - err = RunDropletCreate(config) + err := RunDropletCreate(config) assert.NoError(t, err) }) } diff --git a/internal/droplets/droplets.go b/internal/droplets/droplets.go deleted file mode 100644 index cb5cea6ae..000000000 --- a/internal/droplets/droplets.go +++ /dev/null @@ -1,58 +0,0 @@ -package droplets - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "os" - - "github.com/digitalocean/godo" - "sigs.k8s.io/yaml" -) - -func ReadDropletBackupPolicy(stdin io.Reader, path string) (*godo.DropletBackupPolicyRequest, error) { - var policy io.Reader - if path == "-" && stdin != nil { - policy = stdin - } else { - specFile, err := os.Open(path) // guardrails-disable-line - if err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("opening droplet backup policy: %s does not exist", path) - } - return nil, fmt.Errorf("opening droplet backup policy: %w", err) - } - defer specFile.Close() - policy = specFile - } - - byt, err := io.ReadAll(policy) - if err != nil { - return nil, fmt.Errorf("reading droplet backup policy: %w", err) - } - - s, err := ParseDropletBackupPolicy(byt) - if err != nil { - return nil, fmt.Errorf("parsing droplet backup policy: %w", err) - } - - return s, nil -} - -func ParseDropletBackupPolicy(spec []byte) (*godo.DropletBackupPolicyRequest, error) { - jsonSpec, err := yaml.YAMLToJSON(spec) - if err != nil { - return nil, err - } - - dec := json.NewDecoder(bytes.NewReader(jsonSpec)) - dec.DisallowUnknownFields() - - var policy godo.DropletBackupPolicyRequest - if err := dec.Decode(&policy); err != nil { - return nil, err - } - - return &policy, nil -} diff --git a/internal/droplets/droplets_test.go b/internal/droplets/droplets_test.go deleted file mode 100644 index 2eadb6532..000000000 --- a/internal/droplets/droplets_test.go +++ /dev/null @@ -1 +0,0 @@ -package droplets From 841b2f2dfadf981c01cdd98cdb28af52c896cdf3 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 13 Nov 2024 16:03:06 -0500 Subject: [PATCH 21/32] adding an empty backup policy to integration droplet action test --- integration/compute_droplet_action_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/compute_droplet_action_test.go b/integration/compute_droplet_action_test.go index b9f54c9c1..14bf73ce9 100644 --- a/integration/compute_droplet_action_test.go +++ b/integration/compute_droplet_action_test.go @@ -44,7 +44,7 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. "/v2/droplets/247/actions": {method: http.MethodPost, body: `{"type":"enable_ipv6"}`}, "/v2/droplets/45/actions": {method: http.MethodPost, body: `{"type":"power_on"}`}, "/v2/droplets/1111/actions": {method: http.MethodPost, body: `{"kernel":7777,"type":"change_kernel"}`}, - "/v2/droplets/65/actions": {method: http.MethodPost, body: `{"type":"enable_backups"}`}, + "/v2/droplets/65/actions": {method: http.MethodPost, body: `{{"hour":0, "plan":"daily", "weekday":""},"type":"enable_backups"}`}, "/v2/droplets/4743/actions": {method: http.MethodPost, body: `{"image":9999,"type":"rebuild"}`}, "/v2/droplets/884/actions": {method: http.MethodPost, body: `{"disk":true,"size":"bigger","type":"resize"}`}, "/v2/droplets/789/actions/954": {method: http.MethodGet, body: `{}`}, From 8f9e0e0a9aad6e3af3fd1197252bc927116596b8 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Wed, 13 Nov 2024 16:15:28 -0500 Subject: [PATCH 22/32] add a key to the test --- integration/compute_droplet_action_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/compute_droplet_action_test.go b/integration/compute_droplet_action_test.go index 14bf73ce9..6d9cc2937 100644 --- a/integration/compute_droplet_action_test.go +++ b/integration/compute_droplet_action_test.go @@ -44,7 +44,7 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. "/v2/droplets/247/actions": {method: http.MethodPost, body: `{"type":"enable_ipv6"}`}, "/v2/droplets/45/actions": {method: http.MethodPost, body: `{"type":"power_on"}`}, "/v2/droplets/1111/actions": {method: http.MethodPost, body: `{"kernel":7777,"type":"change_kernel"}`}, - "/v2/droplets/65/actions": {method: http.MethodPost, body: `{{"hour":0, "plan":"daily", "weekday":""},"type":"enable_backups"}`}, + "/v2/droplets/65/actions": {method: http.MethodPost, body: `{"backup_policy": {"hour":0, "plan":"daily", "weekday":""},"type":"enable_backups"}`}, "/v2/droplets/4743/actions": {method: http.MethodPost, body: `{"image":9999,"type":"rebuild"}`}, "/v2/droplets/884/actions": {method: http.MethodPost, body: `{"disk":true,"size":"bigger","type":"resize"}`}, "/v2/droplets/789/actions/954": {method: http.MethodGet, body: `{}`}, From d5a31a5ec1a2cc688693eb0740e6ac5cc4c9455b Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 14 Nov 2024 12:08:08 -0500 Subject: [PATCH 23/32] add a check for a default backup policy when it's missing on backup enabling; revert changes in integration tests --- commands/droplet_actions.go | 9 ++++++++- integration/compute_droplet_action_test.go | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/commands/droplet_actions.go b/commands/droplet_actions.go index abbd5c286..262528a61 100644 --- a/commands/droplet_actions.go +++ b/commands/droplet_actions.go @@ -14,6 +14,8 @@ limitations under the License. package commands import ( + "reflect" + "github.com/digitalocean/doctl" "github.com/digitalocean/doctl/commands/displayers" "github.com/digitalocean/doctl/do" @@ -255,8 +257,13 @@ func RunDropletActionEnableBackups(c *CmdConfig) error { return nil, err } + var defaultPolicyOnEnablingBackups = &godo.DropletBackupPolicyRequest{ + Plan: "daily", + Hour: godo.PtrTo(0), + } + policy, err := readDropletBackupPolicy(c) - if err == nil && policy != nil { + if err == nil && policy != nil && !reflect.DeepEqual(policy, defaultPolicyOnEnablingBackups) { return das.EnableBackupsWithPolicy(id, policy) } diff --git a/integration/compute_droplet_action_test.go b/integration/compute_droplet_action_test.go index 6d9cc2937..b9f54c9c1 100644 --- a/integration/compute_droplet_action_test.go +++ b/integration/compute_droplet_action_test.go @@ -44,7 +44,7 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. "/v2/droplets/247/actions": {method: http.MethodPost, body: `{"type":"enable_ipv6"}`}, "/v2/droplets/45/actions": {method: http.MethodPost, body: `{"type":"power_on"}`}, "/v2/droplets/1111/actions": {method: http.MethodPost, body: `{"kernel":7777,"type":"change_kernel"}`}, - "/v2/droplets/65/actions": {method: http.MethodPost, body: `{"backup_policy": {"hour":0, "plan":"daily", "weekday":""},"type":"enable_backups"}`}, + "/v2/droplets/65/actions": {method: http.MethodPost, body: `{"type":"enable_backups"}`}, "/v2/droplets/4743/actions": {method: http.MethodPost, body: `{"image":9999,"type":"rebuild"}`}, "/v2/droplets/884/actions": {method: http.MethodPost, body: `{"disk":true,"size":"bigger","type":"resize"}`}, "/v2/droplets/789/actions/954": {method: http.MethodGet, body: `{}`}, From 185e29bfca40dcd2c488f85e67a0030403fabb61 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 14 Nov 2024 12:39:56 -0500 Subject: [PATCH 24/32] add a comment and an integration test to enable droplet backups with backup policy --- commands/droplet_actions.go | 1 + integration/compute_droplet_action_test.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/commands/droplet_actions.go b/commands/droplet_actions.go index 262528a61..902082421 100644 --- a/commands/droplet_actions.go +++ b/commands/droplet_actions.go @@ -298,6 +298,7 @@ func readDropletBackupPolicy(c *CmdConfig) (*godo.DropletBackupPolicyRequest, er return nil, err } + // For cases when backup policy is not specified. if policyPlan == "" { return nil, nil } diff --git a/integration/compute_droplet_action_test.go b/integration/compute_droplet_action_test.go index b9f54c9c1..bf1a7f91d 100644 --- a/integration/compute_droplet_action_test.go +++ b/integration/compute_droplet_action_test.go @@ -48,6 +48,7 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. "/v2/droplets/4743/actions": {method: http.MethodPost, body: `{"image":9999,"type":"rebuild"}`}, "/v2/droplets/884/actions": {method: http.MethodPost, body: `{"disk":true,"size":"bigger","type":"resize"}`}, "/v2/droplets/789/actions/954": {method: http.MethodGet, body: `{}`}, + "/v2/droplets/66/actions": {method: http.MethodPost, body: `{"type":"enable_backups","backup_policy":{"plan":"weekly","weekday":"TUE","hour":16}}`}, } auth := req.Header.Get("Authorization") @@ -110,6 +111,7 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. {desc: "snapshot", args: []string{"snapshot", "48", "--snapshot-name", "best-snapshot"}}, {desc: "get", args: []string{"get", "789", "--action-id", "954"}}, {desc: "g", args: []string{"get", "789", "--action-id", "954"}}, + {desc: "enable backups with policy", args: []string{"enable-backups", "66", "--backup-policy-plan", "weekly", "--backup-policy-weekday", "TUE", "--backup-policy-hour", "16"}}, } for _, c := range cases { From 22c868418668f0d411fa26dda30367ba98df8ed7 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 14 Nov 2024 13:59:00 -0500 Subject: [PATCH 25/32] add an integration test for change_backup_policy in droplet_action --- integration/compute_droplet_action_test.go | 39 ++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/integration/compute_droplet_action_test.go b/integration/compute_droplet_action_test.go index bf1a7f91d..4b148c3a3 100644 --- a/integration/compute_droplet_action_test.go +++ b/integration/compute_droplet_action_test.go @@ -49,6 +49,7 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. "/v2/droplets/884/actions": {method: http.MethodPost, body: `{"disk":true,"size":"bigger","type":"resize"}`}, "/v2/droplets/789/actions/954": {method: http.MethodGet, body: `{}`}, "/v2/droplets/66/actions": {method: http.MethodPost, body: `{"type":"enable_backups","backup_policy":{"plan":"weekly","weekday":"TUE","hour":16}}`}, + "/v2/droplets/67/actions": {method: http.MethodPost, body: `{"type":"change_backup_policy","backup_policy":{"plan":"weekly","weekday":"WED","hour":4}}`}, } auth := req.Header.Get("Authorization") @@ -79,7 +80,11 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. expect.JSONEq(matchRequest.body, string(reqBody)) } - w.Write([]byte(dropletActionResponse)) + if strings.Contains(matchRequest.body, "change_backup_policy") { + w.Write([]byte(dropletActionChangeBackupResponse)) + } else { + w.Write([]byte(dropletActionResponse)) + } })) }) @@ -112,6 +117,7 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. {desc: "get", args: []string{"get", "789", "--action-id", "954"}}, {desc: "g", args: []string{"get", "789", "--action-id", "954"}}, {desc: "enable backups with policy", args: []string{"enable-backups", "66", "--backup-policy-plan", "weekly", "--backup-policy-weekday", "TUE", "--backup-policy-hour", "16"}}, + {desc: "change backup policy", args: []string{"change-backup-policy", "67", "--backup-policy-plan", "weekly", "--backup-policy-weekday", "WED", "--backup-policy-hour", "4"}}, } for _, c := range cases { @@ -127,7 +133,11 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. output, err := cmd.CombinedOutput() expect.NoError(err, fmt.Sprintf("received error output: %s", output)) - expect.Equal(strings.TrimSpace(dropletActionOutput), strings.TrimSpace(string(output))) + if strings.Contains(c.desc, "change backup policy") { + expect.Equal(strings.TrimSpace(dropletActionChangeBackupOutput), strings.TrimSpace(string(output))) + } else { + expect.Equal(strings.TrimSpace(dropletActionOutput), strings.TrimSpace(string(output))) + } }) }) } @@ -158,5 +168,30 @@ ID Status Type Started At Co "region_slug": "nyc3" } } +` + dropletActionChangeBackupOutput = ` +ID Status Type Started At Completed At Resource ID Resource Type Region +36804745 in-progress change_backup_policy 2014-11-14 16:30:56 +0000 UTC 3164450 droplet nyc3 + ` + dropletActionChangeBackupResponse = ` +{ + "action": { + "id": 36804745, + "status": "in-progress", + "type": "change_backup_policy", + "started_at": "2014-11-14T16:30:56Z", + "completed_at": null, + "resource_id": 3164450, + "resource_type": "droplet", + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ "s-24vcpu-128gb" ], + "features": [ "image_transfer" ], + "available": true + }, + "region_slug": "nyc3" + } +} ` ) From 028b168b9e230db4eed10d724b5f0ddcc9767f75 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 14 Nov 2024 14:16:49 -0500 Subject: [PATCH 26/32] add an integration test for creating a droplet with backups enabled and backup policy applied --- integration/droplet_create_test.go | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/integration/droplet_create_test.go b/integration/droplet_create_test.go index 136e64b06..65eb62a99 100644 --- a/integration/droplet_create_test.go +++ b/integration/droplet_create_test.go @@ -235,6 +235,47 @@ var _ = suite("compute/droplet/create", func(t *testing.T, when spec.G, it spec. }) } }) + when("the backup policy is passed", func() { + it("polls until the droplet is created", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", + "droplet", + "create", + "backup-policy-on-name", + "--image", "a-test-image", + "--region", "a-test-region", + "--size", "a-test-size", + "--vpc-uuid", "00000000-0000-4000-8000-000000000000", + "--enable-backups", + "--backup-policy-plan", "weekly", + "--backup-policy-hour", "4", + "--backup-policy-weekday", "MON", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dropletCreateOutput), strings.TrimSpace(string(output))) + + request := &struct { + Name string + Image string + Region string + Size string + VPCUUID string `json:"vpc_uuid"` + }{} + + err = json.Unmarshal(reqBody, request) + expect.NoError(err) + + expect.Equal("backup-policy-on-name", request.Name) + expect.Equal("a-test-image", request.Image) + expect.Equal("a-test-region", request.Region) + expect.Equal("a-test-size", request.Size) + expect.Equal("00000000-0000-4000-8000-000000000000", request.VPCUUID) + }) + }) }) const ( From afd94a3b1b75eba28821142139fbdb307e25dd1d Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 14 Nov 2024 16:27:43 -0500 Subject: [PATCH 27/32] add template and format flags to droplet backup policies get; add integration tests for droplet backup policies get --- commands/droplets.go | 21 ++- integration/droplet_get_backup_policy_test.go | 158 ++++++++++++++++++ 2 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 integration/droplet_get_backup_policy_test.go diff --git a/commands/droplets.go b/commands/droplets.go index f314b1b64..c5a2d2b99 100644 --- a/commands/droplets.go +++ b/commands/droplets.go @@ -808,8 +808,10 @@ func dropletBackupPolicies() *Command { }, } - cmdDropletGetBackupPolicy := CmdBuilder(cmd, RunDropletGetBackupPolicy, "get ", "Get droplet's backup policy", `Retrieves a backup policy of a Droplet.`, Writer) - cmdDropletGetBackupPolicy.Example = `The following example retrieves a backup policy for a Droplet with the ID ` + "`" + `386734086` + "`" + `: doctl compute droplet backup-policies get 386734086` + cmdDropletGetBackupPolicy := CmdBuilder(cmd, RunDropletGetBackupPolicy, "get ", "Get droplet's backup policy", `Retrieves a backup policy of a Droplet.`, Writer, + displayerType(&displayers.DropletBackupPolicy{})) + cmdDropletGetBackupPolicy.Example = `The following example retrieves a backup policy for a Droplet with the ID ` + "`" + `386734086` + "`" + `. The command also uses the ` + "`" + `--format` + "`" + ` flag to only return the Droplet's id and backup policy plan: doctl compute droplet backup-policies get 386734086 --format DropletID,BackupPolicyPlan` + AddStringFlag(cmdDropletGetBackupPolicy, doctl.ArgTemplate, "", "", "Go template format. Sample values: ```{{.DropletID}}`, `{{.BackupEnabled}}`, `{{.BackupPolicy.Plan}}`, `{{.BackupPolicy.Weekday}}`, `{{.BackupPolicy.Hour}}`, `{{.BackupPolicy.Plan}}`, `{{.BackupPolicy.WindowLengthHours}}`, `{{.BackupPolicy.RetentionPeriodDays}}`, `{{.NextBackupWindow.Start}}`, `{{.NextBackupWindow.End}}`") cmdDropletListBackupPolicies := CmdBuilder(cmd, RunDropletListBackupPolicies, "list", "List backup policies for all Droplets", `List droplet backup policies for all existing Droplets.`, Writer, aliasOpt("ls")) cmdDropletListBackupPolicies.Example = `The following example list backup policies for all existing Droplets: doctl compute droplet backup-policies list` @@ -867,6 +869,21 @@ func RunDropletGetBackupPolicy(c *CmdConfig) error { } item := &displayers.DropletBackupPolicy{DropletBackupPolicies: []do.DropletBackupPolicy{*policy}} + + getTemplate, err := c.Doit.GetString(c.NS, doctl.ArgTemplate) + if err != nil { + return err + } + + if getTemplate != "" { + t := template.New("Get template") + t, err = t.Parse(getTemplate) + if err != nil { + return err + } + return t.Execute(c.Out, policy) + } + return c.Display(item) } diff --git a/integration/droplet_get_backup_policy_test.go b/integration/droplet_get_backup_policy_test.go new file mode 100644 index 000000000..2fe116d4e --- /dev/null +++ b/integration/droplet_get_backup_policy_test.go @@ -0,0 +1,158 @@ +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/droplet/backup-policies/get", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + server *httptest.Server + configPath string + ) + + it.Before(func() { + expect = require.New(t) + + dir := t.TempDir() + + configPath = filepath.Join(dir, "config.yaml") + + err := os.WriteFile(configPath, []byte(dropletBackupPolicyGetConfig), 0644) + expect.NoError(err) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/droplets/5555/backups/policy": + auth := req.Header.Get("Authorization") + if auth != "Bearer special-broken" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(dropletBackupPolicyGetResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + it.After(func() { + err := os.RemoveAll(configPath) + expect.NoError(err) + }) + + when("all required flags are passed", func() { + it("gets the specified droplet ID", func() { + cmd := exec.Command(builtBinaryPath, + "-c", configPath, + "-u", server.URL, + "compute", + "droplet", + "backup-policies", + "get", + "5555", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dropletBackupPolicyGetOutput), strings.TrimSpace(string(output))) + }) + }) + + when("passing a format", func() { + it("displays only those columns", func() { + cmd := exec.Command(builtBinaryPath, + "-c", configPath, + "-u", server.URL, + "compute", + "droplet", + "backup-policies", + "get", + "5555", + "--format", "DropletID,BackupPolicyPlan", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dropletBackupPolicyGetFormatOutput), strings.TrimSpace(string(output))) + }) + }) + + when("passing a template", func() { + it("renders the template with the values", func() { + cmd := exec.Command(builtBinaryPath, + "-c", configPath, + "-u", server.URL, + "compute", + "droplet", + "backup-policies", + "get", + "5555", + "--template", "this droplet id {{.DropletID}} is making a backup {{.BackupPolicy.Plan}}", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dropletBackupPolicyGetTemplateOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + dropletBackupPolicyGetConfig = ` +--- +access-token: special-broken +` + dropletBackupPolicyGetOutput = ` +Droplet ID Enabled Plan Weekday Hour Window Length Hours Retention Period Days Next Window Start Next Window End +5555 true weekly SUN 20 4 28 2024-11-17 20:00:00 +0000 UTC 2024-11-18 00:00:00 +0000 UTC +` + dropletBackupPolicyGetFormatOutput = ` +Droplet ID Plan +5555 weekly + ` + + dropletBackupPolicyGetTemplateOutput = ` + this droplet id 5555 is making a backup weekly + ` + dropletBackupPolicyGetResponse = ` +{ + "policy": { + "droplet_id": 5555, + "backup_enabled": true, + "backup_policy": { + "plan": "weekly", + "weekday": "SUN", + "hour": 20, + "window_length_hours": 4, + "retention_period_days": 28 + }, + "next_backup_window": { + "start": "2024-11-17T20:00:00Z", + "end": "2024-11-18T00:00:00Z" + } + } +}` +) From adde2c919f930dba849762668d5a6dfe3e942345 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Fri, 15 Nov 2024 12:57:40 -0500 Subject: [PATCH 28/32] rename integration tet file; add integration test for listing backup policies for all droplets --- ...go => droplet_backup_policies_get_test.go} | 22 ++-- .../droplet_backup_policies_list_test.go | 117 ++++++++++++++++++ 2 files changed, 128 insertions(+), 11 deletions(-) rename integration/{droplet_get_backup_policy_test.go => droplet_backup_policies_get_test.go} (83%) create mode 100644 integration/droplet_backup_policies_list_test.go diff --git a/integration/droplet_get_backup_policy_test.go b/integration/droplet_backup_policies_get_test.go similarity index 83% rename from integration/droplet_get_backup_policy_test.go rename to integration/droplet_backup_policies_get_test.go index 2fe116d4e..1a1f7d8e7 100644 --- a/integration/droplet_get_backup_policy_test.go +++ b/integration/droplet_backup_policies_get_test.go @@ -29,7 +29,7 @@ var _ = suite("compute/droplet/backup-policies/get", func(t *testing.T, when spe configPath = filepath.Join(dir, "config.yaml") - err := os.WriteFile(configPath, []byte(dropletBackupPolicyGetConfig), 0644) + err := os.WriteFile(configPath, []byte(dropletBackupPoliciesGetConfig), 0644) expect.NoError(err) server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { @@ -46,7 +46,7 @@ var _ = suite("compute/droplet/backup-policies/get", func(t *testing.T, when spe return } - w.Write([]byte(dropletBackupPolicyGetResponse)) + w.Write([]byte(dropletBackupPoliciesGetResponse)) default: dump, err := httputil.DumpRequest(req, true) if err != nil { @@ -64,7 +64,7 @@ var _ = suite("compute/droplet/backup-policies/get", func(t *testing.T, when spe }) when("all required flags are passed", func() { - it("gets the specified droplet ID", func() { + it("gets backup policy for the specified droplet ID", func() { cmd := exec.Command(builtBinaryPath, "-c", configPath, "-u", server.URL, @@ -77,7 +77,7 @@ var _ = suite("compute/droplet/backup-policies/get", func(t *testing.T, when spe output, err := cmd.CombinedOutput() expect.NoError(err, fmt.Sprintf("received error output: %s", output)) - expect.Equal(strings.TrimSpace(dropletBackupPolicyGetOutput), strings.TrimSpace(string(output))) + expect.Equal(strings.TrimSpace(dropletBackupPoliciesGetOutput), strings.TrimSpace(string(output))) }) }) @@ -96,7 +96,7 @@ var _ = suite("compute/droplet/backup-policies/get", func(t *testing.T, when spe output, err := cmd.CombinedOutput() expect.NoError(err, fmt.Sprintf("received error output: %s", output)) - expect.Equal(strings.TrimSpace(dropletBackupPolicyGetFormatOutput), strings.TrimSpace(string(output))) + expect.Equal(strings.TrimSpace(dropletBackupPoliciesGetFormatOutput), strings.TrimSpace(string(output))) }) }) @@ -115,29 +115,29 @@ var _ = suite("compute/droplet/backup-policies/get", func(t *testing.T, when spe output, err := cmd.CombinedOutput() expect.NoError(err, fmt.Sprintf("received error output: %s", output)) - expect.Equal(strings.TrimSpace(dropletBackupPolicyGetTemplateOutput), strings.TrimSpace(string(output))) + expect.Equal(strings.TrimSpace(dropletBackupPoliciesGetTemplateOutput), strings.TrimSpace(string(output))) }) }) }) const ( - dropletBackupPolicyGetConfig = ` + dropletBackupPoliciesGetConfig = ` --- access-token: special-broken ` - dropletBackupPolicyGetOutput = ` + dropletBackupPoliciesGetOutput = ` Droplet ID Enabled Plan Weekday Hour Window Length Hours Retention Period Days Next Window Start Next Window End 5555 true weekly SUN 20 4 28 2024-11-17 20:00:00 +0000 UTC 2024-11-18 00:00:00 +0000 UTC ` - dropletBackupPolicyGetFormatOutput = ` + dropletBackupPoliciesGetFormatOutput = ` Droplet ID Plan 5555 weekly ` - dropletBackupPolicyGetTemplateOutput = ` + dropletBackupPoliciesGetTemplateOutput = ` this droplet id 5555 is making a backup weekly ` - dropletBackupPolicyGetResponse = ` + dropletBackupPoliciesGetResponse = ` { "policy": { "droplet_id": 5555, diff --git a/integration/droplet_backup_policies_list_test.go b/integration/droplet_backup_policies_list_test.go new file mode 100644 index 000000000..3616cb4dc --- /dev/null +++ b/integration/droplet_backup_policies_list_test.go @@ -0,0 +1,117 @@ +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/droplet/backup-policies/list", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + server *httptest.Server + configPath string + ) + + it.Before(func() { + expect = require.New(t) + + dir := t.TempDir() + + configPath = filepath.Join(dir, "config.yaml") + + err := os.WriteFile(configPath, []byte(dropletBackupPoliciesListConfig), 0644) + expect.NoError(err) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/droplets/backups/policies": + auth := req.Header.Get("Authorization") + if auth != "Bearer special-broken" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(dropletBackupPoliciesListResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + it.After(func() { + err := os.RemoveAll(configPath) + expect.NoError(err) + }) + + when("all required flags are passed", func() { + it("list backup policies for all droplets", func() { + cmd := exec.Command(builtBinaryPath, + "-c", configPath, + "-u", server.URL, + "compute", + "droplet", + "backup-policies", + "list", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dropletBackupPoliciesListOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + dropletBackupPoliciesListConfig = ` +--- +access-token: special-broken +` + dropletBackupPoliciesListOutput = ` +Droplet ID Enabled Plan Weekday Hour Window Length Hours Retention Period Days Next Window Start Next Window End +5555 true weekly SUN 20 4 28 2024-11-17 20:00:00 +0000 UTC 2024-11-18 00:00:00 +0000 UTC +` + dropletBackupPoliciesListResponse = ` +{ + "policies": { + "5555": { + "droplet_id": 5555, + "backup_enabled": true, + "backup_policy": { + "plan": "weekly", + "weekday": "SUN", + "hour": 20, + "window_length_hours": 4, + "retention_period_days": 28 + }, + "next_backup_window": { + "start": "2024-11-17T20:00:00Z", + "end": "2024-11-18T00:00:00Z" + } + } + }, + "links": {}, + "meta": { + "total": 1 + } +}` +) From 4bbdfb0cea2ad5e4efa26fc0a7498bf4038f5489 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Fri, 15 Nov 2024 13:13:08 -0500 Subject: [PATCH 29/32] add integration tests for listing droplet supported droplet backup policies --- ...let_supported_backup_policies_list_test.go | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 integration/droplet_supported_backup_policies_list_test.go diff --git a/integration/droplet_supported_backup_policies_list_test.go b/integration/droplet_supported_backup_policies_list_test.go new file mode 100644 index 000000000..603916f84 --- /dev/null +++ b/integration/droplet_supported_backup_policies_list_test.go @@ -0,0 +1,135 @@ +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/droplet/backup-policies/list-supported", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + server *httptest.Server + configPath string + ) + + it.Before(func() { + expect = require.New(t) + + dir := t.TempDir() + + configPath = filepath.Join(dir, "config.yaml") + + err := os.WriteFile(configPath, []byte(dropletSupportedBackupPoliciesListConfig), 0644) + expect.NoError(err) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/droplets/backups/supported_policies": + auth := req.Header.Get("Authorization") + if auth != "Bearer special-broken" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(dropletSupportedBackupPoliciesListResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + it.After(func() { + err := os.RemoveAll(configPath) + expect.NoError(err) + }) + + when("all required flags are passed", func() { + it("list supported droplet backup policies", func() { + cmd := exec.Command(builtBinaryPath, + "-c", configPath, + "-u", server.URL, + "compute", + "droplet", + "backup-policies", + "list-supported", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dropletSupportedBackupPoliciesListOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + dropletSupportedBackupPoliciesListConfig = ` +--- +access-token: special-broken +` + dropletSupportedBackupPoliciesListOutput = ` +Name Possible Window Starts Window Length Hours Retention Period Days Possible Days +weekly [0 4 8 12 16 20] 4 28 [SUN MON TUE WED THU FRI SAT] +daily [0 4 8 12 16 20] 4 7 [] +` + dropletSupportedBackupPoliciesListResponse = ` +{ + "supported_policies": [ + { + "name": "weekly", + "possible_window_starts": [ + 0, + 4, + 8, + 12, + 16, + 20 + ], + "window_length_hours": 4, + "retention_period_days": 28, + "possible_days": [ + "SUN", + "MON", + "TUE", + "WED", + "THU", + "FRI", + "SAT" + ] + }, + { + "name": "daily", + "possible_window_starts": [ + 0, + 4, + 8, + 12, + 16, + 20 + ], + "window_length_hours": 4, + "retention_period_days": 7, + "possible_days": [] + } + ] +}` +) From 4b6ac161ba169e9855e205ceb05dfdc7eb599e0f Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Mon, 18 Nov 2024 08:10:06 -0500 Subject: [PATCH 30/32] avoid using default values, use api defaults in droplet actions --- commands/droplet_actions.go | 15 ++++----------- integration/compute_droplet_action_test.go | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/commands/droplet_actions.go b/commands/droplet_actions.go index 902082421..fa8f93994 100644 --- a/commands/droplet_actions.go +++ b/commands/droplet_actions.go @@ -14,8 +14,6 @@ limitations under the License. package commands import ( - "reflect" - "github.com/digitalocean/doctl" "github.com/digitalocean/doctl/commands/displayers" "github.com/digitalocean/doctl/do" @@ -75,7 +73,7 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re cmdDropletActionEnableBackups := CmdBuilder(cmd, RunDropletActionEnableBackups, "enable-backups ", "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, "", "daily", `Backup policy frequency plan.`) + 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") @@ -90,9 +88,9 @@ You can use Droplet actions to perform tasks on a Droplet, such as rebooting, re cmdDropletActionChangeBackupPolicy := CmdBuilder(cmd, RunDropletActionChangeBackupPolicy, "change-backup-policy ", "Change backup policy on a Droplet", `Changes backup policy for a Droplet with enabled backups.`, Writer, displayerType(&displayers.Action{})) - AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyPlan, "", "daily", `Backup policy frequency plan.`, requiredOpt()) + AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyPlan, "", "", `Backup policy frequency plan.`, requiredOpt()) AddStringFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyWeekday, "", "", `Backup policy weekday.`) - AddIntFlag(cmdDropletActionChangeBackupPolicy, doctl.ArgDropletBackupPolicyHour, "", 0, `Backup policy hour.`, requiredOpt()) + 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` @@ -257,13 +255,8 @@ func RunDropletActionEnableBackups(c *CmdConfig) error { return nil, err } - var defaultPolicyOnEnablingBackups = &godo.DropletBackupPolicyRequest{ - Plan: "daily", - Hour: godo.PtrTo(0), - } - policy, err := readDropletBackupPolicy(c) - if err == nil && policy != nil && !reflect.DeepEqual(policy, defaultPolicyOnEnablingBackups) { + if err == nil && policy != nil { return das.EnableBackupsWithPolicy(id, policy) } diff --git a/integration/compute_droplet_action_test.go b/integration/compute_droplet_action_test.go index 4b148c3a3..2143c521a 100644 --- a/integration/compute_droplet_action_test.go +++ b/integration/compute_droplet_action_test.go @@ -44,7 +44,7 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. "/v2/droplets/247/actions": {method: http.MethodPost, body: `{"type":"enable_ipv6"}`}, "/v2/droplets/45/actions": {method: http.MethodPost, body: `{"type":"power_on"}`}, "/v2/droplets/1111/actions": {method: http.MethodPost, body: `{"kernel":7777,"type":"change_kernel"}`}, - "/v2/droplets/65/actions": {method: http.MethodPost, body: `{"type":"enable_backups"}`}, + "/v2/droplets/65/actions": {method: http.MethodPost, body: `{"type":"enable_backup"}`}, "/v2/droplets/4743/actions": {method: http.MethodPost, body: `{"image":9999,"type":"rebuild"}`}, "/v2/droplets/884/actions": {method: http.MethodPost, body: `{"disk":true,"size":"bigger","type":"resize"}`}, "/v2/droplets/789/actions/954": {method: http.MethodGet, body: `{}`}, From 2aa4aa005cca1e1decb73cb375015b0596ac9d36 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Mon, 18 Nov 2024 08:15:16 -0500 Subject: [PATCH 31/32] fix test: incorrect update in test --- integration/compute_droplet_action_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/compute_droplet_action_test.go b/integration/compute_droplet_action_test.go index 2143c521a..4b148c3a3 100644 --- a/integration/compute_droplet_action_test.go +++ b/integration/compute_droplet_action_test.go @@ -44,7 +44,7 @@ var _ = suite("compute/droplet-action", func(t *testing.T, when spec.G, it spec. "/v2/droplets/247/actions": {method: http.MethodPost, body: `{"type":"enable_ipv6"}`}, "/v2/droplets/45/actions": {method: http.MethodPost, body: `{"type":"power_on"}`}, "/v2/droplets/1111/actions": {method: http.MethodPost, body: `{"kernel":7777,"type":"change_kernel"}`}, - "/v2/droplets/65/actions": {method: http.MethodPost, body: `{"type":"enable_backup"}`}, + "/v2/droplets/65/actions": {method: http.MethodPost, body: `{"type":"enable_backups"}`}, "/v2/droplets/4743/actions": {method: http.MethodPost, body: `{"image":9999,"type":"rebuild"}`}, "/v2/droplets/884/actions": {method: http.MethodPost, body: `{"disk":true,"size":"bigger","type":"resize"}`}, "/v2/droplets/789/actions/954": {method: http.MethodGet, body: `{}`}, From 614e5c71472e8e5937a38608cf70000c011ea7dd Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Mon, 18 Nov 2024 08:27:09 -0500 Subject: [PATCH 32/32] avoid using defaults; use api defaults in droplet create --- commands/droplets.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/droplets.go b/commands/droplets.go index c5a2d2b99..7b8ca46fd 100644 --- a/commands/droplets.go +++ b/commands/droplets.go @@ -81,7 +81,7 @@ If you do not specify a region, the Droplet is created in the default region for AddStringFlag(cmdDropletCreate, doctl.ArgSizeSlug, "", "", "A `slug` indicating the Droplet's number of vCPUs, RAM, and disk size. For example, `s-1vcpu-1gb` specifies a Droplet with one vCPU and 1 GiB of RAM. The disk size is defined by the slug's plan. Run `doctl compute size list` for a list of valid size slugs and their disk sizes.", requiredOpt()) AddBoolFlag(cmdDropletCreate, doctl.ArgBackups, "", false, "Enables backups for the Droplet. By default, backups are created on a daily basis.") - AddStringFlag(cmdDropletCreate, doctl.ArgDropletBackupPolicyPlan, "", "daily", `Backup policy frequency plan.`) + AddStringFlag(cmdDropletCreate, doctl.ArgDropletBackupPolicyPlan, "", "", `Backup policy frequency plan.`) AddStringFlag(cmdDropletCreate, doctl.ArgDropletBackupPolicyWeekday, "", "", `Backup policy weekday.`) AddIntFlag(cmdDropletCreate, doctl.ArgDropletBackupPolicyHour, "", 0, `Backup policy hour.`) AddBoolFlag(cmdDropletCreate, doctl.ArgIPv6, "", false, "Enables IPv6 support and assigns an IPv6 address to the Droplet")