From 2c9f219b43d17af7466d3a8f54ebe92a1ef8269a Mon Sep 17 00:00:00 2001 From: Anna Lushnikova Date: Tue, 12 Nov 2024 14:09:26 -0500 Subject: [PATCH] [droplets]: add support for backup policy (#1261) * [droplets]: add support for backup policy * update godo version with fixes; rewrite getting backup policy on create * update backup policy (when backup_policy is set) * check backup_policy is set before sending an api request * add an acceptance test for ChangeBackupPolicy * fix indention * update godo * add expandBackupPolicy for create and update * check that backupd are enabled when create or update backup policy * check there is no backup_policy specified when backups are disabled * apply backup_policy if specified, otherwise use the default policy when updating * check backups enabled and run ChangeBackupPolicy only if there is backup_policy specified * update goto to v1.129.0 * remove BackupPolicy definition from opts in create; add TF MaxItems, RequiredWith to backup_policy * add acceptance tests * add an acceptance test for backup policy after re-enabling backups to check that correct policy is used instead of default policy * update documentation for droplets: add backup policy to droplet resources * fix tf config in the doc * Update docs/resources/droplet.md Co-authored-by: Andrew Starr-Bochicchio --------- Co-authored-by: Andrew Starr-Bochicchio --- digitalocean/droplet/resource_droplet.go | 151 ++++++++++++++- digitalocean/droplet/resource_droplet_test.go | 119 ++++++++++++ docs/resources/droplet.md | 18 +- go.mod | 2 +- go.sum | 4 +- .../github.com/digitalocean/godo/CHANGELOG.md | 7 + .../digitalocean/godo/droplet_actions.go | 38 ++++ .../github.com/digitalocean/godo/droplets.go | 172 +++++++++++++++--- vendor/github.com/digitalocean/godo/godo.go | 4 +- .../github.com/digitalocean/godo/registry.go | 120 ++++++++++++ .../github.com/digitalocean/godo/strings.go | 24 +++ vendor/modules.txt | 2 +- 12 files changed, 620 insertions(+), 41 deletions(-) diff --git a/digitalocean/droplet/resource_droplet.go b/digitalocean/droplet/resource_droplet.go index 6f0f67d2d..81010de32 100644 --- a/digitalocean/droplet/resource_droplet.go +++ b/digitalocean/droplet/resource_droplet.go @@ -2,6 +2,7 @@ package droplet import ( "context" + "errors" "fmt" "log" "net" @@ -20,6 +21,10 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +var ( + errDropletBackupPolicy = errors.New("backup_policy can only be set when backups are enabled") +) + func ResourceDigitalOceanDroplet() *schema.Resource { return &schema.Resource{ CreateContext: resourceDigitalOceanDropletCreate, @@ -141,6 +146,37 @@ func ResourceDigitalOceanDroplet() *schema.Resource { Default: false, }, + "backup_policy": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + RequiredWith: []string{"backups"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "plan": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "daily", + "weekly", + }, false), + }, + "weekday": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT", + }, false), + }, + "hour": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 20), + }, + }, + }, + }, + "ipv6": { Type: schema.TypeBool, Optional: true, @@ -271,6 +307,10 @@ func resourceDigitalOceanDropletCreate(ctx context.Context, d *schema.ResourceDa } if attr, ok := d.GetOk("backups"); ok { + _, exist := d.GetOk("backup_policy") + if exist && !attr.(bool) { // Check there is no backup_policy specified when backups are disabled. + return diag.FromErr(errDropletBackupPolicy) + } opts.Backups = attr.(bool) } @@ -323,6 +363,19 @@ func resourceDigitalOceanDropletCreate(ctx context.Context, d *schema.ResourceDa opts.SSHKeys = expandedSshKeys } + // Get configured backup_policy + if policy, ok := d.GetOk("backup_policy"); ok { + if !d.Get("backups").(bool) { + return diag.FromErr(errDropletBackupPolicy) + } + + backupPolicy, err := expandBackupPolicy(policy) + if err != nil { + return diag.FromErr(err) + } + opts.BackupPolicy = backupPolicy + } + log.Printf("[DEBUG] Droplet create configuration: %#v", opts) droplet, _, err := client.Droplets.Create(context.Background(), opts) @@ -557,17 +610,36 @@ func resourceDigitalOceanDropletUpdate(ctx context.Context, d *schema.ResourceDa if d.HasChange("backups") { if d.Get("backups").(bool) { // Enable backups on droplet - action, _, err := client.DropletActions.EnableBackups(context.Background(), id) - if err != nil { - return diag.Errorf( - "Error enabling backups on droplet (%s): %s", d.Id(), err) + var action *godo.Action + // Apply backup_policy if specified, otherwise use the default policy + policy, ok := d.GetOk("backup_policy") + if ok { + backupPolicy, err := expandBackupPolicy(policy) + if err != nil { + return diag.FromErr(err) + } + action, _, err = client.DropletActions.EnableBackupsWithPolicy(context.Background(), id, backupPolicy) + if err != nil { + return diag.Errorf( + "Error enabling backups on droplet (%s): %s", d.Id(), err) + } + } else { + action, _, err = client.DropletActions.EnableBackups(context.Background(), id) + if err != nil { + return diag.Errorf( + "Error enabling backups on droplet (%s): %s", d.Id(), err) + } } - if err := util.WaitForAction(client, action); err != nil { return diag.Errorf("Error waiting for backups to be enabled for droplet (%s): %s", d.Id(), err) } } else { // Disable backups on droplet + // Check there is no backup_policy specified + _, ok := d.GetOk("backup_policy") + if ok { + return diag.FromErr(errDropletBackupPolicy) + } action, _, err := client.DropletActions.DisableBackups(context.Background(), id) if err != nil { return diag.Errorf( @@ -580,6 +652,31 @@ func resourceDigitalOceanDropletUpdate(ctx context.Context, d *schema.ResourceDa } } + if d.HasChange("backup_policy") { + _, ok := d.GetOk("backup_policy") + if ok { + if !d.Get("backups").(bool) { + return diag.FromErr(errDropletBackupPolicy) + } + + _, new := d.GetChange("backup_policy") + newPolicy, err := expandBackupPolicy(new) + if err != nil { + return diag.FromErr(err) + } + + action, _, err := client.DropletActions.ChangeBackupPolicy(context.Background(), id, newPolicy) + if err != nil { + return diag.Errorf( + "error changing backup policy on droplet (%s): %s", d.Id(), err) + } + + if err := util.WaitForAction(client, action); err != nil { + return diag.Errorf("error waiting for backup policy to be changed for droplet (%s): %s", d.Id(), err) + } + } + } + // As there is no way to disable private networking, // we only check if it needs to be enabled if d.HasChange("private_networking") && d.Get("private_networking").(bool) { @@ -920,3 +1017,47 @@ func flattenDigitalOceanDropletVolumeIds(volumeids []string) *schema.Set { return flattenedVolumes } + +func expandBackupPolicy(v interface{}) (*godo.DropletBackupPolicyRequest, error) { + var policy godo.DropletBackupPolicyRequest + policyList := v.([]interface{}) + + for _, rawPolicy := range policyList { + policyMap, ok := rawPolicy.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("droplet backup policy type assertion failed: expected map[string]interface{}, got %T", rawPolicy) + } + + planVal, exists := policyMap["plan"] + if !exists { + return nil, errors.New("backup_policy plan key does not exist") + } + plan, ok := planVal.(string) + if !ok { + return nil, errors.New("backup_policy plan is not a string") + } + policy.Plan = plan + + weekdayVal, exists := policyMap["weekday"] + if !exists { + return nil, errors.New("backup_policy weekday key does not exist") + } + weekday, ok := weekdayVal.(string) + if !ok { + return nil, errors.New("backup_policy weekday is not a string") + } + policy.Weekday = weekday + + hourVal, exists := policyMap["hour"] + if !exists { + return nil, errors.New("backup_policy hour key does not exist") + } + hour, ok := hourVal.(int) + if !ok { + return nil, errors.New("backup_policy hour is not an int") + } + policy.Hour = &hour + } + + return &policy, nil +} diff --git a/digitalocean/droplet/resource_droplet_test.go b/digitalocean/droplet/resource_droplet_test.go index 17910531a..ec2ae748f 100644 --- a/digitalocean/droplet/resource_droplet_test.go +++ b/digitalocean/droplet/resource_droplet_test.go @@ -477,6 +477,112 @@ func TestAccDigitalOceanDroplet_EnableAndDisableBackups(t *testing.T) { }) } +func TestAccDigitalOceanDroplet_ChangeBackupPolicy(t *testing.T) { + var droplet godo.Droplet + name := acceptance.RandomTestName() + backupsEnabled := `backups = true` + backupsDisabled := `backups = false` + dailyPolicy := ` backup_policy { + plan = "daily" + hour = 4 + }` + weeklyPolicy := ` backup_policy { + plan = "weekly" + weekday = "MON" + hour = 0 + }` + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: acceptance.TestAccCheckDigitalOceanDropletDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDigitalOceanDropletConfig_ChangeBackupPolicy(name, backupsEnabled, ""), + Check: resource.ComposeTestCheckFunc( + acceptance.TestAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backups", "true"), + ), + }, + { + Config: testAccCheckDigitalOceanDropletConfig_ChangeBackupPolicy(name, backupsEnabled, weeklyPolicy), + Check: resource.ComposeTestCheckFunc( + acceptance.TestAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.plan", "weekly"), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.weekday", "MON"), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.hour", "0"), + ), + }, + { + Config: testAccCheckDigitalOceanDropletConfig_ChangeBackupPolicy(name, backupsEnabled, dailyPolicy), + Check: resource.ComposeTestCheckFunc( + acceptance.TestAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.plan", "daily"), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.hour", "4"), + ), + }, + // Verify specified backup policy is applied after re-enabling, and default policy is not used. + { + Config: testAccCheckDigitalOceanDropletConfig_ChangeBackupPolicy(name, backupsDisabled, ""), + Check: resource.ComposeTestCheckFunc( + acceptance.TestAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backups", "false"), + ), + }, + { + Config: testAccCheckDigitalOceanDropletConfig_ChangeBackupPolicy(name, backupsEnabled, weeklyPolicy), + Check: resource.ComposeTestCheckFunc( + acceptance.TestAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.plan", "weekly"), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.weekday", "MON"), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.hour", "0"), + ), + }, + }, + }) +} + +func TestAccDigitalOceanDroplet_WithBackupPolicy(t *testing.T) { + var droplet godo.Droplet + name := acceptance.RandomTestName() + backupsEnabled := `backups = true` + backupPolicy := ` backup_policy { + plan = "weekly" + weekday = "MON" + hour = 0 + }` + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: acceptance.TestAccCheckDigitalOceanDropletDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDigitalOceanDropletConfig_ChangeBackupPolicy(name, backupsEnabled, backupPolicy), + Check: resource.ComposeTestCheckFunc( + acceptance.TestAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.plan", "weekly"), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.weekday", "MON"), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "backup_policy.0.hour", "0"), + ), + }, + }, + }) +} + func TestAccDigitalOceanDroplet_EnableAndDisableGracefulShutdown(t *testing.T) { var droplet godo.Droplet name := acceptance.RandomTestName() @@ -1028,6 +1134,19 @@ resource "digitalocean_droplet" "foobar" { }`, name, defaultSize, defaultImage) } +func testAccCheckDigitalOceanDropletConfig_ChangeBackupPolicy(name, backups, backupPolicy string) string { + return fmt.Sprintf(` +resource "digitalocean_droplet" "foobar" { + name = "%s" + size = "%s" + image = "%s" + region = "nyc3" + user_data = "foobar" + %s + %s +}`, name, defaultSize, defaultImage, backups, backupPolicy) +} + func testAccCheckDigitalOceanDropletConfig_DropletAgent(keyName, testAccValidPublicKey, dropletName, image, agent string) string { return fmt.Sprintf(` resource "digitalocean_ssh_key" "foobar" { diff --git a/docs/resources/droplet.md b/docs/resources/droplet.md index b4030586f..9d5611985 100644 --- a/docs/resources/droplet.md +++ b/docs/resources/droplet.md @@ -14,10 +14,16 @@ modify, and delete Droplets. Droplets also support ```hcl # Create a new Web Droplet in the nyc2 region resource "digitalocean_droplet" "web" { - image = "ubuntu-20-04-x64" - name = "web-1" - region = "nyc2" - size = "s-1vcpu-1gb" + image = "ubuntu-20-04-x64" + name = "web-1" + region = "nyc2" + size = "s-1vcpu-1gb" + backups = true + backup_policy { + plan = "weekly" + weekday = "TUE" + hour = 8 + } } ``` @@ -31,6 +37,10 @@ The following arguments are supported: * `size` - (Required) The unique slug that identifies the type of Droplet. You can find a list of available slugs on [DigitalOcean API documentation](https://docs.digitalocean.com/reference/api/api-reference/#tag/Sizes). * `backups` - (Optional) Boolean controlling if backups are made. Defaults to false. +* `backup_policy` - (Optional) An object specifying the backup policy for the Droplet. If omitted and `backups` is `true`, the backup plan will default to daily. + - `plan` - The backup plan used for the Droplet. The plan can be either `daily` or `weekly`. + - `weekday` - The day of the week on which the backup will occur (`SUN`, `MON`, `TUE`, `WED`, `THU`, `FRI`, `SAT`). + - `hour` - The hour of the day that the backup window will start (`0`, `4`, `8`, `12`, `16`, `20`). * `monitoring` - (Optional) Boolean controlling whether monitoring agent is installed. Defaults to false. If set to `true`, you can configure monitor alert policies [monitor alert resource](/providers/digitalocean/digitalocean/latest/docs/resources/monitor_alert) diff --git a/go.mod b/go.mod index a45200125..b7efc11bf 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/digitalocean/terraform-provider-digitalocean require ( github.com/aws/aws-sdk-go v1.42.18 - github.com/digitalocean/godo v1.128.1-0.20241025145008-2654a9d1e887 + github.com/digitalocean/godo v1.129.0 github.com/hashicorp/awspolicyequivalence v1.5.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-uuid v1.0.3 diff --git a/go.sum b/go.sum index ce527179e..6f2f8aceb 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/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.129.0 h1:ov6v/O1N3cSuODgXBeTrwx9iYw44F4ZOHh/m9WsBp0I= +github.com/digitalocean/godo v1.129.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md index 667255b2a..735ab4ff0 100644 --- a/vendor/github.com/digitalocean/godo/CHANGELOG.md +++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [v1.129.0] - 2024-11-06 + +- #752 - @andrewsomething - Support maps in Stringify +- #749 - @loosla - [droplets]: add droplet backup policies +- #730 - @rak16 - DOCR-1201: Add new RegistriesService to support methods for multiple-registry open beta +- #748 - @andrewsomething - Support Droplet GPU information + ## [v1.128.0] - 2024-10-24 - #746 - @blesswinsamuel - Add archive field to AppSpec to archive/restore apps 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..a399bc3eb 100644 --- a/vendor/github.com/digitalocean/godo/godo.go +++ b/vendor/github.com/digitalocean/godo/godo.go @@ -21,7 +21,7 @@ import ( ) const ( - libraryVersion = "1.128.0" + libraryVersion = "1.129.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" @@ -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/github.com/digitalocean/godo/strings.go b/vendor/github.com/digitalocean/godo/strings.go index f92893ed2..5a258131e 100644 --- a/vendor/github.com/digitalocean/godo/strings.go +++ b/vendor/github.com/digitalocean/godo/strings.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "reflect" + "sort" "strings" ) @@ -46,6 +47,8 @@ func stringifyValue(w io.Writer, val reflect.Value) { return case reflect.Struct: stringifyStruct(w, v) + case reflect.Map: + stringifyMap(w, v) default: if v.CanInterface() { fmt.Fprint(w, v.Interface()) @@ -66,6 +69,27 @@ func stringifySlice(w io.Writer, v reflect.Value) { _, _ = w.Write([]byte{']'}) } +func stringifyMap(w io.Writer, v reflect.Value) { + _, _ = w.Write([]byte("map[")) + + // Sort the keys so that the output is stable + keys := v.MapKeys() + sort.Slice(keys, func(i, j int) bool { + return fmt.Sprintf("%v", keys[i]) < fmt.Sprintf("%v", keys[j]) + }) + + for i, key := range keys { + stringifyValue(w, key) + _, _ = w.Write([]byte{':'}) + stringifyValue(w, v.MapIndex(key)) + if i < len(keys)-1 { + _, _ = w.Write([]byte(", ")) + } + } + + _, _ = w.Write([]byte("]")) +} + func stringifyStruct(w io.Writer, v reflect.Value) { if v.Type().Name() != "" { _, _ = w.Write([]byte(v.Type().String())) diff --git a/vendor/modules.txt b/vendor/modules.txt index 04b805a87..9c3c4ecfe 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -58,7 +58,7 @@ github.com/aws/aws-sdk-go/service/sts/stsiface # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew -# github.com/digitalocean/godo v1.128.1-0.20241025145008-2654a9d1e887 +# github.com/digitalocean/godo v1.129.0 ## explicit; go 1.22 github.com/digitalocean/godo github.com/digitalocean/godo/metrics