Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[droplets]: add support for backup policy #1261

Merged
merged 20 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
73fca2f
[droplets]: add support for backup policy
loosla Oct 31, 2024
7dd569f
update godo version with fixes; rewrite getting backup policy on create
loosla Nov 1, 2024
52b1be7
update backup policy (when backup_policy is set)
loosla Nov 1, 2024
f7f9ffa
check backup_policy is set before sending an api request
loosla Nov 4, 2024
e1c3e21
add an acceptance test for ChangeBackupPolicy
loosla Nov 4, 2024
0612c2a
fix indention
loosla Nov 4, 2024
8080ecc
Merge branch 'main' into alushnikova/add-droplet-backup-policy
loosla Nov 5, 2024
10f0f9a
update godo
loosla Nov 5, 2024
25fca06
add expandBackupPolicy for create and update
loosla Nov 6, 2024
fa49df8
check that backupd are enabled when create or update backup policy
loosla Nov 6, 2024
7b40f82
check there is no backup_policy specified when backups are disabled
loosla Nov 6, 2024
c5d0d34
apply backup_policy if specified, otherwise use the default policy wh…
loosla Nov 6, 2024
d35f6e0
check backups enabled and run ChangeBackupPolicy only if there is bac…
loosla Nov 6, 2024
d7d678b
update goto to v1.129.0
loosla Nov 6, 2024
1390234
remove BackupPolicy definition from opts in create; add TF MaxItems, …
loosla Nov 8, 2024
087cda0
add acceptance tests
loosla Nov 8, 2024
924c3ed
add an acceptance test for backup policy after re-enabling backups to…
loosla Nov 8, 2024
bc16146
update documentation for droplets: add backup policy to droplet resou…
loosla Nov 12, 2024
9050e39
fix tf config in the doc
loosla Nov 12, 2024
543265f
Update docs/resources/droplet.md
loosla Nov 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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{
loosla marked this conversation as resolved.
Show resolved Hide resolved
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) {
loosla marked this conversation as resolved.
Show resolved Hide resolved
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
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
7 changes: 7 additions & 0 deletions vendor/github.com/digitalocean/godo/CHANGELOG.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading