Rejections and alterations
Every rejection the control plane returns carries a machine-readable code, the field path that failed, the value it received and the accepted form. Branch on the code; never match on the message text, which is written for a person and changes when the wording improves.
{ "code": "healthcheck_timeout_below_floor", "field": "healthCheck.timeout", "received": "4s", "expected": ">= 10s, or omit the field", "why": "a timed-out exec is SIGKILLed and reparented to a PID 1 that never reaps it", "doc": "https://odysseus.delta-telematics.ca/docs/reference/manifest/deployment#healthcheck"}- · pkg/api/validation_response.goRejections
Section titled “Rejections”A rejection means nothing was stored. Fix the named field and send it again.
Generated from Rule. Hand edits to this table are overwritten on the next build — change the Go doc comment, or the generator.
| Code | Field | Accepted form | Why |
|---|---|---|---|
anchored_secret_dropped |
secrets |
keep the entry {name: %q, vault_path: %q, anchor: %q}, or remove volume %q in the same update to release the anchor | the update drops a secret anchored to a volume it still mounts; the container would be recreated without the credential its own data directory was initialized with |
anchored_secret_repointed |
<computed> |
%q — the path the material at volume %q was initialized from. To rotate, use POST /api/v1/deployments/{name}/secrets/rotate; to release the anchor, remove volume %q in the same update | changing the path drifts the spec hash and recreates the container, which then authenticates against an unchanged data directory with material it has never seen |
deployment_name_invalid |
name |
lowercase letters, digits and hyphens only, starting with a letter and ending with a letter or digit — e.g. %q | the name is used verbatim as a container name, a Traefik router id, a DNS alias on the tenant network and a Consul KV path segment, and every one of those refuses uppercase, underscores, dots and spaces |
deployment_name_too_long |
name |
at most %d characters, e.g. %q | the name becomes a DNS label for the container, and a label longer than 63 characters cannot be resolved by sibling containers |
deployment_not_found |
name |
the name of a deployment that already exists — list them with GET /api/v1/deployments | a PUT updates an existing record and cannot create one; POST /api/v1/deployments creates |
healthcheck_command_missing |
healthCheck.command |
a command the image can run, e.g. [“wget”,“-qO-”,“http://127.0.0.1:8080/health”] | an exec declaration with no command produces no container healthcheck at all |
healthcheck_retries_below_floor |
healthCheck.retries |
set >= %d, or omit retries to take Docker’s default | one unhealthy container degrades the whole deployment, and retries is the only damper |
healthcheck_timeout_below_floor |
healthCheck.timeout |
set >= %s, or omit timeout to take Docker’s 30s default | a timed-out exec is SIGKILLed and reparented to a PID 1 that never reaps it |
healthcheck_type_inert |
healthCheck.type |
set type: “exec” with a command the image can run — shell form [“wget -qO- http://127.0.0.1:%d%s”] or argv [“wget”,“-qO-”,“http://127.0.0.1:%d%s”] | an inert declaration silently SATISFIES the dependsOn: healthy and rolling-update readiness gates it was meant to guard |
healthcheck_type_unsupported |
healthCheck.type |
the only accepted value is “exec” | no other type produces a container healthcheck |
image_required |
image |
a pinned reference, e.g. “nginx:1.27” — never :latest | there is nothing to run without one |
name_required |
name |
a lowercase DNS-style name, e.g. “web-api” | the name is the deployment’s identity and its container name prefix |
network_node_candidates_unresolvable |
nodeId |
enroll a node for this tenant, then create the network | no nodes are registered for this tenant, so there is nowhere to create the network — either none is enrolled or the node store is unreachable, and the two are indistinguishable from here |
network_node_not_a_candidate |
nodeId |
one of: %s | a Docker network exists only on the host it was created on, and this tenant cannot place workloads on the node you named — the network would be unreachable from every container you could run |
network_node_required |
nodeId |
one of: %s | a Docker network is created on ONE host, so with more than one node available the platform cannot know which you meant — picking for you is what made networks appear on a node nobody expected |
network_option_unsupported_on_agent |
<computed> |
omit it — or create the network directly on the node until agent support lands | the node agent’s network wire format carries only name, driver, labels and internal, so this value cannot reach the host and the network would be created without it |
node_candidates_unresolvable |
nodeId |
enroll a node for this tenant, or omit nodeId to let the scheduler place this deployment | no node is registered under this tenant, so no target can be honoured — either none has been enrolled or the node store could not be read, and the two are indistinguishable from here |
node_not_a_placement_candidate |
nodeId |
one of: %s (or omit nodeId to let the scheduler decide) | placement only considers this deployment’s own tenant’s enrolled nodes, so a node outside that set would be accepted here and then silently overridden at dispatch |
replicas_negative |
replicas |
0 or greater — use 0 to stop the deployment without deleting it | a negative replica count has no meaning; scaling to 0 is how you stop something |
rotation_transition_invalid |
phase |
one of %v from phase %q | the overlap window guarantees that at no point are zero credentials valid, and that guarantee only holds if phases advance in order |
secret_anchor_invalid |
<computed> |
one of: “process”, “external”, or “volume(<volume-id>)” — e.g. “volume(codecheck-db-data)” | the anchor tells the platform whether this credential may ever be re-minted; an unrecognised value would be silently ignored, which is the failure this field exists to prevent |
secret_anchor_volume_ephemeral |
<computed> |
a volume with type “volume” or “bind”; %q is type %q. Use “process” if the material is genuinely recreated with the container | tmpfs is ephemeral by definition — it is discarded with the container, so it anchors nothing and an anchor on it would grant a false guarantee |
secret_anchor_volume_not_declared |
<computed> |
volume(<id>) naming a volume this spec declares; declared here: %v | an anchor pointing at a volume the workload does not mount protects nothing — the material would be re-minted on the next recreate and the container would authenticate with a credential its datastore does not have |
secret_compose_no_reference |
<computed> |
a template containing at least one {{secret.<resource>.<key>}} | a template with no secret reference is a constant and belongs in environment:, where it is visible and diffable |
secret_compose_resource_undeclared |
<computed> |
a resource this deployment declares: %v | a template may only read a resource the workload already references, so a typo is caught here instead of failing closed at dispatch with no container |
secret_compose_template_empty |
<computed> |
postgresql://user:{{secret.<resource>.<key>}}@host:5432/db | an empty template renders to an empty env var, which the consumer reads as unset |
secret_compose_template_malformed |
<computed> |
{{secret.<resource>.<key>}} — lowercase DNS resource, env-var-shaped key | an unrecognised placeholder would be delivered to the container as literal braces, and the workload would connect with the wrong credential |
secret_generate_axis_conflict |
generate |
either bytes/encoding (random bytes rendered as text) OR length/charset (characters drawn from an alphabet) — not both | the two describe different things and the platform would have to ignore one of them, which it will not do silently |
secret_generate_below_entropy_floor |
generate.length |
length >= %d with charset %q (or omit length for the default %d) | length %d over a %d-character alphabet is %.0f bits; the platform floor is %d bits, below which offline brute force is a budget question rather than a physics one |
secret_generate_bytes_out_of_range |
generate.bytes |
%d..%d (omit for the default %d) | %d bytes is %d bits; the platform floor is %d bits, and above %d bytes the encoded value exceeds the %d-character ceiling Vault is sized for |
secret_generate_charset_unknown |
generate.charset |
one of %q, %q, %q | an unknown alphabet cannot be entropy-checked, so the platform cannot promise the value meets its floor |
secret_generate_encoding_unknown |
generate.encoding |
one of %q, %q, %q | the platform cannot render bytes in an encoding it does not implement, and guessing one would hand your app material it cannot decode |
secret_generate_key_duplicate |
<computed> |
each key listed once | a duplicate would be minted twice and the second write would silently win |
secret_generate_keys_required |
generate.keys |
at least one key, e.g. [“password”] | a generate block with no keys mints nothing and silently produces a ref to an empty document, which fails closed at dispatch |
secret_generate_length_out_of_range |
generate.length |
1..%d | a negative length is meaningless and an oversized one would be refused by Vault after a partial write |
secret_generate_would_persist |
secrets[%d].generate |
nil — call StripSecretAuthoring before persisting | a stored generate block would be re-evaluated on every rollback and could mint material a datastore does not know |
secret_key_invalid |
<computed> |
an env-var name matching [A-Za-z_][A-Za-z0-9_]*, e.g. “S3_ACCESS_KEY” | the document key becomes the container env var verbatim, and a leading digit is not assignable in sh |
secret_key_provided_and_generated |
<computed> |
declare the key in values OR in generate.keys, not both | the platform would have to choose between your value and a minted one, and either choice is a silent alteration |
secret_material_would_persist |
secrets[%d].values |
an empty values map — call StripSecretAuthoring before persisting | Consul KV holds refs only; a stored value would enter the spec hash, every revision, and every rollback |
secret_mount_path_unsupported |
secrets[%d].mount_path |
remove mount_path; render the file at container start from the env value with an entrypoint | delivery is env-only (WP2 ruling D1) — a file mount would put the value on a disk the control plane does not control |
secret_name_invalid |
secrets[%d].name |
an env-var prefix: [A-Za-z0-9_]+, e.g. “DB_CREDS” | the name is prefixed onto every key resolved from this document (NAME_key), so it must itself be legal environment-variable material |
secret_path_name |
<computed> |
a DNS label after the namespace: lowercase letters, digits and inner hyphens, 1-63 characters, e.g. “resources/codecheck-db” | the name is compared against workload names to detect a credential referenced under two namespaces at once; a path that names no document cannot take part in that comparison and resolves to nothing at dispatch |
secret_path_namespace |
<computed>, namespace |
<computed> | only these namespaces are granted by the control plane’s Vault policy and by tenant application policies; anything else resolves as a 403 at dispatch and the workload goes Degraded with no container |
secret_path_namespace_split |
secrets[%d].vault_path |
one namespace per credential — use %q for every reference to %q | the same credential under two namespaces is two Vault documents that must be kept identical by hand; that duplication is what the resources/ namespace exists to remove |
secret_path_shape |
<computed>, resource |
<computed> | the first segment is the namespace that every Vault policy grant — the control plane’s and each tenant application’s — is written against; a path outside a granted namespace resolves as a 403, and a 403 is indistinguishable from an empty secret to a caller that does not classify it |
secret_rotation_unsupported |
secrets[%d].rotation |
“restart”, or omit the field — it defaults to restart | the container picks up new material only by being recreated; signal/webhook/file_watch were never implemented and would silently do nothing |
secret_value_empty |
<computed> |
a non-empty string | an empty value resolves to an empty env var, which most images treat as unset and then fail obscurely at runtime |
secret_value_too_long |
<computed> |
at most %d bytes | Vault KV v2 caps a document and an oversized value would be rejected at write time, after other keys were already stored |
secret_vault_path_not_relative |
secrets[%d].vault_path |
a relative path with no leading “/” and no “..” elements, e.g. “deployments/codecheck-db” | the path is appended to the tenant’s own Vault namespace; a leading slash or “..” would escape it and reach another tenant’s material |
secret_vault_path_required |
secrets[%d].vault_path |
a relative Vault KV path inside the tenant’s mount, e.g. “deployments/codecheck-db” | without it the control plane has nothing to resolve at dispatch and the deployment fails closed with no container |
secret_write_empty |
values |
either {“values”:{“KEY”:“…”}} or {“generate”:{“keys”:[“KEY”]}} | a write with neither supplies nothing and would create an empty document that fails closed at dispatch |
secrets_version_negative |
secretsVersion |
0 on a new spec; thereafter only the rotation API bumps it | secretsVersion is the rotation counter the spec hash covers — a negative value cannot have been produced by a rotation, so the spec did not come from this platform |
Alterations — changes the server makes that you did not send
Section titled “Alterations — changes the server makes that you did not send”A silent alteration is a silent rejection wearing a 200. Every entry below is announced in the response body and in the log, so a change to your workload is never something you have to discover.
Generated from Rule. Hand edits to this table are overwritten on the next build — change the Go doc comment, or the generator.
| Code | Field | What changes |
|---|---|---|
command_repaired |
command |
the shell command was split on whitespace and has been rejoined into a single argument. Send the script as ONE array element, e.g. [“sh”,“-c”,“a && b”]. |
entrypoint_repaired |
entrypoint |
the entrypoint was split on whitespace and has been rejoined into a single argument. Send the script as ONE array element. |
healthcheck_timeout_clamped |
healthCheck.timeout |
the stored probe timeout was below the <computed> floor and was raised to it. You did not send this field; the change was applied to the stored value. Send healthCheck.timeout explicitly to control it. |
name_from_path |
name |
the name in the body was ignored: a PUT identifies the deployment by its URL and cannot rename it. Delete and recreate to change a name. |
node_auto_assigned |
nodeId |
no nodeId was sent and this tenant has exactly one enrolled node, so the deployment was pinned to it. Send nodeId explicitly to control placement. |
plaintext_credential_in_environment |
environment.%s |
%s %q carries what looks like a credential VALUE in environment.%s. Consul stores it in clear text, it appears in every spec export and backup, and anyone who can read the deployment can read the secret. Move it to Vault and reference it: drop the environment entry and add “secrets”: [{“name”: “%s”, “vault_path”: “deployments/%s”}] — the control plane resolves it at dispatch, so the workload still sees %s in its environment and nothing else changes. The value is not repeated here on purpose. See docs/deployment-yaml-reference.md#secrets. This is a warning, not a rejection: the request was accepted as sent. |
replicas_defaulted |
replicas |
replicas was absent or not positive, so it was set to 1. Send replicas explicitly to control it. |
secret_refs_preserved |
secrets |
secrets[] is read-modify-write, unlike every other array on this endpoint: %s were kept because your body did not mention them. Omitting a ref NEVER removes it. To remove one, send “secrets”: [] to clear the array, then PUT the refs you want to keep. |
secret_rotation_shape_changed |
keys.<computed> |
you asked for a shape that differs from the material currently stored for this key. The new value will not have the same form as the old one; a consumer that decodes it may reject it. Omit length/charset and bytes/encoding to keep the current shape. |
secret_rotation_shape_unknown |
keys.<computed> |
the current material for this key does not match any shape the platform mints, so its form could not be preserved. The replacement uses the platform default. State bytes/encoding or length/charset explicitly to control it. |
secrets_version_bumped |
secretsVersion |
new secret material was written, so secretsVersion was incremented to drift the spec hash and recreate the container — the value would otherwise not reach a running container |