Skip to content

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"
}
Validated against - · pkg/api/validation_response.go

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 &lt;computed&gt; 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 &lt;computed&gt; 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 &lt;computed&gt; 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 &lt;computed&gt; 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 &lt;computed&gt; 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 &lt;computed&gt; 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 &lt;computed&gt; {{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 &lt;computed&gt; 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 &lt;computed&gt; 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 &lt;computed&gt; 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 &lt;computed&gt; 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 &lt;computed&gt;, 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 &lt;computed&gt;, 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 &lt;computed&gt; 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 &lt;computed&gt; 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.&lt;computed&gt; 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.&lt;computed&gt; 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