package io.qameta.allure.bamboo;
import com.atlassian.bamboo.artifact.MutableArtifact;
import com.atlassian.bamboo.artifact.MutableArtifactImpl;
import com.atlassian.bamboo.build.BuildDefinition;
import com.atlassian.bamboo.build.BuildDefinitionManager;
import com.atlassian.bamboo.build.artifact.AgentLocalArtifactHandler;
import com.atlassian.bamboo.build.artifact.ArtifactFileData;
import com.atlassian.bamboo.build.artifact.ArtifactHandler;
import com.atlassian.bamboo.build.artifact.ArtifactHandlerPublishingResult;
import com.atlassian.bamboo.build.artifact.ArtifactHandlerPublishingResultImpl;
import com.atlassian.bamboo.build.artifact.ArtifactHandlingUtils;
import com.atlassian.bamboo.build.artifact.ArtifactLink;
import com.atlassian.bamboo.build.artifact.ArtifactLinkDataProvider;
import com.atlassian.bamboo.build.artifact.ArtifactLinkManager;
import com.atlassian.bamboo.build.artifact.ArtifactPublishingConfig;
import com.atlassian.bamboo.build.artifact.BambooRemoteArtifactHandler;
import com.atlassian.bamboo.build.artifact.FileSystemArtifactLinkDataProvider;
import com.atlassian.bamboo.build.artifact.TrampolineArtifactFileData;
import com.atlassian.bamboo.build.artifact.TrampolineUrlArtifactLinkDataProvider;
import com.atlassian.bamboo.build.artifact.handlers.ArtifactHandlersFunctions;
import com.atlassian.bamboo.build.artifact.handlers.ArtifactHandlersService;
import com.atlassian.bamboo.chains.Chain;
import com.atlassian.bamboo.chains.ChainResultsSummary;
import com.atlassian.bamboo.chains.ChainStageResult;
import com.atlassian.bamboo.plan.PlanResultKey;
import com.atlassian.bamboo.plan.artifact.ArtifactDefinitionContextImpl;
import com.atlassian.bamboo.plugin.BambooPluginUtils;
import com.atlassian.bamboo.plugin.descriptor.predicate.ConjunctionModuleDescriptorPredicate;
import com.atlassian.bamboo.resultsummary.BuildResultsSummary;
import com.atlassian.bamboo.resultsummary.ResultsSummaryManager;
import com.atlassian.bamboo.security.SecureToken;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.predicate.EnabledModulePredicate;
import com.atlassian.plugin.predicate.ModuleOfClassPredicate;
import com.atlassian.sal.api.ApplicationProperties;
import com.atlassian.sal.api.UrlMode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import org.apache.log4j.Logger;
import org.apache.tools.ant.types.FileSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.ws.rs.core.UriBuilder;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.atlassian.bamboo.build.artifact.AbstractArtifactHandler.configProvider;
import static com.atlassian.bamboo.plan.PlanKeys.getPlanKey;
import static com.atlassian.bamboo.plan.PlanKeys.getPlanResultKey;
import static com.google.common.collect.Iterables.size;
import static com.google.common.io.Files.copy;
import static io.qameta.allure.bamboo.AllureBuildResult.allureBuildResult;
import static io.qameta.allure.bamboo.AllureBuildResult.fromCustomData;
import static io.qameta.allure.bamboo.util.ExceptionUtil.stackTraceToString;
import static java.lang.Integer.parseInt;
import static java.util.Optional.ofNullable;
import static javax.ws.rs.core.UriBuilder.fromPath;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.codehaus.plexus.util.FileUtils.copyDirectory;
import static org.codehaus.plexus.util.FileUtils.copyURLToFile;
public class AllureArtifactsManager {
private static final Logger LOGGER = Logger.getLogger(AllureArtifactsManager.class);
private final PluginAccessor pluginAccessor;
private final ArtifactHandlersService artifactHandlersService;
private final BuildDefinitionManager buildDefinitionManager;
private final ResultsSummaryManager resultsSummaryManager;
private final ArtifactLinkManager artifactLinkManager;
private final ApplicationProperties appProperties;
public AllureArtifactsManager(PluginAccessor pluginAccessor, ArtifactHandlersService artifactHandlersService,
BuildDefinitionManager buildDefinitionManager, ResultsSummaryManager resultsSummaryManager,
ArtifactLinkManager artifactLinkManager, ApplicationProperties appProperties) {
this.pluginAccessor = pluginAccessor;
this.artifactHandlersService = artifactHandlersService;
this.buildDefinitionManager = buildDefinitionManager;
this.resultsSummaryManager = resultsSummaryManager;
this.artifactLinkManager = artifactLinkManager;
this.appProperties = appProperties;
}
/**
* Returns the signed url for the requested artifact
*
* @param planKeyString key for plan
* @param buildNumber build number
* @param filePath path of the artifact
* @return empty if cannot get artifact, url if possible
*/
Optional<String> getArtifactUrl(final String planKeyString, final String buildNumber, final String filePath) {
final BuildDefinition buildDefinition = buildDefinitionManager.getBuildDefinition(getPlanKey(planKeyString));
final Map<String, String> artifactConfig = getArtifactHandlersConfig(buildDefinition);
final PlanResultKey planResultKey = getPlanResultKey(planKeyString, parseInt(buildNumber));
return ofNullable(resultsSummaryManager.getResultsSummary(planResultKey)).map(resultsSummary ->
getArtifactHandlerByClassName(fromCustomData(resultsSummary.getCustomBuildData()).getArtifactHandlerClass())
.map(artifactHandler -> {
final ArtifactDefinitionContextImpl artifactDef = getAllureArtifactDef();
return ofNullable(artifactHandler.getArtifactLinkDataProvider(
mutableArtifact(planResultKey, artifactDef.getName()), configProvider(artifactConfig)
)).map(linkProvider -> {
if (linkProvider instanceof TrampolineUrlArtifactLinkDataProvider) {
final TrampolineUrlArtifactLinkDataProvider urlLinkProvider = (TrampolineUrlArtifactLinkDataProvider) linkProvider;
urlLinkProvider.setPlanResultKey(planResultKey);
urlLinkProvider.setArtifactName(artifactDef.getName());
}
return getArtifactFile(filePath, linkProvider);
}).orElse(null);
}).orElse(null));
}
/**
* Downloads all artifacts of a build chain to a temporary directory
*
* @param chainResultsSummary chain results
* @param tempFolder temporary directory
*/
void downloadAllArtifactsTo(@NotNull ChainResultsSummary chainResultsSummary, File tempFolder) {
for (ChainStageResult stageResult : chainResultsSummary.getStageResults()) {
for (BuildResultsSummary resultsSummary : stageResult.getBuildResults()) {
for (ArtifactLink link : resultsSummary.getArtifactLinks()) {
final ArtifactLinkDataProvider dataProvider = artifactLinkManager.getArtifactLinkDataProvider(link.getArtifact());
if (dataProvider instanceof FileSystemArtifactLinkDataProvider) {
downloadAllArtifactsTo((FileSystemArtifactLinkDataProvider) dataProvider, tempFolder);
} else {
downloadAllArtifactsTo(dataProvider, tempFolder, "");
}
}
}
}
}
/**
* Copy all of the build's artifacts for this build across to the builds artifact directory
*
* @param chain chain
* @param summary results summary
* @param reportDir directory of a report
* @return empty if not applicable, result otherwise
*/
Optional<AllureBuildResult> uploadReportArtifacts(@NotNull Chain chain, @NotNull ChainResultsSummary summary, File reportDir) {
try {
final ArtifactDefinitionContextImpl artifact = getAllureArtifactDef();
artifact.setLocation("");
final FileSet sourceFileSet;
sourceFileSet = ArtifactHandlingUtils.createFileSet(reportDir, artifact, true, LOGGER);
sourceFileSet.setDir(reportDir);
sourceFileSet.setIncludes(artifact.getCopyPattern());
final Map<String, String> artifactConfig = getArtifactHandlersConfig(chain.getBuildDefinition());
for (final ArtifactHandler artifactHandler : getArtifactHandlers()) {
if (!artifactHandler.canHandleArtifact(artifact, artifactConfig)) {
continue;
}
if (artifactHandler instanceof BambooRemoteArtifactHandler || artifactHandler instanceof AgentLocalArtifactHandler) {
throw new RuntimeException("Bamboo Remote Artifact Handler is not supported!");
}
final ArtifactPublishingConfig artifactPublishingConfig = new ArtifactPublishingConfig(sourceFileSet, artifactConfig);
final String errorMessage = "Unable to publish artifact via " + artifactHandler;
final ArtifactHandlerPublishingResult publishingResult = BambooPluginUtils.callUnsafeCode(new BambooPluginUtils.NoThrowCallable<ArtifactHandlerPublishingResult>(errorMessage) {
@NotNull
@Override
public ArtifactHandlerPublishingResult call() {
try {
return artifactHandler.publish(summary.getPlanResultKey(), artifact, artifactPublishingConfig);
} catch (final Exception e) {
LOGGER.error("Failed to publish Allure Report using handler " + artifactHandler.getClass().getName(), e);
return ArtifactHandlerPublishingResultImpl.failure();
}
}
});
if (publishingResult == null) {
continue;
}
publishingResult.setArtifactHandlerKey(artifactHandler.getModuleDescriptor().getCompleteKey());
return Optional.of(allureBuildResult(publishingResult.isSuccessful(), null)
.withHandlerClass(artifactHandler.getClass().getName()));
}
} catch (Exception e) {
final String message = "Failed to publish Allure Report from directory " + reportDir;
LOGGER.error(message, e);
return Optional.of(allureBuildResult(false, message + "\n" + stackTraceToString(e)));
}
return Optional.empty();
}
private void downloadAllArtifactsTo(ArtifactLinkDataProvider dataProvider, File tempDir, String startFrom) {
for (ArtifactFileData data : dataProvider.listObjects(startFrom)) {
try {
if (data instanceof TrampolineArtifactFileData) {
final TrampolineArtifactFileData trampolineData = (TrampolineArtifactFileData) data;
data = trampolineData.getDelegate();
if (data.getFileType().equals(ArtifactFileData.FileType.REGULAR_FILE)) {
final String fileName = Paths.get(data.getName()).toFile().getName();
copyURLToFile(new URL(data.getUrl()), Paths.get(tempDir.getPath(), fileName).toFile());
} else {
downloadAllArtifactsTo(dataProvider, tempDir, trampolineData.getTag());
}
}
} catch (IOException e) {
logAndThrow(e, "Failed to download artifacts to " + tempDir);
}
}
}
private void logAndThrow(Exception e, String message) {
LOGGER.error(message, e);
throw new RuntimeException(message, e);
}
private void downloadAllArtifactsTo(FileSystemArtifactLinkDataProvider dataProvider, File tempDir) {
ofNullable(dataProvider.getFile().listFiles()).map(Arrays::asList).ifPresent(list -> list.forEach(file -> {
try {
if (file.isFile()) {
copy(file, Paths.get(tempDir.getPath(), file.getName()).toFile());
} else if (!file.getName().equals(".") && !file.getName().equals("..")) {
copyDirectory(dataProvider.getFile(), tempDir);
}
} catch (IOException e) {
logAndThrow(e, "Failed to download artifacts to " + tempDir);
}
}
));
}
@Nullable
private String getArtifactFile(String filePath, ArtifactLinkDataProvider linkProvider) {
if (linkProvider instanceof FileSystemArtifactLinkDataProvider) {
return linkProvider.getRootUrl()
.replaceFirst("BASE_URL", getBaseUrl().build().toString())
.replace("index.html", isEmpty(filePath) ? "index.html" : filePath);
} else {
final Iterable<ArtifactFileData> datas = linkProvider.listObjects(filePath);
if (size(datas) == 1) {
ArtifactFileData data = datas.iterator().next();
if (data instanceof TrampolineArtifactFileData) {
final TrampolineArtifactFileData trampolineData = (TrampolineArtifactFileData) data;
data = trampolineData.getDelegate();
if (data.getFileType().equals(ArtifactFileData.FileType.REGULAR_FILE)) {
return data.getUrl();
}
} else {
return getBambooArtifactUrl(data);
}
} else if (size(datas) > 1) {
return getArtifactFile("index.html", linkProvider);
}
}
return null;
}
private String getBambooArtifactUrl(ArtifactFileData data) {
return ofNullable(data.getUrl()).map(url -> (url.startsWith("/")) ?
getBaseUrl().path(url).build().toString() : url)
.map(url -> url)
.orElse(null);
}
private UriBuilder getBaseUrl() {
return fromPath(appProperties.getBaseUrl(UrlMode.ABSOLUTE));
}
@NotNull
private ArtifactDefinitionContextImpl getAllureArtifactDef() {
final ArtifactDefinitionContextImpl artifact = new ArtifactDefinitionContextImpl("allure-report", false, SecureToken.create());
artifact.setCopyPattern("**/**");
return artifact;
}
@NotNull
private Map<String, String> getArtifactHandlersConfig(BuildDefinition buildDefinition) {
final Map<String, String> config = artifactHandlersService.getRuntimeConfiguration();
final Map<String, String> planCustomConfiguration = buildDefinition.getCustomConfiguration();
if (ArtifactHandlingUtils.isCustomArtifactHandlingConfigured(planCustomConfiguration)) {
Iterables.removeIf(config.keySet(), ArtifactHandlersFunctions.isArtifactHandlerOnOffSwitch());
final Map<String, String> planLevelConfiguration = Maps.filterKeys(planCustomConfiguration, ArtifactHandlersFunctions.isArtifactHandlerConfiguration());
config.putAll(planLevelConfiguration);
}
return config;
}
@NotNull
private MutableArtifact mutableArtifact(PlanResultKey planResultKey, String name) {
return new MutableArtifactImpl(name, planResultKey, null, false, 0L);
}
private List<ArtifactHandler> getArtifactHandlers() {
final ConjunctionModuleDescriptorPredicate<ArtifactHandler> predicate = new ConjunctionModuleDescriptorPredicate<>();
predicate.append(new ModuleOfClassPredicate<>(ArtifactHandler.class));
predicate.append(new EnabledModulePredicate<>());
return ImmutableList.copyOf(pluginAccessor.getModules(predicate));
}
@SuppressWarnings("unchecked")
private <T extends ArtifactHandler> Optional<T> getArtifactHandlerByClassName(String className) {
final ConjunctionModuleDescriptorPredicate<T> predicate = new ConjunctionModuleDescriptorPredicate<>();
return ofNullable(className).map(clazz -> {
final Class<T> aClass;
try {
aClass = (Class<T>) Class.forName(clazz);
predicate.append(new ModuleOfClassPredicate<>(aClass));
predicate.append(new EnabledModulePredicate<>());
return pluginAccessor.getModules(predicate).stream().findAny().orElse(null);
} catch (ClassNotFoundException e) {
LOGGER.error("Failed to find artifact handler for class name " + className, e);
}
return null;
});
}
}