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

Add network association resources #206

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ The following Environment Variables must be set in your shell prior to running a
- `ARM_TENANT_ID`
- `ARM_METADATA_HOST`
- `ARM_TEST_LOCATION`
- `ARM_TEST_LOCATION_ALT`
- `ARM_TEST_LOCATION_ALT2`

---

Expand Down
105 changes: 105 additions & 0 deletions internal/services/network/azuresdkhacks/network_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package azuresdkhacks

import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"

"github.com/Azure/azure-sdk-for-go/profiles/2020-09-01/network/mgmt/network"
"github.com/Azure/go-autorest/autorest"
)

// UpdateNetworkInterfaceAllowingRemovalOfNSG patches our way around a design flaw in the Azure
// Resource Manager API <-> Azure SDK for Go where it's not possible to remove a Network Security Group
func UpdateNetworkInterfaceAllowingRemovalOfNSG(ctx context.Context, client *network.InterfacesClient, resourceGroupName string, networkInterfaceName string, parameters network.Interface) (result network.InterfacesCreateOrUpdateFuture, err error) {
req, err := updateNetworkInterfaceAllowingRemovalOfNSGPreparer(ctx, client, resourceGroupName, networkInterfaceName, parameters)
if err != nil {
err = autorest.NewErrorWithError(err, "network.InterfacesClient", "CreateOrUpdate", nil, "Failure preparing request")
return
}

result, err = client.CreateOrUpdateSender(req)
if err != nil {
err = autorest.NewErrorWithError(err, "network.InterfacesClient", "CreateOrUpdate", result.Response(), "Failure sending request")
return
}

return
}

// updateNetworkInterfaceAllowingRemovalOfNSGPreparer prepares the CreateOrUpdate request but applies the
// necessary patches to be able to remove the NSG if required
func updateNetworkInterfaceAllowingRemovalOfNSGPreparer(ctx context.Context, client *network.InterfacesClient, resourceGroupName string, networkInterfaceName string, parameters network.Interface) (*http.Request, error) {
pathParameters := map[string]interface{}{
"networkInterfaceName": autorest.Encode("path", networkInterfaceName),
"resourceGroupName": autorest.Encode("path", resourceGroupName),
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
}

const APIVersion = "2018-11-01"
queryParameters := map[string]interface{}{
"api-version": APIVersion,
}

parameters.Etag = nil
preparer := autorest.CreatePreparer(
autorest.AsContentType("application/json; charset=utf-8"),
autorest.AsPut(),
autorest.WithBaseURL(client.BaseURI),
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/networkInterfaces/{networkInterfaceName}", pathParameters),
withJsonWorkingAroundTheBrokenNetworkAPI(parameters),
autorest.WithQueryParameters(queryParameters))
return preparer.Prepare((&http.Request{}).WithContext(ctx))
}

func withJsonWorkingAroundTheBrokenNetworkAPI(v network.Interface) autorest.PrepareDecorator {
return func(p autorest.Preparer) autorest.Preparer {
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err == nil {
b, err := json.Marshal(v)
if err == nil {
// there's a few fields which can be intentionally set to nil - as such here we need to check if they should be nil and force them to be nil
var out map[string]interface{}
if err := json.Unmarshal(b, &out); err != nil {
return r, err
}

// apply the hack
out = patchNICUpdateAPIIssue(v, out)

// then reserialize it as needed
b, err = json.Marshal(out)
if err == nil {
r.ContentLength = int64(len(b))
r.Body = io.NopCloser(bytes.NewReader(b))
}
}
}
return r, err
})
}
}

func patchNICUpdateAPIIssue(nic network.Interface, input map[string]interface{}) map[string]interface{} {
if nic.InterfacePropertiesFormat == nil {
return input
}

output := input

if v, ok := output["properties"]; ok {
props := v.(map[string]interface{})

if nic.InterfacePropertiesFormat.NetworkSecurityGroup == nil {
var hack *string // a nil-pointered string
props["networkSecurityGroup"] = hack
}

output["properties"] = props
}

return output
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package network

import (
"fmt"
"log"
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/profiles/2020-09-01/network/mgmt/network"
"github.com/hashicorp/terraform-provider-azurestack/internal/clients"
"github.com/hashicorp/terraform-provider-azurestack/internal/locks"
"github.com/hashicorp/terraform-provider-azurestack/internal/services/network/azuresdkhacks"
"github.com/hashicorp/terraform-provider-azurestack/internal/services/network/parse"
"github.com/hashicorp/terraform-provider-azurestack/internal/services/network/validate"
"github.com/hashicorp/terraform-provider-azurestack/internal/tf"
"github.com/hashicorp/terraform-provider-azurestack/internal/tf/pluginsdk"
"github.com/hashicorp/terraform-provider-azurestack/internal/tf/timeouts"
"github.com/hashicorp/terraform-provider-azurestack/internal/utils"
)

func networkInterfaceSecurityGroupAssociation() *pluginsdk.Resource {
return &pluginsdk.Resource{
Create: networkInterfaceSecurityGroupAssociationCreate,
Read: networkInterfaceSecurityGroupAssociationRead,
Delete: networkInterfaceSecurityGroupAssociationDelete,
Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error {
splitId := strings.Split(id, "|")
if len(splitId) != 2 {
return fmt.Errorf("expect ID to be the format {networkInterfaceId}|{networkSecurityGroupId} but got %q", id)
}
if _, err := parse.NetworkInterfaceID(splitId[0]); err != nil {
return err
}
if _, err := parse.NetworkSecurityGroupID(splitId[1]); err != nil {
return err
}
return nil
}),

Timeouts: &pluginsdk.ResourceTimeout{
Create: pluginsdk.DefaultTimeout(30 * time.Minute),
Read: pluginsdk.DefaultTimeout(5 * time.Minute),
Update: pluginsdk.DefaultTimeout(30 * time.Minute),
Delete: pluginsdk.DefaultTimeout(30 * time.Minute),
},

Schema: map[string]*pluginsdk.Schema{
"network_interface_id": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.NetworkInterfaceID,
},

"network_security_group_id": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.NetworkSecurityGroupID,
},
},
}
}

func networkInterfaceSecurityGroupAssociationCreate(d *pluginsdk.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).Network.InterfacesClient
ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d)
defer cancel()

log.Printf("[INFO] preparing arguments for Network Interface <-> Network Security Group Association creation.")

networkInterfaceId := d.Get("network_interface_id").(string)
networkSecurityGroupId := d.Get("network_security_group_id").(string)

nicId, err := parse.NetworkInterfaceID(networkInterfaceId)
if err != nil {
return err
}

locks.ByName(nicId.Name, networkInterfaceResourceName)
defer locks.UnlockByName(nicId.Name, networkInterfaceResourceName)

nsgId, err := parse.NetworkSecurityGroupID(networkSecurityGroupId)
if err != nil {
return err
}

locks.ByName(nsgId.Name, networkSecurityGroupResourceName)
defer locks.UnlockByName(nsgId.Name, networkSecurityGroupResourceName)

read, err := client.Get(ctx, nicId.ResourceGroup, nicId.Name, "")
if err != nil {
if utils.ResponseWasNotFound(read.Response) {
return fmt.Errorf("%s was not found!", *nicId)
}

return fmt.Errorf("retrieving %s: %+v", *nicId, err)
}

props := read.InterfacePropertiesFormat
if props == nil {
return fmt.Errorf("Error: `properties` was nil for %s", *nicId)
}

// first double-check it doesn't exist
resourceId := fmt.Sprintf("%s|%s", networkInterfaceId, networkSecurityGroupId)
if props.NetworkSecurityGroup != nil {
return tf.ImportAsExistsError("azurestack_network_interface_security_group_association", resourceId)
}

props.NetworkSecurityGroup = &network.SecurityGroup{
ID: utils.String(networkSecurityGroupId),
}

future, err := client.CreateOrUpdate(ctx, nicId.ResourceGroup, nicId.Name, read)
if err != nil {
return fmt.Errorf("updating Security Group Association for %s: %+v", *nicId, err)
}

if err = future.WaitForCompletionRef(ctx, client.Client); err != nil {
return fmt.Errorf("waiting for completion of Security Group Association for %s: %+v", *nicId, err)
}

d.SetId(resourceId)

return networkInterfaceSecurityGroupAssociationRead(d, meta)
}

func networkInterfaceSecurityGroupAssociationRead(d *pluginsdk.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).Network.InterfacesClient
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

splitId := strings.Split(d.Id(), "|")
if len(splitId) != 2 {
return fmt.Errorf("Expected ID to be in the format {networkInterfaceId}|{networkSecurityGroupId} but got %q", d.Id())
}

nicID, err := parse.NetworkInterfaceID(splitId[0])
if err != nil {
return err
}

read, err := client.Get(ctx, nicID.ResourceGroup, nicID.Name, "")
if err != nil {
if utils.ResponseWasNotFound(read.Response) {
log.Printf("%s was not found - removing from state!", *nicID)
d.SetId("")
return nil
}

return fmt.Errorf("retrieving %s: %+v", *nicID, err)
}

props := read.InterfacePropertiesFormat
if props == nil {
return fmt.Errorf("Error: `properties` was nil for %s", *nicID)
}

if props.NetworkSecurityGroup == nil || props.NetworkSecurityGroup.ID == nil {
log.Printf("%s doesn't have a Security Group attached - removing from state!", *nicID)
d.SetId("")
return nil
}

d.Set("network_interface_id", read.ID)

// nil-checked above
d.Set("network_security_group_id", props.NetworkSecurityGroup.ID)

return nil
}

func networkInterfaceSecurityGroupAssociationDelete(d *pluginsdk.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).Network.InterfacesClient
ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
defer cancel()

splitId := strings.Split(d.Id(), "|")
if len(splitId) != 2 {
return fmt.Errorf("Expected ID to be in the format {networkInterfaceId}/{networkSecurityGroup} but got %q", d.Id())
}

nicID, err := parse.NetworkInterfaceID(splitId[0])
if err != nil {
return err
}

locks.ByName(nicID.Name, networkInterfaceResourceName)
defer locks.UnlockByName(nicID.Name, networkInterfaceResourceName)

read, err := client.Get(ctx, nicID.ResourceGroup, nicID.Name, "")
if err != nil {
if utils.ResponseWasNotFound(read.Response) {
return fmt.Errorf(" %s was not found!", *nicID)
}

return fmt.Errorf("retrieving %s: %+v", *nicID, err)
}

props := read.InterfacePropertiesFormat
if props == nil {
return fmt.Errorf("Error: `properties` was nil for %s", *nicID)
}

props.NetworkSecurityGroup = nil
read.InterfacePropertiesFormat = props

future, err := azuresdkhacks.UpdateNetworkInterfaceAllowingRemovalOfNSG(ctx, client, nicID.ResourceGroup, nicID.Name, read)
if err != nil {
return fmt.Errorf("updating %s: %+v", *nicID, err)
}
if err := future.WaitForCompletionRef(ctx, client.Client); err != nil {
return fmt.Errorf("waiting for update of %s: %+v", *nicID, err)
}

return nil
}
Loading
Loading