fix: track only user-declared tags on project and landing zone#228
fix: track only user-declared tags on project and landing zone#228tfelix wants to merge 3 commits into
Conversation
📊 Test Coverage
Uncovered functions (combined run)Combined with |
grubmeshi
left a comment
There was a problem hiding this comment.
General idea sounds valid, however, we need better test support and also see remarks on feature in general (on changelog entry).
4bde1a0 to
652f0b8
Compare
|
Maybe it make sense to go face 2 face over the commits because I am not familiar especially with the testing code here. |
grubmeshi
left a comment
There was a problem hiding this comment.
Looks much better, see some more comments. Let's also huddle tomorrow I'd say!
| } | ||
| if !priorTags.IsNull() { | ||
| var tracked map[string][]string | ||
| resp.Diagnostics.Append(priorTags.ElementsAs(ctx, &tracked, false)...) |
There was a problem hiding this comment.
f: remove? why is this put into diags?
There was a problem hiding this comment.
Resolved by the reconcileTrackedTags refactor — the ElementsAs conversion (and its diagnostics) now lives inside the helper, not at the call site.
| // for unset ones) plus injected restricted-tag defaults — which the caller may be unable to manage. | ||
| // Keeping only the previously tracked keys prevents those server-side additions from entering the | ||
| // user-managed `tags` attribute and producing spurious drift on the next plan. | ||
| func reconcileTrackedTags(priorTags, apiTags map[string][]string) map[string][]string { |
There was a problem hiding this comment.
I think one can move also the state access inside this helper function saving a redundant
var priorTags types.Map
resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("metadata").AtName("tags"), &priorTags)...)
at call site, maybe?
error handling is a bit annoying though, but pass in diags as pointer (see other methods like SetFromClientDto etc.)
There was a problem hiding this comment.
Done — reconcileTrackedTags(ctx, state, tagsPath, apiTags, &diags) now reads the prior tags from state itself and appends diagnostics via the pointer, so the redundant var priorTags types.Map / GetAttribute block is gone and the five call sites (incl. the BBD one) collapse to a single line. I also extracted a pure reconcileTags(tracked, apiTags) core and unit-tested it (TestReconcileTags).
| config, wsAddr := testconfig.Workspace(t) | ||
| // A second tag definition the workspace does not declare: the backend still returns it as an | ||
| // empty list, so the fix must reconcile it away instead of surfacing it as drift. | ||
| undeclaredTag, _, _ := testconfig.TagDefinition(t, client.MeshObjectKind.Workspace) |
There was a problem hiding this comment.
f: why is this not using restricted tag? not supported for workspaces?
There was a problem hiding this comment.
Workspaces do support restricted tags — the backend injects restricted-tag defaults on create via WorkspaceRegistrationController.applyDefaultTags (same restrictedDefaultsAsSerialized() path as project/landing zone). This subtest deliberately covers the broader superset case instead: the API returns an empty-list entry for every defined tag property, even undeclared ones, which is the more general reconciliation the fix has to handle. Happy to add a dedicated restricted-default workspace subtest too if you'd like both paths covered explicitly.
| Descend("key")(SetString(tagKey)), | ||
| Descend("restricted")(SetRawExpr("true")), | ||
| Descend("value_type", "email")( | ||
| RenameKey("string"), |
There was a problem hiding this comment.
f: looks like instead of cumbersomely adapting an existing example tag definition config, better define a test support file instead with fits better it's purpose (probably everything here can be removed then, except setting dynmaic values such as random suffixes etc)?
in particular spotting RenameKey here is weird and that method should only be used rarely (add this in godoc for that method)!
There was a problem hiding this comment.
Switched to purpose-built test-support_tag.tf / test-support_restricted-tag.tf (both declare value_type = { string = ... } directly), so the email→string RenameKey hack is gone; the builders now only set dynamic values (target_kind, randomized key, default_value). RenameKey stays only to give each block a unique label per run — needed because builders like ProjectAndWorkspace already contribute a tag definition — and I documented that as its rare, sanctioned use in the RenameKey godoc.
meshStack fills in restricted-tag defaults on creation and returns an entry for every defined tag property, so the meshObject API response is a superset of what the caller sent. Writing that superset into the Optional+Computed `tags` attribute broke plan/apply consistency: meshstack_landingzone crashed with "Provider produced inconsistent result after apply" on create, and meshstack_project drifted on every subsequent plan (trying to remove tags the caller may not manage). Create/Update now persist the plan's tags, and Read reconciles the API response down to the keys already tracked in state. On import there is no prior state, so the full set is kept. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the project / landing zone tag fix to every affected resource (workspace, payment_method, building_block_definition) and reworks the tests to exercise the real backend instead of the mock. meshStack returns a tag superset on create: an entry for every defined tag property (empty list when unset), plus injected defaults for restricted tags on meshProject / meshLandingZone / meshBuildingBlockDefinition. Writing that superset into the Optional+Computed `tags` attribute broke plan/apply consistency and produced perpetual drift. Each resource now persists only the declared tags on create/update and reconciles the API response down to the tracked keys on read; imports keep the full set. Also reduces mock usage: the restricted-default / superset scenarios are covered by real-backend acceptance subtests, and the mock clients return copies from Read so callers can't mutate the store. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- reconcileTrackedTags now reads prior tags from state and takes a *diag.Diagnostics, collapsing the five call sites to one line; extract a pure reconcileTags core and unit-test it (TestReconcileTags). - add SetBool testconfig builder and document RenameKey's rare, sanctioned use (per-run block-label uniqueness). - build tag-definition test configs from purpose-built test-support files instead of adapting the example (drops the email->string RenameKey hack). - revert the clientmock copy-on-read changes; the added tests run against the real backend only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0de9f78 to
d6d6c0c
Compare
Problem
Resources whose tag schema contains a restricted tag with a default value could not be managed via Terraform:
meshstack_landingzone—terraform applycrashed on create with "Provider produced inconsistent result after apply".meshstack_project— apply succeeded, but every subsequentterraform planshowed drift, trying to remove a tag the caller may not be permitted to manage.Root cause
On the meshObject API create path, meshStack returns a tag map that is a superset of what the caller sent: it fills in an entry for every defined tag property (empty list for unset ones) and injects defaults for restricted tags (
TagService.determineTags/LandingZoneMaintenanceService.applyLandingZoneTagsOnCreation/ProjectCreationService.createProjectFromModel).tagsis anOptional + Computedattribute, so its planned value is always known. Writing that superset into it violates the plugin framework's plan/apply consistency rule (the landing zone crash), and mirroring it onReadproduces perpetual drift (the project case). For restricted tags the documented "just set it to the default yourself" workaround doesn't apply — the caller may lack permission to set them at all.Fix
tagsnow tracks only the tags the user declares:reconcileTrackedTags), so server-injected restricted defaults never entertagsand never surface as drift.Tests
Added
TestAccLandingZoneRestrictedDefaultTagandTestAccProjectRestrictedDefaultTag. The landing zone / project mock clients gained an opt-inInjectTagsOnCreatehook (and now return copies, like a real backend) to simulate the server-side injection. Verified the tests fail onmain(landing zone: inconsistent result after apply; project: non-empty post-refresh plan) and pass with the fix.Docs & changelog
CHANGELOG.md:v0.23.2FIXES:entry.tagsdescriptions clarifying only user-declared tags are managed.🤖 Generated with Claude Code