/*
* Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.amazonaws.codepipeline.jenkinsplugin;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.codepipeline.jenkinsplugin.CodePipelineStateModel.CategoryType;
import com.amazonaws.codepipeline.jenkinsplugin.CodePipelineStateModel.CompressionType;
/**
* The AWS CodePipeline Publisher compresses the artifacts and uploads them to S3.
* It calls putJobSuccessResult or putJobFailureResult depending on the build result.
* It only works together with the CodePipeline SCM plugin to get access to the Job Data, Credentials and Proxy.
*/
public class AWSCodePipelinePublisher extends Notifier {
@Deprecated // renamed to outputArtifacts
private final transient List<OutputTuple> buildOutputs;
private List<OutputArtifact> outputArtifacts;
private AWSClientFactory awsClientFactory;
@DataBoundConstructor
public AWSCodePipelinePublisher(final JSONArray outputLocations) {
buildOutputs = new ArrayList<>();
outputArtifacts = new ArrayList<>();
if (outputLocations != null) {
for (final Object outputLocation : outputLocations) {
// See AWSCodePipelinePublisher/config.jelly
final JSONObject jsonObject = (JSONObject) outputLocation;
if (jsonObject.has("location")) {
final String locationValue = jsonObject.getString("location");
this.outputArtifacts.add(new OutputArtifact(Validation.sanitize(locationValue.trim())));
}
}
}
awsClientFactory = new AWSClientFactory();
Validation.numberOfOutPutsIsValid(outputArtifacts);
}
public AWSCodePipelinePublisher(final JSONArray outputLocations, final AWSClientFactory awsClientFactory) {
this(outputLocations);
this.awsClientFactory = awsClientFactory;
}
@Override
public boolean perform(
final AbstractBuild<?,?> action,
final Launcher launcher,
final BuildListener listener) {
final CodePipelineStateModel model = CodePipelineStateService.getModel();
final boolean actionSucceeded = action.getResult() == Result.SUCCESS;
boolean awsStatus = actionSucceeded;
String error = "Failed";
if (model == null) {
LoggingHelper.log(listener, "Error with Model Thread Handling");
return false;
}
if (model.isSkipPutJobResult()) {
LoggingHelper.log(
listener,
String.format("Skipping PutJobFailureResult call for the job with ID %s", model.getJob().getId()));
return false;
}
final AWSClients awsClients = awsClientFactory.getAwsClient(
model.getAwsAccessKey(),
model.getAwsSecretKey(),
model.getProxyHost(),
model.getProxyPort(),
model.getRegion(),
JenkinsMetadata.getPluginVersion());
if (!actionSucceeded) {
if (model.getActionTypeCategory() == CategoryType.Build) {
error = "Build failed";
} else if (model.getActionTypeCategory() == CategoryType.Test) {
error = "Tests failed";
}
}
// This is here if the customer pressed BuildNow, we have nowhere to push
// or update. But we want to see if we can build what we have.
if (model.getJob() == null) {
LoggingHelper.log(listener, "No Job, returning early");
return actionSucceeded;
}
try {
LoggingHelper.log(listener, "Publishing artifacts");
if (model.getJob().getData().getOutputArtifacts().size() != outputArtifacts.size()) {
throw new IllegalArgumentException(String.format(
"The number of output artifacts in the Jenkins project and in the AWS "
+ "CodePipeline pipeline action do not match. Please configure the output locations "
+ "of your Jenkins project to match the AWS CodePipeline pipeline action's output artifacts. "
+ "Number of output locations in Jenkins project: %d, number of output artifacts "
+ "in AWS CodePipeline pipeline action: %d [Pipeline: %s, stage: %s, action: %s].",
outputArtifacts.size(),
model.getJob().getData().getOutputArtifacts().size(),
model.getJob().getData().getPipelineContext().getPipelineName(),
model.getJob().getData().getPipelineContext().getStage().getName(),
model.getJob().getData().getPipelineContext().getAction().getName()));
}
if (!outputArtifacts.isEmpty() && actionSucceeded) {
callPublish(action, model, listener);
}
} catch (final AmazonServiceException ex) {
error = "Failed to upload output artifact(s): " + ex.getErrorMessage();
LoggingHelper.log(listener, ex.getMessage());
LoggingHelper.log(listener, ex);
awsStatus = false;
} catch (final RuntimeException | InterruptedException | IOException ex) {
error = "Failed to upload output artifact(s): " + ex.getMessage();
LoggingHelper.log(listener, ex.getMessage());
LoggingHelper.log(listener, ex);
awsStatus = false;
} finally {
PublisherTools.putJobResult(
awsStatus,
error,
action.getId(),
model.getJob().getId(),
awsClients.getCodePipelineClient(),
listener);
cleanUp(model);
}
return awsStatus;
}
public void cleanUp(final CodePipelineStateModel model) {
model.clearJob();
model.setCompressionType(CompressionType.None);
CodePipelineStateService.removeModel();
}
public void callPublish(
final AbstractBuild<?,?> action,
final CodePipelineStateModel model,
final BuildListener listener)
throws IOException, InterruptedException {
action.getWorkspace().act(new PublisherCallable(
action.getProject().getName(),
model,
outputArtifacts,
awsClientFactory,
JenkinsMetadata.getPluginVersion(),
listener));
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.STEP;
}
public OutputArtifact[] getOutputArtifacts() {
if (outputArtifacts.isEmpty()) {
return null;
} else {
return outputArtifacts.toArray(new OutputArtifact[outputArtifacts.size()]);
}
}
/**
* Descriptor for {@link AWSCodePipelinePublisher}. Used as a singleton.
* The class is marked as public so that it can be accessed from views.
* See src/main/resources/com/amazonaws/codepipeline/AWSCodePipelinePublisher/*.jelly
* for the actual HTML fragment for the configuration screen.
*/
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public DescriptorImpl() {
load();
}
public boolean isApplicable(final Class<? extends AbstractProject> aClass) {
// Indicates that this builder can be used with all kinds of project types
return true;
}
/**
* This human readable name is used in the configuration screen.
*/
public String getDisplayName() {
return "AWS CodePipeline Publisher";
}
@Override
public boolean configure(
final StaplerRequest req,
final JSONObject formData)
throws FormException {
req.bindJSON(this, formData);
save();
return super.configure(req, formData);
}
}
// Retain backwards compatibility: migrate from OutputTuple to OutputArtifact
// https://wiki.jenkins-ci.org/display/JENKINS/Hint+on+retaining+backward+compatibility
protected Object readResolve() {
if (buildOutputs != null) {
if (outputArtifacts == null) {
outputArtifacts = new ArrayList<>();
}
for (final OutputTuple tuple : buildOutputs) {
outputArtifacts.add(new OutputArtifact(tuple.getOutput()));
}
}
return this;
}
@Deprecated // plugin now uses OutputArtifact
public static final class OutputTuple implements Serializable {
private static final long serialVersionUID = 1L;
private final String outputString;
public OutputTuple(final String s) {
outputString = s;
}
public String getOutput() {
return outputString;
}
}
}