Add reserved IP related resources and data source (#830)

* Replace upstream godo with fork temporarily

* Add reserved_ip data source.

* Add reserved_ip resource.

* Add reserved_ip_assignment resource.

* Reduce duplication in data sources.

* Add reserved IP docs.

* Protect against panic when resp is nil.

* Clean up names of test resources.

* Add deprecation messaging to docs.

* Vendor godo v1.81.0

* Fix typos in docs.
This commit is contained in:
Andrew Starr-Bochicchio 2022-06-15 18:10:43 -04:00 committed by GitHub
parent 4a36d7dfb1
commit b1709c2e5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1573 additions and 379 deletions

View File

@ -8,11 +8,12 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
func dataSourceDigitalOceanFloatingIp() *schema.Resource {
func dataSourceDigitalOceanFloatingIP() *schema.Resource {
return &schema.Resource{
ReadContext: dataSourceDigitalOceanFloatingIpRead,
// TODO: Uncomment when dates for deprecation timeline are set.
// DeprecationMessage: "This data source is deprecated and will be removed in a future release. Please use digitalocean_reserved_ip instead.",
ReadContext: dataSourceDigitalOceanFloatingIPRead,
Schema: map[string]*schema.Schema{
"ip_address": {
Type: schema.TypeString,
Required: true,
@ -39,27 +40,12 @@ func dataSourceDigitalOceanFloatingIp() *schema.Resource {
}
}
func dataSourceDigitalOceanFloatingIpRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
ipAddress := d.Get("ip_address").(string)
floatingIp, resp, err := client.FloatingIPs.Get(context.Background(), ipAddress)
func dataSourceDigitalOceanFloatingIPRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
err := dataSourceDigitalOceanReservedIPRead(ctx, d, meta)
if err != nil {
if resp != nil && resp.StatusCode == 404 {
return diag.Errorf("floating ip not found: %s", err)
}
return diag.Errorf("Error retrieving floating ip: %s", err)
}
d.SetId(floatingIp.IP)
d.Set("ip_address", floatingIp.IP)
d.Set("urn", floatingIp.URN())
d.Set("region", floatingIp.Region.Slug)
if floatingIp.Droplet != nil {
d.Set("droplet_id", floatingIp.Droplet.ID)
return err
}
reservedIPURNtoFloatingIPURN(d)
return nil
}

View File

@ -0,0 +1,46 @@
package digitalocean
import (
"context"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
func dataSourceDigitalOceanReservedIP() *schema.Resource {
return &schema.Resource{
ReadContext: dataSourceDigitalOceanReservedIPRead,
Schema: map[string]*schema.Schema{
"ip_address": {
Type: schema.TypeString,
Required: true,
Description: "reserved ip address",
ValidateFunc: validation.NoZeroValues,
},
// computed attributes
"urn": {
Type: schema.TypeString,
Computed: true,
Description: "the uniform resource name for the reserved ip",
},
"region": {
Type: schema.TypeString,
Computed: true,
Description: "the region that the reserved ip is reserved to",
},
"droplet_id": {
Type: schema.TypeInt,
Computed: true,
Description: "the droplet id that the reserved ip has been assigned to.",
},
},
}
}
func dataSourceDigitalOceanReservedIPRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
ipAddress := d.Get("ip_address").(string)
d.SetId(ipAddress)
return resourceDigitalOceanReservedIPRead(ctx, d, meta)
}

View File

@ -0,0 +1,108 @@
package digitalocean
import (
"context"
"fmt"
"regexp"
"testing"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
func TestAccDataSourceDigitalOceanReservedIP_Basic(t *testing.T) {
var reservedIP godo.ReservedIP
expectedURNRegEx, _ := regexp.Compile(`do:reservedip:(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`)
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccCheckDataSourceDigitalOceanReservedIPConfig_Basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDataSourceDigitalOceanReservedIPExists("data.digitalocean_reserved_ip.foobar", &reservedIP),
resource.TestCheckResourceAttrSet(
"data.digitalocean_reserved_ip.foobar", "ip_address"),
resource.TestCheckResourceAttr(
"data.digitalocean_reserved_ip.foobar", "region", "nyc3"),
resource.TestMatchResourceAttr("data.digitalocean_reserved_ip.foobar", "urn", expectedURNRegEx),
),
},
},
})
}
func TestAccDataSourceDigitalOceanReservedIP_FindsFloatingIP(t *testing.T) {
var reservedIP godo.ReservedIP
expectedURNRegEx, _ := regexp.Compile(`do:reservedip:(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`)
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccCheckDataSourceDigitalOceanReservedIPConfig_FindsFloatingIP,
Check: resource.ComposeTestCheckFunc(
testAccCheckDataSourceDigitalOceanReservedIPExists("data.digitalocean_reserved_ip.foobar", &reservedIP),
resource.TestCheckResourceAttrSet(
"data.digitalocean_reserved_ip.foobar", "ip_address"),
resource.TestCheckResourceAttr(
"data.digitalocean_reserved_ip.foobar", "region", "nyc3"),
resource.TestMatchResourceAttr("data.digitalocean_reserved_ip.foobar", "urn", expectedURNRegEx),
),
},
},
})
}
func testAccCheckDataSourceDigitalOceanReservedIPExists(n string, reservedIP *godo.ReservedIP) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No reserved IP ID is set")
}
client := testAccProvider.Meta().(*CombinedConfig).godoClient()
foundReservedIP, _, err := client.ReservedIPs.Get(context.Background(), rs.Primary.ID)
if err != nil {
return err
}
if foundReservedIP.IP != rs.Primary.ID {
return fmt.Errorf("reserved IP not found")
}
*reservedIP = *foundReservedIP
return nil
}
}
const testAccCheckDataSourceDigitalOceanReservedIPConfig_FindsFloatingIP = `
resource "digitalocean_floating_ip" "foo" {
region = "nyc3"
}
data "digitalocean_reserved_ip" "foobar" {
ip_address = digitalocean_floating_ip.foo.ip_address
}`
const testAccCheckDataSourceDigitalOceanReservedIPConfig_Basic = `
resource "digitalocean_reserved_ip" "foo" {
region = "nyc3"
}
data "digitalocean_reserved_ip" "foobar" {
ip_address = digitalocean_reserved_ip.foo.ip_address
}`

View File

@ -0,0 +1,51 @@
package digitalocean
import (
"testing"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)
func TestAccDigitalOceanReservedIP_importBasicRegion(t *testing.T) {
resourceName := "digitalocean_reserved_ip.foobar"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanReservedIPDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckDigitalOceanReservedIPConfig_region,
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func TestAccDigitalOceanReservedIP_importBasicDroplet(t *testing.T) {
resourceName := "digitalocean_reserved_ip.foobar"
rInt := acctest.RandInt()
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanReservedIPDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckDigitalOceanReservedIPConfig_droplet(rInt),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

View File

@ -61,7 +61,7 @@ func Provider() *schema.Provider {
"digitalocean_droplets": dataSourceDigitalOceanDroplets(),
"digitalocean_droplet_snapshot": dataSourceDigitalOceanDropletSnapshot(),
"digitalocean_firewall": dataSourceDigitalOceanFirewall(),
"digitalocean_floating_ip": dataSourceDigitalOceanFloatingIp(),
"digitalocean_floating_ip": dataSourceDigitalOceanFloatingIP(),
"digitalocean_image": dataSourceDigitalOceanImage(),
"digitalocean_images": dataSourceDigitalOceanImages(),
"digitalocean_kubernetes_cluster": dataSourceDigitalOceanKubernetesCluster(),
@ -73,6 +73,7 @@ func Provider() *schema.Provider {
"digitalocean_records": dataSourceDigitalOceanRecords(),
"digitalocean_region": dataSourceDigitalOceanRegion(),
"digitalocean_regions": dataSourceDigitalOceanRegions(),
"digitalocean_reserved_ip": dataSourceDigitalOceanReservedIP(),
"digitalocean_sizes": dataSourceDigitalOceanSizes(),
"digitalocean_spaces_bucket": dataSourceDigitalOceanSpacesBucket(),
"digitalocean_spaces_buckets": dataSourceDigitalOceanSpacesBuckets(),
@ -103,8 +104,8 @@ func Provider() *schema.Provider {
"digitalocean_droplet": resourceDigitalOceanDroplet(),
"digitalocean_droplet_snapshot": resourceDigitalOceanDropletSnapshot(),
"digitalocean_firewall": resourceDigitalOceanFirewall(),
"digitalocean_floating_ip": resourceDigitalOceanFloatingIp(),
"digitalocean_floating_ip_assignment": resourceDigitalOceanFloatingIpAssignment(),
"digitalocean_floating_ip": resourceDigitalOceanFloatingIP(),
"digitalocean_floating_ip_assignment": resourceDigitalOceanFloatingIPAssignment(),
"digitalocean_kubernetes_cluster": resourceDigitalOceanKubernetesCluster(),
"digitalocean_kubernetes_node_pool": resourceDigitalOceanKubernetesNodePool(),
"digitalocean_loadbalancer": resourceDigitalOceanLoadbalancer(),
@ -112,6 +113,8 @@ func Provider() *schema.Provider {
"digitalocean_project": resourceDigitalOceanProject(),
"digitalocean_project_resources": resourceDigitalOceanProjectResources(),
"digitalocean_record": resourceDigitalOceanRecord(),
"digitalocean_reserved_ip": resourceDigitalOceanReservedIP(),
"digitalocean_reserved_ip_assignment": resourceDigitalOceanReservedIPAssignment(),
"digitalocean_spaces_bucket": resourceDigitalOceanBucket(),
"digitalocean_spaces_bucket_object": resourceDigitalOceanSpacesBucketObject(),
"digitalocean_spaces_bucket_policy": resourceDigitalOceanSpacesBucketPolicy(),

View File

@ -2,26 +2,24 @@ package digitalocean
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
func resourceDigitalOceanFloatingIp() *schema.Resource {
func resourceDigitalOceanFloatingIP() *schema.Resource {
return &schema.Resource{
CreateContext: resourceDigitalOceanFloatingIpCreate,
UpdateContext: resourceDigitalOceanFloatingIpUpdate,
ReadContext: resourceDigitalOceanFloatingIpRead,
DeleteContext: resourceDigitalOceanFloatingIpDelete,
// TODO: Uncomment when dates for deprecation timeline are set.
// DeprecationMessage: "This resource is deprecated and will be removed in a future release. Please use digitalocean_reserved_ip instead.",
CreateContext: resourceDigitalOceanFloatingIPCreate,
UpdateContext: resourceDigitalOceanFloatingIPUpdate,
ReadContext: resourceDigitalOceanFloatingIPRead,
DeleteContext: resourceDigitalOceanReservedIPDelete,
Importer: &schema.ResourceImporter{
State: resourceDigitalOceanFloatingIpImport,
StateContext: resourceDigitalOceanFloatingIPImport,
},
Schema: map[string]*schema.Schema{
@ -54,189 +52,49 @@ func resourceDigitalOceanFloatingIp() *schema.Resource {
}
}
func resourceDigitalOceanFloatingIpCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
log.Printf("[INFO] Create a FloatingIP In a Region")
regionOpts := &godo.FloatingIPCreateRequest{
Region: d.Get("region").(string),
}
log.Printf("[DEBUG] FloatingIP Create: %#v", regionOpts)
floatingIp, _, err := client.FloatingIPs.Create(context.Background(), regionOpts)
func resourceDigitalOceanFloatingIPCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
err := resourceDigitalOceanReservedIPCreate(ctx, d, meta)
if err != nil {
return diag.Errorf("Error creating FloatingIP: %s", err)
}
d.SetId(floatingIp.IP)
if v, ok := d.GetOk("droplet_id"); ok {
log.Printf("[INFO] Assigning the Floating IP to the Droplet %d", v.(int))
action, _, err := client.FloatingIPActions.Assign(context.Background(), d.Id(), v.(int))
if err != nil {
return diag.Errorf(
"Error Assigning FloatingIP (%s) to the droplet: %s", d.Id(), err)
}
_, unassignedErr := waitForFloatingIPReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for FloatingIP (%s) to be Assigned: %s", d.Id(), unassignedErr)
}
}
return resourceDigitalOceanFloatingIpRead(ctx, d, meta)
}
func resourceDigitalOceanFloatingIpUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
if d.HasChange("droplet_id") {
if v, ok := d.GetOk("droplet_id"); ok {
log.Printf("[INFO] Assigning the Floating IP %s to the Droplet %d", d.Id(), v.(int))
action, _, err := client.FloatingIPActions.Assign(context.Background(), d.Id(), v.(int))
if err != nil {
return diag.Errorf(
"Error Assigning FloatingIP (%s) to the droplet: %s", d.Id(), err)
}
_, unassignedErr := waitForFloatingIPReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for FloatingIP (%s) to be Assigned: %s", d.Id(), unassignedErr)
}
} else {
log.Printf("[INFO] Unassigning the Floating IP %s", d.Id())
action, _, err := client.FloatingIPActions.Unassign(context.Background(), d.Id())
if err != nil {
return diag.Errorf(
"Error unassigning FloatingIP (%s): %s", d.Id(), err)
}
_, unassignedErr := waitForFloatingIPReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for FloatingIP (%s) to be Unassigned: %s", d.Id(), unassignedErr)
}
}
}
return resourceDigitalOceanFloatingIpRead(ctx, d, meta)
}
func resourceDigitalOceanFloatingIpRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
log.Printf("[INFO] Reading the details of the FloatingIP %s", d.Id())
floatingIp, resp, err := client.FloatingIPs.Get(context.Background(), d.Id())
if resp.StatusCode != 404 {
if err != nil {
return diag.Errorf("Error retrieving FloatingIP: %s", err)
}
if _, ok := d.GetOk("droplet_id"); ok && floatingIp.Droplet != nil {
log.Printf("[INFO] A droplet was detected on the FloatingIP so setting the Region based on the Droplet")
log.Printf("[INFO] The region of the Droplet is %s", floatingIp.Droplet.Region.Slug)
d.Set("region", floatingIp.Droplet.Region.Slug)
d.Set("droplet_id", floatingIp.Droplet.ID)
} else {
d.Set("region", floatingIp.Region.Slug)
}
d.Set("ip_address", floatingIp.IP)
d.Set("urn", floatingIp.URN())
} else {
d.SetId("")
return err
}
reservedIPURNtoFloatingIPURN(d)
return nil
}
func resourceDigitalOceanFloatingIpImport(rs *schema.ResourceData, v interface{}) ([]*schema.ResourceData, error) {
client := v.(*CombinedConfig).godoClient()
floatingIp, resp, err := client.FloatingIPs.Get(context.Background(), rs.Id())
if resp.StatusCode != 404 {
if err != nil {
return nil, err
}
rs.Set("ip_address", floatingIp.IP)
rs.Set("urn", floatingIp.URN())
rs.Set("region", floatingIp.Region.Slug)
if floatingIp.Droplet != nil {
rs.Set("droplet_id", floatingIp.Droplet.ID)
}
}
return []*schema.ResourceData{rs}, nil
}
func resourceDigitalOceanFloatingIpDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
if _, ok := d.GetOk("droplet_id"); ok {
log.Printf("[INFO] Unassigning the Floating IP from the Droplet")
action, resp, err := client.FloatingIPActions.Unassign(context.Background(), d.Id())
if resp.StatusCode != 422 {
if err != nil {
return diag.Errorf(
"Error unassigning FloatingIP (%s) from the droplet: %s", d.Id(), err)
}
_, unassignedErr := waitForFloatingIPReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for FloatingIP (%s) to be unassigned: %s", d.Id(), unassignedErr)
}
} else {
log.Printf("[DEBUG] Couldn't unassign FloatingIP (%s) from droplet, possibly out of sync: %s", d.Id(), err)
}
}
log.Printf("[INFO] Deleting FloatingIP: %s", d.Id())
_, err := client.FloatingIPs.Delete(context.Background(), d.Id())
func resourceDigitalOceanFloatingIPUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
err := resourceDigitalOceanReservedIPUpdate(ctx, d, meta)
if err != nil {
return diag.Errorf("Error deleting FloatingIP: %s", err)
return err
}
reservedIPURNtoFloatingIPURN(d)
d.SetId("")
return nil
}
func waitForFloatingIPReady(
ctx context.Context, d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}, actionId int) (interface{}, error) {
log.Printf(
"[INFO] Waiting for FloatingIP (%s) to have %s of %s",
d.Id(), attribute, target)
stateConf := &resource.StateChangeConf{
Pending: pending,
Target: []string{target},
Refresh: newFloatingIPStateRefreshFunc(d, attribute, meta, actionId),
Timeout: 60 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
NotFoundChecks: 60,
func resourceDigitalOceanFloatingIPRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
err := resourceDigitalOceanReservedIPRead(ctx, d, meta)
if err != nil {
return err
}
reservedIPURNtoFloatingIPURN(d)
return stateConf.WaitForStateContext(ctx)
return nil
}
func newFloatingIPStateRefreshFunc(
d *schema.ResourceData, attribute string, meta interface{}, actionId int) resource.StateRefreshFunc {
client := meta.(*CombinedConfig).godoClient()
return func() (interface{}, string, error) {
log.Printf("[INFO] Assigning the Floating IP to the Droplet")
action, _, err := client.FloatingIPActions.Get(context.Background(), d.Id(), actionId)
if err != nil {
return nil, "", fmt.Errorf("Error retrieving FloatingIP (%s) ActionId (%d): %s", d.Id(), actionId, err)
}
log.Printf("[INFO] The FloatingIP Action Status is %s", action.Status)
return &action, action.Status, nil
func resourceDigitalOceanFloatingIPImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
_, err := resourceDigitalOceanReservedIPImport(ctx, d, meta)
if err != nil {
return nil, err
}
reservedIPURNtoFloatingIPURN(d)
return []*schema.ResourceData{d}, nil
}
// reservedIPURNtoFloatingIPURN re-formats a reserved IP URN as floating IP URN.
// TODO: Remove when the projects' API changes return values.
func reservedIPURNtoFloatingIPURN(d *schema.ResourceData) {
ip := d.Get("ip_address")
d.Set("urn", godo.FloatingIP{IP: ip.(string)}.URN())
}

View File

@ -1,27 +1,19 @@
package digitalocean
import (
"context"
"errors"
"fmt"
"log"
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
func resourceDigitalOceanFloatingIpAssignment() *schema.Resource {
func resourceDigitalOceanFloatingIPAssignment() *schema.Resource {
return &schema.Resource{
CreateContext: resourceDigitalOceanFloatingIpAssignmentCreate,
ReadContext: resourceDigitalOceanFloatingIpAssignmentRead,
DeleteContext: resourceDigitalOceanFloatingIpAssignmentDelete,
// TODO: Uncomment when dates for deprecation timeline are set.
// DeprecationMessage: "This resource is deprecated and will be removed in a future release. Please use digitalocean_reserved_ip_assignment instead.",
CreateContext: resourceDigitalOceanReservedIPAssignmentCreate,
ReadContext: resourceDigitalOceanReservedIPAssignmentRead,
DeleteContext: resourceDigitalOceanReservedIPAssignmentDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceDigitalOceanFloatingIPAssignmentImport,
StateContext: resourceDigitalOceanReservedIPAssignmentImport,
},
Schema: map[string]*schema.Schema{
@ -41,131 +33,3 @@ func resourceDigitalOceanFloatingIpAssignment() *schema.Resource {
},
}
}
func resourceDigitalOceanFloatingIpAssignmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
ip_address := d.Get("ip_address").(string)
droplet_id := d.Get("droplet_id").(int)
log.Printf("[INFO] Assigning the Floating IP (%s) to the Droplet %d", ip_address, droplet_id)
action, _, err := client.FloatingIPActions.Assign(context.Background(), ip_address, droplet_id)
if err != nil {
return diag.Errorf(
"Error Assigning FloatingIP (%s) to the droplet: %s", ip_address, err)
}
_, unassignedErr := waitForFloatingIPAssignmentReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for FloatingIP (%s) to be Assigned: %s", ip_address, unassignedErr)
}
d.SetId(resource.PrefixedUniqueId(fmt.Sprintf("%d-%s-", droplet_id, ip_address)))
return resourceDigitalOceanFloatingIpAssignmentRead(ctx, d, meta)
}
func resourceDigitalOceanFloatingIpAssignmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
ip_address := d.Get("ip_address").(string)
droplet_id := d.Get("droplet_id").(int)
log.Printf("[INFO] Reading the details of the FloatingIP %s", ip_address)
floatingIp, _, err := client.FloatingIPs.Get(context.Background(), ip_address)
if err != nil {
return diag.Errorf("Error retrieving FloatingIP: %s", err)
}
if floatingIp.Droplet == nil || floatingIp.Droplet.ID != droplet_id {
log.Printf("[INFO] A droplet was detected on the FloatingIP.")
d.SetId("")
}
return nil
}
func resourceDigitalOceanFloatingIpAssignmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
ip_address := d.Get("ip_address").(string)
droplet_id := d.Get("droplet_id").(int)
log.Printf("[INFO] Reading the details of the FloatingIP %s", ip_address)
floatingIp, _, err := client.FloatingIPs.Get(context.Background(), ip_address)
if err != nil {
return diag.Errorf("Error retrieving FloatingIP: %s", err)
}
if floatingIp.Droplet.ID == droplet_id {
log.Printf("[INFO] Unassigning the Floating IP from the Droplet")
action, _, err := client.FloatingIPActions.Unassign(context.Background(), ip_address)
if err != nil {
return diag.Errorf("Error unassigning FloatingIP (%s) from the droplet: %s", ip_address, err)
}
_, unassignedErr := waitForFloatingIPAssignmentReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for FloatingIP (%s) to be unassigned: %s", ip_address, unassignedErr)
}
} else {
log.Printf("[INFO] Floating IP already unassigned, removing from state.")
}
d.SetId("")
return nil
}
func waitForFloatingIPAssignmentReady(
ctx context.Context, d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}, actionId int) (interface{}, error) {
log.Printf(
"[INFO] Waiting for FloatingIP (%s) to have %s of %s",
d.Get("ip_address").(string), attribute, target)
stateConf := &resource.StateChangeConf{
Pending: pending,
Target: []string{target},
Refresh: newFloatingIPAssignmentStateRefreshFunc(d, attribute, meta, actionId),
Timeout: 60 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
NotFoundChecks: 60,
}
return stateConf.WaitForStateContext(ctx)
}
func newFloatingIPAssignmentStateRefreshFunc(
d *schema.ResourceData, attribute string, meta interface{}, actionId int) resource.StateRefreshFunc {
client := meta.(*CombinedConfig).godoClient()
return func() (interface{}, string, error) {
log.Printf("[INFO] Refreshing the Floating IP state")
action, _, err := client.FloatingIPActions.Get(context.Background(), d.Get("ip_address").(string), actionId)
if err != nil {
return nil, "", fmt.Errorf("Error retrieving FloatingIP (%s) ActionId (%d): %s", d.Get("ip_address").(string), actionId, err)
}
log.Printf("[INFO] The FloatingIP Action Status is %s", action.Status)
return &action, action.Status, nil
}
}
func resourceDigitalOceanFloatingIPAssignmentImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
if strings.Contains(d.Id(), ",") {
s := strings.Split(d.Id(), ",")
d.SetId(resource.PrefixedUniqueId(fmt.Sprintf("%s-%s-", s[1], s[0])))
d.Set("ip_address", s[0])
dropletID, err := strconv.Atoi(s[1])
if err != nil {
return nil, err
}
d.Set("droplet_id", dropletID)
} else {
return nil, errors.New("must use the Floating IP and the ID of the Droplet joined with a comma (e.g. `ip_address,droplet_id`)")
}
return []*schema.ResourceData{d}, nil
}

View File

@ -121,7 +121,7 @@ resource "digitalocean_floating_ip" "foobar" {
resource "digitalocean_droplet" "foobar" {
count = 2
name = "foobar-${count.index}"
name = "tf-acc-test-${count.index}"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
@ -142,7 +142,7 @@ resource "digitalocean_floating_ip" "foobar" {
resource "digitalocean_droplet" "foobar" {
count = 2
name = "foobar-${count.index}"
name = "tf-acc-test-${count.index}"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
@ -163,7 +163,7 @@ resource "digitalocean_floating_ip" "foobar" {
resource "digitalocean_droplet" "foobar" {
count = 2
name = "foobar-${count.index}"
name = "tf-acc-test-${count.index}"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
@ -175,7 +175,7 @@ resource "digitalocean_droplet" "foobar" {
var testAccCheckDigitalOceanFloatingIPAssignmentConfig_createBeforeDestroy = `
resource "digitalocean_droplet" "foobar" {
image = "centos-7-x64"
name = "foo-bar"
name = "tf-acc-test"
region = "nyc3"
size = "s-1vcpu-1gb"
@ -201,7 +201,7 @@ resource "digitalocean_floating_ip_assignment" "foobar" {
var testAccCheckDigitalOceanFloatingIPAssignmentConfig_createBeforeDestroyReassign = `
resource "digitalocean_droplet" "foobar" {
image = "ubuntu-18-04-x64"
name = "foobar"
name = "tf-acc-test"
region = "nyc3"
size = "s-1vcpu-1gb"

View File

@ -160,7 +160,7 @@ resource "digitalocean_floating_ip" "foobar" {
func testAccCheckDigitalOceanFloatingIPConfig_droplet(rInt int) string {
return fmt.Sprintf(`
resource "digitalocean_droplet" "foobar" {
name = "foobar-%d"
name = "tf-acc-test-%d"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
@ -177,7 +177,7 @@ resource "digitalocean_floating_ip" "foobar" {
func testAccCheckDigitalOceanFloatingIPConfig_Reassign(rInt int) string {
return fmt.Sprintf(`
resource "digitalocean_droplet" "baz" {
name = "baz-%d"
name = "tf-acc-test-%d"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
@ -194,7 +194,7 @@ resource "digitalocean_floating_ip" "foobar" {
func testAccCheckDigitalOceanFloatingIPConfig_Unassign(rInt int) string {
return fmt.Sprintf(`
resource "digitalocean_droplet" "baz" {
name = "baz-%d"
name = "tf-acc-test-%d"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"

View File

@ -0,0 +1,240 @@
package digitalocean
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
func resourceDigitalOceanReservedIP() *schema.Resource {
return &schema.Resource{
CreateContext: resourceDigitalOceanReservedIPCreate,
UpdateContext: resourceDigitalOceanReservedIPUpdate,
ReadContext: resourceDigitalOceanReservedIPRead,
DeleteContext: resourceDigitalOceanReservedIPDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceDigitalOceanReservedIPImport,
},
Schema: map[string]*schema.Schema{
"region": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
StateFunc: func(val interface{}) string {
// DO API V2 region slug is always lowercase
return strings.ToLower(val.(string))
},
},
"urn": {
Type: schema.TypeString,
Computed: true,
Description: "the uniform resource name for the reserved ip",
},
"ip_address": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.IsIPv4Address,
},
"droplet_id": {
Type: schema.TypeInt,
Optional: true,
},
},
}
}
func resourceDigitalOceanReservedIPCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
log.Printf("[INFO] Creating a reserved IP in a region")
regionOpts := &godo.ReservedIPCreateRequest{
Region: d.Get("region").(string),
}
log.Printf("[DEBUG] Reserved IP create: %#v", regionOpts)
reservedIP, _, err := client.ReservedIPs.Create(context.Background(), regionOpts)
if err != nil {
return diag.Errorf("Error creating reserved IP: %s", err)
}
d.SetId(reservedIP.IP)
if v, ok := d.GetOk("droplet_id"); ok {
log.Printf("[INFO] Assigning the reserved IP to the Droplet %d", v.(int))
action, _, err := client.ReservedIPActions.Assign(context.Background(), d.Id(), v.(int))
if err != nil {
return diag.Errorf(
"Error Assigning reserved IP (%s) to the Droplet: %s", d.Id(), err)
}
_, unassignedErr := waitForReservedIPReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for reserved IP (%s) to be assigned: %s", d.Id(), unassignedErr)
}
}
return resourceDigitalOceanReservedIPRead(ctx, d, meta)
}
func resourceDigitalOceanReservedIPUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
if d.HasChange("droplet_id") {
if v, ok := d.GetOk("droplet_id"); ok {
log.Printf("[INFO] Assigning the reserved IP %s to the Droplet %d", d.Id(), v.(int))
action, _, err := client.ReservedIPActions.Assign(context.Background(), d.Id(), v.(int))
if err != nil {
return diag.Errorf(
"Error assigning reserved IP (%s) to the Droplet: %s", d.Id(), err)
}
_, unassignedErr := waitForReservedIPReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for reserved IP (%s) to be Assigned: %s", d.Id(), unassignedErr)
}
} else {
log.Printf("[INFO] Unassigning the reserved IP %s", d.Id())
action, _, err := client.ReservedIPActions.Unassign(context.Background(), d.Id())
if err != nil {
return diag.Errorf(
"Error unassigning reserved IP (%s): %s", d.Id(), err)
}
_, unassignedErr := waitForReservedIPReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for reserved IP (%s) to be Unassigned: %s", d.Id(), unassignedErr)
}
}
}
return resourceDigitalOceanReservedIPRead(ctx, d, meta)
}
func resourceDigitalOceanReservedIPRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
log.Printf("[INFO] Reading the details of the reserved IP %s", d.Id())
reservedIP, resp, err := client.ReservedIPs.Get(context.Background(), d.Id())
if err != nil {
if resp != nil && resp.StatusCode == 404 {
log.Printf("[WARN] Reserved IP (%s) not found", d.Id())
d.SetId("")
return nil
}
return diag.Errorf("Error retrieving reserved IP: %s", err)
}
if _, ok := d.GetOk("droplet_id"); ok && reservedIP.Droplet != nil {
d.Set("region", reservedIP.Droplet.Region.Slug)
d.Set("droplet_id", reservedIP.Droplet.ID)
} else {
d.Set("region", reservedIP.Region.Slug)
}
d.Set("ip_address", reservedIP.IP)
d.Set("urn", reservedIP.URN())
return nil
}
func resourceDigitalOceanReservedIPDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
if _, ok := d.GetOk("droplet_id"); ok {
log.Printf("[INFO] Unassigning the reserved IP from the Droplet")
action, resp, err := client.ReservedIPActions.Unassign(context.Background(), d.Id())
if resp.StatusCode != 422 {
if err != nil {
return diag.Errorf(
"Error unassigning reserved IP (%s) from the droplet: %s", d.Id(), err)
}
_, unassignedErr := waitForReservedIPReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for reserved IP (%s) to be unassigned: %s", d.Id(), unassignedErr)
}
} else {
log.Printf("[DEBUG] Couldn't unassign reserved IP (%s) from droplet, possibly out of sync: %s", d.Id(), err)
}
}
log.Printf("[INFO] Deleting reserved IP: %s", d.Id())
_, err := client.ReservedIPs.Delete(context.Background(), d.Id())
if err != nil {
return diag.Errorf("Error deleting reserved IP: %s", err)
}
d.SetId("")
return nil
}
func resourceDigitalOceanReservedIPImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
client := meta.(*CombinedConfig).godoClient()
reservedIP, resp, err := client.ReservedIPs.Get(context.Background(), d.Id())
if resp.StatusCode != 404 {
if err != nil {
return nil, err
}
d.Set("ip_address", reservedIP.IP)
d.Set("urn", reservedIP.URN())
d.Set("region", reservedIP.Region.Slug)
if reservedIP.Droplet != nil {
d.Set("droplet_id", reservedIP.Droplet.ID)
}
}
return []*schema.ResourceData{d}, nil
}
func waitForReservedIPReady(
ctx context.Context, d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}, actionID int) (interface{}, error) {
log.Printf(
"[INFO] Waiting for reserved IP (%s) to have %s of %s",
d.Id(), attribute, target)
stateConf := &resource.StateChangeConf{
Pending: pending,
Target: []string{target},
Refresh: newReservedIPStateRefreshFunc(d, attribute, meta, actionID),
Timeout: 60 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
NotFoundChecks: 60,
}
return stateConf.WaitForStateContext(ctx)
}
func newReservedIPStateRefreshFunc(
d *schema.ResourceData, attribute string, meta interface{}, actionID int) resource.StateRefreshFunc {
client := meta.(*CombinedConfig).godoClient()
return func() (interface{}, string, error) {
log.Printf("[INFO] Assigning the reserved IP to the Droplet")
action, _, err := client.ReservedIPActions.Get(context.Background(), d.Id(), actionID)
if err != nil {
return nil, "", fmt.Errorf("Error retrieving reserved IP (%s) ActionId (%d): %s", d.Id(), actionID, err)
}
log.Printf("[INFO] The reserved IP Action Status is %s", action.Status)
return &action, action.Status, nil
}
}

View File

@ -0,0 +1,170 @@
package digitalocean
import (
"context"
"errors"
"fmt"
"log"
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
func resourceDigitalOceanReservedIPAssignment() *schema.Resource {
return &schema.Resource{
CreateContext: resourceDigitalOceanReservedIPAssignmentCreate,
ReadContext: resourceDigitalOceanReservedIPAssignmentRead,
DeleteContext: resourceDigitalOceanReservedIPAssignmentDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceDigitalOceanReservedIPAssignmentImport,
},
Schema: map[string]*schema.Schema{
"ip_address": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.IsIPv4Address,
},
"droplet_id": {
Type: schema.TypeInt,
Required: true,
ForceNew: true,
ValidateFunc: validation.NoZeroValues,
},
},
}
}
func resourceDigitalOceanReservedIPAssignmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
ipAddress := d.Get("ip_address").(string)
dropletID := d.Get("droplet_id").(int)
log.Printf("[INFO] Assigning the reserved IP (%s) to the Droplet %d", ipAddress, dropletID)
action, _, err := client.ReservedIPActions.Assign(context.Background(), ipAddress, dropletID)
if err != nil {
return diag.Errorf(
"Error Assigning reserved IP (%s) to the droplet: %s", ipAddress, err)
}
_, unassignedErr := waitForReservedIPAssignmentReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for reserved IP (%s) to be Assigned: %s", ipAddress, unassignedErr)
}
d.SetId(resource.PrefixedUniqueId(fmt.Sprintf("%d-%s-", dropletID, ipAddress)))
return resourceDigitalOceanReservedIPAssignmentRead(ctx, d, meta)
}
func resourceDigitalOceanReservedIPAssignmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
ipAddress := d.Get("ip_address").(string)
dropletID := d.Get("droplet_id").(int)
log.Printf("[INFO] Reading the details of the reserved IP %s", ipAddress)
reservedIP, _, err := client.ReservedIPs.Get(context.Background(), ipAddress)
if err != nil {
return diag.Errorf("Error retrieving reserved IP: %s", err)
}
if reservedIP.Droplet == nil || reservedIP.Droplet.ID != dropletID {
log.Printf("[INFO] A Droplet was detected on the reserved IP.")
d.SetId("")
}
return nil
}
func resourceDigitalOceanReservedIPAssignmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()
ipAddress := d.Get("ip_address").(string)
dropletID := d.Get("droplet_id").(int)
log.Printf("[INFO] Reading the details of the reserved IP %s", ipAddress)
reservedIP, _, err := client.ReservedIPs.Get(context.Background(), ipAddress)
if err != nil {
return diag.Errorf("Error retrieving reserved IP: %s", err)
}
if reservedIP.Droplet.ID == dropletID {
log.Printf("[INFO] Unassigning the reserved IP from the Droplet")
action, _, err := client.ReservedIPActions.Unassign(context.Background(), ipAddress)
if err != nil {
return diag.Errorf("Error unassigning reserved IP (%s) from the droplet: %s", ipAddress, err)
}
_, unassignedErr := waitForReservedIPAssignmentReady(ctx, d, "completed", []string{"new", "in-progress"}, "status", meta, action.ID)
if unassignedErr != nil {
return diag.Errorf(
"Error waiting for reserved IP (%s) to be unassigned: %s", ipAddress, unassignedErr)
}
} else {
log.Printf("[INFO] reserved IP already unassigned, removing from state.")
}
d.SetId("")
return nil
}
func waitForReservedIPAssignmentReady(
ctx context.Context, d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}, actionID int) (interface{}, error) {
log.Printf(
"[INFO] Waiting for reserved IP (%s) to have %s of %s",
d.Get("ip_address").(string), attribute, target)
stateConf := &resource.StateChangeConf{
Pending: pending,
Target: []string{target},
Refresh: newReservedIPAssignmentStateRefreshFunc(d, attribute, meta, actionID),
Timeout: 60 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
NotFoundChecks: 60,
}
return stateConf.WaitForStateContext(ctx)
}
func newReservedIPAssignmentStateRefreshFunc(
d *schema.ResourceData, attribute string, meta interface{}, actionID int) resource.StateRefreshFunc {
client := meta.(*CombinedConfig).godoClient()
return func() (interface{}, string, error) {
log.Printf("[INFO] Refreshing the reserved IP state")
action, _, err := client.ReservedIPActions.Get(context.Background(), d.Get("ip_address").(string), actionID)
if err != nil {
return nil, "", fmt.Errorf("Error retrieving reserved IP (%s) ActionId (%d): %s", d.Get("ip_address").(string), actionID, err)
}
log.Printf("[INFO] The reserved IP Action Status is %s", action.Status)
return &action, action.Status, nil
}
}
func resourceDigitalOceanReservedIPAssignmentImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
if strings.Contains(d.Id(), ",") {
s := strings.Split(d.Id(), ",")
d.SetId(resource.PrefixedUniqueId(fmt.Sprintf("%s-%s-", s[1], s[0])))
d.Set("ip_address", s[0])
dropletID, err := strconv.Atoi(s[1])
if err != nil {
return nil, err
}
d.Set("droplet_id", dropletID)
} else {
return nil, errors.New("must use the reserved IP and the ID of the Droplet joined with a comma (e.g. `ip_address,droplet_id`)")
}
return []*schema.ResourceData{d}, nil
}

View File

@ -0,0 +1,225 @@
package digitalocean
import (
"context"
"fmt"
"regexp"
"strconv"
"testing"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
func TestAccDigitalOceanReservedIPAssignment(t *testing.T) {
var reservedIP godo.ReservedIP
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanReservedIPDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckDigitalOceanReservedIPAssignmentConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanReservedIPAttachmentExists("digitalocean_reserved_ip_assignment.foobar"),
resource.TestMatchResourceAttr(
"digitalocean_reserved_ip_assignment.foobar", "id", regexp.MustCompile("[0-9.]+")),
resource.TestMatchResourceAttr(
"digitalocean_reserved_ip_assignment.foobar", "droplet_id", regexp.MustCompile("[0-9]+")),
),
},
{
Config: testAccCheckDigitalOceanReservedIPAssignmentReassign,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanReservedIPAttachmentExists("digitalocean_reserved_ip_assignment.foobar"),
resource.TestMatchResourceAttr(
"digitalocean_reserved_ip_assignment.foobar", "id", regexp.MustCompile("[0-9.]+")),
resource.TestMatchResourceAttr(
"digitalocean_reserved_ip_assignment.foobar", "droplet_id", regexp.MustCompile("[0-9]+")),
),
},
{
Config: testAccCheckDigitalOceanReservedIPAssignmentDeleteAssignment,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanReservedIPExists("digitalocean_reserved_ip.foobar", &reservedIP),
resource.TestMatchResourceAttr(
"digitalocean_reserved_ip.foobar", "ip_address", regexp.MustCompile("[0-9.]+")),
),
},
},
})
}
func TestAccDigitalOceanReservedIPAssignment_createBeforeDestroy(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanReservedIPDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckDigitalOceanReservedIPAssignmentConfig_createBeforeDestroy,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanReservedIPAttachmentExists("digitalocean_reserved_ip_assignment.foobar"),
resource.TestMatchResourceAttr(
"digitalocean_reserved_ip_assignment.foobar", "id", regexp.MustCompile("[0-9.]+")),
resource.TestMatchResourceAttr(
"digitalocean_reserved_ip_assignment.foobar", "droplet_id", regexp.MustCompile("[0-9]+")),
),
},
{
Config: testAccCheckDigitalOceanReservedIPAssignmentConfig_createBeforeDestroyReassign,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanReservedIPAttachmentExists("digitalocean_reserved_ip_assignment.foobar"),
resource.TestMatchResourceAttr(
"digitalocean_reserved_ip_assignment.foobar", "id", regexp.MustCompile("[0-9.]+")),
resource.TestMatchResourceAttr(
"digitalocean_reserved_ip_assignment.foobar", "droplet_id", regexp.MustCompile("[0-9]+")),
),
},
},
})
}
func testAccCheckDigitalOceanReservedIPAttachmentExists(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.Attributes["ip_address"] == "" {
return fmt.Errorf("No floating IP is set")
}
fipID := rs.Primary.Attributes["ip_address"]
dropletID, err := strconv.Atoi(rs.Primary.Attributes["droplet_id"])
if err != nil {
return err
}
client := testAccProvider.Meta().(*CombinedConfig).godoClient()
// Try to find the ReservedIP
foundReservedIP, _, err := client.ReservedIPs.Get(context.Background(), fipID)
if err != nil {
return err
}
if foundReservedIP.IP != fipID || foundReservedIP.Droplet.ID != dropletID {
return fmt.Errorf("wrong floating IP attachment found")
}
return nil
}
}
var testAccCheckDigitalOceanReservedIPAssignmentConfig = `
resource "digitalocean_reserved_ip" "foobar" {
region = "nyc3"
}
resource "digitalocean_droplet" "foobar" {
count = 2
name = "tf-acc-test-${count.index}"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
ipv6 = true
private_networking = true
}
resource "digitalocean_reserved_ip_assignment" "foobar" {
ip_address = digitalocean_reserved_ip.foobar.ip_address
droplet_id = digitalocean_droplet.foobar.0.id
}
`
var testAccCheckDigitalOceanReservedIPAssignmentReassign = `
resource "digitalocean_reserved_ip" "foobar" {
region = "nyc3"
}
resource "digitalocean_droplet" "foobar" {
count = 2
name = "tf-acc-test-${count.index}"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
ipv6 = true
private_networking = true
}
resource "digitalocean_reserved_ip_assignment" "foobar" {
ip_address = digitalocean_reserved_ip.foobar.ip_address
droplet_id = digitalocean_droplet.foobar.1.id
}
`
var testAccCheckDigitalOceanReservedIPAssignmentDeleteAssignment = `
resource "digitalocean_reserved_ip" "foobar" {
region = "nyc3"
}
resource "digitalocean_droplet" "foobar" {
count = 2
name = "tf-acc-test-${count.index}"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
ipv6 = true
private_networking = true
}
`
var testAccCheckDigitalOceanReservedIPAssignmentConfig_createBeforeDestroy = `
resource "digitalocean_droplet" "foobar" {
image = "centos-7-x64"
name = "tf-acc-test"
region = "nyc3"
size = "s-1vcpu-1gb"
lifecycle {
create_before_destroy = true
}
}
resource "digitalocean_reserved_ip" "foobar" {
region = "nyc3"
}
resource "digitalocean_reserved_ip_assignment" "foobar" {
ip_address = digitalocean_reserved_ip.foobar.id
droplet_id = digitalocean_droplet.foobar.id
lifecycle {
create_before_destroy = true
}
}
`
var testAccCheckDigitalOceanReservedIPAssignmentConfig_createBeforeDestroyReassign = `
resource "digitalocean_droplet" "foobar" {
image = "ubuntu-18-04-x64"
name = "tf-acc-test"
region = "nyc3"
size = "s-1vcpu-1gb"
lifecycle {
create_before_destroy = true
}
}
resource "digitalocean_reserved_ip" "foobar" {
region = "nyc3"
}
resource "digitalocean_reserved_ip_assignment" "foobar" {
ip_address = digitalocean_reserved_ip.foobar.id
droplet_id = digitalocean_droplet.foobar.id
lifecycle {
create_before_destroy = true
}
}
`

View File

@ -0,0 +1,208 @@
package digitalocean
import (
"context"
"fmt"
"regexp"
"testing"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
func init() {
resource.AddTestSweepers("digitalocean_reserved_ip", &resource.Sweeper{
Name: "digitalocean_reserved_ip",
F: testSweepReservedIPs,
})
}
func testSweepReservedIPs(region string) error {
meta, err := sharedConfigForRegion(region)
if err != nil {
return err
}
client := meta.(*CombinedConfig).godoClient()
ips, _, err := client.ReservedIPs.List(context.Background(), nil)
if err != nil {
return err
}
for _, ip := range ips {
if _, err := client.ReservedIPs.Delete(context.Background(), ip.IP); err != nil {
return err
}
}
return nil
}
func TestAccDigitalOceanReservedIP_Region(t *testing.T) {
var reservedIP godo.ReservedIP
expectedURNRegEx, _ := regexp.Compile(`do:reservedip:(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`)
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanReservedIPDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckDigitalOceanReservedIPConfig_region,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanReservedIPExists("digitalocean_reserved_ip.foobar", &reservedIP),
resource.TestCheckResourceAttr(
"digitalocean_reserved_ip.foobar", "region", "nyc3"),
resource.TestMatchResourceAttr("digitalocean_reserved_ip.foobar", "urn", expectedURNRegEx),
),
},
},
})
}
func TestAccDigitalOceanReservedIP_Droplet(t *testing.T) {
var reservedIP godo.ReservedIP
rInt := acctest.RandInt()
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanReservedIPDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckDigitalOceanReservedIPConfig_droplet(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanReservedIPExists("digitalocean_reserved_ip.foobar", &reservedIP),
resource.TestCheckResourceAttr(
"digitalocean_reserved_ip.foobar", "region", "nyc3"),
),
},
{
Config: testAccCheckDigitalOceanReservedIPConfig_Reassign(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanReservedIPExists("digitalocean_reserved_ip.foobar", &reservedIP),
resource.TestCheckResourceAttr(
"digitalocean_reserved_ip.foobar", "region", "nyc3"),
),
},
{
Config: testAccCheckDigitalOceanReservedIPConfig_Unassign(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanReservedIPExists("digitalocean_reserved_ip.foobar", &reservedIP),
resource.TestCheckResourceAttr(
"digitalocean_reserved_ip.foobar", "region", "nyc3"),
),
},
},
})
}
func testAccCheckDigitalOceanReservedIPDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*CombinedConfig).godoClient()
for _, rs := range s.RootModule().Resources {
if rs.Type != "digitalocean_reserved_ip" {
continue
}
// Try to find the key
_, _, err := client.ReservedIPs.Get(context.Background(), rs.Primary.ID)
if err == nil {
return fmt.Errorf("Reserved IP still exists")
}
}
return nil
}
func testAccCheckDigitalOceanReservedIPExists(n string, reservedIP *godo.ReservedIP) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No Record ID is set")
}
client := testAccProvider.Meta().(*CombinedConfig).godoClient()
// Try to find the ReservedIP
foundReservedIP, _, err := client.ReservedIPs.Get(context.Background(), rs.Primary.ID)
if err != nil {
return err
}
if foundReservedIP.IP != rs.Primary.ID {
return fmt.Errorf("Record not found")
}
*reservedIP = *foundReservedIP
return nil
}
}
var testAccCheckDigitalOceanReservedIPConfig_region = `
resource "digitalocean_reserved_ip" "foobar" {
region = "nyc3"
}`
func testAccCheckDigitalOceanReservedIPConfig_droplet(rInt int) string {
return fmt.Sprintf(`
resource "digitalocean_droplet" "foobar" {
name = "tf-acc-test-%d"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
ipv6 = true
private_networking = true
}
resource "digitalocean_reserved_ip" "foobar" {
droplet_id = digitalocean_droplet.foobar.id
region = digitalocean_droplet.foobar.region
}`, rInt)
}
func testAccCheckDigitalOceanReservedIPConfig_Reassign(rInt int) string {
return fmt.Sprintf(`
resource "digitalocean_droplet" "baz" {
name = "tf-acc-test-%d"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
ipv6 = true
private_networking = true
}
resource "digitalocean_reserved_ip" "foobar" {
droplet_id = digitalocean_droplet.baz.id
region = digitalocean_droplet.baz.region
}`, rInt)
}
func testAccCheckDigitalOceanReservedIPConfig_Unassign(rInt int) string {
return fmt.Sprintf(`
resource "digitalocean_droplet" "baz" {
name = "tf-acc-test-%d"
size = "s-1vcpu-1gb"
image = "centos-7-x64"
region = "nyc3"
ipv6 = true
private_networking = true
}
resource "digitalocean_reserved_ip" "foobar" {
region = "nyc3"
}`, rInt)
}

View File

@ -4,6 +4,8 @@ page_title: "DigitalOcean: digitalocean_floating_ip"
# digitalocean_floating_ip
~> **Deprecated:** DigitalOcean Floating IPs have been renamed reserved IPs. This data source will be removed in a future release. Please use `digitalocean_reserved_ip` instead.
Get information on a floating ip. This data source provides the region and Droplet id
as configured on your DigitalOcean account. This is useful if the floating IP
in question is not managed by Terraform or you need to find the Droplet the IP is

View File

@ -0,0 +1,42 @@
---
page_title: "DigitalOcean: digitalocean_reserved_ip"
---
# digitalocean_reserved_ip
Get information on a reserved IP. This data source provides the region and Droplet id
as configured on your DigitalOcean account. This is useful if the reserved IP
in question is not managed by Terraform or you need to find the Droplet the IP is
attached to.
An error is triggered if the provided reserved IP does not exist.
## Example Usage
Get the reserved IP:
```hcl
variable "public_ip" {}
data "digitalocean_reserved_ip" "example" {
ip_address = var.public_ip
}
output "fip_output" {
value = data.digitalocean_reserved_ip.example.droplet_id
}
```
## Argument Reference
The following arguments are supported:
* `ip_address` - (Required) The allocated IP address of the specific reserved IP to retrieve.
## Attributes Reference
The following attributes are exported:
* `region`: The region that the reserved IP is reserved to.
* `urn`: The uniform resource name of the reserved IP.
* `droplet_id`: The Droplet id that the reserved IP has been assigned to.

View File

@ -4,6 +4,8 @@ page_title: "DigitalOcean: digitalocean_floating_ip"
# digitalocean\_floating_ip
~> **Deprecated:** DigitalOcean Floating IPs have been renamed reserved IPs. This resource will be removed in a future release. Please use `digitalocean_reserved_ip` instead.
Provides a DigitalOcean Floating IP to represent a publicly-accessible static IP addresses that can be mapped to one of your Droplets.
~> **NOTE:** Floating IPs can be assigned to a Droplet either directly on the `digitalocean_floating_ip` resource by setting a `droplet_id` or using the `digitalocean_floating_ip_assignment` resource, but the two cannot be used together.

View File

@ -4,6 +4,8 @@ page_title: "DigitalOcean: digitalocean_floating_ip_assignment"
# digitalocean\_floating_ip_assignment
~> **Deprecated:** DigitalOcean Floating IPs have been renamed reserved IPs. This resource will be removed in a future release. Please use `digitalocean_reserved_ip_assignment` instead.
Provides a resource for assigning an existing DigitalOcean Floating IP to a Droplet. This
makes it easy to provision floating IP addresses that are not tied to the lifecycle of your
Droplet.

View File

@ -0,0 +1,49 @@
---
page_title: "DigitalOcean: digitalocean_reserved_ip"
---
# digitalocean\_reserved_ip
Provides a DigitalOcean reserved IP to represent a publicly-accessible static IP addresses that can be mapped to one of your Droplets.
~> **NOTE:** Reserved IPs can be assigned to a Droplet either directly on the `digitalocean_reserved_ip` resource by setting a `droplet_id` or using the `digitalocean_reserved_ip_assignment` resource, but the two cannot be used together.
## Example Usage
```hcl
resource "digitalocean_droplet" "example" {
name = "example"
size = "s-1vcpu-1gb"
image = "ubuntu-22-04-x64"
region = "nyc3"
ipv6 = true
private_networking = true
}
resource "digitalocean_reserved_ip" "example" {
droplet_id = digitalocean_droplet.example.id
region = digitalocean_droplet.example.region
}
```
## Argument Reference
The following arguments are supported:
* `region` - (Required) The region that the reserved IP is reserved to.
* `droplet_id` - (Optional) The ID of Droplet that the reserved IP will be assigned to.
## Attributes Reference
The following attributes are exported:
* `ip_address` - The IP Address of the resource
* `urn` - The uniform resource name of the reserved ip
## Import
Reserved IPs can be imported using the `ip`, e.g.
```
terraform import digitalocean_reserved_ip.myip 192.168.0.1
```

View File

@ -0,0 +1,47 @@
---
page_title: "DigitalOcean: digitalocean_reserved_ip_assignment"
---
# digitalocean\_reserved_ip_assignment
Provides a resource for assigning an existing DigitalOcean reserved IP to a Droplet. This
makes it easy to provision reserved IP addresses that are not tied to the lifecycle of your
Droplet.
## Example Usage
```hcl
resource "digitalocean_reserved_ip" "example" {
region = "nyc3"
}
resource "digitalocean_droplet" "example" {
name = "baz"
size = "s-1vcpu-1gb"
image = "ubuntu-22-04-x64"
region = "nyc3"
ipv6 = true
private_networking = true
}
resource "digitalocean_reserved_ip_assignment" "example" {
ip_address = digitalocean_reserved_ip.example.ip_address
droplet_id = digitalocean_droplet.example.id
}
```
## Argument Reference
The following arguments are supported:
* `ip_address` - (Required) The reserved IP to assign to the Droplet.
* `droplet_id` - (Optional) The ID of Droplet that the reserved IP will be assigned to.
## Import
Reserved IP assignments can be imported using the reserved IP itself and the `id` of
the Droplet joined with a comma. For example:
```
terraform import digitalocean_reserved_ip_assignment.foobar 192.0.2.1,123456
```

2
go.mod
View File

@ -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.79.0
github.com/digitalocean/godo v1.81.0
github.com/hashicorp/awspolicyequivalence v1.5.0
github.com/hashicorp/go-uuid v1.0.2
github.com/hashicorp/go-version v1.3.0

4
go.sum
View File

@ -81,8 +81,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.79.0 h1:Uwef1rh/QLnADR1pR6oyZfqA5zLqondiSnWFPwDzbog=
github.com/digitalocean/godo v1.79.0/go.mod h1:BPCqvwbjbGqxuUnIKB4EvS/AX7IDnNmt5fwvIkWo+ew=
github.com/digitalocean/godo v1.81.0 h1:sjb3fOfPfSlUQUK22E87BcI8Zx2qtnF7VUCCO4UK3C8=
github.com/digitalocean/godo v1.81.0/go.mod h1:BPCqvwbjbGqxuUnIKB4EvS/AX7IDnNmt5fwvIkWo+ew=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

View File

@ -1,5 +1,17 @@
# Change Log
## [v1.81.0] - 2022-06-15
- #532 - @senorprogrammer - Add support for Reserved IP addresses
- #538 - @bentranter - util: update droplet create example
- #537 - @rpmoore - Adding project_id to databases
- #536 - @andrewsomething - account: Now may include info on current team.
- #535 - @ElanHasson - APPS-5636 Update App Platform for functions and Starter Tier App Proposals.
## [v1.80.0] - 2022-05-23
- #533 - @ElanHasson - APPS-5636 - App Platform updates
## [v1.79.0] - 2022-04-29
- #530 - @anitgandhi - monitoring: alerts for Load Balancers TLS conns/s utilization

View File

@ -22,14 +22,22 @@ var _ AccountService = &AccountServiceOp{}
// Account represents a DigitalOcean Account
type Account struct {
DropletLimit int `json:"droplet_limit,omitempty"`
FloatingIPLimit int `json:"floating_ip_limit,omitempty"`
VolumeLimit int `json:"volume_limit,omitempty"`
Email string `json:"email,omitempty"`
UUID string `json:"uuid,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
Status string `json:"status,omitempty"`
StatusMessage string `json:"status_message,omitempty"`
DropletLimit int `json:"droplet_limit,omitempty"`
FloatingIPLimit int `json:"floating_ip_limit,omitempty"`
ReservedIPLimit int `json:"reserved_ip_limit,omitempty"`
VolumeLimit int `json:"volume_limit,omitempty"`
Email string `json:"email,omitempty"`
UUID string `json:"uuid,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
Status string `json:"status,omitempty"`
StatusMessage string `json:"status_message,omitempty"`
Team *TeamInfo `json:"team,omitempty"`
}
// TeamInfo contains information about the currently team context.
type TeamInfo struct {
Name string `json:"name,omitempty"`
UUID string `json:"uuid,omitempty"`
}
type accountRoot struct {

View File

@ -117,7 +117,7 @@ const (
AppAlertSpecOperator_LessThan AppAlertSpecOperator = "LESS_THAN"
)
// AppAlertSpecRule - CPU_UTILIZATION: Represents CPU for a given container instance. Only applicable at the component level. - MEM_UTILIZATION: Represents RAM for a given container instance. Only applicable at the component level. - RESTART_COUNT: Represents restart count for a given container instance. Only applicable at the component level. - DEPLOYMENT_FAILED: Represents whether a deployment has failed. Only applicable at the app level. - DEPLOYMENT_LIVE: Represents whether a deployment has succeeded. Only applicable at the app level. - DOMAIN_FAILED: Represents whether a domain configuration has failed. Only applicable at the app level. - DOMAIN_LIVE: Represents whether a domain configuration has succeeded. Only applicable at the app level. - FUNCTIONS_ACTIVATION_COUNT: Represents an activation count for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_DURATION_MS: Represents the average duration for function runtimes. Only applicable to functions components. - FUNCTIONS_ERROR_RATE_PER_MINUTE: Represents an error rate per minute for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_WAIT_TIME_MS: Represents the average wait time for functions. Only applicable to functions components. - FUNCTIONS_ERROR_COUNT: Represents an error count for a given functions instance. Only applicable to functions components.
// AppAlertSpecRule - CPU_UTILIZATION: Represents CPU for a given container instance. Only applicable at the component level. - MEM_UTILIZATION: Represents RAM for a given container instance. Only applicable at the component level. - RESTART_COUNT: Represents restart count for a given container instance. Only applicable at the component level. - DEPLOYMENT_FAILED: Represents whether a deployment has failed. Only applicable at the app level. - DEPLOYMENT_LIVE: Represents whether a deployment has succeeded. Only applicable at the app level. - DOMAIN_FAILED: Represents whether a domain configuration has failed. Only applicable at the app level. - DOMAIN_LIVE: Represents whether a domain configuration has succeeded. Only applicable at the app level. - FUNCTIONS_ACTIVATION_COUNT: Represents an activation count for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_DURATION_MS: Represents the average duration for function runtimes. Only applicable to functions components. - FUNCTIONS_ERROR_RATE_PER_MINUTE: Represents an error rate per minute for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_WAIT_TIME_MS: Represents the average wait time for functions. Only applicable to functions components. - FUNCTIONS_ERROR_COUNT: Represents an error count for a given functions instance. Only applicable to functions components. - FUNCTIONS_GB_RATE_PER_SECOND: Represents the rate of memory consumption (GB x seconds) for functions. Only applicable to functions components.
type AppAlertSpecRule string
// List of AppAlertSpecRule
@ -135,6 +135,7 @@ const (
AppAlertSpecRule_FunctionsErrorRatePerMinute AppAlertSpecRule = "FUNCTIONS_ERROR_RATE_PER_MINUTE"
AppAlertSpecRule_FunctionsAverageWaitTimeMs AppAlertSpecRule = "FUNCTIONS_AVERAGE_WAIT_TIME_MS"
AppAlertSpecRule_FunctionsErrorCount AppAlertSpecRule = "FUNCTIONS_ERROR_COUNT"
AppAlertSpecRule_FunctionsGBRatePerSecond AppAlertSpecRule = "FUNCTIONS_GB_RATE_PER_SECOND"
)
// AppAlertSpecWindow the model 'AppAlertSpecWindow'
@ -189,7 +190,7 @@ type AppDomainSpec struct {
// Optional. If the domain uses DigitalOcean DNS and you would like App Platform to automatically manage it for you, set this to the name of the domain on your account. For example, If the domain you are adding is `app.domain.com`, the zone could be `domain.com`.
Zone string `json:"zone,omitempty"`
Certificate string `json:"certificate,omitempty"`
// Optional. The minimum version of TLS a client application can use to access resources for the domain. Must be one of the following values wrapped within quotations: `\"1.0\"`, `\"1.1\"`, `\"1.2\"`, or `\"1.3\"`.
// Optional. The minimum version of TLS a client application can use to access resources for the domain. Must be one of the following values wrapped within quotations: `\"1.2\"` or `\"1.3\"`.
MinimumTLSVersion string `json:"minimum_tls_version,omitempty"`
}
@ -323,7 +324,7 @@ type AppLogDestinationSpecPapertrail struct {
type AppRouteSpec struct {
// An HTTP path prefix. Paths must start with / and must be unique across all components within an app.
Path string `json:"path,omitempty"`
// An optional flag to preserve the path that is forwarded to the backend service. By default, the HTTP request path will be trimmed from the left when forwarded to the component. For example, a component with `path=/api` will have requests to `/api/list` trimmed to `/list`. If this value is `true`, the path will remain `/api/list`.
// An optional flag to preserve the path that is forwarded to the backend service. By default, the HTTP request path will be trimmed from the left when forwarded to the component. For example, a component with `path=/api` will have requests to `/api/list` trimmed to `/list`. If this value is `true`, the path will remain `/api/list`. Note: this is not applicable for Functions Components.
PreservePathPrefix bool `json:"preserve_path_prefix,omitempty"`
}
@ -387,14 +388,15 @@ type AppServiceSpecHealthCheck struct {
type AppSpec struct {
// The name of the app. Must be unique across all apps in the same account.
Name string `json:"name"`
// Workloads which expose publicy-accessible HTTP services.
// Workloads which expose publicly-accessible HTTP services.
Services []*AppServiceSpec `json:"services,omitempty"`
// Content which can be rendered to static web assets.
StaticSites []*AppStaticSiteSpec `json:"static_sites,omitempty"`
// Workloads which do not expose publicly-accessible HTTP services.
Workers []*AppWorkerSpec `json:"workers,omitempty"`
// Pre and post deployment workloads which do not expose publicly-accessible HTTP routes.
Jobs []*AppJobSpec `json:"jobs,omitempty"`
Jobs []*AppJobSpec `json:"jobs,omitempty"`
// Workloads which expose publicly-accessible HTTP services via Functions Components.
Functions []*AppFunctionsSpec `json:"functions,omitempty"`
// Database instances which can provide persistence to workloads within the application.
Databases []*AppDatabaseSpec `json:"databases,omitempty"`
@ -510,7 +512,7 @@ type AppCORSPolicy struct {
AllowHeaders []string `json:"allow_headers,omitempty"`
// The set of HTTP response headers that browsers are allowed to access. This configures the Access-Control-Expose-Headers header.
ExposeHeaders []string `json:"expose_headers,omitempty"`
// An optional duration specifiying how long browsers can cache the results of a preflight request. This configures the Access-Control-Max-Age header. Example: `5h30m`.
// An optional duration specifying how long browsers can cache the results of a preflight request. This configures the Access-Control-Max-Age header. Example: `5h30m`.
MaxAge string `json:"max_age,omitempty"`
// Whether browsers should expose the response to the client-side JavaScript code when the request's credentials mode is `include`. This configures the Access-Control-Allow-Credentials header.
AllowCredentials bool `json:"allow_credentials,omitempty"`
@ -803,20 +805,29 @@ type AppProposeRequest struct {
// AppProposeResponse struct for AppProposeResponse
type AppProposeResponse struct {
AppIsStatic bool `json:"app_is_static,omitempty"`
// Deprecated. Please use AppIsStarter instead.
AppIsStatic bool `json:"app_is_static,omitempty"`
// Indicates whether the app name is available.
AppNameAvailable bool `json:"app_name_available,omitempty"`
// If the app name is unavailable, this will be set to a suggested available name.
AppNameSuggestion string `json:"app_name_suggestion,omitempty"`
// The number of existing static apps the account has.
// Deprecated. Please use ExistingStarterApps instead.
ExistingStaticApps string `json:"existing_static_apps,omitempty"`
// The maximum number of free static apps the account can have. Any additional static apps will be charged for.
// Deprecated. Please use MaxFreeStarterApps instead.
MaxFreeStaticApps string `json:"max_free_static_apps,omitempty"`
Spec *AppSpec `json:"spec,omitempty"`
AppCost float32 `json:"app_cost,omitempty"`
// The monthly cost of the proposed app in USD using the next pricing plan tier. For example, if you propose an app that uses the Basic tier, the `app_tier_upgrade_cost` field displays the monthly cost of the app if it were to use the Professional tier. If the proposed app already uses the most expensive tier, the field is empty.
// The monthly cost of the proposed app in USD.
AppCost float32 `json:"app_cost,omitempty"`
// The monthly cost of the proposed app in USD using the next pricing plan tier. For example, if you propose an app that uses the Basic tier, the `AppTierUpgradeCost` field displays the monthly cost of the app if it were to use the Professional tier. If the proposed app already uses the most expensive tier, the field is empty.
AppTierUpgradeCost float32 `json:"app_tier_upgrade_cost,omitempty"`
// The monthly cost of the proposed app in USD using the previous pricing plan tier. For example, if you propose an app that uses the Professional tier, the `app_tier_downgrade_cost` field displays the monthly cost of the app if it were to use the Basic tier. If the proposed app already uses the lest expensive tier, the field is empty.
// The monthly cost of the proposed app in USD using the previous pricing plan tier. For example, if you propose an app that uses the Professional tier, the `AppTierDowngradeCost` field displays the monthly cost of the app if it were to use the Basic tier. If the proposed app already uses the lest expensive tier, the field is empty.
AppTierDowngradeCost float32 `json:"app_tier_downgrade_cost,omitempty"`
// The number of existing starter tier apps the account has.
ExistingStarterApps string `json:"existing_starter_apps,omitempty"`
// The maximum number of free starter apps the account can have. Any additional starter apps will be charged for. These include apps with only static sites, functions, and databases.
MaxFreeStarterApps string `json:"max_free_starter_apps,omitempty"`
// Indicates whether the app is a starter tier app.
AppIsStarter bool `json:"app_is_starter,omitempty"`
}
// AppRegion struct for AppRegion

View File

@ -154,6 +154,7 @@ type Database struct {
CreatedAt time.Time `json:"created_at,omitempty"`
PrivateNetworkUUID string `json:"private_network_uuid,omitempty"`
Tags []string `json:"tags,omitempty"`
ProjectID string `json:"project_id,omitempty"`
}
// DatabaseCA represents a database ca.
@ -217,6 +218,7 @@ type DatabaseCreateRequest struct {
PrivateNetworkUUID string `json:"private_network_uuid"`
Tags []string `json:"tags,omitempty"`
BackupRestore *DatabaseBackupRestore `json:"backup_restore,omitempty"`
ProjectID string `json:"project_id"`
}
// DatabaseResizeRequest can be used to initiate a database resize operation.

View File

@ -20,7 +20,7 @@ import (
)
const (
libraryVersion = "1.79.0"
libraryVersion = "1.81.0"
defaultBaseURL = "https://api.digitalocean.com/"
userAgent = "godo/" + libraryVersion
mediaType = "application/json"
@ -64,6 +64,8 @@ type Client struct {
Sizes SizesService
FloatingIPs FloatingIPsService
FloatingIPActions FloatingIPActionsService
ReservedIPs ReservedIPsService
ReservedIPActions ReservedIPActionsService
Snapshots SnapshotsService
Storage StorageService
StorageActions StorageActionsService
@ -219,6 +221,8 @@ func NewClient(httpClient *http.Client) *Client {
c.Firewalls = &FirewallsServiceOp{client: c}
c.FloatingIPs = &FloatingIPsServiceOp{client: c}
c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
c.ReservedIPs = &ReservedIPsServiceOp{client: c}
c.ReservedIPActions = &ReservedIPActionsServiceOp{client: c}
c.Images = &ImagesServiceOp{client: c}
c.ImageActions = &ImageActionsServiceOp{client: c}
c.Invoices = &InvoicesServiceOp{client: c}

145
vendor/github.com/digitalocean/godo/reserved_ips.go generated vendored Normal file
View File

@ -0,0 +1,145 @@
package godo
import (
"context"
"fmt"
"net/http"
)
const resourceType = "ReservedIP"
const reservedIPsBasePath = "v2/reserved_ips"
// ReservedIPsService is an interface for interfacing with the reserved IPs
// endpoints of the Digital Ocean API.
// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Reserved-IPs
type ReservedIPsService interface {
List(context.Context, *ListOptions) ([]ReservedIP, *Response, error)
Get(context.Context, string) (*ReservedIP, *Response, error)
Create(context.Context, *ReservedIPCreateRequest) (*ReservedIP, *Response, error)
Delete(context.Context, string) (*Response, error)
}
// ReservedIPsServiceOp handles communication with the reserved IPs related methods of the
// DigitalOcean API.
type ReservedIPsServiceOp struct {
client *Client
}
var _ ReservedIPsService = &ReservedIPsServiceOp{}
// ReservedIP represents a Digital Ocean reserved IP.
type ReservedIP struct {
Region *Region `json:"region"`
Droplet *Droplet `json:"droplet"`
IP string `json:"ip"`
}
func (f ReservedIP) String() string {
return Stringify(f)
}
// URN returns the reserved IP in a valid DO API URN form.
func (f ReservedIP) URN() string {
return ToURN(resourceType, f.IP)
}
type reservedIPsRoot struct {
ReservedIPs []ReservedIP `json:"reserved_ips"`
Links *Links `json:"links"`
Meta *Meta `json:"meta"`
}
type reservedIPRoot struct {
ReservedIP *ReservedIP `json:"reserved_ip"`
Links *Links `json:"links,omitempty"`
}
// ReservedIPCreateRequest represents a request to create a reserved IP.
// Specify DropletID to assign the reserved IP to a Droplet or Region
// to reserve it to the region.
type ReservedIPCreateRequest struct {
Region string `json:"region,omitempty"`
DropletID int `json:"droplet_id,omitempty"`
}
// List all reserved IPs.
func (r *ReservedIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]ReservedIP, *Response, error) {
path := reservedIPsBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := r.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(reservedIPsRoot)
resp, err := r.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.ReservedIPs, resp, err
}
// Get an individual reserved IP.
func (r *ReservedIPsServiceOp) Get(ctx context.Context, ip string) (*ReservedIP, *Response, error) {
path := fmt.Sprintf("%s/%s", reservedIPsBasePath, ip)
req, err := r.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(reservedIPRoot)
resp, err := r.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root.ReservedIP, resp, err
}
// Create a reserved IP. If the DropletID field of the request is not empty,
// the reserved IP will also be assigned to the droplet.
func (r *ReservedIPsServiceOp) Create(ctx context.Context, createRequest *ReservedIPCreateRequest) (*ReservedIP, *Response, error) {
path := reservedIPsBasePath
req, err := r.client.NewRequest(ctx, http.MethodPost, path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(reservedIPRoot)
resp, err := r.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.ReservedIP, resp, err
}
// Delete a reserved IP.
func (r *ReservedIPsServiceOp) Delete(ctx context.Context, ip string) (*Response, error) {
path := fmt.Sprintf("%s/%s", reservedIPsBasePath, ip)
req, err := r.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
resp, err := r.client.Do(ctx, req, nil)
return resp, err
}

View File

@ -0,0 +1,109 @@
package godo
import (
"context"
"fmt"
"net/http"
)
// ReservedIPActionsService is an interface for interfacing with the
// reserved IPs actions endpoints of the Digital Ocean API.
// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Reserved-IP-Actions
type ReservedIPActionsService interface {
Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error)
Unassign(ctx context.Context, ip string) (*Action, *Response, error)
Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error)
List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error)
}
// ReservedIPActionsServiceOp handles communication with the reserved IPs
// action related methods of the DigitalOcean API.
type ReservedIPActionsServiceOp struct {
client *Client
}
// Assign a reserved IP to a droplet.
func (s *ReservedIPActionsServiceOp) Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error) {
request := &ActionRequest{
"type": "assign",
"droplet_id": dropletID,
}
return s.doAction(ctx, ip, request)
}
// Unassign a rerserved IP from the droplet it is currently assigned to.
func (s *ReservedIPActionsServiceOp) Unassign(ctx context.Context, ip string) (*Action, *Response, error) {
request := &ActionRequest{"type": "unassign"}
return s.doAction(ctx, ip, request)
}
// Get an action for a particular reserved IP by id.
func (s *ReservedIPActionsServiceOp) Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error) {
path := fmt.Sprintf("%s/%d", reservedIPActionPath(ip), actionID)
return s.get(ctx, path)
}
// List the actions for a particular reserved IP.
func (s *ReservedIPActionsServiceOp) List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error) {
path := reservedIPActionPath(ip)
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
return s.list(ctx, path)
}
func (s *ReservedIPActionsServiceOp) doAction(ctx context.Context, ip string, request *ActionRequest) (*Action, *Response, error) {
path := reservedIPActionPath(ip)
req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root.Event, resp, err
}
func (s *ReservedIPActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root.Event, resp, err
}
func (s *ReservedIPActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionsRoot)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Actions, resp, err
}
func reservedIPActionPath(ip string) string {
return fmt.Sprintf("%s/%s/actions", reservedIPsBasePath, ip)
}

2
vendor/modules.txt vendored
View File

@ -64,7 +64,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.79.0
# github.com/digitalocean/godo v1.81.0
## explicit; go 1.18
github.com/digitalocean/godo
github.com/digitalocean/godo/metrics