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 10 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
58 changes: 41 additions & 17 deletions src/main/java/hudson/plugins/s3/S3Profile.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
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;
Expand All @@ -17,6 +25,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 +37,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 +206,42 @@ 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 bucket, String keyPrefix) throws IOException, InterruptedException {
List<String> filesToInvalidate = list(new Destination(bucket, keyPrefix));
return invalidate(build, listener, bucket, filesToInvalidate);
}



private InvalidationRecord invalidate(AbstractBuild<?, ?> build, final BuildListener listener, String bucket, List<String> paths) throws IOException, InterruptedException {
int retryCount = 0;
while (true) {
try {
CloudFrontInvalidationCallable callable = new CloudFrontInvalidationCallable(accessKey, secretKey, useRole);
return callable.invoke(bucket, paths);
} catch (Exception e) {
retryCount++;
if (retryCount >= maxUploadRetries) {
throw new IOException("invalidate paths " + paths.toString() + ": " + 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,188 @@
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.CopyOnWriteList;
import hudson.util.ListBoxModel;

import java.io.IOException;
import java.io.PrintStream;
import java.util.Arrays;
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() {
return getProfile(profileName);
}

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

if (profileName == null && profiles.length > 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can remove this lines and do something like this (in cycle):

if (profileName == null || profile.getName.equals(profileName)) {
   return profile;
}

// default
return profiles[0];

for (S3Profile profile : profiles) {
if (profile.getName().equals(profileName))
return profile;
}
return null;
}

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fail fast if true

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each entry has checkbox whether to perform invalidation if build failed. It was done the same way as upload to s3, if resources were uploaded then probably they should be invalidated


S3Profile profile = getProfile();
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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK invalidation applies to CloudFront, not S3

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, i was looking at CloudFront in relation with S3. Actually, that's how we are using it. After we update some resources on S3, we want them to be updated within cdn as well, so we invoke invalidation on those keys. And because of the relation, we ask user to provide s3 bucket name and key prefix.
The callable, extracts all distributions related with the account and invalidate all distributions related to the bucket, so all of them have updated version of S3 file.

On the other hand, we can ask user to specify distribution alias instead of bucket and CloudFront object path prefix. @bsideup , do you think it would be more useful approach? What if user has multiple caches for same entries, would he/she has to create multiple invalidationEntries in the job config with same entry path?

continue;
}

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

String bucket = Util.replaceMacro(entry.bucket, envVars);
if (StringUtils.isBlank(bucket)) {
log(listener.getLogger(), "S3 bucket was not specified.");
continue;
}

InvalidationRecord invalidationRecord = profile.invalidate(build, listener, bucket, keyPrefix);
log(listener.getLogger(), "With bucket = " + bucket
+ "; keyPrefix = " + keyPrefix + ";\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> {

private final CopyOnWriteList<S3Profile> profiles = new CopyOnWriteList<S3Profile>();

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() {
if(profiles.isEmpty()){
profiles.addAll(Arrays.asList(((hudson.plugins.s3.S3BucketPublisher.DescriptorImpl) Jenkins.getInstance().getDescriptor(S3BucketPublisher.class)).getProfiles()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line is too long, could you please extract some parts like getting descriptors?

}
return profiles.toArray(new S3Profile[0]);
}

@Override
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
}
}
64 changes: 64 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,64 @@
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> {

/**
* S3 bucket the entry belongs to. Can contain macros.
*/
public String bucket;
/**
* Key prefix of S3 files to invalidate.
* Can contain macros.
*/
public String keyPrefix;

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

@DataBoundConstructor
public InvalidationEntry(String bucket, String keyPrefix, boolean noInvalidateOnFailure) {
this.bucket = bucket;
this.keyPrefix = keyPrefix;
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 doCheckBucket(@QueryParameter String bucket) {
return checkNotBlank(bucket, "Bucket name must be speified");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo in "speified" :)

}

public FormValidation doCheckKeyPrefix(@QueryParameter String keyPrefix) {
return checkNotBlank(keyPrefix, "Key prefix name must be speified");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

}

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

}
38 changes: 38 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,38 @@
package hudson.plugins.s3.cloudfront;

import java.util.ArrayList;
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, List<String> paths) {
entries.add(new InvalidationRecordEntry(distribution, paths));
}

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

private class InvalidationRecordEntry {

private DistributionSummary distribution;
private List<String> paths;

public InvalidationRecordEntry(DistributionSummary distribution, List<String> paths) {
this.distribution = distribution;
this.paths = paths;
}

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


}
}
Loading