From bcd2404de1a5ece6a7c1409a6454c80b1af718d3 Mon Sep 17 00:00:00 2001 From: Nim G Date: Thu, 16 Apr 2026 17:24:52 -0300 Subject: [PATCH] droplets: support usage-based backup policies --- droplet_actions.go | 30 ++++--- droplet_actions_test.go | 97 ++++++++++++++++++++++ droplets.go | 8 +- droplets_test.go | 174 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 293 insertions(+), 16 deletions(-) diff --git a/droplet_actions.go b/droplet_actions.go index ed0f583c..4986a2b2 100644 --- a/droplet_actions.go +++ b/droplet_actions.go @@ -177,15 +177,7 @@ func (s *DropletActionsServiceOp) EnableBackupsWithPolicy(ctx context.Context, i 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} + request := &ActionRequest{"type": "enable_backups", "backup_policy": dropletBackupPolicyActionRequest(policy)} return s.doAction(ctx, id, request) } @@ -195,16 +187,28 @@ func (s *DropletActionsServiceOp) ChangeBackupPolicy(ctx context.Context, id int return nil, nil, NewArgError("policy", "policy can't be nil") } + request := &ActionRequest{"type": "change_backup_policy", "backup_policy": dropletBackupPolicyActionRequest(policy)} + return s.doAction(ctx, id, request) +} + +func dropletBackupPolicyActionRequest(policy *DropletBackupPolicyRequest) map[string]interface{} { policyMap := map[string]interface{}{ - "plan": policy.Plan, - "weekday": policy.Weekday, + "plan": policy.Plan, + } + if policy.Weekday != "" { + policyMap["weekday"] = policy.Weekday } if policy.Hour != nil { policyMap["hour"] = policy.Hour } + if policy.WindowLengthHours != 0 { + policyMap["window_length_hours"] = policy.WindowLengthHours + } + if policy.RetentionPeriodDays != 0 { + policyMap["retention_period_days"] = policy.RetentionPeriodDays + } - request := &ActionRequest{"type": "change_backup_policy", "backup_policy": policyMap} - return s.doAction(ctx, id, request) + return policyMap } // DisableBackups disables backups for a Droplet. diff --git a/droplet_actions_test.go b/droplet_actions_test.go index e41651f4..6fa27c15 100644 --- a/droplet_actions_test.go +++ b/droplet_actions_test.go @@ -640,6 +640,49 @@ func TestDropletAction_EnableBackupsWithPolicy(t *testing.T) { } } +func TestDropletAction_EnableBackupsWithPolicyUsageBasedRequest(t *testing.T) { + setup() + defer teardown() + + policyRequest := newUsageBasedDropletBackupPolicyRequest(t) + + request := &ActionRequest{ + "type": "enable_backups", + "backup_policy": map[string]interface{}{ + "hour": float64(0), + "plan": "intra_daily_4h", + "retention_period_days": float64(7), + "window_length_hours": float64(4), + }, + } + + 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.EnableBackupsWithPolicy returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.EnableBackupsWithPolicy returned %+v, expected %+v", action, expected) + } +} + func TestDropletAction_ChangeBackupPolicy(t *testing.T) { setup() defer teardown() @@ -688,6 +731,60 @@ func TestDropletAction_ChangeBackupPolicy(t *testing.T) { } } +func TestDropletAction_ChangeBackupPolicyUsageBasedRequest(t *testing.T) { + setup() + defer teardown() + + policyRequest := newUsageBasedDropletBackupPolicyRequest(t) + + request := &ActionRequest{ + "type": "change_backup_policy", + "backup_policy": map[string]interface{}{ + "hour": float64(0), + "plan": "intra_daily_4h", + "retention_period_days": float64(7), + "window_length_hours": float64(4), + }, + } + + 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.ChangeBackupPolicy(ctx, 1, policyRequest) + if err != nil { + t.Errorf("DropletActions.ChangeBackupPolicy returned error: %v", err) + } + + expected := &Action{Status: "in-progress"} + if !reflect.DeepEqual(action, expected) { + t.Errorf("DropletActions.ChangeBackupPolicy returned %+v, expected %+v", action, expected) + } +} + +func newUsageBasedDropletBackupPolicyRequest(t *testing.T) *DropletBackupPolicyRequest { + t.Helper() + + return &DropletBackupPolicyRequest{ + Plan: "intra_daily_4h", + Hour: PtrTo(0), + RetentionPeriodDays: 7, + WindowLengthHours: 4, + } +} + func TestDropletAction_DisableBackups(t *testing.T) { setup() defer teardown() diff --git a/droplets.go b/droplets.go index bb40ee5e..b6946948 100644 --- a/droplets.go +++ b/droplets.go @@ -261,9 +261,11 @@ type DropletMultiCreateRequest struct { // 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"` + 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"` } // DropletAssociatedResource represents a billable resource associated with a Droplet. diff --git a/droplets_test.go b/droplets_test.go index 51e932b9..a6d4702b 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -398,6 +398,89 @@ func TestDroplets_Create(t *testing.T) { } } +func TestDroplets_CreateWithUsageBasedBackupPolicy(t *testing.T) { + setup() + defer teardown() + + createRequest := &DropletCreateRequest{ + Name: "name", + Region: "region", + Size: "size", + Image: DropletCreateImage{ + ID: 1, + }, + Backups: true, + BackupPolicy: &DropletBackupPolicyRequest{ + Plan: "intra_daily_4h", + Hour: PtrTo(0), + WindowLengthHours: 4, + RetentionPeriodDays: 7, + }, + } + + mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { + expected := map[string]interface{}{ + "name": "name", + "region": "region", + "size": "size", + "image": float64(1), + "ssh_keys": nil, + "backups": true, + "ipv6": false, + "private_networking": false, + "monitoring": false, + "tags": nil, + "backup_policy": map[string]interface{}{ + "plan": "intra_daily_4h", + "hour": float64(0), + "window_length_hours": float64(4), + "retention_period_days": float64(7), + }, + } + jsonBlob := ` +{ + "droplet": { + "id": 1 + }, + "links": { + "actions": [ + { + "id": 1, + "href": "http://example.com", + "rel": "create" + } + ] + } +} +` + + var v map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + if !reflect.DeepEqual(v, expected) { + t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) + } + + fmt.Fprintf(w, jsonBlob) + }) + + droplet, resp, err := client.Droplets.Create(ctx, createRequest) + if err != nil { + t.Errorf("Droplets.Create returned error: %v", err) + } + + if id := droplet.ID; id != 1 { + t.Errorf("expected id '%d', received '%d'", 1, id) + } + + if a := resp.Links.Actions[0]; a.ID != 1 { + t.Errorf("expected action id '%d', received '%d'", 1, a.ID) + } +} + func TestDroplets_CreateWithoutDropletAgent(t *testing.T) { setup() defer teardown() @@ -693,6 +776,97 @@ func TestDroplets_CreateMultiple(t *testing.T) { } } +func TestDroplets_CreateMultipleWithUsageBasedBackupPolicy(t *testing.T) { + setup() + defer teardown() + + createRequest := &DropletMultiCreateRequest{ + Names: []string{"name1", "name2"}, + Region: "region", + Size: "size", + Image: DropletCreateImage{ + ID: 1, + }, + Backups: true, + BackupPolicy: &DropletBackupPolicyRequest{ + Plan: "intra_daily_4h", + Hour: PtrTo(0), + WindowLengthHours: 4, + RetentionPeriodDays: 7, + }, + } + + mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { + expected := map[string]interface{}{ + "names": []interface{}{"name1", "name2"}, + "region": "region", + "size": "size", + "image": float64(1), + "ssh_keys": nil, + "backups": true, + "ipv6": false, + "private_networking": false, + "monitoring": false, + "tags": nil, + "backup_policy": map[string]interface{}{ + "plan": "intra_daily_4h", + "hour": float64(0), + "window_length_hours": float64(4), + "retention_period_days": float64(7), + }, + } + jsonBlob := ` +{ + "droplets": [ + { + "id": 1 + }, + { + "id": 2 + } + ], + "links": { + "actions": [ + { + "id": 1, + "href": "http://example.com", + "rel": "multiple_create" + } + ] + } +} +` + + var v map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + if !reflect.DeepEqual(v, expected) { + t.Errorf("Request body = %#v, expected %#v", v, expected) + } + + fmt.Fprintf(w, jsonBlob) + }) + + droplets, resp, err := client.Droplets.CreateMultiple(ctx, createRequest) + if err != nil { + t.Errorf("Droplets.CreateMultiple returned error: %v", err) + } + + if id := droplets[0].ID; id != 1 { + t.Errorf("expected id '%d', received '%d'", 1, id) + } + if id := droplets[1].ID; id != 2 { + t.Errorf("expected id '%d', received '%d'", 2, id) + } + + if a := resp.Links.Actions[0]; a.ID != 1 { + t.Errorf("expected action id '%d', received '%d'", 1, a.ID) + } +} + func TestDroplets_Destroy(t *testing.T) { setup() defer teardown()