package org.jenkinsci.plugins.github.admin; import com.cloudbees.jenkins.GitHubRepositoryName; import com.google.common.collect.ImmutableMap; import hudson.BulkChange; import hudson.Extension; import hudson.XmlFile; import hudson.model.AdministrativeMonitor; import hudson.model.ManagementLink; import hudson.model.Saveable; import hudson.model.listeners.SaveableListener; import hudson.util.PersistedList; import jenkins.model.Jenkins; import org.jenkinsci.plugins.github.Messages; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.interceptor.RequirePOST; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import static org.apache.commons.lang3.StringUtils.defaultIfBlank; /** * Administrative monitor to track problems of registering/removing hooks for GH. * Holds non-savable map of repo->message and persisted list of ignored projects. * Anyone can register new problem with {@link #registerProblem(GitHubRepositoryName, Throwable)} and check * repo for problems with {@link #isProblemWith(GitHubRepositoryName)} * * Has own page with table with problems and ignoring list in global management section. Link to this page * is visible if any problem or ignored repo is registered * * @author lanwen (Merkushev Kirill) * @since 1.17.0 */ @Extension public class GitHubHookRegisterProblemMonitor extends AdministrativeMonitor implements Saveable { private static final Logger LOGGER = LoggerFactory.getLogger(GitHubHookRegisterProblemMonitor.class); /** * Problems map. Cleared on Jenkins restarts */ private transient Map<GitHubRepositoryName, String> problems = new ConcurrentHashMap<>(); /** * Ignored list. Saved to file on any change. Reloaded after restart */ private PersistedList<GitHubRepositoryName> ignored; public GitHubHookRegisterProblemMonitor() { super(GitHubHookRegisterProblemMonitor.class.getSimpleName()); load(); ignored = ignored == null ? new PersistedList<GitHubRepositoryName>(this) : ignored; ignored.setOwner(this); } /** * @return Immutable copy of map with repo->problem message content */ public Map<GitHubRepositoryName, String> getProblems() { return ImmutableMap.copyOf(problems); } /** * Registers problems. For message {@link Throwable#getMessage()} will be used * * @param repo full named GitHub repo, if null nothing will be done * @param throwable exception with message about problem, if null nothing will be done * * @see #registerProblem(GitHubRepositoryName, String) */ public void registerProblem(GitHubRepositoryName repo, Throwable throwable) { if (throwable == null) { return; } registerProblem(repo, throwable.getMessage()); } /** * Used by {@link #registerProblem(GitHubRepositoryName, Throwable)} * * @param repo full named GitHub repo, if null nothing will be done * @param message message to show in the interface. Will be used default if blank */ private void registerProblem(GitHubRepositoryName repo, String message) { if (repo == null) { return; } if (!ignored.contains(repo)) { problems.put(repo, defaultIfBlank(message, Messages.unknown_error())); } else { LOGGER.debug("Repo {} is ignored by monitor, skip this problem...", repo); } } /** * Removes repo from known problems map * * @param repo full named GitHub repo, if null nothing will be done */ public void resolveProblem(GitHubRepositoryName repo) { if (repo == null) { return; } problems.remove(repo); } /** * Checks that repo is registered in this monitor * * @param repo full named GitHub repo * * @return true if repo is in the map */ public boolean isProblemWith(GitHubRepositoryName repo) { return problems.containsKey(repo); } /** * @return immutable copy of list with ignored repos */ public List<GitHubRepositoryName> getIgnored() { return ignored.toList(); } @Override public String getDisplayName() { return Messages.hooks_problem_administrative_monitor_displayname(); } @Override public boolean isActivated() { return !problems.isEmpty(); } /** * Depending on whether the user said "yes" or "no", send him to the right place. */ @RequirePOST @RequireAdminRights public HttpResponse doAct(StaplerRequest req) throws IOException { if (req.hasParameter("no")) { disable(true); return HttpResponses.redirectViaContextPath("/manage"); } else { return new HttpRedirect("."); } } /** * This web method requires POST, admin rights and nonnull repo. * Responds with redirect to monitor page * * @param repo to be ignored. Never null */ @RequirePOST @ValidateRepoName @RequireAdminRights @RespondWithRedirect public void doIgnore(@Nonnull @GHRepoName GitHubRepositoryName repo) { if (!ignored.contains(repo)) { ignored.add(repo); } resolveProblem(repo); } /** * This web method requires POST, admin rights and nonnull repo. * Responds with redirect to monitor page * * @param repo to be disignored. Never null */ @RequirePOST @ValidateRepoName @RequireAdminRights @RespondWithRedirect public void doDisignore(@Nonnull @GHRepoName GitHubRepositoryName repo) { ignored.remove(repo); } /** * Save the settings to a file. Called on each change of {@code ignored} list */ @Override public synchronized void save() { if (BulkChange.contains(this)) { return; } try { getConfigFile().write(this); SaveableListener.fireOnChange(this, getConfigFile()); } catch (IOException e) { LOGGER.error("{}", e); } } private synchronized void load() { XmlFile file = getConfigFile(); if (!file.exists()) { return; } try { file.unmarshal(this); } catch (IOException e) { LOGGER.warn("Failed to load {}", file, e); } } private XmlFile getConfigFile() { return new XmlFile(new File(Jenkins.getInstance().getRootDir(), getClass().getName() + ".xml")); } /** * @return instance of administrative monitor to register/resolve/ignore/check hook problems */ public static GitHubHookRegisterProblemMonitor get() { return AdministrativeMonitor.all().get(GitHubHookRegisterProblemMonitor.class); } @Extension public static class GitHubHookRegisterProblemManagementLink extends ManagementLink { @Inject private GitHubHookRegisterProblemMonitor monitor; @Override public String getIconFileName() { return monitor.getProblems().isEmpty() && monitor.ignored.isEmpty() ? null : "/plugin/github/img/logo.svg"; } @Override public String getUrlName() { return monitor.getUrl(); } @Override public String getDescription() { return Messages.hooks_problem_administrative_monitor_description(); } @Override public String getDisplayName() { return Messages.hooks_problem_administrative_monitor_displayname(); } } }