diff --git a/NOTICE.txt b/NOTICE.txt index 7519db8c..adf8baa9 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1133,7 +1133,7 @@ THIS SOFTWARE. ------------------------------------------------------------------------ -yaml@2.8.4 +yaml@2.9.0 License: ISC Repository: https://github.com/eemeli/yaml Publisher: Eemeli Aro diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index 6a36027c..2bbf4be8 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -1,4 +1,20 @@ -# Configuration +--- +description: Configure the Elastic CLI by creating a config file with connection contexts for Elasticsearch, Kibana, and Elastic Cloud. +applies_to: + stack: preview + serverless: preview +type: how-to +--- + +# Configure the Elastic CLI + +This guide covers the configuration file format, managing connection contexts with `elastic config`, and using external credential resolvers to keep secrets out of your config file. + +## Before you begin + +[Install the Elastic CLI](./installation.md) before continuing. + +## Set up the config file The CLI looks for a config file in your home directory. The following file names are checked in order: @@ -35,7 +51,7 @@ contexts: Multiple contexts are supported. Override `current_context` for a single command with `--use-context `. -Each context can have any combination of service blocks (`elasticsearch`, `kibana`, `cloud`). Authentication supports `api_key` or `username` + `password`. +Each context can have any combination of service blocks (`elasticsearch`, `kibana`, and `cloud`). Authentication supports `api_key` or `username` + `password`. ## Authoring the config from the CLI @@ -63,7 +79,17 @@ elastic config context edit local elastic config context remove old-lab ``` -If no OS keychain is available or you pass `--inline-secrets`, the secret is written inline and the file is `chmod 0600`. A warning is emitted when a loaded config has inline secrets at looser-than-0600 permissions. +If no OS keychain is available or you pass `--inline-secrets`, the secret is written inline and the file is `chmod 0600`. The CLI emits a warning when a loaded config has inline secrets at looser-than-0600 permissions. + +## Verify your configuration + +Run `elastic status` to check connectivity and authentication for all services in the active context: + +```bash +elastic status +``` + +The command reports the result for each configured service (`elasticsearch`, `kibana`, `cloud`). Services not present in the active context are skipped, not treated as failures. ## Credential-safe project creation @@ -82,14 +108,14 @@ elastic cloud serverless es projects reset-credentials --id \ --save-as scratch --force ``` -`--credentials-file ` writes a standalone YAML config fragment (0600) at `` instead of mutating the main config. Either flag makes stdout safe to capture into an LLM transcript. +`--credentials-file ` writes a standalone YAML config fragment (0600) at `` instead of mutating the main config. Both flags make stdout safe to capture into an LLM transcript. ## External credentials Any string value in the config file can use `$(resolver:params)` expressions to fetch secrets from external sources at runtime. :::{warning} -Review config files before using them if you didn't write them yourself. The `$(cmd:...)` and `$(file:...)` resolvers execute programs and read files on your behalf. This applies especially to CI/CD environments where a repo-checked-in config (e.g. via `ELASTIC_CLI_CONFIG_FILE`) can run arbitrary commands on the runner. +Review config files before using them if you didn't write them yourself. The `$(cmd:...)` and `$(file:...)` resolvers run programs and read files on your behalf. This applies especially to CI/CD environments where a repo-checked-in config (for example, via `ELASTIC_CLI_CONFIG_FILE`) can run arbitrary commands on the runner. ::: `file` @@ -164,3 +190,9 @@ elasticsearch: auth: api_key: $(keychain:elastic-cli/api-key) ``` + +## Next steps + +- Run `elastic --help` to explore available commands. +- Use `elastic cloud serverless` or `elastic cloud hosted` to manage Elastic Cloud resources. +- See the [CLI command reference](./index.md) for the full list of available commands. diff --git a/docs/cli/index.md b/docs/cli/index.md index 7cf4e208..59cc3bb3 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -1,3 +1,16 @@ -Interact with the Elastic Stack and Elastic Cloud from the command line. +--- +description: Use the Elastic CLI to interact with the Elastic Stack and Elastic Cloud from the command line. +applies_to: + stack: preview + serverless: preview +type: overview +--- -Configure the CLI with `elastic config context add` to connect to your Elasticsearch, Kibana, and Elastic Cloud endpoints. See [Installation](./installation.md) and [Configuration](./configuration.md) to get started. +The Elastic CLI (`elastic`) lets you manage Elasticsearch, Kibana, and Elastic Cloud resources from the command line. It supports both self-managed Elastic Stack deployments and Elastic Serverless projects. + +Use the CLI to: +- Connect to multiple clusters or projects using named contexts +- Manage Elastic Cloud Hosted deployments and Serverless projects +- Automate operations in CI/CD pipelines and LLM agent workflows + +To get started, see [Install the Elastic CLI](./installation.md) and [Configure the Elastic CLI](./configuration.md). diff --git a/docs/cli/installation.md b/docs/cli/installation.md index 56439faf..0a8057c6 100644 --- a/docs/cli/installation.md +++ b/docs/cli/installation.md @@ -1,14 +1,39 @@ -# Installation +--- +description: Install the Elastic CLI globally with npm to run elastic commands from your terminal. +applies_to: + stack: preview + serverless: preview +type: how-to +--- -Install globally from `npm` so the elastic binary is available on your `PATH`: +# Install the Elastic CLI -```bash -npm install -g @elastic/cli -elastic --help -``` +## Before you begin + +You need Node.js 22 or later and npm (included with Node.js) installed on your system. The CLI is tested on Linux, macOS, and Windows. + +## Install globally + +1. Install the `elastic` binary to your `PATH`: + + ```bash + npm install -g @elastic/cli + ``` -If you don't want a global install, you can run a one-off invocation with npx, which downloads and runs the CLI without persisting it: +2. Verify the installation: + + ```bash + elastic --version + ``` + +## Run without installing + +To run a one-off command without a permanent install, use `npx`: ```bash npx -y @elastic/cli --help ``` + +## Next steps + +- [Configure the Elastic CLI](./configuration.md) to connect to your Elasticsearch, Kibana, or Elastic Cloud endpoints. diff --git a/docs/cli/schema.json b/docs/cli/schema.json index 5bbc4e6f..41db13bc 100644 --- a/docs/cli/schema.json +++ b/docs/cli/schema.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "name": "elastic", - "version": "0.1.1", + "version": "0.2.0", "reservedMetaCommands": [ "cli-schema" ], @@ -1565,7 +1565,9 @@ "name": "source", "type": "boolean", "required": false, - "summary": "If `false`, turn off source retrieval. You can also specify a comma-separated list of the fields you want to retrieve." + "summary": "If `false`, turn off source retrieval. You can also specify a comma-separated list of the fields you want to retrieve.", + "repeatable": true, + "separator": "," }, { "role": "flag", @@ -2630,7 +2632,9 @@ "name": "source", "type": "boolean", "required": false, - "summary": "The source fields that are returned for matching documents. These fields are returned in the `hits._source` property of the search response. If the `stored_fields` property is specified, the `_source` property defaults to `false`. Otherwise, it defaults to `true`." + "summary": "The source fields that are returned for matching documents. These fields are returned in the `hits._source` property of the search response. If the `stored_fields` property is specified, the `_source` property defaults to `false`. Otherwise, it defaults to `true`.", + "repeatable": true, + "separator": "," }, { "role": "flag", @@ -4646,7 +4650,9 @@ "name": "source", "type": "boolean", "required": false, - "summary": "Indicates which source fields are returned for matching documents. These fields are returned in the hits._source property of the search response." + "summary": "Indicates which source fields are returned for matching documents. These fields are returned in the hits._source property of the search response.", + "repeatable": true, + "separator": "," }, { "role": "flag", @@ -9507,7 +9513,8 @@ "name": "name", "type": "string", "required": true, - "summary": "The view name to remove." + "summary": "The view name to remove.", + "repeatable": true } ], "summary": "Delete an ES|QL view.", @@ -10296,7 +10303,9 @@ "name": "source", "type": "boolean", "required": false, - "summary": "Indicates which source fields are returned for matching documents. These fields are returned in the hits._source property of the search response." + "summary": "Indicates which source fields are returned for matching documents. These fields are returned in the hits._source property of the search response.", + "repeatable": true, + "separator": "," }, { "role": "flag", @@ -16714,7 +16723,8 @@ "enumValues": [ "chat_completion", "completion", - "text_embedding" + "text_embedding", + "embedding" ] }, { @@ -18683,7 +18693,9 @@ "name": "analyzed-fields", "type": "string", "required": false, - "summary": "Specify includes and/or excludes patterns to select which fields will be included in the analysis. The patterns specified in excludes are applied last, therefore excludes takes precedence. In other words, if the same field is specified in both includes and excludes, then the field will not be included in the analysis." + "summary": "Specify includes and/or excludes patterns to select which fields will be included in the analysis. The patterns specified in excludes are applied last, therefore excludes takes precedence. In other words, if the same field is specified in both includes and excludes, then the field will not be included in the analysis.", + "repeatable": true, + "separator": "," }, { "role": "flag", @@ -20210,7 +20222,9 @@ "name": "analyzed-fields", "type": "string", "required": false, - "summary": "Specifies `includes` and/or `excludes` patterns to select which fields will be included in the analysis. The patterns specified in `excludes` are applied last, therefore `excludes` takes precedence. In other words, if the same field is specified in both `includes` and `excludes`, then the field will not be included in the analysis. If `analyzed_fields` is not set, only the relevant fields will be included. For example, all the numeric fields for outlier detection. The supported fields vary for each type of analysis. Outlier detection requires numeric or `boolean` data to analyze. The algorithms don’t support missing values therefore fields that have data types other than numeric or boolean are ignored. Documents where included fields contain missing values, null values, or an array are also ignored. Therefore the `dest` index may contain documents that don’t have an outlier score. Regression supports fields that are numeric, `boolean`, `text`, `keyword`, and `ip` data types. It is also tolerant of missing values. Fields that are supported are included in the analysis, other fields are ignored. Documents where included fields contain an array with two or more values are also ignored. Documents in the `dest` index that don’t contain a results field are not included in the regression analysis. Classification supports fields that are numeric, `boolean`, `text`, `keyword`, and `ip` data types. It is also tolerant of missing values. Fields that are supported are included in the analysis, other fields are ignored. Documents where included fields contain an array with two or more values are also ignored. Documents in the `dest` index that don’t contain a results field are not included in the classification analysis. Classification analysis can be improved by mapping ordinal variable values to a single number. For example, in case of age ranges, you can model the values as `0-14 = 0`, `15-24 = 1`, `25-34 = 2`, and so on." + "summary": "Specifies `includes` and/or `excludes` patterns to select which fields will be included in the analysis. The patterns specified in `excludes` are applied last, therefore `excludes` takes precedence. In other words, if the same field is specified in both `includes` and `excludes`, then the field will not be included in the analysis. If `analyzed_fields` is not set, only the relevant fields will be included. For example, all the numeric fields for outlier detection. The supported fields vary for each type of analysis. Outlier detection requires numeric or `boolean` data to analyze. The algorithms don’t support missing values therefore fields that have data types other than numeric or boolean are ignored. Documents where included fields contain missing values, null values, or an array are also ignored. Therefore the `dest` index may contain documents that don’t have an outlier score. Regression supports fields that are numeric, `boolean`, `text`, `keyword`, and `ip` data types. It is also tolerant of missing values. Fields that are supported are included in the analysis, other fields are ignored. Documents where included fields contain an array with two or more values are also ignored. Documents in the `dest` index that don’t contain a results field are not included in the regression analysis. Classification supports fields that are numeric, `boolean`, `text`, `keyword`, and `ip` data types. It is also tolerant of missing values. Fields that are supported are included in the analysis, other fields are ignored. Documents where included fields contain an array with two or more values are also ignored. Documents in the `dest` index that don’t contain a results field are not included in the classification analysis. Classification analysis can be improved by mapping ordinal variable values to a single number. For example, in case of age ranges, you can model the values as `0-14 = 0`, `15-24 = 1`, `25-34 = 2`, and so on.", + "repeatable": true, + "separator": "," }, { "role": "flag", @@ -24365,6 +24379,13 @@ "required": false, "summary": "The name of the role. You can specify multiple roles as a comma-separated list. If you do not specify this parameter, the API returns information about all roles.", "repeatable": true + }, + { + "role": "flag", + "name": "include-implicit", + "type": "boolean", + "required": false, + "summary": "If `true`, include privileges that are implicitly granted by registered `ImplicitPrivilegesProviders` alongside the explicitly configured privileges. Each implicit entry in the response is annotated with `implicitly_granted: true`." } ], "summary": "Get roles.", @@ -50693,6 +50714,34 @@ "requiresAuth": true } }, + { + "path": [ + "cloud", + "hosted", + "deployments" + ], + "name": "get-deployment-es-resource-tiers", + "parameters": [ + { + "role": "flag", + "name": "deployment-id", + "type": "string", + "required": true, + "summary": "Identifier for the Deployment" + }, + { + "role": "flag", + "name": "ref-id", + "type": "string", + "required": true, + "summary": "User-specified RefId for the Resource (or '_main' if there is only one)" + } + ], + "summary": "Get Elasticsearch tiers", + "intent": { + "requiresAuth": true + } + }, { "path": [ "cloud", @@ -51036,6 +51085,27 @@ "requiresAuth": true } }, + { + "path": [ + "cloud", + "hosted", + "deployments" + ], + "name": "upgrade-deployment", + "parameters": [ + { + "role": "flag", + "name": "deployment-id", + "type": "string", + "required": true, + "summary": "Identifier for the Deployment" + } + ], + "summary": "Upgrade a Deployment to a new Elastic Stack version", + "intent": { + "requiresAuth": true + } + }, { "path": [ "cloud", @@ -53954,35 +54024,5 @@ "summary": "Sanitize values for safe use in Elasticsearch" } ], - "shortcuts": [ - { - "from": "es", - "to": [ - "stack", - "es" - ] - }, - { - "from": "elasticsearch", - "to": [ - "stack", - "es" - ] - }, - { - "from": "kb", - "to": [ - "stack", - "kb" - ] - }, - { - "from": "kibana", - "to": [ - "stack", - "kb" - ] - } - ], "description": "Interface with the Elastic Stack and Elastic Cloud from the command line." } diff --git a/docs/cli/stack/es/index.md b/docs/cli/stack/es/index.md new file mode 100644 index 00000000..26706077 --- /dev/null +++ b/docs/cli/stack/es/index.md @@ -0,0 +1,5 @@ +## Description + +The `elastic stack es` command group exposes Elasticsearch REST APIs as CLI commands. + +These commands are also available using the `elastic es` or `elastic elasticsearch` shortcuts. diff --git a/docs/cli/stack/kb/index.md b/docs/cli/stack/kb/index.md new file mode 100644 index 00000000..25dfd020 --- /dev/null +++ b/docs/cli/stack/kb/index.md @@ -0,0 +1,5 @@ +## Description + +The `elastic stack kb` command group exposes Kibana REST APIs as CLI commands. + +These commands are also available using the `elastic kb` or `elastic kibana` shortcuts. diff --git a/docs/docset.yml b/docs/docset.yml index df7675a1..55b1868a 100644 --- a/docs/docset.yml +++ b/docs/docset.yml @@ -3,8 +3,10 @@ dev_docs: true toc: - file: index.md + children: + - file: cli/installation.md + - file: cli/configuration.md - cli: cli/schema.json folder: cli - children: - - file: installation.md - - file: configuration.md + title: Elastic CLI command reference + navigation_title: Command reference diff --git a/docs/index.md b/docs/index.md index 050009d3..43426230 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,11 @@ +--- +description: Use the Elastic CLI to interact with the Elastic Stack and Elastic Cloud from the command line. +applies_to: + stack: preview + serverless: preview +type: overview +--- + # Elastic CLI Interact with the Elastic Stack and Elastic Cloud from the command line. diff --git a/src/cli-schema.ts b/src/cli-schema.ts index 6a95abc0..57185c4c 100644 --- a/src/cli-schema.ts +++ b/src/cli-schema.ts @@ -9,7 +9,7 @@ import { defineCommand } from './factory.ts' import { stripTransportMeta } from './factory.ts' import type { OpaqueCommandHandle, CommandConfig, CommandIntent, JsonValue } from './factory.ts' import type { SchemaArgDefinition } from './lib/schema-args.ts' -import type { NamespaceEntry, NamespaceShortcut } from './namespaces.ts' +import type { NamespaceEntry } from './namespaces.ts' // --------------------------------------------------------------------------- // CLI schema types @@ -75,11 +75,6 @@ interface CliEnvironment { configFiles: CliConfigFile[] } -interface CliShortcut { - from: string - to: string[] -} - interface CliSchema { schemaVersion: number name: string @@ -89,7 +84,6 @@ interface CliSchema { environment: CliEnvironment commands: CliCommand[] namespaces: CliNamespace[] - shortcuts?: CliShortcut[] description?: string } @@ -524,7 +518,6 @@ export function buildCliSchema ( globalOptions: CliParameter[], version: string, noContextNames: ReadonlySet = new Set(), - shortcuts: NamespaceShortcut[] = [], ): CliSchema { const allCommands: CliCommand[] = [] const namespaces: CliNamespace[] = [] @@ -548,8 +541,6 @@ export function buildCliSchema ( } const promoted = promoteToGlobalOptions(namespaces, allCommands) - const cliShortcuts: CliShortcut[] = shortcuts.map(s => ({ from: s.from, to: s.to })) - return { schemaVersion: 1, name: root.name() || 'elastic', @@ -559,7 +550,6 @@ export function buildCliSchema ( environment: ENVIRONMENT, commands: allCommands, namespaces, - ...(cliShortcuts.length > 0 && { shortcuts: cliShortcuts }), ...(root.description() && { description: root.description() }), } } @@ -597,8 +587,7 @@ export async function registerCliSchemaCommand ( 'version', // root-level version command needs no auth ]) - const allShortcuts = namespaces.flatMap(ns => ns.shortcuts ?? []) - return buildCliSchema(schemaRoot, globalOptions, version, noContextNames, allShortcuts) as unknown as JsonValue + return buildCliSchema(schemaRoot, globalOptions, version, noContextNames) as unknown as JsonValue }, formatOutput: (result) => JSON.stringify(result, null, 2) + '\n', }) diff --git a/test/cli-schema.test.ts b/test/cli-schema.test.ts new file mode 100644 index 00000000..f1b0efa0 --- /dev/null +++ b/test/cli-schema.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { spawn } from 'node:child_process' +import { join } from 'node:path' + +function runCliSchema (): Promise<{ code: number | null, stdout: string, stderr: string }> { + return new Promise((resolve) => { + const child = spawn(process.execPath, [join(process.cwd(), 'dist', 'cli.js'), 'cli-schema'], { + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, ELASTIC_NO_BANNER: '1' }, + }) + child.stdin.end('') + let stdout = '' + let stderr = '' + child.stdout.on('data', (data: Buffer) => { stdout += data }) + child.stderr.on('data', (data: Buffer) => { stderr += data }) + child.on('close', (code: number | null) => resolve({ code, stdout, stderr })) + }) +} + +describe('cli schema', () => { + it('does not emit runtime shortcuts into the docs-builder schema', async () => { + const { code, stdout, stderr } = await runCliSchema() + assert.equal(code, 0, stderr) + + const schema = JSON.parse(stdout) as Record + assert.equal(Object.hasOwn(schema, 'shortcuts'), false) + assert.ok(Array.isArray(schema['namespaces'])) + assert.ok((schema['namespaces'] as Array<{ segment?: string }>).some(ns => ns.segment === 'stack')) + }) +}) diff --git a/test/docs/search.test.ts b/test/docs/search.test.ts index b9127207..66211394 100644 --- a/test/docs/search.test.ts +++ b/test/docs/search.test.ts @@ -73,6 +73,58 @@ describe('createSearchCommand', () => { assert.ok(parseResult !== undefined) }) + it('formats results without product metadata', async () => { + const output: string[] = [] + const response = makeResp({ + results: [ + { + type: 'page', + url: '/reference/no-product', + title: 'No product', + description: '

Fallback description

', + score: 1, + navigationSection: 'Reference', + lastUpdated: '2024-01-01', + relatedProducts: [], + } as DocsSearchResponse['results'][number], + ], + }) + const cmd = createSearchCommand({ + docsSearch: async () => response, + stderr: { write: () => true }, + }) + + cmd.exitOverride() + cmd.configureOutput({ writeOut: (s) => { output.push(s) }, writeErr: () => {} }) + + const restoreStdin = _testSetStdinReader(() => '') + try { + await cmd.parseAsync(['--query', 'no product'], { from: 'user' }) + } finally { restoreStdin() } + + assert.equal(process.exitCode ?? 0, 0) + assert.equal(output.length, 0) + }) + + it('formats an empty result set', async () => { + const output: string[] = [] + const cmd = createSearchCommand({ + docsSearch: async () => makeResp({ results: [], totalResults: 0, pageCount: 0 }), + stderr: { write: () => true }, + }) + + cmd.exitOverride() + cmd.configureOutput({ writeOut: (s) => { output.push(s) }, writeErr: () => {} }) + + const restoreStdin = _testSetStdinReader(() => '') + try { + await cmd.parseAsync(['--query', 'missing'], { from: 'user' }) + } finally { restoreStdin() } + + assert.equal(process.exitCode ?? 0, 0) + assert.equal(output.length, 0) + }) + it('returns error object when docsSearch throws', async () => { const stderrOutput: string[] = [] const cmd = createSearchCommand({