package org.jfrog.bamboo.configuration;
import com.atlassian.bamboo.build.Job;
import com.atlassian.bamboo.collections.ActionParametersMap;
import com.atlassian.bamboo.configuration.AdministrationConfiguration;
import com.atlassian.bamboo.repository.NameValuePair;
import com.atlassian.bamboo.task.*;
import com.atlassian.bamboo.utils.error.ErrorCollection;
import com.atlassian.bamboo.v2.build.agent.capability.Requirement;
import com.atlassian.bamboo.ww2.actions.build.admin.create.UIConfigSupport;
import com.atlassian.sal.api.message.I18nResolver;
import com.atlassian.spring.container.ContainerManager;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import org.apache.commons.configuration.ConversionException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jfrog.bamboo.admin.ServerConfig;
import org.jfrog.bamboo.admin.ServerConfigManager;
import org.jfrog.bamboo.context.AbstractBuildContext;
import org.jfrog.bamboo.release.vcs.git.GitAuthenticationType;
import org.jfrog.bamboo.release.vcs.VcsTypes;
import org.jfrog.bamboo.security.EncryptionHelper;
import org.jfrog.bamboo.util.TaskUtils;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;
/**
* Base class for all {@link com.atlassian.bamboo.task.TaskConfigurator}s that are used by the plugin. It sets the
* {@link ServerConfigManager} to be used for populating the Artifactory relevant fields. It also serves as a common
* ground for setting common fields in the context of the build.
*
* @author Tomer Cohen
*/
public abstract class AbstractArtifactoryConfiguration extends AbstractTaskConfigurator implements
TaskTestResultsSupport, BuildTaskRequirementSupport {
protected I18nResolver i18nResolver;
public static final String CFG_TEST_RESULTS_FILE_PATTERN_OPTION_CUSTOM = "customTestDirectory";
public static final String CFG_TEST_RESULTS_FILE_PATTERN_OPTION_STANDARD = "standardTestDirectory";
private static final Map TEST_RESULTS_FILE_PATTERN_TYPES = ImmutableMap
.of(CFG_TEST_RESULTS_FILE_PATTERN_OPTION_STANDARD, "Look in the standard test results directory.",
CFG_TEST_RESULTS_FILE_PATTERN_OPTION_CUSTOM, "Specify custom results directories");
public static final Map<String, String> SIGN_METHOD_MAP = ImmutableMap.of(
"false", "Don't Sign", "true", "Sign");
public static final String SIGN_METHOD_MAP_KEY = "signMethods";
protected transient ServerConfigManager serverConfigManager;
protected AdministrationConfiguration administrationConfiguration;
protected UIConfigSupport uiConfigSupport;
private String builderContextPrefix;
private String capabilityPrefix;
private static final Logger log = Logger.getLogger(AbstractArtifactoryConfiguration.class);
protected AbstractArtifactoryConfiguration() {
this(null, null);
}
protected AbstractArtifactoryConfiguration(String builderContextPrefix) {
this(builderContextPrefix, null);
}
protected AbstractArtifactoryConfiguration(String builderContextPrefix, @Nullable String capabilityPrefix) {
serverConfigManager = ServerConfigManager.getInstance();
if (administrationConfiguration == null) {
administrationConfiguration =
(AdministrationConfiguration) ContainerManager.getComponent("administrationConfiguration");
}
this.builderContextPrefix = builderContextPrefix;
this.capabilityPrefix = capabilityPrefix;
}
public String getTestDirectory(AbstractBuildContext buildContext) {
String directoryOption = buildContext.getTestDirectoryOption();
if (CFG_TEST_RESULTS_FILE_PATTERN_OPTION_STANDARD.equals(directoryOption)) {
return getDefaultTestDirectory();
} else if (CFG_TEST_RESULTS_FILE_PATTERN_OPTION_CUSTOM.equals(directoryOption)) {
return buildContext.getTestDirectory();
}
return null;
}
@Override
public void populateContextForEdit(@NotNull Map<String, Object> context, @NotNull TaskDefinition taskDefinition) {
super.populateContextForEdit(context, taskDefinition);
serverConfigManager = ServerConfigManager.getInstance();
populateContextForAllOperations(context);
}
@Override
public void populateContextForCreate(@NotNull Map<String, Object> context) {
super.populateContextForCreate(context);
serverConfigManager = ServerConfigManager.getInstance();
populateContextForAllOperations(context);
}
@NotNull
@Override
public Map<String, String> generateTaskConfigMap(@NotNull ActionParametersMap params,
@Nullable TaskDefinition previousTaskDefinition) {
Map<String, String> taskConfigMap = super.generateTaskConfigMap(params, previousTaskDefinition);
taskConfigMap.put("baseUrl", administrationConfiguration.getBaseUrl());
return taskConfigMap;
}
@Override
public void validate(@NotNull ActionParametersMap params, @NotNull ErrorCollection errorCollection) {
String serverKey = "builder." + getKey() + "." + AbstractBuildContext.SERVER_ID_PARAM;
if (!params.containsKey(serverKey)) {
return;
}
long configuredServerId;
try {
configuredServerId = params.getLong(serverKey, -1);
} catch (ConversionException ce) {
configuredServerId = -1;
}
if (configuredServerId == -1) {
return;
}
ServerConfig serverConfig = serverConfigManager.getServerConfigById(configuredServerId);
if (serverConfig == null) {
errorCollection.addError(serverKey,
"Could not find Artifactory server configuration by the ID " + configuredServerId);
}
if (StringUtils.isNotBlank(getDeployableRepoKey())) {
String deployerRepoKey = "builder." + getKey() + "." + getDeployableRepoKey();
if (StringUtils.isBlank(params.getString(deployerRepoKey))) {
errorCollection.addError(deployerRepoKey, "Please choose a repository to deploy to.");
}
}
String runLicensesKey = "builder." + getKey() + "." + AbstractBuildContext.RUN_LICENSE_CHECKS;
String runLicenseChecksValue = params.getString(runLicensesKey);
if (StringUtils.isNotBlank(runLicenseChecksValue) && Boolean.valueOf(runLicenseChecksValue)) {
String violationsKey = "builder." + getKey() + "." + AbstractBuildContext.LICENSE_VIOLATION_RECIPIENTS;
String recipients = params.getString(violationsKey);
if (StringUtils.isNotBlank(recipients)) {
String[] recipientTokens = StringUtils.split(recipients, ' ');
for (String recipientToken : recipientTokens) {
if (StringUtils.isNotBlank(recipientToken) &&
(!recipientToken.contains("@")) || recipientToken.startsWith("@") ||
recipientToken.endsWith("@")) {
errorCollection
.addError(violationsKey, "'" + recipientToken + "' is not a valid e-mail address.");
break;
}
}
}
}
}
@NotNull
@Override
public Set<Requirement> calculateRequirements(@NotNull TaskDefinition taskDefinition, @NotNull Job job) {
Set<Requirement> requirements = Sets.newHashSet();
if (StringUtils.isNotBlank(builderContextPrefix)) {
taskConfiguratorHelper.addJdkRequirement(requirements, taskDefinition,
builderContextPrefix + TaskConfigConstants.CFG_JDK_LABEL);
if (StringUtils.isNotBlank(capabilityPrefix)) {
taskConfiguratorHelper.addSystemRequirementFromConfiguration(requirements, taskDefinition,
builderContextPrefix + AbstractBuildContext.EXECUTABLE, capabilityPrefix);
}
}
return requirements;
}
protected void populateContextWithConfiguration(@NotNull Map<String, Object> context,
@NotNull TaskDefinition taskDefinition, Set<String> fieldsToCopy) {
// Encrypt the password fields, so that they do not appear as free-text on the task configuration UI.
encryptFields(taskDefinition.getConfiguration());
taskConfiguratorHelper.populateContextWithConfiguration(context, taskDefinition, fieldsToCopy);
// Decrypt back the password fields.
decryptFields(taskDefinition.getConfiguration());
}
// populate common objects into context
private void populateContextForAllOperations(@NotNull Map<String, Object> context) {
context.put("uiConfigBean", uiConfigSupport);
context.put("testDirectoryTypes", TEST_RESULTS_FILE_PATTERN_TYPES);
context.put(AbstractBuildContext.PUBLISH_BUILD_INFO_PARAM, "true");
context.put(AbstractBuildContext.ENV_VARS_EXCLUDE_PATTERNS, "*password*,*secret*");
context.put(SIGN_METHOD_MAP_KEY, SIGN_METHOD_MAP);
}
/**
* Sets the UI config bean from bamboo. NOTE: This method is called from Bamboo upon instantiation of this class by
* reflection.
*
* @param uiConfigSupport The UI config bean for select values.
*/
public void setUiConfigSupport(UIConfigSupport uiConfigSupport) {
this.uiConfigSupport = uiConfigSupport;
}
public void setAdministrationConfiguration(AdministrationConfiguration administrationConfiguration) {
this.administrationConfiguration = administrationConfiguration;
}
protected String readFileByKey(final ActionParametersMap params, String keyToRead) {
final File private_key_file = params.getFiles().get(keyToRead);
if (private_key_file != null) {
try {
return FileUtils.readFileToString(private_key_file);
} catch (IOException e) {
log.error("Cannot read uploaded file", e);
}
} else {
log.error("Unable to load file from config submission!");
}
return null;
}
private boolean isEncrypted(String value) {
try {
value = URLDecoder.decode(value, "UTF-8");
} catch (Exception ignore) {
// Ignore. Trying to decode password that was not encoded.
}
String decryptedValue = TaskUtils.decryptIfNeeded(value);
return !decryptedValue.equals(value);
}
/**
* This method is used by the encryptFields and decryptFields methods.
* It encrypts or decrypts the task config fields, if their key ends with 'password'.
* While encrypting / decrypting, if the keys are already encrypted / decrypted,
* the keys values will not change.
*
* @param taskConfigMap The task config fields map.
* @param enc If true - encrypt, else - decrypt.
*/
private void encOrDecFields(Map<String, String> taskConfigMap, boolean enc) {
for (Map.Entry<String, String> entry : taskConfigMap.entrySet()) {
String key = entry.getKey().toLowerCase();
if (shouldEncrypt(key)) {
String value = entry.getValue();
if (isEncrypted(value)) {
try {
value = URLDecoder.decode(value, "UTF-8");
} catch (Exception ignore) {
/* Ignore. Trying to decode password that was not encoded. */
}
value = TaskUtils.decryptIfNeeded(value);
}
if (enc) {
value = EncryptionHelper.encrypt(value);
try {
value = URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
entry.setValue(value);
}
}
}
private boolean shouldEncrypt(String key) {
return key.contains("artifactory") && (key.contains("password") || key.contains("ssh"));
}
/**
* Encrypt the task config fields, if their key ends with 'password'.
* If the keys are already encrypted, their value will not change.
*
* @param taskConfigMap The task config fields map.
*/
private void encryptFields(Map<String, String> taskConfigMap) {
encOrDecFields(taskConfigMap, true);
}
/**
* Decrypt the task config fields, if their key ends with 'password'.
* If the keys are already decrypted, their value will not change.
*
* @param taskConfigMap The task config fields map.
*/
protected void decryptFields(Map<String, String> taskConfigMap) {
encOrDecFields(taskConfigMap, false);
}
/**
* Reset the build context configuration back to the default values if no server id was selected
*
* @param buildContext The build context which holds the environment for the configuration.
*/
protected void resetConfigIfNeeded(AbstractBuildContext buildContext) {
long serverId = buildContext.getArtifactoryServerId();
if (serverId == -1) {
buildContext.resetContextToDefault();
}
}
/**
* @return The unique key identifier of the task configuration.
*/
protected abstract String getKey();
/**
* @return The key for the deployable/publishing repo key for the environment.
*/
protected abstract String getDeployableRepoKey();
protected String getDefaultTestDirectory() {
throw new UnsupportedOperationException("This method is not implemented for class " + this.getClass());
}
/**
* In version 1.8.1 the key containing the Artifactory Server ID was changed
* in the Generic Resolve and Deploy configurations.
* This method migrates to the new name.
*/
protected void migrateServerKeyIfNeeded(Map<String, String> configuration) {
String oldServerId = configuration.get("artifactory.generic.artifactoryServerId");
String newServerId = configuration.get("builder.artifactoryGenericBuilder.artifactoryServerId");
if (StringUtils.isNotBlank(oldServerId)) {
configuration.put("builder.artifactoryGenericBuilder.artifactoryServerId", oldServerId);
}
if (StringUtils.isNotBlank(newServerId)) {
configuration.put("artifactory.generic.artifactoryServerId", newServerId);
}
}
protected List<NameValuePair> getGitAuthenticationTypes() {
return Arrays.stream(GitAuthenticationType.values())
.map(Enum::name)
.map(name -> new NameValuePair(name, getAuthTypeName(name)))
.collect(Collectors.toList());
}
protected List<NameValuePair> getVcsTypes() {
return Arrays.stream(VcsTypes.values())
.map(Enum::name)
.map(name -> new NameValuePair(name, getVcsTypeName(name)))
.collect(Collectors.toList());
}
protected Map<String, String> getSshFileContent(ActionParametersMap params, TaskDefinition previousTaskDefinition) {
Map<String, String> taskConfigMap = new HashMap<>();
String sshFileKey = AbstractBuildContext.VCS_PREFIX + AbstractBuildContext.GIT_SSH_KEY;
String sshFileContent = readFileByKey(params, sshFileKey);
if (StringUtils.isNotBlank(sshFileContent)) {
taskConfigMap.put(sshFileKey, sshFileContent);
} else {
if (previousTaskDefinition != null) {
taskConfigMap.put(sshFileKey, previousTaskDefinition.getConfiguration().get(sshFileKey));
}
}
return taskConfigMap;
}
private String getVcsTypeName(final String vcsType) {
return i18nResolver.getText("artifactory.vcs.type." + StringUtils.lowerCase(vcsType));
}
private String getAuthTypeName(final String authType) {
return i18nResolver.getText("artifactory.vcs.git.authenticationType." + StringUtils.lowerCase(authType));
}
public void setI18nResolver(final I18nResolver i18nResolver) {
this.i18nResolver = i18nResolver;
}
}