-
Notifications
You must be signed in to change notification settings - Fork 216
Add a generic, extensible rest-endpoint provider SPI #5656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a64354c
7580a51
ba90548
cc58b4c
eed9621
7af15b0
7a79709
f83b14b
9afadf8
f2aeb5f
b0eff8e
0ed1f36
dab07e9
387ed50
591fd8b
7077b81
ac3f194
7637f4d
7b63421
65d3f86
8d371a8
19818d4
6a4e66b
0ff4abc
39950e3
31e5297
8d3cf4e
c0b748f
bae0853
8c422dd
f96256b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package org.opensearch.sql.ast.tree; | ||
|
|
||
| import java.util.Collections; | ||
| import lombok.EqualsAndHashCode; | ||
| import lombok.ToString; | ||
| import org.opensearch.sql.ast.expression.UnresolvedExpression; | ||
|
|
||
| /** | ||
| * Extend Relation to mark a {@code rest} leading command. The single table name is a reserved, | ||
| * encoded token (produced by {@link org.opensearch.sql.utils.SystemIndexUtils#restTable}) that | ||
| * carries the validated REST endpoint spec; it resolves through the storage engine to a REST source | ||
| * table on the Calcite path, exactly as {@link DescribeRelation} resolves to a system index. | ||
| */ | ||
| @ToString | ||
| @EqualsAndHashCode(callSuper = false) | ||
| public class RestRelation extends Relation { | ||
| public RestRelation(UnresolvedExpression tableName) { | ||
| super(Collections.singletonList(tableName)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # rest | ||
|
|
||
| The `rest` command is a leading command that reads an allow-listed, read-only in-cluster management endpoint and emits the response as PPL rows. Its rows come from the endpoint dispatch, not from an index, so `rest` appears at the start of a query. | ||
|
|
||
| > **Note**: The `rest` command is supported only on the Calcite query engine (`plugins.calcite.enabled=true`). Each endpoint has a fixed output schema, and the dispatch runs under the caller's security context, so a user who cannot call an endpoint directly cannot call it through `rest`. The command is read-only; mutating and non-allow-listed endpoints are rejected. Each endpoint requires the same cluster-monitor privilege as calling it natively, so `rest` grants no extra access. | ||
|
|
||
| The `rest` command is a generic, extensible framework: a plugin contributes additional read-only endpoints through the `RestEndpointProvider` extension point without changing the grammar. This first version ships a single built-in endpoint, `/_cluster/health`. Additional endpoints (for example `/_cat/nodes`, `/_cat/shards`, `/_cluster/state`, `/_cluster/settings`) can be added in follow-ups, with any response redaction handled inside the provider's own handler. | ||
|
|
||
| ## Enabling the command | ||
|
|
||
| `/_cluster/health` is **enabled by default**: `plugins.ppl.rest.allowed_endpoints` defaults to `["/_cluster/health"]`. Any other endpoint is rejected until a deployment adds it to the allow-list (a node-level setting, applied at node startup and not changeable at runtime): | ||
|
|
||
| ```yaml | ||
| plugins.ppl.rest.allowed_endpoints: ["/_cluster/health"] | ||
| ``` | ||
|
|
||
| Every endpoint must be listed explicitly by name; there is no wildcard, so a newly installed or upgraded provider is never enabled without an explicit allow-list change. Set an empty list to disable the command entirely. | ||
|
|
||
| ## Syntax | ||
|
|
||
| ```syntax | ||
| rest <endpoint-path> [count=<int>] [<get-arg>=<value> ...] | ||
| ``` | ||
|
|
||
| ## Parameters | ||
|
|
||
| | Parameter | Required/Optional | Description | | ||
| | --- | --- | --- | | ||
| | `<endpoint-path>` | Required | An allow-listed, read-only endpoint path (see the allow-list below), for example `/_cluster/health`. | | ||
| | `count=<int>` | Optional | Caps the number of emitted rows. | | ||
| | `<get-arg>=<value>` | Optional | Endpoint query arguments, validated per endpoint by both key and value (for example `local=true` for `/_cluster/health`). | | ||
|
|
||
| ## Allow-list | ||
|
|
||
| `rest` resolves only an explicit, curated set of read-only endpoints. Anything outside the list, including any mutating endpoint, is rejected with a clear error. | ||
|
|
||
| | Endpoint | Output columns | Accepted args | | ||
| | --- | --- | --- | | ||
| | `/_cluster/health` | `response` (string): the full cluster-health response as JSON. Extract fields with `json_extract` or the `spath` command (see the example below). | `local` | | ||
|
|
||
| ## Example: Reading fields from the response | ||
|
|
||
| `/_cluster/health` returns the full health response in a single `response` column as JSON. Extract the fields you need with `json_extract` (or the `spath` command): | ||
|
|
||
| ```ppl ignore | ||
| | rest '/_cluster/health' | ||
| | eval status = json_extract(response, 'status'), | ||
| number_of_nodes = json_extract(response, 'number_of_nodes') | ||
| | fields status, number_of_nodes | ||
| ``` | ||
|
|
||
| The query returns the following results: | ||
|
|
||
| ```text | ||
| fetched rows / total rows = 1/1 | ||
| +--------+-----------------+ | ||
| | status | number_of_nodes | | ||
| |--------+-----------------| | ||
| | green | 1 | | ||
| +--------+-----------------+ | ||
| ``` | ||
|
|
||
| Because the whole response is available, a query can read any field it exposes (for example `active_shards`, `active_primary_shards`, `unassigned_shards`) without the endpoint pre-declaring a column for it. The extracted columns then compose with downstream `where`, `sort`, `stats`, and `fields` exactly like an index scan, for example `| rest '/_cluster/health' | spath input=response path=status output=status | where status = 'green'`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -387,6 +387,8 @@ testClusters { | |
| plugin(getGeoSpatialPlugin()) | ||
| plugin ":opensearch-sql-plugin" | ||
| setting "plugins.query.datasources.encryption.masterkey", "1234567812345678" | ||
| // Only /_cluster/health is registered; pin the allow-list to it for the rest ITs. | ||
| setting 'plugins.ppl.rest.allowed_endpoints', '/_cluster/health' | ||
| } | ||
| yamlRestTest { | ||
| testDistribution = 'archive' | ||
|
|
@@ -405,6 +407,8 @@ testClusters { | |
| testDistribution = 'archive' | ||
| plugin(getJobSchedulerPlugin()) | ||
| plugin ":opensearch-sql-plugin" | ||
| // Only /_cluster/health is registered; pin the allow-list to it for RestCommandSecurityIT. | ||
| setting 'plugins.ppl.rest.allowed_endpoints', '/_cluster/health' | ||
| } | ||
| remoteIntegTestWithSecurity { | ||
| testDistribution = 'archive' | ||
|
|
@@ -419,6 +423,8 @@ testClusters { | |
| plugin(getArrowFlightRpcPlugin()) | ||
| plugin(getAnalyticsEnginePlugin()) | ||
| plugin ":opensearch-sql-plugin" | ||
| // Composite-default cluster: PPL queries route to the analytics engine unless excluded. | ||
| setting 'cluster.pluggable.dataformat', 'composite' | ||
|
Comment on lines
+426
to
+427
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR should not releated to analytics engine?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is required for |
||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.