Skip to content

Commit

Permalink
Add a Painless Context REST API (#39382)
Browse files Browse the repository at this point in the history
This PR adds an internal REST API for querying context information about 
Painless whitelists.

Commands include the following:
GET /_scripts/painless/_context -- retrieves a list of contexts
GET /_scripts/painless/_context?context=%name% retrieves all available 
information about the API for this specific context
  • Loading branch information
jdconrad committed Mar 14, 2019
1 parent ae0ff05 commit b57af6c
Show file tree
Hide file tree
Showing 16 changed files with 1,519 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,7 @@ public void testApiNamingConventions() throws Exception {
"nodes.hot_threads",
"nodes.usage",
"nodes.reload_secure_settings",
"scripts_painless_context",
"search_shards",
};
List<String> booleanReturnMethods = Arrays.asList(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@
package org.elasticsearch.painless;


import org.apache.lucene.util.SetOnce;
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.inject.Module;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.IndexScopedSettings;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsFilter;
import org.elasticsearch.painless.action.PainlessContextAction;
import org.elasticsearch.painless.spi.PainlessExtension;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.painless.spi.WhitelistLoader;
Expand Down Expand Up @@ -81,6 +84,8 @@ public final class PainlessPlugin extends Plugin implements ScriptPlugin, Extens
whitelists = map;
}

private final SetOnce<PainlessScriptEngine> painlessScriptEngine = new SetOnce<>();

@Override
public ScriptEngine getScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) {
Map<ScriptContext<?>, List<Whitelist>> contextsWithWhitelists = new HashMap<>();
Expand All @@ -92,7 +97,13 @@ public ScriptEngine getScriptEngine(Settings settings, Collection<ScriptContext<
}
contextsWithWhitelists.put(context, contextWhitelists);
}
return new PainlessScriptEngine(settings, contextsWithWhitelists);
painlessScriptEngine.set(new PainlessScriptEngine(settings, contextsWithWhitelists));
return painlessScriptEngine.get();
}

@Override
public Collection<Module> createGuiceModules() {
return Collections.singleton(b -> b.bind(PainlessScriptEngine.class).toInstance(painlessScriptEngine.get()));
}

@Override
Expand All @@ -118,16 +129,20 @@ public List<ScriptContext<?>> getContexts() {

@Override
public List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> getActions() {
return Collections.singletonList(
new ActionHandler<>(PainlessExecuteAction.INSTANCE, PainlessExecuteAction.TransportAction.class)
);
List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> actions = new ArrayList<>();
actions.add(new ActionHandler<>(PainlessExecuteAction.INSTANCE, PainlessExecuteAction.TransportAction.class));
actions.add(new ActionHandler<>(PainlessContextAction.INSTANCE, PainlessContextAction.TransportAction.class));
return actions;
}

@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));
List<RestHandler> handlers = new ArrayList<>();
handlers.add(new PainlessExecuteAction.RestAction(settings, restController));
handlers.add(new PainlessContextAction.RestAction(settings, restController));
return handlers;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.painless.Compiler.Loader;
import org.elasticsearch.painless.lookup.PainlessLookup;
import org.elasticsearch.painless.lookup.PainlessLookupBuilder;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.script.ScriptContext;
Expand Down Expand Up @@ -82,6 +83,7 @@ public final class PainlessScriptEngine implements ScriptEngine {
private final CompilerSettings defaultCompilerSettings = new CompilerSettings();

private final Map<ScriptContext<?>, Compiler> contextsToCompilers;
private final Map<ScriptContext<?>, PainlessLookup> contextsToLookups;

/**
* Constructor.
Expand All @@ -91,14 +93,22 @@ public PainlessScriptEngine(Settings settings, Map<ScriptContext<?>, List<Whitel
defaultCompilerSettings.setRegexesEnabled(CompilerSettings.REGEX_ENABLED.get(settings));

Map<ScriptContext<?>, Compiler> contextsToCompilers = new HashMap<>();
Map<ScriptContext<?>, PainlessLookup> contextsToLookups = new HashMap<>();

for (Map.Entry<ScriptContext<?>, List<Whitelist>> entry : contexts.entrySet()) {
ScriptContext<?> context = entry.getKey();
contextsToCompilers.put(context, new Compiler(context.instanceClazz, context.factoryClazz, context.statefulFactoryClazz,
PainlessLookupBuilder.buildFromWhitelists(entry.getValue())));
PainlessLookup lookup = PainlessLookupBuilder.buildFromWhitelists(entry.getValue());
contextsToCompilers.put(context,
new Compiler(context.instanceClazz, context.factoryClazz, context.statefulFactoryClazz, lookup));
contextsToLookups.put(context, lookup);
}

this.contextsToCompilers = Collections.unmodifiableMap(contextsToCompilers);
this.contextsToLookups = Collections.unmodifiableMap(contextsToLookups);
}

public Map<ScriptContext<?>, PainlessLookup> getContextsToLookups() {
return contextsToLookups;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
* 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.action;

import org.elasticsearch.action.Action;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
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.node.NodeClient;
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.io.stream.Writeable;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.painless.PainlessScriptEngine;
import org.elasticsearch.painless.lookup.PainlessLookup;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.action.RestToXContentListener;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.TransportService;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import static org.elasticsearch.rest.RestRequest.Method.GET;

/**
* Internal REST API for querying context information about Painless whitelists.
* Commands include the following:
* <ul>
* <li> GET /_scripts/painless/_context -- retrieves a list of contexts </li>
* <li> GET /_scripts/painless/_context?context=%name% --
* retrieves all available information about the API for this specific context</li>
* </ul>
*/
public class PainlessContextAction extends Action<PainlessContextAction.Response> {

public static final PainlessContextAction INSTANCE = new PainlessContextAction();
private static final String NAME = "cluster:admin/scripts/painless/context";

private static final String SCRIPT_CONTEXT_NAME_PARAM = "context";

private PainlessContextAction() {
super(NAME);
}

@Override
public Response newResponse() {
throw new UnsupportedOperationException();
}

@Override
public Writeable.Reader<Response> getResponseReader() {
return Response::new;
}

public static class Request extends ActionRequest {

private String scriptContextName;

public Request() {
scriptContextName = null;
}

public Request(StreamInput in) throws IOException {
super(in);
scriptContextName = in.readString();
}

public void setScriptContextName(String scriptContextName) {
this.scriptContextName = scriptContextName;
}

public String getScriptContextName() {
return scriptContextName;
}

@Override
public ActionRequestValidationException validate() {
return null;
}

@Override
public void readFrom(StreamInput in) {
throw new UnsupportedOperationException();
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(scriptContextName);
}
}

public static class Response extends ActionResponse implements ToXContentObject {

public static final ParseField CONTEXTS = new ParseField("contexts");

private final List<String> scriptContextNames;
private final PainlessContextInfo painlessContextInfo;

public Response(List<String> scriptContextNames, PainlessContextInfo painlessContextInfo) {
Objects.requireNonNull(scriptContextNames);
scriptContextNames = new ArrayList<>(scriptContextNames);
scriptContextNames.sort(String::compareTo);
this.scriptContextNames = Collections.unmodifiableList(scriptContextNames);
this.painlessContextInfo = painlessContextInfo;
}

public Response(StreamInput in) throws IOException {
super(in);
scriptContextNames = in.readStringList();
painlessContextInfo = in.readOptionalWriteable(PainlessContextInfo::new);
}

@Override
public void readFrom(StreamInput in) {
throw new UnsupportedOperationException();
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeStringCollection(scriptContextNames);
out.writeOptionalWriteable(painlessContextInfo);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
if (painlessContextInfo == null) {
builder.startObject();
builder.field(CONTEXTS.getPreferredName(), scriptContextNames);
builder.endObject();
} else {
painlessContextInfo.toXContent(builder, params);
}

return builder;
}
}

public static class TransportAction extends HandledTransportAction<Request, Response> {

private final PainlessScriptEngine painlessScriptEngine;

@Inject
public TransportAction(TransportService transportService, ActionFilters actionFilters, PainlessScriptEngine painlessScriptEngine) {
super(NAME, transportService, actionFilters, (Writeable.Reader<Request>)Request::new);
this.painlessScriptEngine = painlessScriptEngine;
}

@Override
protected void doExecute(Task task, Request request, ActionListener<Response> listener) {
List<String> scriptContextNames;
PainlessContextInfo painlessContextInfo;

if (request.scriptContextName == null) {
scriptContextNames =
painlessScriptEngine.getContextsToLookups().keySet().stream().map(v -> v.name).collect(Collectors.toList());
painlessContextInfo = null;
} else {
ScriptContext<?> scriptContext = null;
PainlessLookup painlessLookup = null;

for (Map.Entry<ScriptContext<?>, PainlessLookup> contextLookupEntry :
painlessScriptEngine.getContextsToLookups().entrySet()) {
if (contextLookupEntry.getKey().name.equals(request.getScriptContextName())) {
scriptContext = contextLookupEntry.getKey();
painlessLookup = contextLookupEntry.getValue();
break;
}
}

if (scriptContext == null || painlessLookup == null) {
throw new IllegalArgumentException("script context [" + request.getScriptContextName() + "] not found");
}

scriptContextNames = Collections.emptyList();
painlessContextInfo = new PainlessContextInfo(scriptContext, painlessLookup);
}

listener.onResponse(new Response(scriptContextNames, painlessContextInfo));
}
}

public static class RestAction extends BaseRestHandler {

public RestAction(Settings settings, RestController controller) {
super(settings);
controller.registerHandler(GET, "/_scripts/painless/_context", this);
}

@Override
public String getName() {
return "_scripts_painless_context";
}

@Override
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) {
Request request = new Request();
request.setScriptContextName(restRequest.param(SCRIPT_CONTEXT_NAME_PARAM));
return channel -> client.executeLocally(INSTANCE, request, new RestToXContentListener<>(channel));
}
}
}
Loading

0 comments on commit b57af6c

Please sign in to comment.