Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance JFrog CLI Credentials Input During Setup #101

Merged
merged 21 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 88 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,6 +22,7 @@
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.exception.ExceptionUtils;
import org.jenkinsci.Symbol;
Expand All @@ -30,6 +31,7 @@
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 +49,15 @@
public class JfStep extends Builder implements SimpleBuildStep {
private final ObjectMapper mapper = createMapper();
static final String STEP_NAME = "jf";
private static final String MIN_CLI_VERSION_PASSWORD_STDIN = "2.31.0";
@Getter
protected String[] args;
// The current JFrog CLI version in the agent
protected String 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 +69,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 +83,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 +148,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 +170,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 +194,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 +214,25 @@ 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) {
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 (isCliVersionGreaterThanOrEqual(this.currentCliVersion, MIN_CLI_VERSION_PASSWORD_STDIN)) {
builder.add("--password-stdin");
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 +292,21 @@ 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
EyalDelarea marked this conversation as resolved.
Show resolved Hide resolved
*/
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 +321,43 @@ public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
}

String getJfrogCliVersion(Launcher.ProcStarter launcher) throws IOException, InterruptedException {
if (this.currentCliVersion != null && !this.currentCliVersion.isEmpty()) {
return this.currentCliVersion;
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
EyalDelarea marked this conversation as resolved.
Show resolved Hide resolved
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[] versionParts = versionOutput.split(" ");
if (versionOutput.isEmpty() || (versionParts.length < 3)) {
EyalDelarea marked this conversation as resolved.
Show resolved Hide resolved
EyalDelarea marked this conversation as resolved.
Show resolved Hide resolved
throw new IOException(String.format("Failed to parse JFrog CLI version. CLI Output: %s", versionOutput));
}
// Based on the output, the version should be located at the second index
return versionParts[2];
}

boolean isCliVersionGreaterThanOrEqual(String currentVersion, String targetVersion) {
String[] currentParts = currentVersion.split("\\.");
String[] targetParts = targetVersion.split("\\.");
for (int i = 0; i < Math.min(currentParts.length, targetParts.length); i++) {
int currentPart = Integer.parseInt(currentParts[i]);
int targetPart = Integer.parseInt(targetParts[i]);
if (currentPart > targetPart) {
return true;
} else if (currentPart < targetPart) {
return false;
}
}
return currentParts.length >= targetParts.length;
}
EyalDelarea marked this conversation as resolved.
Show resolved Hide resolved
}
57 changes: 57 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,25 @@
package io.jenkins.plugins.jfrog;

import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.util.ArgumentListBuilder;
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 +32,53 @@ void getJFrogCLIPathTest(EnvVars inputEnvVars, boolean isWindows, String expecte
Assertions.assertEquals(expectedOutput, getJFrogCLIPath(inputEnvVars, isWindows));
}

@Test
void isCliVersionGreaterThanTest() {
JfStep jfStep = new JfStep("--version");

// Test cases where the current version is greater
assertTrue(jfStep.isCliVersionGreaterThanOrEqual("2.32.0", "2.31.0"));
assertTrue(jfStep.isCliVersionGreaterThanOrEqual("3.0.0", "2.31.0"));
assertTrue(jfStep.isCliVersionGreaterThanOrEqual("2.31.1", "2.31.0"));

// Test cases where the current version is equal
assertTrue(jfStep.isCliVersionGreaterThanOrEqual("2.31.0", "2.31.0"));

// Test cases where the current version is less
assertFalse(jfStep.isCliVersionGreaterThanOrEqual("2.30.0", "2.31.0"));
assertFalse(jfStep.isCliVersionGreaterThanOrEqual("2.31.0", "2.31.1"));
assertFalse(jfStep.isCliVersionGreaterThanOrEqual("1.31.0", "2.31.0"));
}

@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");
String version = jfStep.getJfrogCliVersion(procStarter);

// Verify the result
assertEquals("2.31.0", version);
}

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
Loading