From bb32e3acc68fe7a5d96fecb1154ccbd901c2b999 Mon Sep 17 00:00:00 2001 From: uk1288 Date: Mon, 20 Jun 2022 16:45:08 -0400 Subject: [PATCH 1/2] add exclude_tags to data_source_workspace_ids --- tfe/data_source_workspace_ids.go | 26 ++++- tfe/data_source_workspace_ids_test.go | 139 ++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/tfe/data_source_workspace_ids.go b/tfe/data_source_workspace_ids.go index c77357251..8d39f9a7b 100644 --- a/tfe/data_source_workspace_ids.go +++ b/tfe/data_source_workspace_ids.go @@ -26,6 +26,12 @@ func dataSourceTFEWorkspaceIDs() *schema.Resource { Optional: true, }, + "exclude_tags": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "organization": { Type: schema.TypeString, Required: true, @@ -70,11 +76,27 @@ func dataSourceTFEWorkspaceIDsRead(d *schema.ResourceData, meta interface{}) err options := &tfe.WorkspaceListOptions{} + excludeTagLookupMap := make(map[string]bool) + var excludeTagBuf strings.Builder + for _, excludedTag := range d.Get("exclude_tags").([]interface{}) { + if exTag, ok := excludedTag.(string); ok && len(strings.TrimSpace(exTag)) != 0 { + excludeTagLookupMap[exTag] = true + + if excludeTagBuf.Len() > 0 { + excludeTagBuf.WriteByte(',') + } + excludeTagBuf.WriteString(exTag) + } + } + + if excludeTagBuf.Len() > 0 { + options.ExcludeTags = excludeTagBuf.String() + } + // Create a search string with all the tag names we are looking for. var tagSearchParts []string for _, tagName := range d.Get("tag_names").([]interface{}) { - name := tagName.(string) - if len(strings.TrimSpace(name)) != 0 { + if name, ok := tagName.(string); ok && len(strings.TrimSpace(name)) != 0 { id += name // add to the state id tagSearchParts = append(tagSearchParts, name) } diff --git a/tfe/data_source_workspace_ids_test.go b/tfe/data_source_workspace_ids_test.go index 54a21637e..6985fab18 100644 --- a/tfe/data_source_workspace_ids_test.go +++ b/tfe/data_source_workspace_ids_test.go @@ -263,6 +263,72 @@ func TestAccTFEWorkspaceIDsDataSource_namesEmpty(t *testing.T) { }) } +func TestAccTFEWorkspaceIDsDataSource_excludeTags(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + orgName := fmt.Sprintf("tst-terraform-%d", rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFEWorkspaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspaceIDsDataSourceConfig_excludeTags(rInt), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "data.tfe_workspace_ids.good", "organization", orgName), + + // full_names attribute + resource.TestCheckResourceAttr( + "data.tfe_workspace_ids.good", "full_names.%", "1"), + resource.TestCheckResourceAttr( + "data.tfe_workspace_ids.good", + fmt.Sprintf("full_names.workspace-bar-%d", rInt), + fmt.Sprintf("tst-terraform-%d/workspace-bar-%d", rInt, rInt), + ), + + // ids attribute + resource.TestCheckResourceAttr( + "data.tfe_workspace_ids.good", "ids.%", "1"), + resource.TestCheckResourceAttrSet( + "data.tfe_workspace_ids.good", fmt.Sprintf("ids.workspace-bar-%d", rInt)), + + // id attribute + resource.TestCheckResourceAttrSet("data.tfe_workspace_ids.good", "id"), + ), + }, + }, + }) +} + +func TestAccTFEWorkspaceIDsDataSource_sameTagInTagNamesAndExcludeTags(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + orgName := fmt.Sprintf("tst-terraform-%d", rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFEWorkspaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspaceIDsDataSourceConfig_sameTagInTagNamesAndExcludeTags(rInt), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "data.tfe_workspace_ids.good", "organization", orgName), + + // full_names attribute should be empty + resource.TestCheckResourceAttr( + "data.tfe_workspace_ids.good", "full_names.%", "0"), + + // ids attribute should be empty + resource.TestCheckResourceAttr( + "data.tfe_workspace_ids.good", "ids.%", "0"), + ), + }, + }, + }) +} + func testAccTFEWorkspaceIDsDataSourceConfig_basic(rInt int) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { @@ -427,3 +493,76 @@ data "tfe_workspace_ids" "good" { organization = tfe_workspace.foo.organization }`, rInt, rInt, rInt, rInt) } + +func testAccTFEWorkspaceIDsDataSourceConfig_excludeTags(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_workspace" "foo" { + name = "workspace-foo-%d" + organization = tfe_organization.foobar.id + tag_names = ["good", "happy"] +} + +resource "tfe_workspace" "bar" { + name = "workspace-bar-%d" + organization = tfe_organization.foobar.id + tag_names = ["good"] +} + +resource "tfe_workspace" "dummy" { + name = "workspace-dummy-%d" + organization = tfe_organization.foobar.id +} + +data "tfe_workspace_ids" "good" { + tag_names = ["good"] + exclude_tags = ["happy"] + organization = tfe_workspace.foo.organization + depends_on = [ + tfe_workspace.foo, + tfe_workspace.bar, + tfe_workspace.dummy + ] +}`, rInt, rInt, rInt, rInt) +} + +func testAccTFEWorkspaceIDsDataSourceConfig_sameTagInTagNamesAndExcludeTags(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_workspace" "foo" { + name = "workspace-foo-%d" + organization = tfe_organization.foobar.id + tag_names = ["good", "happy"] +} + +resource "tfe_workspace" "bar" { + name = "workspace-bar-%d" + organization = tfe_organization.foobar.id + tag_names = ["happy", "play"] +} + +resource "tfe_workspace" "dummy" { + name = "workspace-dummy-%d" + organization = tfe_organization.foobar.id + tag_names = ["good", "play", "happy"] +} + +data "tfe_workspace_ids" "good" { + tag_names = ["good", "happy"] + exclude_tags = ["happy"] + organization = tfe_workspace.foo.organization + depends_on = [ + tfe_workspace.foo, + tfe_workspace.bar, + tfe_workspace.dummy + ] +}`, rInt, rInt, rInt, rInt) +} From 5e0f62a648a47e811df83abf374beff292a89bd6 Mon Sep 17 00:00:00 2001 From: uk1288 Date: Tue, 21 Jun 2022 14:18:12 -0400 Subject: [PATCH 2/2] update markdown and changelog --- CHANGELOG.md | 1 + tfe/data_source_workspace_ids.go | 14 +++++++++++--- tfe/data_source_workspace_ids_test.go | 8 ++++---- website/docs/d/workspace_ids.html.markdown | 9 ++++++++- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10e0a6f00..e5caf047c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ FEATURES: * **New Data Source**: d/tfe_organization_run_task ([#488](https://github.com/hashicorp/terraform-provider-tfe/pull/488)) * **New Data Source**: d/tfe_workspace_run_task ([#488](https://github.com/hashicorp/terraform-provider-tfe/pull/488)) * r/tfe_notification_configuration: Add Microsoft Teams notification type ([#484](https://github.com/hashicorp/terraform-provider-tfe/pull/484)) +* d/workspace_ids: Add `exclude_tags` to `tfe_workspace_ids` attributes ([#523](https://github.com/hashicorp/terraform-provider-tfe/pull/523)) ## 0.31.0 (April 21, 2022) diff --git a/tfe/data_source_workspace_ids.go b/tfe/data_source_workspace_ids.go index 8d39f9a7b..06683a765 100644 --- a/tfe/data_source_workspace_ids.go +++ b/tfe/data_source_workspace_ids.go @@ -27,7 +27,7 @@ func dataSourceTFEWorkspaceIDs() *schema.Resource { }, "exclude_tags": { - Type: schema.TypeList, + Type: schema.TypeSet, Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, }, @@ -78,7 +78,7 @@ func dataSourceTFEWorkspaceIDsRead(d *schema.ResourceData, meta interface{}) err excludeTagLookupMap := make(map[string]bool) var excludeTagBuf strings.Builder - for _, excludedTag := range d.Get("exclude_tags").([]interface{}) { + for _, excludedTag := range d.Get("exclude_tags").(*schema.Set).List() { if exTag, ok := excludedTag.(string); ok && len(strings.TrimSpace(exTag)) != 0 { excludeTagLookupMap[exTag] = true @@ -116,7 +116,15 @@ func dataSourceTFEWorkspaceIDsRead(d *schema.ResourceData, meta interface{}) err for _, w := range wl.Items { nameIncluded := isWildcard || names[w.Name] - if hasOnlyTags || nameIncluded { + // fallback for tfe instances that don't yet support exclude-tags + hasExcludedTag := false + for _, tag := range w.TagNames { + if _, ok := excludeTagLookupMap[tag]; ok { + hasExcludedTag = true + break + } + } + if (hasOnlyTags || nameIncluded) && !hasExcludedTag { fullNames[w.Name] = organization + "/" + w.Name ids[w.Name] = w.ID } diff --git a/tfe/data_source_workspace_ids_test.go b/tfe/data_source_workspace_ids_test.go index 6985fab18..e27cc6f46 100644 --- a/tfe/data_source_workspace_ids_test.go +++ b/tfe/data_source_workspace_ids_test.go @@ -520,9 +520,9 @@ resource "tfe_workspace" "dummy" { data "tfe_workspace_ids" "good" { tag_names = ["good"] - exclude_tags = ["happy"] + exclude_tags = ["happy"] organization = tfe_workspace.foo.organization - depends_on = [ + depends_on = [ tfe_workspace.foo, tfe_workspace.bar, tfe_workspace.dummy @@ -557,9 +557,9 @@ resource "tfe_workspace" "dummy" { data "tfe_workspace_ids" "good" { tag_names = ["good", "happy"] - exclude_tags = ["happy"] + exclude_tags = ["happy"] organization = tfe_workspace.foo.organization - depends_on = [ + depends_on = [ tfe_workspace.foo, tfe_workspace.bar, tfe_workspace.dummy diff --git a/website/docs/d/workspace_ids.html.markdown b/website/docs/d/workspace_ids.html.markdown index e57367f52..060093aef 100644 --- a/website/docs/d/workspace_ids.html.markdown +++ b/website/docs/d/workspace_ids.html.markdown @@ -27,6 +27,12 @@ data "tfe_workspace_ids" "prod-apps" { tag_names = ["prod", "app", "aws"] organization = "my-org-name" } + +data "tfe_workspace_ids" "prod-only" { + tag_names = ["prod"] + exclude_tags = ["app"] + organization = "my-org-name" +} ``` ## Argument Reference @@ -39,11 +45,12 @@ The following arguments are supported. At least one of `names` or `tag_names` mu To select _all_ workspaces for an organization, provide a list with a single asterisk, like `["*"]`. No other use of wildcards is supported. * `tag_names` - (Optional) A list of tag names to search for. +* `exclude_tags` - (Optional) A list of tag names to exclude when searching. * `organization` - (Required) Name of the organization. ## Attributes Reference In addition to all arguments above, the following attributes are exported: -* `full_names` - A map of workspace names and their full names, which look like `/`. +* `full_names` - A map of workspace names and their full names, which look like `/`. * `ids` - A map of workspace names and their opaque, immutable IDs, which look like `ws-`.