Repository navigation
feat: add @name directive for resource-name constraints - #33
Conversation
A chart's naming budget is a property of the chart: it follows from the sub-resources the chart renders and the release names they take. Until now there was no way to write it down, so the only place it could live was in whatever consumer created the resource, as a branch keyed on the chart's kind. @name declares a schema for the resource's own metadata.name instead of for a values key. It reuses the existing constraint accumulator, so @minLength/@maxLength/@pattern attach to it exactly as they do after a @PARAM, and it is published at the root of values.schema.json as x-cozystack-name, beside properties rather than inside it — it does not describe a values key, and the x- prefix keeps JSON Schema validators from tripping over it. The declaration produces no values key, no Go struct field and no README row, and a chart that does not use it generates byte-identical output. Assisted-By: LLM Signed-off-by: Timofei Larkin <[email protected]>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
Timofei Larkin (@lllamnyp) NOT LGTM. For every chart that doesn't declare @name the output is byte-identical to v1.6.0, but a constraint line that @name can't carry is accepted and then dropped without a word.
The directive lets a chart declare the limits on its own metadata.name, so the API server in cozystack/cozystack#4367 can enforce them without a branch per kind.
I ran this head and v1.6.0 over all 33 annotated charts in cozystack main (packages/apps/*, packages/extra/*, packages/library/cozy-lib) with each chart's own make generate, which writes the schema, README, Go types and ApplicationDefinition. diff -r between the two trees is empty. With this head, the kubernetes chart from #4367 regenerates to exactly the values.schema.json and cozyrds/kubernetes.yaml committed there. go test ./... passes: 125 top-level tests, 119 at v1.6.0.
The blocker is that constraint lines which don't apply to a name are swallowed. After @name the parser takes every constraint directive into the name row, and marshalResourceName writes out only type, description, minLength, maxLength and pattern. @minimum, @maximum, @exclusiveMinimum, @exclusiveMaximum, @minItems, @maxItems, @immutable, @x-* and @example all vanish with no error and no warning. A values.yaml with ## @name {string} - Cluster name. followed by @maxLength 32, @minimum 5, @maximum 9, @minItems 1, @immutable, @x-cozystack-foo bar and @example "abc" generates with exit 0, empty stderr, and "x-cozystack-name": {"type":"string","description":"Cluster name.","maxLength":32}.
The likely slip is ## @maximum 32 written instead of ## @maxLength 32. It generates cleanly, the chart ships with no cap on its name, and nobody finds out until a long name gets through the API server. Once a release accepts these lines, turning them into an error later breaks any chart that already has one. The fix is small: in Parse, when lastAnnotated.K == kName and the line is any constraint or extension directive other than @minLength, @maxLength or @pattern, return file:line: @<directive> does not apply to @name. A test per rejected family next to TestParseNameRejectsNonString pins it.
Not blocking:
- A
@nameline the pattern doesn't match is skipped silently, and the constraints under it land on the@paramabove.## @name {string} Cluster name.(no-) followed by## @maxLength 32, placed afterstorageClass, puts"maxLength": 32onstorageClass. The only signal is the late-constraint warning, which namesstorageClass.@paramtypos behave the same way today, so I don't block on it. A loose^#+\s+@name\bcheck that errors when the strict pattern fails would catch it. - Nothing tests
@nameplaced after a@param. I removedfinalizeLastAnnotated()from the@namebranch: the@paramabove is then dropped from the output, and all tests stay green. Seven other mutations of the new code turned exactly the tests I expected red. One test with a@paramabove@namecloses the gap. - The generator could also catch declarations that no name can satisfy:
@maxLength 0,@minLengthabove@maxLength, or a@patternthat Go'sregexpcan't compile. A lookahead like^(?!kube)[a-z]+$is valid JSON Schema but not RE2. All three generate cleanly today, and the API server in #4367 then rejects every name for the kind or drops the pattern.
|
|
||
| // marshalResourceName renders a @name node as the JSON object published under | ||
| // resourceNameExtension, indented to sit at the top level of the schema file. | ||
| func marshalResourceName(n *Node) ([]byte, error) { |
There was a problem hiding this comment.
Only these five keys reach the output, but after @name the parser also accepts @minimum, @maximum, @minItems, @immutable, @x-*, @example and the rest, and they are dropped here without an error. @maximum 32 written for @maxLength 32 generates cleanly with no cap at all.
There was a problem hiding this comment.
Fixed in f8efc44: under @name, each of these directives is now an error at its own line (@maximum does not apply to @name, which takes only @minLength, @maxLength and @pattern), with one subtest per directive in TestParseNameRejectsInapplicableConstraints.
| // Constraint annotations that follow it (@maxLength, @minLength, @pattern) | ||
| // accumulate onto it exactly as they do after a @param. | ||
| // Groups: 1=type, 2=description | ||
| const NamePattern = `^#{1,}\s+@name\s+\{([^}]+)\}(?:\s+-\s+(.*))?$` |
There was a problem hiding this comment.
A @name line this doesn't match (for example one without - before the description) is skipped silently, and the @maxLength under it attaches to the @param above.
There was a problem hiding this comment.
Fixed in f8efc44: a line opening with the @name token that this pattern does not match now fails as malformed @name annotation rather than being skipped. TestParseNameRejectsMalformedHeader covers the missing -, a missing type and a bare @name, each placed under a @param so the old leak would show.
| // so it takes the lastAnnotated slot to collect the constraint lines | ||
| // that follow, but never becomes a values key. | ||
| if m := reName.FindStringSubmatch(line); m != nil { | ||
| finalizeLastAnnotated() |
There was a problem hiding this comment.
No test covers @name after a @param. Without this call the @param above is dropped from the output and every test stays green.
There was a problem hiding this comment.
Covered in f8efc44 by TestParseNameAfterParam: a @param with its own @minLength above @name, asserting the param survives with its constraint and that nothing leaks in either direction. With this call removed the test now fails.
After @name the parser accumulated every constraint directive, but only the string ones reach the schema, so @Minimum, @minItems, @immutable, @x-* and @example under @name vanished without a word. The likely slip is @maximum 32 typed for @maxlength 32: it generated cleanly and shipped a name with no cap. Those directives are now an error at their line. The same silence covered a @name header the strict pattern did not match: it was skipped, and the constraints under it attached to the @PARAM above. A line opening with @name that is not a valid header now fails. A declaration no name can satisfy fails too: @maxlength 0, @minlength above @maxlength, and a @pattern that does not compile as RE2. The Cozystack API server evaluates the pattern with Go's regexp, so a lookahead that is valid JSON Schema would otherwise never be enforced. Rejecting all of this is free before the first release carrying @name and a breaking change after it. Assisted-By: LLM Signed-off-by: Timofei Larkin <[email protected]>
|
Aleksei Sviridkin (@lexfrei) thanks for the mutation runs, they found exactly the gap the tests had. All four points are in f8efc44. Blocker. Every recognised directive a name can't carry — 1. A line opening with 2. 3. Taken in full: The README section lists the restrictions. Rebuilt with this head, cozystack's kubernetes chart from cozystack/cozystack#4367 regenerates byte-identically, so nothing changes there. |
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
Timofei Larkin (@lllamnyp) LGTM. Constraint lines after @name are no longer dropped: every recognised directive a name can't carry now stops generation at its own line.
I ran this head on a values.yaml with ## @name {string} - Cluster name. and ## @maxLength 32, adding one more line per run. @minimum, @maximum, @exclusiveMinimum, @exclusiveMaximum, @minItems, @maxItems, @immutable, @x-cozystack-foo bar and @example "abc" each exit 1 with values.yaml:3: @<directive> does not apply to @name. The same happens with a @section line in between. @minLength and @pattern still reach x-cozystack-name, and @maxLength 0, @minLength 40 over @maxLength 32 and a lookahead pattern fail at the @name line.
Charts that don't use the directive are unaffected. I regenerated all 33 annotated charts in cozystack main (1071802e) with this head and with v1.6.0, each with its own make generate, and diff -r between the two trees is empty. On the head of cozystack/cozystack#4367 this build reproduces the committed kafka and kubernetes schemas and ApplicationDefinitions byte for byte.
go test ./... passes, 129 top-level tests. I made nine mutations of the new code and seven turned the test I expected red. That includes dropping finalizeLastAnnotated() from the @name branch, which TestParseNameAfterParam now catches.
Not blocking:
- Both survivors sit on the
@minLength/@maxLengthboundary. Changing>to>=, or to> max+1, keeps every test green, because the only case is 10 against 5. A 5/5 case that passes and a 6/5 case that fails would pin it. - A directive the parser doesn't recognise is still skipped under
@name.## @maxlength 32,## @maxLength -1and## @maxLength 20.5generate with exit 0, and the name gets no cap from that line.@paramhas always worked this way, so it isn't new here, but under@namethe result is an uncapped name. - The PR body still says
TestNameSchemaAbsentWithoutDirectivepins byte-identical output. That test only checks that the key is absent. The body also still counts six new tests, and there are ten now.
…hema (#4367) ## What this PR does The aggregated API server capped two kinds' names by knowing what their charts render. A `Kubernetes` cluster name was held to 32 characters because every worker pool is a `KubernetesNodes` release named `kubernetes-nodes-<name>-<pool>`, and the default `md0` pool has to fit the 53-character Helm release name limit. A `Kafka` name was held to 44 because the KRaft controller pod `kafka-<name>-c-<hash>-<id>` has to fit the 63-character DNS-1123 label limit. The arithmetic is right, but it is the charts' knowledge living in the platform: the next application with a sub-resource budget needs another `if r.kindName == ...` branch, and an application served from outside this repository — the whole point of `ApplicationDefinition` — cannot get one at all. An application now declares the limits on its own name in its own schema, and the server applies whatever it finds there for whatever kind the definition describes. The chart's `values.yaml` carries a `## @name` annotation, `cozyvalues-gen` v1.7.0 publishes it at the root of `values.schema.json` as `x-cozystack-name`, and `hack/update-crd.sh` carries it into the `ApplicationDefinition`: ```yaml ## @name {string} - Cluster name. Every worker pool is a separate KubernetesNodes release named `kubernetes-nodes-<name>-<pool>`, so the name must leave room for the default `md0` pool inside the 53-character Helm release name limit. ## @maxlength 32 ``` ```json "x-cozystack-name": { "type": "string", "description": "Cluster name. Every worker pool is a separate KubernetesNodes release named `kubernetes-nodes-<name>-<pool>`, so the name must leave room for the default `md0` pool inside the 53-character Helm release name limit.", "maxLength": 32 } ``` `maxLength`, `minLength` and `pattern` are honoured; `pattern` is unanchored, as JSON Schema specifies. A declaration only tightens: `prefix + name` must still fit the 53-character Helm limit. A rejected name is answered with the chart's own description, or with "limit declared by the <Kind> application" when the chart gives none. The schema is untrusted input — definitions from other repositories are written by hand — so a declaration that cannot be enforced as written (not an object, a type error, an unknown keyword such as a misspelled `maxLenght`, `maxLength` below 1, a negative `minLength`, `minLength` above `maxLength` or above the Helm budget, a pattern that is not RE2) refuses every create of that kind with `configuration error: the <Kind> ApplicationDefinition declares an invalid x-cozystack-name: …`, and logs the same at start-up. Dropping the declaration would admit names the chart cannot render; applying it would reject every name and blame the user. Other kinds are unaffected, and existing objects of the affected kind can still be read, updated and deleted. An upgrade brings `cozystack-api` up before `kubernetes-rd` and `kafka-rd` re-render (`kubernetes-rd` depends on `cozystack-engine`), so the new server can briefly read a definition that predates its declaration. For exactly that case — a definition that declares nothing — the two previous caps stay in `legacyNameCaps` as a floor, to be removed once no supported upgrade starts from such a definition. That is the only per-kind knowledge left in the name checks. Also in this PR: - The `pre-commit` job pins `cozyvalues-gen` v1.7.0, the first release that understands `@name` ([cozystack/cozyvalues-gen#33](cozystack/cozyvalues-gen#33)). - The API review gate now diffs the root `x-cozystack-name` of every definition as `metadata.name`, so a chart that lowers its cap or adds a pattern is reported like any other tightened field. On this PR it reports the Kubernetes and Kafka caps as added; their effective limits are unchanged. - The migration 56 comment that pointed at the removed `maxKubernetesClusterName` now points at the chart's declaration. Not in scope, because none of it is a static limit a chart can declare: the `Tenant` name pattern in `pkg/apis/apps/validation`, the computed tenant-namespace length check (it depends on the parent namespace at request time), the tenant quota hierarchy, and the Phase 2 removed-field warning for `Kubernetes`. ### How it is tested `TestNameCapComesFromTheShippedSchema` reads the shipped `kubernetes` and `kafka` ApplicationDefinitions off disk, builds storage through `NewREST` as the server does, and checks the cap and the chart's explanation; it then rewrites `maxLength` inside the shipped schema and checks the limit moves with it, which a cap held in code would not. `TestInvalidNameSchemaFailsClosed` covers each malformed declaration through the same path, `TestLegacyNameCapsCoverUndeclaredDefinitions` pins the upgrade floor and that a declaration replaces it, and `TestClassifyNameDeclaration` covers the gate. ### Downstream repositories Two repositories are affected and neither follow-up is filed yet, so no box is ticked. - `cozystack/website` — `content/en/docs/next/development.md` documents the `values.yaml` annotations and needs `@name`; `content/en/docs/next/cozystack-api/application-definitions.md` describes `openAPISchema` and needs `x-cozystack-name`. - `cozystack/external-apps-example` — it pins no `cozyvalues-gen` version and regenerates against whatever is current; nothing in it declares a name constraint, so it is unaffected until it chooses to. - [ ] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note feat(api): Application name limits now come from the application's own schema instead of being hardcoded per kind in the aggregated API server. A chart declares them with a `## @name` annotation in `values.yaml`, which cozyvalues-gen v1.7.0 publishes as `x-cozystack-name`; the API server enforces `maxLength`, `minLength` and `pattern` on `metadata.name` for any kind, answers a rejected name with the chart's own explanation, and refuses creates of a kind whose declaration it cannot enforce with a configuration error. The Kubernetes (32) and Kafka (44) name caps are unchanged in effect and are now declared by their charts. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Applications can declare minimum and maximum name lengths and formatting patterns. * Kubernetes cluster names are limited to 32 characters, and Kafka cluster names to 44 characters, to meet generated resource naming limits. * Names that violate declared constraints are rejected with an explanatory message. Tighter name constraints are also reported as breaking changes. * Declared maximum lengths cannot exceed the platform’s existing Helm release-name budget. * **Documentation** * Added guidance on declaring and enforcing chart-specific resource-name limits. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What this PR does
A chart's naming budget is a property of the chart: it follows from the sub-resources the chart renders and the release names they take. Until now there was no way to write it down, so the only place it could live was in whatever consumer creates the resource, as a branch keyed on the chart's kind.
cozystack/cozystackcarries exactly that today — its aggregated API server caps aKubernetescluster name at 32 characters because it knows that chart renders worker pools askubernetes-nodes-<name>-<pool>releases.@namelets the chart say it instead. It declares a schema for the resource's ownmetadata.namerather than for a values key, must be declared{string}, and is allowed once per file.becomes, at the root of
values.schema.json, besidepropertiesrather than inside it:It reuses the existing constraint accumulator, so
@minLength,@maxLengthand@patternattach to it exactly as they do after a@param— the directive itself adds one regex, one row kind, one field on the tree root and one root key in the schema writer. Thex-prefix keeps JSON Schema validators from tripping over the key; Helm renders a chart carrying it without complaint.The declaration produces no values key, no Go struct field and no README row. A chart that does not use it generates byte-identical output, which
TestNameSchemaAbsentWithoutDirectivepins.Tests
Six new tests in
internal/openapi/name_test.go: parsing and constraint attachment, rejection of a non-{string}type token and of a duplicate declaration, root-level emission with the description andmaxLength, absence of the key when nothing is declared, andminLength/patternreaching the schema.Downstream
The consumer is cozystack/cozystack#4367, which moves the hardcoded per-kind cap out of the API server and onto this directive. That PR cannot merge until this is released and the
cozyvalues-genpin in itspre-commitworkflow is bumped, so a release after this lands would unblock it.cozystack/external-apps-examplepins no version of this tool and regenerates its charts against whatever is current; nothing in it declares a name constraint, so it is unaffected until it chooses to.