/**
* The MIT License
*
* Copyright (c) 2015, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jenkinsci.plugins.registry.notification;
import hudson.Extension;
import hudson.init.InitMilestone;
import hudson.model.*;
import hudson.security.ACL;
import jenkins.model.FingerprintFacet;
import jenkins.model.Jenkins;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.registry.notification.webhook.PushNotification;
import org.jenkinsci.plugins.registry.notification.webhook.dockerhub.DockerHubCallbackPayload;
import org.jenkinsci.plugins.registry.notification.webhook.dockerhub.DockerHubPushNotification;
import org.jenkinsci.plugins.registry.notification.webhook.dockerhub.DockerHubWebHookPayload;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.io.IOException;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Store of all triggered builds.
*/
@Extension
public final class TriggerStore extends Descriptor<TriggerStore>
implements Describable<TriggerStore> {
@Inject
Jenkins jenkins;
public TriggerStore() {
super(TriggerStore.class);
}
public synchronized void triggered(@Nonnull final PushNotification pushNotification, Job<?, ?> job) {
try {
TriggerEntry entry = getOrCreateEntry(pushNotification);
entry.addEntry(job);
save(entry);
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to update triggered info for " + job.getFullDisplayName(), e);
}
}
public synchronized void started(@Nonnull final PushNotification pushNotification, Run<?, ?> run) {
try {
TriggerEntry entry = getOrCreateEntry(pushNotification);
entry.updateEntry(run);
save(entry);
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to update started info for " + run.getFullDisplayName(), e);
}
}
@CheckForNull
public synchronized TriggerEntry finalized(@Nonnull final PushNotification pushNotification, Run<?, ?> run) {
try {
TriggerEntry entry = getOrCreateEntry(pushNotification);
entry.updateEntry(run);
save(entry);
return entry;
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to update finalized info for " + run.getFullDisplayName(), e);
return null;
}
}
/**
* When a build has been removed from jenkins it should also be removed from this store.
*
* @param payload
* @param run the build.
*/
public synchronized void removed(@Nonnull final PushNotification payload, Run<?, ?> run) {
try {
TriggerEntry entry = getEntry(payload.sha());
if (entry != null) {
entry.removeEntry(run);
if (entry.getEntries().isEmpty()) {
// TODO: FingerprintFacet should have isAlive() method to let it report its liveness.
}
save(entry);
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to remove info for build " + run.getFullDisplayName(), e);
}
}
@Nonnull
private synchronized TriggerEntry getOrCreateEntry(@Nonnull final PushNotification pushNotification) throws IOException, InterruptedException {
Fingerprint fingerprint = jenkins.getFingerprintMap().getOrCreate(null, pushNotification.getRepoName(), pushNotification.sha());
TriggerEntry entry = fingerprint.getFacet(TriggerEntry.class);
if (entry==null) fingerprint.getFacets().add(entry=new TriggerEntry(fingerprint,pushNotification));
return entry;
}
/**
* Gets an existing {@link TriggerEntry}, or null if no such thing exists.
*
* @param sha the {@link PushNotification#sha()}.
* @return the entry if found.
* @throws IOException if so
* @throws InterruptedException if so
*/
@CheckForNull
public synchronized TriggerEntry getEntry(String sha) throws IOException, InterruptedException {
Fingerprint fingerprint = jenkins.getFingerprintMap().get(sha);
if (fingerprint==null) return null;
return fingerprint.getFacet(TriggerEntry.class);
}
private synchronized void onLocationChanged(@Nonnull Job<?,?> item, @Nonnull String oldFullName, @Nonnull String newFullName) {
// no efficient way to do this in fingerprint. But hey, cool job names do not change http://www.w3.org/Provider/Style/URI.html
// try {
// //This could be quite many, but I have no better ideas
// List<String> shas = getAllStoredShas();
// for (String sha : shas) {
// TriggerEntry entry = getEntry(sha);
// if (entry != null) {
// boolean updated = false;
// for (TriggerEntry.RunEntry run : entry.getEntries()) {
// if (oldFullName.equals(run.getJobName())) {
// run.setJobName(newFullName);
// updated = true;
// }
// }
// if (updated) {
// save(entry);
// }
// }
// }
// } catch (Exception e) {
// logger.log(Level.WARNING, "Failed to update a potentially stored job reference \'"+oldFullName+"\' to \'"+newFullName+"\'", e);
// e.printStackTrace();
// }
}
public synchronized void save(@Nonnull final TriggerEntry entry) throws IOException, InterruptedException {
entry.getFingerprint().save();
}
/**
* Gets the effective singleton instance.
*
* @return the effective singleton instance.
* @throws AssertionError if the singleton is missing, i.e. not running on a Jenkins master.
*/
@Nonnull
public static TriggerStore getInstance() {
Jenkins instance = Jenkins.getInstance();
TriggerStore d;
if (instance == null) {
d = null;
} else if (instance.getInitLevel().compareTo(InitMilestone.JOB_LOADED) < 0) {
throw new AssertionError(TriggerStore.class.getName() + " is not available until after all jobs are loaded");
} else {
d = instance.getDescriptorByType(TriggerStore.class);
}
if (d == null) {
throw new AssertionError(TriggerStore.class.getName() + " is missing");
}
return d;
}
@Override
public Descriptor<TriggerStore> getDescriptor() {
return this;
}
@Override
public String getDisplayName() {
return null;
}
public static class TriggerEntry extends FingerprintFacet {
@Nonnull
private PushNotification pushNotification;
@Nonnull
private final List<RunEntry> entries;
@CheckForNull
private DockerHubCallbackPayload callbackData;
public TriggerEntry(Fingerprint fingerprint, @Nonnull PushNotification pushNotification) {
super(fingerprint, pushNotification.getReceived());
this.pushNotification = pushNotification;
entries = new LinkedList<RunEntry>();
}
@Nonnull
public RunEntry addEntry(Job<?, ?> job) {
RunEntry entry = getEntry(job.getFullName());
if (entry == null) {
entry = new RunEntry(job.getFullName());
}
return entry;
}
public RunEntry getEntry(@Nonnull Job<?, ?> job) {
return getEntry(job.getFullName());
}
public RunEntry getEntry(@Nonnull String jobName) {
for (RunEntry entry : entries) {
if (entry.getJobName().equals(jobName)) {
return entry;
}
}
return null;
}
public RunEntry updateEntry(Run<?, ?> run) {
RunEntry entry = getEntry(run.getParent());
if (entry == null) {
entry = new RunEntry(run.getParent().getFullName(), run.getId());
entries.add(entry);
} else {
entry.setRun(run);
}
entry.setDone(!run.isBuilding());
return entry;
}
@Nonnull
public PushNotification getPushNotification() {
return pushNotification;
}
@Nonnull
public List<RunEntry> getEntries() {
return entries;
}
@CheckForNull
public DockerHubCallbackPayload getCallbackData() {
return callbackData;
}
public void setCallbackData(@CheckForNull DockerHubCallbackPayload callbackData) {
this.callbackData = callbackData;
}
public void removeEntry(@Nonnull Run<?, ?> run) {
RunEntry entry = getEntry(run.getParent());
if (entry != null) {
entries.remove(entry);
}
}
public boolean areAllDone() {
for (RunEntry entry : entries) {
if (!entry.isDone()) {
return false;
}
}
return true;
}
private transient DockerHubWebHookPayload payload;
public Object readResolve() {
if (payload != null) {
pushNotification = payload.getPushNotifications().get(0);
}
return this;
}
public static class RunEntry implements Serializable {
private static final long serialVersionUID = -4889803337604416914L;
private String jobName;
private String buildId;
private boolean done;
public RunEntry(@Nonnull String jobName) {
this.jobName = jobName;
}
public RunEntry(@Nonnull String jobName, String buildId) {
this.jobName = jobName;
this.buildId = buildId;
}
@Nonnull
public String getJobName() {
return jobName;
}
public void setJobName(@Nonnull String jobName) {
this.jobName = jobName;
}
@CheckForNull
public String getBuildId() {
return buildId;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
}
@CheckForNull
public Job<?, ?> getJob() {
final Jenkins jenkins = Jenkins.getInstance();
if (jenkins != null) {
SecurityContext old = ACL.impersonate(ACL.SYSTEM);
try {
return jenkins.getItemByFullName(jobName, Job.class);
} catch (Exception e) {
logger.log(Level.WARNING, "Unable to retrieve job " + jobName, e);
} finally {
SecurityContextHolder.setContext(old);
}
}
return null;
}
public void setRun(@CheckForNull Run<?, ?> build) {
if (build == null) {
this.buildId = null;
} else {
this.buildId = build.getId();
}
}
@CheckForNull
public Run<?, ?> getRun() {
if (StringUtils.isBlank(buildId)) {
return null;
}
final Job<?, ?> job = getJob();
if (job != null) {
SecurityContext old = ACL.impersonate(ACL.SYSTEM);
try {
return job.getBuild(buildId);
} catch (Exception e) {
logger.log(Level.WARNING, "Unable to retrieve run " + jobName + ":" + buildId, e);
} finally {
SecurityContextHolder.setContext(old);
}
}
return null;
}
}
}
@Extension
public static class ItemListener extends hudson.model.listeners.ItemListener {
@Override
public void onLocationChanged(Item item, String oldFullName, String newFullName) {
if (item instanceof Job) {
TriggerStore.getInstance().onLocationChanged((Job<?, ?>)item, oldFullName, newFullName);
}
}
}
private static final Logger logger = Logger.getLogger(TriggerStore.class.getName());
}