Skip to content

Commit 38c24ec

Browse files
committed
Output function signature license requirements to Kibana definition files
And also test that this matches the actual licensing behaviour of the functions.
1 parent 6263f44 commit 38c24ec

File tree

15 files changed

+220
-29
lines changed

15 files changed

+220
-29
lines changed

docs/reference/query-languages/esql/kibana/definition/functions/categorize.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/query-languages/esql/kibana/definition/functions/st_extent_agg.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/DataType.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,10 @@ public static boolean isSpatialPoint(DataType t) {
560560
return t == GEO_POINT || t == CARTESIAN_POINT;
561561
}
562562

563+
public static boolean isSpatialShape(DataType t) {
564+
return t == GEO_SHAPE || t == CARTESIAN_SHAPE;
565+
}
566+
563567
public static boolean isSpatialGeo(DataType t) {
564568
return t == GEO_POINT || t == GEO_SHAPE;
565569
}

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

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@
2626
import org.elasticsearch.compute.test.BlockTestUtils;
2727
import org.elasticsearch.compute.test.TestBlockFactory;
2828
import org.elasticsearch.indices.CrankyCircuitBreakerService;
29+
import org.elasticsearch.license.License;
30+
import org.elasticsearch.license.XPackLicenseState;
31+
import org.elasticsearch.license.internal.XPackLicenseStatus;
2932
import org.elasticsearch.logging.LogManager;
3033
import org.elasticsearch.logging.Logger;
3134
import org.elasticsearch.test.ESTestCase;
35+
import org.elasticsearch.xpack.esql.LicenseAware;
3236
import org.elasticsearch.xpack.esql.core.expression.Attribute;
3337
import org.elasticsearch.xpack.esql.core.expression.Expression;
3438
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
@@ -54,7 +58,6 @@
5458
import org.junit.After;
5559
import org.junit.AfterClass;
5660

57-
import java.io.IOException;
5861
import java.lang.reflect.Constructor;
5962
import java.lang.reflect.InvocationTargetException;
6063
import java.lang.reflect.Method;
@@ -730,7 +733,8 @@ public void testSerializationOfSimple() {
730733
*/
731734
@AfterClass
732735
public static void testFunctionInfo() {
733-
Logger log = LogManager.getLogger(getTestClass());
736+
Class<?> testClass = getTestClass();
737+
Logger log = LogManager.getLogger(testClass);
734738
FunctionDefinition definition = definition(functionName());
735739
if (definition == null) {
736740
log.info("Skipping function info checks because the function isn't registered");
@@ -753,7 +757,7 @@ public static void testFunctionInfo() {
753757
for (int i = 0; i < args.size(); i++) {
754758
typesFromSignature.add(new HashSet<>());
755759
}
756-
for (Map.Entry<List<DataType>, DataType> entry : signatures(getTestClass()).entrySet()) {
760+
for (Map.Entry<List<DataType>, DataType> entry : signatures(testClass).entrySet()) {
757761
List<DataType> types = entry.getKey();
758762
for (int i = 0; i < args.size() && i < types.size(); i++) {
759763
typesFromSignature.get(i).add(types.get(i).esNameIfPossible());
@@ -796,6 +800,91 @@ public static void testFunctionInfo() {
796800
assertEquals(returnFromSignature, returnTypes);
797801
}
798802

803+
/**
804+
* This test is meant to validate that the license checks documented match those enforced.
805+
* The expectations are set in the test class using a method with this signature:
806+
* <code>
807+
* public static License.OperationMode licenseRequirement(List<DataType> fieldTypes);
808+
* </code>
809+
* License enforcement in the function class is achieved using the interface <code>LicenseAware</code>.
810+
* This test will make sure the two are in agreement, and does not require that the function class actually
811+
* report its license level. If we add license checks to any function, but fail to also add the expected
812+
* license level to the test class, this test will fail.
813+
*/
814+
@AfterClass
815+
public static void testFunctionLicenseChecks() throws Exception {
816+
Class<?> testClass = getTestClass();
817+
DocsV3Support.LicenseRequirementChecker licenseChecker = new DocsV3Support.LicenseRequirementChecker(testClass);
818+
Logger log = LogManager.getLogger(testClass);
819+
FunctionDefinition definition = definition(functionName());
820+
if (definition == null) {
821+
log.info("Skipping function info checks because the function isn't registered");
822+
return;
823+
}
824+
log.info("Running function license checks");
825+
License.OperationMode functionLicense = licenseChecker.invoke(null);
826+
Constructor<?> ctor = constructorWithFunctionInfo(definition.clazz());
827+
if (LicenseAware.class.isAssignableFrom(definition.clazz()) == false) {
828+
// Perform simpler no-signature tests
829+
assertThat(
830+
"Function " + definition.name() + " should be licensed under " + functionLicense,
831+
functionLicense,
832+
equalTo(License.OperationMode.BASIC)
833+
);
834+
return;
835+
}
836+
// For classes with LicenseAware, we need to check that the license is correct
837+
XPackLicenseState basicLicense = makeLicenseState(License.OperationMode.BASIC);
838+
XPackLicenseState platinumLicense = makeLicenseState(License.OperationMode.PLATINUM);
839+
XPackLicenseState enterpriseLicense = makeLicenseState(License.OperationMode.ENTERPRISE);
840+
841+
// Go through all signatures and assert that the license is as expected
842+
signatures(testClass).forEach((signature, returnType) -> {
843+
try {
844+
License.OperationMode license = licenseChecker.invoke(signature);
845+
assertNotNull("License should not be null", license);
846+
847+
// Construct an instance of the class and then call it's licenseCheck method, and compare the results
848+
Object[] args = new Object[signature.size() + 1];
849+
args[0] = Source.EMPTY;
850+
for (int i = 0; i < signature.size(); i++) {
851+
args[i + 1] = new Literal(Source.EMPTY, null, signature.get(i));
852+
}
853+
Object instance = ctor.newInstance(args);
854+
// Check that object implements the LicenseAware interface
855+
if (LicenseAware.class.isAssignableFrom(instance.getClass())) {
856+
LicenseAware licenseAware = (LicenseAware) instance;
857+
boolean basic = licenseAware.licenseCheck(basicLicense);
858+
boolean platinum = licenseAware.licenseCheck(platinumLicense);
859+
boolean enterprise = licenseAware.licenseCheck(enterpriseLicense);
860+
if (license == License.OperationMode.BASIC) {
861+
assertTrue("Basic license should be accepted for " + signature, basic);
862+
assertTrue("Platinum license should be accepted for " + signature, platinum);
863+
assertTrue("Enterprise license should be accepted for " + signature, enterprise);
864+
}
865+
if (license == License.OperationMode.PLATINUM) {
866+
assertFalse("Basic license should not be accepted for " + signature, basic);
867+
assertTrue("Platinum license should be accepted for " + signature, platinum);
868+
assertTrue("Enterprise license should be accepted for " + signature, enterprise);
869+
}
870+
if (license == License.OperationMode.ENTERPRISE) {
871+
assertFalse("Basic license should not be accepted for " + signature, basic);
872+
assertFalse("Platinum license should not be accepted for " + signature, platinum);
873+
assertTrue("Enterprise license should be accepted for " + signature, enterprise);
874+
}
875+
} else {
876+
fail("Function " + definition.name() + " does not implement LicenseAware");
877+
}
878+
} catch (Exception e) {
879+
fail(e);
880+
}
881+
});
882+
}
883+
884+
private static XPackLicenseState makeLicenseState(License.OperationMode mode) {
885+
return new XPackLicenseState(System::currentTimeMillis, new XPackLicenseStatus(mode, true, ""));
886+
}
887+
799888
/**
800889
* Asserts the result of a test case matches the expected result and warnings.
801890
* <p>
@@ -865,7 +954,7 @@ public static Map<List<DataType>, DataType> signatures(Class<?> testClass) {
865954
}
866955

867956
@AfterClass
868-
public static void renderDocs() throws IOException {
957+
public static void renderDocs() throws Exception {
869958
if (System.getProperty("generateDocs") == null) {
870959
return;
871960
}

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

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import org.elasticsearch.common.Strings;
1313
import org.elasticsearch.core.PathUtils;
14+
import org.elasticsearch.license.License;
1415
import org.elasticsearch.logging.LogManager;
1516
import org.elasticsearch.logging.Logger;
1617
import org.elasticsearch.xcontent.XContentBuilder;
@@ -46,6 +47,7 @@
4647
import java.io.InputStreamReader;
4748
import java.lang.annotation.Annotation;
4849
import java.lang.reflect.Constructor;
50+
import java.lang.reflect.Method;
4951
import java.nio.charset.StandardCharsets;
5052
import java.nio.file.Files;
5153
import java.nio.file.Path;
@@ -107,7 +109,7 @@ static OperatorsDocsSupport forOperators(String name, Class<?> testClass) {
107109
return new OperatorsDocsSupport(name, testClass);
108110
}
109111

110-
static void renderDocs(String name, Class<?> testClass) throws IOException {
112+
static void renderDocs(String name, Class<?> testClass) throws Exception {
111113
if (OPERATORS.containsKey(name)) {
112114
var docs = DocsV3Support.forOperators(name, testClass);
113115
docs.renderSignature();
@@ -126,7 +128,7 @@ public static void renderNegatedOperator(
126128
String name,
127129
Function<String, String> description,
128130
Class<?> testClass
129-
) throws IOException {
131+
) throws Exception {
130132
var docs = forOperators("not " + name.toLowerCase(Locale.ROOT), testClass);
131133
docs.renderDocsForNegatedOperators(ctor, description);
132134
}
@@ -272,12 +274,49 @@ public void writeToTempDir(Path dir, String extension, String str) throws IOExce
272274
}
273275
}
274276

277+
/**
278+
* This class is used to check if a license requirement method exists in the test class.
279+
* This is used to add license requirement information to the generated documentation.
280+
*/
281+
public static class LicenseRequirementChecker {
282+
private Method staticMethod;
283+
private Function<List<DataType>, License.OperationMode> fallbackLambda;
284+
285+
public LicenseRequirementChecker(Class<?> testClass) {
286+
try {
287+
staticMethod = testClass.getMethod("licenseRequirement", List.class);
288+
if (License.OperationMode.class.equals(staticMethod.getReturnType()) == false
289+
|| java.lang.reflect.Modifier.isStatic(staticMethod.getModifiers()) == false) {
290+
staticMethod = null; // Reset if the method doesn't match the signature
291+
}
292+
} catch (NoSuchMethodException e) {
293+
staticMethod = null;
294+
}
295+
296+
if (staticMethod == null) {
297+
fallbackLambda = fieldTypes -> {
298+
// Provide your default implementation here
299+
return License.OperationMode.BASIC;
300+
};
301+
}
302+
}
303+
304+
public License.OperationMode invoke(List<DataType> fieldTypes) throws Exception {
305+
if (staticMethod != null) {
306+
return (License.OperationMode) staticMethod.invoke(null, fieldTypes);
307+
} else {
308+
return fallbackLambda.apply(fieldTypes);
309+
}
310+
}
311+
}
312+
275313
protected final String category;
276314
protected final String name;
277315
protected final FunctionDefinition definition;
278316
protected final Logger logger;
279317
private final Supplier<Map<List<DataType>, DataType>> signatures;
280318
private TempFileWriter tempFileWriter;
319+
private final LicenseRequirementChecker licenseChecker;
281320

282321
protected DocsV3Support(String category, String name, Class<?> testClass, Supplier<Map<List<DataType>, DataType>> signatures) {
283322
this(category, name, null, testClass, signatures);
@@ -296,6 +335,7 @@ private DocsV3Support(
296335
this.logger = LogManager.getLogger(testClass);
297336
this.signatures = signatures;
298337
this.tempFileWriter = new DocsFileWriter();
338+
this.licenseChecker = new LicenseRequirementChecker(testClass);
299339
}
300340

301341
/** Used in tests to capture output for asserting on the content */
@@ -460,7 +500,7 @@ void writeToTempKibanaDir(String subdir, String extension, String str) throws IO
460500

461501
protected abstract void renderSignature() throws IOException;
462502

463-
protected abstract void renderDocs() throws IOException;
503+
protected abstract void renderDocs() throws Exception;
464504

465505
static class FunctionDocsSupport extends DocsV3Support {
466506
private FunctionDocsSupport(String name, Class<?> testClass) {
@@ -488,7 +528,7 @@ protected void renderSignature() throws IOException {
488528
}
489529

490530
@Override
491-
protected void renderDocs() throws IOException {
531+
protected void renderDocs() throws Exception {
492532
if (definition == null) {
493533
logger.info("Skipping rendering docs because the function '{}' isn't registered", name);
494534
} else {
@@ -497,7 +537,7 @@ protected void renderDocs() throws IOException {
497537
}
498538
}
499539

500-
private void renderDocs(FunctionDefinition definition) throws IOException {
540+
private void renderDocs(FunctionDefinition definition) throws Exception {
501541
EsqlFunctionRegistry.FunctionDescription description = EsqlFunctionRegistry.description(definition);
502542
if (name.equals("case")) {
503543
/*
@@ -711,7 +751,7 @@ public void renderSignature() throws IOException {
711751
}
712752

713753
@Override
714-
public void renderDocs() throws IOException {
754+
public void renderDocs() throws Exception {
715755
Constructor<?> ctor = constructorWithFunctionInfo(op.clazz());
716756
if (ctor != null) {
717757
FunctionInfo functionInfo = ctor.getAnnotation(FunctionInfo.class);
@@ -722,7 +762,7 @@ public void renderDocs() throws IOException {
722762
}
723763
}
724764

725-
void renderDocsForNegatedOperators(Constructor<?> ctor, Function<String, String> description) throws IOException {
765+
void renderDocsForNegatedOperators(Constructor<?> ctor, Function<String, String> description) throws Exception {
726766
String baseName = name.toLowerCase(Locale.ROOT).replace("not ", "");
727767
OperatorConfig op = OPERATORS.get(baseName);
728768
assert op != null;
@@ -795,7 +835,7 @@ public Example[] examples() {
795835
}
796836

797837
void renderDocsForOperators(String name, String titleName, Constructor<?> ctor, FunctionInfo info, boolean variadic)
798-
throws IOException {
838+
throws Exception {
799839
renderKibanaInlineDocs(name, titleName, info);
800840

801841
var params = ctor.getParameters();
@@ -999,7 +1039,7 @@ void renderKibanaFunctionDefinition(
9991039
FunctionInfo info,
10001040
List<EsqlFunctionRegistry.ArgSignature> args,
10011041
boolean variadic
1002-
) throws IOException {
1042+
) throws Exception {
10031043

10041044
try (XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint().lfAtEnd().startObject()) {
10051045
builder.field(
@@ -1019,6 +1059,10 @@ void renderKibanaFunctionDefinition(
10191059
});
10201060
}
10211061
builder.field("name", name);
1062+
License.OperationMode license = licenseChecker.invoke(null);
1063+
if (license != null && license != License.OperationMode.BASIC) {
1064+
builder.field("license", license.toString());
1065+
}
10221066
if (titleName != null && titleName.equals(name) == false) {
10231067
builder.field("titleName", titleName);
10241068
}
@@ -1073,6 +1117,10 @@ void renderKibanaFunctionDefinition(
10731117
builder.endObject();
10741118
}
10751119
builder.endArray();
1120+
license = licenseChecker.invoke(sig.getKey());
1121+
if (license != null && license != License.OperationMode.BASIC) {
1122+
builder.field("license", license.toString());
1123+
}
10761124
builder.field("variadic", variadic);
10771125
builder.field("returnType", sig.getValue().esNameIfPossible());
10781126
builder.endObject();

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ public void testRenderingExampleResultEmojis() throws IOException {
228228
assertThat(results, equalTo(expectedResults));
229229
}
230230

231-
public void testRenderingExampleFromClass() throws IOException {
231+
public void testRenderingExampleFromClass() throws Exception {
232232
String expected = """
233233
% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
234234
@@ -306,7 +306,7 @@ public void testRenderingExampleFromClass() throws IOException {
306306
assertThat(rendered.trim(), equalTo(expected.trim()));
307307
}
308308

309-
public void testRenderingLayoutFromClass() throws IOException {
309+
public void testRenderingLayoutFromClass() throws Exception {
310310
String expected = """
311311
% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
312312
@@ -353,7 +353,7 @@ public void testRenderingLayoutFromClass() throws IOException {
353353
assertThat(rendered.trim(), equalTo(expected.trim()));
354354
}
355355

356-
private TestDocsFileWriter renderTestClassDocs() throws IOException {
356+
private TestDocsFileWriter renderTestClassDocs() throws Exception {
357357
FunctionInfo info = functionInfo(TestClass.class);
358358
assert info != null;
359359
FunctionDefinition definition = EsqlFunctionRegistry.def(TestClass.class, TestClass::new, "count");

0 commit comments

Comments
 (0)