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>
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>
d413db8 to
5f50bd4
Compare
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>
AllLineages and ConventionalActions had no user; generate.Render, Model.Paths, Object.AggregationLabels and resetFixState served tests only and move to _test.go files; appendStub no longer returns a bool nobody reads; the lint-time bootstrap no longer scans the templates for what did not render (only the written file carries those notes); recordRemovals becomes recordChanges, since it keeps additions too. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
… provide LegacyKebab is strcase.ToKebab (as the user-authz rule builds the same name); the template walkers are fsutils.GetFiles with one filter (every file under templates/ but partials; unrenderedObjects no longer skips non-YAML files, which hold no documents); the shared fix state uses internal/set; the bootstrap reads the CRD scopes with crdScopes; the document separator of a template is one regular expression; the rbaccontract constants replace the spelled-out capability and legacy prefixes and the module labels; slices.Contains, maps.Clone and IsSubsystem replace hand loops; one table lists the RBAC kinds. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…storage package grantsExactly replaces three copies of the comparison of a produced object's rules, conditional ones included, with rendered rules; the bootstrap no longer shadows the storage package; managedObject literals name their fields. 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>
| // render, so an object or a rule absent under a condition that holds is drift, whichever file it | ||
| // belongs to -- a new grant under a condition some other grant already renders under must be | ||
| // written, not excused (review of #479, finding 47). | ||
| func holdingConditions(model *generate.Model, actual map[string]managedObject) map[string]bool { |
There was a problem hiding this comment.
🔴 1. Declared when missing from the template: the unconditional grant is never reported
The declaration grants challenges only under .Values.certManager.internal.acmeEnabled, while templates/rbacv2/use/view.yaml has no {{ if }} around it. The tuple renders for every value of the flag, so holdingConditions marks the condition as holding and nothing diverges. The text check in unfixable (L381) only goes one way: it catches a condition the template has and the rewrite would not write, never a condition the declaration writes and the template lacks.
Result: zero findings in every --matrix variant, and --fix never narrows the template. The template grants more than the declaration, which is exactly the drift sync exists to catch. A throwaway test with that setup gives findings: [].
Suggestion: when the declaration puts an object/rule under when: X and the file that renders it has no {{ if }} whose condition matches X, report it (and let --fix write the condition).
There was a problem hiding this comment.
Fixed in 5503908: the conditions around each object and its rules are now compared with the template text, per object, both ways. A condition the declaration writes that the template lacks is a divergence the fix writes back. Test: TestSync_ConditionsOfEveryObject.
|
|
||
| for _, doc := range textDocuments(string(content)) { | ||
| held[doc.id] = append(held[doc.id], path) | ||
| if replacedByProduced(object, file.Objects, renderedRoles, run.rendered) { |
There was a problem hiding this comment.
🔴 2. The rename path drops aggregation labels and resource-policy: keep, and logs removed: []
replacedByProduced (L649) pairs an object under an old name with a produced one on rules and subjects only; labels, annotations and aggregationRule of the old object are not looked at, and since it is not a managed object none of its metadata is compared.
Scenario: ClusterRole d8:cert-manager:admin-kubeconfig-old carries rbac.authorization.k8s.io/aggregate-to-admin: "true" (via the generator's own helm_lib_module_labels dict form, so the document stays readable) and helm.sh/resource-policy: keep; the declaration produces the same rules and subjects under the new name. --fix rewrites the file: the aggregation to admin (a right) and keep are gone, and the warning says removed: []. That contradicts the promise that a rewrite never drops what sync does not compare.
Suggestion: treat a replaced object as a rename only when its labels/annotations/aggregationRule are also carried over (or are empty); otherwise withhold the fix and name the case.
There was a problem hiding this comment.
Fixed in 5503908: it counts as a rename only without an aggregationRule and when the new object keeps every label and annotation outside the role model's own. Otherwise the file gets no fix. Test: TestSync_RenameCarriesMetadata. On the platform tree this caught one real case: app: docs-builder in documentation.
| // it: the render skipped the template (and warned about it) or every object in it is under a | ||
| // condition false for these values. Either way the render tells nothing about them, and they are | ||
| // judged where the template renders. | ||
| func (run *syncRun) withoutUnrendered(file generate.File) generate.File { |
There was a problem hiding this comment.
🟡 3. A condition the template adds hides an unconditional object that is absent: no finding and no fix
Wrap templates/rbac-to-us.yaml in {{- if .Values.foo }} with no when anywhere in the declaration. With foo=false nothing renders from the file, so withoutUnrendered drops every object of its text from the comparison. unfixable does find the case and calls withholdFix, but report only emits for paths that have divergences, so the result is neither a finding nor a fix. With foo=true everything renders and matches. A throwaway test gives findings: [].
This breaks "either a lint finding or a fix that succeeds", and "a when excuses an absent object only while the condition is false" (here there is no when at all). Before this PR, Store.Dropped separated a skipped template from a false condition; the heuristic that replaced it does not. At minimum, a path with a withheld fix should get a finding even when it has no divergences.
There was a problem hiding this comment.
Covered by the same change: a condition the declaration does not write is a divergence read from the text, so the file gets a finding without a fix even when nothing renders from it.
| return out | ||
| } | ||
|
|
||
| func (r *reader) doc(text string, docStart, docEnd int) (Doc, bool) { |
There was a problem hiding this comment.
🟡 4. Bootstrap can give an object the condition of the wrong branch
(a) doc: an if/else inside a single YAML document with no --- between the branches. Only the first kind: of the document is read, so an object rendered from the else branch gets the if condition:
---
{{- if .Values.a }}
kind: ClusterRole
metadata: {name: foo}
rules: []
{{- else }}
kind: ClusterRole
metadata: {name: foo}
rules: [{apiGroups: [""], resources: [pods], verbs: [get]}]
{{- end }}Locate returns when: .Values.a with Partial=false, although with default values (a false) foo came from the else branch.
(b) Locate (L440): byName is returned before templated is looked at, and the agreement check runs only within one set. With {{ if .Values.legacy }} name: d8:foo:x {{ else }} name: d8:{{ .Chart.Name }}:x {{ end }}, the object rendered with legacy=false gets when: .Values.legacy.
In both cases holdingConditions later treats the wrong condition as holding (an object declared under it rendered), which skews the check for everything else declared under that condition. --fix is saved only by the unrelated "template holds {{ else }}" case.
Suggestion: mark the doc Partial/Unmanageable when a block around it has an else branch inside the same document (or it has a second kind: span); in Locate, return ok=false when templated candidates disagree with byName[0] on When/Unmanageable.
There was a problem hiding this comment.
Fixed in 293eee7: an object in several branches of one document stays hand-written, and Locate refuses named and computed candidates that disagree. Test: TestLocate_BranchesOfOneBlock.
| if strings.HasPrefix(sa.Path, "/") || strings.HasSuffix(sa.Path, "/") || strings.Contains(sa.Path, "..") { | ||
| report("%s: path must be a directory under templates/ without leading or trailing slashes, got %q", where, sa.Path) | ||
| } | ||
| validateMetadataKeys(sa.Labels, where+".labels", false, report) |
There was a problem hiding this comment.
🟡 5. Names and values the rewrite writes are not all validated
- Labels: only
capabilities.*.labelsgetIsValidLabelValueand the reserved-key check (validateCapabilityLabels). OnserviceAccounts[].labels(here),access[].labels(L595) andprometheusAccess.labels(L197) only the keys are checked, soapp: "has space"orheritage: foo/module: barpass. The value is rejected by the apiserver at install;heritage/modulealso duplicate whathelm_lib_module_labelsalready writes. bindRoles/bindClusterRoles(L556-567) are checked for non-empty only:bindClusterRoles: ["a/b"],bindRoles: [{namespace: "Bad NS", name: "r/1"}]pass, and the binding names and namespace built from them are invalid.- Subjects in
access[](L600-625) andprometheusAccess.deployments/workload names: name/namespace are not validated;deployments: [""]passes, although an emptyresourceNamesvalue is refused for SA rules.
All of these render, pass lint and fail only on install, which contradicts "validate names, paths and conditions as the rewrite writes them".
There was a problem hiding this comment.
Fixed in 8787ef0: the labels of serviceAccounts, access and prometheusAccess (values, no heritage/module), bindRoles/bindClusterRoles, ServiceAccount subjects and workload names are all validated now.
| // No declaration: nothing to cover (R22). A declaration that does not parse, or that lies | ||
| // in an edition overlay (D7), is the sync rule's finding; reporting it twice would only | ||
| // double the noise. | ||
| if err != nil || decl.APIVersion != rbacyaml.APIVersionV1Alpha1 || editionOverlay(modulePath) != "" { |
There was a problem hiding this comment.
🔵 6. The coverage --fix writes rbac.yaml without running Validate
The only caller of rbacyaml.Validate is sync.go:159. A declaration that parses but does not validate (scope: TODO, a duplicate entry, a bad verb) still gets "no entry" findings here plus the stub fix, and appendStub rewrites the file. The doc comment on Validate (validate.go:131-138) says the coverage rule refuses to run on such a declaration. The impact is low since the stub is additive, but either the comment or the code should change.
There was a problem hiding this comment.
Corrected the comment: coverage judges any declaration that parses, since the stub only adds an entry. That way CRDs still owed a decision stay visible while a TODO scope blocks sync.
| held := map[string][]string{} | ||
| // The model before 1.78: rewriting the file would serve the new model only. | ||
| if level := object.Unstructured.GetLabels()[rbaccontract.LabelKind]; kind == "ClusterRole" && rbaccontract.IsLegacyKind(level) { | ||
| misplaced["legacy scheme"] = fmt.Sprintf("the template renders the legacy RBACv2 scheme (%s: %s, the manage/use model before DKP 1.78) where the declaration produces the 1.78 model; migrate the module with rbacv2-migrate-module.sh to serve both, or delete the file and run `%s` to serve the new one only", |
There was a problem hiding this comment.
🔵 7. The "legacy scheme" message is not deterministic
misplaced["legacy scheme"] is assigned inside for id, object := range fromFile, so the last writer by map order wins. A file that renders both manage and use legacy ClusterRoles alternates between ...kind: manage... and ...kind: use... (2 distinct texts in 40 runs). Across --matrix variants dedupeErrors then does not collapse them, and golden tests get flaky. Picking the smaller kind, as legacyFiles does, fixes it.
There was a problem hiding this comment.
Fixed in 5503908: the finding names the smallest legacy kind. Test: TestSync_LegacySchemeMessageIsStable.
|
|
||
| return []string{fmt.Sprintf("unknown key(s) %s under %q: the accepted keys are %s", | ||
| strings.Join(unknown, ", "), path, strings.Join(slices.Sorted(maps.Keys(known)), ", "))} | ||
| return nil |
There was a problem hiding this comment.
🔵 8. Behavioral changes worth a line in the description
- With the strict rbac-key validation removed here,
rules: {sync: {impact: ignore}}(typo) goes through the unknown-string path ofParseStringToLeveland becomeserror: sync runs,--fixrewrites templates, the exit is non-zero.coverge:or a module-levellinters-settings.rbac.rulesis dropped silently. Same as other linters, but these are the rules people will be tuning. --fixdoes not re-lint (cmd/dmt/main.go): once the coverage stub counts as a successful fix,dmt lint --fixexits 0 even with coverage aterror, and the next plain run fails on theTODO.- Bootstrap writes
not (X)/and (A) (B)for{{ else }}and nested ifs, while the generator never writes those blocks, sounfixablekeeps--fixoff every such file until the conditions are removed from the template by hand. The README says so; the PR description does not.
There was a problem hiding this comment.
Added to the description under "Behaviour to know".
…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>
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 seven the platform ships (virtualization) is a lineage of that module, for itsd8:subsystem:<name>:*roles, its capabilities and thesubsystemsofrbac.yaml; another module's own subsystem stays unknown.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.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).