Skip to content

Commit fd337ed

Browse files
authored
feat: elastic config command group + credential-safe project create (#216)
* feat: add `elastic config` command group for authoring contexts (#75) New flag-driven command tree for creating and maintaining the config file without hand-editing YAML: elastic config context list/add/edit/remove elastic config current-context get/set `edit` supports both `--set key=value` patch mode and an `$EDITOR` round-trip. Secrets are written to the OS keychain when available (macOS `security`, Linux `secret-tool`/`pass`, Windows Credential Manager) via a new `SecretStore` abstraction that shells out to the same tools the existing read-side resolvers use. The YAML holds a `$(keychain:...)` resolver expression rather than the raw secret, mirroring the read side. When no keychain is available (or `--inline-secrets` is passed), values are written inline and the file is chmod'd to 0600. `loadConfig` now emits an stderr warning when a loaded config has inline secrets at looser-than-0600 permissions, pointing at chmod 0600 or the new `config context edit` migration path. Skips the config-loading preAction hook for `config` descendants so the commands tolerate the file being absent (they create it). * feat: add --save-as / --credentials-file to serverless project create (#154) `serverless {es,observability,security} projects create` and `reset-credentials` gain three new flags to keep admin credentials off stdout -- critical for agent/LLM workflows where captured output persists into model context and transcripts. --save-as <name> store returned creds in the OS keychain, upsert a context bound to the new project's endpoints, and redact stdout. Next command runs as `elastic --use-context <name> ...` with zero manual wiring. --credentials-file <path> write a standalone 0600 YAML config fragment at <path> instead of mutating the main config. --force overwrite an existing context / file. `reset-credentials --save-as <ctx>` updates the named context's auth in place -- URLs preserved, only passwords rotate. Without either flag, behaviour is unchanged. Reuses the `SecretStore` abstraction from the `elastic config` commands so a single keychain namespace covers both authored and auto-generated creds.
1 parent f10c642 commit fd337ed

12 files changed

Lines changed: 2754 additions & 5 deletions

File tree

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,62 @@ Override `current_context` for a single command with `--use-context <name>`.
6767
Each context can have any combination of service blocks (`elasticsearch`, `kibana`, `cloud`).
6868
Authentication can also use `username` + `password` instead of `api_key`.
6969

70+
### Authoring the config from the CLI
71+
72+
Instead of hand-editing YAML, the `elastic config` command group creates and
73+
maintains contexts and stores secrets in the OS keychain when available
74+
(macOS Keychain, Linux libsecret, `pass`, Windows Credential Manager). The YAML
75+
then holds a `$(keychain:...)` / `$(secret_service:...)` / etc. resolver
76+
expression rather than the raw secret.
77+
78+
```bash
79+
# Add a new context (API key goes to the keychain; YAML gets $(keychain:...))
80+
elastic config context add local \
81+
--es-url http://localhost:9200 \
82+
--es-api-key your-api-key
83+
84+
# List contexts (the current one is marked)
85+
elastic config context list
86+
87+
# Switch the active context
88+
elastic config current-context set staging
89+
90+
# Flag-patch an existing context
91+
elastic config context edit local --es-url http://localhost:9201
92+
93+
# Open the context as YAML in $EDITOR for free-form edits
94+
elastic config context edit local
95+
96+
# Remove a context (keychain entries are cleaned up)
97+
elastic config context remove old-lab
98+
```
99+
100+
If no OS keychain is available (or you pass `--inline-secrets`), the secret is
101+
written inline and the file is `chmod 0600`. A warning is emitted whenever a
102+
loaded config has inline secrets at looser-than-0600 permissions.
103+
104+
### Credential-safe project creation
105+
106+
For agent/LLM workflows, `serverless projects create` and `reset-credentials`
107+
accept `--save-as <context>` to avoid leaking admin credentials through stdout:
108+
109+
```bash
110+
elastic cloud serverless es projects create --wait --save-as scratch \
111+
--name scratch-es --region-id aws-us-east-1
112+
113+
# stdout has endpoints + a `savedAs: scratch` marker, password is redacted.
114+
# The keychain now holds scratch:elasticsearch.auth.password etc.
115+
elastic --use-context scratch stack es indices list
116+
117+
# Rotate creds; URL stays, only the password moves.
118+
elastic cloud serverless es projects reset-credentials --id <id> \
119+
--save-as scratch --force
120+
```
121+
122+
`--credentials-file <path>` is an alternative that writes a standalone YAML
123+
config fragment (0600) at `<path>` instead of mutating the main config. Either
124+
flag makes stdout safe to capture into an LLM transcript.
125+
70126
### External credentials
71127

72128
Instead of storing secrets in plaintext, any string value in the config file can

src/cli.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
3737
if (actionCommand.name() === 'version') return
3838
// docs commands use public elastic.co APIs — no config required
3939
if (actionCommand.parent?.name() === 'docs') return
40+
// `config` commands author the config file itself — loading it would be
41+
// circular (and must tolerate the absence of a file)
42+
for (let c = actionCommand.parent; c != null; c = c.parent) {
43+
if (c.name() === 'config') return
44+
}
4045
const { configFile: configPath, useContext: contextName } = thisCommand.opts()
4146

4247
if (configPath == null && contextName == null && earlyConfig?.ok === true) {
@@ -134,10 +139,17 @@ if (firstArg === 'docs') {
134139
program.addCommand(defineGroup({ name: 'docs', description: 'Search, read, and ask questions about Elastic documentation' }))
135140
}
136141

142+
if (firstArg === 'config') {
143+
const { registerConfigCommands } = await import('./config/commands.ts')
144+
program.addCommand(registerConfigCommands())
145+
} else {
146+
program.addCommand(defineGroup({ name: 'config', description: 'Author and maintain the elastic config file' }))
147+
}
137148
// Load config early so --help can hide blocked commands. Skip for commands
138-
// that don't need config (e.g. `version`) to avoid unnecessary file I/O.
149+
// that don't need config (e.g. `version`, or `config` which authors the file)
150+
// to avoid unnecessary file I/O and a confusing "no config found" path.
139151
// The result is cached in earlyConfig so the preAction hook can reuse it.
140-
if (firstArg !== 'version') {
152+
if (firstArg !== 'version' && firstArg !== 'config') {
141153
earlyConfig = await loadConfig({})
142154
if (earlyConfig.ok) {
143155
setResolvedConfig(earlyConfig.value)

0 commit comments

Comments
 (0)