diff --git a/docs/plugins/authors.asciidoc b/docs/plugins/authors.asciidoc index be58c3edb00..8d5de86c864 100644 --- a/docs/plugins/authors.asciidoc +++ b/docs/plugins/authors.asciidoc @@ -5,8 +5,10 @@ The Elasticsearch repository contains examples of: -* a https://github.com/elastic/elasticsearch/tree/master/plugins/jvm-example[Java plugin] - which contains Java code. +* a https://github.com/elastic/elasticsearch/tree/master/plugins/custom-settings[Java plugin] + which contains a plugin with custom settings. +* a https://github.com/elastic/elasticsearch/tree/master/plugins/rest-handler[Java plugin] + which contains a plugin that registers a Rest handler. * a https://github.com/elastic/elasticsearch/tree/master/plugins/examples/rescore[Java plugin] which contains a rescore plugin. * a https://github.com/elastic/elasticsearch/tree/master/plugins/examples/script-expert-scoring[Java plugin] diff --git a/docs/reference/cat/plugins.asciidoc b/docs/reference/cat/plugins.asciidoc index 6eb3ac74ca0..ca35a23d305 100644 --- a/docs/reference/cat/plugins.asciidoc +++ b/docs/reference/cat/plugins.asciidoc @@ -27,10 +27,10 @@ U7321H6 discovery-gce {version} The Google Compute Engine (GCE) Discov U7321H6 ingest-attachment {version} Ingest processor that uses Apache Tika to extract contents U7321H6 ingest-geoip {version} Ingest processor that uses looksup geo data based on ip adresses using the Maxmind geo database U7321H6 ingest-user-agent {version} Ingest processor that extracts information from a user agent -U7321H6 jvm-example {version} Demonstrates all the pluggable Java entry points in Elasticsearch U7321H6 mapper-murmur3 {version} The Mapper Murmur3 plugin allows to compute hashes of a field's values at index-time and to store them in the index. U7321H6 mapper-size {version} The Mapper Size plugin allows document to record their uncompressed size at index time. U7321H6 store-smb {version} The Store SMB plugin adds support for SMB stores. +U7321H6 transport-nio {version} The nio transport. ------------------------------------------------------------------------------ // TESTRESPONSE[s/([.()])/\\$1/ s/U7321H6/.+/ _cat] diff --git a/plugins/examples/custom-settings/build.gradle b/plugins/examples/custom-settings/build.gradle new file mode 100644 index 00000000000..e0e728cec24 --- /dev/null +++ b/plugins/examples/custom-settings/build.gradle @@ -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. + */ + +apply plugin: 'elasticsearch.esplugin' + +esplugin { + name 'custom-settings' + description 'An example plugin showing how to register custom settings' + classname 'org.elasticsearch.example.customsettings.ExampleCustomSettingsPlugin' +} + +integTestCluster { + // Adds a setting in the Elasticsearch keystore before running the integration tests + keystoreSetting 'custom.secured', 'password' +} \ No newline at end of file diff --git a/plugins/examples/custom-settings/src/main/bin/test b/plugins/examples/custom-settings/src/main/bin/test new file mode 100755 index 00000000000..c9aa63a4b67 --- /dev/null +++ b/plugins/examples/custom-settings/src/main/bin/test @@ -0,0 +1,6 @@ +#!/bin/bash + +# Plugin can contain executable files that are copied by the plugin manager +# to a /bin folder. + +echo test diff --git a/plugins/examples/custom-settings/src/main/bin/test.bat b/plugins/examples/custom-settings/src/main/bin/test.bat new file mode 100644 index 00000000000..0764393fd25 --- /dev/null +++ b/plugins/examples/custom-settings/src/main/bin/test.bat @@ -0,0 +1,4 @@ +REM Plugin can contain executable files that are copied by the plugin manager +REM to a /bin folder. + +echo test diff --git a/plugins/examples/custom-settings/src/main/config/custom.yml b/plugins/examples/custom-settings/src/main/config/custom.yml new file mode 100644 index 00000000000..1759e0ff96d --- /dev/null +++ b/plugins/examples/custom-settings/src/main/config/custom.yml @@ -0,0 +1,5 @@ +# Custom configuration file for the custom-settings plugin +custom: + simple: foo + list: [0, 1, 1, 2, 3, 5, 8, 13, 21] + filtered: secret \ No newline at end of file diff --git a/plugins/examples/custom-settings/src/main/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsConfig.java b/plugins/examples/custom-settings/src/main/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsConfig.java new file mode 100644 index 00000000000..fafe3615f63 --- /dev/null +++ b/plugins/examples/custom-settings/src/main/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsConfig.java @@ -0,0 +1,128 @@ +/* + * 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.example.customsettings; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.settings.SecureSetting; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +/** + * {@link ExampleCustomSettingsConfig} contains the custom settings values and their static declarations. + */ +public class ExampleCustomSettingsConfig { + + /** + * A simple string setting + */ + static final Setting SIMPLE_SETTING = Setting.simpleString("custom.simple", Property.NodeScope); + + /** + * A simple boolean setting that can be dynamically updated using the Cluster Settings API and that is {@code "false"} by default + */ + static final Setting BOOLEAN_SETTING = Setting.boolSetting("custom.bool", false, Property.NodeScope, Property.Dynamic); + + /** + * A string setting that can be dynamically updated and that is validated by some logic + */ + static final Setting VALIDATED_SETTING = Setting.simpleString("custom.validated", (value, settings) -> { + if (value != null && value.contains("forbidden")) { + throw new IllegalArgumentException("Setting must not contain [forbidden]"); + } + }, Property.NodeScope, Property.Dynamic); + + /** + * A setting that is filtered out when listing all the cluster's settings + */ + static final Setting FILTERED_SETTING = Setting.simpleString("custom.filtered", Property.NodeScope, Property.Filtered); + + /** + * A setting which contains a sensitive string. This may be any sensitive string, e.g. a username, a password, an auth token, etc. + */ + static final Setting SECURED_SETTING = SecureSetting.secureString("custom.secured", null); + + /** + * A setting that consists of a list of integers + */ + static final Setting> LIST_SETTING = + Setting.listSetting("custom.list", Collections.emptyList(), Integer::valueOf, Property.NodeScope); + + + private final String simple; + private final String validated; + private final Boolean bool; + private final List list; + private final String filtered; + + public ExampleCustomSettingsConfig(final Environment environment) { + // Elasticsearch config directory + final Path configDir = environment.configFile(); + + // Resolve the plugin's custom settings file + final Path customSettingsYamlFile = configDir.resolve("custom-settings/custom.yml"); + + // Load the settings from the plugin's custom settings file + final Settings customSettings; + try { + customSettings = Settings.builder().loadFromPath(customSettingsYamlFile).build(); + assert customSettings != null; + } catch (IOException e) { + throw new ElasticsearchException("Failed to load settings", e); + } + + this.simple = SIMPLE_SETTING.get(customSettings); + this.bool = BOOLEAN_SETTING.get(customSettings); + this.validated = VALIDATED_SETTING.get(customSettings); + this.filtered = FILTERED_SETTING.get(customSettings); + this.list = LIST_SETTING.get(customSettings); + + // Loads the secured setting from the keystore + final SecureString secured = SECURED_SETTING.get(environment.settings()); + assert secured != null; + } + + public String getSimple() { + return simple; + } + + public Boolean getBool() { + return bool; + } + + public String getValidated() { + return validated; + } + + public String getFiltered() { + return filtered; + } + + public List getList() { + return list; + } + +} diff --git a/plugins/examples/custom-settings/src/main/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsPlugin.java b/plugins/examples/custom-settings/src/main/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsPlugin.java new file mode 100644 index 00000000000..b3a9ae3a32b --- /dev/null +++ b/plugins/examples/custom-settings/src/main/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsPlugin.java @@ -0,0 +1,68 @@ +/* + * 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.example.customsettings; + +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.plugins.Plugin; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import static java.util.stream.Collectors.toList; + +public class ExampleCustomSettingsPlugin extends Plugin { + + private final ExampleCustomSettingsConfig config; + + public ExampleCustomSettingsPlugin(final Settings settings, final Path configPath) { + this.config = new ExampleCustomSettingsConfig(new Environment(settings, configPath)); + + // asserts that the setting has been correctly loaded from the custom setting file + assert "secret".equals(config.getFiltered()); + } + + /** + * @return the plugin's custom settings + */ + @Override + public List> getSettings() { + return Arrays.asList(ExampleCustomSettingsConfig.SIMPLE_SETTING, + ExampleCustomSettingsConfig.BOOLEAN_SETTING, + ExampleCustomSettingsConfig.VALIDATED_SETTING, + ExampleCustomSettingsConfig.FILTERED_SETTING, + ExampleCustomSettingsConfig.SECURED_SETTING, + ExampleCustomSettingsConfig.LIST_SETTING); + } + + @Override + public Settings additionalSettings() { + final Settings.Builder builder = Settings.builder(); + + // Exposes SIMPLE_SETTING and LIST_SETTING as a node settings + builder.put(ExampleCustomSettingsConfig.SIMPLE_SETTING.getKey(), config.getSimple()); + + final List values = config.getList().stream().map(integer -> Integer.toString(integer)).collect(toList()); + builder.putList(ExampleCustomSettingsConfig.LIST_SETTING.getKey(), values); + + return builder.build(); + } +} diff --git a/plugins/examples/custom-settings/src/test/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsClientYamlTestSuiteIT.java b/plugins/examples/custom-settings/src/test/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsClientYamlTestSuiteIT.java new file mode 100644 index 00000000000..cd8f31f7690 --- /dev/null +++ b/plugins/examples/custom-settings/src/test/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsClientYamlTestSuiteIT.java @@ -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.example.customsettings; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; +import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; + +/** + * {@link ExampleCustomSettingsClientYamlTestSuiteIT} executes the plugin's REST API integration tests. + *

+ * The tests can be executed using the command: ./gradlew :example-plugins:custom-settings:check + *

+ * This class extends {@link ESClientYamlSuiteTestCase}, which takes care of parsing the YAML files + * located in the src/test/resources/rest-api-spec/test/ directory and validates them against the + * custom REST API definition files located in src/test/resources/rest-api-spec/api/. + *

+ * Once validated, {@link ESClientYamlSuiteTestCase} executes the REST tests against a single node + * integration cluster which has the plugin already installed by the Gradle build script. + *

+ */ +public class ExampleCustomSettingsClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase { + + public ExampleCustomSettingsClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { + super(testCandidate); + } + + @ParametersFactory + public static Iterable parameters() throws Exception { + // The test executes all the test candidates by default + // see ESClientYamlSuiteTestCase.REST_TESTS_SUITE + return ESClientYamlSuiteTestCase.createParameters(); + } +} diff --git a/plugins/examples/custom-settings/src/test/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsConfigTests.java b/plugins/examples/custom-settings/src/test/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsConfigTests.java new file mode 100644 index 00000000000..d939427126b --- /dev/null +++ b/plugins/examples/custom-settings/src/test/java/org/elasticsearch/example/customsettings/ExampleCustomSettingsConfigTests.java @@ -0,0 +1,44 @@ +/* + * 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.example.customsettings; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; + +import static org.elasticsearch.example.customsettings.ExampleCustomSettingsConfig.VALIDATED_SETTING; + +/** + * {@link ExampleCustomSettingsConfigTests} is a unit test class for {@link ExampleCustomSettingsConfig}. + *

+ * It's a JUnit test class that extends {@link ESTestCase} which provides useful methods for testing. + *

+ * The tests can be executed in the IDE or using the command: ./gradlew :example-plugins:custom-settings:test + */ +public class ExampleCustomSettingsConfigTests extends ESTestCase { + + public void testValidatedSetting() { + final String expected = randomAlphaOfLengthBetween(1, 5); + final String actual = VALIDATED_SETTING.get(Settings.builder().put(VALIDATED_SETTING.getKey(), expected).build()); + assertEquals(expected, actual); + + final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> + VALIDATED_SETTING.get(Settings.builder().put("custom.validated", "it's forbidden").build())); + assertEquals("Setting must not contain [forbidden]", exception.getMessage()); + } +} diff --git a/plugins/examples/custom-settings/src/test/resources/rest-api-spec/test/customsettings/10_plugin.yml b/plugins/examples/custom-settings/src/test/resources/rest-api-spec/test/customsettings/10_plugin.yml new file mode 100644 index 00000000000..891a09fbb75 --- /dev/null +++ b/plugins/examples/custom-settings/src/test/resources/rest-api-spec/test/customsettings/10_plugin.yml @@ -0,0 +1,10 @@ +"Test that the custom-settings plugin is loaded in Elasticsearch": + + # Use the Cat Plugins API to retrieve the list of plugins + - do: + cat.plugins: + local: true + h: component + + - match: + $body: /^custom-settings\n$/ diff --git a/plugins/examples/custom-settings/src/test/resources/rest-api-spec/test/customsettings/10_settings.yml b/plugins/examples/custom-settings/src/test/resources/rest-api-spec/test/customsettings/10_settings.yml new file mode 100644 index 00000000000..bf170f23b27 --- /dev/null +++ b/plugins/examples/custom-settings/src/test/resources/rest-api-spec/test/customsettings/10_settings.yml @@ -0,0 +1,54 @@ +"Test custom settings": + + # Use the Get Cluster Settings API to list the settings including the default ones + - do: + cluster.get_settings: + include_defaults: true + + - is_false: defaults.custom.bool + - match: { defaults.custom.list.0: "0" } + - match: { defaults.custom.list.1: "1" } + - match: { defaults.custom.list.2: "1" } + - match: { defaults.custom.list.3: "2" } + - match: { defaults.custom.list.4: "3" } + - match: { defaults.custom.list.5: "5" } + - match: { defaults.custom.list.6: "8" } + - match: { defaults.custom.list.7: "13" } + - match: { defaults.custom.list.8: "21" } + + # This setting is filtered: it does not appear in the response + - is_false: defaults.custom.filtered + + # Use the Cluster Update Settings API to update some custom settings + - do: + cluster.put_settings: + body: + transient: + custom: + bool: true + validated: "updated" + + # Use the Get Cluster Settings API to list the settings again + - do: + cluster.get_settings: {} + + - is_true: transient.custom.bool + - match: { transient.custom.validated: "updated" } + + # Try to update the "validated" setting with a forbidden value + - do: + catch: bad_request + cluster.put_settings: + body: + transient: + custom: + validated: "forbidden" + + # Reset the settings to their default values + - do: + cluster.put_settings: + body: + transient: + custom: + bool: null + validated: null diff --git a/plugins/jvm-example/build.gradle b/plugins/examples/rest-handler/build.gradle similarity index 81% rename from plugins/jvm-example/build.gradle rename to plugins/examples/rest-handler/build.gradle index 7a229a396f7..2cccb5a8969 100644 --- a/plugins/jvm-example/build.gradle +++ b/plugins/examples/rest-handler/build.gradle @@ -17,15 +17,15 @@ * under the License. */ -esplugin { - description 'Demonstrates all the pluggable Java entry points in Elasticsearch' - classname 'org.elasticsearch.plugin.example.JvmExamplePlugin' -} -// Not published so no need to assemble -tasks.remove(assemble) -build.dependsOn.remove('assemble') +apply plugin: 'elasticsearch.esplugin' -// no unit tests +esplugin { + name 'rest-handler' + description 'An example plugin showing how to register a REST handler' + classname 'org.elasticsearch.example.resthandler.ExampleRestHandlerPlugin' +} + +// No unit tests in this example test.enabled = false configurations { @@ -40,8 +40,8 @@ task exampleFixture(type: org.elasticsearch.gradle.test.AntFixture) { dependsOn project.configurations.exampleFixture executable = new File(project.runtimeJavaHome, 'bin/java') args '-cp', "${ -> project.configurations.exampleFixture.asPath }", - 'example.ExampleTestFixture', - baseDir + 'example.ExampleTestFixture', + baseDir } integTestCluster { diff --git a/plugins/jvm-example/src/main/java/org/elasticsearch/plugin/example/ExampleCatAction.java b/plugins/examples/rest-handler/src/main/java/org/elasticsearch/example/resthandler/ExampleCatAction.java similarity index 81% rename from plugins/jvm-example/src/main/java/org/elasticsearch/plugin/example/ExampleCatAction.java rename to plugins/examples/rest-handler/src/main/java/org/elasticsearch/example/resthandler/ExampleCatAction.java index bdfc51d7d36..759e6bdcfee 100644 --- a/plugins/jvm-example/src/main/java/org/elasticsearch/plugin/example/ExampleCatAction.java +++ b/plugins/examples/rest-handler/src/main/java/org/elasticsearch/example/resthandler/ExampleCatAction.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.plugin.example; +package org.elasticsearch.example.resthandler; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.Table; @@ -28,29 +28,31 @@ import org.elasticsearch.rest.action.cat.AbstractCatAction; import org.elasticsearch.rest.action.cat.RestTable; import static org.elasticsearch.rest.RestRequest.Method.GET; +import static org.elasticsearch.rest.RestRequest.Method.POST; /** * Example of adding a cat action with a plugin. */ public class ExampleCatAction extends AbstractCatAction { - private final ExamplePluginConfiguration config; - public ExampleCatAction(Settings settings, RestController controller, ExamplePluginConfiguration config) { + ExampleCatAction(final Settings settings, final RestController controller) { super(settings); - this.config = config; - controller.registerHandler(GET, "/_cat/configured_example", this); + controller.registerHandler(GET, "/_cat/example", this); + controller.registerHandler(POST, "/_cat/example", this); } @Override public String getName() { - return "example_cat_action"; + return "rest_handler_cat_example"; } @Override protected RestChannelConsumer doCatRequest(final RestRequest request, final NodeClient client) { + final String message = request.param("message", "Hello from Cat Example action"); + Table table = getTableWithHeader(request); table.startRow(); - table.addCell(config.getTestConfig()); + table.addCell(message); table.endRow(); return channel -> { try { @@ -67,7 +69,7 @@ public class ExampleCatAction extends AbstractCatAction { } public static String documentation() { - return "/_cat/configured_example\n"; + return "/_cat/example\n"; } @Override diff --git a/plugins/jvm-example/src/main/java/org/elasticsearch/plugin/example/JvmExamplePlugin.java b/plugins/examples/rest-handler/src/main/java/org/elasticsearch/example/resthandler/ExampleRestHandlerPlugin.java similarity index 63% rename from plugins/jvm-example/src/main/java/org/elasticsearch/plugin/example/JvmExamplePlugin.java rename to plugins/examples/rest-handler/src/main/java/org/elasticsearch/example/resthandler/ExampleRestHandlerPlugin.java index 03321e1c4a2..3ce6d8a42f5 100644 --- a/plugins/jvm-example/src/main/java/org/elasticsearch/plugin/example/JvmExamplePlugin.java +++ b/plugins/examples/rest-handler/src/main/java/org/elasticsearch/example/resthandler/ExampleRestHandlerPlugin.java @@ -17,7 +17,7 @@ * under the License. */ -package org.elasticsearch.plugin.example; +package org.elasticsearch.example.resthandler; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNodes; @@ -25,37 +25,27 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; -import org.elasticsearch.env.Environment; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; -import java.nio.file.Path; import java.util.List; import java.util.function.Supplier; import static java.util.Collections.singletonList; -/** - * Example of a plugin. - */ -public class JvmExamplePlugin extends Plugin implements ActionPlugin { - private final ExamplePluginConfiguration config; - - public JvmExamplePlugin(Settings settings, Path configPath) { - config = new ExamplePluginConfiguration(new Environment(settings, configPath)); - } +public class ExampleRestHandlerPlugin extends Plugin implements ActionPlugin { @Override - public Settings additionalSettings() { - return Settings.EMPTY; - } + public List getRestHandlers(final Settings settings, + final RestController restController, + final ClusterSettings clusterSettings, + final IndexScopedSettings indexScopedSettings, + final SettingsFilter settingsFilter, + final IndexNameExpressionResolver indexNameExpressionResolver, + final Supplier nodesInCluster) { - @Override - public List getRestHandlers(Settings settings, RestController restController, ClusterSettings clusterSettings, - IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter, IndexNameExpressionResolver indexNameExpressionResolver, - Supplier nodesInCluster) { - return singletonList(new ExampleCatAction(settings, restController, config)); + return singletonList(new ExampleCatAction(settings, restController)); } } diff --git a/plugins/jvm-example/src/test/java/org/elasticsearch/plugin/example/ExampleExternalIT.java b/plugins/examples/rest-handler/src/test/java/org/elasticsearch/example/resthandler/ExampleFixtureIT.java similarity index 65% rename from plugins/jvm-example/src/test/java/org/elasticsearch/plugin/example/ExampleExternalIT.java rename to plugins/examples/rest-handler/src/test/java/org/elasticsearch/example/resthandler/ExampleFixtureIT.java index 3ed616cb3af..97fc6b241ea 100644 --- a/plugins/jvm-example/src/test/java/org/elasticsearch/plugin/example/ExampleExternalIT.java +++ b/plugins/examples/rest-handler/src/test/java/org/elasticsearch/example/resthandler/ExampleFixtureIT.java @@ -17,7 +17,7 @@ * under the License. */ -package org.elasticsearch.plugin.example; +package org.elasticsearch.example.resthandler; import org.elasticsearch.mocksocket.MockSocket; import org.elasticsearch.test.ESTestCase; @@ -30,14 +30,18 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Objects; -public class ExampleExternalIT extends ESTestCase { +public class ExampleFixtureIT extends ESTestCase { + public void testExample() throws Exception { - String stringAddress = Objects.requireNonNull(System.getProperty("external.address")); - URL url = new URL("http://" + stringAddress); - InetAddress address = InetAddress.getByName(url.getHost()); - try (Socket socket = new MockSocket(address, url.getPort()); - BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) { - assertEquals("TEST", reader.readLine()); + final String stringAddress = Objects.requireNonNull(System.getProperty("external.address")); + final URL url = new URL("http://" + stringAddress); + + final InetAddress address = InetAddress.getByName(url.getHost()); + try ( + Socket socket = new MockSocket(address, url.getPort()); + BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)) + ) { + assertEquals("TEST", reader.readLine()); } } } diff --git a/plugins/jvm-example/src/test/java/org/elasticsearch/plugin/example/JvmExampleClientYamlTestSuiteIT.java b/plugins/examples/rest-handler/src/test/java/org/elasticsearch/example/resthandler/ExampleRestHandlerClientYamlTestSuiteIT.java similarity index 54% rename from plugins/jvm-example/src/test/java/org/elasticsearch/plugin/example/JvmExampleClientYamlTestSuiteIT.java rename to plugins/examples/rest-handler/src/test/java/org/elasticsearch/example/resthandler/ExampleRestHandlerClientYamlTestSuiteIT.java index a43cca71826..e4c2d6f655c 100644 --- a/plugins/jvm-example/src/test/java/org/elasticsearch/plugin/example/JvmExampleClientYamlTestSuiteIT.java +++ b/plugins/examples/rest-handler/src/test/java/org/elasticsearch/example/resthandler/ExampleRestHandlerClientYamlTestSuiteIT.java @@ -16,24 +16,36 @@ * specific language governing permissions and limitations * under the License. */ - -package org.elasticsearch.plugin.example; +package org.elasticsearch.example.resthandler; import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; - import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; -public class JvmExampleClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase { +/** + * {@link ExampleRestHandlerClientYamlTestSuiteIT} executes the plugin's REST API integration tests. + *

+ * The tests can be executed using the command: ./gradlew :example-plugins:rest-handler:check + *

+ * This class extends {@link ESClientYamlSuiteTestCase}, which takes care of parsing the YAML files + * located in the src/test/resources/rest-api-spec/test/ directory and validates them against the + * custom REST API definition files located in src/test/resources/rest-api-spec/api/. + *

+ * Once validated, {@link ESClientYamlSuiteTestCase} executes the REST tests against a single node + * integration cluster which has the plugin already installed by the Gradle build script. + *

+ */ +public class ExampleRestHandlerClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase { - public JvmExampleClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { + public ExampleRestHandlerClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { super(testCandidate); } @ParametersFactory public static Iterable parameters() throws Exception { + // The test executes all the test candidates by default + // see ESClientYamlSuiteTestCase.REST_TESTS_SUITE return ESClientYamlSuiteTestCase.createParameters(); } } - diff --git a/plugins/jvm-example/src/test/resources/rest-api-spec/api/cat.configured_example.json b/plugins/examples/rest-handler/src/test/resources/rest-api-spec/api/cat.example.json similarity index 59% rename from plugins/jvm-example/src/test/resources/rest-api-spec/api/cat.configured_example.json rename to plugins/examples/rest-handler/src/test/resources/rest-api-spec/api/cat.example.json index 4744ac5fc18..e42617eef7b 100644 --- a/plugins/jvm-example/src/test/resources/rest-api-spec/api/cat.configured_example.json +++ b/plugins/examples/rest-handler/src/test/resources/rest-api-spec/api/cat.example.json @@ -1,10 +1,10 @@ { - "cat.configured_example": { + "cat.example": { "documentation": "", "methods": ["GET"], "url": { - "path": "/_cat/configured_example", - "paths": ["/_cat/configured_example"], + "path": "/_cat/example", + "paths": ["/_cat/example"], "parts": {}, "params": { "help": { @@ -16,6 +16,11 @@ "type": "boolean", "description": "Verbose mode. Display column headers", "default": true + }, + "message": { + "type": "string", + "description": "A simple message that will be printed out in the response", + "default": "Hello from Cat Example action" } } }, diff --git a/plugins/examples/rest-handler/src/test/resources/rest-api-spec/test/resthandler/10_basic.yml b/plugins/examples/rest-handler/src/test/resources/rest-api-spec/test/resthandler/10_basic.yml new file mode 100644 index 00000000000..8f3cc06ed5a --- /dev/null +++ b/plugins/examples/rest-handler/src/test/resources/rest-api-spec/test/resthandler/10_basic.yml @@ -0,0 +1,10 @@ +"Test that the rest-handler plugin is loaded in Elasticsearch": + + # Use the Cat Plugins API to retrieve the list of plugins + - do: + cat.plugins: + local: true + h: component + + - match: + $body: /^rest-handler\n$/ diff --git a/plugins/examples/rest-handler/src/test/resources/rest-api-spec/test/resthandler/20_cat_example.yml b/plugins/examples/rest-handler/src/test/resources/rest-api-spec/test/resthandler/20_cat_example.yml new file mode 100644 index 00000000000..ff054ad3531 --- /dev/null +++ b/plugins/examples/rest-handler/src/test/resources/rest-api-spec/test/resthandler/20_cat_example.yml @@ -0,0 +1,26 @@ +--- +"Help": + - do: + cat.example: + help: true + + - match: + $body: | + /^ test .+ \n + $/ + +--- +"Default message": + - do: + cat.example: + v: false + + - match: {$body: "Hello from Cat Example action\n" } + +--- +"Custom message": + - do: + cat.example: + message: Hello from REST API test + + - match: {$body: "Hello from REST API test\n" } diff --git a/plugins/jvm-example/src/main/bin/test b/plugins/jvm-example/src/main/bin/test deleted file mode 100755 index 13fdcce1e52..00000000000 --- a/plugins/jvm-example/src/main/bin/test +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -echo test diff --git a/plugins/jvm-example/src/main/bin/test.bat b/plugins/jvm-example/src/main/bin/test.bat deleted file mode 100644 index 7c650525059..00000000000 --- a/plugins/jvm-example/src/main/bin/test.bat +++ /dev/null @@ -1 +0,0 @@ -echo test diff --git a/plugins/jvm-example/src/main/config/example.yml b/plugins/jvm-example/src/main/config/example.yml deleted file mode 100644 index 097569ad4cf..00000000000 --- a/plugins/jvm-example/src/main/config/example.yml +++ /dev/null @@ -1 +0,0 @@ -test: foo diff --git a/plugins/jvm-example/src/main/java/org/elasticsearch/plugin/example/ExamplePluginConfiguration.java b/plugins/jvm-example/src/main/java/org/elasticsearch/plugin/example/ExamplePluginConfiguration.java deleted file mode 100644 index 802c2fc67a6..00000000000 --- a/plugins/jvm-example/src/main/java/org/elasticsearch/plugin/example/ExamplePluginConfiguration.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.plugin.example; - -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; - -import java.io.IOException; -import java.nio.file.Path; - -/** - * Example configuration. - */ -public class ExamplePluginConfiguration { - private final Settings customSettings; - - public static final Setting TEST_SETTING = - new Setting("test", "default_value", - (value) -> value, Setting.Property.Dynamic); - - public ExamplePluginConfiguration(Environment env) { - // The directory part of the location matches the artifactId of this plugin - Path path = env.configFile().resolve("jvm-example/example.yml"); - try { - customSettings = Settings.builder().loadFromPath(path).build(); - } catch (IOException e) { - throw new RuntimeException("Failed to load settings, giving up", e); - } - - // asserts for tests - assert customSettings != null; - assert TEST_SETTING.get(customSettings) != null; - } - - public String getTestConfig() { - return TEST_SETTING.get(customSettings); - } -} diff --git a/plugins/jvm-example/src/test/resources/rest-api-spec/test/jvm_example/10_basic.yml b/plugins/jvm-example/src/test/resources/rest-api-spec/test/jvm_example/10_basic.yml deleted file mode 100644 index c671fe2e8ba..00000000000 --- a/plugins/jvm-example/src/test/resources/rest-api-spec/test/jvm_example/10_basic.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Integration tests for JVM Example Plugin -# -"JVM Example loaded": - - do: - cluster.state: {} - - # Get master node id - - set: { master_node: master } - - - do: - nodes.info: {} - - - match: { nodes.$master.plugins.0.name: jvm-example } diff --git a/plugins/jvm-example/src/test/resources/rest-api-spec/test/jvm_example/20_configured_example.yml b/plugins/jvm-example/src/test/resources/rest-api-spec/test/jvm_example/20_configured_example.yml deleted file mode 100644 index 9023f3b3091..00000000000 --- a/plugins/jvm-example/src/test/resources/rest-api-spec/test/jvm_example/20_configured_example.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -"Help": - - do: - cat.configured_example: - help: true - - - match: - $body: | - /^ test .+ \n - $/ - ---- -"Data": - - do: - cat.configured_example: - v: false - - - match: - $body: | - /^ - foo \s+ - $/ diff --git a/qa/vagrant/build.gradle b/qa/vagrant/build.gradle index 40fc3998185..f28f6afd2fc 100644 --- a/qa/vagrant/build.gradle +++ b/qa/vagrant/build.gradle @@ -22,7 +22,7 @@ apply plugin: 'elasticsearch.vagrant' List plugins = [] for (Project subproj : project.rootProject.subprojects) { - if (subproj.path.startsWith(':plugins:')) { + if (subproj.path.startsWith(':plugins:') || subproj.path.equals(':example-plugins:custom-settings')) { // add plugin as a dep dependencies { bats project(path: "${subproj.path}", configuration: 'zip') diff --git a/qa/vagrant/src/test/resources/packaging/tests/module_and_plugin_test_cases.bash b/qa/vagrant/src/test/resources/packaging/tests/module_and_plugin_test_cases.bash index fb721d5c6d9..190f70e9bad 100644 --- a/qa/vagrant/src/test/resources/packaging/tests/module_and_plugin_test_cases.bash +++ b/qa/vagrant/src/test/resources/packaging/tests/module_and_plugin_test_cases.bash @@ -57,7 +57,7 @@ setup() { # other tests. Commenting out lots of test cases seems like a reasonably # common workflow. if [ $BATS_TEST_NUMBER == 1 ] || - [[ $BATS_TEST_NAME =~ install_jvm.*example ]] || + [[ $BATS_TEST_NAME =~ install_a_sample_plugin ]] || [ ! -d "$ESHOME" ]; then clean_before_test install @@ -89,7 +89,7 @@ else } fi -@test "[$GROUP] install jvm-example plugin with a symlinked plugins path" { +@test "[$GROUP] install a sample plugin with a symlinked plugins path" { # Clean up after the last time this test was run rm -rf /tmp/plugins.* rm -rf /tmp/old_plugins.* @@ -99,48 +99,63 @@ fi chown -R elasticsearch:elasticsearch "$es_plugins" ln -s "$es_plugins" "$ESPLUGINS" - install_jvm_example + install_plugin_example start_elasticsearch_service + # check that symlinked plugin was actually picked up - curl -s localhost:9200/_cat/configured_example | sed 's/ *$//' > /tmp/installed - echo "foo" > /tmp/expected + curl -XGET -H 'Content-Type: application/json' 'http://localhost:9200/_cat/plugins?h=component' | sed 's/ *$//' > /tmp/installed + echo "custom-settings" > /tmp/expected diff /tmp/installed /tmp/expected + + curl -XGET -H 'Content-Type: application/json' 'http://localhost:9200/_cluster/settings?include_defaults&filter_path=defaults.custom.simple' > /tmp/installed + echo -n '{"defaults":{"custom":{"simple":"foo"}}}' > /tmp/expected + diff /tmp/installed /tmp/expected + stop_elasticsearch_service - remove_jvm_example + remove_plugin_example unlink "$ESPLUGINS" } -@test "[$GROUP] install jvm-example plugin with a custom CONFIG_DIR" { +@test "[$GROUP] install a sample plugin with a custom CONFIG_DIR" { # Clean up after the last time we ran this test rm -rf /tmp/config.* move_config - ES_PATH_CONF="$ESCONFIG" install_jvm_example + ES_PATH_CONF="$ESCONFIG" install_plugin_example ES_PATH_CONF="$ESCONFIG" start_elasticsearch_service - diff <(curl -s localhost:9200/_cat/configured_example | sed 's/ //g') <(echo "foo") + + # check that symlinked plugin was actually picked up + curl -XGET -H 'Content-Type: application/json' 'http://localhost:9200/_cat/plugins?h=component' | sed 's/ *$//' > /tmp/installed + echo "custom-settings" > /tmp/expected + diff /tmp/installed /tmp/expected + + curl -XGET -H 'Content-Type: application/json' 'http://localhost:9200/_cluster/settings?include_defaults&filter_path=defaults.custom.simple' > /tmp/installed + echo -n '{"defaults":{"custom":{"simple":"foo"}}}' > /tmp/expected + diff /tmp/installed /tmp/expected + stop_elasticsearch_service - ES_PATH_CONF="$ESCONFIG" remove_jvm_example + ES_PATH_CONF="$ESCONFIG" remove_plugin_example } -@test "[$GROUP] install jvm-example plugin from a directory with a space" { +@test "[$GROUP] install a sample plugin from a directory with a space" { rm -rf "/tmp/plugins with space" mkdir -p "/tmp/plugins with space" - local zip=$(ls jvm-example-*.zip) + local zip=$(ls custom-settings-*.zip) cp $zip "/tmp/plugins with space" - install_jvm_example "/tmp/plugins with space/$zip" - remove_jvm_example + install_plugin_example "/tmp/plugins with space/$zip" + remove_plugin_example } -@test "[$GROUP] install jvm-example plugin to elasticsearch directory with a space" { +@test "[$GROUP] install a sample plugin to elasticsearch directory with a space" { [ "$GROUP" == "TAR PLUGINS" ] || skip "Test case only supported by TAR PLUGINS" move_elasticsearch "/tmp/elastic search" - install_jvm_example - remove_jvm_example + install_plugin_example + remove_plugin_example } @test "[$GROUP] fail if java executable is not found" { @@ -161,8 +176,8 @@ fi # Note that all of the tests from here to the end of the file expect to be run # in sequence and don't take well to being run one at a time. -@test "[$GROUP] install jvm-example plugin" { - install_jvm_example +@test "[$GROUP] install a sample plugin" { + install_plugin_example } @test "[$GROUP] install icu plugin" { @@ -293,8 +308,8 @@ fi stop_elasticsearch_service } -@test "[$GROUP] remove jvm-example plugin" { - remove_jvm_example +@test "[$GROUP] remove a sample plugin" { + remove_plugin_example } @test "[$GROUP] remove icu plugin" { @@ -399,8 +414,8 @@ fi stop_elasticsearch_service } -@test "[$GROUP] install jvm-example with different logging modes and check output" { - local relativePath=${1:-$(readlink -m jvm-example-*.zip)} +@test "[$GROUP] install a sample plugin with different logging modes and check output" { + local relativePath=${1:-$(readlink -m custom-settings-*.zip)} sudo -E -u $ESPLUGIN_COMMAND_USER "$ESHOME/bin/elasticsearch-plugin" install "file://$relativePath" > /tmp/plugin-cli-output # exclude progress line local loglines=$(cat /tmp/plugin-cli-output | grep -v "^[[:cntrl:]]" | wc -l) @@ -409,9 +424,9 @@ fi cat /tmp/plugin-cli-output false } - remove_jvm_example + remove_plugin_example - local relativePath=${1:-$(readlink -m jvm-example-*.zip)} + local relativePath=${1:-$(readlink -m custom-settings-*.zip)} sudo -E -u $ESPLUGIN_COMMAND_USER ES_JAVA_OPTS="-Des.logger.level=DEBUG" "$ESHOME/bin/elasticsearch-plugin" install "file://$relativePath" > /tmp/plugin-cli-output local loglines=$(cat /tmp/plugin-cli-output | grep -v "^[[:cntrl:]]" | wc -l) [ "$loglines" -gt "2" ] || { @@ -419,7 +434,7 @@ fi cat /tmp/plugin-cli-output false } - remove_jvm_example + remove_plugin_example } @test "[$GROUP] test java home with space" { @@ -456,7 +471,7 @@ fi } @test "[$GROUP] test umask" { - install_jvm_example $(readlink -m jvm-example-*.zip) 0077 + install_plugin_example $(readlink -m custom-settings-*.zip) 0077 } @test "[$GROUP] hostname" { diff --git a/qa/vagrant/src/test/resources/packaging/utils/plugins.bash b/qa/vagrant/src/test/resources/packaging/utils/plugins.bash index eda3038ee93..403d89b30ec 100644 --- a/qa/vagrant/src/test/resources/packaging/utils/plugins.bash +++ b/qa/vagrant/src/test/resources/packaging/utils/plugins.bash @@ -82,50 +82,49 @@ remove_plugin() { fi } -# Install the jvm-example plugin which fully exercises the special case file -# placements for non-site plugins. -install_jvm_example() { - local relativePath=${1:-$(readlink -m jvm-example-*.zip)} - install_plugin jvm-example "$relativePath" $2 +# Install a sample plugin which fully exercises the special case file placements. +install_plugin_example() { + local relativePath=${1:-$(readlink -m custom-settings-*.zip)} + install_plugin custom-settings "$relativePath" $2 bin_user=$(find "$ESHOME/bin" -maxdepth 0 -printf "%u") bin_owner=$(find "$ESHOME/bin" -maxdepth 0 -printf "%g") - assert_file "$ESHOME/plugins/jvm-example" d $bin_user $bin_owner 755 - assert_file "$ESHOME/plugins/jvm-example/jvm-example-$(cat version).jar" f $bin_user $bin_owner 644 + assert_file "$ESHOME/plugins/custom-settings" d $bin_user $bin_owner 755 + assert_file "$ESHOME/plugins/custom-settings/custom-settings-$(cat version).jar" f $bin_user $bin_owner 644 #owner group and permissions vary depending on how es was installed #just make sure that everything is the same as the parent bin dir, which was properly set up during install - assert_file "$ESHOME/bin/jvm-example" d $bin_user $bin_owner 755 - assert_file "$ESHOME/bin/jvm-example/test" f $bin_user $bin_owner 755 + assert_file "$ESHOME/bin/custom-settings" d $bin_user $bin_owner 755 + assert_file "$ESHOME/bin/custom-settings/test" f $bin_user $bin_owner 755 #owner group and permissions vary depending on how es was installed #just make sure that everything is the same as $CONFIG_DIR, which was properly set up during install config_user=$(find "$ESCONFIG" -maxdepth 0 -printf "%u") config_owner=$(find "$ESCONFIG" -maxdepth 0 -printf "%g") # directories should user the user file-creation mask - assert_file "$ESCONFIG/jvm-example" d $config_user $config_owner 750 - assert_file "$ESCONFIG/jvm-example/example.yml" f $config_user $config_owner 660 + assert_file "$ESCONFIG/custom-settings" d $config_user $config_owner 750 + assert_file "$ESCONFIG/custom-settings/custom.yml" f $config_user $config_owner 660 - run sudo -E -u vagrant LANG="en_US.UTF-8" cat "$ESCONFIG/jvm-example/example.yml" + run sudo -E -u vagrant LANG="en_US.UTF-8" cat "$ESCONFIG/custom-settings/custom.yml" [ $status = 1 ] [[ "$output" == *"Permission denied"* ]] || { echo "Expected permission denied but found $output:" false } - echo "Running jvm-example's bin script...." - "$ESHOME/bin/jvm-example/test" | grep test + echo "Running sample plugin bin script...." + "$ESHOME/bin/custom-settings/test" | grep test } -# Remove the jvm-example plugin which fully exercises the special cases of +# Remove the sample plugin which fully exercises the special cases of # removing bin and not removing config. -remove_jvm_example() { - remove_plugin jvm-example +remove_plugin_example() { + remove_plugin custom-settings - assert_file_not_exist "$ESHOME/bin/jvm-example" - assert_file_exist "$ESCONFIG/jvm-example" - assert_file_exist "$ESCONFIG/jvm-example/example.yml" + assert_file_not_exist "$ESHOME/bin/custom-settings" + assert_file_exist "$ESCONFIG/custom-settings" + assert_file_exist "$ESCONFIG/custom-settings/custom.yml" } # Install a plugin with a special prefix. For the most part prefixes are just