Browse Source

Initial kibana definition files for command, currently only providing License information (#127829)

Initial Kibana definition files for commands, currently only providing License information. We leave the license field out if it works with BASIC, so the only two files that actually have a license line are:

* CHANGE_POINT: PLATINUM
* RRF: ENTERPRISE
Craig Taverner 5 months ago
parent
commit
7d06f815f3
24 changed files with 344 additions and 0 deletions
  1. 6 0
      docs/reference/query-languages/esql/kibana/definition/commands/change_point.json
  2. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/dissect.json
  3. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/drop.json
  4. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/enrich.json
  5. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/eval.json
  6. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/explain.json
  7. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/fork.json
  8. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/grok.json
  9. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/inlinestats.json
  10. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/insist.json
  11. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/keep.json
  12. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/limit.json
  13. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/lookup.json
  14. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/lookup_join.json
  15. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/mv_expand.json
  16. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/rename.json
  17. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/rerank.json
  18. 6 0
      docs/reference/query-languages/esql/kibana/definition/commands/rrf.json
  19. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/sample.json
  20. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/sort.json
  21. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/stats.json
  22. 5 0
      docs/reference/query-languages/esql/kibana/definition/commands/where.json
  23. 43 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3Support.java
  24. 189 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/CommandLicenseTests.java

+ 6 - 0
docs/reference/query-languages/esql/kibana/definition/commands/change_point.json

@@ -0,0 +1,6 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "change_point",
+  "license" : "PLATINUM"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/dissect.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "dissect"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/drop.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "drop"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/enrich.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "enrich"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/eval.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "eval"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/explain.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "explain"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/fork.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "fork"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/grok.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "grok"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/inlinestats.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "inlinestats"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/insist.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "insist"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/keep.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "keep"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/limit.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "limit"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/lookup.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "lookup"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/lookup_join.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "lookup_join"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/mv_expand.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "mv_expand"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/rename.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "rename"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/rerank.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "rerank"
+}

+ 6 - 0
docs/reference/query-languages/esql/kibana/definition/commands/rrf.json

@@ -0,0 +1,6 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "rrf",
+  "license" : "ENTERPRISE"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/sample.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "sample"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/sort.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "sort"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/stats.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "stats"
+}

+ 5 - 0
docs/reference/query-languages/esql/kibana/definition/commands/where.json

@@ -0,0 +1,5 @@
+{
+  "comment" : "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it.",
+  "type" : "command",
+  "name" : "where"
+}

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

@@ -12,6 +12,7 @@ import com.unboundid.util.NotNull;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.core.PathUtils;
 import org.elasticsearch.license.License;
+import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.logging.LogManager;
 import org.elasticsearch.logging.Logger;
 import org.elasticsearch.xcontent.XContentBuilder;
@@ -39,6 +40,7 @@ import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThan;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThanOrEqual;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.NotEquals;
+import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
 import org.elasticsearch.xpack.esql.session.Configuration;
 
 import java.io.BufferedReader;
@@ -874,6 +876,47 @@ public abstract class DocsV3Support {
         }
     }
 
+    /** Command specific docs generating, currently very empty since we only render kibana definition files */
+    public static class CommandsDocsSupport extends DocsV3Support {
+        private final LogicalPlan command;
+        private final XPackLicenseState licenseState;
+
+        public CommandsDocsSupport(String name, Class<?> testClass, LogicalPlan command, XPackLicenseState licenseState) {
+            super("commands", name, testClass, Map::of);
+            this.command = command;
+            this.licenseState = licenseState;
+        }
+
+        @Override
+        public void renderSignature() throws IOException {
+            // Unimplemented until we make command docs dynamically generated
+        }
+
+        @Override
+        public void renderDocs() throws Exception {
+            // Currently we only render kibana definition files, but we could expand to rendering much more if we decide to
+            renderKibanaCommandDefinition();
+        }
+
+        void renderKibanaCommandDefinition() throws Exception {
+            try (XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint().lfAtEnd().startObject()) {
+                builder.field(
+                    "comment",
+                    "This is generated by ESQL’s DocsV3Support. Do not edit it. See ../README.md for how to regenerate it."
+                );
+                builder.field("type", "command");
+                builder.field("name", name);
+                License.OperationMode license = licenseState.getOperationMode();
+                if (license != null && license != License.OperationMode.BASIC) {
+                    builder.field("license", license.toString());
+                }
+                String rendered = Strings.toString(builder.endObject());
+                logger.info("Writing kibana command definition for [{}]:\n{}", name, rendered);
+                writeToTempKibanaDir("definition", "json", rendered);
+            }
+        }
+    }
+
     protected String buildFunctionSignatureSvg() throws IOException {
         return (definition != null) ? RailRoadDiagram.functionSignature(definition) : null;
     }

+ 189 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/CommandLicenseTests.java

@@ -0,0 +1,189 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.plan.logical;
+
+import org.elasticsearch.license.License;
+import org.elasticsearch.license.XPackLicenseState;
+import org.elasticsearch.license.internal.XPackLicenseStatus;
+import org.elasticsearch.logging.LogManager;
+import org.elasticsearch.logging.Logger;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.esql.LicenseAware;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.DocsV3Support;
+import org.elasticsearch.xpack.esql.parser.EsqlBaseParserVisitor;
+import org.elasticsearch.xpack.esql.plan.logical.join.LookupJoin;
+import org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class CommandLicenseTests extends ESTestCase {
+    private static final Logger log = LogManager.getLogger(CommandLicenseTests.class);
+
+    public void testLicenseCheck() {
+        var sourceCommand = new LocalRelation(Source.EMPTY, List.of(), null);
+        for (var commandName : getCommandClasses().keySet()) {
+            Class<? extends LogicalPlan> commandClass = getCommandClasses().get(commandName);
+            try {
+                var arg = (commandClass == InlineStats.class) ? new Aggregate(Source.EMPTY, sourceCommand, null, null) : sourceCommand;
+                checkLicense(commandName, createInstance(commandClass, arg));
+            } catch (Exception e) {
+                Throwable c = e.getCause();
+                fail("Failed to create instance of command class: " + commandClass.getName() + " - " + e.getMessage() + " - " + c);
+            }
+        }
+    }
+
+    public static class TestCheckLicense {
+        XPackLicenseState basicLicense = makeLicenseState(License.OperationMode.BASIC);
+        XPackLicenseState platinumLicense = makeLicenseState(License.OperationMode.PLATINUM);
+        XPackLicenseState enterpriseLicense = makeLicenseState(License.OperationMode.ENTERPRISE);
+
+        private XPackLicenseState licenseLevel(LicenseAware licenseAware) {
+            for (XPackLicenseState license : List.of(basicLicense, platinumLicense, enterpriseLicense)) {
+                if (licenseAware.licenseCheck(license)) {
+                    return license;
+                }
+            }
+            throw new IllegalArgumentException("No license level is supported by " + licenseAware.getClass().getName());
+        }
+    }
+
+    private static XPackLicenseState makeLicenseState(License.OperationMode mode) {
+        return new XPackLicenseState(System::currentTimeMillis, new XPackLicenseStatus(mode, true, null));
+    }
+
+    private static void checkLicense(String commandName, LogicalPlan command) throws Exception {
+        log.info("Running function license checks");
+        TestCheckLicense checkLicense = new TestCheckLicense();
+        if (command instanceof LicenseAware licenseAware) {
+            log.info("Command " + commandName + " implements LicenseAware.");
+            saveLicenseState(commandName, command, checkLicense.licenseLevel(licenseAware));
+        } else {
+            log.info("Command " + commandName + " does not implement LicenseAware.");
+            saveLicenseState(commandName, command, checkLicense.basicLicense);
+        }
+    }
+
+    private static void saveLicenseState(String name, LogicalPlan command, XPackLicenseState licenseState) throws Exception {
+        DocsV3Support.CommandsDocsSupport docs = new DocsV3Support.CommandsDocsSupport(
+            name.toLowerCase(Locale.ROOT),
+            CommandLicenseTests.class,
+            command,
+            licenseState
+        );
+        docs.renderDocs();
+    }
+
+    // Find all command classes, by looking at the public methods of the EsqlBaseParserVisitor
+    private static Map<String, Class<? extends LogicalPlan>> getCommandClasses() {
+        Map<String, Class<? extends LogicalPlan>> commandClasses = new TreeMap<>();
+        Pattern pattern = Pattern.compile("visit(\\w+)Command");
+        String planPackage = "org.elasticsearch.xpack.esql.plan.logical";
+        Map<String, String> commandClassNameMapper = Map.of(
+            "Where",
+            "Filter",
+            "Inlinestats",
+            "InlineStats",
+            "Rrf",
+            "RrfScoreEval",
+            "Sort",
+            "OrderBy",
+            "Stats",
+            "Aggregate",
+            "Join",
+            "LookupJoin"
+        );
+        Map<String, String> commandNameMapper = Map.of("ChangePoint", "CHANGE_POINT", "LookupJoin", "LOOKUP_JOIN", "MvExpand", "MV_EXPAND");
+        Map<String, String> commandPackageMapper = Map.of("Rerank", planPackage + ".inference", "LookupJoin", planPackage + ".join");
+        Set<String> ignoredClasses = Set.of("Processing", "TimeSeries", "Completion", "Source", "From", "Row");
+
+        for (Method method : EsqlBaseParserVisitor.class.getMethods()) {
+            String methodName = method.getName();
+            Matcher matcher = pattern.matcher(methodName);
+            if (matcher.matches()) {
+                String className = matcher.group(1);
+                if (ignoredClasses.contains(className)) {
+                    continue;
+                }
+                String commandName = commandNameMapper.getOrDefault(className, className.toUpperCase(Locale.ROOT));
+                if (commandClassNameMapper.containsKey(className)) {
+                    className = commandClassNameMapper.get(className);
+                    if (commandNameMapper.containsKey(className)) {
+                        commandName = commandNameMapper.get(className);
+                    }
+                }
+                try {
+                    String fullClassName = commandPackageMapper.getOrDefault(className, planPackage) + "." + className;
+                    Class<?> candidateClass = Class.forName(fullClassName);
+
+                    if (LogicalPlan.class.isAssignableFrom(candidateClass)) {
+                        commandClasses.put(commandName, candidateClass.asSubclass(LogicalPlan.class));
+                    } else {
+                        log.info("Class " + className + " does NOT extend LogicalPlan.");
+                    }
+                } catch (ClassNotFoundException e) {
+                    log.info("Class " + className + " not found.");
+                }
+            }
+        }
+        return commandClasses;
+    }
+
+    private static LogicalPlan createInstance(Class<? extends LogicalPlan> clazz, LogicalPlan child) throws InvocationTargetException,
+        InstantiationException, IllegalAccessException {
+        Source source = Source.EMPTY;
+
+        // hard coded cases where the first two parameters are not Source and child LogicalPlan
+        switch (clazz.getSimpleName()) {
+            case "Grok" -> {
+                return new Grok(source, child, null, null, List.of());
+            }
+            case "Fork" -> {
+                return new Fork(source, List.of(child, child));
+            }
+            case "Sample" -> {
+                return new Sample(source, null, null, child);
+            }
+            case "LookupJoin" -> {
+                return new LookupJoin(source, child, child, List.of());
+            }
+            case "Limit" -> {
+                return new Limit(source, null, child);
+            }
+        }
+
+        // For all others, find the constructor that takes Source and LogicalPlan as the first two parameters
+        Constructor<?>[] constructors = clazz.getConstructors();
+
+        Constructor<?> constructor = Arrays.stream(constructors).filter(c -> {
+            Class<?>[] params = c.getParameterTypes();
+            return params.length > 1 && Source.class.isAssignableFrom(params[0]) && LogicalPlan.class.isAssignableFrom(params[1]);
+        })
+            .min(Comparator.comparingInt(c -> c.getParameterTypes().length))
+            .orElseThrow(() -> new IllegalArgumentException("No suitable constructor found for class " + clazz.getName()));
+
+        Class<?>[] paramTypes = constructor.getParameterTypes();
+        Object[] args = new Object[paramTypes.length];
+        args[0] = source;
+        args[1] = child;
+        log.info("Creating instance of " + clazz.getName() + " with constructor: " + constructor);
+        return (LogicalPlan) constructor.newInstance(args);
+    }
+}