diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/StageNodeLink.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/StageNodeLink.tsx new file mode 100644 index 000000000..6c5a6b02f --- /dev/null +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/StageNodeLink.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +export interface StageNodeLinkProps { + agent: string; +} + +function getAgentUrl(name: string) { + // Wrap built-in in brackets + const id = name == "built-in" ? "(built-in)" : name; + const rootPath = document.head.dataset.rooturl + return `${rootPath}/computer/${id}/`; +} + +const StageNodeLink = ({agent}: StageNodeLinkProps) => { + const agentName = agent == "built-in" ? "Jenkins" : agent; + const href = getAgentUrl(agent); + return <> + Running on {agentName} + +}; + +export default StageNodeLink; diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.tsx index da4e2c506..d1033e696 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.tsx @@ -6,6 +6,7 @@ import ScheduleIcon from "@mui/icons-material/Schedule"; import TimerIcon from "@mui/icons-material/Timer"; import InfoIcon from "@mui/icons-material/Info"; import LinkIcon from "@mui/icons-material/Link"; +import ComputerIcon from "@mui/icons-material/Computer"; import { StepInfo, @@ -14,6 +15,7 @@ import { LOG_FETCH_SIZE, } from "./PipelineConsoleModel"; import { ConsoleLogCard } from "./ConsoleLogCard"; +import StageNodeLink from "./StageNodeLink"; export interface StageSummaryProps { stage: StageInfo; @@ -81,6 +83,22 @@ const StageSummary = (props: StageSummaryProps) => ( {props.stage.state} + {props.stage.agent && ( +
+ + + + +
+ )} {props.failedSteps.map((value: StepInfo) => { console.debug(`Found failed step ${value}`); return ( diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/TestData.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/TestData.tsx index 66bc0942c..f7e7d1ce0 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/TestData.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/TestData.tsx @@ -1,5 +1,5 @@ import { Result, StageType, StageInfo, StepInfo } from "./PipelineConsoleModel"; -export const defaultStagesList = [ +export const defaultStagesList: StageInfo[] = [ { name: "Stage A", title: "", @@ -11,6 +11,7 @@ export const defaultStagesList = [ pauseDurationMillis: "", startTimeMillis: "", totalDurationMillis: "", + agent: "built-in", }, { name: "Stage B", @@ -23,6 +24,7 @@ export const defaultStagesList = [ pauseDurationMillis: "", startTimeMillis: "", totalDurationMillis: "", + agent: "not-built-in", }, { name: "Parent C", @@ -43,11 +45,13 @@ export const defaultStagesList = [ pauseDurationMillis: "", startTimeMillis: "", totalDurationMillis: "", + agent: "not-built-in", }, ], pauseDurationMillis: "", startTimeMillis: "", totalDurationMillis: "", + agent: "built-in", }, ]; diff --git a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.spec.ts b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.spec.ts index 44f150214..c17adf588 100644 --- a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.spec.ts +++ b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.spec.ts @@ -14,6 +14,7 @@ describe("PipelineGraphLayout", () => { pauseDurationMillis: "", startTimeMillis: "", totalDurationMillis: "", + agent: "built-in", }; const makeStage = ( diff --git a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx index 3140846fa..7ae5b288b 100644 --- a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx +++ b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx @@ -62,6 +62,7 @@ export interface StageInfo { pauseDurationMillis: string; startTimeMillis: string; totalDurationMillis: string; + agent: string; } interface BaseNodeInfo { diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java index 80fb90d1a..7f2539e09 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java @@ -8,18 +8,16 @@ import hudson.model.Queue; import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeGraphAdapter; import io.jenkins.plugins.pipelinegraphview.utils.legacy.PipelineNodeGraphVisitor; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.io.IOException; +import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.function.Function; import java.util.stream.Collectors; +import org.jenkinsci.plugins.workflow.actions.WorkspaceAction; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,7 +68,8 @@ private List getPipelineNodes(PipelineGraphBuilderApi bui flowNodeWrapper.getType().name(), flowNodeWrapper.getDisplayName(), // TODO blue ocean uses timing information: "Passed in 0s" flowNodeWrapper.isSynthetic(), - flowNodeWrapper.getTiming()); + flowNodeWrapper.getTiming(), + getStageNode(flowNodeWrapper)); }) .collect(Collectors.toList()); } @@ -197,7 +196,7 @@ private PipelineGraph createShallowTree(PipelineGraphBuilderApi builder) { stageToChildrenMap.put(stage.getId(), new ArrayList<>()); topLevelStageIds.add(stage.getId()); } - } catch (java.io.IOException ex) { + } catch (IOException ex) { logger.error("Caught a " + ex.getClass().getSimpleName() + " when trying to find parent of stage '" @@ -233,6 +232,49 @@ private List getAncestors(PipelineStageInternal stage, Map getChildren() { public boolean isSynthetic() { return synthetic; } + + public String getAgent() { + return agent; + } } diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java index 7e3aa83ef..99ba5f185 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java @@ -19,6 +19,7 @@ public class PipelineStageInternal { private boolean sequential; private boolean synthetic; private TimingInfo timingInfo; + private String agent; public PipelineStageInternal( String id, @@ -29,7 +30,8 @@ public PipelineStageInternal( String type, String title, boolean synthetic, - TimingInfo times) { + TimingInfo times, + String agent) { this.id = id; this.name = name; this.parents = parents; @@ -39,6 +41,7 @@ public PipelineStageInternal( this.title = title; this.synthetic = synthetic; this.timingInfo = times; + this.agent = agent; } public boolean isSequential() { @@ -121,6 +124,14 @@ public void setSynthetic(boolean synthetic) { this.synthetic = synthetic; } + public String getAgent() { + return agent; + } + + public void setAgent(String aAgent) { + this.agent = aAgent; + } + public PipelineStage toPipelineStage(List children) { return new PipelineStage( id, @@ -134,6 +145,7 @@ public PipelineStage toPipelineStage(List children) { nextSibling != null ? nextSibling.toPipelineStage(Collections.emptyList()) : null, sequential, synthetic, - timingInfo); + timingInfo, + agent); } } diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java index b82e8aaa3..f8849c135 100644 --- a/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java +++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java @@ -4,7 +4,9 @@ import static org.hamcrest.Matchers.*; import hudson.model.Result; +import hudson.model.labels.LabelAtom; import hudson.model.queue.QueueTaskFuture; +import hudson.slaves.DumbSlave; import io.jenkins.plugins.pipelinegraphview.treescanner.NodeRelationshipFinder; import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeGraphAdapter; import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeTreeScanner; @@ -408,4 +410,76 @@ public void gh_358_parallelStagesMarkedAsSkipped() throws Exception { equalTo( "foo{success},first-parallel{failure}[bar{skipped},baz{failure}],second-parallel{skipped},Post Actions{success}")); } + + @Test + public void getAgentForSingleStagePipeline() throws Exception { + WorkflowRun run = TestUtils.createAndRunJob( + j, "getAgentForSingleStagePipeline", "singleStagePipeline.jenkinsfile", Result.SUCCESS); + + List stages = new PipelineGraphApi(run).createTree().getStages(); + + assertThat(stages.size(), equalTo(1)); + assertThat(stages.get(0).getAgent(), equalTo("built-in")); + } + + @Test + public void getAgentForSingleStagePipelineWithExternalAgent() throws Exception { + var testingLabel = new LabelAtom("external"); + DumbSlave agent = j.createSlave(testingLabel); + j.waitOnline(agent); + + WorkflowRun run = TestUtils.createAndRunJob( + j, + "getAgentForSingleStagePipelineWithExternalAgent", + "singleStagePipelineWithExternalAgent.jenkinsfile", + Result.SUCCESS); + + List stages = new PipelineGraphApi(run).createTree().getStages(); + + assertThat(stages.size(), equalTo(1)); + assertThat(stages.get(0).getAgent(), equalTo(agent.getNodeName())); + } + + @Test + public void getAgentForParallelPipelineWithExternalAgent() throws Exception { + var testingLabel = new LabelAtom("external"); + DumbSlave agent = j.createSlave(testingLabel); + j.waitOnline(agent); + + WorkflowRun run = TestUtils.createAndRunJob( + j, + "getAgentForParallelPipelineWithExternalAgent", + "parallelPipelineWithExternalAgent.jenkinsfile", + Result.SUCCESS); + + List stages = new PipelineGraphApi(run).createTree().getStages(); + + // Parallel pipeline structure: + // name: Parallel, type: STAGE + // name: Parallel, type: PARALLEL_BLOCK + + // name: Builtin, type: PARALLEL + // name: Stage : Start, type: STEPS_BLOCK + // name: Builtin, type: STAGE + // name: Allocate node : Start, type: STEPS_BLOCK + + assertThat(stages.size(), equalTo(1)); + assertThat(stages.get(0).getType(), equalTo("STAGE")); + assertThat(stages.get(0).getName(), equalTo("Parallel")); + assertThat(stages.get(0).getAgent(), equalTo(null)); + + List children = stages.get(0).getChildren(); + + assertThat(children.size(), equalTo(2)); + + PipelineStage builtinStage = children.get(0); + assertThat(builtinStage.getType(), equalTo("PARALLEL")); + assertThat(builtinStage.getName(), equalTo("Builtin")); + assertThat(builtinStage.getAgent(), equalTo("built-in")); + + PipelineStage externalStage = children.get(1); + assertThat(externalStage.getType(), equalTo("PARALLEL")); + assertThat(externalStage.getName(), equalTo("External")); + assertThat(externalStage.getAgent(), equalTo(agent.getNodeName())); + } } diff --git a/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/parallelPipelineWithExternalAgent.jenkinsfile b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/parallelPipelineWithExternalAgent.jenkinsfile new file mode 100644 index 000000000..5acc3e7bc --- /dev/null +++ b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/parallelPipelineWithExternalAgent.jenkinsfile @@ -0,0 +1,21 @@ +pipeline { + agent none + stages { + stage('Parallel') { + parallel { + stage('Builtin') { + agent { label 'built-in' } + steps { + echo "Hello, from home" + } + } + stage('External') { + agent { label 'external' } + steps { + echo "Hello, from far away" + } + } + } + } + } +} diff --git a/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipeline.jenkinsfile b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipeline.jenkinsfile new file mode 100644 index 000000000..b3bcb5501 --- /dev/null +++ b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipeline.jenkinsfile @@ -0,0 +1,10 @@ +pipeline { + agent any + stages { + stage('Hello') { + steps { + echo "Hello, world" + } + } + } +} diff --git a/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipelineWithExternalAgent.jenkinsfile b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipelineWithExternalAgent.jenkinsfile new file mode 100644 index 000000000..a477fe488 --- /dev/null +++ b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipelineWithExternalAgent.jenkinsfile @@ -0,0 +1,11 @@ +pipeline { + agent any + stages { + stage('Hello') { + agent { label 'external' } + steps { + echo "Hello, world" + } + } + } +}