From d1f708a7e192b86ea9de6c78cab48ac9477e6ef2 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Mon, 28 Oct 2024 14:52:53 -0400 Subject: [PATCH 01/14] [droplets]: add droplet backup policies --- droplets.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/droplets.go b/droplets.go index 1ed09ec8..721fb1a8 100644 --- a/droplets.go +++ b/droplets.go @@ -30,6 +30,7 @@ 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) } // DropletsServiceOp handles communication with the Droplet related methods of the @@ -618,3 +619,105 @@ 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"` + BackupEnabled bool `json:"backup_enabled,omitempty"` + BackupPolicy *BackupPolicy `json:"backup_policy,omitempty"` + NextBackupWindow *BackupWindow `json:"next_backup_window,omitempty"` +} + +// BackupPolicy defines the backup policy for a Droplet. +type BackupPolicy struct { + Plan string `json:"plan,omitempty"` + Weekday string `json:"weekday,omitempty"` + Hour string `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 []*DropletBackupPolicy `json:"policies,omitempty"` + Links *Links `json:"links"` + 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) ([]*DropletBackupPolicy, *Response, error) { + path := fmt.Sprintf("%s/backups/policies", dropletBasePath) + 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, err +} + +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, err +} From 5419a19c6f3544bd689692a35f01dc6aeaacb4b7 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Mon, 28 Oct 2024 16:53:54 -0400 Subject: [PATCH 02/14] add droplet backup policy to droplet create request --- droplets.go | 63 +++++++++++++++++++++++++++--------------------- droplets_test.go | 9 +++++-- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/droplets.go b/droplets.go index 721fb1a8..f8c07777 100644 --- a/droplets.go +++ b/droplets.go @@ -219,37 +219,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 *BackupPolicyCreateRequest `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 *BackupPolicyCreateRequest `json:"backup_policy,omitempty"` +} + +// BackupPolicyCreateRequest defines the backup policy when creating a Droplet. +type BackupPolicyCreateRequest struct { + Plan string `json:"plan,omitempty"` + Weekday string `json:"weekday,omitempty"` + Hour int `json:"hour,omitempty"` } func (d DropletCreateRequest) String() string { diff --git a/droplets_test.go b/droplets_test.go index ddd23f9e..6c9016e1 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -316,6 +316,10 @@ func TestDroplets_Create(t *testing.T) { }, Tags: []string{"one", "two"}, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", + BackupPolicy: &BackupPolicyCreateRequest{ + Plan: "weekly", + Weekday: "MON", + }, } mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { @@ -333,8 +337,9 @@ func TestDroplets_Create(t *testing.T) { map[string]interface{}{"id": "hello-im-another-volume"}, map[string]interface{}{"id": "aaa-111-bbb-222-ccc"}, }, - "tags": []interface{}{"one", "two"}, - "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", + "tags": []interface{}{"one", "two"}, + "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", + "backup_policy": map[string]interface{}{"plan": "weekly", "weekday": "MON"}, } jsonBlob := ` { From cf326d2adafc0f550348b494b2ffb797b275c760 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Tue, 29 Oct 2024 10:11:06 -0400 Subject: [PATCH 03/14] updates in code: correct types, omitempty, returns; add tests --- droplets.go | 12 +++-- droplets_test.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/droplets.go b/droplets.go index f8c07777..7a0e6672 100644 --- a/droplets.go +++ b/droplets.go @@ -31,6 +31,8 @@ type DropletsService interface { 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) ([]*DropletBackupPolicy, *Response, error) + ListSupportedBackupPolicies(context.Context) ([]*SupportedBackupPolicy, *Response, error) } // DropletsServiceOp handles communication with the Droplet related methods of the @@ -631,7 +633,7 @@ func (s *DropletsServiceOp) dropletActionStatus(ctx context.Context, uri string) // DropletBackupPolicy defines the information about a droplet's backup policy. type DropletBackupPolicy struct { - DropletID int `json:"droplet_id"` + DropletID int `json:"droplet_id,omitempty"` BackupEnabled bool `json:"backup_enabled,omitempty"` BackupPolicy *BackupPolicy `json:"backup_policy,omitempty"` NextBackupWindow *BackupWindow `json:"next_backup_window,omitempty"` @@ -641,7 +643,7 @@ type DropletBackupPolicy struct { type BackupPolicy struct { Plan string `json:"plan,omitempty"` Weekday string `json:"weekday,omitempty"` - Hour string `json:"hour,omitempty"` + Hour int `json:"hour,omitempty"` WindowLengthHours int `json:"window_length_hours,omitempty"` RetentionPeriodDays int `json:"retention_period_days,omitempty"` } @@ -653,7 +655,7 @@ type dropletBackupPolicyRoot struct { type dropletBackupPoliciesRoot struct { DropletBackupPolicies []*DropletBackupPolicy `json:"policies,omitempty"` - Links *Links `json:"links"` + Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta"` } @@ -699,7 +701,7 @@ func (s *DropletsServiceOp) ListBackupPolicies(ctx context.Context) ([]*DropletB resp.Meta = m } - return root.DropletBackupPolicies, resp, err + return root.DropletBackupPolicies, resp, nil } type SupportedBackupPolicy struct { @@ -728,5 +730,5 @@ func (s *DropletsServiceOp) ListSupportedBackupPolicies(ctx context.Context) ([] return nil, resp, err } - return root.SupportedBackupPolicies, resp, err + return root.SupportedBackupPolicies, resp, nil } diff --git a/droplets_test.go b/droplets_test.go index 6c9016e1..102a737d 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -1,11 +1,16 @@ package godo import ( + "context" "encoding/json" "fmt" "net/http" "reflect" "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDroplets_ListDroplets(t *testing.T) { @@ -952,3 +957,118 @@ func TestDroplets_IPMethods(t *testing.T) { t.Errorf("Droplet.PublicIPv6 returned %s; expected %s", got, expected) } } + +func TestDroplets_GetBackupPolicy(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/droplets/12345/backups/policy", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, `{ + "policy": { + "droplet_id": 12345, + "backup_enabled": true, + "backup_policy": { + "plan": "weekly", + "weekday": "SUN", + "hour": 0, + "window_length_hours": 4, + "retention_period_days": 28 + }, + "next_backup_window": { + "start": "2021-01-01T00:00:00Z", + "end": "2021-01-01T00:00:00Z" + } + } + }`) + }) + + policy, _, err := client.Droplets.GetBackupPolicy(ctx, 12345) + if err != nil { + t.Errorf("Droplets.GetBackupPolicy returned error: %v", err) + } + + pt, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + expected := &DropletBackupPolicy{ + DropletID: 12345, + BackupEnabled: true, + BackupPolicy: &BackupPolicy{ + Plan: "weekly", + Weekday: "SUN", + Hour: 0, + WindowLengthHours: 4, + RetentionPeriodDays: 28, + }, + NextBackupWindow: &BackupWindow{ + Start: &Timestamp{Time: pt}, + End: &Timestamp{Time: pt}, + }, + } + if !reflect.DeepEqual(policy, expected) { + t.Errorf("Droplets.GetBackupPolicy\n got=%#v\nwant=%#v", policy, expected) + } +} + +func TestDroplets_ListBackupPolicies(t *testing.T) { + setup() + defer teardown() + + ctx := context.Background() + pt, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + testBackupPolicy := DropletBackupPolicy{ + DropletID: 12345, + BackupEnabled: true, + BackupPolicy: &BackupPolicy{ + Plan: "weekly", + Weekday: "SUN", + Hour: 0, + WindowLengthHours: 4, + RetentionPeriodDays: 28, + }, + NextBackupWindow: &BackupWindow{ + Start: &Timestamp{Time: pt}, + End: &Timestamp{Time: pt}, + }, + } + + mux.HandleFunc("/v2/droplets/backups/policies", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + + json.NewEncoder(w).Encode(&dropletBackupPoliciesRoot{DropletBackupPolicies: []*DropletBackupPolicy{&testBackupPolicy}}) + }) + + policies, _, err := client.Droplets.ListBackupPolicies(ctx) + require.NoError(t, err) + assert.Equal(t, []*DropletBackupPolicy{&testBackupPolicy}, policies) +} + +func TestDroplets_ListSupportedBackupPolicies(t *testing.T) { + setup() + defer teardown() + + ctx := context.Background() + testSupportedBackupPolicy := SupportedBackupPolicy{ + Name: "weekly", + PossibleWindowStarts: []int{0, 4, 8, 12, 16, 20}, + WindowLengthHours: 4, + RetentionPeriodDays: 28, + PossibleDays: []string{"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}, + } + + mux.HandleFunc("/v2/droplets/backups/supported_policies", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + + json.NewEncoder(w).Encode(&dropletSupportedBackupPoliciesRoot{SupportedBackupPolicies: []*SupportedBackupPolicy{&testSupportedBackupPolicy}}) + }) + + policies, _, err := client.Droplets.ListSupportedBackupPolicies(ctx) + require.NoError(t, err) + assert.Equal(t, []*SupportedBackupPolicy{&testSupportedBackupPolicy}, policies) +} From 86c70c3af7b7bc1db755ee3cc3449767af23c85c Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 31 Oct 2024 08:42:05 -0400 Subject: [PATCH 04/14] Update droplets.go Co-authored-by: Andrew Starr-Bochicchio --- droplets.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/droplets.go b/droplets.go index 7a0e6672..b3829a1f 100644 --- a/droplets.go +++ b/droplets.go @@ -31,7 +31,7 @@ type DropletsService interface { 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) ([]*DropletBackupPolicy, *Response, error) + ListBackupPolicies(context.Context, *ListOptions) (map[int]*DropletBackupPolicy, *Response, error) ListSupportedBackupPolicies(context.Context) ([]*SupportedBackupPolicy, *Response, error) } From 313e07876d5135d5209bf8515e4fdda1320e24f1 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 31 Oct 2024 08:42:14 -0400 Subject: [PATCH 05/14] Update droplets.go Co-authored-by: Andrew Starr-Bochicchio --- droplets.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/droplets.go b/droplets.go index b3829a1f..dabaec5d 100644 --- a/droplets.go +++ b/droplets.go @@ -682,8 +682,12 @@ func (s *DropletsServiceOp) GetBackupPolicy(ctx context.Context, dropletID int) } // List all droplet backup policies. -func (s *DropletsServiceOp) ListBackupPolicies(ctx context.Context) ([]*DropletBackupPolicy, *Response, error) { +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 From 1abca82f201986926600161437a3dce93b286d20 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 31 Oct 2024 08:42:22 -0400 Subject: [PATCH 06/14] Update droplets.go Co-authored-by: Andrew Starr-Bochicchio --- droplets.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/droplets.go b/droplets.go index dabaec5d..269f3790 100644 --- a/droplets.go +++ b/droplets.go @@ -654,7 +654,7 @@ type dropletBackupPolicyRoot struct { } type dropletBackupPoliciesRoot struct { - DropletBackupPolicies []*DropletBackupPolicy `json:"policies,omitempty"` + DropletBackupPolicies map[int]*DropletBackupPolicy `json:"policies,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta"` } From 6ae90e39309308297e4800813585229117be3ddd Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 31 Oct 2024 09:26:20 -0400 Subject: [PATCH 07/14] rename BackupPolicyCreateRequest to DropletBackupPolicyRequest --- droplets.go | 70 ++++++++++++++++++++++++------------------------ droplets_test.go | 2 +- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/droplets.go b/droplets.go index 269f3790..19a099d9 100644 --- a/droplets.go +++ b/droplets.go @@ -221,43 +221,43 @@ 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"` - BackupPolicy *BackupPolicyCreateRequest `json:"backup_policy,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"` - BackupPolicy *BackupPolicyCreateRequest `json:"backup_policy,omitempty"` -} - -// BackupPolicyCreateRequest defines the backup policy when creating a Droplet. -type BackupPolicyCreateRequest 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"` + 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"` @@ -655,8 +655,8 @@ type dropletBackupPolicyRoot struct { type dropletBackupPoliciesRoot struct { DropletBackupPolicies map[int]*DropletBackupPolicy `json:"policies,omitempty"` - Links *Links `json:"links,omitempty"` - Meta *Meta `json:"meta"` + Links *Links `json:"links,omitempty"` + Meta *Meta `json:"meta"` } // Get individual droplet backup policy. diff --git a/droplets_test.go b/droplets_test.go index 102a737d..ac289bef 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -321,7 +321,7 @@ func TestDroplets_Create(t *testing.T) { }, Tags: []string{"one", "two"}, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", - BackupPolicy: &BackupPolicyCreateRequest{ + BackupPolicy: &DropletBackupPolicyRequest{ Plan: "weekly", Weekday: "MON", }, From 3a7bfd290da01046936a36e85a1e0f2a58126cce Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 31 Oct 2024 09:44:31 -0400 Subject: [PATCH 08/14] fix test for listing all policies after changes in a code --- droplets_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/droplets_test.go b/droplets_test.go index ac289bef..9f821ae0 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -1018,6 +1018,7 @@ func TestDroplets_ListBackupPolicies(t *testing.T) { defer teardown() ctx := context.Background() + policyID := 123 pt, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z") if err != nil { t.Fatalf("unexpected error: %s", err) @@ -1041,12 +1042,16 @@ func TestDroplets_ListBackupPolicies(t *testing.T) { mux.HandleFunc("/v2/droplets/backups/policies", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) - json.NewEncoder(w).Encode(&dropletBackupPoliciesRoot{DropletBackupPolicies: []*DropletBackupPolicy{&testBackupPolicy}}) + json.NewEncoder(w).Encode(&dropletBackupPoliciesRoot{ + DropletBackupPolicies: map[int]*DropletBackupPolicy{policyID: &testBackupPolicy}, + Meta: &Meta{Total: 1}, + Links: &Links{}, + }) }) - policies, _, err := client.Droplets.ListBackupPolicies(ctx) + policies, _, err := client.Droplets.ListBackupPolicies(ctx, &ListOptions{Page: 1}) require.NoError(t, err) - assert.Equal(t, []*DropletBackupPolicy{&testBackupPolicy}, policies) + assert.Equal(t, map[int]*DropletBackupPolicy{policyID: &testBackupPolicy}, policies) } func TestDroplets_ListSupportedBackupPolicies(t *testing.T) { From 82f2809fc3405a018c3b08c908f0f9f25979989d Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 31 Oct 2024 10:54:18 -0400 Subject: [PATCH 09/14] rename BackupPolicy to DropletBackupPolicyConfig --- droplets.go | 12 ++++++------ droplets_test.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/droplets.go b/droplets.go index 19a099d9..ddc9fc3e 100644 --- a/droplets.go +++ b/droplets.go @@ -633,14 +633,14 @@ func (s *DropletsServiceOp) dropletActionStatus(ctx context.Context, uri string) // 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 *BackupPolicy `json:"backup_policy,omitempty"` - NextBackupWindow *BackupWindow `json:"next_backup_window,omitempty"` + 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"` } -// BackupPolicy defines the backup policy for a Droplet. -type BackupPolicy struct { +// 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"` diff --git a/droplets_test.go b/droplets_test.go index 9f821ae0..4b68f5dd 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -996,7 +996,7 @@ func TestDroplets_GetBackupPolicy(t *testing.T) { expected := &DropletBackupPolicy{ DropletID: 12345, BackupEnabled: true, - BackupPolicy: &BackupPolicy{ + BackupPolicy: &DropletBackupPolicyConfig{ Plan: "weekly", Weekday: "SUN", Hour: 0, @@ -1026,7 +1026,7 @@ func TestDroplets_ListBackupPolicies(t *testing.T) { testBackupPolicy := DropletBackupPolicy{ DropletID: 12345, BackupEnabled: true, - BackupPolicy: &BackupPolicy{ + BackupPolicy: &DropletBackupPolicyConfig{ Plan: "weekly", Weekday: "SUN", Hour: 0, From e8765e333c2bccb90855ddc68f241c5275e94d15 Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Thu, 31 Oct 2024 14:41:46 -0400 Subject: [PATCH 10/14] add EnableBackupsWithPolicy to droplet actions --- droplet_actions.go | 10 ++++++++++ droplet_actions_test.go | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/droplet_actions.go b/droplet_actions.go index 2e09d0c5..a029c875 100644 --- a/droplet_actions.go +++ b/droplet_actions.go @@ -30,6 +30,7 @@ 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, map[string]interface{}) (*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 +170,15 @@ 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 map[string]interface{}) (*Action, *Response, error) { + // For cases when applying backup policy and backups are disabled: disable backup to reenable with backup_policy config. + requestToDisable := &ActionRequest{"type": "disable_backups"} + s.doAction(ctx, id, requestToDisable) + request := &ActionRequest{"type": "enable_backups", "backup_policy": policy} + 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/droplet_actions_test.go b/droplet_actions_test.go index 4beb06fc..cc4c5ea4 100644 --- a/droplet_actions_test.go +++ b/droplet_actions_test.go @@ -592,6 +592,48 @@ func TestDropletAction_EnableBackupsByTag(t *testing.T) { } } +func TestDropletAction_EnableBackupsWithPolicy(t *testing.T) { + setup() + defer teardown() + + policy := map[string]interface{}{ + "plan": "weekly", + "weekday": "TUE", + "hour": "20", + } + + // request := &ActionRequest{ + // "type": "enable_backups", + // "backup_policy": policy, + // } + + mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { + v := new(ActionRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + testMethod(t, r, http.MethodPost) + + // if !reflect.DeepEqual(v, request) { + // t.Errorf("Request body = %+v, expected %+v", v, request) + // } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + action, _, err := client.DropletActions.EnableBackupsWithPolicy(ctx, 1, policy) + if err != nil { + t.Errorf("DropletActions.EnableBackups returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.EnableBackups returned %+v, expected %+v", action, expected) + } +} + func TestDropletAction_DisableBackups(t *testing.T) { setup() defer teardown() From 5e46424ab1d28dfc104c32e37e16791fc0c47c7e Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Fri, 1 Nov 2024 12:59:28 -0400 Subject: [PATCH 11/14] update EnableBackupsWithPolicy: use change_backup_policy type --- droplet_actions.go | 5 +---- droplet_actions_test.go | 14 +++++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/droplet_actions.go b/droplet_actions.go index a029c875..ba5019ed 100644 --- a/droplet_actions.go +++ b/droplet_actions.go @@ -172,10 +172,7 @@ func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag st // EnableBackupsWithPolicy enables droplet's backup with a backup policy applied. func (s *DropletActionsServiceOp) EnableBackupsWithPolicy(ctx context.Context, id int, policy map[string]interface{}) (*Action, *Response, error) { - // For cases when applying backup policy and backups are disabled: disable backup to reenable with backup_policy config. - requestToDisable := &ActionRequest{"type": "disable_backups"} - s.doAction(ctx, id, requestToDisable) - request := &ActionRequest{"type": "enable_backups", "backup_policy": policy} + request := &ActionRequest{"type": "change_backup_policy", "backup_policy": policy} return s.doAction(ctx, id, request) } diff --git a/droplet_actions_test.go b/droplet_actions_test.go index cc4c5ea4..d881bb0d 100644 --- a/droplet_actions_test.go +++ b/droplet_actions_test.go @@ -602,10 +602,10 @@ func TestDropletAction_EnableBackupsWithPolicy(t *testing.T) { "hour": "20", } - // request := &ActionRequest{ - // "type": "enable_backups", - // "backup_policy": policy, - // } + request := &ActionRequest{ + "type": "change_backup_policy", + "backup_policy": policy, + } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) @@ -616,9 +616,9 @@ func TestDropletAction_EnableBackupsWithPolicy(t *testing.T) { testMethod(t, r, http.MethodPost) - // if !reflect.DeepEqual(v, request) { - // t.Errorf("Request body = %+v, expected %+v", v, request) - // } + if !reflect.DeepEqual(v, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) From e886602c9e0c9da5cd7d9cd930d366616d6d913d Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Fri, 1 Nov 2024 15:37:06 -0400 Subject: [PATCH 12/14] remove omitempty for backup policy hour in DropletBackupPolicyRequest to be able to use zero as a value when encoding in client.NewRequest --- droplets.go | 2 +- droplets_test.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/droplets.go b/droplets.go index ddc9fc3e..0366da7a 100644 --- a/droplets.go +++ b/droplets.go @@ -260,7 +260,7 @@ type DropletMultiCreateRequest struct { type DropletBackupPolicyRequest struct { Plan string `json:"plan,omitempty"` Weekday string `json:"weekday,omitempty"` - Hour int `json:"hour,omitempty"` + Hour int `json:"hour"` // Avoid using omitempty to ensure zero values are included in the JSON output. } func (d DropletCreateRequest) String() string { diff --git a/droplets_test.go b/droplets_test.go index 4b68f5dd..8ec0a85a 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -321,9 +321,11 @@ func TestDroplets_Create(t *testing.T) { }, Tags: []string{"one", "two"}, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", + Backups: true, BackupPolicy: &DropletBackupPolicyRequest{ Plan: "weekly", Weekday: "MON", + Hour: 0, }, } @@ -334,7 +336,6 @@ func TestDroplets_Create(t *testing.T) { "size": "size", "image": float64(1), "ssh_keys": nil, - "backups": false, "ipv6": false, "private_networking": false, "monitoring": false, @@ -344,7 +345,8 @@ func TestDroplets_Create(t *testing.T) { }, "tags": []interface{}{"one", "two"}, "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", - "backup_policy": map[string]interface{}{"plan": "weekly", "weekday": "MON"}, + "backups": true, + "backup_policy": map[string]interface{}{"plan": "weekly", "weekday": "MON", "hour": float64(0)}, } jsonBlob := ` { From ecaa7bdbfebfcc951b7203e9c09c6cf168e39b7e Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Mon, 4 Nov 2024 16:44:52 -0500 Subject: [PATCH 13/14] Update droplet_actions.go Co-authored-by: Andrew Starr-Bochicchio --- droplet_actions.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/droplet_actions.go b/droplet_actions.go index ba5019ed..347d68ab 100644 --- a/droplet_actions.go +++ b/droplet_actions.go @@ -172,7 +172,15 @@ func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag st // EnableBackupsWithPolicy enables droplet's backup with a backup policy applied. func (s *DropletActionsServiceOp) EnableBackupsWithPolicy(ctx context.Context, id int, policy map[string]interface{}) (*Action, *Response, error) { - request := &ActionRequest{"type": "change_backup_policy", "backup_policy": policy} + 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) } From a379869c286c286bed8decabf3d238b89e3e03fa Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Tue, 5 Nov 2024 09:20:35 -0500 Subject: [PATCH 14/14] add ChangeBackupPolicy; use DropletBackupPolicyRequest instead of map in request; update and add tests for changes made --- droplet_actions.go | 27 +++++++++++++++++-- droplet_actions_test.go | 58 +++++++++++++++++++++++++++++++++++++++-- droplets.go | 2 +- droplets_test.go | 2 +- 4 files changed, 83 insertions(+), 6 deletions(-) diff --git a/droplet_actions.go b/droplet_actions.go index 347d68ab..ed0f583c 100644 --- a/droplet_actions.go +++ b/droplet_actions.go @@ -30,7 +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, map[string]interface{}) (*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) @@ -171,7 +172,11 @@ func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag st } // EnableBackupsWithPolicy enables droplet's backup with a backup policy applied. -func (s *DropletActionsServiceOp) EnableBackupsWithPolicy(ctx context.Context, id int, policy map[string]interface{}) (*Action, *Response, error) { +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, @@ -184,6 +189,24 @@ func (s *DropletActionsServiceOp) EnableBackupsWithPolicy(ctx context.Context, i 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/droplet_actions_test.go b/droplet_actions_test.go index d881bb0d..e41651f4 100644 --- a/droplet_actions_test.go +++ b/droplet_actions_test.go @@ -596,10 +596,64 @@ func TestDropletAction_EnableBackupsWithPolicy(t *testing.T) { setup() defer teardown() + policyRequest := &DropletBackupPolicyRequest{ + Plan: "weekly", + Weekday: "TUE", + Hour: PtrTo(20), + } + policy := map[string]interface{}{ + "hour": float64(20), "plan": "weekly", "weekday": "TUE", - "hour": "20", + } + + request := &ActionRequest{ + "type": "enable_backups", + "backup_policy": policy, + } + + mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { + v := new(ActionRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + testMethod(t, r, http.MethodPost) + + if !reflect.DeepEqual(v, request) { + t.Errorf("Request body = %+v, expected %+v", v, request) + } + + fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) + }) + + action, _, err := client.DropletActions.EnableBackupsWithPolicy(ctx, 1, policyRequest) + if err != nil { + t.Errorf("DropletActions.EnableBackups returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.EnableBackups returned %+v, expected %+v", action, expected) + } +} + +func TestDropletAction_ChangeBackupPolicy(t *testing.T) { + setup() + defer teardown() + + policyRequest := &DropletBackupPolicyRequest{ + Plan: "weekly", + Weekday: "SUN", + Hour: PtrTo(0), + } + + policy := map[string]interface{}{ + "hour": float64(0), + "plan": "weekly", + "weekday": "SUN", } request := &ActionRequest{ @@ -623,7 +677,7 @@ func TestDropletAction_EnableBackupsWithPolicy(t *testing.T) { fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) - action, _, err := client.DropletActions.EnableBackupsWithPolicy(ctx, 1, policy) + action, _, err := client.DropletActions.ChangeBackupPolicy(ctx, 1, policyRequest) if err != nil { t.Errorf("DropletActions.EnableBackups returned error: %v", err) } diff --git a/droplets.go b/droplets.go index 0366da7a..2ddd7d6b 100644 --- a/droplets.go +++ b/droplets.go @@ -260,7 +260,7 @@ type DropletMultiCreateRequest struct { type DropletBackupPolicyRequest struct { Plan string `json:"plan,omitempty"` Weekday string `json:"weekday,omitempty"` - Hour int `json:"hour"` // Avoid using omitempty to ensure zero values are included in the JSON output. + Hour *int `json:"hour,omitempty"` } func (d DropletCreateRequest) String() string { diff --git a/droplets_test.go b/droplets_test.go index 8ec0a85a..2b510ac8 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -325,7 +325,7 @@ func TestDroplets_Create(t *testing.T) { BackupPolicy: &DropletBackupPolicyRequest{ Plan: "weekly", Weekday: "MON", - Hour: 0, + Hour: PtrTo(0), }, }