From 7ebb0278bbb124a756521caafbf29ae28517c567 Mon Sep 17 00:00:00 2001 From: Cameron Motevasselani Date: Thu, 25 Jul 2019 01:27:11 -0700 Subject: [PATCH 1/2] feat(plugins): adding halyard commands for plugins --- docs/commands.md | 127 +++++++++++++ .../halyard/cli/command/v1/HalCommand.java | 1 + .../halyard/cli/command/v1/PluginCommand.java | 46 +++++ .../v1/plugins/AbstractHasPluginCommand.java | 56 ++++++ .../command/v1/plugins/AddPluginCommand.java | 60 ++++++ .../v1/plugins/DeletePluginCommand.java | 46 +++++ .../command/v1/plugins/EditPluginCommand.java | 58 ++++++ .../v1/plugins/ListPluginsCommand.java | 55 ++++++ .../PluginEnableDisableCommandBuilder.java | 63 +++++++ .../halyard/cli/services/v1/Daemon.java | 48 +++++ .../cli/services/v1/DaemonService.java | 36 ++++ .../cli/command/v1/CommandTreeSpec.groovy | 9 + .../v1/node/DeploymentConfiguration.java | 2 + .../config/model/v1/node/NodeFilter.java | 18 ++ .../halyard/config/model/v1/node/Plugins.java | 43 +++++ .../config/model/v1/plugins/Manifest.java | 77 ++++++++ .../config/model/v1/plugins/Plugin.java | 75 ++++++++ .../config/services/v1/PluginService.java | 139 ++++++++++++++ .../model/v1/plugins/ManifestSpec.groovy | 59 ++++++ .../services/v1/PluginServiceSpec.groovy | 176 ++++++++++++++++++ .../v1/profile/OrcaProfileFactory.java | 20 ++ .../v1/profile/PluginProfileFactory.java | 78 ++++++++ .../spinnaker/v1/service/OrcaService.java | 11 ++ .../controllers/v1/PluginsController.java | 121 ++++++++++++ 24 files changed, 1424 insertions(+) create mode 100644 halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/PluginCommand.java create mode 100644 halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AbstractHasPluginCommand.java create mode 100644 halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AddPluginCommand.java create mode 100644 halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/DeletePluginCommand.java create mode 100644 halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/EditPluginCommand.java create mode 100644 halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/ListPluginsCommand.java create mode 100644 halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/PluginEnableDisableCommandBuilder.java create mode 100644 halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/Plugins.java create mode 100644 halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Manifest.java create mode 100644 halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Plugin.java create mode 100644 halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/services/v1/PluginService.java create mode 100644 halyard-config/src/test/groovy/com/netflix/spinnaker/halyard/config/model/v1/plugins/ManifestSpec.groovy create mode 100644 halyard-config/src/test/groovy/com/netflix/spinnaker/halyard/config/services/v1/PluginServiceSpec.groovy create mode 100644 halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/PluginProfileFactory.java create mode 100644 halyard-web/src/main/java/com/netflix/spinnaker/halyard/controllers/v1/PluginsController.java diff --git a/docs/commands.md b/docs/commands.md index 05d2e4d497..98c68d6806 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -543,6 +543,13 @@ * [**hal deploy details**](#hal-deploy-details) * [**hal deploy diff**](#hal-deploy-diff) * [**hal deploy rollback**](#hal-deploy-rollback) + * [**hal plugins**](#hal-plugins) + * [**hal plugins add**](#hal-plugins-add) + * [**hal plugins delete**](#hal-plugins-delete) + * [**hal plugins disable**](#hal-plugins-disable) + * [**hal plugins edit**](#hal-plugins-edit) + * [**hal plugins enable**](#hal-plugins-enable) + * [**hal plugins list**](#hal-plugins-list) * [**hal shutdown**](#hal-shutdown) * [**hal spin**](#hal-spin) * [**hal spin install**](#hal-spin-install) @@ -586,6 +593,7 @@ hal [parameters] [subcommands] * `backup`: Backup and restore (remote or local) copies of your halconfig and all required files. * `config`: Configure, validate, and view your halconfig. * `deploy`: Manage the deployment of Spinnaker. This includes where it's deployed, what the infrastructure footprint looks like, what the currently running deployment looks like, etc... + * `plugins`: Show Spinnaker's configured plugins. * `shutdown`: Shutdown the halyard daemon. * `spin`: Manage the lifecycle of spin CLI. * `task`: This set of commands exposes utilities of dealing with Halyard's task engine. @@ -10461,6 +10469,125 @@ hal deploy rollback [parameters] * `--service-names`: (*Default*: `[]`) When supplied, only install or update the specified Spinnaker services. +--- +## hal plugins + +Show Spinnaker's configured plugins. + +#### Usage +``` +hal plugins [parameters] [subcommands] +``` + +#### Parameters + * `--deployment`: If supplied, use this Halyard deployment. This will _not_ create a new deployment. + * `--no-validate`: (*Default*: `false`) Skip validation. + +#### Subcommands + * `add`: Add a plugin + * `delete`: Delete a plugin + * `disable`: Enable or disable all plugins + * `edit`: Edit a plugin + * `enable`: Enable or disable all plugins + * `list`: List all plugins + +--- +## hal plugins add + +Add a plugin + +#### Usage +``` +hal plugins add PLUGIN [parameters] +``` + +#### Parameters +`PLUGIN`: The name of the plugin to operate on. + * `--deployment`: If supplied, use this Halyard deployment. This will _not_ create a new deployment. + * `--enabled`: To enable or disable the plugin. + * `--manifest-location`: (*Required*) The location of the plugin's manifest file. + * `--no-validate`: (*Default*: `false`) Skip validation. + + +--- +## hal plugins delete + +Delete a plugin + +#### Usage +``` +hal plugins delete PLUGIN [parameters] +``` + +#### Parameters +`PLUGIN`: The name of the plugin to operate on. + * `--deployment`: If supplied, use this Halyard deployment. This will _not_ create a new deployment. + * `--no-validate`: (*Default*: `false`) Skip validation. + + +--- +## hal plugins disable + +Enable or disable all plugins + +#### Usage +``` +hal plugins disable [parameters] +``` + +#### Parameters + * `--deployment`: If supplied, use this Halyard deployment. This will _not_ create a new deployment. + * `--no-validate`: (*Default*: `false`) Skip validation. + + +--- +## hal plugins edit + +Edit a plugin + +#### Usage +``` +hal plugins edit PLUGIN [parameters] +``` + +#### Parameters +`PLUGIN`: The name of the plugin to operate on. + * `--deployment`: If supplied, use this Halyard deployment. This will _not_ create a new deployment. + * `--enabled`: To enable or disable the plugin. + * `--manifest-location`: The location of the plugin's manifest file. + * `--no-validate`: (*Default*: `false`) Skip validation. + + +--- +## hal plugins enable + +Enable or disable all plugins + +#### Usage +``` +hal plugins enable [parameters] +``` + +#### Parameters + * `--deployment`: If supplied, use this Halyard deployment. This will _not_ create a new deployment. + * `--no-validate`: (*Default*: `false`) Skip validation. + + +--- +## hal plugins list + +List all plugins + +#### Usage +``` +hal plugins list [parameters] +``` + +#### Parameters + * `--deployment`: If supplied, use this Halyard deployment. This will _not_ create a new deployment. + * `--no-validate`: (*Default*: `false`) Skip validation. + + --- ## hal shutdown diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/HalCommand.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/HalCommand.java index fdd0c0841e..2078fb6ec2 100644 --- a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/HalCommand.java +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/HalCommand.java @@ -58,6 +58,7 @@ public HalCommand() { registerSubcommand(new TaskCommand()); registerSubcommand(new VersionCommand()); registerSubcommand(new SpinCommand()); + registerSubcommand(new PluginCommand()); } static String getVersion() { diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/PluginCommand.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/PluginCommand.java new file mode 100644 index 0000000000..19bdf420c6 --- /dev/null +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/PluginCommand.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.cli.command.v1; + +import com.beust.jcommander.Parameters; +import com.netflix.spinnaker.halyard.cli.command.v1.config.AbstractConfigCommand; +import com.netflix.spinnaker.halyard.cli.command.v1.plugins.*; +import lombok.AccessLevel; +import lombok.Getter; + +@Parameters(separators = "=") +public class PluginCommand extends AbstractConfigCommand { + @Getter(AccessLevel.PUBLIC) + private String commandName = "plugins"; + + @Getter(AccessLevel.PUBLIC) + private String shortDescription = "Show Spinnaker's configured plugins."; + + public PluginCommand() { + registerSubcommand(new AddPluginCommand()); + registerSubcommand(new EditPluginCommand()); + registerSubcommand(new DeletePluginCommand()); + registerSubcommand(new ListPluginsCommand()); + registerSubcommand(new PluginEnableDisableCommandBuilder().setEnable(true).build()); + registerSubcommand(new PluginEnableDisableCommandBuilder().setEnable(false).build()); + } + + @Override + protected void executeThis() { + showHelp(); + } +} diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AbstractHasPluginCommand.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AbstractHasPluginCommand.java new file mode 100644 index 0000000000..3bc34630ac --- /dev/null +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AbstractHasPluginCommand.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.cli.command.v1.plugins; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.netflix.spinnaker.halyard.cli.command.v1.config.AbstractConfigCommand; +import com.netflix.spinnaker.halyard.cli.services.v1.Daemon; +import com.netflix.spinnaker.halyard.cli.services.v1.OperationHandler; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; +import java.util.ArrayList; +import java.util.List; + +/** An abstract definition for commands that accept plugins as a main parameter */ +@Parameters(separators = "=") +public abstract class AbstractHasPluginCommand extends AbstractConfigCommand { + @Parameter(description = "The name of the plugin to operate on.", arity = 1) + List plugins = new ArrayList<>(); + + @Override + public String getMainParameter() { + return "plugin"; + } + + public Plugin getPlugin() { + return new OperationHandler() + .setFailureMesssage("Failed to get plugin") + .setOperation(Daemon.getPlugin(getCurrentDeployment(), plugins.get(0), false)) + .get(); + } + + public String getPluginName() { + switch (plugins.size()) { + case 0: + throw new IllegalArgumentException("No plugin supplied"); + case 1: + return plugins.get(0); + default: + throw new IllegalArgumentException("More than one plugin supplied"); + } + } +} diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AddPluginCommand.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AddPluginCommand.java new file mode 100644 index 0000000000..52becfad88 --- /dev/null +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AddPluginCommand.java @@ -0,0 +1,60 @@ +/* + * Copyright 2019 Armory Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.cli.command.v1.plugins; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.netflix.spinnaker.halyard.cli.services.v1.Daemon; +import com.netflix.spinnaker.halyard.cli.services.v1.OperationHandler; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; +import lombok.AccessLevel; +import lombok.Getter; + +@Parameters(separators = "=") +public class AddPluginCommand extends AbstractHasPluginCommand { + @Getter(AccessLevel.PUBLIC) + private String commandName = "add"; + + @Getter(AccessLevel.PUBLIC) + private String shortDescription = "Add a plugin"; + + @Parameter( + names = "--manifest-location", + description = "The location of the plugin's manifest file.", + required = true) + private String manifestLocation; + + @Parameter(names = "--enabled", description = "To enable or disable the plugin.") + private String enabled; + + @Override + protected void executeThis() { + String currentDeployment = getCurrentDeployment(); + String name = getPluginName(); + Plugin plugin = + new Plugin() + .setName(name) + .setEnabled(isSet(enabled) ? Boolean.parseBoolean(enabled) : false) + .setManifestLocation(manifestLocation); + + new OperationHandler() + .setFailureMesssage("Failed to add plugin: " + name + ".") + .setSuccessMessage("Successfully added plugin" + name + ".") + .setOperation(Daemon.addPlugin(currentDeployment, !noValidate, plugin)) + .get(); + } +} diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/DeletePluginCommand.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/DeletePluginCommand.java new file mode 100644 index 0000000000..4ffa36bf72 --- /dev/null +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/DeletePluginCommand.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.cli.command.v1.plugins; + +import com.beust.jcommander.Parameters; +import com.netflix.spinnaker.halyard.cli.services.v1.Daemon; +import com.netflix.spinnaker.halyard.cli.services.v1.OperationHandler; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; +import lombok.AccessLevel; +import lombok.Getter; + +@Parameters(separators = "=") +public class DeletePluginCommand extends AbstractHasPluginCommand { + @Getter(AccessLevel.PUBLIC) + private String commandName = "delete"; + + @Getter(AccessLevel.PUBLIC) + private String shortDescription = "Delete a plugin"; + + @Override + protected void executeThis() { + String currentDeployment = getCurrentDeployment(); + Plugin plugin = getPlugin(); + String name = plugin.getName(); + + new OperationHandler() + .setFailureMesssage("Failed to delete plugin " + name + ".") + .setSuccessMessage("Successfully deleted plugin " + name + ".") + .setOperation(Daemon.deletePlugin(currentDeployment, name, !noValidate)) + .get(); + } +} diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/EditPluginCommand.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/EditPluginCommand.java new file mode 100644 index 0000000000..adc34f64e8 --- /dev/null +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/EditPluginCommand.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.cli.command.v1.plugins; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.netflix.spinnaker.halyard.cli.services.v1.Daemon; +import com.netflix.spinnaker.halyard.cli.services.v1.OperationHandler; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; +import lombok.AccessLevel; +import lombok.Getter; + +@Parameters(separators = "=") +public class EditPluginCommand extends AbstractHasPluginCommand { + @Getter(AccessLevel.PUBLIC) + private String commandName = "edit"; + + @Getter(AccessLevel.PUBLIC) + private String shortDescription = "Edit a plugin"; + + @Parameter( + names = "--manifest-location", + description = "The location of the plugin's manifest file.") + private String manifestLocation; + + @Parameter(names = "--enabled", description = "To enable or disable the plugin.") + private String enabled; + + @Override + protected void executeThis() { + String currentDeployment = getCurrentDeployment(); + Plugin plugin = getPlugin(); + + plugin.setEnabled(isSet(enabled) ? Boolean.parseBoolean(enabled) : plugin.getEnabled()); + plugin.setManifestLocation( + isSet(manifestLocation) ? manifestLocation : plugin.getManifestLocation()); + + new OperationHandler() + .setFailureMesssage("Failed to edit plugin " + plugin.getName() + ".") + .setSuccessMessage("Successfully edited plugin " + plugin.getName() + ".") + .setOperation(Daemon.setPlugin(currentDeployment, plugin.getName(), !noValidate, plugin)) + .get(); + } +} diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/ListPluginsCommand.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/ListPluginsCommand.java new file mode 100644 index 0000000000..2f555d006f --- /dev/null +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/ListPluginsCommand.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.cli.command.v1.plugins; + +import com.beust.jcommander.Parameters; +import com.netflix.spinnaker.halyard.cli.command.v1.config.AbstractConfigCommand; +import com.netflix.spinnaker.halyard.cli.services.v1.Daemon; +import com.netflix.spinnaker.halyard.cli.services.v1.OperationHandler; +import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiUi; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; +import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; + +@Parameters(separators = "=") +public class ListPluginsCommand extends AbstractConfigCommand { + @Getter(AccessLevel.PUBLIC) + private String commandName = "list"; + + @Getter(AccessLevel.PUBLIC) + private String shortDescription = "List all plugins"; + + private List getPlugins() { + String currentDeployment = getCurrentDeployment(); + return new OperationHandler>() + .setFailureMesssage("Failed to get plugins.") + .setOperation(Daemon.getPlugins(currentDeployment, !noValidate)) + .get(); + } + + @Override + protected void executeThis() { + List plugins = getPlugins(); + if (plugins.isEmpty()) { + AnsiUi.success("No configured plugins."); + } else { + AnsiUi.success("Plugins:"); + plugins.forEach(plugin -> AnsiUi.listItem(plugin.getName())); + } + } +} diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/PluginEnableDisableCommandBuilder.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/PluginEnableDisableCommandBuilder.java new file mode 100644 index 0000000000..3d9c7ec8f3 --- /dev/null +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/PluginEnableDisableCommandBuilder.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.cli.command.v1.plugins; + +import com.beust.jcommander.Parameters; +import com.netflix.spinnaker.halyard.cli.command.v1.AbstractEnableDisableCommand; +import com.netflix.spinnaker.halyard.cli.command.v1.CommandBuilder; +import com.netflix.spinnaker.halyard.cli.command.v1.NestableCommand; +import com.netflix.spinnaker.halyard.cli.services.v1.Daemon; +import java.util.function.Supplier; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +public class PluginEnableDisableCommandBuilder implements CommandBuilder { + @Setter boolean enable; + + @Override + public NestableCommand build() { + return new PluginEnableDisableCommand(enable); + } + + @Parameters(separators = "=") + private static class PluginEnableDisableCommand extends AbstractEnableDisableCommand { + @Override + public String getTargetName() { + return "Plugins"; + } + + private PluginEnableDisableCommand(boolean enable) { + this.enable = enable; + } + + @Getter(AccessLevel.PROTECTED) + boolean enable; + + @Override + public String getShortDescription() { + return "Enable or disable all plugins"; + } + + @Override + protected Supplier getOperationSupplier() { + String currentDeployment = getCurrentDeployment(); + boolean enable = isEnable(); + return Daemon.setPluginEnableDisable(currentDeployment, !noValidate, enable); + } + } +} diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/services/v1/Daemon.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/services/v1/Daemon.java index 7bf9795f35..c7c9b9d12c 100644 --- a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/services/v1/Daemon.java +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/services/v1/Daemon.java @@ -27,6 +27,7 @@ import com.netflix.spinnaker.halyard.config.model.v1.ha.HaService; import com.netflix.spinnaker.halyard.config.model.v1.ha.HaServices; import com.netflix.spinnaker.halyard.config.model.v1.node.*; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; import com.netflix.spinnaker.halyard.config.model.v1.security.*; import com.netflix.spinnaker.halyard.config.model.v1.webook.WebhookTrust; import com.netflix.spinnaker.halyard.core.DaemonOptions; @@ -1312,6 +1313,53 @@ public static Supplier deleteArtifactTemplate( }; } + public static Supplier> getPlugins(String deploymentName, boolean validate) { + return () -> { + Object rawPlugin = ResponseUnwrapper.get(getService().getPlugins(deploymentName, validate)); + return getObjectMapper().convertValue(rawPlugin, new TypeReference>() {}); + }; + } + + public static Supplier getPlugin( + String deploymentName, String pluginName, boolean validate) { + return () -> { + Object rawPlugin = + ResponseUnwrapper.get(getService().getPlugin(deploymentName, pluginName, validate)); + return getObjectMapper().convertValue(rawPlugin, Plugin.class); + }; + } + + public static Supplier addPlugin(String deploymentName, boolean validate, Plugin plugin) { + return () -> { + ResponseUnwrapper.get(getService().addPlugin(deploymentName, validate, plugin)); + return null; + }; + } + + public static Supplier setPlugin( + String deploymentName, String pluginName, boolean validate, Plugin plugin) { + return () -> { + ResponseUnwrapper.get(getService().setPlugin(deploymentName, pluginName, validate, plugin)); + return null; + }; + } + + public static Supplier deletePlugin( + String deploymentName, String pluginName, boolean validate) { + return () -> { + ResponseUnwrapper.get(getService().deletePlugin(deploymentName, pluginName, validate)); + return null; + }; + } + + public static Supplier setPluginEnableDisable( + String deploymentName, boolean validate, boolean enable) { + return () -> { + ResponseUnwrapper.get(getService().setPluginsEnabled(deploymentName, validate, enable)); + return null; + }; + } + private static DaemonService service; private static ObjectMapper objectMapper; diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/services/v1/DaemonService.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/services/v1/DaemonService.java index e5c57d63f7..46b432cb7c 100644 --- a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/services/v1/DaemonService.java +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/services/v1/DaemonService.java @@ -21,6 +21,7 @@ import com.netflix.spinnaker.halyard.config.model.v1.canary.Canary; import com.netflix.spinnaker.halyard.config.model.v1.ha.HaService; import com.netflix.spinnaker.halyard.config.model.v1.node.*; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; import com.netflix.spinnaker.halyard.config.model.v1.security.*; import com.netflix.spinnaker.halyard.config.model.v1.webook.WebhookTrust; import com.netflix.spinnaker.halyard.core.DaemonOptions; @@ -885,6 +886,41 @@ DaemonTask deleteArtifactTemplate( @Path("templateName") String templateName, @Query("validate") boolean validate); + @POST("/v1/config/deployments/{deploymentName}/plugins/") + DaemonTask addPlugin( + @Path("deploymentName") String deploymentName, + @Query("validate") boolean validate, + @Body Plugin plugin); + + @GET("/v1/config/deployments/{deploymentName}/plugins/") + DaemonTask getPlugins( + @Path("deploymentName") String deploymentName, @Query("validate") boolean validate); + + @GET("/v1/config/deployments/{deploymentName}/plugins/{pluginName}/") + DaemonTask getPlugin( + @Path("deploymentName") String deploymentName, + @Path("pluginName") String pluginName, + @Query("validate") boolean validate); + + @PUT("/v1/config/deployments/{deploymentName}/plugins/{pluginName}/") + DaemonTask setPlugin( + @Path("deploymentName") String deploymentName, + @Path("pluginName") String pluginName, + @Query("validate") boolean validate, + @Body Plugin plugin); + + @PUT("/v1/config/deployments/{deploymentName}/plugins/enabled/") + DaemonTask setPluginsEnabled( + @Path("deploymentName") String deploymentName, + @Query("validate") boolean validate, + @Body boolean enabled); + + @DELETE("/v1/config/deployments/{deploymentName}/plugins/{pluginName}/") + DaemonTask deletePlugin( + @Path("deploymentName") String deploymentName, + @Path("pluginName") String pluginName, + @Query("validate") boolean validate); + @GET("/v1/spin/install/latest") DaemonTask installSpin(); } diff --git a/halyard-cli/src/test/groovy/com/netflix/spinnaker/halyard/cli/command/v1/CommandTreeSpec.groovy b/halyard-cli/src/test/groovy/com/netflix/spinnaker/halyard/cli/command/v1/CommandTreeSpec.groovy index ab191d618c..6175df9f61 100644 --- a/halyard-cli/src/test/groovy/com/netflix/spinnaker/halyard/cli/command/v1/CommandTreeSpec.groovy +++ b/halyard-cli/src/test/groovy/com/netflix/spinnaker/halyard/cli/command/v1/CommandTreeSpec.groovy @@ -37,6 +37,10 @@ import com.netflix.spinnaker.halyard.cli.command.v1.config.security.authn.saml.S import com.netflix.spinnaker.halyard.cli.command.v1.config.security.authn.x509.X509Command import com.netflix.spinnaker.halyard.cli.command.v1.config.security.authz.AuthzCommand import com.netflix.spinnaker.halyard.cli.command.v1.config.security.ui.UiSecurityCommand +import com.netflix.spinnaker.halyard.cli.command.v1.plugins.AddPluginCommand +import com.netflix.spinnaker.halyard.cli.command.v1.plugins.DeletePluginCommand +import com.netflix.spinnaker.halyard.cli.command.v1.plugins.EditPluginCommand +import com.netflix.spinnaker.halyard.cli.command.v1.plugins.ListPluginsCommand import spock.lang.Specification import spock.lang.Unroll @@ -117,6 +121,11 @@ class CommandTreeSpec extends Specification { LdapCommand | "disable" | AuthnMethodEnableDisableCommand LdapCommand | "enable" | AuthnMethodEnableDisableCommand LdapCommand | "edit" | EditLdapCommand + + PluginCommand | "list" | ListPluginsCommand + PluginCommand | "edit" | EditPluginCommand + PluginCommand | "delete" | DeletePluginCommand + PluginCommand | "add" | AddPluginCommand } } diff --git a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/DeploymentConfiguration.java b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/DeploymentConfiguration.java index 612646def2..5566335cd7 100644 --- a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/DeploymentConfiguration.java +++ b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/DeploymentConfiguration.java @@ -86,6 +86,8 @@ public class DeploymentConfiguration extends Node { Canary canary = new Canary(); + Plugins plugins = new Plugins(); + Webhook webhook = new Webhook(); @Override diff --git a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/NodeFilter.java b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/NodeFilter.java index 2baef63bcc..92c66c8609 100644 --- a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/NodeFilter.java +++ b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/NodeFilter.java @@ -20,6 +20,7 @@ import com.netflix.spinnaker.halyard.config.model.v1.canary.Canary; import com.netflix.spinnaker.halyard.config.model.v1.ha.HaService; import com.netflix.spinnaker.halyard.config.model.v1.ha.HaServices; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; import com.netflix.spinnaker.halyard.config.model.v1.security.*; import com.netflix.spinnaker.halyard.config.model.v1.webook.WebhookTrust; import java.util.ArrayList; @@ -331,6 +332,23 @@ public NodeFilter setArtifactTemplate(String name) { return this; } + public NodeFilter setPlugin() { + matchers.add(Node.thisNodeAcceptor(Plugins.class)); + return this; + } + + public NodeFilter setPlugin(String name) { + matchers.add(Node.thisNodeAcceptor(Plugins.class)); + matchers.add(Node.namedNodeAcceptor(Plugin.class, name)); + return this; + } + + public NodeFilter withAnyPlugin() { + matchers.add(Node.thisNodeAcceptor(Plugins.class)); + matchers.add(Node.thisNodeAcceptor(Plugin.class)); + return this; + } + public NodeFilter() { withAnyHalconfigFile(); } diff --git a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/Plugins.java b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/Plugins.java new file mode 100644 index 0000000000..14cbbc9943 --- /dev/null +++ b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/Plugins.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.config.model.v1.node; + +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +public class Plugins extends Node { + + @Override + public String getNodeName() { + return "plugins"; + } + + @Override + public NodeIterator getChildren() { + return NodeIteratorFactory.makeListIterator( + plugins.stream().map(a -> (Node) a).collect(Collectors.toList())); + } + + private List plugins = new ArrayList<>(); + private boolean enabled; +} diff --git a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Manifest.java b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Manifest.java new file mode 100644 index 0000000000..93434e10dc --- /dev/null +++ b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Manifest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.config.model.v1.plugins; + +import com.netflix.spinnaker.halyard.config.problem.v1.ConfigProblemBuilder; +import com.netflix.spinnaker.halyard.core.error.v1.HalException; +import com.netflix.spinnaker.halyard.core.problem.v1.Problem; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import lombok.Data; +import lombok.Getter; + +@Data +public class Manifest { + public String name; + public String manifestVersion; + public List jars; + public Map options; + + static final String regex = "^[a-zA-Z0-9]+\\/[\\w-]+$"; + static final Pattern pattern = Pattern.compile(regex); + + public void validate() throws HalException { + + if (Stream.of(name, manifestVersion, jars).anyMatch(Objects::isNull)) { + throw new HalException( + new ConfigProblemBuilder( + Problem.Severity.FATAL, "Invalid plugin manifest, contains null values") + .build()); + } + + Matcher matcher = pattern.matcher(name); + + if (!matcher.find()) { + throw new HalException( + new ConfigProblemBuilder(Problem.Severity.FATAL, "Invalid plugin name: " + name).build()); + } + + if (!manifestVersion.equals(ManifestVersion.V1.getName())) { + throw new HalException( + new ConfigProblemBuilder( + Problem.Severity.FATAL, "Invalid manifest version for plugin: " + name) + .build()); + } + } + + public enum ManifestVersion { + V1("plugins/v1"); + + @Getter String name; + + @Override + public String toString() { + return this.name; + } + + ManifestVersion(String name) { + this.name = name; + } + } +} diff --git a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Plugin.java b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Plugin.java new file mode 100644 index 0000000000..978885699b --- /dev/null +++ b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Plugin.java @@ -0,0 +1,75 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.config.model.v1.plugins; + +import com.netflix.spinnaker.halyard.config.model.v1.node.Node; +import com.netflix.spinnaker.halyard.core.error.v1.HalException; +import com.netflix.spinnaker.halyard.core.problem.v1.Problem; +import com.netflix.spinnaker.halyard.core.problem.v1.ProblemBuilder; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.representer.Representer; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Plugin extends Node { + public String name; + public Boolean enabled; + public String manifestLocation; + + @Override + public String getNodeName() { + return name; + } + + public Manifest generateManifest() { + Representer representer = new Representer(); + representer.getPropertyUtils().setSkipMissingProperties(true); + Yaml yaml = new Yaml(new Constructor(Manifest.class), representer); + + InputStream manifestContents = downloadManifest(); + Manifest manifest = yaml.load(manifestContents); + manifest.validate(); + return manifest; + } + + public InputStream downloadManifest() { + try { + if (manifestLocation.startsWith("http:") || manifestLocation.startsWith("https:")) { + URL url = new URL(manifestLocation); + return url.openStream(); + } else { + return new FileInputStream(manifestLocation); + } + } catch (IOException e) { + throw new HalException( + new ProblemBuilder( + Problem.Severity.FATAL, + "Cannot get plugin manifest file from: " + + manifestLocation + + ": " + + e.getMessage()) + .build()); + } + } +} diff --git a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/services/v1/PluginService.java b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/services/v1/PluginService.java new file mode 100644 index 0000000000..10f9fbad1e --- /dev/null +++ b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/services/v1/PluginService.java @@ -0,0 +1,139 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.config.services.v1; + +import com.netflix.spinnaker.halyard.config.error.v1.ConfigNotFoundException; +import com.netflix.spinnaker.halyard.config.error.v1.IllegalConfigException; +import com.netflix.spinnaker.halyard.config.model.v1.node.DeploymentConfiguration; +import com.netflix.spinnaker.halyard.config.model.v1.node.NodeFilter; +import com.netflix.spinnaker.halyard.config.model.v1.node.Plugins; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; +import com.netflix.spinnaker.halyard.config.problem.v1.ConfigProblemBuilder; +import com.netflix.spinnaker.halyard.core.error.v1.HalException; +import com.netflix.spinnaker.halyard.core.problem.v1.Problem; +import com.netflix.spinnaker.halyard.core.problem.v1.ProblemSet; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PluginService { + private final LookupService lookupService; + private final ValidateService validateService; + private final DeploymentService deploymentService; + + private Plugins getPlugins(String deploymentName) { + NodeFilter filter = new NodeFilter().setDeployment(deploymentName).setPlugin(); + + return lookupService.getSingularNodeOrDefault( + filter, Plugins.class, Plugins::new, n -> setPlugins(deploymentName, n)); + } + + private void setPlugins(String deploymentName, Plugins newPlugins) { + DeploymentConfiguration deploymentConfiguration = + deploymentService.getDeploymentConfiguration(deploymentName); + deploymentConfiguration.setPlugins(newPlugins); + } + + public List getAllPlugins(String deploymentName) { + return getPlugins(deploymentName).getPlugins(); + } + + public Plugin getPlugin(String deploymentName, String pluginName) { + NodeFilter filter = new NodeFilter().setDeployment(deploymentName).setPlugin(pluginName); + List matchingPlugins = lookupService.getMatchingNodesOfType(filter, Plugin.class); + + switch (matchingPlugins.size()) { + case 0: + throw new ConfigNotFoundException( + new ConfigProblemBuilder( + Problem.Severity.FATAL, "No plugin with name \"" + pluginName + "\" was found") + .setRemediation("Create a new plugin with name \"" + pluginName + "\"") + .build()); + case 1: + return matchingPlugins.get(0); + default: + throw new IllegalConfigException( + new ConfigProblemBuilder( + Problem.Severity.FATAL, + "More than one plugin named \"" + pluginName + "\" was found") + .setRemediation( + "Manually delete/rename duplicate plugins with name \"" + + pluginName + + "\" in your halconfig file") + .build()); + } + } + + public void setPlugin(String deploymentName, String pluginName, Plugin newPlugin) { + List plugins = getAllPlugins(deploymentName); + for (int i = 0; i < plugins.size(); i++) { + if (plugins.get(i).getNodeName().equals(pluginName)) { + plugins.set(i, newPlugin); + return; + } + } + throw new HalException( + new ConfigProblemBuilder( + Problem.Severity.FATAL, "Plugin \"" + pluginName + "\" wasn't found") + .build()); + } + + public void deletePlugin(String deploymentName, String pluginName) { + List plugins = getAllPlugins(deploymentName); + boolean removed = plugins.removeIf(plugin -> plugin.getName().equals(pluginName)); + + if (!removed) { + throw new HalException( + new ConfigProblemBuilder( + Problem.Severity.FATAL, "Plugin \"" + pluginName + "\" wasn't found") + .build()); + } + } + + public void addPlugin(String deploymentName, Plugin newPlugin) { + String newPluginName = newPlugin.getName(); + List plugins = getAllPlugins(deploymentName); + for (Plugin plugin : plugins) { + if (plugin.getName().equals(newPluginName)) { + throw new HalException( + new ConfigProblemBuilder( + Problem.Severity.FATAL, "Plugin \"" + newPluginName + "\" already exists") + .build()); + } + } + plugins.add(newPlugin); + } + + public void setPluginsEnabled(String deploymentName, boolean validate, boolean enable) { + DeploymentConfiguration deploymentConfiguration = + deploymentService.getDeploymentConfiguration(deploymentName); + Plugins plugins = deploymentConfiguration.getPlugins(); + plugins.setEnabled(enable); + } + + public ProblemSet validateAllPlugins(String deploymentName) { + NodeFilter filter = new NodeFilter().setDeployment(deploymentName).withAnyPlugin(); + return validateService.validateMatchingFilter(filter); + } + + public ProblemSet validatePlugin(String deploymentName, String pluginName) { + NodeFilter filter = new NodeFilter().setDeployment(deploymentName).setPlugin(pluginName); + return validateService.validateMatchingFilter(filter); + } +} diff --git a/halyard-config/src/test/groovy/com/netflix/spinnaker/halyard/config/model/v1/plugins/ManifestSpec.groovy b/halyard-config/src/test/groovy/com/netflix/spinnaker/halyard/config/model/v1/plugins/ManifestSpec.groovy new file mode 100644 index 0000000000..091e72b381 --- /dev/null +++ b/halyard-config/src/test/groovy/com/netflix/spinnaker/halyard/config/model/v1/plugins/ManifestSpec.groovy @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Bol.com + * + * Licensed 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 com.netflix.spinnaker.halyard.config.model.v1.plugins + +import com.netflix.spinnaker.halyard.core.error.v1.HalException +import spock.lang.Specification + +class ManifestSpec extends Specification { + + void "plugin manifests must have the required fields"() { + setup: + Manifest manifest = new Manifest(); + + when: + manifest.setJars(jars) + manifest.setManifestVersion(manifestVersion) + manifest.setName(name) + manifest.setOptions(options) + manifest.validate() + + then: + thrown HalException + + where: + name | manifestVersion | jars | options + "foo" | "plugins/v1" | Arrays.asList("jar") | null + "foo/bar" | null | Arrays.asList("jar") | null + "foo/bar" | "plugins/v1" | null | null + } + + void "plugin manifests pass validation"() { + setup: + Manifest manifest = new Manifest(); + + when: + manifest.setJars(Arrays.asList("one", "two")) + manifest.setManifestVersion("plugins/v1") + manifest.setName("foo/bar") + manifest.validate() + + then: + noExceptionThrown() + + } +} diff --git a/halyard-config/src/test/groovy/com/netflix/spinnaker/halyard/config/services/v1/PluginServiceSpec.groovy b/halyard-config/src/test/groovy/com/netflix/spinnaker/halyard/config/services/v1/PluginServiceSpec.groovy new file mode 100644 index 0000000000..735805ca35 --- /dev/null +++ b/halyard-config/src/test/groovy/com/netflix/spinnaker/halyard/config/services/v1/PluginServiceSpec.groovy @@ -0,0 +1,176 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.config.services.v1 + +import com.netflix.spinnaker.halyard.config.error.v1.ConfigNotFoundException +import spock.lang.Specification + +class PluginServiceSpec extends Specification { + final String DEPLOYMENT = "default" + final HalconfigParserMocker mocker = new HalconfigParserMocker() + + LookupService getMockLookupService(String config) { + def lookupService = new LookupService() + lookupService.parser = mocker.mockHalconfigParser(config) + return lookupService + } + + PluginService makePluginService(String config) { + def lookupService = getMockLookupService(config) + def deploymentService = new DeploymentService() + deploymentService.lookupService = lookupService + new PluginService(lookupService, new ValidateService(), deploymentService) + } + + def "load an existing plugin node"() { + setup: + String config = """ +halyardVersion: 1 +currentDeployment: $DEPLOYMENT +deploymentConfigurations: +- name: $DEPLOYMENT + version: 1 + providers: null + plugins: + plugins: + - name: test-plugin + manifestLocation: /home/user/test-plugin.yaml +""" + def pluginService = makePluginService(config) + + when: + def result = pluginService.getAllPlugins(DEPLOYMENT) + + then: + result != null + result.size() == 1 + result[0].getName() == "test-plugin" + result[0].getManifestLocation() == "/home/user/test-plugin.yaml" + + when: + result = pluginService.getPlugin(DEPLOYMENT, "test-plugin") + + then: + result != null + result.getName() == "test-plugin" + result.getManifestLocation() == "/home/user/test-plugin.yaml" + + when: + pluginService.getPlugin(DEPLOYMENT, 'non-existent-plugin') + + then: + thrown(ConfigNotFoundException) + } + + def "no error if plugin is empty"() { + setup: + String config = """ +halyardVersion: 1 +currentDeployment: $DEPLOYMENT +deploymentConfigurations: +- name: $DEPLOYMENT + version: 1 + providers: null + plugins: + plugins: [] +""" + def pluginService = makePluginService(config) + + when: + def result = pluginService.getAllPlugins(DEPLOYMENT) + + then: + result != null + result.size() == 0 + + when: + pluginService.getPlugin(DEPLOYMENT, "test-plugin") + + then: + thrown(ConfigNotFoundException) + } + + def "no error if plugin is missing"() { + setup: + String config = """ +halyardVersion: 1 +currentDeployment: $DEPLOYMENT +deploymentConfigurations: +- name: $DEPLOYMENT + version: 1 + providers: null + plugins: +""" + def pluginService = makePluginService(config) + + when: + def result = pluginService.getAllPlugins(DEPLOYMENT) + + then: + result != null + result.size() == 0 + + when: + + pluginService.getPlugin(DEPLOYMENT, "test-template") + + then: + thrown(ConfigNotFoundException) + } + + def "multiple templates are correctly parsed"() { + setup: + String config = """ +halyardVersion: 1 +currentDeployment: $DEPLOYMENT +deploymentConfigurations: +- name: $DEPLOYMENT + version: 1 + providers: null + plugins: + plugins: + - name: test-plugin + manifestLocation: /home/user/test-plugin.yaml + - name: test-plugin-2 + manifestLocation: /home/user/test-plugin-2.yaml +""" + def pluginService = makePluginService(config) + + when: + def result = pluginService.getAllPlugins(DEPLOYMENT) + + then: + result != null + result.size() == 2 + + when: + result = pluginService.getPlugin(DEPLOYMENT, "test-plugin") + + then: + result != null + result.getName() == "test-plugin" + result.getManifestLocation() == "/home/user/test-plugin.yaml" + + when: + result = pluginService.getPlugin(DEPLOYMENT, "test-plugin-2") + + then: + result != null + result.getName() == "test-plugin-2" + result.getManifestLocation() == "/home/user/test-plugin-2.yaml" + } +} diff --git a/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/OrcaProfileFactory.java b/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/OrcaProfileFactory.java index 8d564747af..c49f9c1a45 100644 --- a/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/OrcaProfileFactory.java +++ b/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/OrcaProfileFactory.java @@ -19,15 +19,21 @@ import com.netflix.spinnaker.halyard.config.model.v1.node.DeploymentConfiguration; import com.netflix.spinnaker.halyard.config.model.v1.node.Features; import com.netflix.spinnaker.halyard.config.model.v1.node.Webhook; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; import com.netflix.spinnaker.halyard.config.model.v1.providers.aws.AwsProvider; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.SpinnakerArtifact; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.SpinnakerRuntimeSettings; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.profile.integrations.IntegrationsConfigWrapper; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.Data; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +@Slf4j @Component public class OrcaProfileFactory extends SpringProfileFactory { @Override @@ -73,6 +79,20 @@ protected void setProfile( profile.appendContents("pipelineTemplates.enabled: " + pipelineTemplates); // For backward compatibility profile.appendContents("pipelineTemplate.enabled: " + pipelineTemplates); + + final List plugins = deploymentConfiguration.getPlugins().getPlugins(); + Map fullyRenderedYaml = new LinkedHashMap<>(); + Map pluginMetadata = + plugins.stream() + .filter(p -> p.getEnabled()) + .filter(p -> !p.getManifestLocation().isEmpty()) + .map(p -> p.generateManifest()) + .collect(Collectors.toConcurrentMap(m -> m.getName(), m -> m.getOptions())); + + fullyRenderedYaml.put("plugins", pluginMetadata); + + profile.appendContents( + yamlToString(deploymentConfiguration.getName(), profile, fullyRenderedYaml)); } @Data diff --git a/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/PluginProfileFactory.java b/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/PluginProfileFactory.java new file mode 100644 index 0000000000..55460bcf83 --- /dev/null +++ b/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/PluginProfileFactory.java @@ -0,0 +1,78 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.deploy.spinnaker.v1.profile; + +import com.netflix.spinnaker.halyard.config.model.v1.node.DeploymentConfiguration; +import com.netflix.spinnaker.halyard.config.model.v1.node.Plugins; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Manifest; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; +import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.SpinnakerArtifact; +import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.SpinnakerRuntimeSettings; +import java.util.*; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class PluginProfileFactory extends StringBackedProfileFactory { + @Override + protected void setProfile( + Profile profile, + DeploymentConfiguration deploymentConfiguration, + SpinnakerRuntimeSettings endpoints) { + final Plugins plugins = deploymentConfiguration.getPlugins(); + + Map>> fullyRenderedYaml = new HashMap<>(); + + List> pluginMetadata = + plugins.getPlugins().stream() + .filter(p -> p.getEnabled()) + .filter(p -> !p.getManifestLocation().isEmpty()) + .map(p -> composeMetadata(p, p.generateManifest())) + .collect(Collectors.toList()); + + fullyRenderedYaml.put("plugins", pluginMetadata); + + profile.appendContents( + yamlToString(deploymentConfiguration.getName(), profile, fullyRenderedYaml)); + } + + private Map composeMetadata(Plugin plugin, Manifest manifest) { + Map metadata = new LinkedHashMap<>(); + metadata.put("enabled", plugin.getEnabled()); + metadata.put("name", manifest.getName()); + metadata.put("jars", manifest.getJars()); + metadata.put("manifestVersion", manifest.getManifestVersion()); + return metadata; + } + + @Override + protected String getRawBaseProfile() { + return ""; + } + + @Override + public SpinnakerArtifact getArtifact() { + return SpinnakerArtifact.SPINNAKER; + } + + @Override + protected String commentPrefix() { + return "## "; + } +} diff --git a/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/service/OrcaService.java b/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/service/OrcaService.java index ff964c23c5..aead64d01d 100644 --- a/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/service/OrcaService.java +++ b/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/service/OrcaService.java @@ -21,6 +21,7 @@ import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.SpinnakerArtifact; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.SpinnakerRuntimeSettings; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.profile.OrcaProfileFactory; +import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.profile.PluginProfileFactory; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.profile.Profile; import java.nio.file.Paths; import java.util.HashMap; @@ -39,6 +40,8 @@ public abstract class OrcaService extends SpringService { @Autowired OrcaProfileFactory orcaProfileFactory; + @Autowired PluginProfileFactory pluginProfileFactory; + @Override public SpinnakerArtifact getArtifact() { return SpinnakerArtifact.ORCA; @@ -72,6 +75,14 @@ public List getProfiles( appendReadonlyClouddriver(profile, deploymentConfiguration, endpoints); profiles.add(profile); + + // Plugins + String pluginFilename = "plugins.yml"; + String pluginPath = Paths.get(getConfigOutputPath(), pluginFilename).toString(); + Profile pluginProfile = + pluginProfileFactory.getProfile( + pluginFilename, pluginPath, deploymentConfiguration, endpoints); + profiles.add(pluginProfile); return profiles; } diff --git a/halyard-web/src/main/java/com/netflix/spinnaker/halyard/controllers/v1/PluginsController.java b/halyard-web/src/main/java/com/netflix/spinnaker/halyard/controllers/v1/PluginsController.java new file mode 100644 index 0000000000..f157a8b738 --- /dev/null +++ b/halyard-web/src/main/java/com/netflix/spinnaker/halyard/controllers/v1/PluginsController.java @@ -0,0 +1,121 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed 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 com.netflix.spinnaker.halyard.controllers.v1; + +import com.netflix.spinnaker.halyard.config.config.v1.HalconfigDirectoryStructure; +import com.netflix.spinnaker.halyard.config.config.v1.HalconfigParser; +import com.netflix.spinnaker.halyard.config.model.v1.node.Halconfig; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; +import com.netflix.spinnaker.halyard.config.services.v1.PluginService; +import com.netflix.spinnaker.halyard.core.tasks.v1.DaemonTask; +import com.netflix.spinnaker.halyard.models.v1.ValidationSettings; +import com.netflix.spinnaker.halyard.util.v1.GenericDeleteRequest; +import com.netflix.spinnaker.halyard.util.v1.GenericEnableDisableRequest; +import com.netflix.spinnaker.halyard.util.v1.GenericGetRequest; +import com.netflix.spinnaker.halyard.util.v1.GenericUpdateRequest; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/v1/config/deployments/{deploymentName:.+}/plugins") +@RequiredArgsConstructor +public class PluginsController { + private final PluginService pluginService; + private final HalconfigDirectoryStructure halconfigDirectoryStructure; + private final HalconfigParser halconfigParser; + + @RequestMapping(value = "/", method = RequestMethod.GET) + DaemonTask> getPlugins( + @PathVariable String deploymentName, @ModelAttribute ValidationSettings validationSettings) { + return GenericGetRequest.>builder() + .getter(() -> pluginService.getAllPlugins(deploymentName)) + .validator(() -> pluginService.validateAllPlugins(deploymentName)) + .description("Get plugins") + .build() + .execute(validationSettings); + } + + @RequestMapping(value = "/{pluginName:.+}", method = RequestMethod.GET) + DaemonTask getPlugin( + @PathVariable String deploymentName, + @PathVariable String pluginName, + @ModelAttribute ValidationSettings validationSettings) { + return GenericGetRequest.builder() + .getter(() -> pluginService.getPlugin(deploymentName, pluginName)) + .validator(() -> pluginService.validatePlugin(deploymentName, pluginName)) + .description("Get the " + pluginName + " plugin") + .build() + .execute(validationSettings); + } + + @RequestMapping(value = "/{pluginName:.+}", method = RequestMethod.PUT) + DaemonTask setPlugin( + @PathVariable String deploymentName, + @PathVariable String pluginName, + @ModelAttribute ValidationSettings validationSettings, + @RequestBody Plugin plugin) { + return GenericUpdateRequest.builder(halconfigParser) + .stagePath(halconfigDirectoryStructure.getStagingPath(deploymentName)) + .updater(t -> pluginService.setPlugin(deploymentName, pluginName, t)) + .validator(() -> pluginService.validatePlugin(deploymentName, pluginName)) + .description("Edit the " + pluginName + " plugin") + .build() + .execute(validationSettings, plugin); + } + + @RequestMapping(value = "/", method = RequestMethod.POST) + DaemonTask addPlugin( + @PathVariable String deploymentName, + @ModelAttribute ValidationSettings validationSettings, + @RequestBody Plugin plugin) { + return GenericUpdateRequest.builder(halconfigParser) + .stagePath(halconfigDirectoryStructure.getStagingPath(deploymentName)) + .updater(t -> pluginService.addPlugin(deploymentName, t)) + .validator(() -> pluginService.validatePlugin(deploymentName, plugin.getName())) + .description("Add the " + plugin.getName() + " plugin") + .build() + .execute(validationSettings, plugin); + } + + @RequestMapping(value = "/{pluginName:.+}", method = RequestMethod.DELETE) + DaemonTask deletePlugin( + @PathVariable String deploymentName, + @PathVariable String pluginName, + @ModelAttribute ValidationSettings validationSettings) { + return GenericDeleteRequest.builder(halconfigParser) + .stagePath(halconfigDirectoryStructure.getStagingPath(deploymentName)) + .deleter(() -> pluginService.deletePlugin(deploymentName, pluginName)) + .validator(() -> pluginService.validateAllPlugins(deploymentName)) + .description("Delete the " + pluginName + " plugin") + .build() + .execute(validationSettings); + } + + @RequestMapping(value = "/enabled", method = RequestMethod.PUT) + DaemonTask setPluginsEnabled( + @PathVariable String deploymentName, + @ModelAttribute ValidationSettings validationSettings, + @RequestBody Boolean enabled) { + return GenericEnableDisableRequest.builder(halconfigParser) + .updater(t -> pluginService.setPluginsEnabled(deploymentName, false, enabled)) + .validator(() -> pluginService.validateAllPlugins(deploymentName)) + .description("Enable or disable plugins") + .build() + .execute(validationSettings, enabled); + } +} From ab615bc4dbbdbde2713b521387f66afed489ac87 Mon Sep 17 00:00:00 2001 From: Cameron Motevasselani Date: Tue, 13 Aug 2019 20:02:43 -0700 Subject: [PATCH 2/2] chore(refactor): use toMap instead of a concurrentMap collector --- .../cli/command/v1/plugins/AbstractHasPluginCommand.java | 2 +- .../halyard/deploy/spinnaker/v1/profile/OrcaProfileFactory.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AbstractHasPluginCommand.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AbstractHasPluginCommand.java index 3bc34630ac..e77ea0d5ee 100644 --- a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AbstractHasPluginCommand.java +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/plugins/AbstractHasPluginCommand.java @@ -29,7 +29,7 @@ @Parameters(separators = "=") public abstract class AbstractHasPluginCommand extends AbstractConfigCommand { @Parameter(description = "The name of the plugin to operate on.", arity = 1) - List plugins = new ArrayList<>(); + private List plugins = new ArrayList<>(); @Override public String getMainParameter() { diff --git a/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/OrcaProfileFactory.java b/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/OrcaProfileFactory.java index c49f9c1a45..1c67cacc7e 100644 --- a/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/OrcaProfileFactory.java +++ b/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/OrcaProfileFactory.java @@ -87,7 +87,7 @@ protected void setProfile( .filter(p -> p.getEnabled()) .filter(p -> !p.getManifestLocation().isEmpty()) .map(p -> p.generateManifest()) - .collect(Collectors.toConcurrentMap(m -> m.getName(), m -> m.getOptions())); + .collect(Collectors.toMap(m -> m.getName(), m -> m.getOptions())); fullyRenderedYaml.put("plugins", pluginMetadata);