Skip to content

Commit

Permalink
Added painless execute api. (#29164)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
martijnvg committed Apr 19, 2018
1 parent 7804135 commit 827d5ad
Show file tree
Hide file tree
Showing 8 changed files with 563 additions and 1 deletion.
53 changes: 53 additions & 0 deletions docs/painless/painless-execute-script.asciidoc
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions docs/painless/painless-getting-started.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,5 @@ dispatch *feels* like it'd add a ton of complexity which'd make maintenance and
other improvements much more difficult.

include::painless-debugging.asciidoc[]

include::painless-execute-script.asciidoc[]
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
}

}
Loading

0 comments on commit 827d5ad

Please sign in to comment.