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

[JENKINS-43205][JENKINS-69006] Allow admins to configure a global default URL provider and always make external links go through /display/redirect #202

Merged
merged 7 commits into from
Aug 17, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* The MIT License
*
* Copyright 2023 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.displayurlapi;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.Util;
import hudson.util.ListBoxModel;
import java.util.Objects;
import jenkins.model.GlobalConfiguration;
import org.jenkinsci.plugins.displayurlapi.user.PreferredProviderUserProperty;
import org.kohsuke.stapler.DataBoundSetter;

@Extension
public class DefaultProviderGlobalConfiguration extends GlobalConfiguration {

public DefaultProviderGlobalConfiguration() {
load();
}

private @CheckForNull String providerId;

public @CheckForNull String getProviderId() {
return providerId;
}

@DataBoundSetter
public void setProviderId(@CheckForNull String providerId) {
providerId = Util.fixEmptyAndTrim(providerId);
if (PreferredProviderUserProperty.ProviderOption.DEFAULT_OPTION.getId().equals(providerId)) {
providerId = null;
}
this.providerId = providerId;
save();
}

public @CheckForNull DisplayURLProvider getConfiguredProvider() {
if (providerId == null) {
return null;
}
return DisplayURLProvider.all().stream()
.filter(provider -> provider.getClass().getName().equals(providerId))
.findFirst()
.orElse(null);
}

public static DefaultProviderGlobalConfiguration get() {
return ExtensionList.lookupSingleton(DefaultProviderGlobalConfiguration.class);
}

public ListBoxModel doFillProviderIdItems() {
ListBoxModel items = new ListBoxModel();
for (PreferredProviderUserProperty.ProviderOption providerOption : PreferredProviderUserProperty.getAll()) {
ListBoxModel.Option option = new ListBoxModel.Option(
providerOption.getName(),
providerOption.getId(),
Objects.equals(providerOption.getId(), providerId)
);
items.add(option);
}
return items;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jenkinsci.plugins.displayurlapi;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.ExtensionList;
Expand All @@ -19,7 +20,16 @@
* Generates URLs for well known UI locations for use in notifications (e.g. mailer, HipChat, Slack,
* IRC, etc) Extensible to allow plugins to override common URLs (e.g. Blue Ocean or another future
* secondary UI)
*
* <p>Implementations should generally extend {@link ClassicDisplayURLProvider} and delegate to it for unsupported
* builds instead of extending this class directly.
*/
/* TODO: This API is awkward to implement. All providers must return a non-null value for all URLs, meaning that in
practice they all need to delegate to ClassicDisplayURLProvider. Ideally, the API would allow providers to return
null to indicate that they do not support a particular build/job, and we would loop over all providers in a
user-defined order looking for the first one that returns a non-null URL, which would allow providers which handle
distinct job types to coexist.
*/
public abstract class DisplayURLProvider implements ExtensionPoint {

/**
Expand All @@ -28,8 +38,7 @@ public abstract class DisplayURLProvider implements ExtensionPoint {
* @return DisplayURLProvider
*/
public static DisplayURLProvider get() {
DisplayURLProvider preferredProvider = getPreferredProvider();
return preferredProvider != null ? preferredProvider : DisplayURLProviderImpl.INSTANCE;
return DisplayURLProviderImpl.INSTANCE;
Copy link
Member Author

Choose a reason for hiding this comment

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

As before #42. See discussion in JENKINS-69006.

}

/**
Expand All @@ -41,13 +50,30 @@ public static ExtensionList<DisplayURLProvider> all() {
return ExtensionList.lookup(DisplayURLProvider.class);
}

/**
* Returns the singleton instance of the {@link ClassicDisplayURLProvider} extension.
*
* <p>If you want to retrieve the configured default provider, use {@link getConfiguredDefault}.
*/
public static DisplayURLProvider getDefault() {
DisplayURLProvider defaultProvider = getPreferredProvider();
if (defaultProvider == null) {
defaultProvider = ExtensionList.lookup(DisplayURLProvider.class)
return ExtensionList.lookup(DisplayURLProvider.class)
.get(ClassicDisplayURLProvider.class);
}

/**
* Returns the {@link DisplayURLProvider} that has been configured as a global default, either via {@link DefaultProviderGlobalConfiguration},
* or by environment variable or system property, or {@code null} if there no configured default.
*/
public static @CheckForNull DisplayURLProvider getConfiguredDefault() {
DisplayURLProvider guiProvider = DefaultProviderGlobalConfiguration.get().getConfiguredProvider();
if (guiProvider != null) {
return guiProvider;
}
return defaultProvider;
String clazz = findClass();
if (isNotEmpty(clazz)) {
return ExtensionList.lookup(DisplayURLProvider.class).getDynamic(clazz);
}
return null;
}

/**
Expand Down Expand Up @@ -182,6 +208,12 @@ private static String findClass() {
return clazz;
}

/**
* @deprecated If you need the default implementation for producing display URLs, use {@link #get}.
* If you need a fallback provider for your implementation of {@link DisplayURLProvider} that will work for all jobs and runs, use {@link #getDefault}.
* Actual redirect resolution incorporating user preferences should be left to {@link AbstractDisplayAction#lookupProvider}
*/
@Deprecated
@Nullable
public static DisplayURLProvider getPreferredProvider() {
PreferredProviderUserProperty prefProperty = getUserPreferredProviderProperty();
Expand All @@ -203,5 +235,5 @@ public static PreferredProviderUserProperty getUserPreferredProviderProperty() {
}

private static final String JENKINS_DISPLAYURL_PROVIDER_ENV = "JENKINS_DISPLAYURL_PROVIDER";
private static final String JENKINS_DISPLAYURL_PROVIDER_PROP = "jenkins.displayurl.provider";
static final String JENKINS_DISPLAYURL_PROVIDER_PROP = "jenkins.displayurl.provider";
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,27 @@ DisplayURLProvider lookupProvider(StaplerRequest req) {
}

DisplayURLProvider lookupProvider() {
dwnusbaum marked this conversation as resolved.
Show resolved Hide resolved
// We pick the first non-null option in this order:
// 1: PreferredProviderUserProperty (user preference)
// 2: DefaultProviderGlobalConfiguration (GUI-configured global default)
// 3: Env var/system properties defined in DisplayURLProvider (global default)
// 4: The DisplayUrlProvider extension with the highest ordinal value that is not an instance of ClassicDisplayURLProvider
// 5. ClassicDisplayURLProvider
PreferredProviderUserProperty prefProperty = getUserPreferredProviderProperty();

if (prefProperty != null && prefProperty.getConfiguredProvider() != null) {
return prefProperty.getConfiguredProvider();
}
DisplayURLProvider displayURLProvider = DisplayURLProvider.getPreferredProvider();
if (displayURLProvider == null) {
ExtensionList<DisplayURLProvider> all = DisplayURLProvider.all();
displayURLProvider = all.stream()
DisplayURLProvider globalProvider = DisplayURLProvider.getConfiguredDefault();
if (globalProvider != null) {
return globalProvider;
}
ExtensionList<DisplayURLProvider> all = DisplayURLProvider.all();
DisplayURLProvider displayURLProvider = all.stream()
.filter(
((Predicate<DisplayURLProvider>) ClassicDisplayURLProvider.class::isInstance)
.negate())
.findFirst().orElse(DisplayURLProvider.getDefault());
}
return displayURLProvider;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public DisplayURLProvider getConfiguredProvider() {
.orElse(null);
}

public List<ProviderOption> getAll() {
public static List<ProviderOption> getAll() {
List<ProviderOption> options = DisplayURLProvider.all().stream()
.map(input -> new ProviderOption(input.getClass().getName(), input.getDisplayName()))
.collect(Collectors.toList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public UserProperty newInstance(User user) {
public ListBoxModel doFillProviderIdItems() {
ListBoxModel items = new ListBoxModel();
PreferredProviderUserProperty property = PreferredProviderUserProperty.forCurrentUser();
for (ProviderOption providerOption : property.getAll()) {
for (ProviderOption providerOption : PreferredProviderUserProperty.getAll()) {
ListBoxModel.Option option = new ListBoxModel.Option(
providerOption.getName(),
providerOption.getId(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:section title="${%Default notification URL handler}">
dwnusbaum marked this conversation as resolved.
Show resolved Hide resolved
<f:entry field="providerId">
<f:select/>
</f:entry>
</f:section>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div>
Select the default handler when clicking links to Jenkins from notifications (e.g. Email, Slack, or GitHub).
Individual users can override this value as desired in their preferences.
</div>
Loading