Repository navigation
fix(aws): a Lake Formation tag the contract does not define is associated, and the module validates - #706
Merged
Conversation
…ated, and the module validates A key in a binding's governance.lakeFormation.tags with no governance.lakeFormation.tagDefinitions entry got a depends_on on an aws_lakeformation_lf_tag the module never declared, so tofu validate, fluid plan and fluid apply failed with "Reference to undeclared resource". Such a key is a tag owned outside the contract (a platform's central LF-tag ontology, the model AWS recommends), and it is now associated as written: no edge, no warning, no refusal. - The association waits only for the tags this module creates: the aws_lakeformation_lf_tag of the matching resource name whose key is the association's key. The list comes from the associations that carry a value, and depends_on is omitted when it is empty. A contract that defines every tag it associates emits the same module, byte for byte. - Refused at emit and by fluid validate, through the same functions (lf_tag_definitions, lf_tag_associations), where each used to fail at apply or be dropped: a value the contract's definition of the tag does not allow, compared case-folded as Lake Formation stores keys and values lower-cased (lakeformation-tag-value); a definition with no values, which was skipped, and two definitions that are one resource name, such as a-b and a_b, where the later replaced the earlier (lakeformation-tag-definition); tags on a binding that names no Glue table, which were dropped (lakeformation-tag-association). - The tags and tagDefinitions descriptions in fluid-schema 0.7.3 to 0.7.6, edited in place: a key the contract does not define must already exist in the account, the applying principal needs ASSOCIATE on it, and a key the contract defines must not also be created elsewhere. A test pins the wording in every schema that carries it. - docs/governance-parity.md gets a Lake Formation tags row, the new refusal kinds, and what a platform-owned tag needs. The association's resource key still ignores the database, so two exposures with the same table name in different databases share one association. Changing it changes resource addresses and needs a migration, so it is left for its own change. Prior art: terraform-provider-aws documents aws_lakeformation_resource_lf_tags as attaching existing LF-tags that must exist beforehand, and has no LF-tag data source to check one at plan time; CloudFormation's AWS::LakeFormation::TagAssociation names tag keys literally; AWS's Lake Formation TBAC guidance and its "decentralize LF-tag management" post describe a central steward that creates the tags and domains that hold ASSOCIATE; OpenTofu's depends_on takes only resources declared in the module. In this repo, _wire_aws_deps and the Snowflake _container_deps add an edge only to a resource in the same module, _glue_db_ref inlines the literal name of a container the module does not own, lf_column_exclusions is the shared emit/validate refusal shape, and test_iac_lakeformation_admins.py pinned the last schema correction. Validated: tests/iac/test_iac_lakeformation_tags.py (28 tests, two of them tofu init -backend=false + tofu validate) goes red with the code half of the fix reverted (13 failed), with only the ownership guard removed (6), with the fluid validate mirror removed (6), without case folding (2) and with the schema text reverted (4), and green restored. tests/iac passes with tofu hidden; the AWS tofu validate and moto plan suites pass with tofu on PATH; fluid validate --strict passes a platform-owned contract and refuses the bad ones; fluid generate iac --validate passes. ruff, black 24.10.0, the product guardrail and detect-secrets pass. The full-suite result is in the PR description.
…ence, not by depends_on The association of a tag the contract defines now names it the way the provider documents, key = aws_lakeformation_lf_tag.<name>.key, and carries no depends_on. OpenTofu orders the association after the tag from the reference, and records the tag among its dependencies, so a destroy still removes the association first. A tag the platform owns keeps its literal key and no reference. Every refusal of the previous commit is unchanged. The reference evaluates to the same key, so an association applied from 0.19.0, which emitted literal keys and depends_on, plans no change. Proven with real OpenTofu 1.12.0 and hashicorp/aws 6.67.0 against an in-process moto server, in tests/iac/test_iac_lakeformation_tags_moto.py: - the module 0.19.0 emitted is applied, and the one this commit emits plans "No changes" on the same state; the state then lists both tags as the association's dependencies, and the destroy removes it first; - a tag the platform created beforehand is associated beside one the contract creates, reads back through GetResourceLFTags, plans clean, and is still in the account after the destroy; - a platform-owned key that does not exist passes tofu validate and fails the apply with EntityNotFoundException. moto checks that an associated key exists, not its value, so a wrong value on a platform-owned tag is not proven to fail. iac-tests.yml Stage 1 runs the file and fails if it only skipped; the .secrets.baseline line number of an existing entry in that workflow moves with it. Prior art: the terraform-provider-aws docs for aws_lakeformation_resource_lf_tags reference the tag (key = aws_lakeformation_lf_tag.example.key); OpenTofu's depends_on docs recommend an expression reference over depends_on, which it calls a last resort; cloudposse/terraform-aws-lakeformation wires its associations the same way, with no depends_on on the tags. Validated: the new reference tests go red with the depends_on edges restored (9 failed, the moto no-change test among them), the moto plan gate goes red when the reference names another attribute (the association "must be replaced"), and dropping the ownership guard fails tofu validate and both platform-owned moto tests (12 failed); restored, 35 passed.
…owned # Conflicts: # CHANGELOG.md # docs/governance-parity.md # fluid_build/iac/governance_validation.py # fluid_build/iac/providers/aws.py
📄 Documentation ReminderThis PR appears to be missing a documentation reference. Our docs live in a separate repo. Please update the PR description with one of:
See the Contributing Guide for details. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(aws): a Lake Formation tag the contract does not define is associated, and the module validates
Summary
A binding's
governance.lakeFormation.tagscan name a tag that the contract'sgovernance.lakeFormation.tagDefinitionsdoes not define. That is the normal casewhen a platform owns the account's LF-tag ontology, which is the model AWS recommends.
Before this change the emitted association named such a key in its
depends_on, as anaws_lakeformation_lf_tagthe module never declares.tofu validate,fluid planandfluid applythen rejected the whole module with "Reference to undeclared resource".Now a key the contract does not define is a tag the platform owns. It is associated
by its literal key, with no reference, no warning and no refusal. A key the contract
defines names its tag the way the provider documents it,
key = aws_lakeformation_lf_tag.<name>.key, and the association carries nodepends_onat all. OpenTofu orders the association after the tag from thatreference. The reference evaluates to the same key, so an association applied from
0.19.0 plans no change. This was applied and planned with real OpenTofu against moto.
Four more ways to get a module that fails at apply, or that drops tags without a word,
are refused at emit and at
fluid validate.Why
main(63f3b40) with OpenTofu 1.12.0 and hashicorp/aws 6.67.0:fluid generate iacwroteaws_lakeformation_resource_lf_tags.gold_sales_orders_lf_tags_orders.depends_on = [aws_lakeformation_lf_tag.gold_sales_orders_lf_tag_classification]with no suchresource.
tofu validatefailed: "There is no managed resourceaws_lakeformation_lf_taggold_sales_orders_lf_tag_classificationdefinition inthe root module."
values list (skipped by the emitter, but still a dependency); an association key with
an empty value (dropped from
lf_tag, but still a dependency); and keys that collideunder
safe_ident(a-banda_b), where the edge could point at the wrong tag.defines its tags. No offline test caught this.
depends_onedges for the tags the contract defines were themselves thepattern OpenTofu calls a last resort. The provider's own example orders the
association with a reference to the tag's key.
What changes
fluid_build/iac/providers/aws.pylf_tag_definitions(contract): theaws_lakeformation_lf_tagbodies, by resourcename.
_emit_lf_account_settingsnow emits from it. It refuses a key with no values,which was skipped before, and two keys that are one resource name, where the later
used to replace the earlier. Both are
lakeformation-tag-definition.lf_tag_associations(contract, exposure, binding, index): thelf_tagblocks, oneper key that carries a value. It refuses a value outside a contract-defined key's
values (
lakeformation-tag-value). The comparison is case-folded, because LakeFormation stores keys and values lower-cased. It also refuses tags on a Glue-catalog
binding that names no Glue table (
lakeformation-tag-association); those weredropped before.
_emit_lf_tag_association: section 3 of_emit_lakeformation, moved into a helperthe way
_emit_lf_filtered_grantand_emit_lf_masked_viewsare. A key this moduleowns (
resources["aws_lakeformation_lf_tag"]holds the matchingsafe_identname,and that resource's key equals the association key) becomes
"key": "${aws_lakeformation_lf_tag.<name>.key}", built withtofu_ref, as everyother cross-reference in the emitter is. Any other key stays the literal the contract
wrote, which the renderer escapes. No association carries
depends_on.AwsIacPlugin.emitderiveslf_tagsbesidelf_column_exclusionsandlf_row_filtersand passes it down, the same pattern those use.fluid_build/iac/governance_validation.pyvalidate_governancecalls the same two functions.lf_tag_definitionsruns onceper contract, and only when some expose is bound to AWS.
lf_tag_associationsrunsper AWS binding. So
fluid validatereports each refusal at stage 2 with the emit'sown message. It adds no warning, because the generated pipeline's stage 2 runs
fluid validate --strict.fluid_build/schemas/fluid-schema-0.7.3.jsonto0.7.6.jsontagsandtagDefinitionsdescriptions are edited in place, as plain text. Akey the contract does not define must already exist in the account, and the principal
that runs the apply needs the Lake Formation ASSOCIATE permission on it. A key the
contract defines must not also be created elsewhere. The descriptions also state the
value rule and the refusals. They say the association "waits for" a defined tag, which
the reference still does, so the second commit leaves them alone.
HEADshows that only those twodescriptionstrings changed.tests/iac/test_iac_lakeformation_tags.py(new, 32 tests)depends_onor atag-key reference), a literal key, no
depends_on, andvalidate_governance == ([], []).reference. No association carries
depends_on, for a defined, a mixed, aplatform-owned and a colliding contract.
pinned, with both keys as references.
name (
a-bbesidea_b) stays a literal. A platform-owned key is still escaped ascontract text (
${becomes$${).kindand message) and atvalidate_governance(the same message and remediation). Case-folded values are accepted. Tags with no
value on a binding with no table are not refused. Tags on a non-Glue format are
ignored as before, and a contract bound only to GCP is not checked.
test_iac_lakeformation_admins.pypinned the admins correction.tofu init -backend=falseplustofu validateon a platform-owned module, alone andbeside a defined tag. The test skips without tofu, as
test_iac_aws_validate.pydoes.The file is marked
unit, so iac-tests Stage 1, which has tofu on PATH, runs it.tests/iac/test_iac_lakeformation_tags_moto.py(new, 3 tests; realtofu apply,plananddestroyagainst an in-process moto server)depends_on; the test pins that body) is applied, and the module this branch emitsthen plans
No changeson the same state (tofu plan -detailed-exitcodeexits 0).After an apply, the state lists both tags among the association's dependencies, and
the destroy leaves no tag behind.
tag the contract creates.
GetResourceLFTagsreads both back, the module plansclean, and after the destroy the platform's tag is still in the account while the
contract's tag is gone.
tofu validatepasses, and the apply failswith
EntityNotFoundException, with no association in the state.test_iac_aws_retention_encryption_moto.pyandtest_iac_aws_apply_engine_moto.py: emitted withAWS_ENDPOINT_URLset (emulatormode, so Glue resources name their catalog for provider 6), a
provider_override.tf.jsonsidecar routing each service to moto, everyAWS_*variable dropped from tofu's environment, moto reset per test, and the conftest's
CreateBucket-tags shim. Skipped without tofu or moto's
serverextra..github/workflows/iac-tests.yml(
scripts/ci/assert_lane_coverage.py), as it does for every other moto file. Thestep adds 11 lines, which moves an existing
.secrets.baselineentry for thatworkflow from line 247 to 258; the baseline carries the new line number.
Docs and changelog
docs/governance-parity.md: a Lake Formation tags row that names the reference, thethree new refusal kinds, a short "Lake Formation tags the platform owns" section, a
prerequisite line, and entries in "What is proven, and what is not", including what
moto does not check.
HONESTLY_TESTED.md: a "Stage 1 LF tags" row, with what is not proven.CHANGELOG.md: a### Fixedentry under[Unreleased].Prior art (borrow-before-build)
aws_lakeformation_resource_lf_tagsdocs(
website/docs/r/lakeformation_resource_lf_tags.html.markdown): the resource"Manages an attachment between one or more existing LF-tags", the
lf_tag.keyargument is "Key name for an existing LF-tag", and the Database example writes
key = aws_lakeformation_lf_tag.example.key. That reference is what this changeemits for a defined tag. The provider has no LF-tag data source (checked with
tofu providers schema -jsonon 6.67.0), so an external tag cannot be checked atplan time.
internal/service/lakeformation/resource_lf_tags.goat v6.67.0: create calls
AddLFTagsToResourceand turns each returnedFailuresentry into an error, and every argument is
ForceNew. So a missing platform tagfails the apply, and any change to a key's value would replace the association,
which is why the no-change plan matters.
depends_ondocs: "we recommend using expression references to implydependencies when possible", and
depends_onshould be "a last resort because itcan cause OpenTofu to create more conservative plans". OpenTofu JSON syntax docs: a
JSON string that is a single interpolation evaluates to the referenced value directly.
cloudposse/terraform-aws-lakeformation(main.tf): its association writeskey = aws_lakeformation_lf_tag.default[lf_tag.key].key, with nodepends_ononthe tags. It supports only tags the module defines, so the platform-owned literal is
where this change goes further.
AddLFTagsToResource: it returnsFailures, andEntityNotFoundExceptionmeans "A specified entity does not exist." AWS CloudFormationAWS::LakeFormation::TagAssociationnames tag keys literally. AWS's TBAC guidance andthe Big Data Blog post "Decentralize LF-tag management with AWS Lake Formation": a
central steward creates the tags, and domains hold ASSOCIATE on them.
create_lf_tag,add_lf_tags_to_resource,get_resource_lf_tags,delete_lf_tagandlist_lf_tagsare implemented.add_lf_tags_to_resourcereports a key that does notexist as a failure ("Tag or tag value does not exist.") but does not check the value.
tofu_ref/TofuExpris how the emitter writes every cross-reference;_wire_aws_depsand the Snowflake_container_depsadd an edge only to a resourcein the same module;
_glue_db_refinlines the literal name of a container the moduledoes not own;
lf_column_exclusionsis the shared emit and validate refusal shape;the moto harness is copied from
test_iac_aws_retention_encryption_moto.pyandtest_iac_aws_apply_engine_moto.py.Decisions taken while implementing (beyond the founder's option B)
lakeformation-tag-valueandlakeformation-tag-definitioncome from thebrief. The
safe_identcollision also useslakeformation-tag-definition. Tags on abinding with no table use the new
lakeformation-tag-association.location.databaseorlocation.tableis missing.This is the same condition
lf_column_exclusionsuses. The refusal applies only toGlue-catalog formats. A non-Glue binding keeps ignoring its whole
governance.lakeFormationblock, as documented, so its tags are still ignored.comparison is case-folded. Keys that differ only in case are listed under follow-ups.
depends_on(second commit, on thefounder's OpenTofu-first rule). The first commit had kept
depends_onso that themodule stayed byte for byte the same. The emitted module now changes for every
contract that associates a tag it defines, but only in the association's
lf_tagkeysand the removed
depends_on. The moto test shows such an association, applied from0.19.0, plans no change.
.keyattribute is referenced, not.id. The id is<catalog id>:<key>;pointing at it plans a replacement of every association (the seeded defect below).
Without one it would run in no CI lane:
test_iac_moto_e2e.pyis in that state todayand is red on
main(reported separately, not changed here).tofu_env,_tofu,_initand_provider_override, aseach moto file in
tests/iacdoes. The conftest'stofu_envkeeps the inheritedAWS_*variables, and these tests must not.binding with no table.
fluid validatecheckstagDefinitionsonly when some expose is bound to AWS,because the AWS emitter is the only one that creates the tags.
_emit_lf_tag_association. Ripwire quality-delta hadflagged
_emit_lakeformationgrowing from 66 to 72 in complexity and from 218 to 229lines.
Validation
All commands were run in the clone on this branch.
PYis the shared test venv'spython, run with
PYTHONPATH=<clone>and-p no:randomly. OpenTofu 1.12.0,hashicorp/aws 6.67.0, moto 5.2.3.
The second commit (reference wiring)
scratch venv, with the repo's CreateBucket-tags shim),
AWS_ENDPOINT_URLpointing atit, dummy keys,
AWS_PROFILEunset, shared config files set to/dev/null.origin/main(63f3b40, the samefluid_buildas v0.19.0) emitted the module. Its only difference from this branch's module is
the association (
depends_onand two literal keys against two references). Themain module was applied (6 added), and planned clean on its own. Then this
branch's module was swapped in, and
tofu plan -detailed-exitcodeexited 0 with"No changes. Your infrastructure matches the configuration." After a no-op apply,
the state lists both
aws_lakeformation_lf_tagresources among the association'sdependencies. The destroy removed the association before the tags.
create_lf_tag(classification, [confidential, public])withboto3, then this branch's module (classification as a literal key, domain defined
and referenced) was applied (5 added).
GetResourceLFTagsonsales.ordersreturned
domain=[sales]andclassification=[confidential], and the plan afterwas clean. After the destroy,
list_lf_tagsstill heldclassification.tofu validatepassed. The applyfailed: "creating AWS Lake Formation Resource LF Tags (catalog id:123456789012, tag
key:nonexistent, values:[x]): EntityNotFoundException: Tag or tag value does not
exist." The bucket, database and table were created and are in the state; the
association is not. When the key was then created with a different value only
(
y), moto accepted the association ofx, so moto does not check values.test_iac_lakeformation_tags.pyandtest_iac_lakeformation_tags_moto.py, with tofu on PATH: 35 passed.depends_onedges restored: 9 failed. They are the newreference tests and the moto no-change test.
.idinstead of.key(and the moto test's patternloosened to let it through): the moto no-change test fails at the plan,
"
aws_lakeformation_resource_lf_tags.gold_sales_orders_lf_tags_ordersmust bereplaced ... Plan: 1 to add, 0 to change, 1 to destroy."
if True, so platform keys become references:12 failed, including both
tofu validatetests and both platform-owned mototests.
cmp): 35 passed.--junitxml, thenscripts/ci/assert_lane_coverage.py ... --require "LF tags moto=...": "OK LF tagsmoto: 3 passing", exit 0.
actionlintoniac-tests.yml: clean.tests/test_ci_dbt_ceiling.pyandtests/test_ci_lane_coverage_guard.py: 57 passed.tests/iac, tofu hidden: 1581 passed, 248 skipped.test_iac_aws_validate.py,test_iac_tofu_validate.py, the Lake Formation bucket-policy and column-granttofu planvs moto files,test_iac_aws_column_restrictions.py,test_iac_aws_retention_encryption.py,test_iac_aws_retention_encryption_moto.py,test_iac_aws.py,test_iac_state_drift_moto.pyandtest_iac_aws_apply_engine_moto.py: 239 passed (20 min 45 s).ruff check fluid_build/ tests/: passed. The dead-code ruff gate: passed.black --check fluid_build/ tests/(24.10.0): 1811 files unchanged.python3 tools/product_guardrail/check.py --self-testand the check: 0 failingfindings. Its 13 warnings are all in files this change does not touch.
detect-secrets-hook --baseline .secrets.baselineon every file changed againstorigin/main: it first moved theiac-tests.ymlentry's line number (247 to 258),as above; with that committed, it exits 0.
ripwire --quality-deltaagainst the first commit (exit 2, 24 gating rows), weighedand left alone:
tofu_env,_tofu,_init,_write,_plans_no_changeand_lakeformationmatch the helpers every othermoto file in
tests/iackeeps for itself (retention, column grants, columnrestrictions, apply engine, cross-account LocalStack). That is the harness this
file was asked to copy; a shared module is a follow-up.
_emit_lf_tag_association,_emit_lakeformationand
AwsIacPlugin.emit: this branch revises what its own first commit wrote, andonly comments changed in the last two.
iac-tests.ymlStage 1 job growing by the 11-line step, the sameshape as the five moto steps beside it.
function) and two similar one-key tests.
The first commit
mainthe research agent's fourtests gave 3 failed, 1 passed, and
tofu validatereported "Reference to undeclaredresource".
removed: 6 failed. The
fluid validatemirror removed: 6 failed. No case folding:2 failed. The schema descriptions reverted: 4 failed. Restored: 28 passed.
-x: 6 failed, 20068 passed, 656 skipped. Allsix are dbt tests that need the
dbt-duckdbadapter or adbtexecutable, which theshared venv lacks, and they fail the same way on an untouched
origin/maincopy.fluid validate --strictpasses the platform-owned contract and refuses the bad ones;
fluid generate iac --validatepasses;fluid generate iacon a bad value exits 1 withkind: lakeformation-tag-value.Risks
The provider has no data source to check it earlier, and option B adds no warning.
The apply fails loudly (proven against moto for a key), after the bucket, database
and table were created; re-running once the tag exists completes it.
become references and
depends_ongoes. Proven to plan no change against moto foran association applied from 0.19.0, not yet against real Lake Formation. A defined
key with upper-case letters is the open case: the reference carries the tag
resource's key as the provider reads it back (see follow-ups).
key's values, an empty definition, two definitions that collide, and tags with no
Glue table. The first three already failed at apply or produced a wrong module. The
last one silently dropped the tags.
accepted. Only description text changed.
exists in the account. The control is on the AWS side. The applying principal must
hold ASSOCIATE on the tag, the same authority as before. The literal key is not a
TofuExpr, so the renderer still escapes it, and a test pins that. The reference isbuilt only from
safe_identoutput.Follow-ups
safe_ident(f"{cid}_lf_tags_{table}")ignores thedatabase. Two exposures with the same table name in different databases share one
association, and the second silently replaces the first. The grant keys have the same
shape. Fixing it changes resource addresses, so it needs a migration design (
movedblocks). Not changed here.
Domainwith an associationdomainistreated as platform-owned (no reference, no value check). Two definitions
Domainanddomainare both emitted. AWS stores keys lower-cased, so each pair is one tag, and adefined
Domainmay read back asdomain. Decide whether to refuse case variantsafter checking live behaviour. The same applies to values that collapse under
lower-casing.
fluid validateandfluid generate iacprint refusal messages through Rich markup,which strips
[orders]fromexposes[orders]. This is pre-existing and affectsevery refusal that names an expose in brackets.
association applied from 0.19.0: this needs the sandbox account and approval.
tests/iaccarries its owntofu_env,_tofu,_initandprovider override; ripwire counts them as clone groups. A shared moto harness module,
like
_moto_s3.py, would remove them across all of those files.in the research), if it is wanted.
Merged with main after #703
#703 combined the AWS emitter's column, row-filter and masked-view checks into one
lf_governancecall, whichfluid validateshares through_aws_refusal. This branch's tag checks were rebuilt on that:lf_governance, thenlf_tag_associations._aws_refusaltakes the contract and runs the same two checks in the same order, so an expose gets one refusal, never a refusal plus an "unenforced grants" warning.fluid plan, which never runs the emitter. It broketofu validateandfluid apply.Checked on the merged code:
tests/iacwith tofu hidden: 1,636 passed. The validate and examples-unchanged tests plus both branches' new files: 332 passed._aws_refusalturns 3 tests red.