Skip to content

Commit 8afa7c1

Browse files
authored
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
1 parent 621a193 commit 8afa7c1

File tree

8 files changed

+563
-1
lines changed

8 files changed

+563
-1
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[[painless-execute-api]]
2+
=== Painless execute API
3+
4+
The Painless execute API allows an arbitrary script to be executed and a result to be returned.
5+
6+
[[painless-execute-api-parameters]]
7+
.Parameters
8+
[options="header"]
9+
|======
10+
| Name | Required | Default | Description
11+
| `script` | yes | - | The script to execute
12+
| `context` | no | `execute_api_script` | The context the script should be executed in.
13+
|======
14+
15+
==== Contexts
16+
17+
Contexts control how scripts are executed, what variables are available at runtime and what the return type is.
18+
19+
===== Painless test script context
20+
21+
The `painless_test` context executes scripts as is and do not add any special parameters.
22+
The only variable that is available is `params`, which can be used to access user defined values.
23+
The result of the script is always converted to a string.
24+
If no context is specified then this context is used by default.
25+
26+
==== Example
27+
28+
Request:
29+
30+
[source,js]
31+
----------------------------------------------------------------
32+
POST /_scripts/painless/_execute
33+
{
34+
"script": {
35+
"source": "params.count / params.total",
36+
"params": {
37+
"count": 100.0,
38+
"total": 1000.0
39+
}
40+
}
41+
}
42+
----------------------------------------------------------------
43+
// CONSOLE
44+
45+
Response:
46+
47+
[source,js]
48+
--------------------------------------------------
49+
{
50+
"result": "0.1"
51+
}
52+
--------------------------------------------------
53+
// TESTRESPONSE

docs/painless/painless-getting-started.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,5 @@ dispatch *feels* like it'd add a ton of complexity which'd make maintenance and
389389
other improvements much more difficult.
390390

391391
include::painless-debugging.asciidoc[]
392+
393+
include::painless-execute-script.asciidoc[]
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.painless;
20+
21+
import org.elasticsearch.action.Action;
22+
import org.elasticsearch.action.ActionListener;
23+
import org.elasticsearch.action.ActionRequest;
24+
import org.elasticsearch.action.ActionRequestBuilder;
25+
import org.elasticsearch.action.ActionRequestValidationException;
26+
import org.elasticsearch.action.ActionResponse;
27+
import org.elasticsearch.action.support.ActionFilters;
28+
import org.elasticsearch.action.support.HandledTransportAction;
29+
import org.elasticsearch.client.ElasticsearchClient;
30+
import org.elasticsearch.client.node.NodeClient;
31+
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
32+
import org.elasticsearch.common.ParseField;
33+
import org.elasticsearch.common.inject.Inject;
34+
import org.elasticsearch.common.io.stream.StreamInput;
35+
import org.elasticsearch.common.io.stream.StreamOutput;
36+
import org.elasticsearch.common.settings.Settings;
37+
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
38+
import org.elasticsearch.common.xcontent.ToXContent;
39+
import org.elasticsearch.common.xcontent.ToXContentObject;
40+
import org.elasticsearch.common.xcontent.XContentBuilder;
41+
import org.elasticsearch.common.xcontent.XContentParser;
42+
import org.elasticsearch.rest.BaseRestHandler;
43+
import org.elasticsearch.rest.BytesRestResponse;
44+
import org.elasticsearch.rest.RestController;
45+
import org.elasticsearch.rest.RestRequest;
46+
import org.elasticsearch.rest.RestResponse;
47+
import org.elasticsearch.rest.action.RestBuilderListener;
48+
import org.elasticsearch.script.Script;
49+
import org.elasticsearch.script.ScriptContext;
50+
import org.elasticsearch.script.ScriptService;
51+
import org.elasticsearch.script.ScriptType;
52+
import org.elasticsearch.threadpool.ThreadPool;
53+
import org.elasticsearch.transport.TransportService;
54+
55+
import java.io.IOException;
56+
import java.util.Locale;
57+
import java.util.Map;
58+
import java.util.Objects;
59+
60+
import static org.elasticsearch.action.ValidateActions.addValidationError;
61+
import static org.elasticsearch.rest.RestRequest.Method.GET;
62+
import static org.elasticsearch.rest.RestRequest.Method.POST;
63+
import static org.elasticsearch.rest.RestStatus.OK;
64+
65+
public class PainlessExecuteAction extends Action<PainlessExecuteAction.Request, PainlessExecuteAction.Response,
66+
PainlessExecuteAction.RequestBuilder> {
67+
68+
static final PainlessExecuteAction INSTANCE = new PainlessExecuteAction();
69+
private static final String NAME = "cluster:admin/scripts/painless/execute";
70+
71+
private PainlessExecuteAction() {
72+
super(NAME);
73+
}
74+
75+
@Override
76+
public RequestBuilder newRequestBuilder(ElasticsearchClient client) {
77+
return new RequestBuilder(client);
78+
}
79+
80+
@Override
81+
public Response newResponse() {
82+
return new Response();
83+
}
84+
85+
public static class Request extends ActionRequest implements ToXContent {
86+
87+
private static final ParseField SCRIPT_FIELD = new ParseField("script");
88+
private static final ParseField CONTEXT_FIELD = new ParseField("context");
89+
private static final ConstructingObjectParser<Request, Void> PARSER = new ConstructingObjectParser<>(
90+
"painless_execute_request", args -> new Request((Script) args[0], (SupportedContext) args[1]));
91+
92+
static {
93+
PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> Script.parse(p), SCRIPT_FIELD);
94+
PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> {
95+
// For now only accept an empty json object:
96+
XContentParser.Token token = p.nextToken();
97+
assert token == XContentParser.Token.FIELD_NAME;
98+
String contextType = p.currentName();
99+
token = p.nextToken();
100+
assert token == XContentParser.Token.START_OBJECT;
101+
token = p.nextToken();
102+
assert token == XContentParser.Token.END_OBJECT;
103+
token = p.nextToken();
104+
assert token == XContentParser.Token.END_OBJECT;
105+
return SupportedContext.valueOf(contextType.toUpperCase(Locale.ROOT));
106+
}, CONTEXT_FIELD);
107+
}
108+
109+
private Script script;
110+
private SupportedContext context;
111+
112+
static Request parse(XContentParser parser) throws IOException {
113+
return PARSER.parse(parser, null);
114+
}
115+
116+
Request(Script script, SupportedContext context) {
117+
this.script = Objects.requireNonNull(script);
118+
this.context = context != null ? context : SupportedContext.PAINLESS_TEST;
119+
}
120+
121+
Request() {
122+
}
123+
124+
public Script getScript() {
125+
return script;
126+
}
127+
128+
public SupportedContext getContext() {
129+
return context;
130+
}
131+
132+
@Override
133+
public ActionRequestValidationException validate() {
134+
ActionRequestValidationException validationException = null;
135+
if (script.getType() != ScriptType.INLINE) {
136+
validationException = addValidationError("only inline scripts are supported", validationException);
137+
}
138+
return validationException;
139+
}
140+
141+
@Override
142+
public void readFrom(StreamInput in) throws IOException {
143+
super.readFrom(in);
144+
script = new Script(in);
145+
context = SupportedContext.fromId(in.readByte());
146+
}
147+
148+
@Override
149+
public void writeTo(StreamOutput out) throws IOException {
150+
super.writeTo(out);
151+
script.writeTo(out);
152+
out.writeByte(context.id);
153+
}
154+
155+
// For testing only:
156+
@Override
157+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
158+
builder.field(SCRIPT_FIELD.getPreferredName(), script);
159+
builder.startObject(CONTEXT_FIELD.getPreferredName());
160+
{
161+
builder.startObject(context.name());
162+
builder.endObject();
163+
}
164+
builder.endObject();
165+
return builder;
166+
}
167+
168+
@Override
169+
public boolean equals(Object o) {
170+
if (this == o) return true;
171+
if (o == null || getClass() != o.getClass()) return false;
172+
Request request = (Request) o;
173+
return Objects.equals(script, request.script) &&
174+
context == request.context;
175+
}
176+
177+
@Override
178+
public int hashCode() {
179+
return Objects.hash(script, context);
180+
}
181+
182+
public enum SupportedContext {
183+
184+
PAINLESS_TEST((byte) 0);
185+
186+
private final byte id;
187+
188+
SupportedContext(byte id) {
189+
this.id = id;
190+
}
191+
192+
public static SupportedContext fromId(byte id) {
193+
switch (id) {
194+
case 0:
195+
return PAINLESS_TEST;
196+
default:
197+
throw new IllegalArgumentException("unknown context [" + id + "]");
198+
}
199+
}
200+
}
201+
202+
}
203+
204+
public static class RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder> {
205+
206+
RequestBuilder(ElasticsearchClient client) {
207+
super(client, INSTANCE, new Request());
208+
}
209+
}
210+
211+
public static class Response extends ActionResponse implements ToXContentObject {
212+
213+
private Object result;
214+
215+
Response() {}
216+
217+
Response(Object result) {
218+
this.result = result;
219+
}
220+
221+
public Object getResult() {
222+
return result;
223+
}
224+
225+
@Override
226+
public void readFrom(StreamInput in) throws IOException {
227+
super.readFrom(in);
228+
result = in.readGenericValue();
229+
}
230+
231+
@Override
232+
public void writeTo(StreamOutput out) throws IOException {
233+
super.writeTo(out);
234+
out.writeGenericValue(result);
235+
}
236+
237+
@Override
238+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
239+
builder.startObject();
240+
builder.field("result", result);
241+
return builder.endObject();
242+
}
243+
244+
@Override
245+
public boolean equals(Object o) {
246+
if (this == o) return true;
247+
if (o == null || getClass() != o.getClass()) return false;
248+
Response response = (Response) o;
249+
return Objects.equals(result, response.result);
250+
}
251+
252+
@Override
253+
public int hashCode() {
254+
return Objects.hash(result);
255+
}
256+
}
257+
258+
public abstract static class PainlessTestScript {
259+
260+
private final Map<String, Object> params;
261+
262+
public PainlessTestScript(Map<String, Object> params) {
263+
this.params = params;
264+
}
265+
266+
/** Return the parameters for this script. */
267+
public Map<String, Object> getParams() {
268+
return params;
269+
}
270+
271+
public abstract Object execute();
272+
273+
public interface Factory {
274+
275+
PainlessTestScript newInstance(Map<String, Object> params);
276+
277+
}
278+
279+
public static final String[] PARAMETERS = {};
280+
public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("painless_test", Factory.class);
281+
282+
}
283+
284+
public static class TransportAction extends HandledTransportAction<Request, Response> {
285+
286+
287+
private final ScriptService scriptService;
288+
289+
@Inject
290+
public TransportAction(Settings settings, ThreadPool threadPool, TransportService transportService,
291+
ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver,
292+
ScriptService scriptService) {
293+
super(settings, NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, Request::new);
294+
this.scriptService = scriptService;
295+
}
296+
@Override
297+
protected void doExecute(Request request, ActionListener<Response> listener) {
298+
switch (request.context) {
299+
case PAINLESS_TEST:
300+
PainlessTestScript.Factory factory = scriptService.compile(request.script, PainlessTestScript.CONTEXT);
301+
PainlessTestScript painlessTestScript = factory.newInstance(request.script.getParams());
302+
String result = Objects.toString(painlessTestScript.execute());
303+
listener.onResponse(new Response(result));
304+
break;
305+
default:
306+
throw new UnsupportedOperationException("unsupported context [" + request.context + "]");
307+
}
308+
}
309+
310+
}
311+
312+
static class RestAction extends BaseRestHandler {
313+
314+
RestAction(Settings settings, RestController controller) {
315+
super(settings);
316+
controller.registerHandler(GET, "/_scripts/painless/_execute", this);
317+
controller.registerHandler(POST, "/_scripts/painless/_execute", this);
318+
}
319+
320+
@Override
321+
public String getName() {
322+
return "_scripts_painless_execute";
323+
}
324+
325+
@Override
326+
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
327+
final Request request = Request.parse(restRequest.contentOrSourceParamParser());
328+
return channel -> client.executeLocally(INSTANCE, request, new RestBuilderListener<Response>(channel) {
329+
@Override
330+
public RestResponse buildResponse(Response response, XContentBuilder builder) throws Exception {
331+
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
332+
return new BytesRestResponse(OK, builder);
333+
}
334+
});
335+
}
336+
}
337+
338+
}

0 commit comments

Comments
 (0)