-
Notifications
You must be signed in to change notification settings - Fork 193
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
base: master
Are you sure you want to change the base?
Changes from 10 commits
e69e6df
b08f46f
f1d9be5
075ecb5
87ee499
c2a1e40
d34b43a
9c9001f
4e02b6f
66429ac
63e7370
190b2d7
69fef13
fe2ff95
4347350
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
// 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fail fast if true There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIK invalidation applies to CloudFront, not S3 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
} |
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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
}; | ||
|
||
} |
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 + "]"; | ||
} | ||
|
||
|
||
} | ||
} |
There was a problem hiding this comment.
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):