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 #112

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f57a192
Use stdin if available
EyalDelarea Sep 8, 2024
b4cd09f
Add tests and test on local machine
EyalDelarea Sep 9, 2024
6c265b3
Fix static analysis
EyalDelarea Sep 9, 2024
3c41f76
Test
EyalDelarea Sep 10, 2024
e4ce2d5
Fix tests
EyalDelarea Sep 10, 2024
d8d23b0
Move set version
EyalDelarea Sep 10, 2024
33ccd7d
Fix test
EyalDelarea Sep 11, 2024
19196fd
Test one server
EyalDelarea Sep 11, 2024
eda7a0f
Revert
EyalDelarea Sep 11, 2024
7bc52c4
Test
EyalDelarea Sep 11, 2024
22afcb3
Fix tests
EyalDelarea Sep 11, 2024
364831b
Diff
EyalDelarea Sep 11, 2024
5b9d566
Add password for dummy test server
EyalDelarea Sep 11, 2024
be50054
Set new server, use class variables
EyalDelarea Sep 12, 2024
4f65153
fix
EyalDelarea Sep 12, 2024
1e4ec20
Dont change stdout
EyalDelarea Sep 12, 2024
53e149a
Move the get version to start
EyalDelarea Sep 12, 2024
6d5121a
Refactor
EyalDelarea Sep 12, 2024
0bc0ff0
CR
EyalDelarea Sep 12, 2024
cea9c45
try with closed resources
EyalDelarea Sep 12, 2024
9ec102a
try with closed resources
EyalDelarea Sep 12, 2024
0761a7e
Merge branch 'main' of https://github.com/jfrog/jenkins-jfrog-plugin …
EyalDelarea Nov 5, 2024
4e62e09
Don't use stdin in plugins launchers
EyalDelarea Nov 6, 2024
fb4587a
Add test
EyalDelarea Nov 11, 2024
b79092d
Add test
EyalDelarea Nov 11, 2024
0d9b207
CR
EyalDelarea Nov 11, 2024
d5016e3
Remove getter
EyalDelarea Nov 11, 2024
a8b2571
Add comments
EyalDelarea Nov 13, 2024
e24cc8f
Add comments
EyalDelarea Nov 13, 2024
4bb0c5a
Extract to function
EyalDelarea Nov 14, 2024
9b974af
test
EyalDelarea Nov 14, 2024
7a2bab0
remove class field cli version
EyalDelarea Nov 14, 2024
ac5b3b7
Typo
EyalDelarea Nov 14, 2024
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
118 changes: 94 additions & 24 deletions src/main/java/io/jenkins/plugins/jfrog/JfStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
import io.jenkins.plugins.jfrog.plugins.PluginsUtils;
import jenkins.tasks.SimpleBuildStep;
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 @@ -46,8 +49,18 @@
@SuppressWarnings("unused")
public class JfStep extends Builder implements SimpleBuildStep {
private final ObjectMapper mapper = createMapper();
private static final Version MIN_CLI_VERSION_PASSWORD_STDIN = new Version("2.31.3");
static final String STEP_NAME = "jf";

protected String[] args;
// The current JFrog CLI version in the agent
protected Version currentCliVersion;
EyalDelarea marked this conversation as resolved.
Show resolved Hide resolved
// The JFrog CLI binary path in the agent
protected String jfrogBinaryPath;
// True if the agent's OS is windows
protected boolean isWindows;
// Indicates the launcher type
protected boolean isPluginLauncher;

@DataBoundConstructor
public JfStep(Object args) {
Expand Down Expand Up @@ -77,19 +90,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 +155,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 +177,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 +201,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,27 +221,48 @@ private void configAllServers(Launcher.ProcStarter launcher, String jfrogBinaryP
}
}

private void addConfigArguments(ArgumentListBuilder builder, JFrogPlatformInstance jfrogPlatformInstance, String jfrogBinaryPath, Job<?, ?> job) {
String credentialsId = jfrogPlatformInstance.getCredentialsConfig().getCredentialsId();
private void addConfigArguments(ArgumentListBuilder builder, JFrogPlatformInstance jfrogPlatformInstance, String jfrogBinaryPath, Job<?, ?> job, Launcher.ProcStarter launcher) throws IOException {
builder.add(jfrogBinaryPath).add("c").add("add").add(jfrogPlatformInstance.getId());
// Add credentials
addCredentialsArguments(builder, jfrogPlatformInstance, job, launcher);
addUrlArguments(builder, jfrogPlatformInstance);
builder.add("--interactive=false").add("--overwrite=true");
}

void addCredentialsArguments(ArgumentListBuilder builder, JFrogPlatformInstance jfrogPlatformInstance, Job<?, ?> job, Launcher.ProcStarter launcher) throws IOException {
String credentialsId = jfrogPlatformInstance.getCredentialsConfig().getCredentialsId();
StringCredentials accessTokenCredentials = PluginsUtils.accessTokenCredentialsLookup(credentialsId, job);

if (accessTokenCredentials != null) {
builder.addMasked("--access-token=" + accessTokenCredentials.getSecret().getPlainText());
} else {
Credentials credentials = PluginsUtils.credentialsLookup(credentialsId, job);
builder.add("--user=" + credentials.getUsername());
addPasswordArgument(builder, credentials, launcher);
}
}

// Password can be provided via stdin if supported; otherwise, default-to --password.
// Stdin support requires a minimum CLI version and not a special plugin launcher.
// In other types of launchers, the stdin input gets lost and the command fails.
private void addPasswordArgument(ArgumentListBuilder builder, Credentials credentials, Launcher.ProcStarter launcher) throws IOException {
boolean isCliVersionSupported = this.currentCliVersion.isAtLeast(MIN_CLI_VERSION_PASSWORD_STDIN);
if (isCliVersionSupported && !this.isPluginLauncher) {
// Use stdin
builder.add("--password-stdin");
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(credentials.getPassword().getPlainText().getBytes(StandardCharsets.UTF_8))) {
launcher.stdin(inputStream);
}
} else {
// Use default
builder.addMasked("--password=" + credentials.getPassword());
}
// Add URLs
}

private void addUrlArguments(ArgumentListBuilder builder, JFrogPlatformInstance jfrogPlatformInstance) {
builder.add("--url=" + jfrogPlatformInstance.getUrl());
builder.add("--artifactory-url=" + jfrogPlatformInstance.inferArtifactoryUrl());
builder.add("--distribution-url=" + jfrogPlatformInstance.inferDistributionUrl());
builder.add("--xray-url=" + jfrogPlatformInstance.inferXrayUrl());

builder.add("--interactive=false");
// The installation process takes place more than once per build, so we will configure the same server ID several times.
builder.add("--overwrite=true");
}

/**
Expand Down Expand Up @@ -280,6 +312,23 @@ 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);
EyalDelarea marked this conversation as resolved.
Show resolved Hide resolved
this.isPluginLauncher = launcher.getClass().getName().contains("org.jenkinsci.plugins");
}

@Symbol("jf")
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
Expand All @@ -294,4 +343,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);
}
}
}
105 changes: 101 additions & 4 deletions src/test/java/io/jenkins/plugins/jfrog/JfStepTest.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package io.jenkins.plugins.jfrog;

import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.Job;
import hudson.util.ArgumentListBuilder;
import io.jenkins.plugins.jfrog.configuration.CredentialsConfig;
import io.jenkins.plugins.jfrog.configuration.JFrogPlatformInstance;
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.*;

/**
* @author yahavi
**/
public class JfStepTest {

@ParameterizedTest
Expand All @@ -37,4 +47,91 @@ private static Stream<Arguments> jfrogCLIPathProvider() {
Arguments.of(new EnvVars(), true, "jf.exe")
);
}
}

@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());
}

/**
* Tests the addCredentialsArguments method logic with password-stdin vs.-- password flag.
* Password-stdin flag should only be set if the CLI version is supported
* AND the launcher is not the plugin launcher.
* Plugin launchers do not support password-stdin, as they do not have access to the standard input by default.
*
* @param cliVersion The CLI version
* @param isPluginLauncher Whether the launcher is the plugin launcher
* @param expectedOutput The expected output
* @throws IOException error
*/
@ParameterizedTest
@MethodSource("provideTestArguments")
void testAddCredentialsArguments(String cliVersion, boolean isPluginLauncher, String expectedOutput) throws IOException {
// Mock the necessary objects
JFrogPlatformInstance jfrogPlatformInstance = mock(JFrogPlatformInstance.class);
CredentialsConfig credentialsConfig = mock(CredentialsConfig.class);
when(jfrogPlatformInstance.getId()).thenReturn("instance-id");
when(jfrogPlatformInstance.getCredentialsConfig()).thenReturn(credentialsConfig);
when(credentialsConfig.getCredentialsId()).thenReturn("credentials-id");

Job<?, ?> job = mock(Job.class);
Launcher.ProcStarter launcher = mock(Launcher.ProcStarter.class);

// Create an instance of JfStep
JfStep jfStep = new JfStep("Mock Test");
jfStep.currentCliVersion = new Version(cliVersion);
jfStep.isPluginLauncher = isPluginLauncher;


// Create an ArgumentListBuilder
ArgumentListBuilder builder = new ArgumentListBuilder();

// Call the addCredentialsArguments method
jfStep.addCredentialsArguments(builder, jfrogPlatformInstance, job, launcher);

// Verify the arguments
assertTrue(builder.toList().contains(expectedOutput));
}

private static Stream<Arguments> provideTestArguments() {
String passwordFlag = "--password=";
String passwordStdinFlag = "--password-stdin";
// Min version for password stdin is 2.31.3
return Stream.of(
// Supported CLI version but Plugin Launcher
Arguments.of("2.57.0", true, passwordFlag),
// Unsupported Version
Arguments.of("2.31.0", false, passwordFlag),
// Supported CLI version and local launcher
Arguments.of("2.57.0", false, passwordStdinFlag),
// Unsupported CLI version and Plugin Launcher
Arguments.of("2.31.0", true, passwordFlag),
// Minimum supported CLI version for password stdin
Arguments.of("2.31.3", false, passwordStdinFlag)
);
}
}
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