Migrated tags to sets instead of lists and moved the definitions and related functions to a separate global space to be used by all resources that can be tagged

This commit is contained in:
Tilen Faganel 2018-08-24 19:58:53 +01:00
parent fe9492c23d
commit 6ef90f7ed7
No known key found for this signature in database
GPG Key ID: 3DDA5ABF228F8E7A
10 changed files with 243 additions and 120 deletions

View File

@ -135,12 +135,6 @@ func resourceDigitalOceanDroplet() *schema.Resource {
Elem: &schema.Schema{Type: schema.TypeString},
},
"tags": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"user_data": {
Type: schema.TypeString,
Optional: true,
@ -157,6 +151,8 @@ func resourceDigitalOceanDroplet() *schema.Resource {
Optional: true,
ForceNew: true,
},
"tags": tagsSchema(),
},
}
}
@ -172,6 +168,7 @@ func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{})
Name: d.Get("name").(string),
Region: d.Get("region").(string),
Size: d.Get("size").(string),
Tags: expandTags(d.Get("tags").(*schema.Set).List()),
}
if attr, ok := d.GetOk("backups"); ok {
@ -249,12 +246,6 @@ func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{})
"Error waiting for droplet (%s) to become ready: %s", d.Id(), err)
}
// droplet needs to be active in order to set tags
err = setTags(client, d)
if err != nil {
return fmt.Errorf("Error setting tags: %s", err)
}
return resourceDigitalOceanDropletRead(d, meta)
}
@ -331,7 +322,7 @@ func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) e
"host": findIPv4AddrByType(droplet, "public"),
})
d.Set("tags", droplet.Tags)
d.Set("tags", flattenTags(droplet.Tags))
return nil
}

View File

@ -67,12 +67,6 @@ func resourceDigitalOceanFirewall() *schema.Resource {
Optional: true,
},
"tags": {
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
"inbound_rule": {
Type: schema.TypeList,
Optional: true,
@ -158,6 +152,8 @@ func resourceDigitalOceanFirewall() *schema.Resource {
},
},
},
"tags": tagsSchema(),
},
}
}
@ -206,7 +202,7 @@ func resourceDigitalOceanFirewallRead(d *schema.ResourceData, meta interface{})
d.Set("pending_changes", firewallPendingChanges(d, firewall))
d.Set("name", firewall.Name)
d.Set("droplet_ids", firewall.DropletIDs)
d.Set("tags", firewall.Tags)
d.Set("tags", flattenTags(firewall.Tags))
if err := d.Set("inbound_rule", flattenFirewallInboundRules(d, firewall.InboundRules)); err != nil {
return fmt.Errorf("[DEBUG] Error setting Firewall inbound_rule error: %#v", err)
@ -296,20 +292,15 @@ func firewallRequest(d *schema.ResourceData, client *godo.Client) (*godo.Firewal
opts.DropletIDs = droplets
}
if v, ok := d.GetOk("tags"); ok {
var tags []string
for _, tag := range v.([]interface{}) {
tags = append(tags, tag.(string))
}
opts.Tags = tags
}
// Get inbound_rules
opts.InboundRules = expandFirewallInboundRules(d)
// Get outbound_rules
opts.OutboundRules = expandFirewallOutboundRules(d)
// Get tags
opts.Tags = expandTags(d.Get("tags").(*schema.Set).List())
return opts, nil
}

View File

@ -145,11 +145,14 @@ func resourceDigitalOceanLoadbalancer() *schema.Resource {
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Computed: true,
},
"droplet_tag": {
Type: schema.TypeString,
Optional: true,
Type: schema.TypeString,
Optional: true,
DiffSuppressFunc: CaseSensitive,
ValidateFunc: validateTag,
},
"redirect_http_to_https": {

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"log"
"regexp"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/schema"
@ -24,22 +23,12 @@ func resourceDigitalOceanTag() *schema.Resource {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateTagName,
ValidateFunc: validateTag,
},
},
}
}
var tagNameRe = regexp.MustCompile("^[a-z0-9:\\-_]{1,255}$")
func validateTagName(value interface{}, key string) ([]string, []error) {
if !tagNameRe.MatchString(value.(string)) {
return nil, []error{fmt.Errorf("tags may contain letters, numbers, colons, dashes, and underscores; there is a limit of 255 characters per tag")}
}
return nil, nil
}
func resourceDigitalOceanTagCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)

View File

@ -6,7 +6,6 @@ import (
"testing"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
@ -32,72 +31,6 @@ func TestAccDigitalOceanTag_Basic(t *testing.T) {
})
}
func TestAccDigitalOceanTag_NameValidation(t *testing.T) {
cases := []struct {
Input string
ExpectError bool
}{
{
Input: "",
ExpectError: true,
},
{
Input: "foo",
ExpectError: false,
},
{
Input: "foo-bar",
ExpectError: false,
},
{
Input: "foo:bar",
ExpectError: false,
},
{
Input: "foo_bar",
ExpectError: false,
},
{
Input: "foo-001",
ExpectError: false,
},
{
Input: "foo/bar",
ExpectError: true,
},
{
Input: "foo\bar",
ExpectError: true,
},
{
Input: "foo.bar",
ExpectError: true,
},
{
Input: "foo*",
ExpectError: true,
},
{
Input: acctest.RandString(256),
ExpectError: true,
},
}
for _, tc := range cases {
_, errors := validateTagName(tc.Input, tc.Input)
hasError := len(errors) > 0
if tc.ExpectError && !hasError {
t.Fatalf("Expected the DigitalOcean Tag Name to trigger a validation error for '%s'", tc.Input)
}
if hasError && !tc.ExpectError {
t.Fatalf("Unexpected error validating the DigitalOcean Tag Name '%s': %s", tc.Input, errors[0])
}
}
}
func testAccCheckDigitalOceanTagDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*godo.Client)

12
digitalocean/set.go Normal file
View File

@ -0,0 +1,12 @@
package digitalocean
import (
"strings"
"github.com/hashicorp/terraform/helper/hashcode"
)
// Helper function for sets of strings that are case insensitive
func HashStringIgnoreCase(v interface{}) int {
return hashcode.String(strings.ToLower(v.(string)))
}

11
digitalocean/suppress.go Normal file
View File

@ -0,0 +1,11 @@
package digitalocean
import (
"strings"
"github.com/hashicorp/terraform/helper/schema"
)
func CaseSensitive(_, old, new string, _ *schema.ResourceData) bool {
return strings.ToLower(old) == strings.ToLower(new)
}

View File

@ -0,0 +1,51 @@
package digitalocean
import "testing"
func TestCaseSensitive(t *testing.T) {
cases := []struct {
Name string
Left string
Right string
Suppress bool
}{
{
Name: "empty",
Left: "",
Right: "",
Suppress: true,
},
{
Name: "empty and text",
Left: "text",
Right: "",
Suppress: false,
},
{
Name: "different text",
Left: "text",
Right: "different text",
Suppress: false,
},
{
Name: "same text",
Left: "text",
Right: "text",
Suppress: true,
},
{
Name: "same text different case",
Left: "text",
Right: "TeXT",
Suppress: true,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
if CaseSensitive("test", tc.Left, tc.Right, nil) != tc.Suppress {
t.Fatalf("Expected CaseSensitive to return %t for '%q' == '%q'", tc.Suppress, tc.Left, tc.Right)
}
})
}
}

View File

@ -2,12 +2,36 @@ package digitalocean
import (
"context"
"fmt"
"log"
"regexp"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/schema"
)
var tagNameRe = regexp.MustCompile("^[a-zA-Z0-9:\\-_]{1,255}$")
func tagsSchema() *schema.Schema {
return &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validateTag,
},
Set: HashStringIgnoreCase,
}
}
func validateTag(value interface{}, key string) ([]string, []error) {
if !tagNameRe.MatchString(value.(string)) {
return nil, []error{fmt.Errorf("tags may contain lowercase letters, numbers, colons, dashes, and underscores; there is a limit of 255 characters per tag")}
}
return nil, nil
}
// setTags is a helper to set the tags for a resource. It expects the
// tags field to be named "tags"
func setTags(conn *godo.Client, d *schema.ResourceData) error {
@ -31,7 +55,15 @@ func setTags(conn *godo.Client, d *schema.ResourceData) error {
log.Printf("[DEBUG] Creating tags: %s for %s", create, d.Id())
for _, tag := range create {
_, err := conn.Tags.TagResources(context.Background(), tag, &godo.TagResourcesRequest{
createdTag, _, err := conn.Tags.Create(context.Background(), &godo.TagCreateRequest{
Name: tag,
})
if err != nil {
return err
}
_, err = conn.Tags.TagResources(context.Background(), createdTag.Name, &godo.TagResourcesRequest{
Resources: []godo.Resource{
{
ID: d.Id(),
@ -51,7 +83,7 @@ func setTags(conn *godo.Client, d *schema.ResourceData) error {
// properly asserted map[string]string
func tagsFromSchema(raw interface{}) map[string]string {
result := make(map[string]string)
for _, t := range raw.([]interface{}) {
for _, t := range raw.(*schema.Set).List() {
result[t.(string)] = t.(string)
}
@ -71,3 +103,25 @@ func diffTags(oldTags, newTags map[string]string) (map[string]string, map[string
return oldTags, newTags
}
func expandTags(tags []interface{}) []string {
expandedTags := make([]string, len(tags))
for i, v := range tags {
expandedTags[i] = v.(string)
}
return expandedTags
}
func flattenTags(tags []string) *schema.Set {
if tags == nil {
return nil
}
flattenedTags := schema.NewSet(HashStringIgnoreCase, []interface{}{})
for _, v := range tags {
flattenedTags.Add(v)
}
return flattenedTags
}

View File

@ -3,21 +3,24 @@ package digitalocean
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/schema"
)
func TestDiffTags(t *testing.T) {
cases := []struct {
Old, New []interface{}
Old, New *schema.Set
Create, Remove map[string]string
}{
// Basic add/remove
{
Old: []interface{}{
Old: schema.NewSet(HashStringIgnoreCase, []interface{}{
"foo",
},
New: []interface{}{
}),
New: schema.NewSet(HashStringIgnoreCase, []interface{}{
"bar",
},
}),
Create: map[string]string{
"bar": "bar",
},
@ -28,12 +31,12 @@ func TestDiffTags(t *testing.T) {
// Noop
{
Old: []interface{}{
Old: schema.NewSet(HashStringIgnoreCase, []interface{}{
"foo",
},
New: []interface{}{
}),
New: schema.NewSet(HashStringIgnoreCase, []interface{}{
"foo",
},
}),
Create: map[string]string{},
Remove: map[string]string{},
},
@ -49,3 +52,88 @@ func TestDiffTags(t *testing.T) {
}
}
}
func TestAccDigitalOceanTag_NameValidation(t *testing.T) {
cases := []struct {
Input string
ExpectError bool
}{
{
Input: "",
ExpectError: true,
},
{
Input: "foo",
ExpectError: false,
},
{
Input: "foo-bar",
ExpectError: false,
},
{
Input: "foo:bar",
ExpectError: false,
},
{
Input: "foo_bar",
ExpectError: false,
},
{
Input: "foo-001",
ExpectError: false,
},
{
Input: "foo/bar",
ExpectError: true,
},
{
Input: "foo\bar",
ExpectError: true,
},
{
Input: "foo.bar",
ExpectError: true,
},
{
Input: "foo*",
ExpectError: true,
},
{
Input: acctest.RandString(256),
ExpectError: true,
},
}
for _, tc := range cases {
_, errors := validateTag(tc.Input, tc.Input)
hasError := len(errors) > 0
if tc.ExpectError && !hasError {
t.Fatalf("Expected the DigitalOcean Tag Name to trigger a validation error for '%s'", tc.Input)
}
if hasError && !tc.ExpectError {
t.Fatalf("Unexpected error validating the DigitalOcean Tag Name '%s': %s", tc.Input, errors[0])
}
}
}
func TestExpandTags(t *testing.T) {
tags := []interface{}{"foo", "bar"}
expandedTags := expandTags(tags)
if len(tags) != len(expandedTags) {
t.Fatalf("incorrect expected length of expanded tags")
}
}
func TestFlattenTags(t *testing.T) {
tags := []string{"foo", "bar"}
flattenedTags := flattenTags(tags)
if len(tags) != flattenedTags.Len() {
t.Fatalf("incorrect expected length of flattened tags")
}
}