diff --git a/src/main/java/jenkins/plugins/slack/SlackNotifier.java b/src/main/java/jenkins/plugins/slack/SlackNotifier.java index f746f64a..4878e89a 100755 --- a/src/main/java/jenkins/plugins/slack/SlackNotifier.java +++ b/src/main/java/jenkins/plugins/slack/SlackNotifier.java @@ -8,6 +8,7 @@ import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; +import hudson.model.Item; import hudson.model.Descriptor; import hudson.model.listeners.ItemListener; import hudson.security.ACL; @@ -18,7 +19,9 @@ import hudson.util.FormValidation; import hudson.util.ListBoxModel; import jenkins.model.Jenkins; +import jenkins.plugins.slack.config.ItemConfigMigrator; import net.sf.json.JSONObject; + import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; import org.jenkinsci.plugins.plaincredentials.StringCredentials; @@ -29,7 +32,6 @@ import java.io.IOException; import java.util.Map; -import java.util.logging.Level; import java.util.logging.Logger; import static com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials; @@ -65,14 +67,26 @@ public String getTeamDomain() { return teamDomain; } + public void setTeamDomain(final String teamDomain) { + this.teamDomain = teamDomain; + } + public String getRoom() { return room; } + public void setRoom(String room) { + this.room = room; + } + public String getAuthToken() { return authToken; } + public void setAuthToken(String authToken) { + this.authToken = authToken; + } + public String getAuthTokenCredentialId() { return authTokenCredentialId; } @@ -129,6 +143,54 @@ public String getCustomMessage() { return customMessage; } + public void setStartNotification(boolean startNotification) { + this.startNotification = startNotification; + } + + public void setNotifySuccess(boolean notifySuccess) { + this.notifySuccess = notifySuccess; + } + + public void setCommitInfoChoice(CommitInfoChoice commitInfoChoice) { + this.commitInfoChoice = commitInfoChoice; + } + + public void setNotifyAborted(boolean notifyAborted) { + this.notifyAborted = notifyAborted; + } + + public void setNotifyFailure(boolean notifyFailure) { + this.notifyFailure = notifyFailure; + } + + public void setNotifyNotBuilt(boolean notifyNotBuilt) { + this.notifyNotBuilt = notifyNotBuilt; + } + + public void setNotifyUnstable(boolean notifyUnstable) { + this.notifyUnstable = notifyUnstable; + } + + public void setNotifyBackToNormal(boolean notifyBackToNormal) { + this.notifyBackToNormal = notifyBackToNormal; + } + + public void setIncludeTestSummary(boolean includeTestSummary) { + this.includeTestSummary = includeTestSummary; + } + + public void setNotifyRepeatedFailure(boolean notifyRepeatedFailure) { + this.notifyRepeatedFailure = notifyRepeatedFailure; + } + + public void setIncludeCustomMessage(boolean includeCustomMessage) { + this.includeCustomMessage = includeCustomMessage; + } + + public void setCustomMessage(String customMessage) { + this.customMessage = customMessage; + } + @DataBoundConstructor public SlackNotifier(final String teamDomain, final String authToken, final String room, final String authTokenCredentialId, final String sendAs, final boolean startNotification, final boolean notifyAborted, final boolean notifyFailure, @@ -246,7 +308,6 @@ public String getSendAs() { return sendAs; } - @SuppressWarnings("unused") public ListBoxModel doFillTokenCredentialIdItems() { if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { return new ListBoxModel(); @@ -480,66 +541,21 @@ public String getCustomMessage() { } @Extension(ordinal = 100) public static final class Migrator extends ItemListener { - @SuppressWarnings("deprecation") @Override public void onLoaded() { logger.info("Starting Settings Migration Process"); - for (AbstractProject, ?> p : Jenkins.getInstance().getAllItems(AbstractProject.class)) { - logger.info("processing Job: " + p.getName()); - final SlackJobProperty slackJobProperty = p.getProperty(SlackJobProperty.class); + ItemConfigMigrator migrator = new ItemConfigMigrator(); - if (slackJobProperty == null) { - logger.info(String - .format("Configuration is already up to date for \"%s\", skipping migration", - p.getName())); + for (Item item : Jenkins.getInstance().getAllItems()) { + if (!migrator.migrate(item)) { + logger.info(String.format("Skipping job \"%s\" with type %s", item.getName(), + item.getClass().getName())); continue; } - - SlackNotifier slackNotifier = p.getPublishersList().get(SlackNotifier.class); - - if (slackNotifier == null) { - logger.info(String - .format("Configuration does not have a notifier for \"%s\", not migrating settings", - p.getName())); - } else { - - //map settings - if (StringUtils.isBlank(slackNotifier.teamDomain)) { - slackNotifier.teamDomain = slackJobProperty.getTeamDomain(); - } - if (StringUtils.isBlank(slackNotifier.authToken)) { - slackNotifier.authToken = slackJobProperty.getToken(); - } - if (StringUtils.isBlank(slackNotifier.room)) { - slackNotifier.room = slackJobProperty.getRoom(); - } - - slackNotifier.startNotification = slackJobProperty.getStartNotification(); - - slackNotifier.notifyAborted = slackJobProperty.getNotifyAborted(); - slackNotifier.notifyFailure = slackJobProperty.getNotifyFailure(); - slackNotifier.notifyNotBuilt = slackJobProperty.getNotifyNotBuilt(); - slackNotifier.notifySuccess = slackJobProperty.getNotifySuccess(); - slackNotifier.notifyUnstable = slackJobProperty.getNotifyUnstable(); - slackNotifier.notifyBackToNormal = slackJobProperty.getNotifyBackToNormal(); - slackNotifier.notifyRepeatedFailure = slackJobProperty.getNotifyRepeatedFailure(); - - slackNotifier.includeTestSummary = slackJobProperty.includeTestSummary(); - slackNotifier.commitInfoChoice = slackJobProperty.getShowCommitList() ? CommitInfoChoice.AUTHORS_AND_TITLES : CommitInfoChoice.NONE; - slackNotifier.includeCustomMessage = slackJobProperty.includeCustomMessage(); - slackNotifier.customMessage = slackJobProperty.getCustomMessage(); - } - - try { - //property section is not used anymore - remove - p.removeProperty(SlackJobProperty.class); - p.save(); - logger.info("Configuration updated successfully"); - } catch (IOException e) { - logger.log(Level.SEVERE, e.getMessage(), e); - } } + + logger.info("Completed Settings Migration Process"); } } } diff --git a/src/main/java/jenkins/plugins/slack/config/AbstractProjectConfigMigrator.java b/src/main/java/jenkins/plugins/slack/config/AbstractProjectConfigMigrator.java new file mode 100644 index 00000000..a09bd16f --- /dev/null +++ b/src/main/java/jenkins/plugins/slack/config/AbstractProjectConfigMigrator.java @@ -0,0 +1,98 @@ +package jenkins.plugins.slack.config; + +import hudson.model.AbstractProject; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jenkins.plugins.slack.CommitInfoChoice; +import jenkins.plugins.slack.SlackNotifier; +import jenkins.plugins.slack.SlackNotifier.SlackJobProperty; + +import org.apache.commons.lang.StringUtils; + +/** + * Configuration migrator for migrating the Slack plugin configuration for a + * {@link AbstractProject} from the 1.8 format to the 2.0 format. It does so by + * removing the SlackJobProperty from the job properties (if there is one) and + * moving the Slack notification settings to a {@link SlackNotifier} in the list + * of publishers (if there is one). + */ +@SuppressWarnings("deprecation") +public class AbstractProjectConfigMigrator { + + private static final Logger logger = Logger.getLogger(AbstractProjectConfigMigrator.class + .getName()); + + public void migrate(final AbstractProject, ?> project) { + + logger.info(String.format("Migrating project \"%s\" with type %s", project.getName(), + project.getClass().getName())); + + final SlackJobProperty slackJobProperty = project.getProperty(SlackJobProperty.class); + + if (slackJobProperty == null) { + logger.info(String.format( + "Configuration is already up to date for \"%s\", skipping migration", + project.getName())); + return; + } + + SlackNotifier slackNotifier = project.getPublishersList().get(SlackNotifier.class); + + if (slackNotifier == null) { + logger.info(String.format( + "Configuration does not have a notifier for \"%s\", not migrating settings", + project.getName())); + } else { + updateSlackNotifier(slackNotifier, slackJobProperty); + } + + try { + // property section is not used anymore - remove + project.removeProperty(SlackJobProperty.class); + project.save(); + logger.info("Configuration updated successfully"); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } + + private void updateSlackNotifier(final SlackNotifier slackNotifier, + final SlackJobProperty slackJobProperty) { + + if (StringUtils.isBlank(slackNotifier.getTeamDomain())) { + slackNotifier.setTeamDomain(slackJobProperty.getTeamDomain()); + } + if (StringUtils.isBlank(slackNotifier.getAuthToken())) { + slackNotifier.setAuthToken(slackJobProperty.getToken()); + } + if (StringUtils.isBlank(slackNotifier.getRoom())) { + slackNotifier.setRoom(slackJobProperty.getRoom()); + } + + slackNotifier.setStartNotification(slackJobProperty.getStartNotification()); + + slackNotifier.setNotifyAborted(slackJobProperty.getNotifyAborted()); + slackNotifier.setNotifyFailure(slackJobProperty.getNotifyFailure()); + slackNotifier.setNotifyNotBuilt(slackJobProperty.getNotifyNotBuilt()); + slackNotifier.setNotifySuccess(slackJobProperty.getNotifySuccess()); + slackNotifier.setNotifyUnstable(slackJobProperty.getNotifyUnstable()); + slackNotifier.setNotifyBackToNormal(slackJobProperty.getNotifyBackToNormal()); + slackNotifier.setNotifyRepeatedFailure(slackJobProperty.getNotifyRepeatedFailure()); + + slackNotifier.setIncludeTestSummary(slackJobProperty.includeTestSummary()); + slackNotifier.setCommitInfoChoice(getCommitInfoChoice(slackJobProperty)); + slackNotifier.setIncludeCustomMessage(slackJobProperty.includeCustomMessage()); + slackNotifier.setCustomMessage(slackJobProperty.getCustomMessage()); + } + + private CommitInfoChoice getCommitInfoChoice(final SlackJobProperty slackJobProperty) { + if (slackJobProperty.getShowCommitList()) { + return CommitInfoChoice.AUTHORS_AND_TITLES; + } else { + return CommitInfoChoice.NONE; + } + } +} diff --git a/src/main/java/jenkins/plugins/slack/config/ItemConfigMigrator.java b/src/main/java/jenkins/plugins/slack/config/ItemConfigMigrator.java new file mode 100644 index 00000000..327b9140 --- /dev/null +++ b/src/main/java/jenkins/plugins/slack/config/ItemConfigMigrator.java @@ -0,0 +1,112 @@ +package jenkins.plugins.slack.config; + +import java.lang.reflect.Method; + +import org.springframework.util.ReflectionUtils; + +import hudson.model.AbstractProject; +import hudson.model.Item; +import hudson.model.Job; +import hudson.util.DescribableList; + +/** + * Configuration migrator for migrating the Slack plugin configuration from the + * 1.8 format to the 2.0 format, for an item like a job or project. Mainly just + * decides what needs to be done and delegates the actual migration work to + * other migrators. + * + * @see AbstractProjectConfigMigrator + * @see JobConfigMigrator + * @see ItemWithTemplateConfigMigrator + */ +public class ItemConfigMigrator { + + private final AbstractProjectConfigMigrator projectMigrator; + private final JobConfigMigrator jobMigrator; + private final ItemWithTemplateConfigMigrator templateMigrator; + + public ItemConfigMigrator() { + projectMigrator = new AbstractProjectConfigMigrator(); + jobMigrator = new JobConfigMigrator(); + templateMigrator = new ItemWithTemplateConfigMigrator(projectMigrator); + } + + /** + * Constructor for injecting migrators for testing. + */ + protected ItemConfigMigrator(AbstractProjectConfigMigrator projectMigrator, + JobConfigMigrator jobMigrator, ItemWithTemplateConfigMigrator templateMigrator) { + this.projectMigrator = projectMigrator; + this.jobMigrator = jobMigrator; + this.templateMigrator = templateMigrator; + } + + /** + * Migrate configuration for a {@link Item} from the 1.8 format to the 2.0 + * format. This primarily removes job properties and adds them to a + * notifier. + * + * @param item + * Item to migrate + * @return true if migration was attempted for an expected scenario; false + * if migration was not attempted due to an unexpected data type or + * other reason + */ + public boolean migrate(Item item) { + + // Attempt migrations in priority order + return migrateAbstractProject(item) || migrateJobWithoutPublishersList(item) + || templateMigrator.migrate(item); + } + + /** + * Migrate an item if it is a subclass of AbstractProject. + * + * @param item + * Item to migrate + * @return true if migration was attempted + */ + private boolean migrateAbstractProject(Item item) { + + if (item instanceof AbstractProject) { + AbstractProject, ?> project = (AbstractProject, ?>) item; + projectMigrator.migrate(project); + return true; + } + return false; + } + + /** + * Migrate an item if it is subclass of Job. It might have Slack job + * properties that need to be removed in the migration, but does not handle + * the possibility of a Job that has publishers. That should theoretically + * not happen because then it should be a subclass of AbstractProject, but + * we want to avoid losing settings. So if it looks like it has publishers, + * migration is skipped. + * + * @param item + * Item to migrate + * @return true if migration was attempted + */ + private boolean migrateJobWithoutPublishersList(Item item) { + if (item instanceof Job) { + if (!hasMethodGetPublishersList(item)) { + Job, ?> job = (Job, ?>) item; + jobMigrator.migrate(job); + return true; + } + } + return false; + } + + private boolean hasMethodGetPublishersList(Item item) { + + Method method = ReflectionUtils.findMethod(item.getClass(), "getPublishersList"); + + if (method != null && method.getReturnType().equals(DescribableList.class)) { + return true; + } + + return false; + } +} diff --git a/src/main/java/jenkins/plugins/slack/config/ItemWithTemplateConfigMigrator.java b/src/main/java/jenkins/plugins/slack/config/ItemWithTemplateConfigMigrator.java new file mode 100644 index 00000000..b8773701 --- /dev/null +++ b/src/main/java/jenkins/plugins/slack/config/ItemWithTemplateConfigMigrator.java @@ -0,0 +1,100 @@ +package jenkins.plugins.slack.config; + +import java.lang.reflect.Method; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.springframework.util.ReflectionUtils; + +import hudson.model.AbstractProject; +import hudson.model.Item; +import hudson.model.Job; + +/** + * Configuration migrator for migrating the Slack plugin configuration for an + * {@link AbstractProject} that belongs to an {@link Item} as a template, from + * the 1.8 format to the 2.0 format. + * + *
+ * This is a workaround for installations that use the multi-branch-project + * plugin, which uses a template to configure jobs for all branches in a repo. + * The template will be updated by an {@link AbstractProjectConfigMigrator}. + *
+ */ +public class ItemWithTemplateConfigMigrator { + + private static final Logger logger = Logger.getLogger(ItemWithTemplateConfigMigrator.class + .getName()); + + private AbstractProjectConfigMigrator projectMigrator; + + /** + * Default constructor. + * + * @param projectMigrator + * Migrator to be used for migrating the template + */ + public ItemWithTemplateConfigMigrator(final AbstractProjectConfigMigrator projectMigrator) { + this.projectMigrator = projectMigrator; + } + + /** + * Migrate an item if it has a template that is a subclass of + * AbstractProject. + * + * @param item + * Item to migrate + * @return true if migration was attempted on a template + */ + public boolean migrate(final Item item) { + AbstractProject, ?> project = getTemplateProject(item); + + if (project != null) { + projectMigrator.migrate(project); + return true; + } + + return false; + } + + /** + * Examine an Item to determine if it has a "getTemplate" method that + * returns an AbstractProject, and return the AbstractProject if it does. + * + * @param item + * Item to examine + * @return AbstractProject that is returned by item.getTemplate(), or null + */ + private AbstractProject, ?> getTemplateProject(final Item item) { + + logger.log(Level.FINE, + String.format("Checking \"%s\" for AbstractProject template", item.getName())); + + Method getTemplate = ReflectionUtils.findMethod(item.getClass(), "getTemplate"); + + if (getTemplate == null) { + logger.log(Level.FINE, "No template getter method found"); + return null; + } + + Object obj = null; + + try { + obj = getTemplate.invoke(item); + } catch (Exception e) { + logger.info("Error getting \"template\" value: " + e.getMessage()); + return null; + } + + if (obj == null) { + logger.log(Level.FINE, "Item has no template"); + } else if (obj instanceof AbstractProject) { + return (AbstractProject, ?>) obj; + } else { + logger.log(Level.FINE, "Template is not an AbstractProject; type is: " + + obj.getClass().getName()); + } + + return null; + } +} diff --git a/src/main/java/jenkins/plugins/slack/config/JobConfigMigrator.java b/src/main/java/jenkins/plugins/slack/config/JobConfigMigrator.java new file mode 100644 index 00000000..d6c8a589 --- /dev/null +++ b/src/main/java/jenkins/plugins/slack/config/JobConfigMigrator.java @@ -0,0 +1,51 @@ +package jenkins.plugins.slack.config; + +import hudson.model.Job; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jenkins.plugins.slack.SlackNotifier.SlackJobProperty; + +/** + * Configuration migrator for migrating the Slack plugin configuration for a + * {@link Job} from the 1.8 format to the 2.0 format. It does so by removing the + * SlackJobProperty from the job properties (if there is one). + * + *+ * SlackJobProperty settings are usually migrated to a publisher, but there are + * no publishers in a Job so the settings are lost. For this reason, be + * careful of how you use this migrator.. + *
+ */ +@SuppressWarnings("deprecation") +public class JobConfigMigrator { + + private static final Logger logger = Logger.getLogger(JobConfigMigrator.class.getName()); + + public void migrate(final Job, ?> job) { + + logger.info(String.format("Migrating job \"%s\" with type %s", job.getName(), job + .getClass().getName())); + + final SlackJobProperty slackJobProperty = job.getProperty(SlackJobProperty.class); + + if (slackJobProperty == null) { + logger.info(String.format( + "Configuration is already up to date for \"%s\", skipping migration", + job.getName())); + return; + } + + try { + // property section is not used anymore - remove + job.removeProperty(SlackJobProperty.class); + job.save(); + logger.info(String.format("Configuration for \"%s\" updated successfully", + job.getName())); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } +} diff --git a/src/test/java/jenkins/plugins/slack/config/ItemConfigMigratorTest.java b/src/test/java/jenkins/plugins/slack/config/ItemConfigMigratorTest.java new file mode 100644 index 00000000..e6c72c44 --- /dev/null +++ b/src/test/java/jenkins/plugins/slack/config/ItemConfigMigratorTest.java @@ -0,0 +1,111 @@ +package jenkins.plugins.slack.config; + +import jenkins.model.AbstractTopLevelItem; +import jenkins.model.Jenkins; +import hudson.model.Descriptor; +import hudson.model.Item; +import hudson.model.ItemGroup; +import hudson.model.ViewJob; +import hudson.model.AbstractProject; +import hudson.model.Job; +import hudson.tasks.Publisher; +import hudson.util.DescribableList; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +public class ItemConfigMigratorTest { + + private AbstractProjectConfigMigrator projectMigrator; + private JobConfigMigrator jobMigrator; + private ItemWithTemplateConfigMigrator templateMigrator; + private ItemConfigMigrator migrator; + + private Jenkins jenkins; + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Before + public void setUp() { + jenkins = j.getInstance(); + + projectMigrator = mock(AbstractProjectConfigMigrator.class); + jobMigrator = mock(JobConfigMigrator.class); + templateMigrator = mock(ItemWithTemplateConfigMigrator.class); + + migrator = new ItemConfigMigrator(projectMigrator, jobMigrator, templateMigrator); + } + + @Test + public void testMigrate_AbstractProject() { + AbstractProject, ?> item = mock(AbstractProject.class); + + assertTrue(migrator.migrate(item)); + + verify(projectMigrator).migrate(any(AbstractProject.class)); + verify(jobMigrator, never()).migrate(any(Job.class)); + verify(templateMigrator, never()).migrate(any(AbstractProject.class)); + } + + @Test + public void testMigrate_Job() { + Job, ?> item = mock(Job.class); + + assertTrue(migrator.migrate(item)); + + verify(projectMigrator, never()).migrate(any(AbstractProject.class)); + verify(jobMigrator).migrate(any(Job.class)); + verify(templateMigrator, never()).migrate(any(AbstractProject.class)); + } + + @Test + public void testMigrate_JobWithPublishersList() { + Job, ?> item = new JobWithPublishers(jenkins, "Random Name"); + + doReturn(false).when(templateMigrator).migrate(item); + + assertFalse(migrator.migrate(item)); + + verify(projectMigrator, never()).migrate(any(AbstractProject.class)); + verify(jobMigrator, never()).migrate(any(Job.class)); + verify(templateMigrator).migrate(any(AbstractProject.class)); + } + + @Test + public void testMigrate_ItemWithTemplate() { + + Item item = mock(Item.class); + + doReturn(true).when(templateMigrator).migrate(item); + + assertTrue(migrator.migrate(item)); + + verify(projectMigrator, never()).migrate(any(AbstractProject.class)); + verify(jobMigrator, never()).migrate(any(Job.class)); + verify(templateMigrator).migrate(eq(item)); + } + + @SuppressWarnings("rawtypes") + private static class JobWithPublishers extends ViewJob { + + public JobWithPublishers(ItemGroup parent, String name) { + super(parent, name); + } + + @Override + public void reload() { + } + + @SuppressWarnings("unused") + public DescribableList