secrets: add doctl commands for Secrets Manager#1864
secrets: add doctl commands for Secrets Manager#1864MaxLivesInTheMatrix wants to merge 5 commits into
Conversation
Add create, list, get, update, delete, restore, and list-versions commands with interactive prompts for region, name, and version where appropriate.
Resolve conflicts with main by keeping secrets support alongside the new VectorDBs commands and godo v1.197.0. Co-authored-by: Cursor <cursoragent@cursor.com>
| return c.Display(&displayers.SecretWriteResult{Result: *result}) | ||
| } | ||
|
|
||
| // RunCmdSecretsDelete schedules a secret container for deletion. |
There was a problem hiding this comment.
delete has no confirmation or --force
Every other destructive command in doctl gates deletion behind a confirmation prompt and a --force flag (see RunVolumeDelete in volumes.go). This deletes immediately with no guardrail. Even though it's a soft delete with restore, we should stay consistent with the rest of the CLI and protect against fat-fingering the wrong secret/region.
There was a problem hiding this comment.
Done. secrets delete now uses AskForConfirmDelete like the other destructive commands, with --force / -f to skip the prompt.
| return c.Display(&displayers.SecretVersions{Versions: versions}) | ||
| } | ||
|
|
||
| // RunCmdSecretsUpdate updates a secret container. |
There was a problem hiding this comment.
full-replace can silently drop keys
update replaces the entire container, so any key the user forgets to re-supply is silently deleted.
Would it be possible to pick one of 2 -
Preferred: split intent — add secrets set key=val (merge: fetch → apply → write) and secrets unset key, and reserve full-replace for an explicit update --replace.
Minimum: before writing, fetch current keys and require confirmation (gated by --force) when keys would be removed, e.g. This will remove keys: api_key, token. Continue?.
There was a problem hiding this comment.
This was a concern I initially had too, but we intentionally made this very CRUD heavy. This also simplifies how we can delete keys from the secret container. As deleting a key is just sending the entire payload, but with whatever key missing to delete it. I tried to make it explicit as possible to notify users that all of the values get replaced when the users update the secret. It’s a common (if blunt) design for secret stores:
- Simpler storage — one encrypted blob per version, not per-key CRUD
- Atomic updates — the new version is a single snapshot
- Easier versioning — each version is a complete state of the secret
- Optimistic locking — version guards whole-container writes
- Tradeoff: callers must send the full desired state, not a delta.
AWS Secrets Manager is quite similar in this regard too. I will bring this up in my team's convos to see what they think.
| If no `+"`"+`--value`+"`"+` flags are provided, key-value pairs are read interactively with --interactive. You are prompted for each key, then each value is masked.`, Writer, | ||
| aliasOpt("c"), displayerType(&displayers.SecretWriteResult{})) | ||
| AddStringFlag(cmdCreate, doctl.ArgRegionSlug, "", "", secretRegionFlagDesc) | ||
| AddStringSliceFlag(cmdCreate, doctl.ArgSecretValue, "", nil, |
There was a problem hiding this comment.
plaintext --value leaks secret material
The only non-interactive way to supply values is --value key=plaintext, which lands secrets in shell history, the process table (ps aux), and CI logs
can we read it from a source as env var , matching vault/kubectl: -
--value password=@./pw.txt # from file
--value password=- # from stdin
--from-env-file .env # bulk
File reading already exists in the codebase (certificates.go).
There was a problem hiding this comment.
Done. --value now supports key=@ path and key=- (stdin). Added --from-env-file on create/update; --value flags override env-file keys. Reused the existing file-read pattern from certificates.go.
| } | ||
|
|
||
| if len(list.UnavailableRegions) > 0 { | ||
| notice("Some regions were unavailable: %s", strings.Join(list.UnavailableRegions, ", ")) |
There was a problem hiding this comment.
notice() writes to color.Output (stdout), so secrets list ("Some regions were unavailable…") and secrets update ("Using current version…") emit a Notice: line onto stdout ahead of the JSON/table. That breaks doctl secrets list -o json for scripting. These informational messages should go to os.Stderr (consistent with how the interactive prompts already write).
There was a problem hiding this comment.
Done. Informational messages for unavailable regions now go to stderr via secretNotice() instead of notice().
|
|
||
| secret, err := c.Secrets().Get(name, region) | ||
| if err == nil && secret.Version > 0 { | ||
| notice("Using current version %d of %q in %s", secret.Version, name, region) |
There was a problem hiding this comment.
notice() writes to color.Output (stdout), so secrets list ("Some regions were unavailable…") and secrets update ("Using current version…") emit a Notice: line onto stdout ahead of the JSON/table. That breaks doctl secrets list -o json for scripting. These informational messages should go to os.Stderr (consistent with how the interactive prompts already write).
There was a problem hiding this comment.
Done. The “Using current version…” message also goes to stderr via secretNotice().
| ) | ||
|
|
||
| // secretRegionReader reads interactive region input. It can be replaced in tests. | ||
| var secretRegionReader = bufio.NewReader(os.Stdin) |
There was a problem hiding this comment.
This file mixes two input styles: raw bufio readers on stdin for name/region/version, and charm/input for key/value (plus three separate stdin readers). That's fragile — a bufio reader can read past its own line and swallow input meant for the next prompt, and mixing it with charm (which drives the terminal itself) makes dropped/misplaced input easy. Let's standardize on charm everywhere, like the rest of doctl — input for name/version, list for region. It also lets the global test-only input vars go away.
There was a problem hiding this comment.
Done. Removed the bufio stdin readers. Name and version use charm/input; region uses charm/list. Dropped the test-only reader vars
| return c.Display(&displayers.SecretWriteResult{Result: *result}) | ||
| } | ||
|
|
||
| // RunCmdSecretsGet retrieves a secret container. |
There was a problem hiding this comment.
get prints all decrypted values to stdout by default, and the only way to read one value is to print them all and grep. Consider masking by default with --show to reveal, and add secrets get --key password --raw for safe piping (no table chrome / trailing-newline surprises).
There was a problem hiding this comment.
Done. Values are masked by default (********). Added --show to reveal, and --key + --raw for safe scripting (raw prints only the value, no table formatting).
Adds in confirm to deletes --value now supports key=@path and key=- (stdin). Added --from-env-file on create/update; --value flags override env-file keys. Removed the bufio stdin readers. Name and version use charm/input; region uses charm/list. Dropped the test-only reader vars. Values are masked by default (********). Added --show to reveal, and --key + --raw for safe scripting (raw prints only the value, no table formatting).
Add create, list, get, update, delete, restore, and list-versions commands with interactive prompts for region, name, and version where appropriate. I find that a lot of CLI tools can be clunky to use. This was written with the goal in mind of DO Simple, where the CLI will even guide you to fix your mistakes instead of erroring out immediately.

All testing was done locally.
Starting with doctl secrets create:
The user can either go through the tags one by one which is automated by the CLI or they can pass in all arguments at once (like if they had a script doing it instead of manually)
One liner:
doctl secrets create test --region nyc3 --value hello=world --value helloagain=worldManual workflow:
Users can list all of their secret containers by:
doctl secrets listUsers can retrieve their encrypted secrets from their containers by:

doctl secrets get hello --region nyc3or the workflow:
Users can delete and restore secrets using simple:
doctl secrets delete <name> --region <region>doctl secrets restore <name> --region <region>Or the exact same workflows that are shown above will prompt the user to take the correct actions
Updating a secret requires the secret name, region, and version.

Once again users can one shot this or follow the workflow:
It's important to keep in mind that updating a secret container replaces all of the information inside that container with what is being added in the command. this is shown to the user within the workflow.
Also the most basic command:

doctl secrets --help