Skip to content

Commit

Permalink
SYSENG-1335: reserve available addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
Mario Schäfer committed Jun 6, 2024
1 parent 907c2e6 commit f53c034
Show file tree
Hide file tree
Showing 10 changed files with 413 additions and 99 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ If the change isn't user-facing but still relevant enough for a changelog entry,
* (internal)? scope: short description (#pr, @author)
-->

### Changed
* resource/anxcloud_ip_address: reserve an available address based on filters (#163, @anx-mschaefer)

## [0.6.2] - 2024-05-28

### Added
Expand Down
71 changes: 56 additions & 15 deletions anxcloud/data_source_ip_address.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,62 @@ Retrieves an IP address.
- When using the address argument, only IP addresses unique to the scope of your access token for Anexia Cloud can be retrieved. You can however get a unique result by specifying the related VLAN or network prefix.
`,
ReadContext: dataSourceIPAddressRead,
Schema: schemaWith(schemaIPAddress(),
fieldsExactlyOneOf("id", "address"),
fieldsOptional(
"vlan_id",
"network_prefix_id",
),
fieldsComputed(
"description_customer",
"description_internal",
"role",
"version",
"status",
"organization",
),
),
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: identifierDescription,
ExactlyOneOf: []string{"id", "address"},
},
"network_prefix_id": {
Type: schema.TypeString,
Optional: true,
Description: "Identifier of the related network prefix.",
},
"address": {
Type: schema.TypeString,
Optional: true,
Description: "IP address.",
},
"vlan_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "The associated VLAN identifier.",
},

"description_customer": {
Type: schema.TypeString,
Computed: true,
Description: "Additional customer description.",
},
"description_internal": {
Type: schema.TypeString,
Computed: true,
Description: "Internal description.",
},
"role": {
Type: schema.TypeString,
Computed: true,
Description: "Role of the IP address",
},
"organization": {
Type: schema.TypeString,
Computed: true,
Description: "Customer of yours. Reseller only.",
},
"version": {
Type: schema.TypeInt,
Computed: true,
Description: "IP version.",
},
"status": {
Type: schema.TypeString,
Computed: true,
Description: "Status of the IP address",
},
},
}
}

Expand Down
10 changes: 10 additions & 0 deletions anxcloud/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ func handleNotFoundError(err error) error {
return err
}

// isLegacyNotFoundError returns true, if the provided [err]
// is a "Not Found" status error returned by the legacy api client
func isLegacyNotFoundError(err error) bool {
var respErr *client.ResponseError
if errors.As(err, &respErr) && respErr.ErrorData.Code == http.StatusNotFound {
return true
}
return false
}

func apiFromProviderConfig(m interface{}) api.API {
return m.(providerContext).api
}
154 changes: 150 additions & 4 deletions anxcloud/resource_ip_address.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"go.anx.io/go-anxcloud/pkg/ipam/address"
"go.anx.io/go-anxcloud/pkg/vlan"
)

const (
Expand All @@ -19,7 +20,8 @@ const (

func resourceIPAddress() *schema.Resource {
return &schema.Resource{
Description: "This resource allows you to create and configure IP addresses.",
Description: "This resource allows you to create and configure IP addresses. " +
"Addresses created without the `address` attribute will expire if the reservation period exceeds before assigned to a VM.",
CreateContext: tagsMiddlewareCreate(resourceIPAddressCreate),
ReadContext: tagsMiddlewareRead(resourceIPAddressRead),
UpdateContext: tagsMiddlewareUpdate(resourceIPAddressUpdate),
Expand All @@ -33,18 +35,144 @@ func resourceIPAddress() *schema.Resource {
Update: schema.DefaultTimeout(10 * time.Minute),
Delete: schema.DefaultTimeout(5 * time.Minute),
},
Schema: withTagsAttribute(schemaIPAddress()),
Schema: withTagsAttribute(
map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
Description: identifierDescription,
},
"address": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Description: "IP address.",
ExactlyOneOf: []string{"address", "vlan_id"},
RequiredWith: []string{"network_prefix_id"}, // network_prefix_id is required if address is set
},
"vlan_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Description: "The associated VLAN identifier.",
ExactlyOneOf: []string{"address", "vlan_id"},
},
"network_prefix_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Description: "Identifier of the related network prefix.",
},
"version": {
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
Description: "IP version.",
},
"description_customer": {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "Additional customer description.",
ConflictsWith: []string{"vlan_id"},
},
"description_internal": {
Type: schema.TypeString,
Computed: true,
Description: "Internal description.",
},
"role": {
Type: schema.TypeString,
Optional: true,
Default: "Default",
Description: "Role of the IP address",
ConflictsWith: []string{"vlan_id"},
},
"organization": {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "Customer of yours. Reseller only.",
ConflictsWith: []string{"vlan_id"},
},
"status": {
Type: schema.TypeString,
Computed: true,
Description: "Status of the IP address",
},
"reservation_period_seconds": {
Type: schema.TypeInt,
Optional: true,
ConflictsWith: []string{"address"},
Description: "Period for the requested reservation in seconds. Defaults to 30 minutes if not set.",
},
},
),
}
}

func resourceIPAddressCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
c := m.(providerContext).legacyClient
a := address.NewAPI(c)
prefixID := d.Get("network_prefix_id").(string)
v := vlan.NewAPI(c)

// if `vlan_id` was provided, we will perform an ip reservation
if vlanID, ok := d.GetOk("vlan_id"); ok {
vlan, err := v.Get(ctx, vlanID.(string))
if err != nil {
return diag.FromErr(fmt.Errorf("fetch vlan: %w", err))
} else if len(vlan.Locations) < 1 {
return diag.Errorf("vlan has no locations specified")
}

reserveOpts := address.ReserveRandom{
VlanID: vlan.Identifier,
LocationID: vlan.Locations[0].Identifier,
Count: 1,
}

if reservationPeriodSeconds, ok := d.GetOk("reservation_period_seconds"); ok {
reserveOpts.ReservationPeriod = uint(reservationPeriodSeconds.(int))
}

if prefixID, ok := d.GetOk("network_prefix_id"); ok {
reserveOpts.PrefixID = prefixID.(string)
} else if ipVersion, ok := d.GetOk("version"); ok {
reserveOpts.IPVersion = address.IPReserveVersionLimit(ipVersion.(int))
}

reserveSummary, err := a.ReserveRandom(ctx, reserveOpts)
if err != nil {
return diag.FromErr(fmt.Errorf("reserve address: %w", err))
} else if len(reserveSummary.Data) < 1 {
return diag.Errorf("reserve endpoint didn't return any addresses")
}

d.SetId(reserveSummary.Data[0].ID)

if err := retry.RetryContext(ctx, d.Timeout(schema.TimeoutCreate), func() *retry.RetryError {
if addr, err := a.Get(ctx, reserveSummary.Data[0].ID); err != nil {
return retry.NonRetryableError(err)
} else if addr.VLANID == "" {
return retry.RetryableError(fmt.Errorf("VLAN id is not set"))
}

return nil
}); err != nil {
return diag.FromErr(fmt.Errorf("wait for VLAN to be set on address resource: %w", err))
}

return resourceIPAddressRead(ctx, d, m)
}

// create specific address
def := address.Create{
PrefixID: prefixID,
Address: d.Get("address").(string),
PrefixID: d.Get("network_prefix_id").(string),
DescriptionCustomer: d.Get("description_customer").(string),
Role: d.Get("role").(string),
Organization: d.Get("organization").(string),
Expand Down Expand Up @@ -139,6 +267,24 @@ func resourceIPAddressDelete(ctx context.Context, d *schema.ResourceData, m inte
c := m.(providerContext).legacyClient
a := address.NewAPI(c)

if addr, err := a.Get(ctx, d.Id()); isLegacyNotFoundError(err) {
// handle not found error by just deleting the resource
d.SetId("")
return nil
} else if err != nil {
// return unhandled error
return diag.FromErr(err)
} else if addr.DescriptionInternal == "reserved" {
d.SetId("")
var diags diag.Diagnostics
diags = append(diags, diag.Diagnostic{
Severity: diag.Warning,
Summary: "Could not delete reserved address",
Detail: "Reserved addresses cannot be deleted manually. They'll expire eventually.",
})
return diags
}

err := a.Delete(ctx, d.Id())
if err != nil {
if err := handleNotFoundError(err); err != nil {
Expand Down
Loading

0 comments on commit f53c034

Please sign in to comment.