Repository navigation
Conversation
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>
8da8b0e to
9f5da1d
Compare
…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>
…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>
d413db8 to
5f50bd4
Compare
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>
A run with global.deckhouseVersion below the version gate renders none of the d8:subsystem:<name>:* roles a module ships behind the gate, so contract reports the module.yaml subsystem as unrendered and sync refuses an rbac.yaml subsystems: entry that names it. The default render and --matrix answer the gate with the new model; the README names the manual run as a known limit. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The platform lists its subsystems as iam, security, cluster, delivery, network, storage, observability and managed-service, with the roles d8:subsystem:<name>:<level> and the lineage labels rbac.deckhouse.io/aggregate-to-<name>-as. The deckhouse, infrastructure and kubernetes subsystems go into cluster and networking becomes network, with no period that accepts both. A module that still declares or aggregates into an old one gets the module.yaml and unknown lineage findings instead of passing the contract. Objects of the manage/use scheme keep their old lineages and are still not judged by them. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Virtualization moves into the cluster subsystem and ships no d8:subsystem:virtualization roles of its own, so the README and the comments stop giving it as the module that does. A module may still ship a subsystem of its own; the tests keep the name as plain test data. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
splitSubsystems took any non-platform name in module.yaml for a subsystem of the module's own once the render held a d8:subsystem:<name>:<level> role for it, the legacy deckhouse, infrastructure, kubernetes and networking included. The migration script and the legacy custom role alert treat those ids as retired, and the finding for subsystems: [kubernetes] listed the eight subsystems without saying which one took its place. The four legacy ids are now refused as the module's own even when a role backs them, and the module.yaml finding, the rbac.yaml subsystems finding and both unknown-lineage findings name the subsystem of the role model that replaced them (kubernetes is cluster, networking is network). ReplacementOf in rbaccontract holds the mapping. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The DKP subsystem table calls it Managed Services, and the deckhouse role model names it managed-services. The contract set, the tests and the README follow, and the singular id is now the unknown one. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A use capability of the RBACv2 scheme before DKP 1.78 gives rules in a namespace. A system or subsystem role is bound cluster-wide, so a selector of it that takes such a capability by its aggregation label gives these rules in every namespace. The platform test in deckhouse requires every selector of these roles to leave out rbac.deckhouse.io/kind: use with a NotIn expression, and a module that ships the roles of a subsystem of its own was not held to it. The contract rule now reports such a selector with the platform test's wording. The tests cover a subsystem role without the expression, with NotIn [use] and NotIn [role, use], with an expression that keeps use, a system role, and a namespace role, which is not held to it; the README names the check. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
|
|
||
| // splitSubsystems sorts the non-platform subsystems of module.yaml into those the render backs with | ||
| // roles (own) and the rest (unrendered, sorted and without duplicates). A subsystem of the legacy | ||
| // scheme is never the module's own, even when the render holds a role for it: the role model |
There was a problem hiding this comment.
🟡 13. module.yaml subsystems may diverge from the lineages the system capabilities carry, which the platform test in deckhouse forbids
Deckhouse MR !352 adds TestRBACv2ModuleSubsystemsValidation (testing/rbacv2/rbacv2_legacy_aliases_test.go:124-233, f47e77e01c). It requires that a module with templates/rbacv2/manage/*.yaml declares in module.yaml exactly the set of aggregate-to-<lineage>-as lineages (excluding system) its capabilities carry. Docs and the console read module.yaml, and the aggregation controller reads the labels. kube-dns, kube-proxy and istio were aligned to this there.
dmt goes the other way:
- the
rbac.yaml subsystems:override (rbacyaml/types.go:56-59) exists to aggregate into subsystems module.yaml does not declare. Its comment still names kube-dns/kube-proxy/istio, which is stale after !352. - bootstrap writes that override itself when the lineages differ from module.yaml (
bootstrap/bootstrap.go:1116-1121), and only adds a note. - the README (
:1362-1364,:1796) suggests settingsubsystemsinrbac.yaml.
Nothing in contract compares module.yaml with the carried lineages. So an external module passes dmt with a mismatch the platform treats as a bug, and an in-tree module that follows dmt's advice fails testing/rbacv2.
Suggestion: port the equality check (module.yaml subsystems == lineages of the system capabilities) into contract. Turn the rbac.yaml subsystems: override into a finding ("declare it in module.yaml"), or drop it. Bootstrap should propose a module.yaml change instead of writing the override. If dmt is deliberately looser here, please state it explicitly in the README.
There was a problem hiding this comment.
Done in 735b44c, 47854ef, 506d4ca and f0807f8: contract now holds the module.yaml subsystems to the lineages the rendered system capabilities carry and reports a difference on module.yaml, a system capability carrying the namespace or project lineage gets its own finding, rbac.yaml no longer sets subsystems (the key still parses and sync reports it), bootstrap proposes the module.yaml change instead of writing an override, and the README follows.
…ages the system capabilities carry The platform test TestRBACv2ModuleSubsystemsValidation in deckhouse requires a module with system capabilities to declare in module.yaml exactly the subsystems whose aggregate-to-<lineage>-as labels they carry, the system lineage left out: the documentation and the console read module.yaml, the aggregation controller reads the labels. dmt went the other way: an rbac.yaml subsystems: list aggregated the system capabilities into subsystems module.yaml did not declare, bootstrap wrote that list itself when the lineages differed, and nothing compared module.yaml with the labels. The contract rule now holds the module.yaml subsystems to the lineages the rendered system capabilities carry and reports a difference on module.yaml, without a fix, naming both sets and what to change: declare a carried lineage in module.yaml, and aggregate into a declared subsystem or remove it. A module that renders no system capability is not judged, as the platform test skips a module without manage templates. A subsystem of the module's own backed by its roles is compared as any other; an unrendered or legacy module.yaml subsystem and an unknown lineage keep their own finding and stay out of the comparison. rbac.yaml no longer sets the subsystems. The key still parses, and sync reports it as a declaration error that names what module.yaml lacks; the generator takes the subsystems from module.yaml only. Bootstrap writes no subsystems key: when the lineages differ from module.yaml, a note proposes the module.yaml change, and the fix succeeds. The README describes the check, drops the advice to set subsystems in rbac.yaml, and names the limit of a conditional system capability under --matrix. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…for modules outside the deckhouse tree testing/rbacv2 in deckhouse keeps the closed list of the eight DKP subsystems for in-tree modules, in module.yaml and in the d8:subsystem:<name>:<level> roles. The contract rule accepts a subsystem of the module's own, declared in module.yaml and backed by its rendered roles, as agreed for external modules, while the README said the rule checks a module the way the platform test checks an in-tree one. An in-tree module with such a subsystem is green in dmt and red in the platform test. The README purpose, item 5 of the contract checks and the limits, and the doc comment of the rule, now say that this extension holds for modules outside the deckhouse tree only. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ate into the namespace or project lineage TestRBACv2ModuleSubsystemsValidation in deckhouse takes every lineage a system capability carries, system left out, for a module.yaml subsystem. The module.yaml comparison of the contract rule counted a lineage only when it is a subsystem, so a system capability with aggregate-to-namespace-as or aggregate-to-project-as passed dmt and failed the platform test, and no other check reported it: both lineages are known to the aggregation label check, and the capability check had no scope-against- lineage check. The doc comment and the README said such a lineage has a finding of its own. The capability check now reports a system capability that carries the namespace or the project lineage, once per capability, without a fix: remove the label. The module.yaml comparison leaves the two lineages out, so the object is not reported twice. The doc comment and the README name what the comparison leaves out and the finding each of them has. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ems module.yaml can declare The bootstrap note on module.yaml subsystems proposed every lineage the system capabilities carry but system, so it could propose to declare kubernetes, namespace or an unknown lineage in module.yaml, which the contract rule then rejects as a subsystem of the legacy scheme or an unrendered one. The note now proposes what contract accepts: a subsystem of the role model, or one of the module's own that module.yaml declares and the render backs. A lineage of the legacy scheme is proposed as the subsystem that replaced it, as the subsystems finding of rbac.yaml names it; the namespace and project lineages and an unknown one are left out, since contract reports their label. The note stays a note: the fix still succeeds. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…platform test selects by The platform test takes every file of templates/rbacv2/manage/ whatever it renders, while contract judges the system capabilities wherever they render. The README limits now say so, and item 6 no longer reads as if the two selections were the same. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…withholds The bootstrap notes every object the generator names differently as "will be named X by --fix". When the template that object renders from holds a case the declaration does not close (a document the linter cannot read, an account with imagePullSecrets, a condition the declaration does not write), the next --fix leaves that file as it is, so the rename does not happen and the note promised something the fix does not do. A dry run on security-events-manager showed three such notes. The bootstrap fix now asks the sync rule, for the declaration it has just derived, which generated templates the next fix would leave as they are, with the same check the lint runs once the file is written. A rename into such a template is noted as waiting: the object keeps its name for now, and --fix renames it once what dmt lint reports on that file is resolved. A rename into a template the fix rewrites is noted as before. A declaration the generator refuses changes no note. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…tes of a real module
A dry run on security-events-manager showed five places where the bootstrap or the lint after it
told the author something the next step contradicted.
- The rename notes judged the templates against an empty render: dmt releases the render of a
module once it is linted, before any fix runs, so every rename read as an object the declaration
does not produce and was noted as waiting. Each render variant now records what the judgment
reads (the identity of every rendered object, and the objects of the templates that render a
kind the declaration writes), and the fix judges the templates with those records.
- A RoleBinding that bindRoles writes in a namespace of the platform was named
d8:<module>:<path>:<role>, which the placement rule rejects there, and a Role named after the
account repeated the module and the account. The name now follows the placement rule
(<module>:<path>:... in d8-system, d8-monitoring and the other platform namespaces, d8: in
default and kube-system), and a role named after the account lends the binding the rest of its
name.
- A ServiceAccount with imagePullSecrets entered the declaration without a word, and the lint then
withheld the fix of its template. The bootstrap notes it, and both say to set them in the pod
spec of the workloads that run as the account.
- A document with namespace: d8-{{ $.Chart.Name }} was unreadable, which kept the fix off its file.
The lint reads the chart name action as the module name, as Helm renders it, and quotes an
unreadable document at the line that made it so.
- A Role in another namespace that bindRoles names was listed as bound to nothing the declaration
describes, and the lint advised to declare it. The note now says bindRoles binds it and the
declaration does not write it; where it sits in a template the declaration writes, the note and
the finding say to move it.
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…namespace A namespaced resource granted at a system level already needs a reason, because the right applies across the whole cluster. For the resources through which a write grants the identity of a ServiceAccount of the namespace (pods, replicationcontrollers, podtemplates, secrets, serviceaccounts, the apps and batch workloads, roles, rolebindings) and for the subresources that grant it with any verb (pods/exec, attach, portforward, ephemeralcontainers, serviceaccounts/token) no reason is good enough: the role model writes into the namespaces of the platform at the superadmin level only, and the capability of a module must not hand the holders of its subsystem manager the ServiceAccounts of the module. Reads stay allowed; the writes belong to a namespace level. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
22df468 to
d82bedc
Compare
The pilot on security-events-manager excluded the two system capabilities the declaration produces for the module.yaml subsystems, view and edit, which the module does not ship. Their files did not exist, so the lint still reported both files as missing with a fix, and --fix wrote the excluded objects: the check of a missing file counted them as absent, and the case that keeps the fix off a file with an excluded object looked only at a file that holds it. A missing file now counts only the objects sync compares, so a file the declaration puts only excluded objects in is no finding, whether it exists or not. A file that would hold an excluded object beside others is compared on the others, and its finding carries no fix whether the template holds the excluded object or not: the rewrite writes every object the declaration puts in the file. The README says so in one paragraph instead of listing the case among what a file holds. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The pilot on user-authn emptied an existing file of the declaration that holds only an account under `when` (templates/user-api/rbac-for-us.yaml). With values under which the condition holds, nothing rendered the account, its ClusterRole or its binding, and the lint was clean and --fix wrote nothing: an object under a `when` absent from the render was excused unless a sibling under the same `when` rendered, and none could. The account was lost in silence while the Deployment still named it. The render cannot tell a false condition from a template nobody wrote; the text can, as the file system can for a file that does not exist. An object under a `when` absent from the render is now excused only while a template holds it by its text: a document of its kind under its name or a computed name that can produce it, read with the template parser so that a document written by hand counts, or an include of a named template in its own file. Otherwise it is a divergence, the same in every render variant, and the fix writes it back. A declared file that does not exist keeps its one finding for all of its objects. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…nders from The pilot on user-authn bootstrapped an account whose objects render from templates/user-api/rbac.yaml. The bootstrap derived `path` only from templates/<dir>/rbac-for-us.yaml, so the account got none and the declaration put its objects in templates/rbac-for-us.yaml, away from the template that renders them; following that, the account under `when` left its directory for the root file. An account that renders from another template of templates/<dir>/ now gets `path: <dir>` the same way, when it is named after the directory as the placement rule wants it in that directory's rbac-for-us.yaml (<dir> with / as -, or <module>-<dir> in a namespace of the platform) and the directory is a path the declaration accepts. The generator refuses any other path, and with it the whole declaration, so such an account, one at the root of templates/ and one of a module in default or kube-system go to templates/rbac-for-us.yaml as before. The note names the file the fix writes the account to. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…int of CI asks
golangci-lint v2.8.0 of CI reports `verbs := []string{}` before the append loop of identityVerbsOf
(prealloc, added with the identity check of the system levels), which fails the golangci-lint job
and skips the tests. No change in behaviour.
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…scrape Role The pilots renamed and moved the scrape access of both modules: prometheusAccess always produced the Role and RoleBinding access-to-<module> in templates/rbac-to-us.yaml, and the bootstrap folded every scrape Role of a module into it. user-authn's access-to-dex-prometheus-metrics left templates/dex/rbac-to-us.yaml for the root under a new name, security-events-manager's access-to-security-events-manager-metrics lost its suffix. Deckhouse main names its scrape Roles in many ways (access-to-<module>, access-to-<x>-prometheus-metrics, -metrics, -role), and node-manager has them in five component directories. A prometheusAccess entry now takes `path` (the component directory, as access[].path) and `name`; its objects are named as access[] names them (rbaccontract.AccessRoleName): access-to-<module>-<name> at the root, access-to-<path with dashes>-<name> in templates/<path>/rbac-to-us.yaml. With neither the entry is exactly what it was, access-to-<module> at the root. A path needs a name, which the placement rule wants there, and two entries of one path and name are refused. prometheusAccess takes one entry as a mapping, the form modules/101-cert-manager/rbac.yaml of deckhouse main uses and that keeps producing the same objects, or a list of entries; a list of one is written back as the mapping. The Role stays unconditional and the binding under the entry's `when`, as before. Unknown keys of an entry are still refused: the section has an unmarshaller of its own, which the strict decoder does not reach into. The bootstrap writes one entry per scrape Role with the path and name it has: the directory of its template when the name is access-to-<directory>-<name>, else the root. A name no entry can express (one the placement rule does not want) or a RoleBinding named otherwise than its Role folds into access-to-<module> as before, and a note says why. A Role of templates/<dir>/<other file> keeps its directory and moves to its rbac-to-us.yaml, with a note. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The pilot on security-events-manager renamed the controller's RoleBinding d8:security-events-manager:controller:log-shipper-secrets in d8-log-shipper to security-events-manager:controller:log-shipper-secrets: the generator names every binding of bindRoles rbaccontract.AccountForeignBindingName, whatever the module named it. The module's exclude-rules.placement already passes the binding under its name. A bindRoles entry now takes `bindingName`, the name of the RoleBinding; without it the binding gets the derived name as before. The bootstrap fills it from the existing RoleBinding when its name differs from the derived one and the placement rule accepts it, with the module's exclude-rules.placement, in the file the declaration writes it to; the sync rule gets the exclusions of the placement rule for that. It keeps the name too when the rule refuses the derived name as well -- a binding in the namespace of another module, as terraform-manager's in d8-cloud-provider-yandex in the whole-tree run of deckhouse main: a rename would gain nothing, and a note says so. Otherwise the note names the name --fix gives. The placement rule's checks of Roles and RoleBindings are pure functions now, the same for the rule and the bootstrap; its messages do not change. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The templates --fix wrote in the pilots carried the module namespace literally, namespace:
d8-user-authn, where every hand-written template of the module writes d8-{{ $.Chart.Name }}. The lint
compares the rendered objects, so either text passes, but a rewritten file read differently from the
rest of the module.
When the module lives in d8-<module> and its chart is named after the module, the generator now writes
that namespace as d8-{{ $.Chart.Name }}: in metadata.namespace and in the namespace of a ServiceAccount
subject. dmt names the chart after Chart.yaml, or after the module when there is none, as Deckhouse
does; every Chart.yaml of deckhouse main names the module, and security-events-manager has none. A
module whose Chart.yaml names the chart otherwise, and every other namespace (d8-system, d8-monitoring,
kube-system, the namespace of another module), is written literally, as is the rbac.deckhouse.io/namespace
label. The sync rule reads the generated text with the chart name in place, as it reads the templates.
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…d again The whole-tree bootstrap of deckhouse main named openvpn's Role and RoleBinding access-to-openvpn access-to-openvpn-access-to-openvpn: the entry took the whole name, since there is nothing to strip after access-to-<module>, and the generator put the prefix in front of it once more. rbaccontract.AccessRoleName now takes a name that already starts with the prefix of its file -- access-to-<module>- at the root, or access-to-<module> itself, and access-to-<directory>- in a component directory -- as the object's name as it is. A name without the prefix gets it as before, so every declaration written so far produces the same objects; only the doubled names go. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…d bindingName The whole-tree bootstrap of deckhouse main renamed the ClusterRoleBindings of user-authz's access entries, d8:user-authz:<role>:system-authenticated, to the names of their ClusterRoles: an access entry wrote its role and its binding under one derived name, d8:<module>:<name> or the access-to-... name, whatever the module named them. An access entry now takes roleName, the name of its ClusterRole or Role, and bindingName, the name of its binding; without them the role gets the derived name and the binding the role's, as before. The bootstrap describes what exists: the role and the binding keep their names when the placement rule, with the module's exclude-rules.placement, accepts them in the file the declaration writes them to, or refuses the derived name as well (then a note says so). Otherwise --fix renames them, and the note now says why: the placement rule refuses the name in that file. The bindRoles binding name of the previous change follows the same helper, and the placement rule's check of ClusterRoles and their bindings is a pure function the bootstrap asks too. The entry of a namespace access is named after its Role now, not after its binding. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…e than its Role The whole-tree bootstrap of deckhouse main folded the scrape access of cloud-data-discoverer into access-to-<module>: the 11 cloud providers and csi-vsphere name the Role access-to-cloud-data-discoverer-role and its RoleBinding access-to-cloud-data-discoverer-binding, and an entry wrote both under one name. istio's access-to-multicluster-metrics-exporter-role and -role-binding went the same way. A prometheusAccess entry now takes bindingName, the name of its RoleBinding; without it the binding takes the Role's name, as before. The bootstrap keeps the Role's own entry for such a pair and fills bindingName where the placement rule, with the module's exclude-rules.placement, accepts the binding's name in the entry's file, or refuses the derived name as well; otherwise --fix renames the binding and the note says why. A Role whose name no entry can express (vertical-pod-autoscaler's access-to-vpa-metrics in templates/rbac-to-us.yaml, refused by the placement rule) still folds into access-to-<module>, with its binding. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ngName The whole-tree bootstrap of deckhouse main renamed the ClusterRoleBindings an account has to roles it does not own -- kube-dns's d8:kube-dns:coredns:rbac-proxy, kube-proxy's d8:kube-proxy and d8:kube-proxy-metrics-rbac, node-local-dns's d8:node-local-dns, user-authz's d8:user-authz:permission-browser-apiserver:auth-delegator, cloud-provider-dvp's ...:validation-webhook:d8-rbac-proxy -- to d8:<module>:<account>:<role>: a bindClusterRoles entry was the role's name only. An entry is now the role's name, as before, or a mapping of name and bindingName, the name of the ClusterRoleBinding; an entry without bindingName is written back as the name alone, so every declaration written so far reads and writes as it did. The bootstrap fills bindingName where the placement rule, with the module's exclude-rules.placement, accepts the binding's name in the account's file, or refuses the derived name as well; otherwise --fix renames the binding and the note says why. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ame and roleBindingName The whole-tree bootstrap of deckhouse main renamed the Role an account has in the module namespace and its RoleBinding to the account's name, or to its directories: the leader-election Roles of the cloud providers (capdvp-controller-manager-leader-election-role -> capdvp-controller-manager, vcd's ...-leader-election with its ...-leader-election-role-binding), kube-proxy's and control-plane-manager's d8:<module> in kube-system, node-local-dns's d8:node-local-dns:endpoint-slice-reader, node-manager's bashible-apiserver-readconfig and capi-controller-manager:leader-election-role. An account now takes roleName, the name of the Role of its namespaceRules, and roleBindingName, the name of its RoleBinding; without them the Role gets the derived name and the binding the Role's, as before. Either without namespaceRules is an error. The bootstrap fills them where the placement rule, with the module's exclude-rules.placement, accepts the name in the account's file, or refuses the derived name as well; otherwise --fix renames the object and the note says why. A Role of the module and its RoleBinding that both live in an rbac-to-us.yaml are an access entry whoever they bind: registry-packages-proxy's access-to-registry-packages-proxy-from-http, bound to the module's own account, was taken as the account's namespaceRules and renamed and moved to templates/rbac-for-us.yaml; it now stays in templates/rbac-to-us.yaml under its name. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The whole-tree bootstrap of deckhouse main renamed the ClusterRoleBinding of an account's own ClusterRole to the role's name: cloud-provider-vcd's d8:cloud-provider-vcd:capcd-controller-manager-binding, and node-manager's bindings of extra roles, ...:bashible-apiserver:auth-reader, ...:nodeconfigurations-reader, ...:nodeusers-reader and ...:capi-controller-manager:manager-rolebinding. An account now takes clusterRoleBindingName, the name of the ClusterRoleBinding of its clusterRules, and an extraClusterRoles entry bindingName, the name of its ClusterRoleBinding; without them a binding takes the role's name, as before. clusterRoleBindingName without clusterRules, and bindingName of an extra role with bind: false, are errors. The bootstrap fills them where the placement rule, with the module's exclude-rules.placement, accepts the name in the account's file, or refuses the derived name as well; otherwise --fix renames the binding and the note says why. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…nother template An access entry writes namespace access to rbac-to-us.yaml and cluster access to rbac-for-us.yaml, whatever template its role came from, and nothing said so. In deckhouse main node-manager's d8:node-manager:bashible:rpp, which keeps its name (excluded from placement), moves from templates/bashible/rbac-for-us.yaml to templates/rbac-to-us.yaml in silence, and the rename notes of deckhouse's bashible-apiserver-readregsecret and node-manager's bashible named a file the objects are not in. A note now names the move: under the name it has the object is moved by hand, since --fix moves no object between templates; under a new name --fix writes the new one there and the lint then asks to delete the old one. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
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 therbaclinter around it --contract,coverageandsync-- with--fixwriting the first declaration from the render and the templates from the declaration. The full description ispkg/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: RBACv2namespaceandsystemlevels (capabilitiesd8:namespace-capability:<module>:<action>andd8:system-capability:<module>:<action>) and legacyuser-authzlevels (d8:user-authz:<module>:<level>); explicit verbs,whenconditions,noAccesswith a reason,scopeandreasonwhere 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'saccess_terminal), which a resource entry grants under its action andlevelplaces in a level of the lineage;labelsof 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,annotationsandrbacAnnotations,pathfor component directories (nested ones included),automountServiceAccountToken;access[]for arbitrary subjects andprometheusAccessfor the scraper, both withlabels,annotationsandwhen.The rules.
contractruns on every module, with or withoutrbac.yaml: the rendered ClusterRoles undertemplates/rbacv2/follow the platform's label and naming contract. It is a port of the platform'stesting/rbacv2validation, 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 ownmodule.yamldeclares beyond the eight subsystems of the platform is a lineage of that module, for itsd8:subsystem:<name>:*roles, its capabilities and thesubsystemsofrbac.yaml; another module's own subsystem stays unknown. Every selector of a system or subsystem role leaves outrbac.deckhouse.io/kind: usewith aNotInexpression, as the platform test requires: a use capability of the scheme before DKP 1.78 gives rules in a namespace, and a role bound cluster-wide would give them in every namespace.coverage: every CRD undercrds/has an entry that grants levels or denies access with a reason.--fixappends an undecidednoAccess: "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. Awhenexcuses 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:
module.yaml, a declaration in an edition overlay and anrbac.yamlof the earlier shape are findings without a fix.rbac.yaml,--fixwrites it from the render and succeeds; theTODOvalues it leaves are reported by the lint that follows. The coverage stub works the same way.range, a computed name, afail/requiredguard), the version gate, the legacy scheme, a condition ({{ if }}) the declaration does not write, an objectexclude-rules.synckeeps 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--matrixa variant that finds one keeps the fix of every other variant off the file.--fixexits by the level of what is left, as for every other linter. The object store keeps noDroppedlist: 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 }}becomeswhen: X, an{{ else }}branchnot (X), nested blocks anand; a condition with a template variable, and an account whose objects render under different conditions, become aTODO. Objects insiderange,withordefine, 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.Names and namespace. Bootstrap keeps the names the templates already give. A
prometheusAccessentry -- one, or a list when the module has several scrape Roles -- carries thepathandnameof an existing scrape Role, and abindRolesentry thebindingNameof an existing binding; without them the generator writesaccess-to-<module>at the root and the derived binding name, as before. Where an existing name cannot be expressed (a Role and RoleBinding named apart, a name the placement rule refuses) bootstrap falls back to the generated name and says why in its note. The generator writes the module namespace asd8-{{ $.Chart.Name }}when the module lives ind8-<chart name>, as the hand-written templates do; any other namespace stays literal.Names of the other objects. Each kind of object the declaration names gets an optional name field of its own:
access[].roleNameandbindingName,prometheusAccess[].bindingName,serviceAccounts[].clusterRoleBindingName,roleNameandroleBindingName(the Role ofnamespaceRulesand its RoleBinding),extraClusterRoles[].bindingName, and abindClusterRolesentry, which may now be a mapping ofnameandbindingNamebeside the plain role name, asbindRoles[].bindingNamealready was. Without a field an object gets the derived name as before, so existing declarations read, validate and render unchanged; a field naming an object the entry does not produce is an error. The bootstrap fills a field only where the placement rule, with the module'sexclude-rules.placement, accepts the existing name in the file the declaration writes the object to, or refuses the derived name as well; otherwise--fixrenames the object and the note says which file refuses which name. An access name that already starts withaccess-to-<module>is no longer prefixed twice, and a note names an access entry's object that moves to another template. On deckhouse main the rename notes of the bootstrap drop from 117 to 13, each a name the placement rule refuses in its target file.Configuration.
global.linters-settings.rbac.rules.{contract,coverage,sync}.impactsets each rule's level, read from the root.dmtlint.yamlas every dmt setting;linters-settings.rbac.exclude-rules.{contract,coverage,sync}excludes objects orgroup/resourcekeys. The three rules start atwarnwherever nothing sets them, and a rule atignoredis not run, so--fixrewrites nothing on behalf of findings nobody sees; the four originalrbacrules 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, andno-cyrillic, which leaves alone the module'srbac.yamland theru.meta.deckhouse.io/title|descriptionlines the role model requires -- Cyrillic anywhere else in those files is still reported.Behaviour to know.
error, as for every linter:rules: {sync: {impact: ignore}}(forignored) runs sync aterror, and a misspelled rule key such ascoverge:is dropped. The rbac blocks get no stricter key check than the other linters.--fixdoes not lint again (cmd/dmt/main.go): a coverage stub is a fix that succeeds, sodmt lint --fixexits 0 with coverage aterror, and the next plain run reports theTODOit wrote.not (X)for an{{ else }}branch andand (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.
-race, golangci-lint 2.13. The e2e cases undertest/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.main, 41 modules):dmt lint --linter rbacgives 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 inrbac.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-authzroles,rbac-for-us.yamlandrbac-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:
dmtalready renders the chart, sosynccompares 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: ADRplatform-security/2026-04-27-module-rbac-yaml.md(architecture-decision-records).