Skip to content

Commit

Permalink
fix: Update CRUD for metal port to support timeouts (#377)
Browse files Browse the repository at this point in the history
- Adds support for timeout in Create, update and delete operation.
- Adds acceptance tests

Fixes #357

---------

Signed-off-by: Ayush Rangwala <[email protected]>
  • Loading branch information
aayushrangwala authored Sep 27, 2023
1 parent 352a76c commit 97e8ea6
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 52 deletions.
11 changes: 11 additions & 0 deletions docs/resources/equinix_metal_port.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ ports.
attached VLANs (from `vlan_ids` parameter).
* `reset_on_delete` - (Optional) Behavioral setting to reset the port to default settings (layer3 bonded mode without any vlan attached) before delete/destroy.

### Timeouts

The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/configuration/resources#operation-timeouts) for certain actions:

These timeout includes the time to disbond, convert to L2/L3, bond and update native vLAN.

* `create` - (Defaults to 30 mins) Used when creating the Port.
* `update` - (Defaults to 30 mins) Used when updating the Port.
* `delete` - (Defaults to 30 mins) Used when deleting the Port.


## Attributes Reference

In addition to all arguments above, the following attributes are exported:
Expand Down
2 changes: 1 addition & 1 deletion equinix/data_source_metal_port.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

func dataSourceMetalPort() *schema.Resource {
return &schema.Resource{
Read: resourceMetalPortRead,
ReadWithoutTimeout: diagnosticsWrapper(resourceMetalPortRead),

Schema: map[string]*schema.Schema{
"port_id": {
Expand Down
72 changes: 39 additions & 33 deletions equinix/port_helpers.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package equinix

import (
"context"
"fmt"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/packethost/packngo"
"github.com/pkg/errors"
)

type portVlanAction func(*packngo.PortAssignRequest) (*packngo.Port, *packngo.Response, error)

type ClientPortResource struct {
Client *packngo.Client
Port *packngo.Port
Expand Down Expand Up @@ -126,20 +127,7 @@ func specifiedVlanIds(d *schema.ResourceData) []string {
return []string{}
}

func processVlansOnPort(port *packngo.Port, vlanIds []string, f portVlanAction) (*packngo.Port, error) {
par := packngo.PortAssignRequest{PortID: port.ID}
for _, vId := range vlanIds {
par.VirtualNetworkID = vId
var err error
port, _, err = f(&par)
if err != nil {
return nil, err
}
}
return port, nil
}

func batchVlans(removeOnly bool) func(*ClientPortResource) error {
func batchVlans(ctx context.Context, start time.Time, removeOnly bool) func(*ClientPortResource) error {
return func(cpr *ClientPortResource) error {
var vlansToAssign []string
var currentNative string
Expand Down Expand Up @@ -171,35 +159,53 @@ func batchVlans(removeOnly bool) func(*ClientPortResource) error {
Native: &native,
})
}
return createAndWaitForBatch(cpr.Client, cpr.Port.ID, vacr)
return createAndWaitForBatch(ctx, start, cpr, vacr)
}
}

func createAndWaitForBatch(c *packngo.Client, portID string, vacr *packngo.VLANAssignmentBatchCreateRequest) error {
func createAndWaitForBatch(ctx context.Context, start time.Time, cpr *ClientPortResource, vacr *packngo.VLANAssignmentBatchCreateRequest) error {
if len(vacr.VLANAssignments) == 0 {
return nil
}

portID := cpr.Port.ID
c := cpr.Client

b, _, err := c.VLANAssignments.CreateBatch(portID, vacr, nil)
if err != nil {
return fmt.Errorf("vlan assignment batch could not be created: %w", err)
}

// 15 minutes = 180 * 5sec-retry
for i := 0; i < 180; i++ {
<-time.After(5 * time.Second)
b, _, err := c.VLANAssignments.GetBatch(portID, b.ID, nil)
if err != nil {
return fmt.Errorf("vlan assignment batch %s could not be polled: %w", b.ID, err)
}
if b.State == packngo.VLANAssignmentBatchCompleted {
return nil
}
if b.State == packngo.VLANAssignmentBatchFailed {
return fmt.Errorf("vlan assignment batch %s provisioning failed: %s", b.ID, strings.Join(b.ErrorMessages, "; "))
}
deadline, _ := ctx.Deadline()
// originally set timeout in ctx by TF
ctxTimeout := deadline.Sub(start)

stateChangeConf := &retry.StateChangeConf{
Delay: 5 * time.Second,
Pending: []string{string(packngo.VLANAssignmentBatchQueued), string(packngo.VLANAssignmentBatchInProgress)},
Target: []string{string(packngo.VLANAssignmentBatchCompleted)},
MinTimeout: 5 * time.Second,
Timeout: ctxTimeout - time.Since(start) - 30*time.Second,
Refresh: func() (result interface{}, state string, err error) {
b, _, err := c.VLANAssignments.GetBatch(portID, b.ID, nil)
switch b.State {
case packngo.VLANAssignmentBatchFailed:
return b, string(packngo.VLANAssignmentBatchFailed),
fmt.Errorf("vlan assignment batch %s provisioning failed: %s", b.ID, strings.Join(b.ErrorMessages, "; "))
case packngo.VLANAssignmentBatchCompleted:
return b, string(packngo.VLANAssignmentBatchCompleted), nil
default:
if err != nil {
return b, "", fmt.Errorf("vlan assignment batch %s could not be polled: %w", b.ID, err)
}
return b, string(b.State), err
}
},
}

return fmt.Errorf("vlan assignment batch %s is not complete after timeout", b.ID)
if _, err = stateChangeConf.WaitForStateContext(ctx); err != nil {
return errors.Wrapf(err, "vlan assignment batch %s is not complete after timeout", b.ID)
}
return nil
}

func updateNativeVlan(cpr *ClientPortResource) error {
Expand Down
33 changes: 18 additions & 15 deletions equinix/resource_metal_port.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package equinix

import (
"context"
"log"
"time"

Expand All @@ -21,17 +22,17 @@ var (
func resourceMetalPort() *schema.Resource {
return &schema.Resource{
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(20 * time.Minute),
Update: schema.DefaultTimeout(20 * time.Minute),
Delete: schema.DefaultTimeout(20 * time.Minute),
Create: schema.DefaultTimeout(30 * time.Minute),
Update: schema.DefaultTimeout(30 * time.Minute),
Delete: schema.DefaultTimeout(30 * time.Minute),
},
Read: resourceMetalPortRead,
ReadWithoutTimeout: diagnosticsWrapper(resourceMetalPortRead),
// Create and Update are the same func
Create: resourceMetalPortUpdate,
Update: resourceMetalPortUpdate,
Delete: resourceMetalPortDelete,
CreateContext: diagnosticsWrapper(resourceMetalPortUpdate),
UpdateContext: diagnosticsWrapper(resourceMetalPortUpdate),
DeleteContext: diagnosticsWrapper(resourceMetalPortDelete),
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
Expand Down Expand Up @@ -116,31 +117,32 @@ func resourceMetalPort() *schema.Resource {
}
}

func resourceMetalPortUpdate(d *schema.ResourceData, meta interface{}) error {
func resourceMetalPortUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
start := time.Now()
cpr, _, err := getClientPortResource(d, meta)
if err != nil {
return friendlyError(err)
}

for _, f := range [](func(*ClientPortResource) error){
portSanityChecks,
batchVlans(true),
batchVlans(ctx, start, true),
makeDisbond,
convertToL2,
makeBond,
convertToL3,
batchVlans(false),
batchVlans(ctx, start, false),
updateNativeVlan,
} {
if err := f(cpr); err != nil {
return friendlyError(err)
}
}

return resourceMetalPortRead(d, meta)
return resourceMetalPortRead(ctx, d, meta)
}

func resourceMetalPortRead(d *schema.ResourceData, meta interface{}) error {
func resourceMetalPortRead(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

Expand Down Expand Up @@ -195,9 +197,10 @@ func resourceMetalPortRead(d *schema.ResourceData, meta interface{}) error {
return setMap(d, m)
}

func resourceMetalPortDelete(d *schema.ResourceData, meta interface{}) error {
func resourceMetalPortDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
resetRaw, resetOk := d.GetOk("reset_on_delete")
if resetOk && resetRaw.(bool) {
start := time.Now()
cpr, resp, err := getClientPortResource(d, meta)
if ignoreResponseErrors(httpForbidden, httpNotFound)(resp, err) != nil {
return err
Expand All @@ -219,7 +222,7 @@ func resourceMetalPortDelete(d *schema.ResourceData, meta interface{}) error {
return err
}
for _, f := range [](func(*ClientPortResource) error){
batchVlans(true),
batchVlans(ctx, start, true),
makeBond,
convertToL3,
} {
Expand Down
Loading

0 comments on commit 97e8ea6

Please sign in to comment.