Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix delete and import for redundant l2 connections #103

Merged
merged 4 commits into from
Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions docs/resources/equinix_ecx_l2_connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ resource "equinix_ecx_l2_connection" "azure" {
vlan_stag = 1482
vlan_ctag = 2512
seller_metro_code = "SV"
named_tag = "Public"
named_tag = "PRIVATE"
authorization_key = "c4dff8e8-b52f-4b34-b0d4-c4588f7338f3
secondary_connection {
name = "tf-azure-sec"
Expand Down Expand Up @@ -118,7 +118,7 @@ the Network Edge virtual device from which the connection would originate.
- `vlan_ctag` - (Optional) C-Tag/Inner-Tag of the connection - a numeric
character ranging from 2 - 4094.
- `named_tag` - (Optional) The type of peering to set up in case when connecting
to Azure Express Route. One of _"Public"_, _"Private"_, _"Microsoft"_, _"Manual"_
to Azure Express Route. One of _"PRIVATE"_, _"MICROSOFT"_, _"MANUAL"_
- `additional_info` - (Optional) one or more additional information key-value objects
- `name` - (Required) additional information key
- `value` - (Required) additional information value
Expand Down Expand Up @@ -167,8 +167,8 @@ are exported:
- `provider_status` - Connection provisioning status on service provider's side
- `redundant_uuid` - Unique identifier of the redundant connection, applicable for
HA connections
- `redundancy_type` - Connection redundancy type, applicable for HA connections.
Either primary or secondary.
- `redundancy_type` - Connection redundancy type, applicable for HA connections. Either PRIMARY or SECONDARY
- `redundancy_group` - Unique identifier of group containing a primary and secondary connection.
- `zside_port_uuid` - when not provided as an argument, it is identifier of the
z-side port, assigned by the Fabric
- `zside_vlan_stag` - when not provided as an argument, it is S-Tag/Outer-Tag of
Expand All @@ -179,6 +179,8 @@ z-side port, assigned by the Fabric
- `zside_port_uuid`
- `zside_vlan_stag`
- `zside_vlan_ctag`
- `redundancy_type`
- `redundancy_group`

## Update operation behavior

Expand All @@ -202,8 +204,17 @@ options:

## Import

This resource can be imported using an existing ID:
Equinix L2 connections can be imported using an existing `id`:

```sh
terraform import equinix_ecx_l2_connection.example {existing_id}
existing_connection_id='00000000-0000-0000-0000-1111111111'
terraform import equinix_ecx_l2_connection.example ${existing_connection_id}
```

To import a redundant connection it is required a single string with both connection `id` separated by `:`, e.g.,

```sh
existing_primary_connection_id='00000000-0000-0000-0000-1111111111'
existing_secondary_connection_id='00000000-0000-0000-0000-2222222222'
terraform import equinix_ecx_l2_connection.example ${existing_primary_connection_id}:${existing_secondary_connection_id}
```
200 changes: 127 additions & 73 deletions equinix/resource_ecx_l2_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import (
"fmt"
"log"
"time"
"strings"

"github.com/equinix/ecx-go/v2"
"github.com/equinix/rest-go"
"github.com/hashicorp/go-cty/cty"
"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"
Expand Down Expand Up @@ -40,6 +40,7 @@ var ecxL2ConnectionSchemaNames = map[string]string{
"AuthorizationKey": "authorization_key",
"RedundantUUID": "redundant_uuid",
"RedundancyType": "redundancy_type",
"RedundancyGroup": "redundancy_group",
"SecondaryConnection": "secondary_connection",
"Actions": "actions",
"ServiceToken": "service_token",
Expand Down Expand Up @@ -70,6 +71,7 @@ var ecxL2ConnectionDescriptions = map[string]string{
"AuthorizationKey": "Text field used to authorize connection on the provider side. Value depends on a provider service profile used for connection",
"RedundantUUID": "Unique identifier of the redundant connection, applicable for HA connections",
"RedundancyType": "Connection redundancy type, applicable for HA connections. Either primary or secondary",
"RedundancyGroup": "Unique identifier of group containing a primary and secondary connection",
"SecondaryConnection": "Definition of secondary connection for redundant, HA connectivity",
"Actions": "One or more pending actions to complete connection provisioning",
"ServiceToken": "Unique Equinix Fabric key given by a provider that grants you authorization to enable connectivity from a shared multi-tenant port (a-side)",
Expand Down Expand Up @@ -115,14 +117,27 @@ var ecxL2ConnectionActionDataDescriptions = map[string]string{
"ValidationPattern": "Action data pattern",
}

type (
getL2Connection func(uuid string) (*ecx.L2Connection, error)
)

func resourceECXL2Connection() *schema.Resource {
return &schema.Resource{
CreateContext: resourceECXL2ConnectionCreate,
ReadContext: resourceECXL2ConnectionRead,
UpdateContext: resourceECXL2ConnectionUpdate,
DeleteContext: resourceECXL2ConnectionDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
// The expected ID to import redundant connections is '(primaryID):(secondaryID)', e.g.,
// terraform import equinix_ecx_l2_connection.example 1111-11-11-1111:2222-22-22-2222
ids := strings.Split(d.Id(), ":")
d.SetId(ids[0])
if len(ids) > 1 {
d.Set(ecxL2ConnectionSchemaNames["RedundantUUID"], ids[1])
}
return []*schema.ResourceData{d}, nil
},
},
Schema: createECXL2ConnectionResourceSchema(),
Timeouts: &schema.ResourceTimeout{
Expand Down Expand Up @@ -242,8 +257,11 @@ func createECXL2ConnectionResourceSchema() map[string]*schema.Schema {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{"Private", "Public", "Microsoft", "Manual"}, false),
ValidateFunc: validation.StringInSlice([]string{"PRIVATE", "MICROSOFT", "MANUAL", "Private", "Public", "Microsoft", "Manual"}, false),
Description: ecxL2ConnectionDescriptions["NamedTag"],
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
return strings.EqualFold(old, new)
},
},
ecxL2ConnectionSchemaNames["AdditionalInfo"]: {
Type: schema.TypeSet,
Expand Down Expand Up @@ -312,6 +330,11 @@ func createECXL2ConnectionResourceSchema() map[string]*schema.Schema {
Computed: true,
Description: ecxL2ConnectionDescriptions["RedundancyType"],
},
ecxL2ConnectionSchemaNames["RedundancyGroup"]: {
Type: schema.TypeString,
Computed: true,
Description: ecxL2ConnectionDescriptions["RedundancyGroup"],
},
ecxL2ConnectionSchemaNames["Actions"]: {
Type: schema.TypeSet,
Computed: true,
Expand Down Expand Up @@ -523,12 +546,18 @@ func createECXL2ConnectionSecondaryResourceSchema() map[string]*schema.Schema {
Type: schema.TypeString,
Computed: true,
Description: ecxL2ConnectionDescriptions["RedundantUUID"],
Deprecated: "SecondaryConnection.0.RedundantUUID will not be returned. Use UUID instead",
},
ecxL2ConnectionSchemaNames["RedundancyType"]: {
Type: schema.TypeString,
Computed: true,
Description: ecxL2ConnectionDescriptions["RedundancyType"],
},
ecxL2ConnectionSchemaNames["RedundancyGroup"]: {
Type: schema.TypeString,
Computed: true,
Description: ecxL2ConnectionDescriptions["RedundancyGroup"],
},
ecxL2ConnectionSchemaNames["Actions"]: {
Type: schema.TypeSet,
Computed: true,
Expand Down Expand Up @@ -585,42 +614,21 @@ func resourceECXL2ConnectionCreate(ctx context.Context, d *schema.ResourceData,
return diag.FromErr(err)
}
d.SetId(ecx.StringValue(primaryID))
createStateConf := &resource.StateChangeConf{
Pending: []string{
ecx.ConnectionStatusProvisioning,
ecx.ConnectionStatusPendingAutoApproval,
},
Target: []string{
ecx.ConnectionStatusProvisioned,
ecx.ConnectionStatusPendingApproval,
ecx.ConnectionStatusPendingBGPPeering,
ecx.ConnectionStatusPendingProviderVlan,
},
Timeout: d.Timeout(schema.TimeoutCreate),
Delay: 2 * time.Second,
MinTimeout: 2 * time.Second,
Refresh: func() (interface{}, string, error) {
resp, err := conf.ecx.GetL2Connection(d.Id())
if err != nil {
return nil, "", err
}
return resp, ecx.StringValue(resp.Status), nil
},
}
if _, err := createStateConf.WaitForStateContext(ctx); err != nil {
return diag.Errorf("error waiting for connection (%s) to be created: %s", d.Id(), err)
waitConfigs := []*resource.StateChangeConf{
createConnectionStatusProvisioningWaitConfiguration(conf.ecx.GetL2Connection, d.Id(), 5*time.Second, d.Timeout(schema.TimeoutCreate)),
}
if ecx.StringValue(secondaryID) != "" {
createStateConf.Refresh = func() (interface{}, string, error) {
resp, err := conf.ecx.GetL2Connection(ecx.StringValue(secondaryID))
if err != nil {
return nil, "", err
}
return resp, ecx.StringValue(resp.Status), nil
d.Set(ecxL2ConnectionSchemaNames["RedundantUUID"], secondaryID)
waitConfigs = append(waitConfigs,
createConnectionStatusProvisioningWaitConfiguration(conf.ecx.GetL2Connection, ecx.StringValue(secondaryID), 2*time.Second, d.Timeout(schema.TimeoutCreate)),
)
}
for _, config := range waitConfigs {
if config == nil {
continue
}

if _, err := createStateConf.WaitForStateContext(ctx); err != nil {
return diag.Errorf("error waiting for secondary connection (%s) to be created: %s", d.Id(), err)
if _, err := config.WaitForStateContext(ctx); err != nil {
return diag.Errorf("error waiting for connection (%s) to be created: %s", d.Id(), err)
}
}
diags = append(diags, resourceECXL2ConnectionRead(ctx, d, m)...)
Expand All @@ -645,14 +653,26 @@ func resourceECXL2ConnectionRead(ctx context.Context, d *schema.ResourceData, m
ecx.ConnectionStatusDeleted,
}) {
d.SetId("")
return nil
return diags
}
if ecx.StringValue(primary.RedundantUUID) != "" {
secondary, err = conf.ecx.GetL2Connection(ecx.StringValue(primary.RedundantUUID))

// RedundantUUID value is set in CreateContext/Importer functions
// Implementing a l2_connection datasource will require search for secondary connection before using
// resourceECXL2ConnectionRead or explicitly request the names or identifiers of each connection
if redID, ok := d.GetOk(ecxL2ConnectionSchemaNames["RedundantUUID"]); ok {
secondary, err = conf.ecx.GetL2Connection(redID.(string))
if err != nil {
return diag.Errorf("cannot fetch secondary connection due to %v", err)
}
if ecx.StringValue(primary.RedundancyGroup) != ecx.StringValue(secondary.RedundancyGroup) || !strings.EqualFold(ecx.StringValue(secondary.RedundancyType), "secondary") {
return diag.Errorf("connection '%s' (%s) was found but is not the redundant connection for '%s' (%s)",
ecx.StringValue(secondary.Name),
ecx.StringValue(secondary.UUID),
ecx.StringValue(primary.Name),
ecx.StringValue(primary.UUID))
}
}

if err := updateECXL2ConnectionResource(primary, secondary, d); err != nil {
return diag.FromErr(err)
}
Expand All @@ -672,9 +692,9 @@ func resourceECXL2ConnectionUpdate(ctx context.Context, d *schema.ResourceData,
if err := fillFabricL2ConnectionUpdateRequest(primaryUpdateReq, primaryChanges).Execute(); err != nil {
return diag.FromErr(err)
}
if v, ok := d.GetOk(ecxL2ConnectionSchemaNames["RedundantUUID"]); ok {
if redID, ok := d.GetOk(ecxL2ConnectionSchemaNames["RedundantUUID"]); ok {
secondaryChanges := getResourceDataListElementChanges(supportedChanges, ecxL2ConnectionSchemaNames["SecondaryConnection"], 0, d)
secondaryUpdateReq := conf.ecx.NewL2ConnectionUpdateRequest(v.(string))
secondaryUpdateReq := conf.ecx.NewL2ConnectionUpdateRequest(redID.(string))
if err := fillFabricL2ConnectionUpdateRequest(secondaryUpdateReq, secondaryChanges).Execute(); err != nil {
return diag.FromErr(err)
}
Expand All @@ -696,38 +716,28 @@ func resourceECXL2ConnectionDelete(ctx context.Context, d *schema.ResourceData,
}
return diag.FromErr(err)
}
// remove secondary connection, don't fail on error as there is no partial state on delete
waitConfigs := []*resource.StateChangeConf{
createConnectionStatusDeleteWaitConfiguration(conf.ecx.GetL2Connection, d.Id(), 5*time.Second, d.Timeout(schema.TimeoutDelete)),
}
if redID, ok := d.GetOk(ecxL2ConnectionSchemaNames["RedundantUUID"]); ok {
if err := conf.ecx.DeleteL2Connection(redID.(string)); err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Warning,
Summary: fmt.Sprintf("Failed to remove secondary connection with UUID %q", redID.(string)),
Detail: err.Error(),
AttributePath: cty.GetAttrPath(ecxL2ConnectionSchemaNames["RedundantUUID"]),
})
}
}
deleteStateConf := &resource.StateChangeConf{
Pending: []string{
ecx.ConnectionStatusDeprovisioning,
},
Target: []string{
ecx.ConnectionStatusPendingDelete,
ecx.ConnectionStatusDeprovisioned,
},
Timeout: d.Timeout(schema.TimeoutDelete),
Delay: 2 * time.Second,
MinTimeout: 2 * time.Second,
Refresh: func() (interface{}, string, error) {
resp, err := conf.ecx.GetL2Connection(d.Id())
if err != nil {
return nil, "", err
restErr, ok := err.(rest.Error)
if ok {
// IC-LAYER2-4021 = Connection already deleted
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this could be ok && ...

if hasApplicationErrorCode(restErr.ApplicationErrors, "IC-LAYER2-4021") {
return diags
}
}
return resp, ecx.StringValue(resp.Status), nil
},
return diag.FromErr(err)
}
waitConfigs = append(waitConfigs,
createConnectionStatusDeleteWaitConfiguration(conf.ecx.GetL2Connection, redID.(string), 2*time.Second, d.Timeout(schema.TimeoutDelete)),
)
}
if _, err := deleteStateConf.WaitForStateContext(ctx); err != nil {
return diag.Errorf("error waiting for connection (%s) to be removed: %s", d.Id(), err)
for _, config := range waitConfigs {
if _, err := config.WaitForStateContext(ctx); err != nil {
return diag.Errorf("error waiting for connection (%s) to be removed: %s", d.Id(), err)
}
}
return diags
}
Expand Down Expand Up @@ -865,12 +875,12 @@ func updateECXL2ConnectionResource(primary *ecx.L2Connection, secondary *ecx.L2C
if err := d.Set(ecxL2ConnectionSchemaNames["AuthorizationKey"], primary.AuthorizationKey); err != nil {
return fmt.Errorf("error reading AuthorizationKey: %s", err)
}
if err := d.Set(ecxL2ConnectionSchemaNames["RedundantUUID"], primary.RedundantUUID); err != nil {
return fmt.Errorf("error reading RedundantUUID: %s", err)
}
if err := d.Set(ecxL2ConnectionSchemaNames["RedundancyType"], primary.RedundancyType); err != nil {
return fmt.Errorf("error reading RedundancyType: %s", err)
}
if err := d.Set(ecxL2ConnectionSchemaNames["RedundancyGroup"], primary.RedundancyGroup); err != nil {
return fmt.Errorf("error reading RedundancyGroup: %s", err)
}
if err := d.Set(ecxL2ConnectionSchemaNames["ServiceToken"], primary.ServiceToken); err != nil {
return fmt.Errorf("error reading ServiceToken: %s", err)
}
Expand Down Expand Up @@ -912,8 +922,8 @@ func flattenECXL2ConnectionSecondary(previous, conn *ecx.L2Connection) interface
transformed[ecxL2ConnectionSchemaNames["SellerRegion"]] = conn.SellerRegion
transformed[ecxL2ConnectionSchemaNames["SellerMetroCode"]] = conn.SellerMetroCode
transformed[ecxL2ConnectionSchemaNames["AuthorizationKey"]] = conn.AuthorizationKey
transformed[ecxL2ConnectionSchemaNames["RedundantUUID"]] = conn.RedundantUUID
transformed[ecxL2ConnectionSchemaNames["RedundancyType"]] = conn.RedundancyType
transformed[ecxL2ConnectionSchemaNames["RedundancyGroup"]] = conn.RedundancyGroup
transformed[ecxL2ConnectionSchemaNames["Actions"]] = flattenECXL2ConnectionActions(conn.Actions)
return []interface{}{transformed}
}
Expand Down Expand Up @@ -1031,3 +1041,47 @@ func fillFabricL2ConnectionUpdateRequest(updateReq ecx.L2ConnectionUpdateRequest
}
return updateReq
}

func createConnectionStatusProvisioningWaitConfiguration(fetchFunc getL2Connection, id string, delay time.Duration, timeout time.Duration) *resource.StateChangeConf {
pending := []string{
ecx.ConnectionStatusProvisioning,
ecx.ConnectionStatusPendingAutoApproval,
}
target := []string{
ecx.ConnectionStatusProvisioned,
ecx.ConnectionStatusPendingApproval,
ecx.ConnectionStatusPendingBGPPeering,
ecx.ConnectionStatusPendingProviderVlan,
}
return createConnectionStatusWaitConfiguration(fetchFunc, id, delay, timeout, target, pending)
}


func createConnectionStatusDeleteWaitConfiguration(fetchFunc getL2Connection, id string, delay time.Duration, timeout time.Duration) *resource.StateChangeConf {
pending := []string{
ecx.ConnectionStatusDeprovisioning,
}
target := []string{
ecx.ConnectionStatusPendingDelete,
ecx.ConnectionStatusDeprovisioned,
ecx.ConnectionStatusDeleted,
}
return createConnectionStatusWaitConfiguration(fetchFunc, id, delay, timeout, target, pending)
}

func createConnectionStatusWaitConfiguration(fetchFunc getL2Connection, id string, delay time.Duration, timeout time.Duration, target []string, pending []string) *resource.StateChangeConf {
return &resource.StateChangeConf{
Pending: pending,
Target: target,
Timeout: timeout,
Delay: delay,
MinTimeout: delay,
Refresh: func() (interface{}, string, error) {
resp, err := fetchFunc(id)
if err != nil {
return nil, "", err
}
return resp, ecx.StringValue(resp.Status), nil
},
}
}
Loading