-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #101 from anexia-it/syseng-1313/resource-tagging
Add tagging capabilities to supported resources
- Loading branch information
Showing
22 changed files
with
385 additions
and
115 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.