Skip to content

Commit

Permalink
Merge pull request #101 from anexia-it/syseng-1313/resource-tagging
Browse files Browse the repository at this point in the history
Add tagging capabilities to supported resources
  • Loading branch information
anx-mschaefer committed Jun 28, 2022
2 parents f5dee77 + 4d55a4e commit d8df7d2
Show file tree
Hide file tree
Showing 22 changed files with 385 additions and 115 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ If the change isn't user-facing but still relevant enough for a changelog entry,
- anxcloud_ip_addresses
- anxcloud_tags
- anxcloud_vlans

### Fixed
* resource/anxcloud_virtual_server: tags changed outside of terraform will now get reverted back to terraform config on apply (#101, @marioreggiori)

### Added
* tagging capabilities to supported resources (#101, @marioreggiori)
- anxcloud_ip_address
- anxcloud_network_prefix
- anxcloud_vlan

## [0.3.5] - 2022-06-13

Expand Down
115 changes: 115 additions & 0 deletions anxcloud/common_resource_tagging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package anxcloud

import (
"context"
"fmt"
"reflect"
"sort"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"go.anx.io/go-anxcloud/pkg/api"
corev1 "go.anx.io/go-anxcloud/pkg/apis/core/v1"
)

func withTagsAttribute(s schemaMap) schemaMap {
s["tags"] = &schema.Schema{
Type: schema.TypeList,
Optional: true,
Computed: true,
Description: "List of tags attached to the resource.",
Elem: &schema.Schema{
Type: schema.TypeString,
},
// suppress diff when only the order has changed
DiffSuppressOnRefresh: true,
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
o, n := d.GetChange("tags")
oStringArray := mustCastInterfaceArray[string](o.([]interface{}))
nStringArray := mustCastInterfaceArray[string](n.([]interface{}))
sort.Strings(oStringArray)
sort.Strings(nStringArray)
return reflect.DeepEqual(oStringArray, nStringArray)
},
}
return s
}

type schemaContextCreateOrUpdateFunc interface {
schema.CreateContextFunc | schema.UpdateContextFunc
}

func tagsMiddlewareCreate(wrapped schema.CreateContextFunc) schema.CreateContextFunc {
return ensureTagsMiddleware(wrapped)
}

func tagsMiddlewareUpdate(wrapped schema.UpdateContextFunc) schema.UpdateContextFunc {
return ensureTagsMiddleware(wrapped)
}

func ensureTagsMiddleware[T schemaContextCreateOrUpdateFunc](wrapped T) T {
return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
diags := wrapped(ctx, d, m)
if diags.HasError() {
return diags
}

// we don't touch remote tags when tags attribute is not set
// remote tags are also kept when tags attribute was unset
if d.GetRawConfig().GetAttr("tags").IsNull() {
return diags
}

tags := mustCastInterfaceArray[string](d.Get("tags").([]interface{}))
if err := ensureTags(ctx, m.(providerContext).api, d.Id(), tags); err != nil {
diags = append(diags, diag.FromErr(err)...)
}

return diags
}
}

func tagsMiddlewareRead(wrapped schema.ReadContextFunc) schema.ReadContextFunc {
return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
diags := wrapped(ctx, d, m)
if diags.HasError() {
return diags
}

tags, err := readTags(ctx, m.(providerContext).api, d.Id())
if err != nil {
return append(diags, diag.FromErr(err)...)
}

if err := d.Set("tags", tags); err != nil {
diags = append(diags, diag.FromErr(err)...)
}

return diags
}
}

func ensureTags(ctx context.Context, a api.API, resourceID string, tags []string) error {
resource := corev1.Resource{Identifier: resourceID}

remote, err := readTags(ctx, a, resourceID)
if err != nil {
return fmt.Errorf("failed to fetch remote tags: %w", err)
}

toRemove := sliceSubstract(remote, tags)
if err := corev1.Untag(ctx, a, &resource, toRemove...); err != nil {
return fmt.Errorf("failed to untag resource: %w", err)
}

toAdd := sliceSubstract(tags, remote)
if err := corev1.Tag(ctx, a, &resource, toAdd...); err != nil {
return fmt.Errorf("failed to tag resource: %w", err)
}

return nil
}

func readTags(ctx context.Context, a api.API, resourceID string) ([]string, error) {
return corev1.ListTags(ctx, a, &corev1.Resource{Identifier: resourceID})
}
107 changes: 107 additions & 0 deletions anxcloud/common_resource_tagging_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package anxcloud

import (
"context"
"fmt"
"reflect"
"sort"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
corev1 "go.anx.io/go-anxcloud/pkg/apis/core/v1"
)

func testAccAnxCloudCheckResourceTagged(resourcePath string, expectedTags ...string) resource.TestCheckFunc {
return func(s *terraform.State) error {
a := testAccProvider.Meta().(providerContext).api
rs, ok := s.RootModule().Resources[resourcePath]
if !ok {
return fmt.Errorf("resource %q not found", resourcePath)
}

remoteTags, err := readTags(context.TODO(), a, rs.Primary.ID)
if err != nil {
return fmt.Errorf("failed to fetch remote tags: %w", err)
}

sort.Strings(expectedTags)
sort.Strings(remoteTags)

if !reflect.DeepEqual(expectedTags, remoteTags) {
return fmt.Errorf("resource %s tags didn't match remote tags, got %v - expected %v", resourcePath, remoteTags, expectedTags)
}

return nil
}
}

func testAccAnxCloudAddRemoteTag(resourcePath, tag string) resource.ImportStateIdFunc {
return func(s *terraform.State) (string, error) {
a := testAccProvider.Meta().(providerContext).api
rs, ok := s.RootModule().Resources[resourcePath]
if !ok {
return "", fmt.Errorf("resource %q not found", resourcePath)
}

if err := corev1.Tag(context.TODO(), a, &corev1.Resource{Identifier: rs.Primary.ID}, tag); err != nil {
return "", fmt.Errorf("failed to tag resource: %w", err)
}

return rs.Primary.ID, nil
}
}

func testAccAnxCloudCommonResourceTagTestSteps(tpl, resourcePath string) []resource.TestStep {
return []resource.TestStep{
// create resource with tags
{
Config: fmt.Sprintf(tpl, generateTagsString("foo", "bar")),
Check: testAccAnxCloudCheckResourceTagged(resourcePath, "foo", "bar"),
},
// remove tag
{
Config: fmt.Sprintf(tpl, generateTagsString("foo")),
Check: testAccAnxCloudCheckResourceTagged(resourcePath, "foo"),
},
// add tag
{
Config: fmt.Sprintf(tpl, generateTagsString("foo", "bar", "baz")),
Check: testAccAnxCloudCheckResourceTagged(resourcePath, "foo", "bar", "baz"),
},
// change remote tags
{
// this should technically be a PreConfig
// since PreConfig does not expose the *terraform.State we use this as a workaround
ImportStateIdFunc: testAccAnxCloudAddRemoteTag(resourcePath, "foobaz"),
ImportState: true,
ResourceName: resourcePath,
},
// reconcile tags (should remove previously created "foobaz" tag)
{
Config: fmt.Sprintf(tpl, generateTagsString("foo", "bar", "baz")),
Check: testAccAnxCloudCheckResourceTagged(resourcePath, "foo", "bar", "baz"),
},
// removed tags argument -> expect remote to be untouched
{
Config: fmt.Sprintf(tpl, ""),
Check: testAccAnxCloudCheckResourceTagged(resourcePath, "foo", "bar", "baz"),
},
}
}

func generateTagsString(tags ...string) string {
if len(tags) == 0 {
return ""
}

ret := strings.Builder{}
ret.WriteString("tags = [\n")

for _, tag := range tags {
ret.WriteString(fmt.Sprintf("%q,\n", tag))
}

ret.WriteString("]\n")
return ret.String()
}
8 changes: 4 additions & 4 deletions anxcloud/resource_ip_address.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ const (
func resourceIPAddress() *schema.Resource {
return &schema.Resource{
Description: "This resource allows you to create and configure IP addresses.",
CreateContext: resourceIPAddressCreate,
ReadContext: resourceIPAddressRead,
UpdateContext: resourceIPAddressUpdate,
CreateContext: tagsMiddlewareCreate(resourceIPAddressCreate),
ReadContext: tagsMiddlewareRead(resourceIPAddressRead),
UpdateContext: tagsMiddlewareUpdate(resourceIPAddressUpdate),
DeleteContext: resourceIPAddressDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
Expand All @@ -33,7 +33,7 @@ func resourceIPAddress() *schema.Resource {
Update: schema.DefaultTimeout(10 * time.Minute),
Delete: schema.DefaultTimeout(5 * time.Minute),
},
Schema: schemaIPAddress(),
Schema: withTagsAttribute(schemaIPAddress()),
}
}

Expand Down
28 changes: 28 additions & 0 deletions anxcloud/resource_ip_address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,34 @@ func TestAccAnxCloudIPAddressReserved(t *testing.T) {
})
}

func TestAccAnxCloudIPAddressTags(t *testing.T) {
environment.SkipIfNoEnvironment(t)
envInfo := environment.GetEnvInfo(t)

prefixID := envInfo.Prefix.ID
ipAddress := envInfo.Prefix.GetNextIP()
role := "Default"

tpl := fmt.Sprintf(`
resource "anxcloud_ip_address" "foo" {
network_prefix_id = "%s"
address = "%s"
role = "%s"
description_customer = "tf-acc-tags"
%%s // tags
}`, prefixID, ipAddress, role)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviders,
CheckDestroy: testAccAnxCloudIPAddressDestroy,
Steps: testAccAnxCloudCommonResourceTagTestSteps(
tpl, "anxcloud_ip_address.foo",
),
})
}

func testAccAnxCloudIPAddress(resourceName, prefixID, ipAddress, role string) string {
return fmt.Sprintf(`
resource "anxcloud_ip_address" "%s" {
Expand Down
8 changes: 4 additions & 4 deletions anxcloud/resource_network_prefix.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ const (
func resourceNetworkPrefix() *schema.Resource {
return &schema.Resource{
Description: "This resource allows you to create and configure network prefix.",
CreateContext: resourceNetworkPrefixCreate,
ReadContext: resourceNetworkPrefixRead,
UpdateContext: resourceNetworkPrefixUpdate,
CreateContext: tagsMiddlewareCreate(resourceNetworkPrefixCreate),
ReadContext: tagsMiddlewareRead(resourceNetworkPrefixRead),
UpdateContext: tagsMiddlewareUpdate(resourceNetworkPrefixUpdate),
DeleteContext: resourceNetworkPrefixDelete,
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(60 * time.Minute),
Expand All @@ -33,7 +33,7 @@ func resourceNetworkPrefix() *schema.Resource {
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: schemaNetworkPrefix(),
Schema: withTagsAttribute(schemaNetworkPrefix()),
}
}

Expand Down
26 changes: 26 additions & 0 deletions anxcloud/resource_network_prefix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,32 @@ func TestAccAnxCloudNetworkPrefix(t *testing.T) {
})
}

func TestAccAnxCloudNetworkPrefixTags(t *testing.T) {
environment.SkipIfNoEnvironment(t)
envInfo := environment.GetEnvInfo(t)

tpl := fmt.Sprintf(`
resource "anxcloud_network_prefix" "foo" {
vlan_id = "%s"
location_id = "%s"
ip_version = 4
type = 1
netmask = 31
description_customer = "tf-acc-tags"
%%s // tags
}`, envInfo.VlanID, envInfo.Location)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviders,
CheckDestroy: testAccCheckAnxCloudNetworkPrefixDestroy,
Steps: testAccAnxCloudCommonResourceTagTestSteps(
tpl, "anxcloud_network_prefix.foo",
),
})
}

func testAccCheckAnxCloudNetworkPrefixDestroy(s *terraform.State) error {
c := testAccProvider.Meta().(providerContext).legacyClient
p := prefix.NewAPI(c)
Expand Down
17 changes: 0 additions & 17 deletions anxcloud/resource_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"go.anx.io/go-anxcloud/pkg/core/resource"
"go.anx.io/go-anxcloud/pkg/core/tags"
)

Expand Down Expand Up @@ -89,19 +88,3 @@ func resourceTagDelete(ctx context.Context, d *schema.ResourceData, m interface{

return nil
}

func attachTag(ctx context.Context, m providerContext, resourceID, tagName string) error {
r := resource.NewAPI(m.legacyClient)
if _, err := r.AttachTag(ctx, resourceID, tagName); err != nil {
return err
}
return nil
}

func detachTag(ctx context.Context, m providerContext, resourceID, tagName string) error {
r := resource.NewAPI(m.legacyClient)
if err := r.DetachTag(ctx, resourceID, tagName); err != nil {
return err
}
return nil
}

0 comments on commit d8df7d2

Please sign in to comment.