Skip to content

TF-39573: Added TagSelectorMatchingLogic field to policy set structs - #1436

Open
anubhav-goel wants to merge 1 commit into
mainfrom
anubhav-goel/TF-39147-TagSelectorMatchingLogic
Open

TF-39573: Added TagSelectorMatchingLogic field to policy set structs#1436
anubhav-goel wants to merge 1 commit into
mainfrom
anubhav-goel/TF-39147-TagSelectorMatchingLogic

Conversation

@anubhav-goel

@anubhav-goel anubhav-goel commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

Add TagSelectorMatchingLogic *string field to PolicySet, PolicySetCreateOptions,
and PolicySetUpdateOptions in the v1 package.

This field maps to the tag-selector-matching-logic JSON-API attribute and controls
how multiple tag selectors are combined when scoping a policy set to workspaces:

  • "any" (default) - OR semantics: applies to workspaces matching any one selector
  • "all" - AND semantics: applies only to workspaces matching all selectors simultaneously

https://hashicorp.atlassian.net/browse/TF-39573

Testing

Script

export TFE_ADDRESS="<>"
export TFE_TOKEN="<>"
export ENABLE_BETA=1
export TFE_ORG="anubhav-1"
go test -v -run 'TestPolicySetsCreateWithMatchingLogicAll|TestPolicySetsUpdateMatchingLogic|TestPolicySetsCreateMatchingLogicInvalidValue' ./...

Output:

=== RUN   TestPolicySetsCreateWithMatchingLogicAll
--- PASS: TestPolicySetsCreateWithMatchingLogicAll (0.76s)
=== RUN   TestPolicySetsUpdateMatchingLogic
--- PASS: TestPolicySetsUpdateMatchingLogic (0.62s)
=== RUN   TestPolicySetsCreateMatchingLogicInvalidValue
--- PASS: TestPolicySetsCreateMatchingLogicInvalidValue (0.84s)
PASS
ok      github.com/hashicorp/go-tfe     4.028s
?       github.com/hashicorp/go-tfe/mocks       [no test files]

Test file:

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: MPL-2.0

package tfe

import (
	"context"
	"os"
	"testing"
)

// TestPolicySetsCreateWithMatchingLogicAll verifies that a policy set can be
// created with TagSelectorMatchingLogic set to "all" (AND semantics) and that
// the value is reflected in the read response.
// Requires TFE_ADDRESS and TFE_TOKEN env vars pointing to a live TFE instance
// with the tag_selector_matching_logic DB migration deployed.
func TestPolicySetsCreateWithMatchingLogicAll(t *testing.T) {
	if os.Getenv("TFE_ADDRESS") == "" || os.Getenv("TFE_TOKEN") == "" {
		t.Skip("TFE_ADDRESS and TFE_TOKEN must be set for this test")
	}

	client, err := NewClient(&Config{
		Address: os.Getenv("TFE_ADDRESS"),
		Token:   os.Getenv("TFE_TOKEN"),
	})
	if err != nil {
		t.Fatalf("failed to create client: %v", err)
	}

	org := os.Getenv("TFE_ORG")
	if org == "" {
		t.Skip("TFE_ORG must be set for this test")
	}

	ctx := context.Background()
	matchingLogic := "all"
	tagKey := "sentinel"
	tagVal := "true"
	ps, err := client.PolicySets.Create(ctx, org, PolicySetCreateOptions{
		Name:                     String("test-matching-logic-all"),
		TagSelectorMatchingLogic: &matchingLogic,
		TagSelectors: []*PolicySetTagSelector{
			{Key: tagKey, Value: &tagVal, IsExclude: false},
		},
	})
	if err != nil {
		t.Fatalf("Create failed: %v", err)
	}
	defer func() {
		if delErr := client.PolicySets.Delete(ctx, ps.ID); delErr != nil {
			t.Logf("cleanup Delete failed: %v", delErr)
		}
	}()

	got, err := client.PolicySets.Read(ctx, ps.ID)
	if err != nil {
		t.Fatalf("Read failed: %v", err)
	}
	if got.TagSelectorMatchingLogic == nil {
		t.Fatal("expected TagSelectorMatchingLogic to be non-nil after create with 'all'")
	}
	if *got.TagSelectorMatchingLogic != "all" {
		t.Errorf("expected TagSelectorMatchingLogic = 'all', got %q", *got.TagSelectorMatchingLogic)
	}
}

// TestPolicySetsUpdateMatchingLogic verifies that TagSelectorMatchingLogic can
// be changed on an existing policy set via Update.
// Requires TFE_ADDRESS, TFE_TOKEN, and TFE_ORG env vars.
func TestPolicySetsUpdateMatchingLogic(t *testing.T) {
	if os.Getenv("TFE_ADDRESS") == "" || os.Getenv("TFE_TOKEN") == "" {
		t.Skip("TFE_ADDRESS and TFE_TOKEN must be set for this test")
	}

	client, err := NewClient(&Config{
		Address: os.Getenv("TFE_ADDRESS"),
		Token:   os.Getenv("TFE_TOKEN"),
	})
	if err != nil {
		t.Fatalf("failed to create client: %v", err)
	}

	org := os.Getenv("TFE_ORG")
	if org == "" {
		t.Skip("TFE_ORG must be set for this test")
	}

	ctx := context.Background()
	tagKey := "sentinel"
	tagVal := "true"
	ps, err := client.PolicySets.Create(ctx, org, PolicySetCreateOptions{
		Name: String("test-matching-logic-update"),
		TagSelectors: []*PolicySetTagSelector{
			{Key: tagKey, Value: &tagVal, IsExclude: false},
		},
	})
	if err != nil {
		t.Fatalf("Create failed: %v", err)
	}
	defer func() {
		if delErr := client.PolicySets.Delete(ctx, ps.ID); delErr != nil {
			t.Logf("cleanup Delete failed: %v", delErr)
		}
	}()

	updated := "all"
	_, err = client.PolicySets.Update(ctx, ps.ID, PolicySetUpdateOptions{
		TagSelectorMatchingLogic: &updated,
	})
	if err != nil {
		t.Fatalf("Update failed: %v", err)
	}

	got, err := client.PolicySets.Read(ctx, ps.ID)
	if err != nil {
		t.Fatalf("Read after update failed: %v", err)
	}
	if got.TagSelectorMatchingLogic == nil {
		t.Fatal("expected TagSelectorMatchingLogic to be non-nil after update")
	}
	if *got.TagSelectorMatchingLogic != "all" {
		t.Errorf("expected TagSelectorMatchingLogic = 'all' after update, got %q", *got.TagSelectorMatchingLogic)
	}
}

// TestPolicySetsCreateMatchingLogicInvalidValue verifies that client-side
// validation rejects invalid TagSelectorMatchingLogic values before making
// any HTTP request. This test does NOT require a live TFE instance.
func TestPolicySetsCreateMatchingLogicInvalidValue(t *testing.T) {
	client, err := NewClient(&Config{
		Address: "https://app.terraform.io",
		Token:   "placeholder-not-used",
	})
	if err != nil {
		t.Fatalf("failed to create client: %v", err)
	}

	invalid := "invalid"
	_, err = client.PolicySets.Create(context.Background(), "org-test", PolicySetCreateOptions{
		Name:                     String("test-invalid-matching-logic"),
		TagSelectorMatchingLogic: &invalid,
	})
	if err != ErrInvalidTagSelectorMatchingLogic {
		t.Fatalf("expected ErrInvalidTagSelectorMatchingLogic, got: %v", err)
	}
}

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.
  • If applicable, I've documented a plan to revert these changes if they require more
    than reverting the pull request.
  • If applicable, I've documented the impact of any changes to security controls.

@anubhav-goel
anubhav-goel requested a review from a team as a code owner August 13, 2026 10:28
@anubhav-goel anubhav-goel changed the title Added TagSelectorMatchingLogic field to policy set structs TF-39573: Added TagSelectorMatchingLogic field to policy set structs Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant