Skip to content

Commit

Permalink
[droplets]: add support for backup policy (#1261)
Browse files Browse the repository at this point in the history
* [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 <[email protected]>

---------

Co-authored-by: Andrew Starr-Bochicchio <[email protected]>
  • Loading branch information
loosla and andrewsomething authored Nov 12, 2024
1 parent ffbed06 commit 2c9f219
Show file tree
Hide file tree
Showing 12 changed files with 620 additions and 41 deletions.
151 changes: 146 additions & 5 deletions digitalocean/droplet/resource_droplet.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package droplet

import (
"context"
"errors"
"fmt"
"log"
"net"
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}
119 changes: 119 additions & 0 deletions digitalocean/droplet/resource_droplet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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" {
Expand Down
18 changes: 14 additions & 4 deletions docs/resources/droplet.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
```

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading

0 comments on commit 2c9f219

Please sign in to comment.