feat(secret): --from-file for secret create and secret set - #14
Merged
Conversation
Importing a file into a Secret was only possible through compose
(`secrets: {name: {file: ./path}}`). From the CLI the closest thing was
--from-literal KEY="$(cat file)", which is not the same thing: command
substitution strips the trailing newline, so a PEM key or token arrives subtly
wrong and the parser that rejects it points nowhere near the cause. Arguments
are also capped at ARG_MAX and cannot carry NUL, so binary was out entirely.
--from-file ./api.key key is the file name
--from-file apikey=./api.key key is given explicitly
--from-file ./conf.d every file in the directory becomes a key
Files are read with os.ReadFile, so the bytes land verbatim. Both flags are
repeatable and can be mixed with --from-literal; `secret set` takes the same
inputs and still merges.
Keys are validated against what Kubernetes accepts before anything is written,
so a file named "not a key.txt" is reported with the KEY=path form to fix it
rather than being rejected later by the API server. A key set twice — by two
files, or by a file and a literal — is an error naming both, because silently
keeping one of two values is worse than refusing. Nested directories are
skipped rather than flattened, so keys stay predictable.
The HTTP counterpart is a dataBase64 map on SecretRequest, honoured by both
POST and PATCH. Not a file path: a path in a request body would name a file on
the server, not on the caller's machine, and invite reading arbitrary server
files.
Verified against a live cluster — a PEM with a trailing newline and a file
containing NUL and 0xFF both round-trip byte-identical, through the CLI and
through dataBase64.
COMPOSE.md showed `secrets: {x: {file: ./path}}` in an example and never said
what it produces, so the compose-side equivalent of --from-file was effectively
undiscoverable.
The part worth spelling out is that creating a Secret and consuming it are
separate steps. The top-level entry creates it whether or not any service
references it, so pairing it with x-orcinus-env-from-secret turns a file into
environment variables with no volume anywhere — and listing it under a service's
`secrets:` mounts it as a file. Both, either, or neither.
Also records the two edges found while checking: the data key is always the
secret's name, so the variable that reaches the container is named after the
secret rather than the file (use the CLI's KEY=path form to choose); and
`environment:` as a secret source parses and is then silently skipped, unlike
`content:`, which the schema rejects outright.
Two tests pin the file-to-env path, including that it produces no volumes, since
nothing else in the suite covered it.
Auditing every claim the docs make about secrets turned up four gaps, all of
the same kind: the tables describe one route and omit the others, so a reader
scanning them draws the wrong conclusion.
- USAGE.md's compose mapping table had no `secrets:` or `configs:` row at all.
Both are supported; someone scanning that table would conclude otherwise.
- COMPOSE.md's service-key table said `secrets` is "mounted at the target",
which reads as the only option. Creating a Secret and consuming it are
separate steps, and the env route is not mentioned.
- USAGE.md described x-orcinus-env-from-secret as loading an "existing" Secret
"created with orcinus secret create". True but narrow: the Secret can equally
come from a `secrets: {name: {file: …}}` entry in the same compose file.
- ARCHITECTURE.md's mapping table also had no configs/secrets row.
The skills card gained the same correction, since it is what an agent reads
before answering this question.
Every YAML example in the three edited COMPOSE.md sections was run through
--dry-run: the "env var and mounted file together" example produces both from
one Secret, and `target: apikey` does land at /run/secrets/apikey as its
comment claims.
…sts did not Two gaps between what I had verified by hand and what the suite would catch on its own. A directory whose contents include a name Kubernetes cannot use as a key goes through addDir → addFile, a different path from passing that file directly, and only the direct one was covered. The live e2e now exercises --from-file, asserting the stored bytes match the file exactly — trailing newline included, which is the whole reason the flag exists — and that it leaves the Secret's other keys alone. It stays behind ORCINUS_E2E_LIVE like the rest. Also checked by hand, all behaving correctly and left as-is: a dotfile is a valid key, the same directory passed twice collides rather than silently winning, symlinks are followed, and `set --from-file` on a TLS Secret keeps its kubernetes.io/tls type.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Importing a file into a Secret only worked through compose (
secrets: {name: {file: ./path}}). From the CLI:The usual workaround is not equivalent:
$(cat f)strips the trailing newline, so a PEM key or token arrives subtly wrong and whatever rejects it points nowhere near the cause. Arguments are also capped atARG_MAXand cannot carry NUL, so binary was impossible.Change
Read with
os.ReadFile, so bytes land verbatim. Repeatable, mixable with--from-literal, andsecret settakes the same inputs and still merges.Decisions worth reviewing:
not a key.txtis reported with the fix (pass KEY=./not a key.txt to name it) instead of being rejected later by the API server.API counterpart
SecretRequestgainsdataBase64, honoured by bothPOSTandPATCH:curl -d "{\"name\":\"apikey\",\"dataBase64\":{\"apikey\":\"$(base64 < api.key | tr -d '\n')\"}}" .../api/v1/secretsDeliberately not a file path: a path in a request body would name a file on the server, not the caller's machine, and invite reading arbitrary server files. A key may appear in
dataordataBase64, not both.Verification
Live against the 3-node cluster, all three
--from-fileforms plus a literal in one command → 5 keys:Byte-fidelity, comparing the file's base64 to what the cluster stored:
…LS0tLS0K…LS0tLS0KA \x00 B \xFF C)QQBC/0M=QQBC/0M=secret set --from-fileupdated one key and left the other four alone. Every error case reported clearly and created nothing:dataBase64verified live too: binary round-tripped identically throughPOST, andPATCHmerged without disturbing the existing key.Offline: 12 new tests (base-name key, explicit key, directory with a nested dir, several files in one command, binary, all six error cases, base64 decode and validation), suite green. Test secrets cleaned up; the cluster's own are untouched.
Note
I caught a bug in my own first draft:
addFilesreturned after the first file instead of continuing the loop, so only one--from-filewould have landed.TestSecretDataCombinedcovers exactly that now.