Skip to content

Commit

Permalink
Enhance JFrog CLI Credentials Input During Setup (#101)
Browse files Browse the repository at this point in the history
  • Loading branch information
EyalDelarea authored Sep 19, 2024
1 parent 4e451f6 commit 6f34090
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 28 deletions.
96 changes: 74 additions & 22 deletions src/main/java/io/jenkins/plugins/jfrog/JfStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -59,10 +71,6 @@ public JfStep(Object args) {
this.args = split(args.toString());
}

public String[] getArgs() {
return args;
}

/**
* Build and run a 'jf' command.
*
Expand All @@ -77,19 +85,19 @@ 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();
}

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) {
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand All @@ -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<JFrogPlatformInstance> 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();
}
Expand All @@ -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());
Expand Down Expand Up @@ -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<Builder> {
Expand All @@ -294,4 +325,25 @@ public boolean isApplicable(Class<? extends AbstractProject> 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);
}
}
}
40 changes: 40 additions & 0 deletions src/test/java/io/jenkins/plugins/jfrog/JfStepTest.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Arguments> jfrogCLIPathProvider() {
return Stream.of(
// Unix agent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<JFrogPlatformInstance> artifactoryServers = new ArrayList<JFrogPlatformInstance>() {{
// 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<JFrogPlatformInstance> 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);
Expand Down

0 comments on commit 6f34090

Please sign in to comment.