diff --git a/src/main/java/org/jfrog/bamboo/configuration/JfrogCliDeploymentConfiguration.java b/src/main/java/org/jfrog/bamboo/configuration/JfrogCliDeploymentConfiguration.java new file mode 100644 index 00000000..c2f600b8 --- /dev/null +++ b/src/main/java/org/jfrog/bamboo/configuration/JfrogCliDeploymentConfiguration.java @@ -0,0 +1,58 @@ +package org.jfrog.bamboo.configuration; + +import com.atlassian.bamboo.collections.ActionParametersMap; +import com.atlassian.bamboo.task.TaskDefinition; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jfrog.bamboo.context.JfrogCliDeploymentContext; + +import java.util.Map; + +/** + * Task configurator for the JFrog CLI Deployment task. + * Populates the UI context with the configured Artifactory servers and the saved jf command. + */ +public class JfrogCliDeploymentConfiguration extends AbstractArtifactoryConfiguration { + + @Override + public void populateContextForCreate(@NotNull Map context) { + super.populateContextForCreate(context); + context.put("serverConfigManager", serverConfigManager); + context.put("selectedServerId", -1); + context.put(JfrogCliDeploymentContext.JF_COMMAND, ""); + } + + @Override + public void populateContextForEdit(@NotNull Map context, + @NotNull TaskDefinition taskDefinition) { + super.populateContextForEdit(context, taskDefinition); + populateContextWithConfiguration(context, taskDefinition, JfrogCliDeploymentContext.getFieldsToCopy()); + + String selectedServerId = taskDefinition.getConfiguration().get(JfrogCliDeploymentContext.SERVER_ID); + context.put("serverConfigManager", serverConfigManager); + context.put("selectedServerId", StringUtils.defaultIfBlank(selectedServerId, "-1")); + context.put(JfrogCliDeploymentContext.JF_COMMAND, + StringUtils.defaultString(taskDefinition.getConfiguration().get(JfrogCliDeploymentContext.JF_COMMAND))); + } + + @NotNull + @Override + public Map generateTaskConfigMap(@NotNull ActionParametersMap params, + @Nullable TaskDefinition previousTaskDefinition) { + Map taskConfigMap = super.generateTaskConfigMap(params, previousTaskDefinition); + taskConfiguratorHelper.populateTaskConfigMapWithActionParameters( + taskConfigMap, params, JfrogCliDeploymentContext.getFieldsToCopy()); + return taskConfigMap; + } + + @Override + public boolean taskProducesTestResults(@NotNull TaskDefinition taskDefinition) { + return false; + } + + @Override + protected String getKey() { + return JfrogCliDeploymentContext.PREFIX; + } +} diff --git a/src/main/java/org/jfrog/bamboo/context/ArtifactoryBuildContext.java b/src/main/java/org/jfrog/bamboo/context/ArtifactoryBuildContext.java index 011b0c71..0da34b95 100644 --- a/src/main/java/org/jfrog/bamboo/context/ArtifactoryBuildContext.java +++ b/src/main/java/org/jfrog/bamboo/context/ArtifactoryBuildContext.java @@ -113,6 +113,22 @@ public String getBuildNumber(BuildContext buildContext) { return String.valueOf(buildContext.getBuildNumber()); } + /** + * Get build name from context without falling back to a build plan. + * Used by deployment-project tasks where no {@link BuildContext} is available. + */ + public String getBuildName() { + return env.get(BUILD_NAME); + } + + /** + * Get build number from context without falling back to a build plan. + * Used by deployment-project tasks where no {@link BuildContext} is available. + */ + public String getBuildNumber() { + return env.get(BUILD_NUMBER); + } + public String getOverriddenUsername(Map runtimeTaskContext, Log log, boolean deployer) { switch (StringUtils.defaultString(deployer ? getDeployerOverrideCredentialsChoice() : getResolverOverrideCredentialsChoice())) { case CVG_CRED_NO_OVERRIDE: diff --git a/src/main/java/org/jfrog/bamboo/context/JfrogCliDeploymentContext.java b/src/main/java/org/jfrog/bamboo/context/JfrogCliDeploymentContext.java new file mode 100644 index 00000000..5317dcf2 --- /dev/null +++ b/src/main/java/org/jfrog/bamboo/context/JfrogCliDeploymentContext.java @@ -0,0 +1,45 @@ +package org.jfrog.bamboo.context; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; +import java.util.Set; + +/** + * Configuration context for the JFrog CLI Deployment task. + * Holds the server selection and the free-form jf CLI command entered by the user. + */ +public class JfrogCliDeploymentContext { + + public static final String PREFIX = "artifactory.jfrogCli."; + public static final String SERVER_ID = PREFIX + "serverId"; + public static final String JF_COMMAND = PREFIX + "command"; + + private static final Set FIELDS_TO_COPY = Set.of(SERVER_ID, JF_COMMAND); + + private final Map env; + + public JfrogCliDeploymentContext(Map env) { + this.env = env; + } + + public long getSelectedServerId() { + String serverId = env.get(SERVER_ID); + if (StringUtils.isBlank(serverId)) { + return -1; + } + try { + return Long.parseLong(serverId); + } catch (NumberFormatException e) { + return -1; + } + } + + public String getJfCommand() { + return StringUtils.trimToEmpty(env.get(JF_COMMAND)); + } + + public static Set getFieldsToCopy() { + return FIELDS_TO_COPY; + } +} diff --git a/src/main/java/org/jfrog/bamboo/task/ArtifactoryDeploymentJfrogCliTask.java b/src/main/java/org/jfrog/bamboo/task/ArtifactoryDeploymentJfrogCliTask.java new file mode 100644 index 00000000..2956e25f --- /dev/null +++ b/src/main/java/org/jfrog/bamboo/task/ArtifactoryDeploymentJfrogCliTask.java @@ -0,0 +1,153 @@ +package org.jfrog.bamboo.task; + +import com.atlassian.bamboo.deployments.execution.DeploymentTaskContext; +import com.atlassian.bamboo.task.CommonTaskContext; +import com.atlassian.bamboo.task.TaskException; +import com.atlassian.bamboo.task.TaskResult; +import com.atlassian.bamboo.task.TaskResultBuilder; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jfrog.bamboo.admin.ServerConfig; +import org.jfrog.bamboo.admin.ServerConfigManager; +import org.jfrog.bamboo.context.JfrogCliDeploymentContext; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Deployment-project variant of the JFrog CLI task. + * + *

Allows running any {@code jf} CLI command from a Bamboo Deployment Project environment. + * The selected JFrog server configuration (URL, credentials) is injected into the process + * environment via the standard JFrog CLI environment variables so that the user-supplied + * command does not need to embed credentials. + * + *

The {@code jf} binary must be pre-installed and available on the build agent's PATH. + */ +public class ArtifactoryDeploymentJfrogCliTask extends ArtifactoryDeploymentTaskType { + + /** Standard JFrog CLI environment variables for server authentication. */ + private static final String JF_ENV_URL = "JF_URL"; + private static final String JF_ENV_USER = "JF_USER"; + private static final String JF_ENV_PASSWORD = "JF_PASSWORD"; + + private JfrogCliDeploymentContext cliContext; + private ServerConfig serverConfig; + + @Override + protected void initTask(@NotNull CommonTaskContext context) throws TaskException { + super.initTask(context); + cliContext = new JfrogCliDeploymentContext(context.getConfigurationMap()); + + ServerConfigManager serverConfigManager = ServerConfigManager.getInstance(); + serverConfig = serverConfigManager.getServerConfigById(cliContext.getSelectedServerId()); + if (serverConfig == null) { + throw new TaskException( + "JFrog CLI Deployment task: no Artifactory server is configured. " + + "Please select a server in the task configuration."); + } + } + + @NotNull + @Override + protected TaskResult runTask(@NotNull DeploymentTaskContext deploymentTaskContext) { + String jfCommand = cliContext.getJfCommand(); + if (StringUtils.isBlank(jfCommand)) { + buildInfoLog.error("JFrog CLI Deployment task: the 'JFrog CLI command' field is empty."); + return TaskResultBuilder.newBuilder(deploymentTaskContext).failedWithError().build(); + } + + try { + int exitCode = executeJfCommand(jfCommand, deploymentTaskContext.getWorkingDirectory()); + if (exitCode != 0) { + buildInfoLog.error("JFrog CLI command exited with code " + exitCode); + return TaskResultBuilder.newBuilder(deploymentTaskContext).failed().build(); + } + return TaskResultBuilder.newBuilder(deploymentTaskContext).success().build(); + } catch (InterruptedException e) { + // Restore interrupt status so callers / Bamboo can react to the cancellation. + Thread.currentThread().interrupt(); + buildInfoLog.error("JFrog CLI command was interrupted: " + e.getMessage(), e); + return TaskResultBuilder.newBuilder(deploymentTaskContext).failedWithError().build(); + } catch (IOException e) { + buildInfoLog.error("Exception while running JFrog CLI command: " + e.getMessage(), e); + return TaskResultBuilder.newBuilder(deploymentTaskContext).failedWithError().build(); + } + } + + /** + * Executes the user-supplied jf CLI command as a subprocess. + * + *

Auth is injected through environment variables so the user never has to embed + * credentials in the command string. + */ + private int executeJfCommand(String jfCommand, File workingDir) + throws IOException, InterruptedException { + + List commandParts = buildCommandParts(jfCommand); + buildInfoLog.info("Running: " + String.join(" ", commandParts)); + + ProcessBuilder pb = new ProcessBuilder(commandParts); + pb.directory(workingDir); + pb.redirectErrorStream(true); + + Map env = new HashMap<>(pb.environment()); + env.put(JF_ENV_URL, serverConfig.getUrl()); + if (StringUtils.isNotBlank(serverConfig.getUsername())) { + env.put(JF_ENV_USER, serverConfig.getUsername()); + } + if (StringUtils.isNotBlank(serverConfig.getPassword())) { + env.put(JF_ENV_PASSWORD, serverConfig.getPassword()); + } + pb.environment().putAll(env); + + Process process = pb.start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + logger.addBuildLogEntry(line); + } + } + return process.waitFor(); + } + + /** + * Splits the raw command string into tokens, prepending the "jf" binary. + * The user enters the command WITHOUT the leading "jf", e.g. "rt dl --spec=spec.json". + * If the user already prefixed "jf", the binary is still only added once. + */ + private List buildCommandParts(String jfCommand) { + List parts = new ArrayList<>(); + parts.add("jf"); + + String trimmed = jfCommand.trim(); + // Strip redundant leading "jf" if the user included it + if (trimmed.startsWith("jf ")) { + trimmed = trimmed.substring(3).trim(); + } + + // Simple whitespace split; respects quoted args via shell on each OS + for (String token : trimmed.split("\\s+")) { + if (!token.isEmpty()) { + parts.add(token); + } + } + return parts; + } + + @Override + protected ServerConfig getUsageServerConfig() { + return serverConfig; + } + + @Override + protected String getTaskUsageName() { + return "deployment_jfrog_cli"; + } +} diff --git a/src/main/java/org/jfrog/bamboo/task/ArtifactoryDeploymentPublishBuildInfoTask.java b/src/main/java/org/jfrog/bamboo/task/ArtifactoryDeploymentPublishBuildInfoTask.java new file mode 100644 index 00000000..983f89e2 --- /dev/null +++ b/src/main/java/org/jfrog/bamboo/task/ArtifactoryDeploymentPublishBuildInfoTask.java @@ -0,0 +1,102 @@ +package org.jfrog.bamboo.task; + +import com.atlassian.bamboo.deployments.execution.DeploymentTaskContext; +import com.atlassian.bamboo.task.CommonTaskContext; +import com.atlassian.bamboo.task.TaskException; +import com.atlassian.bamboo.task.TaskResult; +import com.atlassian.bamboo.task.TaskResultBuilder; +import com.atlassian.bamboo.variable.CustomVariableContext; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jfrog.bamboo.admin.ServerConfig; +import org.jfrog.bamboo.admin.ServerConfigManager; +import org.jfrog.bamboo.configuration.BuildParamsOverrideManager; +import org.jfrog.bamboo.context.PublishBuildInfoContext; +import org.jfrog.bamboo.util.TaskUtils; +import org.jfrog.build.extractor.builder.BuildInfoBuilder; +import org.jfrog.build.extractor.ci.BuildInfo; +import org.jfrog.build.extractor.clientConfiguration.ArtifactoryManagerBuilder; +import org.jfrog.build.extractor.clientConfiguration.client.artifactory.ArtifactoryManager; + +import java.util.Date; +import java.util.Map; + +/** + * Deployment-project variant of {@link ArtifactoryPublishBuildInfoTask}. + *

+ * A Bamboo Deployment Project does not provide a {@link com.atlassian.bamboo.v2.build.BuildContext}, so + * this task reads the build name / number directly from its configuration and publishes a minimal + * BuildInfo (without the in-plan build-info aggregation that the build-time task performs). + * + *

This task addresses customer requests for the JFrog plugin to be selectable from the Deployment + * Projects task picker, where previously only "Artifactory Download" / "Artifactory Deployment" were + * available. + */ +public class ArtifactoryDeploymentPublishBuildInfoTask extends ArtifactoryDeploymentTaskType { + + public static final String TASK_NAME = "artifactoryDeploymentPublishBuildInfoTask"; + + private CustomVariableContext customVariableContext; + private ServerConfig publishServerConfig; + private PublishBuildInfoContext publishBuildInfoContext; + + @Override + protected void initTask(@NotNull CommonTaskContext context) throws TaskException { + super.initTask(context); + publishBuildInfoContext = new PublishBuildInfoContext(context.getConfigurationMap()); + publishServerConfig = resolvePublishServerConfig(context); + } + + @NotNull + @Override + protected TaskResult runTask(@NotNull DeploymentTaskContext deploymentTaskContext) { + String buildName = publishBuildInfoContext.getBuildName(); + String buildNumber = publishBuildInfoContext.getBuildNumber(); + if (StringUtils.isBlank(buildName) || StringUtils.isBlank(buildNumber)) { + String message = "Build name and build number must be configured for the deployment Publish Build Info task."; + buildInfoLog.error(message); + return TaskResultBuilder.newBuilder(deploymentTaskContext).failedWithError().build(); + } + + ArtifactoryManagerBuilder clientBuilder = TaskUtils.getArtifactoryManagerBuilderBuilder(publishServerConfig, buildInfoLog); + try (ArtifactoryManager client = clientBuilder.build()) { + BuildInfo build = new BuildInfoBuilder(buildName) + .number(buildNumber) + .startedDate(new Date()) + .build(); + client.publishBuildInfo(build, ""); + buildInfoLog.info("Published build-info for '" + buildName + "/" + buildNumber + "' to " + client.getUrl()); + } catch (Exception e) { + buildInfoLog.error("Exception occurred while publishing build-info: " + e.getMessage(), e); + return TaskResultBuilder.newBuilder(deploymentTaskContext).failedWithError().build(); + } + return TaskResultBuilder.newBuilder(deploymentTaskContext).success().build(); + } + + private ServerConfig resolvePublishServerConfig(@NotNull CommonTaskContext context) { + ServerConfigManager serverConfigManager = ServerConfigManager.getInstance(); + ServerConfig selectedServerConfig = serverConfigManager.getServerConfigById(publishBuildInfoContext.getArtifactoryServerId()); + if (selectedServerConfig == null) { + throw new IllegalArgumentException("Could not find Artifactory server. Please check the Artifactory server in the task configuration."); + } + Map runtimeContext = context.getRuntimeTaskContext(); + return TaskUtils.getDeploymentServerConfig( + publishBuildInfoContext.getOverriddenUsername(runtimeContext, buildInfoLog, true), + publishBuildInfoContext.getOverriddenPassword(runtimeContext, buildInfoLog, true), + serverConfigManager, selectedServerConfig, new BuildParamsOverrideManager(customVariableContext)); + } + + @Override + protected ServerConfig getUsageServerConfig() { + return publishServerConfig; + } + + @Override + protected String getTaskUsageName() { + return "deployment_publish_build_info"; + } + + public void setCustomVariableContext(CustomVariableContext customVariableContext) { + this.customVariableContext = customVariableContext; + } +} diff --git a/src/main/java/org/jfrog/bamboo/task/ArtifactoryDeploymentXrayScanTask.java b/src/main/java/org/jfrog/bamboo/task/ArtifactoryDeploymentXrayScanTask.java new file mode 100644 index 00000000..0a68700b --- /dev/null +++ b/src/main/java/org/jfrog/bamboo/task/ArtifactoryDeploymentXrayScanTask.java @@ -0,0 +1,103 @@ +package org.jfrog.bamboo.task; + +import com.atlassian.bamboo.deployments.execution.DeploymentTaskContext; +import com.atlassian.bamboo.task.CommonTaskContext; +import com.atlassian.bamboo.task.TaskException; +import com.atlassian.bamboo.task.TaskResult; +import com.atlassian.bamboo.task.TaskResultBuilder; +import com.atlassian.bamboo.variable.CustomVariableContext; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jfrog.bamboo.admin.ServerConfig; +import org.jfrog.bamboo.admin.ServerConfigManager; +import org.jfrog.bamboo.configuration.BuildParamsOverrideManager; +import org.jfrog.bamboo.context.XrayScanContext; +import org.jfrog.bamboo.util.TaskUtils; +import org.jfrog.build.client.artifactoryXrayResponse.ArtifactoryXrayResponse; +import org.jfrog.build.client.artifactoryXrayResponse.Summary; +import org.jfrog.build.extractor.clientConfiguration.client.artifactory.ArtifactoryManager; + +import java.util.Map; + +/** + * Deployment-project variant of {@link ArtifactoryXrayScanTask}. + *

+ * The Xray scan only requires a build name, build number and an Artifactory server, so it works + * naturally in deployment projects. This implementation extends {@link ArtifactoryDeploymentTaskType} + * (which implements Bamboo's {@code DeploymentTaskType}) so the task is selectable from the + * Deployment Projects task picker. + */ +public class ArtifactoryDeploymentXrayScanTask extends ArtifactoryDeploymentTaskType { + + public static final String TASK_NAME = "artifactoryDeploymentXrayScanTask"; + + private CustomVariableContext customVariableContext; + private ServerConfig xrayServerConfig; + private XrayScanContext xrayContext; + + @Override + protected void initTask(@NotNull CommonTaskContext context) throws TaskException { + super.initTask(context); + xrayContext = new XrayScanContext(context.getConfigurationMap()); + xrayServerConfig = resolveXrayServerConfig(context); + } + + @NotNull + @Override + protected TaskResult runTask(@NotNull DeploymentTaskContext deploymentTaskContext) { + String buildName = xrayContext.getBuildName(); + String buildNumber = xrayContext.getBuildNumber(); + if (StringUtils.isBlank(buildName) || StringUtils.isBlank(buildNumber)) { + buildInfoLog.error("Build name and build number must be configured for the deployment Xray Scan task."); + return TaskResultBuilder.newBuilder(deploymentTaskContext).failedWithError().build(); + } + + try (ArtifactoryManager client = TaskUtils.getArtifactoryManagerBuilderBuilder(xrayServerConfig, buildInfoLog).build()) { + ArtifactoryXrayResponse buildScanResult = client.scanBuild(buildName, buildNumber, "", "bamboo"); + Summary summary = buildScanResult.getSummary(); + if (summary == null) { + throw new IllegalStateException("Failed while processing the JSON result: 'summary' field is missing."); + } + if (StringUtils.isNotEmpty(summary.getMoreDetailsUrl())) { + logger.addBuildLogEntry("Xray scan details are available at: " + summary.getMoreDetailsUrl()); + } + String scanMessage = summary.getMessage(); + if (summary.isFailBuild() && xrayContext.isFailIfVulnerable()) { + buildInfoLog.error(scanMessage); + return TaskResultBuilder.newBuilder(deploymentTaskContext).failedWithError().build(); + } + buildInfoLog.info(scanMessage); + } catch (Exception e) { + buildInfoLog.error("Exception occurred while executing task", e); + return TaskResultBuilder.newBuilder(deploymentTaskContext).failedWithError().build(); + } + return TaskResultBuilder.newBuilder(deploymentTaskContext).success().build(); + } + + private ServerConfig resolveXrayServerConfig(@NotNull CommonTaskContext context) { + ServerConfigManager serverConfigManager = ServerConfigManager.getInstance(); + ServerConfig selectedServerConfig = serverConfigManager.getServerConfigById(xrayContext.getArtifactoryServerId()); + if (selectedServerConfig == null) { + throw new IllegalArgumentException("Could not find Artifactory server. Please check the Artifactory server in the task configuration."); + } + Map runtimeContext = context.getRuntimeTaskContext(); + return TaskUtils.getResolutionServerConfig( + xrayContext.getOverriddenUsername(runtimeContext, buildInfoLog, true), + xrayContext.getOverriddenPassword(runtimeContext, buildInfoLog, true), + serverConfigManager, selectedServerConfig, new BuildParamsOverrideManager(customVariableContext)); + } + + @Override + protected ServerConfig getUsageServerConfig() { + return xrayServerConfig; + } + + @Override + protected String getTaskUsageName() { + return "deployment_xray_scan"; + } + + public void setCustomVariableContext(CustomVariableContext customVariableContext) { + this.customVariableContext = customVariableContext; + } +} diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml index 722ebb3c..c642a283 100644 --- a/src/main/resources/atlassian-plugin.xml +++ b/src/main/resources/atlassian-plugin.xml @@ -397,6 +397,50 @@ + + + Publish build-info to Artifactory from a Deployment Project task. + + + + + + + + + + + Scan a build with JFrog Xray from a Deployment Project task. The scanned build must be published to Artifactory prior to scanning. + + + + + + + + + + + Run any JFrog CLI (jf) command from a Deployment Project task. The jf binary must be available on the agent PATH. + + + + + + + + diff --git a/src/main/resources/i18n-jfrog.properties b/src/main/resources/i18n-jfrog.properties index 7af24291..414d9547 100644 --- a/src/main/resources/i18n-jfrog.properties +++ b/src/main/resources/i18n-jfrog.properties @@ -377,6 +377,12 @@ artifactory.task.collectBuildIssues.header.username.description = Override the d artifactory.task.collectBuildIssues.header.password = Override Default Password artifactory.task.collectBuildIssues.header.password.description = The password of the user entered above. +#JFrog CLI Deployment Task +artifactory.task.jfrogCli.deployment.title = JFrog CLI Task +artifactory.task.jfrogCli.deployment.server = JFrog configuration to use +artifactory.task.jfrogCli.deployment.command = JFrog CLI command to run +artifactory.task.jfrogCli.deployment.command.description = Enter your JFrog CLI command after 'jf'. There is no need to provide the URL or credentials — they are injected automatically from the selected server configuration. Example: rt dl --spec=download_spec.json + #errors: error.title=Artifactory error reporting error.heading=An unexpected error has occurred \ No newline at end of file diff --git a/src/main/resources/templates/plugins/deployment/editJfrogCliDeploymentTask.ftl b/src/main/resources/templates/plugins/deployment/editJfrogCliDeploymentTask.ftl new file mode 100644 index 00000000..e8e6348a --- /dev/null +++ b/src/main/resources/templates/plugins/deployment/editJfrogCliDeploymentTask.ftl @@ -0,0 +1,24 @@ +[@ui.bambooSection titleKey='artifactory.task.jfrogCli.deployment.title'] + + [@ww.select + name='artifactory.jfrogCli.serverId' + labelKey='artifactory.task.jfrogCli.deployment.server' + list=serverConfigManager.allServerConfigs + listKey='id' + listValue='url' + emptyOption=true + toggle='true' + /] + +

+ [@ww.textarea + name='artifactory.jfrogCli.command' + labelKey='artifactory.task.jfrogCli.deployment.command' + descriptionKey='artifactory.task.jfrogCli.deployment.command.description' + rows='4' + cols='80' + cssClass='long-field' + /] +
+ +[/@ui.bambooSection]