Scripting: add available languages & contexts API (#49652) (#49815)

Adds `GET /_script_language` to support Kibana dynamic scripting
language selection.

Response contains whether `inline` and/or `stored` scripts are
enabled as determined by the `script.allowed_types` settings.

For each scripting language registered, such as `painless`,
`expression`, `mustache` or custom, available contexts for the language
are included as determined by the `script.allowed_contexts` setting.

Response format:
```
{
  "types_allowed": [
    "inline",
    "stored"
  ],
  "language_contexts": [
    {
      "language": "expression",
      "contexts": [
        "aggregation_selector",
        "aggs"
        ...
      ]
    },
    {
      "language": "painless",
      "contexts": [
        "aggregation_selector",
        "aggs",
        "aggs_combine",
        ...
      ]
    }
...
  ]
}
```

Fixes: #49463 

**Backport**
This commit is contained in:
Stuart Tettemer 2019-12-04 16:18:22 -07:00 committed by GitHub
parent dbf6183469
commit 426c7a5e8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 866 additions and 37 deletions

View File

@ -765,6 +765,7 @@ public class RestHighLevelClientTests extends ESTestCase {
"cluster.remote_info",
"create",
"get_script_context",
"get_script_languages",
"get_source",
"indices.exists_type",
"indices.get_upgrade",

View File

@ -52,9 +52,12 @@ import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
/**
* Provides the infrastructure for Lucene expressions as a scripting language for Elasticsearch.
@ -65,6 +68,45 @@ public class ExpressionScriptEngine implements ScriptEngine {
public static final String NAME = "expression";
private static Map<ScriptContext<?>, Function<Expression,Object>> contexts;
static {
Map<ScriptContext<?>, Function<Expression,Object>> contexts = new HashMap<ScriptContext<?>, Function<Expression,Object>>();
contexts.put(BucketAggregationScript.CONTEXT,
ExpressionScriptEngine::newBucketAggregationScriptFactory);
contexts.put(BucketAggregationSelectorScript.CONTEXT,
(Expression expr) -> {
BucketAggregationScript.Factory factory = newBucketAggregationScriptFactory(expr);
BucketAggregationSelectorScript.Factory wrappedFactory = parameters -> new BucketAggregationSelectorScript(parameters) {
@Override
public boolean execute() {
return factory.newInstance(getParams()).execute().doubleValue() == 1.0;
}
};
return wrappedFactory; });
contexts.put(FilterScript.CONTEXT,
(Expression expr) -> (FilterScript.Factory) (p, lookup) -> newFilterScript(expr, lookup, p));
contexts.put(ScoreScript.CONTEXT,
(Expression expr) -> (ScoreScript.Factory) (p, lookup) -> newScoreScript(expr, lookup, p));
contexts.put(TermsSetQueryScript.CONTEXT,
(Expression expr) -> (TermsSetQueryScript.Factory) (p, lookup) -> newTermsSetQueryScript(expr, lookup, p));
contexts.put(AggregationScript.CONTEXT,
(Expression expr) -> (AggregationScript.Factory) (p, lookup) -> newAggregationScript(expr, lookup, p));
contexts.put(NumberSortScript.CONTEXT,
(Expression expr) -> (NumberSortScript.Factory) (p, lookup) -> newSortScript(expr, lookup, p));
contexts.put(FieldScript.CONTEXT,
(Expression expr) -> (FieldScript.Factory) (p, lookup) -> newFieldScript(expr, lookup, p));
ExpressionScriptEngine.contexts = Collections.unmodifiableMap(contexts);
}
@Override
public String getType() {
return NAME;
@ -102,37 +144,15 @@ public class ExpressionScriptEngine implements ScriptEngine {
}
}
});
if (context.instanceClazz.equals(BucketAggregationScript.class)) {
return context.factoryClazz.cast(newBucketAggregationScriptFactory(expr));
} else if (context.instanceClazz.equals(BucketAggregationSelectorScript.class)) {
BucketAggregationScript.Factory factory = newBucketAggregationScriptFactory(expr);
BucketAggregationSelectorScript.Factory wrappedFactory = parameters -> new BucketAggregationSelectorScript(parameters) {
@Override
public boolean execute() {
return factory.newInstance(getParams()).execute().doubleValue() == 1.0;
}
};
return context.factoryClazz.cast(wrappedFactory);
} else if (context.instanceClazz.equals(FilterScript.class)) {
FilterScript.Factory factory = (p, lookup) -> newFilterScript(expr, lookup, p);
return context.factoryClazz.cast(factory);
} else if (context.instanceClazz.equals(ScoreScript.class)) {
ScoreScript.Factory factory = (p, lookup) -> newScoreScript(expr, lookup, p);
return context.factoryClazz.cast(factory);
} else if (context.instanceClazz.equals(TermsSetQueryScript.class)) {
TermsSetQueryScript.Factory factory = (p, lookup) -> newTermsSetQueryScript(expr, lookup, p);
return context.factoryClazz.cast(factory);
} else if (context.instanceClazz.equals(AggregationScript.class)) {
AggregationScript.Factory factory = (p, lookup) -> newAggregationScript(expr, lookup, p);
return context.factoryClazz.cast(factory);
} else if (context.instanceClazz.equals(NumberSortScript.class)) {
NumberSortScript.Factory factory = (p, lookup) -> newSortScript(expr, lookup, p);
return context.factoryClazz.cast(factory);
} else if (context.instanceClazz.equals(FieldScript.class)) {
FieldScript.Factory factory = (p, lookup) -> newFieldScript(expr, lookup, p);
return context.factoryClazz.cast(factory);
if (contexts.containsKey(context) == false) {
throw new IllegalArgumentException("expression engine does not know how to handle script context [" + context.name + "]");
}
throw new IllegalArgumentException("expression engine does not know how to handle script context [" + context.name + "]");
return context.factoryClazz.cast(contexts.get(context).apply(expr));
}
@Override
public Set<ScriptContext<?>> getSupportedContexts() {
return contexts.keySet();
}
private static BucketAggregationScript.Factory newBucketAggregationScriptFactory(Expression expr) {
@ -166,7 +186,7 @@ public class ExpressionScriptEngine implements ScriptEngine {
};
}
private NumberSortScript.LeafFactory newSortScript(Expression expr, SearchLookup lookup, @Nullable Map<String, Object> vars) {
private static NumberSortScript.LeafFactory newSortScript(Expression expr, SearchLookup lookup, @Nullable Map<String, Object> vars) {
// NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings,
// instead of complicating SimpleBindings (which should stay simple)
SimpleBindings bindings = new SimpleBindings();
@ -193,7 +213,7 @@ public class ExpressionScriptEngine implements ScriptEngine {
return new ExpressionNumberSortScript(expr, bindings, needsScores);
}
private TermsSetQueryScript.LeafFactory newTermsSetQueryScript(Expression expr, SearchLookup lookup,
private static TermsSetQueryScript.LeafFactory newTermsSetQueryScript(Expression expr, SearchLookup lookup,
@Nullable Map<String, Object> vars) {
// NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings,
// instead of complicating SimpleBindings (which should stay simple)
@ -216,7 +236,7 @@ public class ExpressionScriptEngine implements ScriptEngine {
return new ExpressionTermSetQueryScript(expr, bindings);
}
private AggregationScript.LeafFactory newAggregationScript(Expression expr, SearchLookup lookup,
private static AggregationScript.LeafFactory newAggregationScript(Expression expr, SearchLookup lookup,
@Nullable Map<String, Object> vars) {
// NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings,
// instead of complicating SimpleBindings (which should stay simple)
@ -252,7 +272,7 @@ public class ExpressionScriptEngine implements ScriptEngine {
return new ExpressionAggregationScript(expr, bindings, needsScores, specialValue);
}
private FieldScript.LeafFactory newFieldScript(Expression expr, SearchLookup lookup, @Nullable Map<String, Object> vars) {
private static FieldScript.LeafFactory newFieldScript(Expression expr, SearchLookup lookup, @Nullable Map<String, Object> vars) {
SimpleBindings bindings = new SimpleBindings();
for (String variable : expr.variables) {
try {
@ -273,7 +293,7 @@ public class ExpressionScriptEngine implements ScriptEngine {
* This is a hack for filter scripts, which must return booleans instead of doubles as expression do.
* See https://github.com/elastic/elasticsearch/issues/26429.
*/
private FilterScript.LeafFactory newFilterScript(Expression expr, SearchLookup lookup, @Nullable Map<String, Object> vars) {
private static FilterScript.LeafFactory newFilterScript(Expression expr, SearchLookup lookup, @Nullable Map<String, Object> vars) {
ScoreScript.LeafFactory searchLeafFactory = newScoreScript(expr, lookup, vars);
return ctx -> {
ScoreScript script = searchLeafFactory.newInstance(ctx);
@ -290,7 +310,7 @@ public class ExpressionScriptEngine implements ScriptEngine {
};
}
private ScoreScript.LeafFactory newScoreScript(Expression expr, SearchLookup lookup, @Nullable Map<String, Object> vars) {
private static ScoreScript.LeafFactory newScoreScript(Expression expr, SearchLookup lookup, @Nullable Map<String, Object> vars) {
// NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings,
// instead of complicating SimpleBindings (which should stay simple)
SimpleBindings bindings = new SimpleBindings();
@ -327,7 +347,7 @@ public class ExpressionScriptEngine implements ScriptEngine {
/**
* converts a ParseException at compile-time or link-time to a ScriptException
*/
private ScriptException convertToScriptException(String message, String source, String portion, Throwable cause) {
private static ScriptException convertToScriptException(String message, String source, String portion, Throwable cause) {
List<String> stack = new ArrayList<>();
stack.add(portion);
StringBuilder pointer = new StringBuilder();

View File

@ -41,6 +41,7 @@ import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
/**
* Main entry point handling template registration, compilation and
@ -79,6 +80,11 @@ public final class MustacheScriptEngine implements ScriptEngine {
}
@Override
public Set<ScriptContext<?>> getSupportedContexts() {
return Collections.singleton(TemplateScript.CONTEXT);
}
private CustomMustacheFactory createMustacheFactory(Map<String, String> options) {
if (options == null || options.isEmpty() || options.containsKey(Script.CONTENT_TYPE_OPTION) == false) {
return new CustomMustacheFactory();

View File

@ -146,6 +146,11 @@ public final class PainlessScriptEngine implements ScriptEngine {
}
}
@Override
public Set<ScriptContext<?>> getSupportedContexts() {
return contextsToCompilers.keySet();
}
/**
* Generates a stateful factory class that will return script instances. Acts as a middle man between
* the {@link ScriptContext#factoryClazz} and the {@link ScriptContext#instanceClazz} when used so that

View File

@ -34,7 +34,9 @@ import org.elasticsearch.search.lookup.SearchLookup;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
/**
* An example script plugin that adds a {@link ScriptEngine} implementing expert scoring.
@ -76,6 +78,11 @@ public class ExpertScriptPlugin extends Plugin implements ScriptPlugin {
// optionally close resources
}
@Override
public Set<ScriptContext<?>> getSupportedContexts() {
return Collections.singleton(ScoreScript.CONTEXT);
}
private static class PureDfLeafFactory implements LeafFactory {
private final Map<String, Object> params;
private final SearchLookup lookup;

View File

@ -0,0 +1,19 @@
{
"get_script_languages":{
"documentation":{
"description":"Returns available script types, languages and contexts"
},
"stability":"experimental",
"url":{
"paths":[
{
"path":"/_script_language",
"methods":[
"GET"
]
}
]
},
"params":{}
}
}

View File

@ -0,0 +1,9 @@
"Action to get script languages":
- skip:
version: " - 7.6.0"
reason: "get_script_languages introduced in 7.6.0"
- do:
get_script_languages: {}
- match: { types_allowed.0: "inline" }
- match: { types_allowed.1: "stored" }

View File

@ -80,10 +80,12 @@ import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction;
import org.elasticsearch.action.admin.cluster.stats.TransportClusterStatsAction;
import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptAction;
import org.elasticsearch.action.admin.cluster.storedscripts.GetScriptContextAction;
import org.elasticsearch.action.admin.cluster.storedscripts.GetScriptLanguageAction;
import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptAction;
import org.elasticsearch.action.admin.cluster.storedscripts.PutStoredScriptAction;
import org.elasticsearch.action.admin.cluster.storedscripts.TransportDeleteStoredScriptAction;
import org.elasticsearch.action.admin.cluster.storedscripts.TransportGetScriptContextAction;
import org.elasticsearch.action.admin.cluster.storedscripts.TransportGetScriptLanguageAction;
import org.elasticsearch.action.admin.cluster.storedscripts.TransportGetStoredScriptAction;
import org.elasticsearch.action.admin.cluster.storedscripts.TransportPutStoredScriptAction;
import org.elasticsearch.action.admin.cluster.tasks.PendingClusterTasksAction;
@ -248,6 +250,7 @@ import org.elasticsearch.rest.action.admin.cluster.RestDeleteSnapshotAction;
import org.elasticsearch.rest.action.admin.cluster.RestDeleteStoredScriptAction;
import org.elasticsearch.rest.action.admin.cluster.RestGetRepositoriesAction;
import org.elasticsearch.rest.action.admin.cluster.RestGetScriptContextAction;
import org.elasticsearch.rest.action.admin.cluster.RestGetScriptLanguageAction;
import org.elasticsearch.rest.action.admin.cluster.RestGetSnapshotsAction;
import org.elasticsearch.rest.action.admin.cluster.RestGetStoredScriptAction;
import org.elasticsearch.rest.action.admin.cluster.RestGetTaskAction;
@ -531,6 +534,7 @@ public class ActionModule extends AbstractModule {
actions.register(GetStoredScriptAction.INSTANCE, TransportGetStoredScriptAction.class);
actions.register(DeleteStoredScriptAction.INSTANCE, TransportDeleteStoredScriptAction.class);
actions.register(GetScriptContextAction.INSTANCE, TransportGetScriptContextAction.class);
actions.register(GetScriptLanguageAction.INSTANCE, TransportGetScriptLanguageAction.class);
actions.register(FieldCapabilitiesAction.INSTANCE, TransportFieldCapabilitiesAction.class,
TransportFieldCapabilitiesIndexAction.class);
@ -661,6 +665,7 @@ public class ActionModule extends AbstractModule {
registerHandler.accept(new RestPutStoredScriptAction(restController));
registerHandler.accept(new RestDeleteStoredScriptAction(restController));
registerHandler.accept(new RestGetScriptContextAction(restController));
registerHandler.accept(new RestGetScriptLanguageAction(restController));
registerHandler.accept(new RestFieldCapabilitiesAction(restController));

View File

@ -0,0 +1,31 @@
/*
* 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.action.admin.cluster.storedscripts;
import org.elasticsearch.action.ActionType;
public class GetScriptLanguageAction extends ActionType<GetScriptLanguageResponse> {
public static final GetScriptLanguageAction INSTANCE = new GetScriptLanguageAction();
public static final String NAME = "cluster:admin/script_language/get";
private GetScriptLanguageAction() {
super(NAME, GetScriptLanguageResponse::new);
}
}

View File

@ -0,0 +1,42 @@
/*
* 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.action.admin.cluster.storedscripts;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.StreamInput;
import java.io.IOException;
public class GetScriptLanguageRequest extends ActionRequest {
public GetScriptLanguageRequest() {
super();
}
GetScriptLanguageRequest(StreamInput in) throws IOException {
super(in);
}
@Override
public ActionRequestValidationException validate() { return null; }
@Override
public String toString() { return "get script languages"; }
}

View File

@ -0,0 +1,78 @@
/*
* 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.action.admin.cluster.storedscripts;
import org.elasticsearch.action.ActionResponse;
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.xcontent.StatusToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.script.ScriptLanguagesInfo;
import java.io.IOException;
import java.util.Objects;
public class GetScriptLanguageResponse extends ActionResponse implements StatusToXContentObject, Writeable {
public final ScriptLanguagesInfo info;
GetScriptLanguageResponse(ScriptLanguagesInfo info) {
this.info = info;
}
GetScriptLanguageResponse(StreamInput in) throws IOException {
super(in);
info = new ScriptLanguagesInfo(in);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
info.writeTo(out);
}
@Override
public RestStatus status() {
return RestStatus.OK;
}
public static GetScriptLanguageResponse fromXContent(XContentParser parser) throws IOException {
return new GetScriptLanguageResponse(ScriptLanguagesInfo.fromXContent(parser));
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
GetScriptLanguageResponse that = (GetScriptLanguageResponse) o;
return info.equals(that.info);
}
@Override
public int hashCode() { return Objects.hash(info); }
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return info.toXContent(builder, params);
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.action.admin.cluster.storedscripts;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.TransportService;
public class TransportGetScriptLanguageAction extends HandledTransportAction<GetScriptLanguageRequest, GetScriptLanguageResponse> {
private final ScriptService scriptService;
@Inject
public TransportGetScriptLanguageAction(TransportService transportService, ActionFilters actionFilters, ScriptService scriptService) {
super(GetScriptLanguageAction.NAME, transportService, actionFilters, GetScriptLanguageRequest::new);
this.scriptService = scriptService;
}
@Override
protected void doExecute(Task task, GetScriptLanguageRequest request, ActionListener<GetScriptLanguageResponse> listener) {
listener.onResponse(new GetScriptLanguageResponse(scriptService.getScriptLanguages()));
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.rest.action.admin.cluster;
import org.elasticsearch.action.admin.cluster.storedscripts.GetScriptLanguageAction;
import org.elasticsearch.action.admin.cluster.storedscripts.GetScriptLanguageRequest;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.action.RestToXContentListener;
import java.io.IOException;
import static org.elasticsearch.rest.RestRequest.Method.GET;
public class RestGetScriptLanguageAction extends BaseRestHandler {
@Inject
public RestGetScriptLanguageAction(RestController controller) {
controller.registerHandler(GET, "/_script_language", this);
}
@Override public String getName() {
return "script_language_action";
}
@Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
return channel -> client.execute(GetScriptLanguageAction.INSTANCE,
new GetScriptLanguageRequest(),
new RestToXContentListener<>(channel));
}
}

View File

@ -22,6 +22,7 @@ package org.elasticsearch.script;
import java.io.Closeable;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
/**
* A script language implementation.
@ -45,4 +46,9 @@ public interface ScriptEngine extends Closeable {
@Override
default void close() throws IOException {}
/**
* Script contexts supported by this engine.
*/
Set<ScriptContext<?>> getSupportedContexts();
}

View File

@ -0,0 +1,170 @@
/*
* 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 org.elasticsearch.common.ParseField;
import org.elasticsearch.common.collect.Tuple;
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.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
/**
* The allowable types, languages and their corresponding contexts. When serialized there is a top level <code>types_allowed</code> list,
* meant to reflect the setting <code>script.allowed_types</code> with the allowed types (eg <code>inline</code>, <code>stored</code>).
*
* The top-level <code>language_contexts</code> list of objects have the <code>language</code> (eg. <code>painless</code>,
* <code>mustache</code>) and a list of <code>contexts</code> available for the language. It is the responsibility of the caller to ensure
* these contexts are filtered by the <code>script.allowed_contexts</code> setting.
*
* The json serialization of the object has the form:
* <code>
* {
* "types_allowed": [
* "inline",
* "stored"
* ],
* "language_contexts": [
* {
* "language": "expression",
* "contexts": [
* "aggregation_selector",
* "aggs"
* ...
* ]
* },
* {
* "language": "painless",
* "contexts": [
* "aggregation_selector",
* "aggs",
* "aggs_combine",
* ...
* ]
* }
* ...
* ]
* }
* </code>
*/
public class ScriptLanguagesInfo implements ToXContentObject, Writeable {
private static final ParseField TYPES_ALLOWED = new ParseField("types_allowed");
private static final ParseField LANGUAGE_CONTEXTS = new ParseField("language_contexts");
private static final ParseField LANGUAGE = new ParseField("language");
private static final ParseField CONTEXTS = new ParseField("contexts");
public final Set<String> typesAllowed;
public final Map<String,Set<String>> languageContexts;
public ScriptLanguagesInfo(Set<String> typesAllowed, Map<String,Set<String>> languageContexts) {
this.typesAllowed = typesAllowed != null ? Collections.unmodifiableSet(typesAllowed): Collections.emptySet();
this.languageContexts = languageContexts != null ? Collections.unmodifiableMap(languageContexts): Collections.emptyMap();
}
public ScriptLanguagesInfo(StreamInput in) throws IOException {
typesAllowed = in.readSet(StreamInput::readString);
languageContexts = in.readMap(StreamInput::readString, sin -> sin.readSet(StreamInput::readString));
}
@SuppressWarnings("unchecked")
public static ConstructingObjectParser<ScriptLanguagesInfo,Void> PARSER =
new ConstructingObjectParser<>("script_languages_info", true,
(a) -> new ScriptLanguagesInfo(
new HashSet<>((List<String>)a[0]),
((List<Tuple<String,Set<String>>>)a[1]).stream().collect(Collectors.toMap(Tuple::v1, Tuple::v2))
)
);
@SuppressWarnings("unchecked")
private static ConstructingObjectParser<Tuple<String,Set<String>>,Void> LANGUAGE_CONTEXT_PARSER =
new ConstructingObjectParser<>("language_contexts", true,
(m, name) -> new Tuple<>((String)m[0], Collections.unmodifiableSet(new HashSet<>((List<String>)m[1])))
);
static {
PARSER.declareStringArray(constructorArg(), TYPES_ALLOWED);
PARSER.declareObjectArray(constructorArg(), LANGUAGE_CONTEXT_PARSER, LANGUAGE_CONTEXTS);
LANGUAGE_CONTEXT_PARSER.declareString(constructorArg(), LANGUAGE);
LANGUAGE_CONTEXT_PARSER.declareStringArray(constructorArg(), CONTEXTS);
}
public static ScriptLanguagesInfo fromXContent(XContentParser parser) throws IOException {
return PARSER.parse(parser, null);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeStringCollection(typesAllowed);
out.writeMap(languageContexts, StreamOutput::writeString, StreamOutput::writeStringCollection);
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ScriptLanguagesInfo that = (ScriptLanguagesInfo) o;
return Objects.equals(typesAllowed, that.typesAllowed) &&
Objects.equals(languageContexts, that.languageContexts);
}
@Override
public int hashCode() {
return Objects.hash(typesAllowed, languageContexts);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject().startArray(TYPES_ALLOWED.getPreferredName());
for (String type: typesAllowed.stream().sorted().collect(Collectors.toList())) {
builder.value(type);
}
builder.endArray().startArray(LANGUAGE_CONTEXTS.getPreferredName());
List<Map.Entry<String,Set<String>>> languagesByName = languageContexts.entrySet().stream().sorted(
Map.Entry.comparingByKey()
).collect(Collectors.toList());
for (Map.Entry<String,Set<String>> languageContext: languagesByName) {
builder.startObject().field(LANGUAGE.getPreferredName(), languageContext.getKey()).startArray(CONTEXTS.getPreferredName());
for (String context: languageContext.getValue().stream().sorted().collect(Collectors.toList())) {
builder.value(context);
}
builder.endArray().endObject();
}
return builder.endArray().endObject();
}
}

View File

@ -52,12 +52,14 @@ import java.io.Closeable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
public class ScriptService implements Closeable, ClusterStateApplier {
@ -546,6 +548,26 @@ public class ScriptService implements Closeable, ClusterStateApplier {
return infos;
}
public ScriptLanguagesInfo getScriptLanguages() {
Set<String> types = typesAllowed;
if (types == null) {
types = new HashSet<>();
for (ScriptType type: ScriptType.values()) {
types.add(type.getName());
}
}
final Set<String> contexts = contextsAllowed != null ? contextsAllowed : this.contexts.keySet();
Map<String,Set<String>> languageContexts = new HashMap<>();
engines.forEach(
(key, value) -> languageContexts.put(
key,
value.getSupportedContexts().stream().map(c -> c.name).filter(contexts::contains).collect(Collectors.toSet())
)
);
return new ScriptLanguagesInfo(types, languageContexts);
}
public ScriptStats stats() {
return scriptMetrics.stats();
}

View File

@ -0,0 +1,136 @@
/*
* 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.action.admin.cluster.storedscripts;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.script.ScriptLanguagesInfo;
import org.elasticsearch.test.AbstractSerializingTestCase;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class GetScriptLanguageResponseTests extends AbstractSerializingTestCase<GetScriptLanguageResponse> {
private static int MAX_VALUES = 4;
private static final int MIN_LENGTH = 1;
private static final int MAX_LENGTH = 16;
@Override
protected GetScriptLanguageResponse createTestInstance() {
if (randomBoolean()) {
return new GetScriptLanguageResponse(
new ScriptLanguagesInfo(Collections.emptySet(), Collections.emptyMap())
);
}
return new GetScriptLanguageResponse(randomInstance());
}
@Override
protected GetScriptLanguageResponse doParseInstance(XContentParser parser) throws IOException {
return GetScriptLanguageResponse.fromXContent(parser);
}
@Override
protected Writeable.Reader<GetScriptLanguageResponse> instanceReader() { return GetScriptLanguageResponse::new; }
@Override
protected GetScriptLanguageResponse mutateInstance(GetScriptLanguageResponse instance) throws IOException {
switch (randomInt(2)) {
case 0:
// mutate typesAllowed
return new GetScriptLanguageResponse(
new ScriptLanguagesInfo(mutateStringSet(instance.info.typesAllowed), instance.info.languageContexts)
);
case 1:
// Add language
String language = randomValueOtherThanMany(
instance.info.languageContexts::containsKey,
() -> randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH)
);
Map<String,Set<String>> languageContexts = new HashMap<>();
instance.info.languageContexts.forEach(languageContexts::put);
languageContexts.put(language, randomStringSet(randomIntBetween(1, MAX_VALUES)));
return new GetScriptLanguageResponse(new ScriptLanguagesInfo(instance.info.typesAllowed, languageContexts));
default:
// Mutate languageContexts
Map<String,Set<String>> lc = new HashMap<>();
if (instance.info.languageContexts.size() == 0) {
lc.put(randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), randomStringSet(randomIntBetween(1, MAX_VALUES)));
} else {
int toModify = randomInt(instance.info.languageContexts.size()-1);
List<String> keys = new ArrayList<>(instance.info.languageContexts.keySet());
for (int i=0; i<keys.size(); i++) {
String key = keys.get(i);
Set<String> value = instance.info.languageContexts.get(keys.get(i));
if (i == toModify) {
value = mutateStringSet(instance.info.languageContexts.get(keys.get(i)));
}
lc.put(key, value);
}
}
return new GetScriptLanguageResponse(new ScriptLanguagesInfo(instance.info.typesAllowed, lc));
}
}
private static ScriptLanguagesInfo randomInstance() {
Map<String,Set<String>> contexts = new HashMap<>();
for (String context: randomStringSet(randomIntBetween(1, MAX_VALUES))) {
contexts.put(context, randomStringSet(randomIntBetween(1, MAX_VALUES)));
}
return new ScriptLanguagesInfo(randomStringSet(randomInt(MAX_VALUES)), contexts);
}
private static Set<String> randomStringSet(int numInstances) {
Set<String> rand = new HashSet<>(numInstances);
for (int i = 0; i < numInstances; i++) {
rand.add(randomValueOtherThanMany(rand::contains, () -> randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH)));
}
return rand;
}
private static Set<String> mutateStringSet(Set<String> strings) {
if (strings.isEmpty()) {
return Collections.singleton(randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH));
}
if (randomBoolean()) {
Set<String> updated = new HashSet<>(strings);
updated.add(randomValueOtherThanMany(updated::contains, () -> randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH)));
return updated;
} else {
List<String> sorted = strings.stream().sorted().collect(Collectors.toList());
int toRemove = randomInt(sorted.size() - 1);
Set<String> updated = new HashSet<>();
for (int i = 0; i < sorted.size(); i++) {
if (i != toRemove) {
updated.add(sorted.get(i));
}
}
return updated;
}
}
}

View File

@ -0,0 +1,134 @@
/*
* 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 org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ESTestCase;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class ScriptLanguagesInfoTests extends ESTestCase {
public void testEmptyTypesAllowedReturnsAllTypes() {
ScriptService ss = getMockScriptService(Settings.EMPTY);
ScriptLanguagesInfo info = ss.getScriptLanguages();
ScriptType[] types = ScriptType.values();
assertEquals(types.length, info.typesAllowed.size());
for(ScriptType type: types) {
assertTrue("[" + type.getName() + "] is allowed", info.typesAllowed.contains(type.getName()));
}
}
public void testSingleTypesAllowedReturnsThatType() {
for (ScriptType type: ScriptType.values()) {
ScriptService ss = getMockScriptService(
Settings.builder().put("script.allowed_types", type.getName()).build()
);
ScriptLanguagesInfo info = ss.getScriptLanguages();
assertEquals(1, info.typesAllowed.size());
assertTrue("[" + type.getName() + "] is allowed", info.typesAllowed.contains(type.getName()));
}
}
public void testBothTypesAllowedReturnsBothTypes() {
List<String> types = Arrays.stream(ScriptType.values()).map(ScriptType::getName).collect(Collectors.toList());
Settings.Builder settings = Settings.builder().putList("script.allowed_types", types);
ScriptService ss = getMockScriptService(settings.build());
ScriptLanguagesInfo info = ss.getScriptLanguages();
assertEquals(types.size(), info.typesAllowed.size());
for(String type: types) {
assertTrue("[" + type + "] is allowed", info.typesAllowed.contains(type));
}
}
private ScriptService getMockScriptService(Settings settings) {
MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME,
Collections.singletonMap("test_script", script -> 1),
Collections.emptyMap());
Map<String, ScriptEngine> engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine);
return new ScriptService(settings, engines, ScriptModule.CORE_CONTEXTS);
}
public interface MiscContext {
void execute();
Object newInstance();
}
public void testOnlyScriptEngineContextsReturned() {
MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME,
Collections.singletonMap("test_script", script -> 1),
Collections.emptyMap());
Map<String, ScriptEngine> engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine);
Map<String, ScriptContext<?>> mockContexts = scriptEngine.getSupportedContexts().stream().collect(Collectors.toMap(
c -> c.name,
Function.identity()
));
String miscContext = "misc_context";
assertFalse(mockContexts.containsKey(miscContext));
Map<String, ScriptContext<?>> mockAndMiscContexts = new HashMap<>(mockContexts);
mockAndMiscContexts.put(miscContext, new ScriptContext<>(miscContext, MiscContext.class));
ScriptService ss = new ScriptService(Settings.EMPTY, engines, mockAndMiscContexts);
ScriptLanguagesInfo info = ss.getScriptLanguages();
assertTrue(info.languageContexts.containsKey(MockScriptEngine.NAME));
assertEquals(1, info.languageContexts.size());
assertEquals(mockContexts.keySet(), info.languageContexts.get(MockScriptEngine.NAME));
}
public void testContextsAllowedSettingRespected() {
MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME,
Collections.singletonMap("test_script", script -> 1),
Collections.emptyMap());
Map<String, ScriptEngine> engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine);
Map<String, ScriptContext<?>> mockContexts = scriptEngine.getSupportedContexts().stream().collect(Collectors.toMap(
c -> c.name,
Function.identity()
));
List<String> allContexts = new ArrayList<>(mockContexts.keySet());
List<String> allowed = allContexts.subList(0, allContexts.size()/2);
String miscContext = "misc_context";
allowed.add(miscContext);
// check that allowing more than available doesn't pollute the returned contexts
Settings.Builder settings = Settings.builder().putList("script.allowed_contexts", allowed);
Map<String, ScriptContext<?>> mockAndMiscContexts = new HashMap<>(mockContexts);
mockAndMiscContexts.put(miscContext, new ScriptContext<>(miscContext, MiscContext.class));
ScriptService ss = new ScriptService(settings.build(), engines, mockAndMiscContexts);
ScriptLanguagesInfo info = ss.getScriptLanguages();
assertTrue(info.languageContexts.containsKey(MockScriptEngine.NAME));
assertEquals(1, info.languageContexts.size());
assertEquals(new HashSet<>(allContexts.subList(0, allContexts.size()/2)), info.languageContexts.get(MockScriptEngine.NAME));
}
}

View File

@ -50,6 +50,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import static org.elasticsearch.client.Requests.searchRequest;
@ -90,6 +91,11 @@ public class ExplainableScriptIT extends ESIntegTestCase {
};
return context.factoryClazz.cast(factory);
}
@Override
public Set<ScriptContext<?>> getSupportedContexts() {
return Collections.singleton(ScoreScript.CONTEXT);
}
};
}
}

View File

@ -55,6 +55,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS;
@ -1155,6 +1156,11 @@ public class SuggestSearchIT extends ESIntegTestCase {
};
return context.factoryClazz.cast(factory);
}
@Override
public Set<ScriptContext<?>> getSupportedContexts() {
return Collections.singleton(TemplateScript.CONTEXT);
}
}
public void testPhraseSuggesterCollate() throws InterruptedException, ExecutionException, IOException {

View File

@ -35,7 +35,10 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Collections.emptyMap;
@ -304,6 +307,35 @@ public class MockScriptEngine implements ScriptEngine {
throw new IllegalArgumentException("mock script engine does not know how to handle context [" + context.name + "]");
}
@Override
public Set<ScriptContext<?>> getSupportedContexts() {
// TODO(stu): make part of `compile()`
return Stream.of(
FieldScript.CONTEXT,
TermsSetQueryScript.CONTEXT,
NumberSortScript.CONTEXT,
StringSortScript.CONTEXT,
IngestScript.CONTEXT,
AggregationScript.CONTEXT,
IngestConditionalScript.CONTEXT,
UpdateScript.CONTEXT,
BucketAggregationScript.CONTEXT,
BucketAggregationSelectorScript.CONTEXT,
SignificantTermsHeuristicScoreScript.CONTEXT,
TemplateScript.CONTEXT,
FilterScript.CONTEXT,
SimilarityScript.CONTEXT,
SimilarityWeightScript.CONTEXT,
MovingFunctionScript.CONTEXT,
ScoreScript.CONTEXT,
ScriptedMetricAggContexts.InitScript.CONTEXT,
ScriptedMetricAggContexts.MapScript.CONTEXT,
ScriptedMetricAggContexts.CombineScript.CONTEXT,
ScriptedMetricAggContexts.ReduceScript.CONTEXT,
IntervalFilterScript.CONTEXT
).collect(Collectors.toSet());
}
private Map<String, Object> createVars(Map<String, Object> params) {
Map<String, Object> vars = new HashMap<>();
vars.put("params", params);