Skip to content

Commit 81e4460

Browse files
Hammietimja
andauthored
Add link to agent a stage is running on to stage summary (#495)
Co-authored-by: Tim Jacomb <[email protected]>
1 parent 6e8fdb5 commit 81e4460

File tree

12 files changed

+235
-12
lines changed

12 files changed

+235
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from "react";
2+
3+
export interface StageNodeLinkProps {
4+
agent: string;
5+
}
6+
7+
function getAgentUrl(name: string) {
8+
// Wrap built-in in brackets
9+
const id = name == "built-in" ? "(built-in)" : name;
10+
const rootPath = document.head.dataset.rooturl
11+
return `${rootPath}/computer/${id}/`;
12+
}
13+
14+
const StageNodeLink = ({agent}: StageNodeLinkProps) => {
15+
const agentName = agent == "built-in" ? "Jenkins" : agent;
16+
const href = getAgentUrl(agent);
17+
return <>
18+
Running on <a href={href}>{agentName}</a>
19+
</>
20+
};
21+
22+
export default StageNodeLink;

src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ScheduleIcon from "@mui/icons-material/Schedule";
66
import TimerIcon from "@mui/icons-material/Timer";
77
import InfoIcon from "@mui/icons-material/Info";
88
import LinkIcon from "@mui/icons-material/Link";
9+
import ComputerIcon from "@mui/icons-material/Computer";
910

1011
import {
1112
StepInfo,
@@ -14,6 +15,7 @@ import {
1415
LOG_FETCH_SIZE,
1516
} from "./PipelineConsoleModel";
1617
import { ConsoleLogCard } from "./ConsoleLogCard";
18+
import StageNodeLink from "./StageNodeLink";
1719

1820
export interface StageSummaryProps {
1921
stage: StageInfo;
@@ -81,6 +83,22 @@ const StageSummary = (props: StageSummaryProps) => (
8183
{props.stage.state}
8284
</span>
8385
</div>
86+
{props.stage.agent && (
87+
<div
88+
className="detail-element"
89+
key={`stage-detail-agent-container-${props.stage.id}`}
90+
>
91+
<ComputerIcon
92+
className="detail-icon"
93+
key={`stage-detail-agent-icon-${props.stage.id}`}
94+
/>
95+
<span
96+
key={`stage-detail-agent-text-${props.stage.id}`}
97+
>
98+
<StageNodeLink agent={props.stage.agent} />
99+
</span>
100+
</div>
101+
)}
84102
{props.failedSteps.map((value: StepInfo) => {
85103
console.debug(`Found failed step ${value}`);
86104
return (

src/main/frontend/pipeline-console-view/pipeline-console/main/TestData.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Result, StageType, StageInfo, StepInfo } from "./PipelineConsoleModel";
2-
export const defaultStagesList = [
2+
export const defaultStagesList: StageInfo[] = [
33
{
44
name: "Stage A",
55
title: "",
@@ -11,6 +11,7 @@ export const defaultStagesList = [
1111
pauseDurationMillis: "",
1212
startTimeMillis: "",
1313
totalDurationMillis: "",
14+
agent: "built-in",
1415
},
1516
{
1617
name: "Stage B",
@@ -23,6 +24,7 @@ export const defaultStagesList = [
2324
pauseDurationMillis: "",
2425
startTimeMillis: "",
2526
totalDurationMillis: "",
27+
agent: "not-built-in",
2628
},
2729
{
2830
name: "Parent C",
@@ -43,11 +45,13 @@ export const defaultStagesList = [
4345
pauseDurationMillis: "",
4446
startTimeMillis: "",
4547
totalDurationMillis: "",
48+
agent: "not-built-in",
4649
},
4750
],
4851
pauseDurationMillis: "",
4952
startTimeMillis: "",
5053
totalDurationMillis: "",
54+
agent: "built-in",
5155
},
5256
];
5357

src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe("PipelineGraphLayout", () => {
1414
pauseDurationMillis: "",
1515
startTimeMillis: "",
1616
totalDurationMillis: "",
17+
agent: "built-in",
1718
};
1819

1920
const makeStage = (

src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export interface StageInfo {
6262
pauseDurationMillis: string;
6363
startTimeMillis: string;
6464
totalDurationMillis: string;
65+
agent: string;
6566
}
6667

6768
interface BaseNodeInfo {

src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java

+50-8
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,16 @@
88
import hudson.model.Queue;
99
import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeGraphAdapter;
1010
import io.jenkins.plugins.pipelinegraphview.utils.legacy.PipelineNodeGraphVisitor;
11-
import java.util.ArrayList;
12-
import java.util.HashMap;
13-
import java.util.LinkedHashMap;
14-
import java.util.List;
15-
import java.util.Locale;
16-
import java.util.Map;
11+
import java.io.IOException;
12+
import java.util.*;
1713
import java.util.concurrent.ExecutionException;
1814
import java.util.concurrent.TimeoutException;
1915
import java.util.function.Function;
2016
import java.util.stream.Collectors;
17+
import org.jenkinsci.plugins.workflow.actions.WorkspaceAction;
2118
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
2219
import org.jenkinsci.plugins.workflow.graph.FlowNode;
20+
import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner;
2321
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
2422
import org.slf4j.Logger;
2523
import org.slf4j.LoggerFactory;
@@ -70,7 +68,8 @@ private List<PipelineStageInternal> getPipelineNodes(PipelineGraphBuilderApi bui
7068
flowNodeWrapper.getType().name(),
7169
flowNodeWrapper.getDisplayName(), // TODO blue ocean uses timing information: "Passed in 0s"
7270
flowNodeWrapper.isSynthetic(),
73-
flowNodeWrapper.getTiming());
71+
flowNodeWrapper.getTiming(),
72+
getStageNode(flowNodeWrapper));
7473
})
7574
.collect(Collectors.toList());
7675
}
@@ -197,7 +196,7 @@ private PipelineGraph createShallowTree(PipelineGraphBuilderApi builder) {
197196
stageToChildrenMap.put(stage.getId(), new ArrayList<>());
198197
topLevelStageIds.add(stage.getId());
199198
}
200-
} catch (java.io.IOException ex) {
199+
} catch (IOException ex) {
201200
logger.error("Caught a "
202201
+ ex.getClass().getSimpleName()
203202
+ " when trying to find parent of stage '"
@@ -233,6 +232,49 @@ private List<String> getAncestors(PipelineStageInternal stage, Map<String, Pipel
233232
return ancestors;
234233
}
235234

235+
private static String getStageNode(FlowNodeWrapper flowNodeWrapper) {
236+
FlowNode flowNode = flowNodeWrapper.getNode();
237+
DepthFirstScanner scan = new DepthFirstScanner();
238+
logger.debug("Checking node {}", flowNode);
239+
FlowExecution execution = flowNode.getExecution();
240+
for (FlowNode n : scan.allNodes(execution)) {
241+
WorkspaceAction ws = n.getAction(WorkspaceAction.class);
242+
if (ws != null) {
243+
logger.debug("Found workspace node: {}", n);
244+
boolean isWorkspaceNode = Objects.equals(n.getId(), flowNode.getId())
245+
|| Objects.equals(n.getEnclosingId(), flowNode.getId())
246+
|| flowNode.getAllEnclosingIds().contains(n.getId());
247+
248+
// Parallel stages have a sub-stage, so we need to check the 3rd parent for a match
249+
if (flowNodeWrapper.getType() == FlowNodeWrapper.NodeType.PARALLEL) {
250+
try {
251+
if (n.getEnclosingId() != null) {
252+
FlowNode p = execution.getNode(n.getEnclosingId());
253+
if (p != null && p.getEnclosingId() != null) {
254+
p = execution.getNode(p.getEnclosingId());
255+
if (p != null && p.getEnclosingId() != null) {
256+
isWorkspaceNode = Objects.equals(flowNode.getId(), p.getEnclosingId());
257+
}
258+
}
259+
}
260+
} catch (IOException e) {
261+
throw new RuntimeException(e);
262+
}
263+
}
264+
265+
if (isWorkspaceNode) {
266+
logger.debug("Found correct stage node: {}", n.getId());
267+
String node = ws.getNode();
268+
if (node.isEmpty()) {
269+
node = "built-in";
270+
}
271+
return node;
272+
}
273+
}
274+
}
275+
return null;
276+
}
277+
236278
public PipelineGraph createTree() {
237279
return createTree(new PipelineNodeGraphAdapter(run));
238280
}

src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStage.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class PipelineStage extends AbstractPipelineNode {
1010
private final PipelineStage nextSibling;
1111
private boolean sequential;
1212
private boolean synthetic;
13+
private String agent;
1314

1415
public PipelineStage(
1516
String id,
@@ -23,13 +24,15 @@ public PipelineStage(
2324
PipelineStage nextSibling,
2425
boolean sequential,
2526
boolean synthetic,
26-
TimingInfo timingInfo) {
27+
TimingInfo timingInfo,
28+
String agent) {
2729
super(id, name, state, completePercent, type, title, timingInfo);
2830
this.children = children;
2931
this.seqContainerName = seqContainerName;
3032
this.nextSibling = nextSibling;
3133
this.sequential = sequential;
3234
this.synthetic = synthetic;
35+
this.agent = agent;
3336
}
3437

3538
public PipelineStage getNextSibling() {
@@ -53,4 +56,8 @@ public List<PipelineStage> getChildren() {
5356
public boolean isSynthetic() {
5457
return synthetic;
5558
}
59+
60+
public String getAgent() {
61+
return agent;
62+
}
5663
}

src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java

+14-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class PipelineStageInternal {
1919
private boolean sequential;
2020
private boolean synthetic;
2121
private TimingInfo timingInfo;
22+
private String agent;
2223

2324
public PipelineStageInternal(
2425
String id,
@@ -29,7 +30,8 @@ public PipelineStageInternal(
2930
String type,
3031
String title,
3132
boolean synthetic,
32-
TimingInfo times) {
33+
TimingInfo times,
34+
String agent) {
3335
this.id = id;
3436
this.name = name;
3537
this.parents = parents;
@@ -39,6 +41,7 @@ public PipelineStageInternal(
3941
this.title = title;
4042
this.synthetic = synthetic;
4143
this.timingInfo = times;
44+
this.agent = agent;
4245
}
4346

4447
public boolean isSequential() {
@@ -121,6 +124,14 @@ public void setSynthetic(boolean synthetic) {
121124
this.synthetic = synthetic;
122125
}
123126

127+
public String getAgent() {
128+
return agent;
129+
}
130+
131+
public void setAgent(String aAgent) {
132+
this.agent = aAgent;
133+
}
134+
124135
public PipelineStage toPipelineStage(List<PipelineStage> children) {
125136
return new PipelineStage(
126137
id,
@@ -134,6 +145,7 @@ public PipelineStage toPipelineStage(List<PipelineStage> children) {
134145
nextSibling != null ? nextSibling.toPipelineStage(Collections.emptyList()) : null,
135146
sequential,
136147
synthetic,
137-
timingInfo);
148+
timingInfo,
149+
agent);
138150
}
139151
}

src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java

+74
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import static org.hamcrest.Matchers.*;
55

66
import hudson.model.Result;
7+
import hudson.model.labels.LabelAtom;
78
import hudson.model.queue.QueueTaskFuture;
9+
import hudson.slaves.DumbSlave;
810
import io.jenkins.plugins.pipelinegraphview.treescanner.NodeRelationshipFinder;
911
import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeGraphAdapter;
1012
import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeTreeScanner;
@@ -408,4 +410,76 @@ public void gh_358_parallelStagesMarkedAsSkipped() throws Exception {
408410
equalTo(
409411
"foo{success},first-parallel{failure}[bar{skipped},baz{failure}],second-parallel{skipped},Post Actions{success}"));
410412
}
413+
414+
@Test
415+
public void getAgentForSingleStagePipeline() throws Exception {
416+
WorkflowRun run = TestUtils.createAndRunJob(
417+
j, "getAgentForSingleStagePipeline", "singleStagePipeline.jenkinsfile", Result.SUCCESS);
418+
419+
List<PipelineStage> stages = new PipelineGraphApi(run).createTree().getStages();
420+
421+
assertThat(stages.size(), equalTo(1));
422+
assertThat(stages.get(0).getAgent(), equalTo("built-in"));
423+
}
424+
425+
@Test
426+
public void getAgentForSingleStagePipelineWithExternalAgent() throws Exception {
427+
var testingLabel = new LabelAtom("external");
428+
DumbSlave agent = j.createSlave(testingLabel);
429+
j.waitOnline(agent);
430+
431+
WorkflowRun run = TestUtils.createAndRunJob(
432+
j,
433+
"getAgentForSingleStagePipelineWithExternalAgent",
434+
"singleStagePipelineWithExternalAgent.jenkinsfile",
435+
Result.SUCCESS);
436+
437+
List<PipelineStage> stages = new PipelineGraphApi(run).createTree().getStages();
438+
439+
assertThat(stages.size(), equalTo(1));
440+
assertThat(stages.get(0).getAgent(), equalTo(agent.getNodeName()));
441+
}
442+
443+
@Test
444+
public void getAgentForParallelPipelineWithExternalAgent() throws Exception {
445+
var testingLabel = new LabelAtom("external");
446+
DumbSlave agent = j.createSlave(testingLabel);
447+
j.waitOnline(agent);
448+
449+
WorkflowRun run = TestUtils.createAndRunJob(
450+
j,
451+
"getAgentForParallelPipelineWithExternalAgent",
452+
"parallelPipelineWithExternalAgent.jenkinsfile",
453+
Result.SUCCESS);
454+
455+
List<PipelineStage> stages = new PipelineGraphApi(run).createTree().getStages();
456+
457+
// Parallel pipeline structure:
458+
// name: Parallel, type: STAGE
459+
// name: Parallel, type: PARALLEL_BLOCK
460+
461+
// name: Builtin, type: PARALLEL
462+
// name: Stage : Start, type: STEPS_BLOCK
463+
// name: Builtin, type: STAGE
464+
// name: Allocate node : Start, type: STEPS_BLOCK
465+
466+
assertThat(stages.size(), equalTo(1));
467+
assertThat(stages.get(0).getType(), equalTo("STAGE"));
468+
assertThat(stages.get(0).getName(), equalTo("Parallel"));
469+
assertThat(stages.get(0).getAgent(), equalTo(null));
470+
471+
List<PipelineStage> children = stages.get(0).getChildren();
472+
473+
assertThat(children.size(), equalTo(2));
474+
475+
PipelineStage builtinStage = children.get(0);
476+
assertThat(builtinStage.getType(), equalTo("PARALLEL"));
477+
assertThat(builtinStage.getName(), equalTo("Builtin"));
478+
assertThat(builtinStage.getAgent(), equalTo("built-in"));
479+
480+
PipelineStage externalStage = children.get(1);
481+
assertThat(externalStage.getType(), equalTo("PARALLEL"));
482+
assertThat(externalStage.getName(), equalTo("External"));
483+
assertThat(externalStage.getAgent(), equalTo(agent.getNodeName()));
484+
}
411485
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
pipeline {
2+
agent none
3+
stages {
4+
stage('Parallel') {
5+
parallel {
6+
stage('Builtin') {
7+
agent { label 'built-in' }
8+
steps {
9+
echo "Hello, from home"
10+
}
11+
}
12+
stage('External') {
13+
agent { label 'external' }
14+
steps {
15+
echo "Hello, from far away"
16+
}
17+
}
18+
}
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)