When a customer gets a capability request wrong, the error usually does not tell them how to fix it. A typo reads as if the platform is withholding a real capability. A lowercase name returns a regex. A single mistake can produce two errors where the second contradicts the first. And an unsupported seccomp profile surfaces as a decoder error that never mentions seccomp. In every one of these cases the customer is one small edit away from a working manifest, and the message does not say which edit.
This is the validation surface introduced in #310.
The model to follow
One message already does this right. Input capabilities: {add: [CAP_NET_ADMIN]}:
Invalid value: "CAP_NET_ADMIN": must omit the CAP_ prefix, for example "NET_ADMIN"
It names the mistake and states the fix. The cases below should read like this.
The cases
1. A typo is reported as a policy denial. Input capabilities: {add: [NOT_A_CAPABILITY]}:
Forbidden: capability NOT_A_CAPABILITY is not granted by the "general-purpose" runtime class, which grants AUDIT_CONTROL, AUDIT_READ, ... WAKE_ALARM
This implies the capability exists and is being withheld — a customer reasonably goes looking for a different runtime class or files a support request. It does not exist. The platform never distinguishes an unknown capability name from a real one, so there is no "unknown capability" message and no "did you mean".
2. A lowercase name leaks the raw validator. Input capabilities: {add: [net_admin]}:
Invalid value: "net_admin": spec.template.spec.runtime.sandbox.containers[0].securityContext.capabilities.add[0] in body should match '^[A-Z_]+$'
A regex is not product copy. The message never says capability names are uppercase, and the customer is one character-case away from correct.
3. One mistake produces two errors, and the second is often wrong or contradictory. Input capabilities: {add: [ALL]}:
* Forbidden: ALL may not be added; list each capability the container needs
* Forbidden: capability ALL is not granted by the "general-purpose" runtime class, which grants AUDIT_CONTROL, ... WAKE_ALARM
The second is noise — ALL is not a capability the class could ever grant. The platform rejects ALL and then goes on to check the same entry against the class anyway.
Worse on a class that supports no capability requests at all. Input capabilities: {add: [ALL]} with class: unikernel:
* Forbidden: ALL may not be added; list each capability the container needs
* Forbidden: container capability requests are not supported by the "unikernel" runtime class
The customer is told to list the capabilities they need, immediately before being told the class accepts none. Following the first instruction still fails.
4. An unsupported seccomp profile surfaces as a decoder error. Input seccompProfile: {type: Localhost, localhostProfile: foo.json}:
Error from server (BadRequest): ... strict decoding error: unknown field "spec.template.spec.runtime.sandbox.containers[0].securityContext.seccompProfile.localhostProfile"
localhostProfile is not part of the published API at all, so the request is rejected before any customer-facing validation runs and the message never says Localhost profiles are unsupported. For contrast, type: Localhost with the field omitted gives a good message: Unsupported value: "Localhost": supported values: "RuntimeDefault", "Unconfined".
5. The grantable-capability list is repeated inline in every rejection. The general-purpose class grants 41 capabilities, and the full list is inlined in each capability error — twice when two errors fire. A customer fixing one typo reads roughly 500 characters of allowlist, twice.
Reproduction
Against a control plane with the RuntimeClasses gate on and the general-purpose class published, kubectl apply --dry-run=server a sandbox Workload, varying only containers[0].securityContext.
apiVersion: compute.datumapis.com/v1alpha
kind: Workload
metadata:
name: cap-errors
spec:
template:
spec:
runtime:
class: general-purpose
sandbox:
containers:
- name: app
image: docker.io/library/nginx:latest
securityContext:
capabilities:
add: [NET_BIND_SERVICE]
placements:
- name: default
cityCodes: [DFW]
scaleSettings:
minReplicas: 1
Inputs tested:
securityContext |
Case |
capabilities: {add: [NOT_A_CAPABILITY]} |
1 |
capabilities: {add: [net_admin]} |
2 |
capabilities: {add: [ALL]} |
3, 5 |
capabilities: {add: [ALL]} with class: unikernel |
3 |
seccompProfile: {type: Localhost, localhostProfile: foo.json} |
4 |
seccompProfile: {type: Localhost} |
good message, contrast |
capabilities: {add: [CAP_NET_ADMIN]} |
the model to follow |
When a customer gets a capability request wrong, the error usually does not tell them how to fix it. A typo reads as if the platform is withholding a real capability. A lowercase name returns a regex. A single mistake can produce two errors where the second contradicts the first. And an unsupported seccomp profile surfaces as a decoder error that never mentions seccomp. In every one of these cases the customer is one small edit away from a working manifest, and the message does not say which edit.
This is the validation surface introduced in #310.
The model to follow
One message already does this right. Input
capabilities: {add: [CAP_NET_ADMIN]}:It names the mistake and states the fix. The cases below should read like this.
The cases
1. A typo is reported as a policy denial. Input
capabilities: {add: [NOT_A_CAPABILITY]}:This implies the capability exists and is being withheld — a customer reasonably goes looking for a different runtime class or files a support request. It does not exist. The platform never distinguishes an unknown capability name from a real one, so there is no "unknown capability" message and no "did you mean".
2. A lowercase name leaks the raw validator. Input
capabilities: {add: [net_admin]}:A regex is not product copy. The message never says capability names are uppercase, and the customer is one character-case away from correct.
3. One mistake produces two errors, and the second is often wrong or contradictory. Input
capabilities: {add: [ALL]}:The second is noise —
ALLis not a capability the class could ever grant. The platform rejectsALLand then goes on to check the same entry against the class anyway.Worse on a class that supports no capability requests at all. Input
capabilities: {add: [ALL]}withclass: unikernel:The customer is told to list the capabilities they need, immediately before being told the class accepts none. Following the first instruction still fails.
4. An unsupported seccomp profile surfaces as a decoder error. Input
seccompProfile: {type: Localhost, localhostProfile: foo.json}:localhostProfileis not part of the published API at all, so the request is rejected before any customer-facing validation runs and the message never says Localhost profiles are unsupported. For contrast,type: Localhostwith the field omitted gives a good message:Unsupported value: "Localhost": supported values: "RuntimeDefault", "Unconfined".5. The grantable-capability list is repeated inline in every rejection. The general-purpose class grants 41 capabilities, and the full list is inlined in each capability error — twice when two errors fire. A customer fixing one typo reads roughly 500 characters of allowlist, twice.
Reproduction
Against a control plane with the RuntimeClasses gate on and the
general-purposeclass published,kubectl apply --dry-run=servera sandbox Workload, varying onlycontainers[0].securityContext.Inputs tested:
securityContextcapabilities: {add: [NOT_A_CAPABILITY]}capabilities: {add: [net_admin]}capabilities: {add: [ALL]}capabilities: {add: [ALL]}withclass: unikernelseccompProfile: {type: Localhost, localhostProfile: foo.json}seccompProfile: {type: Localhost}capabilities: {add: [CAP_NET_ADMIN]}