Skip to content

feat: add @name directive for resource-name constraints - #33

Merged
Aleksei Sviridkin (lexfrei) merged 2 commits into
mainfrom
feat/name-schema
Sep 23, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 2 commits into
mainfrom
feat/name-schema

Conversation

@lllamnyp

Copy link
Copy Markdown
Member

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/cozystack carries exactly that today — its aggregated API server caps a Kubernetes cluster name at 32 characters because it knows that chart renders worker pools as kubernetes-nodes-<name>-<pool> releases.

@name lets the chart say it instead. It declares a schema for the resource's own metadata.name rather than for a values key, must be declared {string}, and is allowed once per file.

## @name {string} - Cluster name. Worker pools are separate releases 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

becomes, at the root of values.schema.json, beside properties rather than inside it:

"x-cozystack-name": {
  "type": "string",
  "description": "Cluster name. Worker pools are separate releases 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
}

It reuses the existing constraint accumulator, so @minLength, @maxLength and @pattern attach 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. The x- 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 TestNameSchemaAbsentWithoutDirective pins.

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 and maxLength, absence of the key when nothing is declared, and minLength/pattern reaching 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-gen pin in its pre-commit workflow is bumped, so a release after this lands would unblock it. cozystack/external-apps-example pins 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.

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]>
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: b8066570-c057-4f35-9066-b68a8fc7233c


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

  1. A @name line the pattern doesn't match is skipped silently, and the constraints under it land on the @param above. ## @name {string} Cluster name. (no -) followed by ## @maxLength 32, placed after storageClass, puts "maxLength": 32 on storageClass. The only signal is the late-constraint warning, which names storageClass. @param typos behave the same way today, so I don't block on it. A loose ^#+\s+@name\b check that errors when the strict pattern fails would catch it.
  2. Nothing tests @name placed after a @param. I removed finalizeLastAnnotated() from the @name branch: the @param above 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 @param above @name closes the gap.
  3. The generator could also catch declarations that no name can satisfy: @maxLength 0, @minLength above @maxLength, or a @pattern that Go's regexp can'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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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+(.*))?$`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No test covers @name after a @param. Without this call the @param above is dropped from the output and every test stays green.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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]>
@lllamnyp

Copy link
Copy Markdown
Member Author

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 — @minimum, @maximum, @exclusiveMinimum, @exclusiveMaximum, @minItems, @maxItems, @immutable, @x-*, @example — is now an error at its own line under @name: values.yaml:2: @maximum does not apply to @name, which takes only @minLength, @maxLength and @pattern. One subtest per directive in TestParseNameRejectsInapplicableConstraints.

1. A line opening with @name that the strict pattern doesn't match now fails as malformed @name annotation instead of being skipped. The trigger is the @name token at the start of a comment, so ordinary prose can't hit it; the only such line across cozystack's charts today is the one #4367 adds.

2. TestParseNameAfterParam puts a @param with its own constraint above @name and checks the param survives with its constraint and that neither side's constraint leaks onto the other. Removing finalizeLastAnnotated() from the @name branch now turns it red.

3. Taken in full: @maxLength 0, @minLength above @maxLength, and a @pattern that doesn't compile as RE2 all fail generation, pointing at the @name line. RE2 is deliberate — the API server evaluates the pattern with Go's regexp, so your lookahead example would otherwise generate cleanly and then never be enforced.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

  1. Both survivors sit on the @minLength/@maxLength boundary. 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.
  2. A directive the parser doesn't recognise is still skipped under @name. ## @maxlength 32, ## @maxLength -1 and ## @maxLength 20.5 generate with exit 0, and the name gets no cap from that line. @param has always worked this way, so it isn't new here, but under @name the result is an uncapped name.
  3. The PR body still says TestNameSchemaAbsentWithoutDirective pins 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.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit bfb3a06 into main Sep 23, 2026
2 checks passed
Timofei Larkin (lllamnyp) added a commit to cozystack/cozystack that referenced this pull request Sep 24, 2026
…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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants