Skip to content
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

Allow plugins to define custom operations that they use scripts for #10419

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 11 additions & 3 deletions docs/reference/modules/scripting.asciidoc
Expand Up @@ -302,12 +302,17 @@ supported operations are:
| `mapping` |Mappings (script transform feature)
| `search` |Search api, Percolator api and Suggester api (e.g filters, script_fields)
| `update` |Update api
| `plugin` |Any plugin that makes use of scripts under the generic `plugin` category
|=======================================================================

The following example disables scripting for `update` and `mapping` operations,
Plugins can also define custom operations that they use scripts for instead
of using the generic `plugin` category. Those operations can be referred to
in the following form: `$pluginName_$operation`.

The following example disables scripting for `upnother rounddate` and `mapping` operations,
regardless of the script source, for any engine. Scripts can still be
executed from sandboxed languages as part of `aggregations` and `search`
operations though, as the above defaults still get applied.
executed from sandboxed languages as part of `aggregations`, `search`
and `plugin` operations though, as the above defaults still get applied.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we add a NOTE: here about custom plugin contexts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugins are briefly mentioned a few lines above:

Plugins can also define custom operations that they use scripts for instead of using the generic plugin category. Those operations can be referred to in the following form: $pluginName_$operation.

That said I haven't explained how plugins can do it, but we don't really have reference for plugins so maybe this is enough?

[source,yaml]
-----------------------------------
Expand All @@ -327,14 +332,17 @@ script.engine.groovy.file.aggs: on
script.engine.groovy.file.mapping: on
script.engine.groovy.file.search: on
script.engine.groovy.file.update: on
script.engine.groovy.file.plugin: on
script.engine.groovy.indexed.aggs: on
script.engine.groovy.indexed.mapping: off
script.engine.groovy.indexed.search: on
script.engine.groovy.indexed.update: off
script.engine.groovy.indexed.plugin: off
script.engine.groovy.inline.aggs: on
script.engine.groovy.inline.mapping: off
script.engine.groovy.inline.search: off
script.engine.groovy.inline.update: off
script.engine.groovy.inline.plugin: off

-----------------------------------

Expand Down
Expand Up @@ -42,11 +42,11 @@
import org.elasticsearch.index.mapper.internal.RoutingFieldMapper;
import org.elasticsearch.index.mapper.internal.TTLFieldMapper;
import org.elasticsearch.index.mapper.internal.TimestampFieldMapper;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.index.shard.IndexShard;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.search.fetch.source.FetchSourceContext;
import org.elasticsearch.search.lookup.SourceLookup;

Expand Down Expand Up @@ -94,7 +94,7 @@ public Result prepare(UpdateRequest request, IndexShard indexShard) {
ctx.put("op", "create");
ctx.put("_source", upsertDoc);
try {
ExecutableScript script = scriptService.executable(request.scriptLang, request.script, request.scriptType, ScriptContext.UPDATE, request.scriptParams);
ExecutableScript script = scriptService.executable(request.scriptLang, request.script, request.scriptType, ScriptContext.Standard.UPDATE, request.scriptParams);
script.setNextVar("ctx", ctx);
script.run();
// we need to unwrap the ctx...
Expand Down Expand Up @@ -193,7 +193,7 @@ public Result prepare(UpdateRequest request, IndexShard indexShard) {
ctx.put("_source", sourceAndContent.v2());

try {
ExecutableScript script = scriptService.executable(request.scriptLang, request.script, request.scriptType, ScriptContext.UPDATE, request.scriptParams);
ExecutableScript script = scriptService.executable(request.scriptLang, request.script, request.scriptType, ScriptContext.Standard.UPDATE, request.scriptParams);
script.setNextVar("ctx", ctx);
script.run();
// we need to unwrap the ctx...
Expand Down
Expand Up @@ -22,7 +22,6 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
Expand Down Expand Up @@ -51,9 +50,9 @@
import org.elasticsearch.index.mapper.object.ObjectMapper;
import org.elasticsearch.index.mapper.object.RootObjectMapper;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.script.ScriptService.ScriptType;
import org.elasticsearch.script.ScriptContext;

import java.io.IOException;
import java.util.*;
Expand Down Expand Up @@ -849,7 +848,7 @@ public ScriptTransform(ScriptService scriptService, String script, ScriptType sc
public Map<String, Object> transformSourceAsMap(Map<String, Object> sourceAsMap) {
try {
// We use the ctx variable and the _source name to be consistent with the update api.
ExecutableScript executable = scriptService.executable(language, script, scriptType, ScriptContext.MAPPING, parameters);
ExecutableScript executable = scriptService.executable(language, script, scriptType, ScriptContext.Standard.MAPPING, parameters);
Map<String, Object> ctx = new HashMap<>(1);
ctx.put("_source", sourceAsMap);
executable.setNextVar("ctx", ctx);
Expand Down
Expand Up @@ -133,7 +133,7 @@ public static class ScriptFilter extends Filter {
public ScriptFilter(String scriptLang, String script, ScriptService.ScriptType scriptType, Map<String, Object> params, ScriptService scriptService, SearchLookup searchLookup) {
this.script = script;
this.params = params;
this.searchScript = scriptService.search(searchLookup, scriptLang, script, scriptType, ScriptContext.SEARCH, newHashMap(params));
this.searchScript = scriptService.search(searchLookup, scriptLang, script, scriptType, ScriptContext.Standard.SEARCH, newHashMap(params));
}

@Override
Expand Down
Expand Up @@ -26,8 +26,8 @@
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.script.mustache.MustacheScriptEngineService;

import java.io.IOException;
Expand Down Expand Up @@ -77,7 +77,7 @@ public String[] names() {
public Query parse(QueryParseContext parseContext) throws IOException {
XContentParser parser = parseContext.parser();
TemplateContext templateContext = parse(parser, PARAMS, parametersToTypes);
ExecutableScript executable = this.scriptService.executable(MustacheScriptEngineService.NAME, templateContext.template(), templateContext.scriptType(), ScriptContext.SEARCH, templateContext.params());
ExecutableScript executable = this.scriptService.executable(MustacheScriptEngineService.NAME, templateContext.template(), templateContext.scriptType(), ScriptContext.Standard.SEARCH, templateContext.params());

BytesReference querySource = (BytesReference) executable.run();

Expand Down
Expand Up @@ -87,7 +87,7 @@ public ScoreFunction parse(QueryParseContext parseContext, XContentParser parser

SearchScript searchScript;
try {
searchScript = parseContext.scriptService().search(parseContext.lookup(), scriptParameterParser.lang(), script, scriptType, ScriptContext.SEARCH, vars);
searchScript = parseContext.scriptService().search(parseContext.lookup(), scriptParameterParser.lang(), script, scriptType, ScriptContext.Standard.SEARCH, vars);
return new ScriptScoreFunction(script, vars, searchScript);
} catch (Exception e) {
throw new QueryParsingException(parseContext.index(), NAMES[0] + " the script could not be loaded", e);
Expand Down
89 changes: 77 additions & 12 deletions src/main/java/org/elasticsearch/script/ScriptContext.java
Expand Up @@ -19,20 +19,85 @@

package org.elasticsearch.script;

import java.util.Locale;
import org.elasticsearch.ElasticsearchIllegalArgumentException;
import org.elasticsearch.common.Strings;

/**
* Operation/api that uses a script as part of its execution.
* Note that the suggest api is considered part of search for simplicity, as well as the percolate api.
* Context of an operation that uses scripts as part of its execution.
*/
public enum ScriptContext {
MAPPING,
UPDATE,
SEARCH,
AGGS;

@Override
public String toString() {
return name().toLowerCase(Locale.ROOT);
public interface ScriptContext {

/**
* @return the name of the operation
*/
String getKey();

/**
* Standard operations that make use of scripts as part of their execution.
* Note that the suggest api is considered part of search for simplicity, as well as the percolate api.
*/
enum Standard implements ScriptContext {

AGGS("aggs"), MAPPING("mapping"), SEARCH("search"), UPDATE("update"),
/**
* Generic custom operation exposed via plugin
*
* @deprecated create a new {@link org.elasticsearch.script.ScriptContext.Plugin} instance instead
*/
@Deprecated
GENERIC_PLUGIN("plugin");

private final String key;

Standard(String key) {
this.key = key;
}

@Override
public String getKey() {
return key;
}
}

/**
* Custom operation exposed via plugin, which makes use of scripts as part of its execution
*/
final class Plugin implements ScriptContext {

private final String pluginName;
private final String operation;
private final String key;

/**
* Creates a new custom scripts based operation exposed via plugin.
* The name of the plugin combined with the operation name can be used to enable/disable scripts via fine-grained settings.
*
* @param pluginName the name of the plugin
* @param operation the name of the operation
*/
public Plugin(String pluginName, String operation) {
if (Strings.hasLength(pluginName) == false) {
throw new ElasticsearchIllegalArgumentException("plugin name cannot be empty when registering a custom script context");
}
if (Strings.hasLength(operation) == false) {
throw new ElasticsearchIllegalArgumentException("operation name cannot be empty when registering a custom script context");
}
this.pluginName = pluginName;
this.operation = operation;
this.key = pluginName + "_" + operation;
}

public String getPluginName() {
return pluginName;
}

public String getOperation() {
return operation;
}

@Override
public final String getKey() {
return key;
}
}
}
90 changes: 90 additions & 0 deletions src/main/java/org/elasticsearch/script/ScriptContextRegistry.java
@@ -0,0 +1,90 @@
/*
* 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.script;

import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import org.elasticsearch.ElasticsearchIllegalArgumentException;

import java.util.Map;

/**
* Registry for operations that use scripts as part of their execution. Can be standard operations of custom defined ones (via plugin).
* Allows plugins to register custom operations that they use scripts for, via {@link ScriptModule#registerScriptContext(org.elasticsearch.script.ScriptContext.Plugin)}.
* Scripts can be enabled/disabled via fine-grained settings for each single registered operation.
*/
public final class ScriptContextRegistry {
static final ImmutableSet<String> RESERVED_SCRIPT_CONTEXTS = reservedScriptContexts();

private final ImmutableMap<String, ScriptContext> scriptContexts;

ScriptContextRegistry(Iterable<ScriptContext.Plugin> customScriptContexts) {
Map<String, ScriptContext> scriptContexts = Maps.newHashMap();
for (ScriptContext.Standard scriptContext : ScriptContext.Standard.values()) {
scriptContexts.put(scriptContext.getKey(), scriptContext);
}
for (ScriptContext.Plugin customScriptContext : customScriptContexts) {
validateScriptContext(customScriptContext);
ScriptContext previousContext = scriptContexts.put(customScriptContext.getKey(), customScriptContext);
if (previousContext != null) {
throw new ElasticsearchIllegalArgumentException("script context [" + customScriptContext.getKey() + "] cannot be registered twice");
}
}
this.scriptContexts = ImmutableMap.copyOf(scriptContexts);
}

/**
* @return a list that contains all the supported {@link ScriptContext}s, both standard ones and registered via plugins
*/
ImmutableCollection<ScriptContext> scriptContexts() {
return scriptContexts.values();
}

/**
* @return <tt>true</tt> if the provided {@link ScriptContext} is supported, <tt>false</tt> otherwise
*/
boolean isSupportedContext(ScriptContext scriptContext) {
return scriptContexts.containsKey(scriptContext.getKey());
}

//script contexts can be used in fine-grained settings, we need to be careful with what we allow here
private void validateScriptContext(ScriptContext.Plugin scriptContext) {
if (RESERVED_SCRIPT_CONTEXTS.contains(scriptContext.getPluginName())) {
throw new ElasticsearchIllegalArgumentException("[" + scriptContext.getPluginName() + "] is a reserved name, it cannot be registered as a custom script context");
}
if (RESERVED_SCRIPT_CONTEXTS.contains(scriptContext.getOperation())) {
throw new ElasticsearchIllegalArgumentException("[" + scriptContext.getOperation() + "] is a reserved name, it cannot be registered as a custom script context");
}
}

private static ImmutableSet<String> reservedScriptContexts() {
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
for (ScriptService.ScriptType scriptType : ScriptService.ScriptType.values()) {
builder.add(scriptType.toString());
}
for (ScriptContext.Standard scriptContext : ScriptContext.Standard.values()) {
builder.add(scriptContext.getKey());
}
builder.add("script").add("engine");
return builder.build();
}
}