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

implemented invalidation of uploaded to S3 file among related CDN distributions #74

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 36 additions & 18 deletions src/main/java/hudson/plugins/s3/S3Profile.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package hudson.plugins.s3;

import com.amazonaws.ClientConfiguration;
import hudson.FilePath;
import hudson.ProxyConfiguration;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.Run;
import hudson.plugins.s3.callable.S3DownloadCallable;
import hudson.plugins.s3.callable.S3UploadCallable;
import hudson.plugins.s3.cloudfront.InvalidationRecord;
import hudson.plugins.s3.cloudfront.callable.CloudFrontInvalidationCallable;
import hudson.util.Secret;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URL;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
Expand All @@ -17,6 +26,7 @@
import org.apache.tools.ant.types.selectors.FilenameSelector;
import org.kohsuke.stapler.DataBoundConstructor;

import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
Expand All @@ -28,14 +38,6 @@
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.google.common.collect.Lists;

import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.Run;
import hudson.ProxyConfiguration;
import hudson.plugins.s3.callable.S3DownloadCallable;
import hudson.plugins.s3.callable.S3UploadCallable;
import hudson.util.Secret;

public class S3Profile {
private String name;
private String accessKey;
Expand Down Expand Up @@ -205,19 +207,35 @@ public FingerprintRecord upload(AbstractBuild<?,?> build, final BuildListener li
Thread.sleep(retryWaitTime * 1000);
}
}
}

public List<String> list(Run build, String bucket, String expandedFilter) {
AmazonS3Client s3client = getClient();

}

public InvalidationRecord invalidate(AbstractBuild<?, ?> build, BuildListener listener, String origin, String...paths) throws IOException, InterruptedException {
int retryCount = 0;
while (true) {
try {
CloudFrontInvalidationCallable callable = new CloudFrontInvalidationCallable(accessKey, secretKey, useRole);
return callable.invoke(origin, paths);
} catch (Exception e) {
retryCount++;
if (retryCount >= maxUploadRetries) {
throw new IOException("invalidate paths " + Arrays.toString(paths) + ": " + e
+ ":: Failed after " + retryCount + " tries.", e);
}
Thread.sleep(retryWaitTime * 1000);
}
}
}

public List<String> list(Run build, String bucket, String expandedFilter) {
String buildName = build.getDisplayName();
int buildID = build.getNumber();
Destination dest = new Destination(bucket, "jobs/" + buildName + "/" + buildID + "/" + name);
return list(dest);
}

ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
.withBucketName(dest.bucketName)
.withPrefix(dest.objectName);

private List<String> list(Destination dest) {
AmazonS3Client s3client = getClient();
ListObjectsRequest listObjectsRequest = new ListObjectsRequest().withBucketName(dest.bucketName).withPrefix(dest.objectName);
List<String> files = Lists.newArrayList();

ObjectListing objectListing;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package hudson.plugins.s3.cloudfront;

import hudson.Extension;
import hudson.Launcher;
import hudson.Util;
import hudson.model.BuildListener;
import hudson.model.Describable;
import hudson.model.Result;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.plugins.s3.S3BucketPublisher;
import hudson.plugins.s3.S3Profile;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.util.ListBoxModel;

import java.io.IOException;
import java.io.PrintStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import jenkins.model.Jenkins;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;

public final class CloudFrontInvalidatePublisher extends Recorder implements Describable<Publisher> {

@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

private String profileName;
private final List<InvalidationEntry> invalidationEntries;

public CloudFrontInvalidatePublisher() {
super();
this.invalidationEntries = Collections.emptyList();
}

@DataBoundConstructor
public CloudFrontInvalidatePublisher(String profileName,
List<InvalidationEntry> invalidationEntries) {
if (StringUtils.isBlank(profileName)) {
// defaults to the first one
S3Profile[] sites = DESCRIPTOR.getProfiles();
if (sites.length > 0)
profileName = sites[0].getName();
}

this.profileName = profileName;
this.invalidationEntries = invalidationEntries;

}

public String getProfileName() {
return profileName;
}

public List<InvalidationEntry> getInvalidationEntries() {
return invalidationEntries;
}

public S3Profile getProfile(String profileName) {
S3Profile[] profiles = DESCRIPTOR.getProfiles();

if (profiles.length == 0) {
return null;
}
for (S3Profile profile : profiles) {
if (profile.getName().equals(profileName)) {
return profile;
}
}
return profiles[0];
}

protected void log(final PrintStream logger, final String message) {
logger.println(StringUtils.defaultString(getDescriptor().getDisplayName()) + " " + message);
}

@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
throws InterruptedException, IOException {

final boolean buildFailed = build.getResult() == Result.FAILURE;

S3Profile profile = getProfile(profileName);
if (profile == null) {
log(listener.getLogger(), "No S3 profile is configured.");
build.setResult(Result.UNSTABLE);
return true;
}
log(listener.getLogger(), "Using S3 profile: " + profile.getName());
try {
Map<String, String> envVars = build.getEnvironment(listener);
for (InvalidationEntry entry : invalidationEntries) {

if (entry.noInvalidateOnFailure && buildFailed) {
// build failed. don't post
log(listener.getLogger(), "Skipping S3 key invalidation because build failed");
continue;
}

String origin = Util.replaceMacro(entry.origin, envVars);
if (StringUtils.isBlank(origin)) {
log(listener.getLogger(), "Origin was not provided.");
continue;
}

String keyPath = Util.replaceMacro(entry.invalidationPath, envVars);
if (StringUtils.isBlank(keyPath)) {
log(listener.getLogger(), "No S3 asset key was provided.");
continue;
}

InvalidationRecord invalidationRecord = profile.invalidate(build, listener, origin, keyPath.split(","));
log(listener.getLogger(), "With keyPath = " + keyPath
+ ";\n\t" + invalidationRecord);
}
} catch (IOException e) {
e.printStackTrace(listener.error("Failed to upload files"));
build.setResult(Result.UNSTABLE);
}
return true;
}

@Override
public BuildStepDescriptor<Publisher> getDescriptor() {
return DESCRIPTOR;
}

@Override
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.STEP;
}

public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {

public DescriptorImpl(Class<? extends Publisher> clazz) {
super(clazz);
load();
}

public DescriptorImpl() {
this(CloudFrontInvalidatePublisher.class);
}

@Override
public String getDisplayName() {
return "Invalidate S3 assets among CloudFront distributions";
}

@Override
public String getHelpFile() {
return "/plugin/s3/help-invalidate.html";
}

public ListBoxModel doFillProfileNameItems() {
ListBoxModel model = new ListBoxModel();
for (S3Profile profile : getProfiles()) {
model.add(profile.getName(), profile.getName());
}
return model;
}

public S3Profile[] getProfiles() {
S3BucketPublisher.DescriptorImpl s3PublisherDescriptor = (S3BucketPublisher.DescriptorImpl) Jenkins
.getInstance().getDescriptor(S3BucketPublisher.class);
return s3PublisherDescriptor.getProfiles();
}

@Override
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
}
}
65 changes: 65 additions & 0 deletions src/main/java/hudson/plugins/s3/cloudfront/InvalidationEntry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package hudson.plugins.s3.cloudfront;

import hudson.Extension;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.util.FormValidation;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

public final class InvalidationEntry implements Describable<InvalidationEntry> {

/**
* Origin of the distribution.
* Can contain macros.
*/
public String origin;
/**
* Invalidation Path.
* Can contain macros and wildcards.
*/
public String invalidationPath;

/**
* Do not invalidate the artifacts when build fails
*/
public boolean noInvalidateOnFailure;

@DataBoundConstructor
public InvalidationEntry(String origin, String invalidationPath, boolean noInvalidateOnFailure) {
this.origin = origin;
this.invalidationPath = invalidationPath;
this.noInvalidateOnFailure = noInvalidateOnFailure;
}

public Descriptor<InvalidationEntry> getDescriptor() {
return DESCRIPOR;
}

@Extension
public final static DescriptorImpl DESCRIPOR = new DescriptorImpl();

public static class DescriptorImpl extends Descriptor<InvalidationEntry> {

@Override
public String getDisplayName() {
return "Files to invalidate";
}

public FormValidation doCheckOrigin(@QueryParameter String origin) {
return checkNotBlank(origin, "Origin name must be speсified");
}

public FormValidation doCheckInvalidationPathx(@QueryParameter String invalidationPath) {
return checkNotBlank(invalidationPath, "Invalidation path must be speсified");
}

private FormValidation checkNotBlank(String value, String errorMessage) {
return StringUtils.isNotBlank(value) ? FormValidation.ok()
: FormValidation.error(errorMessage);
}
};

}
39 changes: 39 additions & 0 deletions src/main/java/hudson/plugins/s3/cloudfront/InvalidationRecord.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package hudson.plugins.s3.cloudfront;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.amazonaws.services.cloudfront.model.DistributionSummary;

public class InvalidationRecord {

List<InvalidationRecordEntry> entries = new ArrayList<InvalidationRecordEntry>();

public void add(DistributionSummary distribution, String...invalidationPath) {
entries.add(new InvalidationRecordEntry(distribution, invalidationPath));
}

@Override
public String toString() {
return "InvalidationDetails [" + entries + "]";
}

private class InvalidationRecordEntry {

private DistributionSummary distribution;
private String[] path;

public InvalidationRecordEntry(DistributionSummary distribution, String...invalidationPath) {
this.distribution = distribution;
this.path = invalidationPath;
}

@Override
public String toString() {
return "[distribution=" + distribution.getAliases().getItems() + ", paths=" + Arrays.toString(path) + "]";
}


}
}
Loading