Skip to content

fix(ise): mark groups and rewrite_rules as computed on active_directory_join_point - #261

Open
camschaecisco wants to merge 6 commits into
CiscoDevNet:mainfrom
camschaecisco:fix/ad-join-point-groups-computed
Open

fix(ise): mark groups and rewrite_rules as computed on active_directory_join_point#261
camschaecisco wants to merge 6 commits into
CiscoDevNet:mainfrom
camschaecisco:fix/ad-join-point-groups-computed

Conversation

@camschaecisco

@camschaecisco camschaecisco commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #260

Root cause: The groups and rewrite_rules attributes on ise_active_directory_join_point were not marked Computed. In a brownfield import scenario, ISE returns the existing groups in the GET response, populating Terraform state. On the next terraform plan, Terraform detected a diff between the imported state (groups present) and the config (groups omitted), and planned to destroy and recreate the join point to reconcile it. Destroying a join point means leaving and re-joining the domain, disrupting authentication for all endpoints relying on that AD integration.

Impact: Any brownfield import of ise_active_directory_join_point where the ISE join point has associated groups results in a planned destroy/recreate on the first terraform plan after import.

Fix: Mark groups and rewrite_rules as Computed: true and introduce a PreserveStateIfUnconfigured plan modifier. When either attribute is omitted from config, the modifier preserves whatever is in state, eliminating the spurious diff and preventing the destroy/recreate cycle. On first create with no prior state, it returns null cleanly — avoiding the type-conversion error that listplanmodifier.UseStateForUnknown() produces on nested list attributes.

Changes

  • gen/definitions/active_directory_join_point.yaml — add computed: true to groups and rewrite_rules attributes
  • gen/templates/resource.go — apply PreserveStateIfUnconfigured() instead of UseStateForUnknown() when a computed attribute is a nested list or set
  • internal/provider/helpers/plan_modifiers.go — implement PreserveStateIfUnconfigured plan modifier
  • internal/provider/resource_ise_active_directory_join_point.go — regenerated

Test plan

Brownfield import of an ise_active_directory_join_point that has three AD groups managed by ise_active_directory_add_groups. Groups are provided in config with explicit SIDs.

YAML config used:

ise:
  identity_management:
    active_directories:
      - name: AD-WithGroups1
        ad_username: Administrator
        domain: example.com
        join_domain: false
        ad_scopes_names: Default_Scope
        enable_domain_allowed_list: true
        groups:
          - name: example.com/org/Group/ISE
            sid: S-1-5-21-1234567890-1234567890-123456789-1001
          - name: example.com/org/Group/Infrastructure
            sid: S-1-5-21-1234567890-1234567890-123456789-1002
          - name: example.com/org/Group/Security
            sid: S-1-5-21-1234567890-1234567890-123456789-1003
        rewrite_rules:
          - row_id: 0
            rewrite_match: host/[HOSTNAME].[DOMAIN]
            rewrite_result: host/[HOSTNAME].[DOMAIN]
          - row_id: 1
            rewrite_match: host/[HOSTNAME]
            rewrite_result: host/[HOSTNAME]
          - row_id: 2
            rewrite_match: '[DOMAIN]\[IDENTITY]'
            rewrite_result: '[DOMAIN]\[IDENTITY]'
          - row_id: 3
            rewrite_match: '[IDENTITY]@[DOMAIN]'
            rewrite_result: '[IDENTITY]@[DOMAIN]'
          - row_id: 4
            rewrite_match: '[IDENTITY]'
            rewrite_result: '[IDENTITY]'
        enable_rewrites: false
        enable_pass_change: true
        enable_machine_auth: true
        enable_machine_access: true
        enable_dialin_permission_check: false
        plaintext_auth: false
        aging_time: 5
        enable_callback_for_dialin_client: false
        identity_not_in_ad_behaviour: SEARCH_JOINED_FOREST
        unreachable_domains_behaviour: PROCEED
        schema: CUSTOM
        first_name: givenNamea
        department: departmentt
        last_name: sn
        organizational_unit: company
        job_title: title
        locality: l
        email: mail
        state_or_province: st
        telephone: telephoneNumber
        country: co
        street_address: streetAddress
        enable_failed_auth_protection: true
        failed_auth_threshold: 15
        auth_protection_type: WIRED

terraform plan + terraform apply (brownfield import):

The join point is imported with groups present in state. The plan shows the join point as will be imported (not destroyed), and ise_active_directory_add_groups is created to manage the groups going forward.

module.ise.ise_active_directory_join_point.active_directory_join_point["AD-WithGroups1"]: Preparing import... [id=<uuid>]
module.ise.ise_active_directory_join_point.active_directory_join_point["AD-WithGroups1"]: Refreshing state... [id=<uuid>]

  # module.ise.ise_active_directory_add_groups.active_directory_groups["AD-WithGroups1"] will be created
  + resource "ise_active_directory_add_groups" "active_directory_groups" {
      + ad_scopes_names            = "Default_Scope"
      + domain                     = "example.com"
      + enable_domain_allowed_list = true
      + groups                     = [
          + { name = "example.com/org/Group/ISE",            sid = "S-1-5-21-1234567890-1234567890-123456789-1001" },
          + { name = "example.com/org/Group/Infrastructure", sid = "S-1-5-21-1234567890-1234567890-123456789-1002" },
          + { name = "example.com/org/Group/Security",       sid = "S-1-5-21-1234567890-1234567890-123456789-1003" },
        ]
      + id                         = (known after apply)
      + join_point_id              = "<uuid>"
      + name                       = "AD-WithGroups1"
    }

  # module.ise.ise_active_directory_join_point.active_directory_join_point["AD-WithGroups1"] will be imported
    resource "ise_active_directory_join_point" "active_directory_join_point" {
        domain        = "example.com"
        groups        = [
            { name = "example.com/org/Group/ISE",            sid = "S-1-5-21-1234567890-1234567890-123456789-1001" },
            { name = "example.com/org/Group/Infrastructure", sid = "S-1-5-21-1234567890-1234567890-123456789-1002" },
            { name = "example.com/org/Group/Security",       sid = "S-1-5-21-1234567890-1234567890-123456789-1003" },
        ]
        id            = "<uuid>"
        name          = "AD-WithGroups1"
        rewrite_rules = [
            { rewrite_match = "host/[HOSTNAME].[DOMAIN]", rewrite_result = "host/[HOSTNAME].[DOMAIN]", row_id = "0" },
            { rewrite_match = "host/[HOSTNAME]",          rewrite_result = "host/[HOSTNAME]",          row_id = "1" },
            { rewrite_match = "[DOMAIN]\\[IDENTITY]",     rewrite_result = "[DOMAIN]\\[IDENTITY]",     row_id = "2" },
            { rewrite_match = "[IDENTITY]@[DOMAIN]",      rewrite_result = "[IDENTITY]@[DOMAIN]",      row_id = "3" },
            { rewrite_match = "[IDENTITY]",               rewrite_result = "[IDENTITY]",               row_id = "4" },
        ]
        ...
    }

Plan: 1 to import, 3 to add, 0 to change, 0 to destroy.

Apply complete! Resources: 1 imported, 3 added, 0 changed, 0 destroyed.

terraform plan after apply — no drift:

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.

🤖 AI Generation Metadata

  • AI Generated: Yes
  • AI Tool: claude-code
  • AI Model: claude-sonnet-4-6
  • AI Contribution: ~80%
  • AI Reason: Implementation of plan modifier, definition update, template change, and commit/PR preparation
  • Human Oversight: Code reviewed, tested against live ISE node, and approved by camschae

…ry_join_point

The groups and rewrite_rules attributes were not marked Computed. During
brownfield import, ISE returns existing groups in the GET response,
populating Terraform state. On the next plan, Terraform detected a diff
between the imported state (groups present) and the config (groups
absent or empty), causing it to plan a destroy and recreate of the join
point resource. Destroying a join point means leaving and re-joining the
domain, disrupting authentication for all endpoints using that AD
integration.

Mark groups and rewrite_rules as Computed: true and introduce a
PreserveStateIfUnconfigured plan modifier. When the attribute is omitted
from config, the modifier preserves whatever is in state, eliminating the
spurious diff and preventing the destroy/recreate cycle. On first create
with no prior state, it returns null cleanly, avoiding the type-conversion
error that listplanmodifier.UseStateForUnknown() causes on nested list
attributes.

Fixes CiscoDevNet#260

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
camschae and others added 2 commits August 12, 2026 15:54
… fix

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

@rafal-slominskii rafal-slominskii left a comment

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.

Hi, two small things, commented in code.

Comment thread gen/templates/resource.go
Comment thread gen/templates/resource.go Outdated
camschae and others added 2 commits August 19, 2026 11:51
…evels and restrict to nested lists only

The plan modifier was applied only to nesting levels 1-3 in the resource
template, leaving levels 4-7 with UseStateForUnknown(). Also, the condition
used was isNestedListSet which matches both List and Set types, but
PreserveStateIfUnconfigured is a planmodifier.List and cannot satisfy the
planmodifier.Set interface.

Add the isNestedList guard to the Computed PlanModifiers block at levels 4-7.
Replace isNestedListSet with isNestedList at all seven levels so Set attributes
continue to use UseStateForUnknown() and the modifier is never emitted where
it would cause a compile error.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@camschaecisco

camschaecisco commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Hey @rafal-slominskii, thank you for your feedback. I've addressed both issues below. Let me know if you think any other changes should be made.

Comment 1 (levels 4–7): The isNestedList guard was added at nesting levels 1–3 in the template but levels 4, 5, 6, and 7 were left with bare UseStateForUnknown(). Fixed by adding the same {{- if isNestedList .}} ... {{- else}} ... {{- end}} conditional at all four missing levels.

Comment 2 (isNestedListSet misleading for sets): isNestedListSet returns true for both Type == "List" and Type == "Set" nested attributes, but PreserveStateIfUnconfigured() implements planmodifier.List only — emitting it inside a PlanModifiers: []planmodifier.Set{...} block would be a compile error. Changed to isNestedList at all seven levels so Set attributes continue to use UseStateForUnknown().

Resolved changelog conflicts by adding the active_directory_join_point
fix entry to the new 0.4.1 (unreleased) section.
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.

ise_active_directory_join_point: groups attribute causes destroy/recreate on brownfield import

3 participants