diff --git a/pom.xml b/pom.xml index a63663bc..aafab605 100644 --- a/pom.xml +++ b/pom.xml @@ -194,7 +194,6 @@ org.jenkins-ci.plugins.workflow workflow-step-api - test org.jenkins-ci.plugins.workflow diff --git a/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java b/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java index 87767820..c26b3c71 100644 --- a/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java +++ b/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java @@ -3,11 +3,38 @@ import com.bettercloud.vault.Vault; import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; +import com.bettercloud.vault.json.Json; +import com.bettercloud.vault.json.JsonArray; +import com.bettercloud.vault.json.JsonValue; import com.bettercloud.vault.response.LogicalResponse; import com.bettercloud.vault.response.VaultResponse; +import com.bettercloud.vault.rest.RestResponse; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsUnavailableException; +import com.cloudbees.plugins.credentials.matchers.IdMatcher; +import com.datapipe.jenkins.vault.configuration.VaultConfigResolver; +import com.datapipe.jenkins.vault.configuration.VaultConfiguration; import com.datapipe.jenkins.vault.credentials.VaultCredential; import com.datapipe.jenkins.vault.exception.VaultPluginException; +import com.datapipe.jenkins.vault.model.VaultSecret; +import com.datapipe.jenkins.vault.model.VaultSecretValue; +import hudson.EnvVars; +import hudson.ExtensionList; +import hudson.Util; +import hudson.model.Run; +import hudson.security.ACL; +import java.io.PrintStream; import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; public class VaultAccessor implements Serializable { @@ -102,4 +129,147 @@ public VaultResponse revoke(String leaseId) { "could not revoke vault lease (" + leaseId + "):" + e.getMessage()); } } + + public static Map retrieveVaultSecrets(Run run, PrintStream logger, EnvVars envVars, VaultAccessor vaultAccessor, VaultConfiguration initialConfiguration, List vaultSecrets) { + Map overrides = new HashMap<>(); + + VaultConfiguration config = pullAndMergeConfiguration(run, initialConfiguration); + String url = config.getVaultUrl(); + + if (StringUtils.isBlank(url)) { + throw new VaultPluginException( + "The vault url was not configured - please specify the vault url to use."); + } + + VaultConfig vaultConfig = config.getVaultConfig(); + VaultCredential credential = config.getVaultCredential(); + if (credential == null) { + credential = retrieveVaultCredentials(run, config); + } + + String prefixPath = StringUtils.isBlank(config.getPrefixPath()) + ? "" + : Util.ensureEndsWith(envVars.expand(config.getPrefixPath()), "/"); + + if (vaultAccessor == null) { + vaultAccessor = new VaultAccessor(); + } + vaultAccessor.setConfig(vaultConfig); + vaultAccessor.setCredential(credential); + vaultAccessor.setMaxRetries(config.getMaxRetries()); + vaultAccessor.setRetryIntervalMilliseconds(config.getRetryIntervalMilliseconds()); + vaultAccessor.init(); + + for (VaultSecret vaultSecret : vaultSecrets) { + String path = prefixPath + envVars.expand(vaultSecret.getPath()); + logger.printf("Retrieving secret: %s%n", path); + Integer engineVersion = Optional.ofNullable(vaultSecret.getEngineVersion()) + .orElse(config.getEngineVersion()); + try { + LogicalResponse response = vaultAccessor.read(path, engineVersion); + if (responseHasErrors(config, logger, path, response)) { + continue; + } + Map values = response.getData(); + for (VaultSecretValue value : vaultSecret.getSecretValues()) { + String vaultKey = value.getVaultKey(); + String secret = values.get(vaultKey); + if (StringUtils.isBlank(secret)) { + throw new IllegalArgumentException( + "Vault Secret " + vaultKey + " at " + path + + " is either null or empty. Please check the Secret in Vault."); + } + overrides.put(value.getEnvVar(), secret); + } + } catch (VaultPluginException ex) { + VaultException e = (VaultException) ex.getCause(); + if (e != null) { + throw new VaultPluginException(String + .format("Vault response returned %d for secret path %s", + e.getHttpStatusCode(), path), + e); + } + throw ex; + } + } + + return overrides; + } + + public static VaultCredential retrieveVaultCredentials(Run build, VaultConfiguration config) { + if (Jenkins.getInstanceOrNull() != null) { + String id = config.getVaultCredentialId(); + if (StringUtils.isBlank(id)) { + throw new VaultPluginException( + "The credential id was not configured - please specify the credentials to use."); + } + List credentials = CredentialsProvider + .lookupCredentials(VaultCredential.class, build.getParent(), ACL.SYSTEM, + Collections.emptyList()); + VaultCredential credential = CredentialsMatchers + .firstOrNull(credentials, new IdMatcher(id)); + + if (credential == null) { + throw new CredentialsUnavailableException(id); + } + + return credential; + } + + return null; + } + + public static boolean responseHasErrors(VaultConfiguration configuration, PrintStream logger, + String path, LogicalResponse response) { + RestResponse restResponse = response.getRestResponse(); + if (restResponse == null) { + return false; + } + int status = restResponse.getStatus(); + if (status == 403) { + logger.printf("Access denied to Vault Secrets at '%s'%n", path); + return true; + } else if (status == 404) { + if (configuration.getFailIfNotFound()) { + throw new VaultPluginException( + String.format("Vault credentials not found for '%s'", path)); + } else { + logger.printf("Vault credentials not found for '%s'%n", path); + return true; + } + } else if (status >= 400) { + String errors = Optional + .of(Json.parse(new String(restResponse.getBody(), StandardCharsets.UTF_8))).map( + JsonValue::asObject) + .map(j -> j.get("errors")).map(JsonValue::asArray).map(JsonArray::values) + .map(j -> j.stream().map(JsonValue::asString).collect(Collectors.joining("\n"))) + .orElse(""); + logger.printf("Vault responded with %d error code.%n", status); + if (StringUtils.isNotBlank(errors)) { + logger.printf("Vault responded with errors: %s%n", errors); + } + return true; + } + return false; + } + + public static VaultConfiguration pullAndMergeConfiguration(Run build, + VaultConfiguration buildConfiguration) { + VaultConfiguration configuration = buildConfiguration; + for (VaultConfigResolver resolver : ExtensionList.lookup(VaultConfigResolver.class)) { + if (configuration != null) { + configuration = configuration + .mergeWithParent(resolver.forJob(build.getParent())); + } else { + configuration = resolver.forJob(build.getParent()); + } + } + if (configuration == null) { + throw new VaultPluginException( + "No configuration found - please configure the VaultPlugin."); + } + configuration.fixDefaults(); + + return configuration; + } } diff --git a/src/main/java/com/datapipe/jenkins/vault/VaultBindingStep.java b/src/main/java/com/datapipe/jenkins/vault/VaultBindingStep.java new file mode 100644 index 00000000..9e64d477 --- /dev/null +++ b/src/main/java/com/datapipe/jenkins/vault/VaultBindingStep.java @@ -0,0 +1,191 @@ +/* + * The MIT License (MIT) + *

+ * Copyright (c) 2016 Datapipe, Inc. + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.datapipe.jenkins.vault; + +import com.datapipe.jenkins.vault.configuration.VaultConfiguration; +import com.datapipe.jenkins.vault.log.MaskingConsoleLogFilter; +import com.datapipe.jenkins.vault.model.VaultSecret; +import com.google.common.annotations.VisibleForTesting; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.EnvVars; +import hudson.Extension; +import hudson.console.ConsoleLogFilter; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.util.Secret; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; +import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback.TailCall; +import org.jenkinsci.plugins.workflow.steps.BodyInvoker; +import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander; +import org.jenkinsci.plugins.workflow.steps.GeneralNonBlockingStepExecution; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +public class VaultBindingStep extends Step { + + private VaultConfiguration configuration; + private List vaultSecrets; + + @DataBoundConstructor + public VaultBindingStep(@CheckForNull List vaultSecrets) { + this.vaultSecrets = vaultSecrets; + } + + public List getVaultSecrets() { + return vaultSecrets; + } + + @DataBoundSetter + public void setConfiguration(VaultConfiguration configuration) { + this.configuration = configuration; + } + + public VaultConfiguration getConfiguration() { + return configuration; + } + + @Override + public StepExecution start(StepContext context) throws Exception { + return new Execution(this, context); + } + + protected static class Execution extends GeneralNonBlockingStepExecution { + + private static final long serialVersionUID = 1; + + private transient VaultBindingStep step; + private transient VaultAccessor vaultAccessor; + + public Execution(VaultBindingStep step, StepContext context) { + super(context); + this.step = step; + } + + @VisibleForTesting + public void setVaultAccessor(VaultAccessor vaultAccessor) { + this.vaultAccessor = vaultAccessor; + } + + @Override + public boolean start() throws Exception { + run(this::doStart); + return false; + } + + private void doStart() throws Exception { + Run run = getContext().get(Run.class); + TaskListener listener = getContext().get(TaskListener.class); + EnvVars envVars = getContext().get(EnvVars.class); + + Map overrides = VaultAccessor + .retrieveVaultSecrets(run, listener.getLogger(), envVars, vaultAccessor, + step.getConfiguration(), step.getVaultSecrets()); + + List secretValues = new ArrayList<>(); + secretValues.addAll(overrides.values()); + + getContext().newBodyInvoker() + .withContext(EnvironmentExpander.merge(getContext().get(EnvironmentExpander.class), + new VaultBindingStep.Overrider(overrides))) + .withContext(BodyInvoker + .mergeConsoleLogFilters(getContext().get(ConsoleLogFilter.class), + new MaskingConsoleLogFilter(run.getCharset().name(), secretValues))) + .withCallback(new Callback()) + .start(); + } + } + + private static final class Overrider extends EnvironmentExpander { + + private static final long serialVersionUID = 1; + + private final Map overrides = new HashMap(); + + Overrider(Map overrides) { + for (Map.Entry override : overrides.entrySet()) { + this.overrides.put(override.getKey(), Secret.fromString(override.getValue())); + } + } + + @Override + public void expand(EnvVars env) throws IOException, InterruptedException { + for (Map.Entry override : overrides.entrySet()) { + env.override(override.getKey(), override.getValue().getPlainText()); + } + } + + @Override + public Set getSensitiveVariables() { + return Collections.unmodifiableSet(overrides.keySet()); + } + } + + private static class Callback extends TailCall { + + @Override + protected void finished(StepContext context) throws Exception { + + } + } + + @Extension + public static final class DescriptorImpl extends StepDescriptor { + + @Override + public Set> getRequiredContext() { + return Collections + .unmodifiableSet( + new HashSet<>(Arrays.asList(TaskListener.class, Run.class, EnvVars.class))); + } + + @Override + public boolean takesImplicitBlockArgument() { + return true; + } + + @Override + public String getFunctionName() { + return "withVault"; + } + + @Nonnull + @Override + public String getDisplayName() { + return "Vault Plugin"; + } + } +} diff --git a/src/main/java/com/datapipe/jenkins/vault/VaultBuildWrapper.java b/src/main/java/com/datapipe/jenkins/vault/VaultBuildWrapper.java index 9c05c265..fcc15bbb 100644 --- a/src/main/java/com/datapipe/jenkins/vault/VaultBuildWrapper.java +++ b/src/main/java/com/datapipe/jenkins/vault/VaultBuildWrapper.java @@ -23,24 +23,11 @@ */ package com.datapipe.jenkins.vault; -import com.bettercloud.vault.VaultConfig; -import com.bettercloud.vault.VaultException; -import com.bettercloud.vault.json.Json; -import com.bettercloud.vault.json.JsonArray; -import com.bettercloud.vault.json.JsonValue; -import com.bettercloud.vault.response.LogicalResponse; -import com.bettercloud.vault.rest.RestResponse; -import com.cloudbees.plugins.credentials.CredentialsMatchers; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.CredentialsUnavailableException; -import com.cloudbees.plugins.credentials.matchers.IdMatcher; import com.datapipe.jenkins.vault.configuration.VaultConfigResolver; import com.datapipe.jenkins.vault.configuration.VaultConfiguration; -import com.datapipe.jenkins.vault.credentials.VaultCredential; import com.datapipe.jenkins.vault.exception.VaultPluginException; import com.datapipe.jenkins.vault.log.MaskingConsoleLogFilter; import com.datapipe.jenkins.vault.model.VaultSecret; -import com.datapipe.jenkins.vault.model.VaultSecretValue; import com.google.common.annotations.VisibleForTesting; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; @@ -49,24 +36,16 @@ import hudson.ExtensionList; import hudson.FilePath; import hudson.Launcher; -import hudson.Util; import hudson.console.ConsoleLogFilter; import hudson.model.AbstractProject; import hudson.model.Run; import hudson.model.TaskListener; -import hudson.security.ACL; import hudson.tasks.BuildWrapperDescriptor; import java.io.PrintStream; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; import jenkins.tasks.SimpleBuildWrapper; -import org.apache.commons.lang.StringUtils; -import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -114,124 +93,16 @@ public void setVaultAccessor(VaultAccessor vaultAccessor) { this.vaultAccessor = vaultAccessor; } - private List retrieveLeaseIds(List logicalResponses) { - List leaseIds = new ArrayList<>(); - for (LogicalResponse response : logicalResponses) { - String leaseId = response.getLeaseId(); - if (leaseId != null && !leaseId.isEmpty()) { - leaseIds.add(leaseId); - } - } - return leaseIds; - } - - protected void provideEnvironmentVariablesFromVault(Context context, Run build, EnvVars envVars) { - VaultConfiguration config = getConfiguration(); - String url = config.getVaultUrl(); - - if (StringUtils.isBlank(url)) { - throw new VaultPluginException( - "The vault url was not configured - please specify the vault url to use."); - } - - VaultConfig vaultConfig = config.getVaultConfig(); - VaultCredential credential = config.getVaultCredential(); - if (credential == null) credential = retrieveVaultCredentials(build); - - String prefixPath = StringUtils.isBlank(config.getPrefixPath()) - ? "" - : Util.ensureEndsWith(envVars.expand(config.getPrefixPath()), "/"); - - if (vaultAccessor == null) vaultAccessor = new VaultAccessor(); - vaultAccessor.setConfig(vaultConfig); - vaultAccessor.setCredential(credential); - vaultAccessor.setMaxRetries(config.getMaxRetries()); - vaultAccessor.setRetryIntervalMilliseconds(config.getRetryIntervalMilliseconds()); - vaultAccessor.init(); - - for (VaultSecret vaultSecret : vaultSecrets) { - String path = prefixPath + envVars.expand(vaultSecret.getPath()); - logger.printf("Retrieving secret: %s%n", path); - Integer engineVersion = Optional.ofNullable(vaultSecret.getEngineVersion()) - .orElse(configuration.getEngineVersion()); - try { - LogicalResponse response = vaultAccessor.read(path, engineVersion); - if (responseHasErrors(path, response)) { - continue; - } - Map values = response.getData(); - for (VaultSecretValue value : vaultSecret.getSecretValues()) { - String vaultKey = value.getVaultKey(); - String secret = values.get(vaultKey); - if (StringUtils.isBlank(secret)) { - throw new IllegalArgumentException( - "Vault Secret " + vaultKey + " at " + path - + " is either null or empty. Please check the Secret in Vault."); - } else { - valuesToMask.add(secret); - } - context.env(value.getEnvVar(), secret); - } - } catch (VaultPluginException ex) { - VaultException e = (VaultException) ex.getCause(); - if (e != null) { - throw new VaultPluginException(String - .format("Vault response returned %d for secret path %s", - e.getHttpStatusCode(), path), - e); - } - throw ex; - } - } - } + protected void provideEnvironmentVariablesFromVault(Context context, Run build, + EnvVars envVars) { + Map overrides = VaultAccessor + .retrieveVaultSecrets(build, logger, envVars, vaultAccessor, + getConfiguration(), getVaultSecrets()); - private boolean responseHasErrors(String path, LogicalResponse response) { - RestResponse restResponse = response.getRestResponse(); - if (restResponse == null) return false; - int status = restResponse.getStatus(); - if (status == 403) { - logger.printf("Access denied to Vault Secrets at '%s'%n", path); - return true; - } else if (status == 404) { - if (configuration.getFailIfNotFound()) { - throw new VaultPluginException( - String.format("Vault credentials not found for '%s'", path)); - } else { - logger.printf("Vault credentials not found for '%s'%n", path); - return true; - } - } else if (status >= 400) { - String errors = Optional - .of(Json.parse(new String(restResponse.getBody(), StandardCharsets.UTF_8))).map(JsonValue::asObject) - .map(j -> j.get("errors")).map(JsonValue::asArray).map(JsonArray::values) - .map(j -> j.stream().map(JsonValue::asString).collect(Collectors.joining("\n"))) - .orElse(""); - logger.printf("Vault responded with %d error code.%n", status); - if (StringUtils.isNotBlank(errors)) { - logger.printf("Vault responded with errors: %s%n", errors); - } - return true; + for (Map.Entry secret : overrides.entrySet()) { + valuesToMask.add(secret.getValue()); + context.env(secret.getKey(), secret.getValue()); } - return false; - } - - protected VaultCredential retrieveVaultCredentials(Run build) { - String id = getConfiguration().getVaultCredentialId(); - if (StringUtils.isBlank(id)) { - throw new VaultPluginException( - "The credential id was not configured - please specify the credentials to use."); - } - List credentials = CredentialsProvider - .lookupCredentials(VaultCredential.class, build.getParent(), ACL.SYSTEM, - Collections.emptyList()); - VaultCredential credential = CredentialsMatchers - .firstOrNull(credentials, new IdMatcher(id)); - - if (credential == null) { - throw new CredentialsUnavailableException(id); - } - - return credential; } private void pullAndMergeConfiguration(Run build) { @@ -257,11 +128,10 @@ public ConsoleLogFilter createLoggerDecorator( /** - * Descriptor for {@link VaultBuildWrapper}. Used as a singleton. The class is marked as public so - * that it can be accessed from views. + * Descriptor for {@link VaultBuildWrapper}. Used as a singleton. The class is marked as public + * so that it can be accessed from views. */ @Extension - @Symbol("withVault") public static final class DescriptorImpl extends BuildWrapperDescriptor { public DescriptorImpl() { diff --git a/src/main/resources/com/datapipe/jenkins/vault/VaultBindingStep/config.jelly b/src/main/resources/com/datapipe/jenkins/vault/VaultBindingStep/config.jelly new file mode 100644 index 00000000..205f181d --- /dev/null +++ b/src/main/resources/com/datapipe/jenkins/vault/VaultBindingStep/config.jelly @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/test/java/com/datapipe/jenkins/vault/VaultBuildWrapperWithMockAccessor.java b/src/test/java/com/datapipe/jenkins/vault/VaultBindingStepWithMockAccessor.java similarity index 68% rename from src/test/java/com/datapipe/jenkins/vault/VaultBuildWrapperWithMockAccessor.java rename to src/test/java/com/datapipe/jenkins/vault/VaultBindingStepWithMockAccessor.java index 57c3efd4..df19d27b 100644 --- a/src/test/java/com/datapipe/jenkins/vault/VaultBuildWrapperWithMockAccessor.java +++ b/src/test/java/com/datapipe/jenkins/vault/VaultBindingStepWithMockAccessor.java @@ -6,29 +6,36 @@ import com.datapipe.jenkins.vault.credentials.VaultAppRoleCredential; import com.datapipe.jenkins.vault.credentials.VaultCredential; import com.datapipe.jenkins.vault.model.VaultSecret; -import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.EnvVars; import hudson.Extension; -import hudson.model.AbstractProject; -import hudson.model.Descriptor; -import hudson.tasks.BuildWrapper; +import hudson.model.Run; +import hudson.model.TaskListener; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; import org.kohsuke.stapler.DataBoundConstructor; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -/* -This class is only used for testing the Jenkinsfile - we can not inject our - MockAccessor there and therefore need to mimic it's behaviour here. - */ -public class VaultBuildWrapperWithMockAccessor extends VaultBuildWrapper { - +public class VaultBindingStepWithMockAccessor extends VaultBindingStep { @DataBoundConstructor - public VaultBuildWrapperWithMockAccessor(@CheckForNull List vaultSecrets) { + public VaultBindingStepWithMockAccessor(List vaultSecrets) { super(vaultSecrets); - setVaultAccessor(new VaultAccessor() { + } + + @Override + public StepExecution start(StepContext context) throws Exception { + Execution execution = new Execution(this, context); + execution.setVaultAccessor(new VaultAccessor() { @Override public void setConfig(VaultConfig config) { @@ -73,20 +80,30 @@ public LogicalResponse read(String path, Integer engineVersion) { return resp; } }); + return execution; } @Extension - public static final class DescriptorImpl extends Descriptor { + public static final class DescriptorImpl extends StepDescriptor { - public DescriptorImpl() { - super(VaultBuildWrapperWithMockAccessor.class); - load(); + @Override + public Set> getRequiredContext() { + return Collections + .unmodifiableSet( + new HashSet<>(Arrays.asList(TaskListener.class, Run.class, EnvVars.class))); } - public boolean isApplicable(AbstractProject item) { + @Override + public boolean takesImplicitBlockArgument() { return true; } + @Override + public String getFunctionName() { + return "withVaultMock"; + } + + @Nonnull @Override public String getDisplayName() { return "Vault Mock Plugin"; diff --git a/src/test/java/com/datapipe/jenkins/vault/VaultBuildWrapperTest.java b/src/test/java/com/datapipe/jenkins/vault/VaultBuildWrapperTest.java index de9c9548..b8b714ac 100644 --- a/src/test/java/com/datapipe/jenkins/vault/VaultBuildWrapperTest.java +++ b/src/test/java/com/datapipe/jenkins/vault/VaultBuildWrapperTest.java @@ -3,7 +3,6 @@ import com.bettercloud.vault.response.LogicalResponse; import com.bettercloud.vault.rest.RestResponse; import com.datapipe.jenkins.vault.configuration.VaultConfiguration; -import com.datapipe.jenkins.vault.credentials.VaultCredential; import com.datapipe.jenkins.vault.exception.VaultPluginException; import com.datapipe.jenkins.vault.model.VaultSecret; import com.datapipe.jenkins.vault.model.VaultSecretValue; @@ -106,15 +105,9 @@ public void run(Context context, Run build, EnvVars envVars, PrintStream logger) provideEnvironmentVariablesFromVault(context, build, envVars); } - @Override - protected VaultCredential retrieveVaultCredentials(Run build) { - return null; - } - public void verifyCalls() { verify(mockAccessor, times(2)).init(); verify(mockAccessor, times(2)).read("not/existing", 2); } } - } diff --git a/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java b/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java index bd92e5dd..b9c2707a 100644 --- a/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java +++ b/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java @@ -31,7 +31,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jenkinsci.plugins.workflow.actions.ArgumentsAction; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.flow.FlowExecution; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; +import org.jenkinsci.plugins.workflow.graphanalysis.NodeStepTypePredicate; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.Before; @@ -41,8 +46,10 @@ import static hudson.Functions.isWindows; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.collection.IsMapContaining.hasEntry; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; @@ -254,16 +261,13 @@ public void shouldDealWithTokenBasedCredential() throws Exception { public void shouldUseJenkinsfileConfiguration() throws Exception { WorkflowJob pipeline = jenkins.createProject(WorkflowJob.class, "Pipeline"); pipeline.setDefinition(new CpsFlowDefinition("node {\n" + - " wrap([$class: 'VaultBuildWrapperWithMockAccessor', \n" + - " configuration: [$class: 'VaultConfiguration', \n" + - " vaultCredentialId: '" + GLOBAL_CREDENTIALS_ID_2 + "', \n" - + - " vaultUrl: '" + JENKINSFILE_URL + "'], \n" + - " vaultSecrets: [\n" + - " [$class: 'VaultSecret', path: 'secret/path1', secretValues: [\n" - + - " [$class: 'VaultSecretValue', envVar: 'envVar1', vaultKey: 'key1']]]]]) {\n" - + + " withVaultMock(\n" + + " configuration: [ \n" + + " vaultCredentialId: '" + GLOBAL_CREDENTIALS_ID_2 + "', \n" + + " vaultUrl: '" + JENKINSFILE_URL + "'], \n" + + " vaultSecrets: [\n" + + " [path: 'secret/path1', secretValues: [\n" + + " [envVar: 'envVar1', vaultKey: 'key1']]]]) {\n" + " " + getShellString() + " \"echo ${env.envVar1}\"\n" + " }\n" + "}", true)); @@ -273,6 +277,13 @@ public void shouldUseJenkinsfileConfiguration() throws Exception { jenkins.assertBuildStatus(Result.SUCCESS, build); jenkins.assertLogContains("echo ****", build); jenkins.assertLogNotContains("some-secret", build); + + FlowExecution execution = build.getExecution(); + DepthFirstScanner scanner = new DepthFirstScanner(); + List shellSteps = scanner.filteredNodes(execution, new NodeStepTypePredicate(getShellString())); + assertThat(shellSteps, hasSize(1)); + assertThat(shellSteps.get(0).getAction(ArgumentsAction.class), is(notNullValue())); + assertThat(shellSteps.get(0).getAction(ArgumentsAction.class).getArguments(), hasEntry("script", "echo ${envVar1}")); } @Test diff --git a/src/test/java/com/datapipe/jenkins/vault/it/folder/FolderIT.java b/src/test/java/com/datapipe/jenkins/vault/it/folder/FolderIT.java index 3dbe6e50..8064700a 100644 --- a/src/test/java/com/datapipe/jenkins/vault/it/folder/FolderIT.java +++ b/src/test/java/com/datapipe/jenkins/vault/it/folder/FolderIT.java @@ -251,16 +251,13 @@ public void jobInFolderShouldNotBeAbleToAccessCredentialsScopedToAnotherFolder() public void jenkinsfileShouldOverrideFolderConfig() throws Exception { WorkflowJob pipeline = folder1.createProject(WorkflowJob.class, "Pipeline"); pipeline.setDefinition(new CpsFlowDefinition("node {\n" + - " wrap([$class: 'VaultBuildWrapperWithMockAccessor', \n" + - " configuration: [$class: 'VaultConfiguration', \n" + - " vaultCredentialId: '" + GLOBAL_CREDENTIALS_ID_2 + "', \n" - + - " vaultUrl: '" + JENKINSFILE_URL + "'], \n" + - " vaultSecrets: [\n" + - " [$class: 'VaultSecret', path: 'secret/path1', secretValues: [\n" - + - " [$class: 'VaultSecretValue', envVar: 'envVar1', vaultKey: 'key1']]]]]) {\n" - + + " withVaultMock(\n" + + " configuration: [ \n" + + " vaultCredentialId: '" + GLOBAL_CREDENTIALS_ID_2 + "', \n" + + " vaultUrl: '" + JENKINSFILE_URL + "'], \n" + + " vaultSecrets: [\n" + + " [path: 'secret/path1', secretValues: [\n" + + " [envVar: 'envVar1', vaultKey: 'key1']]]]) {\n" + " " + getShellString() + " \"echo ${env.envVar1}\"\n" + " }\n" + "}", true));