Added painless execute api. (#29164)

Added an api that allows to execute an arbitrary script and a result to be returned.

```
POST /_scripts/painless/_execute
{
  "script": {
    "source": "params.var1 / params.var2",
    "params": {
      "var1": 1,
      "var2": 1
    }
  }
}
```

Relates to #27875
This commit is contained in:
Martijn van Groningen 2018-04-19 09:33:34 +02:00 committed by GitHub
parent 621a1935b8
commit 8afa7c174f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 563 additions and 1 deletions

View File

@ -0,0 +1,53 @@
[[painless-execute-api]]
=== Painless execute API
The Painless execute API allows an arbitrary script to be executed and a result to be returned.
[[painless-execute-api-parameters]]
.Parameters
[options="header"]
|======
| Name | Required | Default | Description
| `script` | yes | - | The script to execute
| `context` | no | `execute_api_script` | The context the script should be executed in.
|======
==== Contexts
Contexts control how scripts are executed, what variables are available at runtime and what the return type is.
===== Painless test script context
The `painless_test` context executes scripts as is and do not add any special parameters.
The only variable that is available is `params`, which can be used to access user defined values.
The result of the script is always converted to a string.
If no context is specified then this context is used by default.
==== Example
Request:
[source,js]
----------------------------------------------------------------
POST /_scripts/painless/_execute
{
"script": {
"source": "params.count / params.total",
"params": {
"count": 100.0,
"total": 1000.0
}
}
}
----------------------------------------------------------------
// CONSOLE
Response:
[source,js]
--------------------------------------------------
{
"result": "0.1"
}
--------------------------------------------------
// TESTRESPONSE

View File

@ -389,3 +389,5 @@ dispatch *feels* like it'd add a ton of complexity which'd make maintenance and
other improvements much more difficult. other improvements much more difficult.
include::painless-debugging.asciidoc[] include::painless-debugging.asciidoc[]
include::painless-execute-script.asciidoc[]

View File

@ -0,0 +1,338 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.painless;
import org.elasticsearch.action.Action;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import static org.elasticsearch.action.ValidateActions.addValidationError;
import static org.elasticsearch.rest.RestRequest.Method.GET;
import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestStatus.OK;
public class PainlessExecuteAction extends Action<PainlessExecuteAction.Request, PainlessExecuteAction.Response,
PainlessExecuteAction.RequestBuilder> {
static final PainlessExecuteAction INSTANCE = new PainlessExecuteAction();
private static final String NAME = "cluster:admin/scripts/painless/execute";
private PainlessExecuteAction() {
super(NAME);
}
@Override
public RequestBuilder newRequestBuilder(ElasticsearchClient client) {
return new RequestBuilder(client);
}
@Override
public Response newResponse() {
return new Response();
}
public static class Request extends ActionRequest implements ToXContent {
private static final ParseField SCRIPT_FIELD = new ParseField("script");
private static final ParseField CONTEXT_FIELD = new ParseField("context");
private static final ConstructingObjectParser<Request, Void> PARSER = new ConstructingObjectParser<>(
"painless_execute_request", args -> new Request((Script) args[0], (SupportedContext) args[1]));
static {
PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> Script.parse(p), SCRIPT_FIELD);
PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> {
// For now only accept an empty json object:
XContentParser.Token token = p.nextToken();
assert token == XContentParser.Token.FIELD_NAME;
String contextType = p.currentName();
token = p.nextToken();
assert token == XContentParser.Token.START_OBJECT;
token = p.nextToken();
assert token == XContentParser.Token.END_OBJECT;
token = p.nextToken();
assert token == XContentParser.Token.END_OBJECT;
return SupportedContext.valueOf(contextType.toUpperCase(Locale.ROOT));
}, CONTEXT_FIELD);
}
private Script script;
private SupportedContext context;
static Request parse(XContentParser parser) throws IOException {
return PARSER.parse(parser, null);
}
Request(Script script, SupportedContext context) {
this.script = Objects.requireNonNull(script);
this.context = context != null ? context : SupportedContext.PAINLESS_TEST;
}
Request() {
}
public Script getScript() {
return script;
}
public SupportedContext getContext() {
return context;
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (script.getType() != ScriptType.INLINE) {
validationException = addValidationError("only inline scripts are supported", validationException);
}
return validationException;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
script = new Script(in);
context = SupportedContext.fromId(in.readByte());
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
script.writeTo(out);
out.writeByte(context.id);
}
// For testing only:
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.field(SCRIPT_FIELD.getPreferredName(), script);
builder.startObject(CONTEXT_FIELD.getPreferredName());
{
builder.startObject(context.name());
builder.endObject();
}
builder.endObject();
return builder;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Request request = (Request) o;
return Objects.equals(script, request.script) &&
context == request.context;
}
@Override
public int hashCode() {
return Objects.hash(script, context);
}
public enum SupportedContext {
PAINLESS_TEST((byte) 0);
private final byte id;
SupportedContext(byte id) {
this.id = id;
}
public static SupportedContext fromId(byte id) {
switch (id) {
case 0:
return PAINLESS_TEST;
default:
throw new IllegalArgumentException("unknown context [" + id + "]");
}
}
}
}
public static class RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder> {
RequestBuilder(ElasticsearchClient client) {
super(client, INSTANCE, new Request());
}
}
public static class Response extends ActionResponse implements ToXContentObject {
private Object result;
Response() {}
Response(Object result) {
this.result = result;
}
public Object getResult() {
return result;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
result = in.readGenericValue();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeGenericValue(result);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field("result", result);
return builder.endObject();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Response response = (Response) o;
return Objects.equals(result, response.result);
}
@Override
public int hashCode() {
return Objects.hash(result);
}
}
public abstract static class PainlessTestScript {
private final Map<String, Object> params;
public PainlessTestScript(Map<String, Object> params) {
this.params = params;
}
/** Return the parameters for this script. */
public Map<String, Object> getParams() {
return params;
}
public abstract Object execute();
public interface Factory {
PainlessTestScript newInstance(Map<String, Object> params);
}
public static final String[] PARAMETERS = {};
public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("painless_test", Factory.class);
}
public static class TransportAction extends HandledTransportAction<Request, Response> {
private final ScriptService scriptService;
@Inject
public TransportAction(Settings settings, ThreadPool threadPool, TransportService transportService,
ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver,
ScriptService scriptService) {
super(settings, NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, Request::new);
this.scriptService = scriptService;
}
@Override
protected void doExecute(Request request, ActionListener<Response> listener) {
switch (request.context) {
case PAINLESS_TEST:
PainlessTestScript.Factory factory = scriptService.compile(request.script, PainlessTestScript.CONTEXT);
PainlessTestScript painlessTestScript = factory.newInstance(request.script.getParams());
String result = Objects.toString(painlessTestScript.execute());
listener.onResponse(new Response(result));
break;
default:
throw new UnsupportedOperationException("unsupported context [" + request.context + "]");
}
}
}
static class RestAction extends BaseRestHandler {
RestAction(Settings settings, RestController controller) {
super(settings);
controller.registerHandler(GET, "/_scripts/painless/_execute", this);
controller.registerHandler(POST, "/_scripts/painless/_execute", this);
}
@Override
public String getName() {
return "_scripts_painless_execute";
}
@Override
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
final Request request = Request.parse(restRequest.contentOrSourceParamParser());
return channel -> client.executeLocally(INSTANCE, request, new RestBuilderListener<Response>(channel) {
@Override
public RestResponse buildResponse(Response response, XContentBuilder builder) throws Exception {
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
return new BytesRestResponse(OK, builder);
}
});
}
}
}

View File

@ -20,28 +20,40 @@
package org.elasticsearch.painless; package org.elasticsearch.painless;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.IndexScopedSettings;
import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsFilter;
import org.elasticsearch.painless.spi.PainlessExtension; import org.elasticsearch.painless.spi.PainlessExtension;
import org.elasticsearch.painless.spi.Whitelist; import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.ExtensiblePlugin; import org.elasticsearch.plugins.ExtensiblePlugin;
import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.ScriptPlugin; import org.elasticsearch.plugins.ScriptPlugin;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine; import org.elasticsearch.script.ScriptEngine;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import java.util.function.Supplier;
/** /**
* Registers Painless as a plugin. * Registers Painless as a plugin.
*/ */
public final class PainlessPlugin extends Plugin implements ScriptPlugin, ExtensiblePlugin { public final class PainlessPlugin extends Plugin implements ScriptPlugin, ExtensiblePlugin, ActionPlugin {
private final Map<ScriptContext<?>, List<Whitelist>> extendedWhitelists = new HashMap<>(); private final Map<ScriptContext<?>, List<Whitelist>> extendedWhitelists = new HashMap<>();
@ -74,4 +86,24 @@ public final class PainlessPlugin extends Plugin implements ScriptPlugin, Extens
} }
} }
} }
@SuppressWarnings("rawtypes")
public List<ScriptContext> getContexts() {
return Collections.singletonList(PainlessExecuteAction.PainlessTestScript.CONTEXT);
}
@Override
public List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> getActions() {
return Collections.singletonList(
new ActionHandler<>(PainlessExecuteAction.INSTANCE, PainlessExecuteAction.TransportAction.class)
);
}
@Override
public List<RestHandler> getRestHandlers(Settings settings, RestController restController, ClusterSettings clusterSettings,
IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter,
IndexNameExpressionResolver indexNameExpressionResolver,
Supplier<DiscoveryNodes> nodesInCluster) {
return Collections.singletonList(new PainlessExecuteAction.RestAction(settings, restController));
}
} }

View File

@ -0,0 +1,61 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.painless;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import java.io.IOException;
import java.util.Collections;
public class PainlessExecuteRequestTests extends AbstractStreamableXContentTestCase<PainlessExecuteAction.Request> {
@Override
protected PainlessExecuteAction.Request createTestInstance() {
Script script = new Script(randomAlphaOfLength(10));
PainlessExecuteAction.Request.SupportedContext context = randomBoolean() ?
PainlessExecuteAction.Request.SupportedContext.PAINLESS_TEST : null;
return new PainlessExecuteAction.Request(script, context);
}
@Override
protected PainlessExecuteAction.Request createBlankInstance() {
return new PainlessExecuteAction.Request();
}
@Override
protected PainlessExecuteAction.Request doParseInstance(XContentParser parser) throws IOException {
return PainlessExecuteAction.Request.parse(parser);
}
@Override
protected boolean supportsUnknownFields() {
return false;
}
public void testValidate() {
Script script = new Script(ScriptType.STORED, null, randomAlphaOfLength(10), Collections.emptyMap());
PainlessExecuteAction.Request request = new PainlessExecuteAction.Request(script, null);
Exception e = request.validate();
assertNotNull(e);
assertEquals("Validation Failed: 1: only inline scripts are supported;", e.getMessage());
}
}

View File

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.painless;
import org.elasticsearch.test.AbstractStreamableTestCase;
public class PainlessExecuteResponseTests extends AbstractStreamableTestCase<PainlessExecuteAction.Response> {
@Override
protected PainlessExecuteAction.Response createBlankInstance() {
return new PainlessExecuteAction.Response();
}
@Override
protected PainlessExecuteAction.Response createTestInstance() {
return new PainlessExecuteAction.Response(randomAlphaOfLength(10));
}
}

View File

@ -0,0 +1,25 @@
---
"Execute with defaults":
- do:
scripts_painless_execute:
body:
script:
source: "params.count / params.total"
params:
count: 100.0
total: 1000.0
- match: { result: "0.1" }
---
"Execute with execute_api_script context":
- do:
scripts_painless_execute:
body:
script:
source: "params.var1 - params.var2"
params:
var1: 10
var2: 100
context:
painless_test: {}
- match: { result: "-90" }

View File

@ -0,0 +1,17 @@
{
"scripts_painless_execute": {
"documentation": "https://www.elastic.co/guide/en/elasticsearch/painless/master/painless-execute-api.html",
"methods": ["GET", "POST"],
"url": {
"path": "/_scripts/painless/_execute",
"paths": ["/_scripts/painless/_execute"],
"parts": {
},
"params": {
}
},
"body": {
"description": "The script to execute"
}
}
}