Skip to content

feat!: key-address launchdarkly_project environments (REL-14236)#481

Open
monsagri wants to merge 6 commits into
preview-v3from
ffeldberg/REL-14236/environments-map
Open

feat!: key-address launchdarkly_project environments (REL-14236)#481
monsagri wants to merge 6 commits into
preview-v3from
ffeldberg/REL-14236/environments-map

Conversation

@monsagri

@monsagri monsagri commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements REL-14236 (epic REL-13579): re-model launchdarkly_project.environments from a positional list to a map keyed by environment key.

# before (v2 / early v3 preview)        # after (this PR)
environments = [{                       environments = {
  key   = "production"                    "production" = {
  name  = "Production"                      name  = "Production"
  color = "EEEEEE"                          color = "EEEEEE"
}]                                        }
                                        }

Reordering, adding, or removing one environment no longer shifts array indices and forces destructive plans on its siblings. The inner key argument moves out of the object and becomes the map key.

Behavior changes

  • environments is now Optional + Computed and the non-empty (SizeAtLeast(1)) constraint is dropped.
  • Manage a subset: Read refreshes only the keys already in state and the reconcile loop only deletes formerly-managed environments, so an environment created outside Terraform (e.g. via the UI) is never pulled into state and never deleted.
  • environments = {} manages no environments; omitting the attribute entirely emits a plan-time warning steering toward {} or an explicit map.
  • Import is order-independent (map keyed by env key), so the old ImportStateVerifyIgnore: ["environments"] workaround is gone.

Supporting changes

  • migrate-tf-syntax: new map_key transform — forward hoists the block's key to the map key (dropping it from the object); reverse expands the map back to repeated blocks; positional environments[N] references are rewritten to environments["<key>"]. Non-literal keys warn + skip.
  • State upgrader: v0 (SDKv2 ordered list) → v3 map, preserving the approval-settings API-defaults nulling. The v0 prior schema is pinned to the original list shape so genuine v2 state still decodes.
  • Docs, examples, migration guide, and the terraform-provider-block-to-nested-attrs skill (→ 2.1.0) updated for the map syntax.

Breaking change

launchdarkly_project.environments is a map keyed by environment key instead of an ordered list; the inner key attribute is removed. Configs are rewritten with migrate-tf-syntax; the v2 → v3 state upgrade is automatic. Only the v2 → v3.0.0 GA boundary is a compatibility obligation — preview→preview state is intentionally allowed to break (existing 3.0.0-beta.N state stored as a list will not decode and must be re-imported).

Test plan

  • Unit: migrate-tool map round-trip (TestForwardConvertsMapBlock, TestReverseMapBlock, non-literal skip); v0-list→map upgrader (TestEnvironmentsMapFromV0List, incl. approval-defaults nulling).
  • Local: go build · go vet · make fmtcheck · terraform validate (map accepted / legacy list rejected / {} & omit accepted).
  • Acceptance (CI, real LD): existing project CRUD/import/many-env tests rewritten to map keys, plus new TestAccProject_ZeroEnvironments and TestAccProject_ManageSubset (out-of-band env is ignored and not deleted). 11 sibling resources' acc fixtures updated to map syntax.

Reviewer notes / residual

  • The first-Apply plan-vs-apply consistency for the Optional+Computed map with the three computed-sensitive inner fields (api_key/mobile_key/client_side_id) is exercised by TestAccProject_WithEnvironments create+import.
  • There are no statecompat fixtures for any resource in this line, so the v2→v3 state upgrade is unit-tested (consistent with feat!: use object syntax for single-object flag/project attributes (REL-14237) #480).

Draft pending the CI acceptance gate.

🤖 Generated with Claude Code


Note

High Risk
Breaking Terraform schema with authoritative environment deletes and irreversible key changes; incorrect configs or partial maps can destroy environments and targeting in LaunchDarkly.

Overview
Breaking change: launchdarkly_project.environments moves from an ordered list / repeated blocks to a required map keyed by environment key, with at least one entry. Each value keeps an Optional+Computed inner key that must match the map key. The map is authoritative—keys dropped from config are deleted on apply (SDK keys and flag targeting), with plan-time warnings when removals or renames are detected.

Provider logic is refactored for maps: reconciliation creates new envs before deleting removed ones (so renames never leave a project with zero environments), ValidateConfig enforces map key ↔ nested key, and ModifyPlan fixes sensitive computed fields and pins key to the map key. Read/import builds a full map from the API (order-independent; flaky list-order import workarounds removed). State upgrade re-keys v0 list state to a map via environmentsMapFromV0List (incl. approval-settings default nulling).

migrate-tf-syntax gains map_key for block ↔ environments = { "prod" = { ... } } conversion; it warns on positional environments[N] / [*] refs for manual fix-up rather than rewriting them. Docs, examples, migration guide, skill 2.2.0, and acceptance tests switch to map syntax and key-based references (e.g. environments["test-env"].client_side_side_id).

Reviewed by Cursor Bugbot for commit 8f721db. Bugbot is set up for automated code reviews on this repo. Configure here.

Re-model `launchdarkly_project.environments` from a positional list to a
map keyed by the environment `key`, so reordering, adding, or removing
one environment no longer shifts array indices and forces destructive
plans on its siblings. The inner `key` argument moves out of the object
and becomes the map key.

The attribute is now Optional+Computed and the non-empty constraint is
dropped: declare the environments you want to manage and the rest are
left untouched (manage a subset, leave the others to the LaunchDarkly
UI). Set `environments = {}` to manage none; omitting it entirely emits
a plan-time warning. Read refreshes only the keys already in state and
the reconcile loop only deletes formerly-managed environments, so an
environment created outside Terraform is never deleted.

Supporting changes:
- migrate-tf-syntax: new `map_key` transform converts the v2 block form
  to the v3 map (forward hoists `key` to the map key; reverse expands
  back to blocks) and rewrites positional `environments[N]` references
  to `environments["<key>"]`.
- v0 -> v1 state upgrader converts the SDKv2 ordered list to the map,
  preserving the approval-settings API-defaults nulling. The v0 prior
  schema is pinned to the original list shape so genuine v2 state still
  decodes.
- docs, examples, migration guide, and the block-to-nested-attrs skill
  updated for the map syntax.

BREAKING CHANGE: `launchdarkly_project.environments` is now a map keyed
by environment key (`environments = { "production" = { ... } }`) instead
of an ordered list. The inner `key` attribute is removed; the map key
carries it. Rewrite configurations with `migrate-tf-syntax` (it also
rewrites `environments[N]` references to `environments["<key>"]`). The
v2 -> v3 state upgrade is automatic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cursor[bot]

This comment was marked as resolved.

- resource_project: skip environment reconciliation entirely when
  plan.Environments is null or unknown (Optional+Computed omitted/
  not-yet-computed). Previously a null/unknown plan yielded zero desired
  keys and the reconcile loop deleted every managed environment — not the
  same as an explicit `environments = {}`. An explicit empty map is
  concrete (not null), so it still deletes managed envs as intended.
- migrate-tf-syntax: validate every block's map key before mutating any
  block in the forward map_key path. A missing/non-literal key on a later
  block previously aborted with earlier blocks already stripped of their
  key, leaving invalid v2 syntax; now the file is left untouched on skip.
  Adds a multi-block regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cursor[bot]

This comment was marked as resolved.

- resource_project: distinguish unknown from null prior in
  environmentsMapFromAPI. A create that omits environments now manages
  NONE (returns the empty map) instead of materializing the
  auto-provisioned environments into state. Previously omit -> append-all
  stored every auto-provisioned env, so adding a partial environments map
  later deleted the undeclared ones — data loss the omit warning promised
  would not happen. Null prior (import) still surfaces all environments.
  Adds TestAccProject_OmitThenSubset. Warning + schema description updated
  to match (omit == manage none, same as `{}`).
- docs/migration guide: correct the claim that migrate-tf-syntax rewrites
  positional `environments[N]` references. The tool converts the block to
  a map but does not touch resource index expressions; that is now listed
  as a manual follow-up step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cursor[bot]

This comment was marked as resolved.

monsagri and others added 2 commits June 30, 2026 12:07
environmentsMapFromV0List returned a null map for a null/empty v0
environments list. A null prior makes the next Read import every
LaunchDarkly environment (the null branch of environmentsMapFromAPI),
undoing manage-none/subset semantics and risking later unintended
deletes. Return an empty map instead so the upgraded state manages no
environments. (v2 required at least one environment, so this is
defensive.) Updates the unit test accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…236)

Simplify the environments re-model per design review:

- Re-add the environment `key` attribute (Optional+Computed, validated to
  equal the map key) so references like environments["prod"].key keep
  working. UseStateForUnknown avoids plan churn.
- environments is Required with at least one entry (mapvalidator), matching
  the API (a project always has at least one environment).
- Manage environments authoritatively: the map is the project's complete
  env set and Read surfaces all of them. To manage environments outside
  Terraform, use lifecycle { ignore_changes = [environments] } (composes
  with the standalone launchdarkly_environment resource).
- Warn at plan time when any managed environment is removed (rename or
  delete) since it is irreversible. Reconcile creates new envs before
  deleting removed ones (LD rejects deleting a project's last environment).
- Remove the manage-subset machinery (managed-keys-only Read, {}/omit
  manage-none, null-vs-unknown prior split) — the source of the earlier
  Bugbot findings; subset management is not a supported use case.
- migrate-tf-syntax: keep `key` inside the converted map object; detect and
  warn on positional environments[N] references with the exact map-key
  replacement (it does not edit expressions).

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 48095d2. Configure here.

obj, d := environmentObjectFromAPI(ctx, e, pm)
diags.Append(d...)
ordered = append(ordered, obj)
elements[e.Key] = obj

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Read imports all environments

High Severity

environmentsMapFromAPI walks every environment returned by the API and writes each into state, even when that key was never in prior state. That contradicts refresh-only-managed-keys behavior and pulls UI-created environments into state so a later authoritative apply can delete them.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 48095d2. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is now working as designed. Per the design review, launchdarkly_project.environments is authoritative — the map is the project's complete set of managed environments, so Read surfaces all of them and an apply deletes any not declared. Manage-a-subset was explicitly dropped as a use case. To manage the project in Terraform but its environments elsewhere (UI or the standalone launchdarkly_environment resource), use lifecycle { ignore_changes = [environments] }. A plan-time warning fires before any environment is deleted.

Comment thread scripts/migrate-tf-syntax/main.go
…e tool

- resource_project: ModifyPlan now pins each environment's Optional+Computed
  `key` to its map key. The nested-attribute schema synthesizes "" for a new
  env's omitted key, which Apply replaces with the real key — an inconsistency
  the framework reports as "inconsistent values for sensitive" because the env
  object contains sensitive members. Fixes TestAccProject_WithEnvironments and
  TestAccProject_EnvApprovalUpdate on the add-new-env step.
- migrate-tf-syntax: the forward map_key conversion now aborts (warn, file
  untouched) when two blocks share a literal key value, instead of silently
  collapsing them into one map entry and dropping a block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@monsagri monsagri marked this pull request as ready for review June 30, 2026 13:57
@monsagri monsagri requested review from a team as code owners June 30, 2026 13:57

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

⚠️ 1 issue in files not directly in the diff

⚠️ Published example and docs for view_filter_links use broken list-index syntax on the now-map environments attribute (examples/resources/launchdarkly_view_filter_links/resource.tf:14)

The example file still references environments with positional list-index syntax (environments[0] at examples/resources/launchdarkly_view_filter_links/resource.tf:14 and :22), but this PR changed environments to a map, so users who copy the example will get a Terraform error.

Impact: Published documentation shows invalid Terraform code that fails on terraform validate.

The example file was not updated alongside the List→Map schema change

The PR converts launchdarkly_project.environments from a ListNestedAttribute to a MapNestedAttribute (launchdarkly/environments_framework.go:84). List-index access like environments[0].client_side_id is not valid for a map attribute — the correct syntax is environments["<key>"].client_side_id.

The example at examples/resources/launchdarkly_view_filter_links/resource.tf (lines 14 and 22) was not updated. Since docs/ is generated from examples/ via tfplugindocs (per CLAUDE.md), the generated docs/resources/view_filter_links.md lines 41 and 49 carry the same broken syntax into published documentation.

Every other test and example in the PR was updated to use the new map-key syntax (e.g. environments["test-env"].client_side_id), but this example was missed.

Open in Devin Review

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