Skip to content
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
94 changes: 94 additions & 0 deletions cloudscale/datasource_cloudscale_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,73 @@ func TestAccCloudscaleServer_DS_NotExisting(t *testing.T) {
})
}

// Tag filtering is implemented generically in dataSourceResourceRead (datasources.go),
// so this test covers all data sources; no need to duplicate it per resource.
func TestAccCloudscaleServer_DS_FilterByTags(t *testing.T) {
var server cloudscale.Server
rInt := acctest.RandInt()
name := fmt.Sprintf("terraform-%d", rInt)
config := serverConfig_baselineWithTag(rInt)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudscaleServerDestroy,
Steps: []resource.TestStep{
{
Config: config,
},
{
Config: config + testAccCheckCloudscaleServerConfig_tags(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudscaleServerExists("data.cloudscale_server.foo", &server),
resource.TestCheckResourceAttrPtr(
"cloudscale_server.basic", "id", &server.UUID),
resource.TestCheckResourceAttrPtr(
"data.cloudscale_server.foo", "id", &server.UUID),
resource.TestCheckResourceAttr(
"data.cloudscale_server.foo", "name", name),
),
},
{
Config: config + testAccCheckCloudscaleServerConfig_tags(rInt+1),
ExpectError: regexp.MustCompile(`Found zero servers`),
},
},
})
}

func TestAccCloudscaleServer_DS_FilterByTags_NoTags(t *testing.T) {
rInt := acctest.RandInt()
config := serverConfig_baseline(2, rInt)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudscaleServerDestroy,
Steps: []resource.TestStep{
{
Config: config,
},
{
// No filter at all: matches all servers.
Config: config + "\n" + `data "cloudscale_server" "foo" {}`,
ExpectError: regexp.MustCompile(`Found \d+ servers, expected one`),
},
{
// tags = {} is identical: it also matches all servers without tags.
Config: config + "\n" + `data "cloudscale_server" "foo" { tags = {} }`,
ExpectError: regexp.MustCompile(`Found \d+ servers, expected one`),
},
{
// A server with no tags must not match a non-empty tag filter.
Config: config + testAccCheckCloudscaleServerConfig_tags(rInt),
ExpectError: regexp.MustCompile(`Found zero servers`),
},
},
})
}

func testAccCheckCloudscaleServerConfig_name(name string) string {
return fmt.Sprintf(`
data "cloudscale_server" "foo" {
Expand Down Expand Up @@ -154,3 +221,30 @@ resource "cloudscale_server" "basic" {
ssh_keys = ["ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFEepRNW5hDct4AdJ8oYsb4lNP5E9XY5fnz3ZvgNCEv7m48+bhUjJXUPuamWix3zigp2lgJHC6SChI/okJ41GUY=", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFEepRNW5hDct4AdJ8oYsb4lNP5E9XY5fnz3ZvgNCEv7m48+bhUjJXUPuamWix3zigp2lgJHC6SChI/okJ41GUY="]
}`, count, rInt, DefaultImageSlug)
}

func serverConfig_baselineWithTag(rInt int) string {
return fmt.Sprintf(`
resource "cloudscale_server" "basic" {
name = "terraform-%d"
flavor_slug = "flex-4-1"
allow_stopping_for_update = true
image_slug = "%s"
volume_size_gb = 10
zone_slug = "rma1"
ssh_keys = ["ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFEepRNW5hDct4AdJ8oYsb4lNP5E9XY5fnz3ZvgNCEv7m48+bhUjJXUPuamWix3zigp2lgJHC6SChI/okJ41GUY=", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFEepRNW5hDct4AdJ8oYsb4lNP5E9XY5fnz3ZvgNCEv7m48+bhUjJXUPuamWix3zigp2lgJHC6SChI/okJ41GUY="]
tags = {
test-id = "%d"
provider = "cloudscale-tf-test"
}
}`, rInt, DefaultImageSlug, rInt)
}

func testAccCheckCloudscaleServerConfig_tags(rInt int) string {
return fmt.Sprintf(`
data "cloudscale_server" "foo" {
tags = {
test-id = "%d"
}
}
`, rInt)
}
45 changes: 41 additions & 4 deletions cloudscale/datasources.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cloudscale

import (
"context"
"reflect"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -29,14 +30,35 @@ func dataSourceResourceRead(
}
var foundItems []map[string]any

// Filter resources: each set attribute must match (maps use subset semantics).
for _, m := range resources {
match := true
for key := range sourceSchema {
if attr, ok := d.GetOk(key); ok {
if m[key] != attr {
for key, schemaEntry := range sourceSchema {
attr, ok := d.GetOk(key)
if !ok {
continue // not a filter criterion
}
if schemaEntry.Type == schema.TypeMap {
// Tags: all filter key-value pairs must be present in the resource (subset, not exact).
filterMap := attr.(map[string]interface{})
resourceMap, _ := m[key].(map[string]interface{})
for fk, fv := range filterMap {
if resourceMap[fk] != fv {
match = false
break // one tag mismatch is sufficient
}
}
} else if schemaEntry.Type == schema.TypeList || schemaEntry.Type == schema.TypeSet {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

TypeSet

TypeSet returns a *schema.Set which isn't handled by toInterfaceSlice.

// Gather functions return []string from the SDK struct; d.GetOk returns []interface{}.
// Normalise before comparing so reflect.DeepEqual sees the same dynamic type.
if !reflect.DeepEqual(toInterfaceSlice(m[key]), attr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is equal match right or subset match here too?
also: order matters or not? I guess with sets it should not matter, with lists I'm not sure.

match = false
break
}
} else if !reflect.DeepEqual(m[key], attr) {
match = false
}
if !match {
break // skip remaining attributes
}
}
if match {
Expand All @@ -57,6 +79,21 @@ func dataSourceResourceRead(
}
}

func toInterfaceSlice(v any) []interface{} {
switch s := v.(type) {
case []string:
result := make([]interface{}, len(s))
for i, str := range s {
result[i] = str
}
return result
case []interface{}:
return s
default:
return nil
}
}

Comment on lines +82 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: we probably should use either interface{} or any (any == interface{})

Suggested change
func toInterfaceSlice(v any) []interface{} {
switch s := v.(type) {
case []string:
result := make([]interface{}, len(s))
for i, str := range s {
result[i] = str
}
return result
case []interface{}:
return s
default:
return nil
}
}
func toAnySlice(v any) []any {
switch s := v.(type) {
case []string:
result := make([]any, len(s))
for i, str := range s {
result[i] = str
}
return result
case []any:
return s
default:
return nil
}
}

func getFetchFunc[TResource any](
listFunc func(d *schema.ResourceData, meta any) ([]TResource, error),
gatherFunc func(resource *TResource) ResourceDataRaw,
Expand Down
Loading