Skip to content

[dmt] feat: module rbac.yaml declaration, final version (stacked on #479) - #480

Open
Jabejixo wants to merge 41 commits into
feat/rbac-yamlfrom
feat/rbac-yaml-template-conditions
Open

Jabejixo wants to merge 41 commits into
feat/rbac-yamlfrom
feat/rbac-yaml-template-conditions

Conversation

@Jabejixo

@Jabejixo Jabejixo commented Sep 24, 2026 •

Copy link
Copy Markdown

Description

Stacked on #479, and the final version of the module RBAC declaration: #479 is the skeleton and is not merged without this PR. Together they add modules/<module>/rbac.yaml, one declaration of a module's RBAC, and three rules of the rbac linter around it -- contract, coverage and sync -- with --fix writing the first declaration from the render and the templates from the declaration. The full description is pkg/linters/rbac/README.md, section "The module RBAC declaration"; what follows is what the two PRs deliver, and what this one changes in the skeleton.

The declaration. rbac.deckhouse.io/v1alpha1, unknown keys refused, validated before anything is compared or written:

  • resources[] -- user-facing access to every resource the module ships, in both role models: RBACv2 namespace and system levels (capabilities d8:namespace-capability:<module>:<action> and d8:system-capability:<module>:<action>) and legacy user-authz levels (d8:user-authz:<module>:<level>); explicit verbs, when conditions, noAccess with a reason, scope and reason where the linter cannot know them.
  • capabilities -- keyed <lineage>.<action>: localized texts outside the view/edit convention; a capability with an action of its own (download_snapshots, as the platform's access_terminal), which a resource entry grants under its action and level places in a level of the lineage; labels of the module, by which a role of the module outside the role model aggregates its capabilities.
  • serviceAccounts[] with cluster and namespace rules, bindings to existing roles, extraClusterRoles, labels, annotations and rbacAnnotations, path for component directories (nested ones included), automountServiceAccountToken; access[] for arbitrary subjects and prometheusAccess for the scraper, both with labels, annotations and when.

The rules.

  • contract runs on every module, with or without rbac.yaml: the rendered ClusterRoles under templates/rbacv2/ follow the platform's label and naming contract. It is a port of the platform's testing/rbacv2 validation, so an external module is checked as an in-tree one; a module still on the scheme before DKP 1.78 gets one "migrate" finding per object. A subsystem the module's own module.yaml declares beyond the seven the platform ships (virtualization) is a lineage of that module, for its d8:subsystem:<name>:* roles, its capabilities and the subsystems of rbac.yaml; another module's own subsystem stays unknown.
  • coverage: every CRD under crds/ has an entry that grants levels or denies access with a reason. --fix appends an undecided noAccess: "TODO" stub, which the lint then reports.
  • sync: the render and the declaration say the same thing, in both directions -- rules as (apiGroup, resource, resourceName, verb) tuples, aggregation edges, the labels of the module and the localized texts of a capability, roleRefs and subjects. A when excuses an absent object or rule only while the condition is false: when anything the declaration writes under it renders, in any file, it holds, and a grant newly declared under it is written by --fix. The conditions around each object and its rules are also compared with the text of the template, both ways, so every render variant agrees: a condition the declaration writes that the template lacks is written by --fix, one the template holds that the declaration does not write keeps the fix off the file. It owns three classes of objects (legacy roles, the module's own capabilities, the objects whose names the declaration builds); everything else in the render is neither written nor reported. The render is judged, not the text of a template.

Either a lint finding or a fix that succeeds. Every case is one or the other, and this PR moves every refusal of the skeleton's autofix to lint time:

  • A declaration that does not parse or validate, a broken module.yaml, a declaration in an edition overlay and an rbac.yaml of the earlier shape are findings without a fix.
  • Without rbac.yaml, --fix writes it from the render and succeeds; the TODO values it leaves are reported by the lint that follows. The coverage stub works the same way.
  • A file the declaration produces is rewritten from it unless the lint finds a case only a change of the templates or of the declaration closes: an object the declaration does not produce, one it puts in another file, a document the linter cannot read (an include, a range, a computed name, a fail/required guard), the version gate, the legacy scheme, a condition ({{ if }}) the declaration does not write, an object exclude-rules.sync keeps out of the comparison, a label or annotation of a legacy role or a capability the format cannot hold, the secrets of a ServiceAccount, a template that is a symbolic link. The finding then names the case and carries no fix: a rewrite never drops or widens what sync does not compare. The case is read from the template text, which is the same in every render variant; under --matrix a variant that finds one keeps the fix of every other variant off the file.
  • A template the declaration produces nothing for is a finding without a fix; the autofix deletes no file.
  • The templates carry no generator header, no file is copied aside, and --fix exits by the level of what is left, as for every other linter. The object store keeps no Dropped list: the render already warns about a template it skipped.

The first declaration. Bootstrap reads the template text around each rendered object with text/template/parse: {{ if X }} becomes when: X, an {{ else }} branch not (X), nested blocks an and; a condition with a template variable, and an account whose objects render under different conditions, become a TODO. Objects inside range, with or define, ones a named template renders (helm_lib), objects of a subchart and roles without rules stay hand-written; an RBAC object the text holds but no render showed is named in a note on top of the written file. Labels and annotations are imported where the format holds them and named in a note where it does not.

Configuration. global.linters-settings.rbac.rules.{contract,coverage,sync}.impact sets each rule's level, read from the root .dmtlint.yaml as every dmt setting; linters-settings.rbac.exclude-rules.{contract,coverage,sync} excludes objects or group/resource keys. The three rules start at warn wherever nothing sets them, and a rule at ignored is not run, so --fix rewrites nothing on behalf of findings nobody sees; the four original rbac rules keep their level and their output. Outside the linter the PRs change only the wiring of these levels and exclusions, the registration of the rules, and no-cyrillic, which leaves alone the module's rbac.yaml and the ru.meta.deckhouse.io/title|description lines the role model requires -- Cyrillic anywhere else in those files is still reported.

Behaviour to know.

  • A level dmt does not know is read as error, as for every linter: rules: {sync: {impact: ignore}} (for ignored) runs sync at error, and a misspelled rule key such as coverge: is dropped. The rbac blocks get no stricter key check than the other linters.
  • --fix does not lint again (cmd/dmt/main.go): a coverage stub is a fix that succeeds, so dmt lint --fix exits 0 with coverage at error, and the next plain run reports the TODO it wrote.
  • Bootstrap writes not (X) for an {{ else }} branch and and (A) (B) for nested blocks, which the declaration writes as {{- if <when> }}; until the template's blocks are rewritten that way by hand, such a file is a finding without a fix (the template holds a condition the declaration does not write).

Verified.

  • Unit and e2e tests with -race, golangci-lint 2.13. The e2e cases under test/e2e/testdata/rbac/ cover the three schemes a module can be on (legacy, 1.78, both behind the version gate), a bootstrap, a clean declaration, a hand edit, a foreign object that keeps the fix off, a missing file the fix writes, a template the render skips, and a role of the module outside the role model with its capabilities and the alias of its old name.
  • The platform tree (deckhouse main, 41 modules): dmt lint --linter rbac gives no errors; the 40 warnings are 37 declarations still missing and 3 contract findings (a cluster-scoped resource in a namespace capability). The cert-manager pilot, declared in rbac.yaml, is clean. Bootstrap on every module writes a declaration that parses, and no fix fails.

Why do we need it, and what problem does it solve?

A module's RBAC lives in four places nobody keeps in step: the RBACv2 capability templates, the legacy user-authz roles, rbac-for-us.yaml and rbac-to-us.yaml. On the platform tree 66 of 181 CRDs have no user-facing access at all, and nothing says whether that is a decision or an omission. A change to the platform's role contract is a hand edit of every module, and the platform test that checks the contract sees only in-tree modules, so external modules drift until aggregation breaks in a cluster.

The declaration makes the decision explicit per resource, and the templates follow from it. The linter is where this belongs: dmt already renders the chart, so sync compares rendered objects rather than template text, and an external module gets the same check as an in-tree one from the tool it already runs. Design: ADR platform-security/2026-04-27-module-rbac-yaml.md (architecture-decision-records).

@Jabejixo
Jabejixo added this pull request to stack #481 September 24, 2026 09:50
Jabejixo added a commit that referenced this pull request Sep 24, 2026
Review of #479, finding 37 (after nested naming moved to #480):

- access[] with a path and namespaceRules is refused: the generator would
  name the Role access-to-<module>-<name> in templates/<path>/rbac-to-us.yaml,
  where the placement rule wants access-to-<dir>-; bootstrap keeps such a
  Role hand-written instead of producing access-to-<module>-access-to-...;
- an account named <module>-<dir> is accepted only in a namespace of the
  platform, and there without namespaceRules or bindRoles, whose objects
  the placement rule would name <module>:<dir>;
- a test runs the placement rule over what the generator writes for the
  shapes this version supports. The platform namespaces now live in
  rbaccontract, shared with the placement rule.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
@Jabejixo
Jabejixo force-pushed the feat/rbac-yaml-template-conditions branch 5 times, most recently from 8da8b0e to 9f5da1d Compare September 24, 2026 15:04
@Jabejixo
Jabejixo marked this pull request as ready for review September 25, 2026 07:28
@AlwxSin
AlwxSin self-requested a review September 25, 2026 07:31
Comment thread pkg/linters/rbac/rules/sync.go Outdated
Comment thread pkg/linters/rbac/rules/bootstrap/conditions.go
Comment thread pkg/linters/rbac/rules/rbacyaml/validate.go
Jabejixo added a commit that referenced this pull request Sep 25, 2026
…ry documents

- A document counts as rendered only by an object of the same template,
  in the namespace it names if it names one: a computed name such as
  {{ .Chart.Name }} matched any object of its kind in the module, and a
  conditional document then fell out of the "not rendered" note.
- Several library documents in one template are one answer only when
  their conditions agree, as for documents matched by name: an include
  under a condition beside one without otherwise gave its objects no
  condition, and a capability it renders no TODO.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Jabejixo added a commit that referenced this pull request Sep 25, 2026
…library's

When the library documents of a template disagree on the condition, Locate still answers that a library renders the object, its condition unknown: the object stays unmanaged and its file hand-written instead of being imported as the module's own, and a legacy role or capability sync owns by class gets a TODO condition, so the run stays red.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Jabejixo added a commit that referenced this pull request Sep 25, 2026
…ry documents

- A document counts as rendered only by an object of the same template,
  in the namespace it names if it names one: a computed name such as
  {{ .Chart.Name }} matched any object of its kind in the module, and a
  conditional document then fell out of the "not rendered" note.
- Several library documents in one template are one answer only when
  their conditions agree, as for documents matched by name: an include
  under a condition beside one without otherwise gave its objects no
  condition, and a capability it renders no TODO.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Jabejixo added a commit that referenced this pull request Sep 25, 2026
…library's

When the library documents of a template disagree on the condition, Locate still answers that a library renders the object, its condition unknown: the object stays unmanaged and its file hand-written instead of being imported as the module's own, and a legacy role or capability sync owns by class gets a TODO condition, so the run stays red.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
@Jabejixo
Jabejixo force-pushed the feat/rbac-yaml-template-conditions branch from d413db8 to 5f50bd4 Compare September 25, 2026 10:34
@Jabejixo Jabejixo changed the title [dmt] feat: rbac.yaml template conditions, labels and annotations (follow-up of #479) [dmt] feat: module rbac.yaml declaration, final version (stacked on #479) Sep 28, 2026
Comment thread pkg/linters/rbac/rules/sync.go
Comment thread pkg/linters/rbac/rules/sync.go
Comment thread pkg/linters/rbac/rules/sync.go
Comment thread pkg/linters/rbac/rules/bootstrap/conditions.go
Comment thread pkg/linters/rbac/rules/rbacyaml/validate.go Outdated
Comment thread pkg/linters/rbac/rules/coverage.go
Comment thread pkg/linters/rbac/rules/sync.go
Comment thread pkg/config/loader.go
Comment thread pkg/linters/rbac/rules/contract.go Outdated
Comment thread pkg/linters/rbac/rules/sync.go
validateWhen(d.PrometheusAccess.When, "prometheusAccess", report)
validateObjectLabels(d.PrometheusAccess.Labels, "prometheusAccess.labels", report)

for kind, names := range map[string][]string{"deployments": d.PrometheusAccess.Deployments, "daemonsets": d.PrometheusAccess.DaemonSets, "statefulsets": d.PrometheusAccess.StatefulSets} {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 11. prometheusAccess workload findings come out in random order

range over a map literal (deployments/daemonsets/statefulsets) makes the order of these validation errors vary between runs. The texts are the same, so dedupe is fine, but output and golden tests are not stable -- the same class as finding 7. A slice of pairs fixes it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No change needed: ValidateFor sorts the errors by their text before returning (validate.go:220), and each message names its list and index, so the map order never reaches the output.

Follow-up of #479 (review, scope): what was taken out of the pilot PR
because the ADR does not describe it yet.

- bootstrap reads the template text around each object: {{ if }} becomes
  `when`, objects in range/with/define and helm_lib includes stay
  hand-written, objects no render showed are named and fail the fix;
- serviceAccounts[].annotations/rbacAnnotations, access[].when, labels and
  annotations on access and prometheusAccess, compared by sync and
  imported by bootstrap;
- nested account paths with placement-conform names, the namespace label
  on every module namespace but default, the edit-distance misspelling
  warning;
- validation refusals (built-in scopes, */<sub>, capability texts, names,
  empty values, bare functions in when) and the refusal of global rbac
  settings in a module .dmtlint.yaml.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…apabilities

Review of #479, findings 33-35, for the follow-up:

- 35: the template reader is text/template/parse instead of patterns: a
  condition is the pipeline as the parser prints it (string literals
  kept, `{{if(.x)}}` and else-if chains read), a `---` or an object inside
  a comment is none, metadata is read at any indentation and in flow
  style, and a document whose name cannot be read matches no object.
- 34: a document with a computed name that no render showed is listed as
  the template writes it.
- 33: a capability or a legacy role under a condition leaves a TODO
  reason on the resources it grants: resources[] have no `when`, and the
  regenerated role would render for every value.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ry documents

- A document counts as rendered only by an object of the same template,
  in the namespace it names if it names one: a computed name such as
  {{ .Chart.Name }} matched any object of its kind in the module, and a
  conditional document then fell out of the "not rendered" note.
- Several library documents in one template are one answer only when
  their conditions agree, as for documents matched by name: an include
  under a condition beside one without otherwise gave its objects no
  condition, and a capability it renders no TODO.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…library's

When the library documents of a template disagree on the condition, Locate still answers that a library renders the object, its condition unknown: the object stays unmanaged and its file hand-written instead of being imported as the module's own, and a legacy role or capability sync owns by class gets a TODO condition, so the run stays red.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A declaration that does not parse or validate, one the generator cannot turn into objects, a broken module.yaml, a declaration in an edition overlay and an rbac.yaml of the earlier shape are reported by the linter without an autofix; --fix no longer attaches a fix that fails on purpose to them.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The fix that writes the first rbac.yaml and the one that appends a coverage stub return nil once the file is written; the TODO values they leave are lint findings without a fix, reported by the lint that follows --fix. The TODO finding of coverage no longer carries a fix that fails on purpose.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ader, no Dropped

The templates carry no generator header and no list of owned objects, and there is no aside copy for a file kept by hand. A file the declaration produces is rewritten from it by --fix unless the lint finds a case in it that only a change of the templates or of the declaration closes -- an object the declaration does not produce, one it puts in another file, a document the linter cannot read, the version gate, the legacy scheme; the finding then names the case and carries no fix. The case is read from the template text, which is the same in every render variant, and the render adds what the text cannot show; a variant that finds one keeps the fix of every other variant off the file. A template the declaration produces nothing for is a finding without a fix: the autofix deletes no file.

Dropped goes from the object store: the render already warns about a template it skipped, and sync does not report the objects the text of a template holds when nothing rendered from it. The text of a file is no longer compared byte for byte; the render is judged.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
… on purpose

No rbac fix fails on purpose any more, so the exit on a fix error whatever the finding's level goes, with Manager.HasFailedFixes and LintRuleErrorsList.ContainsFailedFixes: after --fix the lint reports what is left at its own level, as for every other linter.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The coverage and sync sections say what is a lint finding without a fix and what the autofix does, without the generator header, the aside copies, the orphan deletion or a --fix that fails on purpose.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…e texts once per check

Check builds a syncRun -- where the declaration puts each object, the rendered objects the rule owns, the rendered objects by template and the text of every template -- and the steps read it instead of building the same maps and reading the same files again. No change in what is reported or fixed.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
templateTexts dropped a template it could not read, so the fix found no case in it and wrote over a file the lint never saw. Its read error is kept, and the file is a case without a fix: what it holds is unknown.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
resetFixState held fixState while it took fixOutcomes, the reverse of fixOnce, whose fix takes fixState under fixOutcomes: the two could deadlock. It now takes them in turn; the comment on fixOutcomes says why it is held across the fix, file I/O included.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
An object in the wrong file was reported up to three times in one finding: by the divergence, by the case of the text, by the render seen from the other file and by the text of the other file. The divergence states the fact once and the case says the one thing to do (move X to Y, move X here from Z). The legacy RBACv2 scheme is explained once, by the case that keeps the fix off.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The doc of SyncRule and CoverageRule, the fix state and writeBootstrapped describe the lint-or-fix model; the note on an object kept in a file the declaration writes says the file gets no fix until it is moved, not that the fix drops it or writes a copy beside it; a role without rules is no longer said to be dropped by a fix it may not get; the bootstrap parse error is one wrapped sentence; regenerateFix is rewriteFix, and the comments say rewrite.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The capability marker, a ServiceAccount name and a label or annotation key are checked with validation.IsValidLabelValue, IsDNS1123Subdomain and IsQualifiedName instead of regular expressions of their own. IsQualifiedName is stricter: it enforces the 63-character name and the 253-character prefix the message already stated, where the expression accepted a key with an 80-character name.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Two regressions that no code path can break any more (no file is deleted; the move is covered by TestSync_ObjectHeldByAnotherTemplateIsNotWrittenTwice) and a copy of TestSync_FileWithForeignObjectsIsNotRegenerated go; assertLintOnly also requires that no fix is attached; fixOnce is tested with a call counter; the removal-log regression checks that the change is logged as a removal.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The comment on top of the declaration bootstrap writes no longer says it was written by dmt: it says what the file is and that its TODOs are decisions dmt lint reports; the notes stay.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
writeBootstrapped returned an error when its own output did not parse, although the file was written: a fix that did its work and failed. It writes and returns nil; an rbac.yaml that does not parse is a finding of the next lint, with the line.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Findings, notes and the README say what --fix writes and what the declaration produces or accepts instead of what a generator does: nothing is compared or written until the declaration parses, keys are set by dmt or by Helm, an object will be named by --fix, the templates are written from rbac.yaml. The e2e case sync-fix-regenerates is sync-fix-writes-missing-file.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A scope left as TODO was reported by coverage and, as an invalid scope, by the validation of sync. The validation reports it as undecided, as it does a TODO when, and stops sync on it -- the templates depend on the scope; coverage reports the TODO noAccess and reason values.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The finding said every TODO and note in the written rbac.yaml is a decision for a person before --fix rewrites the templates, which nothing enforced. It says what holds: every TODO is a decision dmt lint reports, and the notes say what the declaration does not carry.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A when may call a helper of the chart (include "cert_manager.yandex_dns_configured" .), so the condition line --fix writes for it held an include and the lint read the document as unreadable: the file could never get a fix. Only an abort of the render (fail, required) in a condition line makes a document someone else's.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A module may ship a role of its own outside the role model that aggregates capabilities by a label of the module (state-snapshotter's backup agent role, agreed with the ADR author), and a capability may have an action other than the one of its level (download_snapshots; the platform has kubernetes access_terminal). The declaration could say neither, and a label of the module on a capability was dropped by --fix and by bootstrap without a finding: sync compared the labels of the role model only.

capabilities.<lineage>.<action> takes a level, which an action of its own requires and a resource entry grants under that action, and labels, none of them dmt's. The generator writes both, bootstrap reads them back from the render, and sync compares every label of a capability but helm_lib's. The e2e case sync-module-role holds such a role, its capabilities and the alias of its old name.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ries

What sync checks on a capability and what the bootstrap notes as dropped now say that the labels of the module are compared and carried into the capabilities entry.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…on a file without a declaration

A module.yaml that does not parse and a CRD document that does not parse are findings of the module and openapi linters; sync and coverage stop on the first and skip the second without reporting them again. An rbac.yaml that holds no declaration of this version -- an empty document, the earlier shape -- passed the load, so coverage attached the stub fix to it and the fix failed on a file that is not a mapping; coverage now leaves such a file to the finding of sync.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The strict check of the rbac configuration keys in the shared config loader, the refusal of a module .dmtlint.yaml that sets global.linters-settings.rbac, the nolintlint setting and the change of GetFixes for ignored findings are gone: none of them is needed by the rbac rules, and each changed dmt for every linter or for rbac alone. So that --fix does not rewrite files on behalf of findings nobody sees, a rule of the declaration at the ignored level is not run.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A variant reports a file with its fix before a later variant withholds it, and dmt has no point after the last variant where a rule could revise its findings; the README says so, what the fix then does and when it happens.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…on across the render

A rewrite dropped or widened rights without a finding in several cases, and a grant declared under a condition that already holds was never written.

- A file whose text holds a condition the declaration does not write, an object exclude-rules.sync keeps out of the comparison, a label or annotation of a legacy role or a capability the format cannot hold, the secrets of a ServiceAccount, and a template that is a symbolic link now keep the fix off the file, and the finding names the case. The localized texts of a capability are compared.
- A condition holds when anything the declaration writes under it renders, in any file, rules included: an absent object or rule under it is drift the fix writes.
- The template reader keeps a string literal with a brace inside a condition and a field named required, so every file the declaration produces reads back as its own.
- The coverage stub is not offered for a symbolic link, the rewrite log prints empty lists, and a missing file is logged among the additions; an empty rbac.yaml is named as empty.
- Tests that could not fail now make the file diverge or differ on disk first, so that a fix that ran or was kept off shows.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Bootstrap took the last render variant of an object whose rules differ between --matrix variants and did not count a variant with no RBAC object, so a conditional right could be written as undecided and removed by the next fix; the variants now give the union, marked as varied by a block. A scrape Role became prometheusAccess whatever it granted, narrowing verbs or writing an empty section; it now does so only for get on the named workloads. An object left hand-written by several passes was listed, and counted, once per pass. The generator writes no system capability for a module without a subsystem: it would aggregate into no role and fail the contract.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A component path is a clean relative path of DNS labels, the name of an access entry or an extra role a valid role name, and a when a single line: otherwise the file the declaration names never matches the render, or the rewrite writes a document the lint cannot read back. A trailing --- holds no document. An own action without a level gets one finding, and a view or edit key names the level that grants it.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The README says what now keeps the fix off a file, that a condition is judged across the render, where the namespace label goes, what the coverage stub does to the layout of the file and when sync stays silent. The e2e README lists sync-module-role, and two coverage cases describe the level they expect.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ly what keeps its metadata

A condition the declaration writes around an object or a rule that the template lacked was never reported: the rule rendered for every value, so it counted as holding. The conditions each object and its rules render under are now read from the text of the template and of what the declaration writes, per object, both ways: a missing one is a divergence the fix writes, an extra one a divergence without a fix. The text is the same in every variant, so a template gated by a condition the declaration does not write is a finding even where nothing of it renders.

An object under an old name counted as renamed on its rules and subjects alone, so a rewrite dropped its rbac.authorization.k8s.io aggregation labels and helm.sh/resource-policy and logged nothing; it is a rename now only when it has no aggregationRule and the new object keeps every label and annotation outside the role model's own.

The legacy-scheme case names the smallest kind the file renders, not the one map order gives.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
An object written in two branches of one block inside one document, or as a named and a computed name in two branches, was given the condition of the first branch found. Such an object now stays hand-written with the reason, and Locate refuses candidates of the named and the computed kind that disagree.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…te writes

The labels of accounts, access entries and the scrape access are held to valid values and may not set heritage or module; bound roles and cluster roles to role names and a namespace to a DNS label; ServiceAccount subjects to their name and namespace rules; scraped workloads to DNS subdomains. They rendered and linted before and failed only on install. The comment on Validate says what coverage does with a declaration that does not validate.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
… and the new validation

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A module may ship a subsystem of its own (virtualization: module.yaml subsystems, its d8:subsystem:virtualization:<level> roles and the capabilities that aggregate into it). The contract called that lineage unknown and the validation of rbac.yaml refused it in subsystems. A subsystem the module's module.yaml declares beyond the seven the platform ships now has the levels of every subsystem for that module; another module's own subsystem stays unknown.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A module.yaml subsystem beyond the platform's counted as the module's
own whatever it was, so a typo silenced the contract and reached the
declaration check and the generator. It is the module's own now only
when the render holds a d8:subsystem:<name>:<level> role labelled with
the module; any other is a contract finding on module.yaml, and sync
validates and derives against the filtered list.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A block opened inside a document and still open at the next separator
was left off that document, so its object read back as unconditional.
It belongs to its own document now when document content follows the
opening line; the generator's {{- if X }} right before the next ---
still belongs to the next object only, which a round trip over the
generated files checks.

Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
@Jabejixo
Jabejixo force-pushed the feat/rbac-yaml-template-conditions branch from a7ec1e4 to 1d770f8 Compare September 29, 2026 12:45
// renderedSubsystems are the subsystems the module renders a d8:subsystem:<name>:<level> ClusterRole
// for, labelled as the module's own.
func renderedSubsystems(m pkg.Module) map[string]bool {
rendered := map[string]bool{}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 12. A subsystem whose roles render conditionally is "unrendered" in the variants where they don't render

renderedSubsystems reads the current render variant only. If a module's own d8:subsystem:<name>:* roles are rendered under a condition (the version gate, a values flag), then in a --matrix variant where they don't render the contract rule reports the subsystem on module.yaml as unrendered, and knownSubsystems (sync.go:159) drops it, so ValidateFor rejects the rbac.yaml subsystems: entry and sync says "nothing is compared or written until the declaration is valid" for that variant: a false finding plus a blind sync.

This comes from reading the code only; I did not reproduce it. If no module gates its own subsystem roles (virtualization being the known case), feel free to drop it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No change needed: --matrix varies only the module's own values (buildOverride keys the overrides by the module name), and the version gate reads global.deckhouseVersion, which is dev/test in every render, so the gate always renders the new roles. No module gates its own subsystem roles on a module value: none in deckhouse main declares one, and virtualization has no d8:subsystem:virtualization:* roles yet.

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.

2 participants