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-71715] Allow plugins with alternate build log visualizations to handle console links from core and other plugins #8321

Merged
merged 34 commits into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4862fa0
[JENKINS-71715] Allow plugins with alternate build log visualizations…
jgreffe Aug 1, 2023
ecc7e50
Merge branch 'master' into JENKINS-71715_first-approach
jgreffe Aug 1, 2023
d1f3aa2
Merge branch 'master' into JENKINS-71715_first-approach
jgreffe Aug 1, 2023
4316970
[JENKINS-71715] Apply suggestions with ordinal extension and @since TODO
jgreffe Aug 1, 2023
519adfd
[JENKINS-71715] Public @Extension
jgreffe Aug 2, 2023
618f538
Add or fix @since todo
batmat Aug 4, 2023
f8fa8fe
[JENKINS-71715] Apply suggestions from reviews
jgreffe Aug 4, 2023
a976eb6
[JENKINS-71715] Switch ConsoleURLProvider to interface and add Functi…
jgreffe Aug 4, 2023
d853004
[JENKINS-71715] Refactor ConsoleUrlProvider API and add Functions.get…
dwnusbaum Aug 4, 2023
2dd4e97
[JENKINS-71715] Fix casing of ConsoleURLProvider.java
dwnusbaum Aug 7, 2023
018789a
[JENKINS-71715] Add global and per-user configuration options for Con…
dwnusbaum Aug 7, 2023
3d9b41c
[JENKINS-71715] Fix Javadoc reference
dwnusbaum Aug 7, 2023
7305016
[JENKINS-71715] Remove outdated TODO comment
dwnusbaum Aug 7, 2023
6491cbb
Merge branch 'master' into JENKINS-71715_first-approach
dwnusbaum Aug 7, 2023
5f051c9
[JENKINS-71715] Miscellaneous updates based on review feedback
dwnusbaum Aug 8, 2023
e1b938b
[JENKINS-71715] Fix Javadoc links
dwnusbaum Aug 8, 2023
ae52b83
[JENKINS-71715] Update console links for build list and build time tr…
dwnusbaum Aug 9, 2023
46b9cda
[JENKINS-71715] Functions.getConsoleUrl must take Queue.Executable in…
dwnusbaum Aug 11, 2023
4f12b2b
[JENKINS-71715] Simplify test using WebClient.executeOnServer
dwnusbaum Aug 11, 2023
2ab2ea9
Merge branch 'master' into JENKINS-71715_first-approach
dwnusbaum Aug 18, 2023
c34df9d
Merge branch 'master' into JENKINS-71715_first-approach
dwnusbaum Aug 21, 2023
2f669da
[JENKINS-71715] Add constructor
jgreffe Aug 22, 2023
a7ac1fd
Merge branch 'master' into JENKINS-71715_first-approach
dwnusbaum Aug 22, 2023
f650e1f
Merge branch 'master' into JENKINS-71715_first-approach
jgreffe Sep 1, 2023
f8c0aac
Merge branch 'master' into JENKINS-71715_first-approach
dwnusbaum Sep 5, 2023
598ca1c
Merge branch 'master' into JENKINS-71715_first-approach
dwnusbaum Sep 15, 2023
848b20c
Merge branch 'master' into JENKINS-71715_first-approach
dwnusbaum Sep 26, 2023
48dcff8
[JENKINS-71715] Preserve old behavior of p:console-link as a fallback…
dwnusbaum Sep 26, 2023
b5420ad
[JENKINS-71715] Ignore invalid URLs and consult global providers if u…
dwnusbaum Sep 27, 2023
bcf46b6
[JENKINS-71715] Move ConsoleUrlProviderGlobalConfiguration into Appea…
dwnusbaum Sep 27, 2023
a4d5a21
[JENKINS-71715] Hide AppearanceGlobalConfiguration if ConsoleUrlProvi…
dwnusbaum Sep 28, 2023
64c3faa
[JENKINS-71715] Update help text and Javadoc based on review feedback…
dwnusbaum Oct 30, 2023
7e51da5
Merge branch 'master' into JENKINS-71715_first-approach
dwnusbaum Oct 30, 2023
88ea796
[JENKINS-71715] getProvidersDescriptors can be an instance method
dwnusbaum Oct 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions core/src/main/java/hudson/Functions.java
dwnusbaum marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jenkins.console.ConsoleUrlProvider;
import jenkins.model.GlobalConfiguration;
import jenkins.model.GlobalConfigurationCategory;
import jenkins.model.Jenkins;
Expand Down Expand Up @@ -1903,6 +1904,16 @@ public static String joinPath(String... components) {
return joinPath(Stapler.getCurrentRequest().getContextPath() + '/' + itUrl, urlName);
}

/**
* Computes the link to the console for the specified build, taking {@link ConsoleUrlProvider} into account.
* @param run the build
* @return the absolute URL for accessing the specified build's console
* @since TODO
*/
public static @CheckForNull String getConsoleUrl(Run<?, ?> run) {
Copy link
Member

Choose a reason for hiding this comment

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

This method is intended for use in Jelly files (both in core and in plugins). I think it is a little cleaner to put it here rather than directly on Run, but that is just my opinion.

return ConsoleUrlProvider.getRedirectUrl(run);
}

/**
* Escapes the character unsafe for e-mail address.
* See <a href="https://en.wikipedia.org/wiki/Email_address">the Wikipedia page</a> for the details,
Expand Down
90 changes: 90 additions & 0 deletions core/src/main/java/jenkins/console/ConsoleURLProvider.java
dwnusbaum marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* The MIT License
*
* Copyright (c) 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 jenkins.console;


import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.Functions;
import hudson.model.Run;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.Stapler;

/**
* Extension point that allows implementations to redirect build console links to a specified URL.
* <p>In order to produce links to console URLs in Jelly templates, use {@link Functions#getConsoleUrl()}.
* @see Functions#getConsoleUrl
* @since TODO
*/
public interface ConsoleUrlProvider extends ExtensionPoint {
@Restricted(NoExternalUse.class)
Logger LOGGER = Logger.getLogger(ConsoleUrlProvider.class.getName());

/**
* Get a URL relative to the context path of Jenkins which should be used to link to the console for the specified build.
* <p>Should only be used in the context of serving an HTTP request.
* @param run the build
* @return the URL for the console for the specified build, relative to the context of Jenkins, or {@code null}
* if this implementation does not want to server a special console view for this build.
*/
@CheckForNull String getConsoleUrl(Run<?, ?> run);

/**
* Get a URL relative to the web server root which should be used to link to the console for the specified build.
* <p>Should only be used in the context of serving an HTTP request.
* <p>Use {@link Functions#getConsoleUrl} to obtain this link in a Jelly template.
* @param run the build
* @return the URL for the console for the specified build, relative to the web server root
*/
static @NonNull String getRedirectUrl(Run<?, ?> run) {
Copy link
Member

Choose a reason for hiding this comment

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

This method is intended to be used directly in cases where Java code wants to redirect a request to the console as in jenkinsci/pipeline-input-step-plugin#145.

String url = null;
for (ConsoleUrlProvider provider : ExtensionList.lookup(ConsoleUrlProvider.class)) {
Copy link
Member

@dwnusbaum dwnusbaum Aug 4, 2023

Choose a reason for hiding this comment

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

@daniel-beck raised a few important points in #8321 (review) and I am not entirely sure how we should address them.

With my latest changes to the API, we can create implementations that only care about Pipelines, or builds with odd numbers, etc. However, if (for example) both Blue Ocean and Pipeline Graph View wanted to implement ConsoleUrlProvider for all Pipeline builds, and you have both plugins installed on a single controller, they would compete for precedence based on ordinal. How do we want to address this? I can see a few approaches:

  • Do nothing, let admins use https://plugins.jenkins.io/extension-filter/ as needed. Maybe this is good enough if we only expect a few overlapping implementations of this extension point, especially if we think the implementations are likely to come from competing plugins where admins are going to choose one instead of installing them all at the same time.
  • Provide a GlobalConfiguration UI in core to allow admins to select which ConsoleUrlProvider extensions should be enabled on a given controller, or allow them to be sorted based on the admin's preference. Same end result as above, though arguably more user-friendly, but it adds additional implementation complexity.
  • Provide a user-based configuration in core to let users select which ConsoleUrlProvider extensions they want to allow, or allow them to be sorted based on the user's preference. Adds even more implementation complexity than the above options, but now if there are two plugins installed with competing ConsoleUrlProvider implementations, individual users get to pick which one they want to use. (I think all of the links in question are generated dynamically during page rendering/request handling and so would be able to use the current user's preferences without significant technical issues, but perhaps it would be more complex than that.)

For now at least I need to update jenkinsci/pipeline-graph-view-plugin#288 and jenkinsci/pipeline-input-step-plugin#145 based on the latest changes.

Copy link
Member

Choose a reason for hiding this comment

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

I suspect you will want 2 and 3, admins to set a default and allow users to override it

Copy link
Member

Choose a reason for hiding this comment

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

Missed this thread and posted #8321 (comment). For now IMO option 2 should be enough (option 3 only if sufficient user demand exists).

Copy link
Member

Choose a reason for hiding this comment

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

I feel you'll get users that are used to it but some users who would want the other implementation, also with the display url only having a per user setting it seems weird for this one not to?

Copy link
Member

Choose a reason for hiding this comment

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

with the display url only having a per user setting

For what it's worth, related to this work we will add a global configuration to display-url-api to allow admins to pick the default setting for users who have not yet configured their own preference. (Right now I think the default behavior is effectively random if you have more than one DisplayUrlProvider extension on a controller.)

As far as whether we want to allow per-user settings here, my main hesitation is just that it adds complexity to core and I am not sure how useful it will be in practice. For the record, I don't know if it makes sense to actually implement the new API in Blue Ocean because I don't think that it is a sufficient replacement for the main console view in general.
That leaves pipeline-graph-view as the only OSS visualization plugin that I was planning on filing a PR against for now (let me know if I am missing something obvious). Beyond that, CloudBees has a proprietary visualization plugin that we would like to have implement the new API for most Pipelines. All that to say, I am not sure how often conflicts would occur in practice, at least for now.

If you see this API as one intended for admins to control the UX of their instance and guide users to a preferred console visualization, then having a global configuration alone seems fine. If you think it is just a matter of personal preference then a per-user configuration makes sense.

Copy link
Member

Choose a reason for hiding this comment

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

For a cloudbees plugin it may make more sense as a control of the whole instance.

In the case of pipeline graph view plugin someone may want to fallback to the default console view if it doesn't handle something for them or they have used it for 10+ years and do not want to change.

also PGV doesn't support freestyle builds at all and I haven't seen any handling for that in the downstream PR.

Copy link
Member

Choose a reason for hiding this comment

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

also PGV doesn't support freestyle builds at all and I haven't seen any handling for that in the downstream PR.

Yes sorry, all of the downstream PRs need to be updated. I will get to that later today.

Copy link
Member

Choose a reason for hiding this comment

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

See 018789a and let me know what you think. We can dial it back as desired.

Copy link
Member

Choose a reason for hiding this comment

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

this looks good to me

try {
url = provider.getConsoleUrl(run);
if (url != null) {
break;
}
} catch (Exception e) { // Intentionally broad catch clause to guard against broken implementations.
LOGGER.log(Level.WARNING, e, () -> "Error looking up console URL for " + run + " from " + provider.getClass());
}
}
if (url == null) {
url = run.getUrl() + "console";
}
// TODO:
// * Fail if absolute URL?
// * Fail if invalid URI (as above in getActionUrl?).
if (url.startsWith("/")) {
return Stapler.getCurrentRequest().getContextPath() + url;
} else {
return Stapler.getCurrentRequest().getContextPath() + '/' + url;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ THE SOFTWARE.
${%Not yet determined}
</j:when>
<j:otherwise>
${%Failed to determine} (<a href="console">${%log}</a>)
${%Failed to determine} (<a href="${h.getConsoleUrl(it)}">${%log}</a>)
dwnusbaum marked this conversation as resolved.
Show resolved Hide resolved
</j:otherwise>
</j:choose>
</l:main-panel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ THE SOFTWARE.
${%Not yet determined}
</j:when>
<j:otherwise>
${%Failed to determine} (<a href="console">${%log}</a>)
${%Failed to determine} (<a href="${h.getConsoleUrl(it)}">${%log}</a>)
</j:otherwise>
</j:choose>
</t:summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ THE SOFTWARE.
<td class="build-row-cell">
<div class="pane build-name">
<div class="build-icon">
<a class="build-status-link" href="${link}console" tooltip="${build.iconColor.description} > ${%Console Output}">
<a class="build-status-link" href="${h.getConsoleUrl(build)}" tooltip="${build.iconColor.description} > ${%Console Output}">
<l:icon class="${build.buildStatusIconClassName} icon-sm" />
</a>
</div>
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/resources/lib/hudson/buildProgressBar.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ THE SOFTWARE.
<st:documentation>
Progress bar for a build in progress.

<!-- TODO: Do Functions.getConsoleUrl and ConsoleUrlProvider need to be adapted to use Queue.Executable instead of Run? -->
dwnusbaum marked this conversation as resolved.
Show resolved Hide resolved
<st:attribute name="build" use="required" type="hudson.model.Queue.Executable">
Build in progress. Must have a url property.
</st:attribute>
Expand All @@ -36,5 +37,5 @@ THE SOFTWARE.
<j:set var="executor" value="${executor?:build.executor}" /> <!-- TODO use Executor.of -->
<t:progressBar tooltip="${%text(executor.timestampString,executor.estimatedRemainingTime)}"
red="${executor.isLikelyStuck()}"
pos="${executor.progress}" href="${rootURL}/${build.url}console"/>
pos="${executor.progress}" href="${h.getConsoleUrl(build)}"/>
</j:jelly>
4 changes: 2 additions & 2 deletions core/src/main/resources/lib/hudson/project/console-link.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ THE SOFTWARE.
<j:choose>
<j:when test="${it.logFile.length() > 200000}">
<!-- Show raw link directly so user need not click through live console page, though this is not so bad now as they would just see: Skipping nnn KB.. Full Log. -->
<l:task href="${buildUrl.baseUrl}/console" icon="icon-terminal icon-md" title="${%Console Output}"/>
<l:task href="${h.getConsoleUrl(it)}" icon="icon-terminal icon-md" title="${%Console Output}"/>
<l:task href="${buildUrl.baseUrl}/consoleText" icon="icon-document icon-md" title="${%View as plain text}"/>
Copy link
Member

@dwnusbaum dwnusbaum Aug 4, 2023

Choose a reason for hiding this comment

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

I am not entirely sure what we want to do about this case. I think it makes sense to redirect the main "console output" link, but IDK if we should force-show the "View as plain text" link when the console URL is non-default or something like that.

</j:when>
<j:otherwise>
<l:task icon="images/svgs/terminal.svg" href="${buildUrl.baseUrl}/console" title="${%Console Output}">
<l:task icon="images/svgs/terminal.svg" href="${h.getConsoleUrl(it)}" title="${%Console Output}">
<l:task href="${buildUrl.baseUrl}/consoleText" icon="icon-document icon-md" title="${%View as plain text}"/>
</l:task>
</j:otherwise>
Expand Down
93 changes: 93 additions & 0 deletions test/src/test/java/jenkins/console/ConsoleUrlProviderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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 jenkins.console;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.not;

import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Run;
import org.htmlunit.html.DomElement;
import org.htmlunit.html.HtmlPage;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.TestExtension;

public class ConsoleUrlProviderTest {
@ClassRule
public static BuildWatcher watcher = new BuildWatcher();

@Rule
public JenkinsRule r = new JenkinsRule();

@Test
public void getConsoleUrl() throws Exception {
FreeStyleProject p = r.createProject(FreeStyleProject.class);
// Default URL
FreeStyleBuild b1 = r.buildAndAssertSuccess(p);
assertCustomConsoleUrl(r.contextPath + '/' + b1.getUrl() + "console", b1);
// Custom URL without leading slash
FreeStyleBuild b2 = r.buildAndAssertSuccess(p);
b2.setDescription("custom my/build/console");
assertCustomConsoleUrl(r.contextPath + "/my/build/console", b2);
// Custom URL with leading slash
FreeStyleBuild b3 = r.buildAndAssertSuccess(p);
b3.setDescription("custom /my/build/console");
assertCustomConsoleUrl(r.contextPath + "/my/build/console", b3);
// Default URL is used when extensions throw exceptions.
FreeStyleBuild b4 = r.buildAndAssertSuccess(p);
b4.setDescription("NullPointerException");
assertCustomConsoleUrl(r.contextPath + '/' + b4.getUrl() + "console", b4);
}

// Awkward, but we can only call Functions.getConsoleUrl in the context of an HTTP request.
public void assertCustomConsoleUrl(String expectedUrl, Run<?, ?> run) throws Exception {
HtmlPage page = r.createWebClient().getPage(run.getParent());
DomElement buildHistoryDiv = page.getElementById("buildHistory");
assertThat("Console link for " + run + " should be " + expectedUrl,
buildHistoryDiv.getByXPath("//a[@href='" + expectedUrl + "']"), not(empty()));
}

@TestExtension("getConsoleUrl")
public static class CustomConsoleUrlProvider implements ConsoleUrlProvider {
@Override
public String getConsoleUrl(Run<?, ?> run) {
String description = run.getDescription();
if (description == null) {
return null;
} else if (description.startsWith("custom ")) {
return description.substring("custom ".length());
} else {
throw new NullPointerException("getConsoleUrl should be robust against runtime errors");
}
}
}

}