diff --git a/src/main/java/io/jenkins/plugins/jfrog/JfStep.java b/src/main/java/io/jenkins/plugins/jfrog/JfStep.java index 9a721ee1..b79e9ebf 100644 --- a/src/main/java/io/jenkins/plugins/jfrog/JfStep.java +++ b/src/main/java/io/jenkins/plugins/jfrog/JfStep.java @@ -22,14 +22,18 @@ import io.jenkins.plugins.jfrog.models.BuildInfoOutputModel; import io.jenkins.plugins.jfrog.plugins.PluginsUtils; import jenkins.tasks.SimpleBuildStep; +import lombok.Getter; import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.plaincredentials.StringCredentials; import org.jfrog.build.api.util.Log; +import org.jfrog.build.client.Version; import org.kohsuke.stapler.DataBoundConstructor; import javax.annotation.Nonnull; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -47,7 +51,15 @@ public class JfStep extends Builder implements SimpleBuildStep { private final ObjectMapper mapper = createMapper(); static final String STEP_NAME = "jf"; + private static final Version MIN_CLI_VERSION_PASSWORD_STDIN = new Version("2.31.3"); + @Getter protected String[] args; + // The current JFrog CLI version in the agent + protected Version currentCliVersion; + // The JFrog CLI binary path in the agent + protected String jfrogBinaryPath; + // True if the agent's OS is windows + protected boolean isWindows; @DataBoundConstructor public JfStep(Object args) { @@ -59,10 +71,6 @@ public JfStep(Object args) { this.args = split(args.toString()); } - public String[] getArgs() { - return args; - } - /** * Build and run a 'jf' command. * @@ -77,11 +85,11 @@ public String[] getArgs() { @Override public void perform(@NonNull Run run, @NonNull FilePath workspace, @NonNull EnvVars env, @NonNull Launcher launcher, @NonNull TaskListener listener) throws InterruptedException, IOException { workspace.mkdirs(); + // Initialize values to be used across the class + initClassValues(workspace, env, launcher); + // Build the 'jf' command ArgumentListBuilder builder = new ArgumentListBuilder(); - boolean isWindows = !launcher.isUnix(); - String jfrogBinaryPath = getJFrogCLIPath(env, isWindows); - builder.add(jfrogBinaryPath).add(args); if (isWindows) { builder = builder.toWindowsCommand(); @@ -89,7 +97,7 @@ public void perform(@NonNull Run run, @NonNull FilePath workspace, @NonNul try (ByteArrayOutputStream taskOutputStream = new ByteArrayOutputStream()) { JfTaskListener jfTaskListener = new JfTaskListener(listener, taskOutputStream); - Launcher.ProcStarter jfLauncher = setupJFrogEnvironment(run, env, launcher, jfTaskListener, workspace, jfrogBinaryPath, isWindows); + Launcher.ProcStarter jfLauncher = setupJFrogEnvironment(run, env, launcher, jfTaskListener, workspace); // Running the 'jf' command int exitValue = jfLauncher.cmds(builder).join(); if (exitValue != 0) { @@ -142,18 +150,16 @@ private void logIfNoToolProvided(EnvVars env, TaskListener listener) { /** * Configure all JFrog relevant environment variables and all servers (if they haven't been configured yet). * - * @param run running as part of a specific build - * @param env environment variables applicable to this step - * @param launcher a way to start processes - * @param listener a place to send output - * @param workspace a workspace to use for any file operations - * @param jfrogBinaryPath path to jfrog cli binary on the filesystem - * @param isWindows is Windows the applicable OS + * @param run running as part of a specific build + * @param env environment variables applicable to this step + * @param launcher a way to start processes + * @param listener a place to send output + * @param workspace a workspace to use for any file operations * @return launcher applicable to this step. * @throws InterruptedException if the step is interrupted * @throws IOException in case of any I/O error, or we failed to run the 'jf' command */ - public Launcher.ProcStarter setupJFrogEnvironment(Run run, EnvVars env, Launcher launcher, TaskListener listener, FilePath workspace, String jfrogBinaryPath, boolean isWindows) throws IOException, InterruptedException { + public Launcher.ProcStarter setupJFrogEnvironment(Run run, EnvVars env, Launcher launcher, TaskListener listener, FilePath workspace) throws IOException, InterruptedException { JFrogCliConfigEncryption jfrogCliConfigEncryption = run.getAction(JFrogCliConfigEncryption.class); if (jfrogCliConfigEncryption == null) { // Set up the config encryption action to allow encrypting the JFrog CLI configuration and make sure we only create one key @@ -166,7 +172,7 @@ public Launcher.ProcStarter setupJFrogEnvironment(Run run, EnvVars env, La // Configure all servers, skip if all server ids have already been configured. if (shouldConfig(jfrogHomeTempDir)) { logIfNoToolProvided(env, listener); - configAllServers(jfLauncher, jfrogBinaryPath, isWindows, run.getParent()); + configAllServers(jfLauncher, run.getParent()); } return jfLauncher; } @@ -190,14 +196,14 @@ private boolean shouldConfig(FilePath jfrogHomeTempDir) throws IOException, Inte /** * Locally configure all servers that was configured in the Jenkins UI. */ - private void configAllServers(Launcher.ProcStarter launcher, String jfrogBinaryPath, boolean isWindows, Job job) throws IOException, InterruptedException { + private void configAllServers(Launcher.ProcStarter launcher, Job job) throws IOException, InterruptedException { // Config all servers using the 'jf c add' command. List jfrogInstances = JFrogPlatformBuilder.getJFrogPlatformInstances(); - if (jfrogInstances != null && jfrogInstances.size() > 0) { + if (jfrogInstances != null && !jfrogInstances.isEmpty()) { for (JFrogPlatformInstance jfrogPlatformInstance : jfrogInstances) { // Build 'jf' command ArgumentListBuilder builder = new ArgumentListBuilder(); - addConfigArguments(builder, jfrogPlatformInstance, jfrogBinaryPath, job); + addConfigArguments(builder, jfrogPlatformInstance, jfrogBinaryPath, job, launcher); if (isWindows) { builder = builder.toWindowsCommand(); } @@ -210,17 +216,26 @@ private void configAllServers(Launcher.ProcStarter launcher, String jfrogBinaryP } } - private void addConfigArguments(ArgumentListBuilder builder, JFrogPlatformInstance jfrogPlatformInstance, String jfrogBinaryPath, Job job) { + private void addConfigArguments(ArgumentListBuilder builder, JFrogPlatformInstance jfrogPlatformInstance, String jfrogBinaryPath, Job job, Launcher.ProcStarter launcher) throws IOException { String credentialsId = jfrogPlatformInstance.getCredentialsConfig().getCredentialsId(); builder.add(jfrogBinaryPath).add("c").add("add").add(jfrogPlatformInstance.getId()); // Add credentials StringCredentials accessTokenCredentials = PluginsUtils.accessTokenCredentialsLookup(credentialsId, job); + // Access Token if (accessTokenCredentials != null) { builder.addMasked("--access-token=" + accessTokenCredentials.getSecret().getPlainText()); } else { Credentials credentials = PluginsUtils.credentialsLookup(credentialsId, job); builder.add("--user=" + credentials.getUsername()); - builder.addMasked("--password=" + credentials.getPassword()); + // Use password-stdin if available + if (this.currentCliVersion.isAtLeast(MIN_CLI_VERSION_PASSWORD_STDIN)) { + builder.add("--password-stdin"); + try(ByteArrayInputStream inputStream = new ByteArrayInputStream(credentials.getPassword().getPlainText().getBytes(StandardCharsets.UTF_8))) { + launcher.stdin(inputStream); + } + } else { + builder.addMasked("--password=" + credentials.getPassword()); + } } // Add URLs builder.add("--url=" + jfrogPlatformInstance.getUrl()); @@ -280,6 +295,22 @@ private void logIllegalBuildPublishOutput(Log log, ByteArrayOutputStream taskOut log.warn("Illegal build-publish output: " + taskOutputStream.toString(StandardCharsets.UTF_8)); } + /** + * initialize values to be used across the class. + * + * @param env environment variables applicable to this step + * @param launcher a way to start processes + * @param workspace a workspace to use for any file operations + * @throws IOException in case of any I/O error, or we failed to run the 'jf' + * @throws InterruptedException if the step is interrupted + */ + private void initClassValues(FilePath workspace, EnvVars env, Launcher launcher) throws IOException, InterruptedException { + this.isWindows = !launcher.isUnix(); + this.jfrogBinaryPath = getJFrogCLIPath(env, isWindows); + Launcher.ProcStarter procStarter = launcher.launch().envs(env).pwd(workspace); + this.currentCliVersion = getJfrogCliVersion(procStarter); + } + @Symbol("jf") @Extension public static final class DescriptorImpl extends BuildStepDescriptor { @@ -294,4 +325,25 @@ public boolean isApplicable(Class jobType) { return true; } } + + Version getJfrogCliVersion(Launcher.ProcStarter launcher) throws IOException, InterruptedException { + if (this.currentCliVersion != null) { + return this.currentCliVersion; + } + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()){ + ArgumentListBuilder builder = new ArgumentListBuilder(); + builder.add(jfrogBinaryPath).add("-v"); + int exitCode = launcher + .cmds(builder) + .pwd(launcher.pwd()) + .stdout(outputStream) + .join(); + if (exitCode != 0) { + throw new IOException("Failed to get JFrog CLI version: " + outputStream.toString(StandardCharsets.UTF_8)); + } + String versionOutput = outputStream.toString(StandardCharsets.UTF_8).trim(); + String version = StringUtils.substringAfterLast(versionOutput, " "); + return new Version(version); + } + } } diff --git a/src/test/java/io/jenkins/plugins/jfrog/JfStepTest.java b/src/test/java/io/jenkins/plugins/jfrog/JfStepTest.java index afa4025f..d2b4a995 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/JfStepTest.java +++ b/src/test/java/io/jenkins/plugins/jfrog/JfStepTest.java @@ -1,15 +1,26 @@ package io.jenkins.plugins.jfrog; import hudson.EnvVars; +import hudson.FilePath; +import hudson.Launcher; +import hudson.util.ArgumentListBuilder; +import org.jfrog.build.client.Version; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.stream.Stream; import static io.jenkins.plugins.jfrog.JfStep.getJFrogCLIPath; import static io.jenkins.plugins.jfrog.JfrogInstallation.JFROG_BINARY_PATH; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * @author yahavi @@ -22,6 +33,35 @@ void getJFrogCLIPathTest(EnvVars inputEnvVars, boolean isWindows, String expecte Assertions.assertEquals(expectedOutput, getJFrogCLIPath(inputEnvVars, isWindows)); } + @Test + void getJfrogCliVersionTest() throws IOException, InterruptedException { + // Mock the Launcher + Launcher launcher = mock(Launcher.class); + // Mock the Launcher.ProcStarter + Launcher.ProcStarter procStarter = mock(Launcher.ProcStarter.class); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + // Mocks the return value of --version command + outputStream.write("jf version 2.31.0 ".getBytes()); + // Mock the behavior of the Launcher and ProcStarter + when(launcher.launch()).thenReturn(procStarter); + when(procStarter.cmds(any(ArgumentListBuilder.class))).thenReturn(procStarter); + when(procStarter.pwd((FilePath) any())).thenReturn(procStarter); + when(procStarter.stdout(any(ByteArrayOutputStream.class))).thenAnswer(invocation -> { + ByteArrayOutputStream out = invocation.getArgument(0); + out.write(outputStream.toByteArray()); + return procStarter; + }); + when(procStarter.join()).thenReturn(0); + + // Create an instance of JfStep and call the method + JfStep jfStep = new JfStep("--version"); + jfStep.isWindows = System.getProperty("os.name").toLowerCase().contains("win"); + Version version = jfStep.getJfrogCliVersion(procStarter); + + // Verify the result + assertEquals("2.31.0", version.toString()); + } + private static Stream jfrogCLIPathProvider() { return Stream.of( // Unix agent diff --git a/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java b/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java index 9cba91a9..64746fc6 100644 --- a/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java +++ b/src/test/java/io/jenkins/plugins/jfrog/integration/PipelineTestBase.java @@ -19,7 +19,6 @@ import io.jenkins.plugins.jfrog.BinaryInstaller; import io.jenkins.plugins.jfrog.JfrogInstallation; import io.jenkins.plugins.jfrog.ReleasesInstaller; -import io.jenkins.plugins.jfrog.configuration.Credentials; import io.jenkins.plugins.jfrog.configuration.CredentialsConfig; import io.jenkins.plugins.jfrog.configuration.JFrogPlatformBuilder; import io.jenkins.plugins.jfrog.configuration.JFrogPlatformInstance; @@ -69,6 +68,7 @@ public class PipelineTestBase { static final String JFROG_CLI_TOOL_NAME_1 = "jfrog-cli"; static final String JFROG_CLI_TOOL_NAME_2 = "jfrog-cli-2"; static final String TEST_CONFIGURED_SERVER_ID = "serverId"; + static final String TEST_CONFIGURED_SERVER_ID_2 = "serverId2"; // Set up jenkins and configure latest JFrog CLI. public void initPipelineTest(JenkinsRule jenkins) throws Exception { @@ -161,13 +161,11 @@ private static void verifyEnvironment() { private void setGlobalConfiguration() throws IOException { JFrogPlatformBuilder.DescriptorImpl jfrogBuilder = (JFrogPlatformBuilder.DescriptorImpl) jenkins.getInstance().getDescriptor(JFrogPlatformBuilder.class); Assert.assertNotNull(jfrogBuilder); - CredentialsConfig emptyCred = new CredentialsConfig(StringUtils.EMPTY, Credentials.EMPTY_CREDENTIALS); CredentialsConfig platformCred = new CredentialsConfig(Secret.fromString(ARTIFACTORY_USERNAME), Secret.fromString(ARTIFACTORY_PASSWORD), Secret.fromString(ACCESS_TOKEN), "credentials"); - List artifactoryServers = new ArrayList() {{ - // Dummy server to test multiple configured servers. - // The dummy server should be configured first to ensure the right server is being used (and not the first one). - add(new JFrogPlatformInstance("dummyServerId", "", emptyCred, "", "", "")); + List artifactoryServers = new ArrayList<>() {{ + // Configure multiple servers to test multiple servers. add(new JFrogPlatformInstance(TEST_CONFIGURED_SERVER_ID, PLATFORM_URL, platformCred, ARTIFACTORY_URL, "", "")); + add(new JFrogPlatformInstance(TEST_CONFIGURED_SERVER_ID_2, PLATFORM_URL, platformCred, ARTIFACTORY_URL, "", "")); }}; jfrogBuilder.setJfrogInstances(artifactoryServers); Jenkins.get().getDescriptorByType(JFrogPlatformBuilder.DescriptorImpl.class).setJfrogInstances(artifactoryServers);