Skip to content

Auto-detach policy from watches on delete to fix circular dependency(#358) - #428

Open
soumyas-dev wants to merge 3 commits into
mainfrom
JTFPR-358-1
Open

Auto-detach policy from watches on delete to fix circular dependency(#358)#428
soumyas-dev wants to merge 3 commits into
mainfrom
JTFPR-358-1

Conversation

@soumyas-dev

Copy link
Copy Markdown
Collaborator

Auto-detach policy from watches before deletion to resolve circular dependency between policy and watch resources.

Fixes #358

Problem

When a policy (xray_security_policy, xray_license_policy, or xray_operational_risk_policy) is attached to a watch via the assigned_policy block, terraform destroy fails because the Xray API rejects policy deletion with "Policy is assigned to N watches". Users had to manually detach policies from watches before destroying them, making automated teardown impossible.

Implementation

Changed files

pkg/xray/resource/policies.go (3 new functions/constants, 1 modified function)

deletePolicy(policyName, projectKey) (int, string, error)

Extracted helper that sends DELETE /xray/api/v2/policies/{name} and returns the HTTP status code, error message, and any transport error. Used by both the initial optimistic attempt and the retry after detach.

detachPolicyFromWatches(ctx, policyName, projectKey) diag.Diagnostics

Handles the detach-from-watches flow:

  1. List watchesGET /xray/api/v2/watches (single API call, scoped by projectKey if set)
  2. Filter client-side — Iterates all watches, identifies those whose assigned_policies contain the target policy name, and builds a list of watches to update with the policy removed
  3. Early exit — If no watches reference the policy, returns immediately (zero update calls)
  4. Parallel updates — Spawns a goroutine per watch, capped at maxConcurrentWatchUpdates = 10 concurrent requests using a buffered channel as a semaphore. Each goroutine sends PUT /xray/api/v2/watches/{name} with the policy removed from assigned_policies
  5. Error aggregation — Uses sync.Mutex to collect errors from all goroutines. Returns all failures as Terraform diagnostics rather than failing fast, so the user sees every watch that failed to update

Concurrency primitives used:

  • sync.WaitGroup — waits for all goroutines to complete
  • chan struct{} (buffered, size 10) — semaphore to limit concurrent API calls
  • sync.Mutex — protects the shared error slice

Delete(ctx, req, resp) (modified)

Now implements an optimistic-delete-then-detach strategy:

  1. Attempt delete — Calls deletePolicy() directly
  2. Detect "in use" error — Checks if the response indicates the policy is attached:
    • HTTP 409 Conflict status code, OR
    • Error message contains "assigned" (matches "Policy is assigned to N watches"), OR
    • Error message contains "attached" (future-proofing for API wording changes)
  3. Detach and retry — If policy is in use, calls detachPolicyFromWatches() then retries deletePolicy()
  4. Final error check — If the retry still fails (status >= 400), reports the error

API call count by scenario:

Scenario API calls
Policy not attached to any watch 1 (DELETE)
Policy attached to N watches 1 (DELETE) + 1 (GET list) + N (PUT updates, parallel) + 1 (DELETE retry)

CHANGELOG.md

Added entry under ## 3.1.12 documenting the fix.

Test details

pkg/xray/resource/resource_xray_security_policy_test.go

TestAccSecurityPolicy_deleteDetachesFromWatch

Acceptance test that verifies the auto-detach logic works end-to-end against a live Xray instance.

Step 1 — Uses policyWithWatchTemplate to create:

  • xray_security_policy.<resource_name> — A security policy with CVSS criteria
  • xray_watch.test — A watch on all repos with the policy attached via assigned_policy block (references the policy by name, creating an implicit Terraform dependency)

Checks:

  • Policy name matches expected value
  • Watch's assigned_policy.0.name references the original policy

Step 2 — Uses watchWithReplacementPolicyTemplate which:

  • Removes the original policy resource from config entirely
  • Creates a new xray_security_policy.replacement with a different name
  • Updates xray_watch.test to reference the replacement policy

This forces Terraform to:

  1. Create the replacement policy
  2. Update the watch to point to the replacement
  3. Delete the original policy — this is where the auto-detach logic is exercised, since the watch was referencing the original policy at the time of deletion

Checks:

  • Watch's assigned_policy.0.name now references the replacement policy

CheckDestroy — Verifies the replacement policy (the one that exists in the final state) is properly cleaned up after the test. The original policy was already verified deleted by step 2 succeeding.

Templates

  • policyWithWatchTemplate — Creates a security policy + watch with the policy attached
  • watchWithReplacementPolicyTemplate — Creates a different policy + same watch pointing to the new policy (original policy removed from config)

Test plan

  • TestAccSecurityPolicy_deleteDetachesFromWatch passes
  • Existing TestAccSecurityPolicy_* tests still pass
  • Existing TestAccWatch_* tests still pass (watch CRUD unaffected)
  • go build ./... and go vet ./... pass cleanly

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.

Resolve circular dependency between Policy and Watch

2 participants