Skip to content

Commit 5f293f3

Browse files
authored
ESQL: Add a LicenseAware interface for licensed Nodes (#118931)
This adds a new interface that elements that require a proper license state can implement to enforce the license requirement. This can be now applied to any node or node property. The check still happens in the Verifier, since the plan needs to be analysed first and the check still only happens if no other verification faults exist already. Fixes #117405
1 parent b2879c3 commit 5f293f3

File tree

6 files changed

+98
-36
lines changed

6 files changed

+98
-36
lines changed

docs/changelog/118931.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 118931
2+
summary: Add a `LicenseAware` interface for licensed Nodes
3+
area: ES|QL
4+
type: enhancement
5+
issues:
6+
- 117405

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/function/Function.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
*/
77
package org.elasticsearch.xpack.esql.core.expression.function;
88

9-
import org.elasticsearch.license.XPackLicenseState;
109
import org.elasticsearch.xpack.esql.core.expression.Expression;
1110
import org.elasticsearch.xpack.esql.core.expression.Expressions;
1211
import org.elasticsearch.xpack.esql.core.expression.Nullability;
@@ -43,11 +42,6 @@ public Nullability nullable() {
4342
return Expressions.nullable(children());
4443
}
4544

46-
/** Return true if this function can be executed under the provided {@link XPackLicenseState}, otherwise false.*/
47-
public boolean checkLicense(XPackLicenseState state) {
48-
return true;
49-
}
50-
5145
@Override
5246
public int hashCode() {
5347
return Objects.hash(getClass(), children());
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.esql;
9+
10+
import org.elasticsearch.license.XPackLicenseState;
11+
12+
public interface LicenseAware {
13+
/** Return true if the implementer can be executed under the provided {@link XPackLicenseState}, otherwise false.*/
14+
boolean licenseCheck(XPackLicenseState state);
15+
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package org.elasticsearch.xpack.esql.analysis;
99

1010
import org.elasticsearch.license.XPackLicenseState;
11+
import org.elasticsearch.xpack.esql.LicenseAware;
1112
import org.elasticsearch.xpack.esql.action.EsqlCapabilities;
1213
import org.elasticsearch.xpack.esql.common.Failure;
1314
import org.elasticsearch.xpack.esql.core.capabilities.Unresolvable;
@@ -26,6 +27,7 @@
2627
import org.elasticsearch.xpack.esql.core.expression.predicate.logical.Not;
2728
import org.elasticsearch.xpack.esql.core.expression.predicate.logical.Or;
2829
import org.elasticsearch.xpack.esql.core.expression.predicate.operator.comparison.BinaryComparison;
30+
import org.elasticsearch.xpack.esql.core.tree.Node;
2931
import org.elasticsearch.xpack.esql.core.type.DataType;
3032
import org.elasticsearch.xpack.esql.expression.function.UnsupportedAttribute;
3133
import org.elasticsearch.xpack.esql.expression.function.aggregate.AggregateFunction;
@@ -209,7 +211,7 @@ else if (p instanceof Lookup lookup) {
209211
checkRemoteEnrich(plan, failures);
210212

211213
if (failures.isEmpty()) {
212-
checkLicense(plan, licenseState, failures);
214+
licenseCheck(plan, failures);
213215
}
214216

215217
// gather metrics
@@ -587,11 +589,15 @@ private static void checkBinaryComparison(LogicalPlan p, Set<Failure> failures)
587589
});
588590
}
589591

590-
private void checkLicense(LogicalPlan plan, XPackLicenseState licenseState, Set<Failure> failures) {
591-
plan.forEachExpressionDown(Function.class, p -> {
592-
if (p.checkLicense(licenseState) == false) {
593-
failures.add(new Failure(p, "current license is non-compliant for function [" + p.sourceText() + "]"));
592+
private void licenseCheck(LogicalPlan plan, Set<Failure> failures) {
593+
Consumer<Node<?>> licenseCheck = n -> {
594+
if (n instanceof LicenseAware la && la.licenseCheck(licenseState) == false) {
595+
failures.add(fail(n, "current license is non-compliant for [{}]", n.sourceText()));
594596
}
597+
};
598+
plan.forEachDown(p -> {
599+
licenseCheck.accept(p);
600+
p.forEachExpression(Expression.class, licenseCheck);
595601
});
596602
}
597603

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialAggregateFunction.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference;
1212
import org.elasticsearch.license.License;
1313
import org.elasticsearch.license.XPackLicenseState;
14+
import org.elasticsearch.xpack.esql.LicenseAware;
1415
import org.elasticsearch.xpack.esql.core.expression.Expression;
1516
import org.elasticsearch.xpack.esql.core.tree.Source;
1617

@@ -24,7 +25,7 @@
2425
* The AggregateMapper class will generate multiple aggregation functions for each combination, allowing the planner to
2526
* select the best one.
2627
*/
27-
public abstract class SpatialAggregateFunction extends AggregateFunction {
28+
public abstract class SpatialAggregateFunction extends AggregateFunction implements LicenseAware {
2829
protected final FieldExtractPreference fieldExtractPreference;
2930

3031
protected SpatialAggregateFunction(Source source, Expression field, Expression filter, FieldExtractPreference fieldExtractPreference) {
@@ -41,7 +42,7 @@ protected SpatialAggregateFunction(StreamInput in, FieldExtractPreference fieldE
4142
public abstract SpatialAggregateFunction withDocValues();
4243

4344
@Override
44-
public boolean checkLicense(XPackLicenseState state) {
45+
public boolean licenseCheck(XPackLicenseState state) {
4546
return switch (field().dataType()) {
4647
case GEO_SHAPE, CARTESIAN_SHAPE -> state.isAllowedByLicense(License.OperationMode.PLATINUM);
4748
default -> true;

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/CheckLicenseTests.java

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.elasticsearch.license.internal.XPackLicenseStatus;
1616
import org.elasticsearch.test.ESTestCase;
1717
import org.elasticsearch.xpack.esql.EsqlTestUtils;
18+
import org.elasticsearch.xpack.esql.LicenseAware;
1819
import org.elasticsearch.xpack.esql.VerificationException;
1920
import org.elasticsearch.xpack.esql.analysis.Analyzer;
2021
import org.elasticsearch.xpack.esql.analysis.AnalyzerContext;
@@ -25,10 +26,12 @@
2526
import org.elasticsearch.xpack.esql.core.tree.Source;
2627
import org.elasticsearch.xpack.esql.core.type.DataType;
2728
import org.elasticsearch.xpack.esql.parser.EsqlParser;
29+
import org.elasticsearch.xpack.esql.plan.logical.Limit;
2830
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
2931
import org.elasticsearch.xpack.esql.stats.Metrics;
3032

3133
import java.util.List;
34+
import java.util.Objects;
3235

3336
import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.analyzerDefaultMapping;
3437
import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.defaultEnrichResolution;
@@ -44,33 +47,42 @@ public void testLicense() {
4447
final LicensedFeature functionLicenseFeature = random().nextBoolean()
4548
? LicensedFeature.momentary("test", "license", functionLicense)
4649
: LicensedFeature.persistent("test", "license", functionLicense);
47-
final EsqlFunctionRegistry.FunctionBuilder builder = (source, expression, cfg) -> {
48-
final LicensedFunction licensedFunction = new LicensedFunction(source);
49-
licensedFunction.setLicensedFeature(functionLicenseFeature);
50-
return licensedFunction;
51-
};
5250
for (License.OperationMode operationMode : License.OperationMode.values()) {
5351
if (License.OperationMode.TRIAL != operationMode && License.OperationMode.compare(operationMode, functionLicense) < 0) {
5452
// non-compliant license
55-
final VerificationException ex = expectThrows(VerificationException.class, () -> analyze(builder, operationMode));
56-
assertThat(ex.getMessage(), containsString("current license is non-compliant for function [license()]"));
53+
final VerificationException ex = expectThrows(
54+
VerificationException.class,
55+
() -> analyze(operationMode, functionLicenseFeature)
56+
);
57+
assertThat(ex.getMessage(), containsString("current license is non-compliant for [license()]"));
58+
assertThat(ex.getMessage(), containsString("current license is non-compliant for [LicensedLimit]"));
5759
} else {
5860
// compliant license
59-
assertNotNull(analyze(builder, operationMode));
61+
assertNotNull(analyze(operationMode, functionLicenseFeature));
6062
}
6163
}
6264
}
6365
}
6466

65-
private LogicalPlan analyze(EsqlFunctionRegistry.FunctionBuilder builder, License.OperationMode operationMode) {
67+
private LogicalPlan analyze(License.OperationMode operationMode, LicensedFeature functionLicenseFeature) {
68+
final EsqlFunctionRegistry.FunctionBuilder builder = (source, expression, cfg) -> new LicensedFunction(
69+
source,
70+
functionLicenseFeature
71+
);
6672
final FunctionDefinition def = EsqlFunctionRegistry.def(LicensedFunction.class, builder, "license");
6773
final EsqlFunctionRegistry registry = new EsqlFunctionRegistry(def) {
6874
@Override
6975
public EsqlFunctionRegistry snapshotRegistry() {
7076
return this;
7177
}
7278
};
73-
return analyzer(registry, operationMode).analyze(parser.createStatement(esql));
79+
80+
var plan = parser.createStatement(esql);
81+
plan = plan.transformDown(
82+
Limit.class,
83+
l -> Objects.equals(l.limit().fold(), 10) ? new LicensedLimit(l.source(), l.limit(), l.child(), functionLicenseFeature) : l
84+
);
85+
return analyzer(registry, operationMode).analyze(plan);
7486
}
7587

7688
private static Analyzer analyzer(EsqlFunctionRegistry registry, License.OperationMode operationMode) {
@@ -88,25 +100,18 @@ private static XPackLicenseState getLicenseState(License.OperationMode operation
88100

89101
// It needs to be public because we run validation on it via reflection in org.elasticsearch.xpack.esql.tree.EsqlNodeSubclassTests.
90102
// This test prevents to add the license as constructor parameter too.
91-
public static class LicensedFunction extends Function {
103+
public static class LicensedFunction extends Function implements LicenseAware {
92104

93-
private LicensedFeature licensedFeature;
105+
private final LicensedFeature licensedFeature;
94106

95-
public LicensedFunction(Source source) {
107+
public LicensedFunction(Source source, LicensedFeature licensedFeature) {
96108
super(source, List.of());
97-
}
98-
99-
void setLicensedFeature(LicensedFeature licensedFeature) {
100109
this.licensedFeature = licensedFeature;
101110
}
102111

103112
@Override
104-
public boolean checkLicense(XPackLicenseState state) {
105-
if (licensedFeature instanceof LicensedFeature.Momentary momentary) {
106-
return momentary.check(state);
107-
} else {
108-
return licensedFeature.checkWithoutTracking(state);
109-
}
113+
public boolean licenseCheck(XPackLicenseState state) {
114+
return checkLicense(state, licensedFeature);
110115
}
111116

112117
@Override
@@ -121,7 +126,7 @@ public Expression replaceChildren(List<Expression> newChildren) {
121126

122127
@Override
123128
protected NodeInfo<? extends Expression> info() {
124-
return NodeInfo.create(this);
129+
return NodeInfo.create(this, LicensedFunction::new, licensedFeature);
125130
}
126131

127132
@Override
@@ -135,4 +140,39 @@ public void writeTo(StreamOutput out) {
135140
}
136141
}
137142

143+
public static class LicensedLimit extends Limit implements LicenseAware {
144+
145+
private final LicensedFeature licensedFeature;
146+
147+
public LicensedLimit(Source source, Expression limit, LogicalPlan child, LicensedFeature licensedFeature) {
148+
super(source, limit, child);
149+
this.licensedFeature = licensedFeature;
150+
}
151+
152+
@Override
153+
public boolean licenseCheck(XPackLicenseState state) {
154+
return checkLicense(state, licensedFeature);
155+
}
156+
157+
@Override
158+
public Limit replaceChild(LogicalPlan newChild) {
159+
return new LicensedLimit(source(), limit(), newChild, licensedFeature);
160+
}
161+
162+
@Override
163+
protected NodeInfo<Limit> info() {
164+
return NodeInfo.create(this, LicensedLimit::new, limit(), child(), licensedFeature);
165+
}
166+
167+
@Override
168+
public String sourceText() {
169+
return "LicensedLimit";
170+
}
171+
}
172+
173+
private static boolean checkLicense(XPackLicenseState state, LicensedFeature licensedFeature) {
174+
return licensedFeature instanceof LicensedFeature.Momentary momentary
175+
? momentary.check(state)
176+
: licensedFeature.checkWithoutTracking(state);
177+
}
138178
}

0 commit comments

Comments
 (0)