From 8a73d5aac14664f5af6df47d598b38c5b564207b Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Thu, 28 Mar 2024 20:26:43 -0300 Subject: [PATCH 1/4] add plugin group --- build.gradle | 3 +- .../ansible/AnsibleRunnerContextBuilder.java | 56 ++++++++++++-- ...AnsiblePlaybookInlineWorkflowNodeStep.java | 17 ++++- .../AnsiblePlaybookInlineWorkflowStep.java | 17 ++++- .../AnsiblePlaybookWorflowNodeStep.java | 17 ++++- .../plugin/AnsiblePlaybookWorkflowStep.java | 17 ++++- .../ansible/plugin/AnsiblePluginGroup.java | 75 +++++++++++++++++++ 7 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePluginGroup.java diff --git a/build.gradle b/build.gradle index e7989e15..4fded657 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,8 @@ ext.pluginClassNames = [ 'com.rundeck.plugins.ansible.plugin.AnsibleModuleWorkflowStep', 'com.rundeck.plugins.ansible.plugin.AnsiblePlaybookWorflowNodeStep', 'com.rundeck.plugins.ansible.plugin.AnsiblePlaybookInlineWorkflowNodeStep', - 'com.rundeck.plugins.ansible.logging.AnsibleSetStatsFilterPlugin' + 'com.rundeck.plugins.ansible.logging.AnsibleSetStatsFilterPlugin', + 'com.rundeck.plugins.ansible.plugin.AnsiblePluginGroup' ].join(',') apply plugin: 'java' diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java index 77f3294f..3d38face 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java @@ -21,6 +21,7 @@ import java.nio.file.Files; import java.nio.file.Paths; +import com.rundeck.plugins.ansible.plugin.AnsiblePluginGroup; import lombok.Getter; import org.rundeck.storage.api.Path; @@ -39,6 +40,8 @@ public class AnsibleRunnerContextBuilder { private final Collection nodes; private final Collection tempFiles; + private AnsiblePluginGroup pluginGroup; + public AnsibleRunnerContextBuilder(final ExecutionContext context, final Framework framework, INodeSet nodes, final Map configuration) { this.context = context; @@ -58,6 +61,16 @@ public AnsibleRunnerContextBuilder(final INodeEntry node, final ExecutionContext this.tempFiles = new LinkedList<>(); } + public AnsibleRunnerContextBuilder(final ExecutionContext context, final Framework framework, INodeSet nodes, final Map configuration, final AnsiblePluginGroup pluginGroup) { + this.context = context; + this.framework = framework; + this.frameworkProject = context.getFrameworkProject(); + this.jobConf = configuration; + this.nodes = nodes.getNodes(); + this.tempFiles = new LinkedList<>(); + this.pluginGroup = pluginGroup; + } + private byte[] loadStoragePathData(final String passwordStoragePath) throws IOException { if (null == passwordStoragePath) { return null; @@ -667,7 +680,7 @@ public String getLimit() throws ConfigurationException { public String getConfigFile() { - final String configFile; + String configFile; configFile = PropertyResolver.resolveProperty( AnsibleDescribable.ANSIBLE_CONFIG_FILE_PATH, null, @@ -680,6 +693,18 @@ public String getConfigFile() { if (null != configFile && configFile.contains("${")) { return DataContextUtils.replaceDataReferencesInString(configFile, getContext().getDataContext()); } + + if(null == configFile || configFile.isEmpty()) { + if (this.pluginGroup != null && this.pluginGroup.getAnsibleConfigFilePath() != null && !this.pluginGroup.getAnsibleConfigFilePath().isEmpty()) { + this.context.getExecutionLogger().log( + 4, "plugin group set getAnsibleConfigFilePath: " + this.pluginGroup.getAnsibleConfigFilePath() + ); + + configFile = this.pluginGroup.getAnsibleConfigFilePath(); + } + } + + return configFile; } @@ -705,11 +730,21 @@ public String getBinariesFilePath() { getNode(), getJobConf() ); - if (null != binariesFilePathStr) { - if (binariesFilePathStr.contains("${")) { - return DataContextUtils.replaceDataReferencesInString(binariesFilePathStr, getContext().getDataContext()); + + if (null != binariesFilePathStr && binariesFilePathStr.contains("${")) { + return DataContextUtils.replaceDataReferencesInString(binariesFilePathStr, getContext().getDataContext()); + } + + if(null == binariesFilePathStr || binariesFilePathStr.isEmpty()){ + if(this.pluginGroup!=null && this.pluginGroup.getAnsibleBinariesDirPath()!= null && !this.pluginGroup.getAnsibleBinariesDirPath().isEmpty()){ + this.context.getExecutionLogger().log( + 4, "plugin group set getAnsibleBinariesDirPath: "+this.pluginGroup.getAnsibleBinariesDirPath() + ); + binariesFilePathStr = this.pluginGroup.getAnsibleBinariesDirPath(); } } + + return binariesFilePathStr; } @@ -808,7 +843,7 @@ public String getPassphraseStorageData(String storagePath) throws ConfigurationE } public boolean encryptExtraVars() throws ConfigurationException { - return PropertyResolver.resolveBooleanProperty( + boolean encryptExtraVars = PropertyResolver.resolveBooleanProperty( AnsibleDescribable.ANSIBLE_ENCRYPT_EXTRA_VARS, false, getFrameworkProject(), @@ -816,6 +851,17 @@ public boolean encryptExtraVars() throws ConfigurationException { getNode(), getJobConf() ); + + if(!encryptExtraVars){ + if(this.pluginGroup!=null && this.pluginGroup.getEncryptExtraVars()!= null && this.pluginGroup.getEncryptExtraVars()){ + this.context.getExecutionLogger().log( + 4, "plugin group set getEncryptExtraVars: "+this.pluginGroup.getEncryptExtraVars() + ); + encryptExtraVars = this.pluginGroup.getEncryptExtraVars(); + } + } + + return encryptExtraVars; } public Map getListOptions(){ diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java index 20ef6a10..9ecb0200 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java @@ -2,6 +2,7 @@ import com.dtolabs.rundeck.core.execution.ExecutionContext; import com.dtolabs.rundeck.core.execution.proxy.ProxyRunnerPlugin; +import com.dtolabs.rundeck.plugins.config.ConfiguredBy; import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleException; import com.rundeck.plugins.ansible.ansible.AnsibleRunner; @@ -22,7 +23,9 @@ import java.util.Map; @Plugin(name = AnsiblePlaybookInlineWorkflowNodeStep.SERVICE_PROVIDER_NAME, service = ServiceNameConstants.WorkflowNodeStep) -public class AnsiblePlaybookInlineWorkflowNodeStep implements NodeStepPlugin, AnsibleDescribable, ProxyRunnerPlugin { +public class AnsiblePlaybookInlineWorkflowNodeStep implements NodeStepPlugin, AnsibleDescribable, ProxyRunnerPlugin, ConfiguredBy { + + private AnsiblePluginGroup pluginGroup; public static final String SERVICE_PROVIDER_NAME = "com.batix.rundeck.plugins.AnsiblePlaybookInlineWorkflowNodeStep"; @@ -30,6 +33,7 @@ public class AnsiblePlaybookInlineWorkflowNodeStep implements NodeStepPlugin, An static { DescriptionBuilder builder = DescriptionBuilder.builder(); + builder.pluginGroup(AnsiblePluginGroup.class); builder.name(SERVICE_PROVIDER_NAME); builder.title("Ansible Playbook Inline Workflow Node Step"); builder.description("Runs an Inline Ansible Playbook."); @@ -81,7 +85,11 @@ public void executeNodeStep( configuration.put(AnsibleDescribable.ANSIBLE_DEBUG,"False"); } - AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), + context.getFramework(), + context.getNodes(), + configuration, + pluginGroup); try { runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); @@ -124,4 +132,9 @@ public Map getRuntimeProperties(ExecutionContext context) { public Map getRuntimeFrameworkProperties(ExecutionContext context) { return AnsibleUtil.getRuntimeProperties(context, AnsibleDescribable.FWK_PROP_PREFIX); } + + @Override + public void setPluginGroup(AnsiblePluginGroup ansiblePluginGroup) { + this.pluginGroup = ansiblePluginGroup; + } } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java index afef685c..01c11c99 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java @@ -2,6 +2,7 @@ import com.dtolabs.rundeck.core.execution.ExecutionContext; import com.dtolabs.rundeck.core.execution.proxy.ProxyRunnerPlugin; +import com.dtolabs.rundeck.plugins.config.ConfiguredBy; import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleException; import com.rundeck.plugins.ansible.ansible.AnsibleRunner; @@ -22,14 +23,17 @@ import java.util.Objects; @Plugin(name = AnsiblePlaybookInlineWorkflowStep.SERVICE_PROVIDER_NAME, service = ServiceNameConstants.WorkflowStep) -public class AnsiblePlaybookInlineWorkflowStep implements StepPlugin, AnsibleDescribable, ProxyRunnerPlugin { +public class AnsiblePlaybookInlineWorkflowStep implements StepPlugin, AnsibleDescribable, ProxyRunnerPlugin, ConfiguredBy { public static final String SERVICE_PROVIDER_NAME = "com.batix.rundeck.plugins.AnsiblePlaybookInlineWorkflowStep"; public static Description DESC = null; + private AnsiblePluginGroup pluginGroup; + static { DescriptionBuilder builder = DescriptionBuilder.builder(); + builder.pluginGroup(AnsiblePluginGroup.class); builder.name(SERVICE_PROVIDER_NAME); builder.title("Ansible Playbook Inline"); builder.description("Runs an Inline Ansible Playbook."); @@ -83,7 +87,11 @@ public void executeStep(PluginStepContext context, Map configura configuration.put(AnsibleDescribable.ANSIBLE_DEBUG, "False"); } - AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), + context.getFramework(), + context.getNodes(), + configuration, + pluginGroup); try { runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); @@ -135,4 +143,9 @@ public Map getRuntimeProperties(ExecutionContext context) { public Map getRuntimeFrameworkProperties(ExecutionContext context) { return AnsibleUtil.getRuntimeProperties(context, AnsibleDescribable.FWK_PROP_PREFIX); } + + @Override + public void setPluginGroup(AnsiblePluginGroup ansiblePluginGroup) { + this.pluginGroup = ansiblePluginGroup; + } } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java index 6299919f..b21f864a 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java @@ -2,6 +2,7 @@ import com.dtolabs.rundeck.core.execution.ExecutionContext; import com.dtolabs.rundeck.core.execution.proxy.ProxyRunnerPlugin; +import com.dtolabs.rundeck.plugins.config.ConfiguredBy; import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleException; import com.rundeck.plugins.ansible.ansible.AnsibleRunner; @@ -21,14 +22,17 @@ import java.util.Map; @Plugin(name = AnsiblePlaybookWorflowNodeStep.SERVICE_PROVIDER_NAME, service = ServiceNameConstants.WorkflowNodeStep) -public class AnsiblePlaybookWorflowNodeStep implements NodeStepPlugin, AnsibleDescribable, ProxyRunnerPlugin { +public class AnsiblePlaybookWorflowNodeStep implements NodeStepPlugin, AnsibleDescribable, ProxyRunnerPlugin, ConfiguredBy { public static final String SERVICE_PROVIDER_NAME = "com.batix.rundeck.plugins.AnsiblePlaybookWorflowNodeStep"; public static Description DESC = null; + private AnsiblePluginGroup pluginGroup; + static { DescriptionBuilder builder = DescriptionBuilder.builder(); + builder.pluginGroup(AnsiblePluginGroup.class); builder.name(SERVICE_PROVIDER_NAME); builder.title("Ansible Playbook Workflow Node Step."); builder.description("Runs an Ansible Playbook"); @@ -79,7 +83,11 @@ public void executeNodeStep( } AnsibleRunnerContextBuilder - contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); + contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), + context.getFramework(), + context.getNodes(), + configuration, + pluginGroup); try { runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); @@ -114,4 +122,9 @@ public Map getRuntimeProperties(ExecutionContext context) { public Map getRuntimeFrameworkProperties(ExecutionContext context) { return AnsibleUtil.getRuntimeProperties(context, AnsibleDescribable.FWK_PROP_PREFIX); } + + @Override + public void setPluginGroup(AnsiblePluginGroup ansiblePluginGroup) { + this.pluginGroup = ansiblePluginGroup; + } } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java index 8b4ca4bb..efba7303 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java @@ -2,6 +2,7 @@ import com.dtolabs.rundeck.core.execution.ExecutionContext; import com.dtolabs.rundeck.core.execution.proxy.ProxyRunnerPlugin; +import com.dtolabs.rundeck.plugins.config.ConfiguredBy; import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleException; import com.rundeck.plugins.ansible.ansible.AnsibleRunner; @@ -21,14 +22,17 @@ import java.util.Map; @Plugin(name = AnsiblePlaybookWorkflowStep.SERVICE_PROVIDER_NAME, service = ServiceNameConstants.WorkflowStep) -public class AnsiblePlaybookWorkflowStep implements StepPlugin, AnsibleDescribable, ProxyRunnerPlugin { +public class AnsiblePlaybookWorkflowStep implements StepPlugin, AnsibleDescribable, ProxyRunnerPlugin, ConfiguredBy { public static final String SERVICE_PROVIDER_NAME = "com.batix.rundeck.plugins.AnsiblePlaybookWorkflowStep"; public static Description DESC = null; + private AnsiblePluginGroup pluginGroup; + static { DescriptionBuilder builder = DescriptionBuilder.builder(); + builder.pluginGroup(AnsiblePluginGroup.class); builder.name(SERVICE_PROVIDER_NAME); builder.title("Ansible Playbook"); builder.description("Runs an Ansible Playbook."); @@ -82,7 +86,11 @@ public void executeStep(PluginStepContext context, Map configura configuration.put(AnsibleDescribable.ANSIBLE_DEBUG, "False"); } - AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), + context.getFramework(), + context.getNodes(), + configuration, + pluginGroup); try { runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); @@ -129,4 +137,9 @@ public Map getRuntimeProperties(ExecutionContext context) { public Map getRuntimeFrameworkProperties(ExecutionContext context) { return AnsibleUtil.getRuntimeProperties(context, AnsibleDescribable.FWK_PROP_PREFIX); } + + @Override + public void setPluginGroup(AnsiblePluginGroup ansiblePluginGroup) { + this.pluginGroup = ansiblePluginGroup; + } } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePluginGroup.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePluginGroup.java new file mode 100644 index 00000000..8bdf57c4 --- /dev/null +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePluginGroup.java @@ -0,0 +1,75 @@ +package com.rundeck.plugins.ansible.plugin; + +import com.dtolabs.rundeck.core.plugins.Plugin; +import com.dtolabs.rundeck.core.plugins.configuration.Describable; +import com.dtolabs.rundeck.core.plugins.configuration.Description; +import com.dtolabs.rundeck.core.plugins.configuration.PluginAdapterUtility; +import com.dtolabs.rundeck.plugins.ServiceNameConstants; +import com.dtolabs.rundeck.plugins.config.PluginGroup; +import com.dtolabs.rundeck.plugins.descriptions.PluginDescription; +import com.dtolabs.rundeck.plugins.descriptions.PluginProperty; +import com.dtolabs.rundeck.plugins.util.DescriptionBuilder; + + +@Plugin(service = ServiceNameConstants.PluginGroup, name = "Ansible Project Configuration") +@PluginDescription(title = "Ansible", description = "Plugin basic ansible configurations") +public class AnsiblePluginGroup implements PluginGroup, Describable { + + public String getAnsibleConfigFilePath() { + return ansibleConfigFilePath; + } + + public void setAnsibleConfigFilePath(String ansibleConfigFilePath) { + this.ansibleConfigFilePath = ansibleConfigFilePath; + } + + public String getAnsibleBinariesDirPath() { + return ansibleBinariesDirPath; + } + + public void setAnsibleBinariesDirPath(String ansibleBinariesDirPath) { + this.ansibleBinariesDirPath = ansibleBinariesDirPath; + } + + public Boolean getEncryptExtraVars() { + return encryptExtraVars; + } + + public void setEncryptExtraVars(Boolean encryptExtraVars) { + this.encryptExtraVars = encryptExtraVars; + } + + @PluginProperty( + title = "Ansible config file path", + description = "Set ansible config file path." + ) + //@RenderingOptions({ + // @RenderingOption(key = StringRenderingConstants.GROUP_NAME, value = "Authentication") + //}) + String ansibleConfigFilePath; + + @PluginProperty( + title = "Ansible binaries directory path", + description = "Set ansible binaries directory path." + ) + //@RenderingOptions({ + // @RenderingOption(key = StringRenderingConstants.GROUP_NAME, value = "Authentication") + //}) + String ansibleBinariesDirPath; + + @PluginProperty( + title = "Encrypt Extra Vars.", + description = "Encrypt the value of the extra vars keys." + ) + //@RenderingOptions({ + // @RenderingOption(key = StringRenderingConstants.GROUP_NAME, value = "Authentication") + //}) + Boolean encryptExtraVars; + + @Override + public Description getDescription() { + DescriptionBuilder builder = DescriptionBuilder.builder(); + Description description = PluginAdapterUtility.buildDescription(this, builder); + return builder.build(); + } +} From 1f4080c1f3ced04dff961f7d99371134d13e88d1 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Mon, 1 Apr 2024 09:52:15 -0300 Subject: [PATCH 2/4] add unit test --- .../AnsibleRunnerContextBuilderSpec.groovy | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilderSpec.groovy diff --git a/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilderSpec.groovy b/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilderSpec.groovy new file mode 100644 index 00000000..0b7f9f46 --- /dev/null +++ b/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilderSpec.groovy @@ -0,0 +1,83 @@ +package com.rundeck.plugins.ansible.ansible + +import com.dtolabs.rundeck.core.common.Framework +import com.dtolabs.rundeck.core.common.INodeSet +import com.dtolabs.rundeck.core.execution.ExecutionContext +import com.dtolabs.rundeck.plugins.step.PluginStepContext +import com.rundeck.plugins.ansible.plugin.AnsiblePluginGroup +import spock.lang.Specification +import com.dtolabs.rundeck.core.execution.ExecutionLogger + +class AnsibleRunnerContextBuilderSpec extends Specification { + + def "test plugin group"(){ + given: + + PluginStepContext context = Mock(PluginStepContext){ + getDataContext() >> ['job': ['loglevel':'INFO']] + getExecutionContext() >> Mock(ExecutionContext){ + getDataContext() >> [:] + getExecutionLogger() >> Mock(ExecutionLogger) + } + getFramework() >> Mock(Framework) + getNodes() >> Mock(INodeSet){ + getNodes() >> [] + } + } + + Map configuration = [ + 'ansible-playbook' : 'path/to/playbook' + ] + + AnsiblePluginGroup pluginGroup = new AnsiblePluginGroup() + pluginGroup.setAnsibleConfigFilePath("/etc/ansible/ansible.cfg") + pluginGroup.setEncryptExtraVars(true) + pluginGroup.setAnsibleBinariesDirPath("/usr/local/lib") + + when: + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), + context.getFramework(), + context.getNodes(), + configuration, + pluginGroup) + + then: + contextBuilder.getConfigFile() == "/etc/ansible/ansible.cfg" + contextBuilder.getBinariesFilePath() == "/usr/local/lib" + contextBuilder.encryptExtraVars() + contextBuilder.getPlaybookPath() == "path/to/playbook" + } + + def "test plugin group not set"(){ + given: + + PluginStepContext context = Mock(PluginStepContext){ + getDataContext() >> ['job': ['loglevel':'INFO']] + getExecutionContext() >> Mock(ExecutionContext){ + getDataContext() >> [:] + } + getFramework() >> Mock(Framework) + getNodes() >> Mock(INodeSet){ + getNodes() >> [] + } + } + + Map configuration = [ + 'ansible-playbook' : 'path/to/playbook', + 'ansible-config-file-path': '/etc/ansible/ansible.cfg' + ] + + when: + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), + context.getFramework(), + context.getNodes(), + configuration, + null) + + then: + contextBuilder.getConfigFile() == "/etc/ansible/ansible.cfg" + contextBuilder.getBinariesFilePath() == null + !contextBuilder.encryptExtraVars() + contextBuilder.getPlaybookPath() == "path/to/playbook" + } +} From f6da910af8ddbfd08fe84f8a41ded0940b010e18 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Mon, 1 Apr 2024 11:32:24 -0300 Subject: [PATCH 3/4] add functional test using plugin group --- .../functional/BasicIntegrationSpec.groovy | 61 +------------ .../PluginGroupIntegrationSpec.groovy | 91 +++++++++++++++++++ .../groovy/functional/RundeckCompose.groovy | 2 +- .../functional/TestConfiguration.groovy | 62 +++++++++++++ .../test/resources/docker/docker-compose.yml | 3 + .../files/acls/node-acl.aclpolicy | 8 ++ .../files/etc/project.properties | 34 +++++++ ...b-572367d2-e41a-4fdb-b6fc-effa32185b61.xml | 48 ++++++++++ ...b-fa0e401b-b5a8-436a-b13b-0e8092858021.xml | 41 +++++++++ .../ansible/ansible/AnsibleRunner.java | 2 +- .../ansible/plugin/AnsiblePluginGroup.java | 13 +-- 11 files changed, 292 insertions(+), 73 deletions(-) create mode 100644 functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy create mode 100644 functional-test/src/test/groovy/functional/TestConfiguration.groovy create mode 100644 functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/files/acls/node-acl.aclpolicy create mode 100644 functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/files/etc/project.properties create mode 100644 functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/jobs/job-572367d2-e41a-4fdb-b6fc-effa32185b61.xml create mode 100644 functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/jobs/job-fa0e401b-b5a8-436a-b13b-0e8092858021.xml diff --git a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy index a2a4b7dd..646179e7 100644 --- a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy +++ b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy @@ -1,25 +1,17 @@ package functional import functional.util.TestUtil -import org.rundeck.client.api.RundeckApi -import org.rundeck.client.api.model.ExecLog -import org.rundeck.client.api.model.ExecOutput -import org.rundeck.client.api.model.ExecutionStateResponse import org.rundeck.client.api.model.JobRun -import org.rundeck.client.util.Client import spock.lang.Shared -import spock.lang.Specification import org.testcontainers.spock.Testcontainers @Testcontainers -class BasicIntegrationSpec extends Specification { +class BasicIntegrationSpec extends TestConfiguration { @Shared public static RundeckCompose rundeckEnvironment = new RundeckCompose(new File("src/test/resources/docker/docker-compose.yml").toURI()) - @Shared - Client client static String PROJ_NAME = 'ansible-test' @@ -270,55 +262,4 @@ class BasicIntegrationSpec extends Specification { logs.findAll {it.log.contains("\"token\": 13231232312321321321321")}.size() == 1 } - ExecutionStateResponse waitForJob(String executionId){ - def finalStatus = [ - 'aborted', - 'failed', - 'succeeded', - 'timedout', - 'other' - ] - - while(true) { - ExecutionStateResponse result=client.apiCall { api-> api.getExecutionState(executionId)} - if (finalStatus.contains(result?.getExecutionState()?.toLowerCase())) { - return result - } else { - sleep (10000) - } - } - - } - - - List getLogs(String executionId){ - def offset = 0 - def maxLines = 1000 - def lastmod = 0 - boolean isCompleted = false - - List logs = [] - - while (!isCompleted){ - ExecOutput result = client.apiCall { api -> api.getOutput(executionId, offset,lastmod, maxLines)} - isCompleted = result.completed - offset = result.offset - lastmod = result.lastModified - - logs.addAll(result.entries) - - if(result.unmodified){ - sleep(5000) - }else{ - sleep(2000) - } - } - - return logs - } - - - - - } diff --git a/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy b/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy new file mode 100644 index 00000000..64d8b5fd --- /dev/null +++ b/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy @@ -0,0 +1,91 @@ +package functional + +import functional.util.TestUtil +import org.rundeck.client.api.RundeckApi +import org.rundeck.client.api.model.JobRun +import org.rundeck.client.util.Client +import org.testcontainers.spock.Testcontainers +import spock.lang.Shared + + +@Testcontainers +class PluginGroupIntegrationSpec extends TestConfiguration { + + @Shared + public static RundeckCompose rundeckEnvironment = new RundeckCompose(new File("src/test/resources/docker/docker-compose.yml").toURI()) + + @Shared + Client client + + static String PROJ_NAME = 'ansible-plugin-group-test' + + def setupSpec() { + rundeckEnvironment.startCompose() + client = rundeckEnvironment.configureRundeck(PROJ_NAME) + } + + def "test simple inline playbook"(){ + when: + + def jobId = "fa0e401b-b5a8-436a-b13b-0e8092858021" + + JobRun request = new JobRun() + request.loglevel = 'DEBUG' + + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id + + def executionState = waitForJob(executionId) + + def logs = getLogs(executionId) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + ansibleNodeExecutionStatus.get("ok")!=0 + ansibleNodeExecutionStatus.get("unreachable")==0 + ansibleNodeExecutionStatus.get("failed")==0 + ansibleNodeExecutionStatus.get("skipped")==0 + ansibleNodeExecutionStatus.get("ignored")==0 + + logs.findAll {it.log.contains("plugin group set getAnsibleConfigFilePath: /home/rundeck/ansible")}.size() == 1 + logs.findAll {it.log.contains("ANSIBLE_CONFIG: /home/rundeck/ansible")}.size() == 1 + + } + + def "test simple inline playbook with env vars"(){ + when: + + def jobId = "572367d2-e41a-4fdb-b6fc-effa32185b61" + + JobRun request = new JobRun() + request.loglevel = 'DEBUG' + + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id + + def executionState = waitForJob(executionId) + + def logs = getLogs(executionId) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + ansibleNodeExecutionStatus.get("ok")!=0 + ansibleNodeExecutionStatus.get("unreachable")==0 + ansibleNodeExecutionStatus.get("failed")==0 + ansibleNodeExecutionStatus.get("skipped")==0 + ansibleNodeExecutionStatus.get("ignored")==0 + logs.findAll {it.log.contains("plugin group set getAnsibleConfigFilePath: /home/rundeck/ansible")}.size() == 1 + logs.findAll {it.log.contains("plugin group set getEncryptExtraVars: true")}.size() == 1 + logs.findAll {it.log.contains("ANSIBLE_CONFIG: /home/rundeck/ansible")}.size() == 1 + logs.findAll {it.log.contains("encryptVariable password")}.size() == 1 + logs.findAll {it.log.contains("encryptVariable username")}.size() == 1 + logs.findAll {it.log.contains("\"msg\": \"rundeck\"")}.size() == 1 + logs.findAll {it.log.contains("\"msg\": \"demo\"")}.size() == 1 + + } + +} diff --git a/functional-test/src/test/groovy/functional/RundeckCompose.groovy b/functional-test/src/test/groovy/functional/RundeckCompose.groovy index 58720636..7c398aae 100644 --- a/functional-test/src/test/groovy/functional/RundeckCompose.groovy +++ b/functional-test/src/test/groovy/functional/RundeckCompose.groovy @@ -84,7 +84,7 @@ class RundeckCompose extends DockerComposeContainer { } //import project - File projectFile = TestUtil.createArchiveJarFile(projectName, new File("src/test/resources/project-import/ansible-test")) + File projectFile = TestUtil.createArchiveJarFile(projectName, new File("src/test/resources/project-import/" + projectName)) RequestBody body = RequestBody.create(projectFile, Client.MEDIA_TYPE_ZIP) client.apiCall(api -> api.importProjectArchive(projectName, "preserve", true, true, true, true, true, true, true, [:], body) diff --git a/functional-test/src/test/groovy/functional/TestConfiguration.groovy b/functional-test/src/test/groovy/functional/TestConfiguration.groovy new file mode 100644 index 00000000..32fffc73 --- /dev/null +++ b/functional-test/src/test/groovy/functional/TestConfiguration.groovy @@ -0,0 +1,62 @@ +package functional + +import org.rundeck.client.api.RundeckApi +import org.rundeck.client.api.model.ExecLog +import org.rundeck.client.api.model.ExecOutput +import org.rundeck.client.api.model.ExecutionStateResponse +import org.rundeck.client.util.Client +import spock.lang.Shared +import spock.lang.Specification + +class TestConfiguration extends Specification{ + + @Shared + Client client + + ExecutionStateResponse waitForJob(String executionId){ + def finalStatus = [ + 'aborted', + 'failed', + 'succeeded', + 'timedout', + 'other' + ] + + while(true) { + ExecutionStateResponse result=client.apiCall { api-> api.getExecutionState(executionId)} + if (finalStatus.contains(result?.getExecutionState()?.toLowerCase())) { + return result + } else { + sleep (10000) + } + } + + } + + + List getLogs(String executionId){ + def offset = 0 + def maxLines = 1000 + def lastmod = 0 + boolean isCompleted = false + + List logs = [] + + while (!isCompleted){ + ExecOutput result = client.apiCall { api -> api.getOutput(executionId, offset,lastmod, maxLines)} + isCompleted = result.completed + offset = result.offset + lastmod = result.lastModified + + logs.addAll(result.entries) + + if(result.unmodified){ + sleep(5000) + }else{ + sleep(2000) + } + } + + return logs + } +} diff --git a/functional-test/src/test/resources/docker/docker-compose.yml b/functional-test/src/test/resources/docker/docker-compose.yml index 80b85e4e..f2942cdc 100644 --- a/functional-test/src/test/resources/docker/docker-compose.yml +++ b/functional-test/src/test/resources/docker/docker-compose.yml @@ -24,6 +24,9 @@ services: RUNDECK_GRAILS_URL: http://localhost:4440 RUNDECK_MULTIURL_ENABLED: "true" RUNDECK_SERVER_FORWARDED: "true" + RUNDECK_FEATURE_PLUGINGROUPS_ENABLED: "true" + RUNDECK_FEATURE_PLUGINGROUPS_NAME: "pluginGroups" + networks: - rundeck ports: diff --git a/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/files/acls/node-acl.aclpolicy b/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/files/acls/node-acl.aclpolicy new file mode 100644 index 00000000..3248190e --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/files/acls/node-acl.aclpolicy @@ -0,0 +1,8 @@ +by: + urn: project:ansible-plugin-group-test +for: + storage: + - match: + path: 'keys/.*' + allow: [read] +description: Allow access to key storage \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/files/etc/project.properties b/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/files/etc/project.properties new file mode 100644 index 00000000..fff15e8b --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/files/etc/project.properties @@ -0,0 +1,34 @@ +#Exported configuration +project.PluginGroup.AnsiblePluginGroup.enabled=true +project.description= +project.disable.executions=false +project.disable.schedule=false +project.execution.history.cleanup.batch=500 +project.execution.history.cleanup.enabled=false +project.execution.history.cleanup.retention.days=60 +project.execution.history.cleanup.retention.minimum=50 +project.execution.history.cleanup.schedule=0 0 0 1/1 * ? * +project.jobs.gui.groupExpandLevel=1 +project.label= +project.later.executions.disable=false +project.later.executions.enable=false +project.later.schedule.disable=false +project.later.schedule.enable=false +project.name=ansible-plugin-group-test +project.nodeCache.enabled=true +project.nodeCache.firstLoadSynch=true +project.output.allowUnsanitized=false +project.plugin.PluginGroup.AnsiblePluginGroup.ansibleConfigFilePath=/home/rundeck/ansible +project.plugin.PluginGroup.AnsiblePluginGroup.encryptExtraVars=true +project.retry-counter=3 +project.ssh-authentication=privateKey +resources.source.1.type=local +resources.source.2.config.ansible-config-file-path=/home/rundeck/ansible +resources.source.2.config.ansible-gather-facts=true +resources.source.2.config.ansible-ignore-errors=true +resources.source.2.config.ansible-ssh-auth-type=privateKey +resources.source.2.config.ansible-ssh-key-storage-path=keys/project/ansible-plugin-group-test/ssh-node.key +resources.source.2.config.ansible-ssh-user=rundeck +resources.source.2.type=com.batix.rundeck.plugins.AnsibleResourceModelSourceFactory +service.FileCopier.default.provider=sshj-scp +service.NodeExecutor.default.provider=sshj-ssh \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/jobs/job-572367d2-e41a-4fdb-b6fc-effa32185b61.xml b/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/jobs/job-572367d2-e41a-4fdb-b6fc-effa32185b61.xml new file mode 100644 index 00000000..1a55fbcd --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/jobs/job-572367d2-e41a-4fdb-b6fc-effa32185b61.xml @@ -0,0 +1,48 @@ + + + + + + + nodes + + + true + false + ascending + false + 1 + + true + 572367d2-e41a-4fdb-b6fc-effa32185b61 + INFO + simple-inline-playbook-env-vars + false + + name: ssh-node + + true + + true + + + + + + + + + + + + + + + + + + 572367d2-e41a-4fdb-b6fc-effa32185b61 + + \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/jobs/job-fa0e401b-b5a8-436a-b13b-0e8092858021.xml b/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/jobs/job-fa0e401b-b5a8-436a-b13b-0e8092858021.xml new file mode 100644 index 00000000..3d22c018 --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-plugin-group-test/rundeck-ansible-plugin-group-test/jobs/job-fa0e401b-b5a8-436a-b13b-0e8092858021.xml @@ -0,0 +1,41 @@ + + + nodes + + + true + false + ascending + false + 1 + + true + fa0e401b-b5a8-436a-b13b-0e8092858021 + INFO + simple-inline-playbook + false + + name: ssh-node + + true + + true + + + + + + + + + + + + + + + + + fa0e401b-b5a8-436a-b13b-0e8092858021 + + \ No newline at end of file diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java index 3b8e7980..7e9a1927 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -381,7 +381,7 @@ public int run() throws Exception { // 1) if the encryptExtraVars is enabled (user input) // 2) ssh-password is used for node authentication // 3) become-password is used for node authentication - if (encryptExtraVars || + if (encryptExtraVars && extraVars != null && !extraVars.isEmpty() || sshUsePassword || (become && becomePassword != null && !becomePassword.isEmpty())) { diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePluginGroup.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePluginGroup.java index 8bdf57c4..743665f0 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePluginGroup.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePluginGroup.java @@ -11,8 +11,8 @@ import com.dtolabs.rundeck.plugins.util.DescriptionBuilder; -@Plugin(service = ServiceNameConstants.PluginGroup, name = "Ansible Project Configuration") -@PluginDescription(title = "Ansible", description = "Plugin basic ansible configurations") +@Plugin(service = ServiceNameConstants.PluginGroup, name = "AnsiblePluginGroup") +@PluginDescription(title = "Ansible Project Configuration", description = "Plugin basic ansible configurations") public class AnsiblePluginGroup implements PluginGroup, Describable { public String getAnsibleConfigFilePath() { @@ -43,27 +43,18 @@ public void setEncryptExtraVars(Boolean encryptExtraVars) { title = "Ansible config file path", description = "Set ansible config file path." ) - //@RenderingOptions({ - // @RenderingOption(key = StringRenderingConstants.GROUP_NAME, value = "Authentication") - //}) String ansibleConfigFilePath; @PluginProperty( title = "Ansible binaries directory path", description = "Set ansible binaries directory path." ) - //@RenderingOptions({ - // @RenderingOption(key = StringRenderingConstants.GROUP_NAME, value = "Authentication") - //}) String ansibleBinariesDirPath; @PluginProperty( title = "Encrypt Extra Vars.", description = "Encrypt the value of the extra vars keys." ) - //@RenderingOptions({ - // @RenderingOption(key = StringRenderingConstants.GROUP_NAME, value = "Authentication") - //}) Boolean encryptExtraVars; @Override From c86b7ad1fd39274e7a2c6807da9e5be33d11e510 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Mon, 1 Apr 2024 14:27:18 -0300 Subject: [PATCH 4/4] refactor to execute multiples test classes --- .../functional/BasicIntegrationSpec.groovy | 12 +- .../PluginGroupIntegrationSpec.groovy | 16 +- .../groovy/functional/RundeckCompose.groovy | 144 ------------------ .../functional/TestConfiguration.groovy | 62 -------- .../base/BaseTestConfiguration.groovy | 134 ++++++++++++++++ .../functional/base/RundeckCompose.groovy | 58 +++++++ .../groovy/functional/util/TestUtil.groovy | 24 +++ 7 files changed, 224 insertions(+), 226 deletions(-) delete mode 100644 functional-test/src/test/groovy/functional/RundeckCompose.groovy delete mode 100644 functional-test/src/test/groovy/functional/TestConfiguration.groovy create mode 100644 functional-test/src/test/groovy/functional/base/BaseTestConfiguration.groovy create mode 100644 functional-test/src/test/groovy/functional/base/RundeckCompose.groovy diff --git a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy index 646179e7..963cf556 100644 --- a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy +++ b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy @@ -1,23 +1,19 @@ package functional +import functional.base.BaseTestConfiguration import functional.util.TestUtil import org.rundeck.client.api.model.JobRun -import spock.lang.Shared import org.testcontainers.spock.Testcontainers @Testcontainers -class BasicIntegrationSpec extends TestConfiguration { - - @Shared - public static RundeckCompose rundeckEnvironment = new RundeckCompose(new File("src/test/resources/docker/docker-compose.yml").toURI()) - +class BasicIntegrationSpec extends BaseTestConfiguration { static String PROJ_NAME = 'ansible-test' def setupSpec() { - rundeckEnvironment.startCompose() - client = rundeckEnvironment.configureRundeck(PROJ_NAME) + startCompose() + configureRundeck(PROJ_NAME) } def "test simple inline playbook"(){ diff --git a/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy b/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy index 64d8b5fd..09ca61cf 100644 --- a/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy +++ b/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy @@ -1,27 +1,19 @@ package functional +import functional.base.BaseTestConfiguration import functional.util.TestUtil -import org.rundeck.client.api.RundeckApi import org.rundeck.client.api.model.JobRun -import org.rundeck.client.util.Client import org.testcontainers.spock.Testcontainers -import spock.lang.Shared @Testcontainers -class PluginGroupIntegrationSpec extends TestConfiguration { - - @Shared - public static RundeckCompose rundeckEnvironment = new RundeckCompose(new File("src/test/resources/docker/docker-compose.yml").toURI()) - - @Shared - Client client +class PluginGroupIntegrationSpec extends BaseTestConfiguration { static String PROJ_NAME = 'ansible-plugin-group-test' def setupSpec() { - rundeckEnvironment.startCompose() - client = rundeckEnvironment.configureRundeck(PROJ_NAME) + startCompose() + configureRundeck(PROJ_NAME) } def "test simple inline playbook"(){ diff --git a/functional-test/src/test/groovy/functional/RundeckCompose.groovy b/functional-test/src/test/groovy/functional/RundeckCompose.groovy deleted file mode 100644 index 7c398aae..00000000 --- a/functional-test/src/test/groovy/functional/RundeckCompose.groovy +++ /dev/null @@ -1,144 +0,0 @@ -package functional - -import com.jcraft.jsch.JSch -import com.jcraft.jsch.KeyPair -import functional.util.TestUtil -import okhttp3.RequestBody -import org.rundeck.client.RundeckClient -import org.rundeck.client.api.RundeckApi -import org.rundeck.client.api.model.ProjectItem -import org.rundeck.client.util.Client -import org.testcontainers.containers.DockerComposeContainer -import org.testcontainers.containers.wait.strategy.Wait - -import java.nio.file.Files -import java.nio.file.attribute.PosixFilePermission -import java.time.Duration - -class RundeckCompose extends DockerComposeContainer { - - public static final String RUNDECK_IMAGE = System.getenv("RUNDECK_TEST_IMAGE") ?: System.getProperty("RUNDECK_TEST_IMAGE") - public static final String NODE_USER_PASSWORD = "testpassword123" - public static final String NODE_KEY_PASSPHRASE = "testpassphrase123" - public static final String USER_VAULT_PASSWORD = "vault123" - - RundeckCompose(URI composeFilePath) { - super(new File(composeFilePath)) - - withExposedService("rundeck", 4440, - Wait.forHttp("/api/41/system/info").forStatusCode(403).withStartupTimeout(Duration.ofMinutes(5)) - ) - withEnv("RUNDECK_IMAGE", RUNDECK_IMAGE) - withEnv("NODE_USER_PASSWORD", NODE_USER_PASSWORD) - } - - - def startCompose() { - //generate SSH private key for node authentication - File keyPath = new File("src/test/resources/docker/keys") - generatePrivateKey(keyPath.getAbsolutePath(),"id_rsa") - generatePrivateKey(keyPath.getAbsolutePath(),"id_rsa_passphrase", NODE_KEY_PASSPHRASE) - - start() - } - - - Client configureRundeck(String projectName){ - - //configure rundeck api - String address = getServiceHost("rundeck",4440) - Integer port = getServicePort("rundeck",4440) - def rdUrl = "http://${address}:${port}/api/41" - System.err.println("rdUrl: $rdUrl") - Client client = RundeckClient.builder().with { - baseUrl rdUrl - passwordAuth('admin', 'admin') - logger(new TestLogger()) - build() - } - //add private key - RequestBody requestBody = RequestBody.create(new File("src/test/resources/docker/keys/id_rsa"), Client.MEDIA_TYPE_OCTET_STREAM) - def keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node.key", requestBody)} - - //add private key with passphrase - requestBody = RequestBody.create(new File("src/test/resources/docker/keys/id_rsa_passphrase"), Client.MEDIA_TYPE_OCTET_STREAM) - keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node-passphrase.key", requestBody)} - - //add passphrase - requestBody = RequestBody.create(NODE_KEY_PASSPHRASE.getBytes(), Client.MEDIA_TYPE_X_RUNDECK_PASSWORD) - keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node-passphrase.pass", requestBody)} - - //add node user ssh-password - requestBody = RequestBody.create(NODE_USER_PASSWORD.getBytes(), Client.MEDIA_TYPE_X_RUNDECK_PASSWORD) - keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node.pass", requestBody)} - - //user vault password - requestBody = RequestBody.create(USER_VAULT_PASSWORD.getBytes(), Client.MEDIA_TYPE_X_RUNDECK_PASSWORD) - keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/vault-user.pass", requestBody)} - - //create project - def projList = client.apiCall(api -> api.listProjects()) - - if (!projList*.name.contains(projectName)) { - def project = client.apiCall(api -> api.createProject(new ProjectItem(name: projectName))) - } - - //import project - File projectFile = TestUtil.createArchiveJarFile(projectName, new File("src/test/resources/project-import/" + projectName)) - RequestBody body = RequestBody.create(projectFile, Client.MEDIA_TYPE_ZIP) - client.apiCall(api -> - api.importProjectArchive(projectName, "preserve", true, true, true, true, true, true, true, [:], body) - ) - - //wait for node to be available - def result = client.apiCall {api-> api.listNodes(projectName,".*")} - def count =0 - - while(result.get("ssh-node")==null && count<5){ - sleep(2000) - result = client.apiCall {api-> api.listNodes(projectName,".*")} - count++ - } - - return client - } - - static def generatePrivateKey(String filePath, String keyName, String passphrase = null){ - JSch jsch=new JSch() - KeyPair keyPair=KeyPair.genKeyPair(jsch, KeyPair.RSA) - if(passphrase){ - keyPair.writePrivateKey(filePath + File.separator + keyName, passphrase.getBytes()) - }else{ - keyPair.writePrivateKey(filePath + File.separator + keyName) - } - - keyPair.writePublicKey(filePath + File.separator + keyName + ".pub", "test private key") - - keyPair.dispose() - - File privateKey = new File(filePath + File.separator + keyName) - Set perms = new HashSet() - perms.add(PosixFilePermission.OWNER_READ) - perms.add(PosixFilePermission.OWNER_WRITE) - Files.setPosixFilePermissions(privateKey.toPath(), perms) - } - - - static class TestLogger implements Client.Logger { - @Override - void output(String out) { - println(out) - } - - @Override - void warning(String warn) { - System.err.println(warn) - } - - @Override - void error(String err) { - System.err.println(err) - } - } - -} diff --git a/functional-test/src/test/groovy/functional/TestConfiguration.groovy b/functional-test/src/test/groovy/functional/TestConfiguration.groovy deleted file mode 100644 index 32fffc73..00000000 --- a/functional-test/src/test/groovy/functional/TestConfiguration.groovy +++ /dev/null @@ -1,62 +0,0 @@ -package functional - -import org.rundeck.client.api.RundeckApi -import org.rundeck.client.api.model.ExecLog -import org.rundeck.client.api.model.ExecOutput -import org.rundeck.client.api.model.ExecutionStateResponse -import org.rundeck.client.util.Client -import spock.lang.Shared -import spock.lang.Specification - -class TestConfiguration extends Specification{ - - @Shared - Client client - - ExecutionStateResponse waitForJob(String executionId){ - def finalStatus = [ - 'aborted', - 'failed', - 'succeeded', - 'timedout', - 'other' - ] - - while(true) { - ExecutionStateResponse result=client.apiCall { api-> api.getExecutionState(executionId)} - if (finalStatus.contains(result?.getExecutionState()?.toLowerCase())) { - return result - } else { - sleep (10000) - } - } - - } - - - List getLogs(String executionId){ - def offset = 0 - def maxLines = 1000 - def lastmod = 0 - boolean isCompleted = false - - List logs = [] - - while (!isCompleted){ - ExecOutput result = client.apiCall { api -> api.getOutput(executionId, offset,lastmod, maxLines)} - isCompleted = result.completed - offset = result.offset - lastmod = result.lastModified - - logs.addAll(result.entries) - - if(result.unmodified){ - sleep(5000) - }else{ - sleep(2000) - } - } - - return logs - } -} diff --git a/functional-test/src/test/groovy/functional/base/BaseTestConfiguration.groovy b/functional-test/src/test/groovy/functional/base/BaseTestConfiguration.groovy new file mode 100644 index 00000000..fd0e2243 --- /dev/null +++ b/functional-test/src/test/groovy/functional/base/BaseTestConfiguration.groovy @@ -0,0 +1,134 @@ +package functional.base + +import functional.util.TestUtil +import okhttp3.RequestBody +import org.rundeck.client.api.RundeckApi +import org.rundeck.client.api.model.ExecLog +import org.rundeck.client.api.model.ExecOutput +import org.rundeck.client.api.model.ExecutionStateResponse +import org.rundeck.client.api.model.ProjectItem +import org.rundeck.client.util.Client +import spock.lang.Shared +import spock.lang.Specification + +class BaseTestConfiguration extends Specification{ + + @Shared + Client client + + @Shared + public static RundeckCompose rundeckEnvironment + + public static final String NODE_USER_PASSWORD = "testpassword123" + public static final String NODE_KEY_PASSPHRASE = "testpassphrase123" + public static final String USER_VAULT_PASSWORD = "vault123" + + def startCompose() { + if(rundeckEnvironment==null){ + //generate SSH private key for node authentication + File keyPath = new File("src/test/resources/docker/keys") + TestUtil.generatePrivateKey(keyPath.getAbsolutePath(),"id_rsa") + TestUtil.generatePrivateKey(keyPath.getAbsolutePath(),"id_rsa_passphrase", NODE_KEY_PASSPHRASE) + + rundeckEnvironment = new RundeckCompose(new File("src/test/resources/docker/docker-compose.yml").toURI()) + rundeckEnvironment.start() + } + + client = rundeckEnvironment.getClient() + } + + ExecutionStateResponse waitForJob(String executionId){ + def finalStatus = [ + 'aborted', + 'failed', + 'succeeded', + 'timedout', + 'other' + ] + + while(true) { + ExecutionStateResponse result=client.apiCall { api-> api.getExecutionState(executionId)} + if (finalStatus.contains(result?.getExecutionState()?.toLowerCase())) { + return result + } else { + sleep (10000) + } + } + + } + + + List getLogs(String executionId){ + def offset = 0 + def maxLines = 1000 + def lastmod = 0 + boolean isCompleted = false + + List logs = [] + + while (!isCompleted){ + ExecOutput result = client.apiCall { api -> api.getOutput(executionId, offset,lastmod, maxLines)} + isCompleted = result.completed + offset = result.offset + lastmod = result.lastModified + + logs.addAll(result.entries) + + if(result.unmodified){ + sleep(5000) + }else{ + sleep(2000) + } + } + + return logs + } + + def configureRundeck(String projectName){ + + //add private key + RequestBody requestBody = RequestBody.create(new File("src/test/resources/docker/keys/id_rsa"), Client.MEDIA_TYPE_OCTET_STREAM) + def keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node.key", requestBody)} + + //add private key with passphrase + requestBody = RequestBody.create(new File("src/test/resources/docker/keys/id_rsa_passphrase"), Client.MEDIA_TYPE_OCTET_STREAM) + keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node-passphrase.key", requestBody)} + + //add passphrase + requestBody = RequestBody.create(NODE_KEY_PASSPHRASE.getBytes(), Client.MEDIA_TYPE_X_RUNDECK_PASSWORD) + keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node-passphrase.pass", requestBody)} + + //add node user ssh-password + requestBody = RequestBody.create(NODE_USER_PASSWORD.getBytes(), Client.MEDIA_TYPE_X_RUNDECK_PASSWORD) + keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node.pass", requestBody)} + + //user vault password + requestBody = RequestBody.create(USER_VAULT_PASSWORD.getBytes(), Client.MEDIA_TYPE_X_RUNDECK_PASSWORD) + keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/vault-user.pass", requestBody)} + + //create project + def projList = client.apiCall(api -> api.listProjects()) + + if (!projList*.name.contains(projectName)) { + def project = client.apiCall(api -> api.createProject(new ProjectItem(name: projectName))) + } + + //import project + File projectFile = TestUtil.createArchiveJarFile(projectName, new File("src/test/resources/project-import/" + projectName)) + RequestBody body = RequestBody.create(projectFile, Client.MEDIA_TYPE_ZIP) + client.apiCall(api -> + api.importProjectArchive(projectName, "preserve", true, true, true, true, true, true, true, [:], body) + ) + + //wait for node to be available + def result = client.apiCall {api-> api.listNodes(projectName,".*")} + def count =0 + + while(result.get("ssh-node")==null && count<5){ + sleep(2000) + result = client.apiCall {api-> api.listNodes(projectName,".*")} + count++ + } + + } +} diff --git a/functional-test/src/test/groovy/functional/base/RundeckCompose.groovy b/functional-test/src/test/groovy/functional/base/RundeckCompose.groovy new file mode 100644 index 00000000..3f843b3a --- /dev/null +++ b/functional-test/src/test/groovy/functional/base/RundeckCompose.groovy @@ -0,0 +1,58 @@ +package functional.base + +import org.rundeck.client.RundeckClient +import org.rundeck.client.api.RundeckApi +import org.rundeck.client.util.Client +import org.testcontainers.containers.DockerComposeContainer +import org.testcontainers.containers.wait.strategy.Wait +import java.time.Duration + +class RundeckCompose extends DockerComposeContainer { + + public static final String RUNDECK_IMAGE = System.getenv("RUNDECK_TEST_IMAGE") ?: System.getProperty("RUNDECK_TEST_IMAGE") + + + RundeckCompose(URI composeFilePath) { + super(new File(composeFilePath)) + + withExposedService("rundeck", 4440, + Wait.forHttp("/api/41/system/info").forStatusCode(403).withStartupTimeout(Duration.ofMinutes(5)) + ) + withEnv("RUNDECK_IMAGE", RUNDECK_IMAGE) + withEnv("NODE_USER_PASSWORD", BaseTestConfiguration.NODE_USER_PASSWORD) + } + + Client getClient(){ + //configure rundeck api + String address = getServiceHost("rundeck",4440) + Integer port = getServicePort("rundeck",4440) + def rdUrl = "http://${address}:${port}/api/41" + System.err.println("rdUrl: $rdUrl") + Client client = RundeckClient.builder().with { + baseUrl rdUrl + passwordAuth('admin', 'admin') + logger(new TestLogger()) + build() + } + return client + } + + + static class TestLogger implements Client.Logger { + @Override + void output(String out) { + println(out) + } + + @Override + void warning(String warn) { + System.err.println(warn) + } + + @Override + void error(String err) { + System.err.println(err) + } + } + +} diff --git a/functional-test/src/test/groovy/functional/util/TestUtil.groovy b/functional-test/src/test/groovy/functional/util/TestUtil.groovy index 63904f0e..44d5354d 100644 --- a/functional-test/src/test/groovy/functional/util/TestUtil.groovy +++ b/functional-test/src/test/groovy/functional/util/TestUtil.groovy @@ -1,7 +1,11 @@ package functional.util +import com.jcraft.jsch.JSch +import com.jcraft.jsch.KeyPair import org.rundeck.client.api.model.ExecLog +import java.nio.file.Files +import java.nio.file.attribute.PosixFilePermission import java.text.SimpleDateFormat import java.util.jar.JarEntry import java.util.jar.JarOutputStream @@ -65,4 +69,24 @@ class TestUtil { return listNodeResultStatus.stream().collect(Collectors.toMap(s -> s.toString().split("=")[0], s -> Integer.valueOf(s.toString().split("=")[1]))) } + static def generatePrivateKey(String filePath, String keyName, String passphrase = null){ + JSch jsch=new JSch() + KeyPair keyPair=KeyPair.genKeyPair(jsch, KeyPair.RSA) + if(passphrase){ + keyPair.writePrivateKey(filePath + File.separator + keyName, passphrase.getBytes()) + }else{ + keyPair.writePrivateKey(filePath + File.separator + keyName) + } + + keyPair.writePublicKey(filePath + File.separator + keyName + ".pub", "test private key") + + keyPair.dispose() + + File privateKey = new File(filePath + File.separator + keyName) + Set perms = new HashSet() + perms.add(PosixFilePermission.OWNER_READ) + perms.add(PosixFilePermission.OWNER_WRITE) + Files.setPosixFilePermissions(privateKey.toPath(), perms) + } + }