feat!: key-address launchdarkly_project environments (REL-14236)#481
feat!: key-address launchdarkly_project environments (REL-14236)#481monsagri wants to merge 6 commits into
Conversation
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>
- 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>
- 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>
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ 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 |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 48095d2. Configure here.
There was a problem hiding this comment.
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.
…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>
There was a problem hiding this comment.
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.


Summary
Implements REL-14236 (epic REL-13579): re-model
launchdarkly_project.environmentsfrom a positional list to a map keyed by environmentkey.Reordering, adding, or removing one environment no longer shifts array indices and forces destructive plans on its siblings. The inner
keyargument moves out of the object and becomes the map key.Behavior changes
environmentsis nowOptional + Computedand the non-empty (SizeAtLeast(1)) constraint is dropped.environments = {}manages no environments; omitting the attribute entirely emits a plan-time warning steering toward{}or an explicit map.ImportStateVerifyIgnore: ["environments"]workaround is gone.Supporting changes
migrate-tf-syntax: newmap_keytransform — forward hoists the block'skeyto the map key (dropping it from the object); reverse expands the map back to repeated blocks; positionalenvironments[N]references are rewritten toenvironments["<key>"]. Non-literal keys warn + skip.terraform-provider-block-to-nested-attrsskill (→ 2.1.0) updated for the map syntax.Breaking change
launchdarkly_project.environmentsis a map keyed by environment key instead of an ordered list; the innerkeyattribute is removed. Configs are rewritten withmigrate-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 (existing3.0.0-beta.Nstate stored as a list will not decode and must be re-imported).Test plan
TestForwardConvertsMapBlock,TestReverseMapBlock, non-literal skip); v0-list→map upgrader (TestEnvironmentsMapFromV0List, incl. approval-defaults nulling).go build·go vet·make fmtcheck·terraform validate(map accepted / legacy list rejected /{}& omit accepted).TestAccProject_ZeroEnvironmentsandTestAccProject_ManageSubset(out-of-band env is ignored and not deleted). 11 sibling resources' acc fixtures updated to map syntax.Reviewer notes / residual
Optional+Computedmap with the three computed-sensitive inner fields (api_key/mobile_key/client_side_id) is exercised byTestAccProject_WithEnvironmentscreate+import.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.environmentsmoves from an ordered list / repeated blocks to a required map keyed by environmentkey, with at least one entry. Each value keeps an Optional+Computed innerkeythat 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),
ValidateConfigenforces map key ↔ nestedkey, and ModifyPlan fixes sensitive computed fields and pinskeyto 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 viaenvironmentsMapFromV0List(incl. approval-settings default nulling).migrate-tf-syntaxgainsmap_keyfor block ↔environments = { "prod" = { ... } }conversion; it warns on positionalenvironments[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.