Skip to content

Commit 5e719e2

Browse files
authored
feat(front): partial pipeline rebuild in UI (#414)
## πŸ“ Description Related task: #29 ## βœ… Checklist - [x] I have tested this change - [ ] This change requires documentation update
1 parent 583ca4b commit 5e719e2

File tree

12 files changed

+225
-1
lines changed

12 files changed

+225
-1
lines changed

β€Žfront/assets/js/workflow_view/interactive_pipeline_tree.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export var InteractivePipelineTree = {
1111
init: function(opts = {}) {
1212
InteractivePipelineTree.handleWorkflowTreeItemClicks(opts);
1313
InteractivePipelineTree.handlePipelineStopClicks();
14+
InteractivePipelineTree.handlePipelineRebuildClicks();
1415
InteractivePipelineTree.handleToggleSkippedBlocksClicks();
1516
},
1617

@@ -41,6 +42,35 @@ export var InteractivePipelineTree = {
4142
});
4243
},
4344

45+
handlePipelineRebuildClicks: function() {
46+
$("body").on("click", "[pipeline-rebuild-button]", function(event) {
47+
event.preventDefault();
48+
let button = $(event.currentTarget);
49+
let href = button.attr("href");
50+
button.text("Rebuilding...")
51+
button.attr("disabled", true);
52+
53+
let req = $.ajax({
54+
url: href,
55+
type: "POST",
56+
beforeSend: function(xhr) {
57+
xhr.setRequestHeader("X-CSRF-Token", $("meta[name='csrf-token']").attr("content"));
58+
}
59+
});
60+
61+
req.done(function(data) {
62+
if(data.error != undefined) {
63+
Notice.error(data.error)
64+
button.text("Rebuild Pipeline")
65+
button.attr("disabled", false);
66+
} else {
67+
Notice.notice(data.message)
68+
button.remove();
69+
}
70+
})
71+
});
72+
},
73+
4474
onWorkflowTreeItemClick: function(event) {
4575
let pipelineId = $(event.currentTarget).data("pipeline-id");
4676

β€Žfront/lib/front/clients/pipeline.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,28 @@ defmodule Front.Clients.Pipeline do
184184
response
185185
end)
186186
end
187+
188+
def partial_rebuild(request) do
189+
Watchman.benchmark("pipeline.partial_rebuild.duration", fn ->
190+
response =
191+
channel()
192+
|> Stub.partial_rebuild(request, metadata: metadata(), timeout: timeout())
193+
194+
case response do
195+
{:ok, _} -> Watchman.increment("pipeline.partial_rebuild.success")
196+
{:error, _} -> Watchman.increment("pipeline.partial_rebuild.failure")
197+
end
198+
199+
Logger.debug(fn ->
200+
"""
201+
Pipeline API partial_rebuild returned response
202+
#{inspect(response)}
203+
for request
204+
#{inspect(request)}
205+
"""
206+
end)
207+
208+
response
209+
end)
210+
end
187211
end

β€Žfront/lib/front/models/pipeline.ex

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ defmodule Front.Models.Pipeline do
1414
DescribeTopologyRequest,
1515
ListKeysetRequest,
1616
ListRequest,
17+
PartialRebuildRequest,
1718
Pipeline,
1819
TerminateRequest
1920
}
@@ -349,6 +350,23 @@ defmodule Front.Models.Pipeline do
349350
end
350351
end
351352

353+
def rebuild(id, requester_id, _tracing_headers \\ nil) do
354+
request =
355+
PartialRebuildRequest.new(
356+
ppl_id: id,
357+
user_id: requester_id,
358+
request_token: UUID.uuid4()
359+
)
360+
361+
{:ok, response} = Clients.Pipeline.partial_rebuild(request)
362+
363+
case ResponseCode.key(response.response_status.code) do
364+
:OK -> {:ok, response.ppl_id}
365+
:BAD_PARAM -> {:error, response.response_status.message}
366+
_ -> {:error, "Failed to rebuild pipeline"}
367+
end
368+
end
369+
352370
defp request_stream(req, tracing_headers, override \\ nil) do
353371
request(req, tracing_headers) |> stream_if_needed(override)
354372
end

β€Žfront/lib/front_web/controllers/pipeline_controller.ex

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ defmodule FrontWeb.PipelineController do
1818
plug(PublicPageAccess when action in @public_endpoints)
1919
plug(PageAccess, [permissions: "project.view"] when action not in @public_endpoints)
2020
plug(PageAccess, [permissions: "project.job.stop"] when action == :stop)
21+
plug(PageAccess, [permissions: "project.job.rerun"] when action == :rebuild)
2122

2223
plug(:assign_pipeline_with_blocks when action in [:show, :poll])
23-
plug(:assign_pipeline_without_blocks when action in [:status, :switch, :stop])
24+
plug(:assign_pipeline_without_blocks when action in [:status, :switch, :stop, :rebuild])
2425
plug(:preload_switch when action in [:show, :poll, :switch])
2526

2627
def path(conn, params) do
@@ -130,6 +131,32 @@ defmodule FrontWeb.PipelineController do
130131
end
131132
end
132133

134+
def rebuild(conn, _params) do
135+
Watchman.benchmark("rebuild.duration", fn ->
136+
project = conn.assigns.project
137+
workflow = conn.assigns.workflow
138+
pipeline = conn.assigns.pipeline
139+
140+
log_rebuild(conn, project, workflow, pipeline)
141+
rebuild_pipeline(conn, pipeline.id, conn.assigns.user_id, conn.assigns.tracing_headers)
142+
end)
143+
end
144+
145+
defp rebuild_pipeline(conn, ppl_id, user_id, tracing_headers) do
146+
case Pipeline.rebuild(ppl_id, user_id, tracing_headers) do
147+
{:ok, new_pipeline_id} ->
148+
conn
149+
|> json(%{
150+
message: "Pipeline rebuild initiated successfully.",
151+
pipeline_id: new_pipeline_id
152+
})
153+
154+
{:error, message} ->
155+
conn
156+
|> json(%{error: message})
157+
end
158+
end
159+
133160
defp organization_matches?(organization_id, pipeline_organization_id) do
134161
organization_id == pipeline_organization_id
135162
end
@@ -152,6 +179,20 @@ defmodule FrontWeb.PipelineController do
152179
|> Audit.log()
153180
end
154181

182+
defp log_rebuild(conn, project, workflow, pipeline) do
183+
conn
184+
|> Audit.new(:Pipeline, :Rebuild)
185+
|> Audit.add(:resource_name, pipeline.name)
186+
|> Audit.add(:description, "Rebuilt the pipeline")
187+
|> Audit.metadata(project_id: project.id)
188+
|> Audit.metadata(project_name: project.name)
189+
|> Audit.metadata(branch_name: workflow.branch_name)
190+
|> Audit.metadata(workflow_id: workflow.id)
191+
|> Audit.metadata(commit_sha: workflow.commit_sha)
192+
|> Audit.metadata(pipeline_id: pipeline.id)
193+
|> Audit.log()
194+
end
195+
155196
defp pipeline_data(conn, params) do
156197
diagram =
157198
if FeatureProvider.feature_enabled?(:toggle_skipped_blocks,

β€Žfront/lib/front_web/router.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,10 @@ defmodule FrontWeb.Router do
638638
as: :pipeline_stop
639639
)
640640

641+
post("/workflows/:workflow_id/pipelines/:pipeline_id/rebuild", PipelineController, :rebuild,
642+
as: :pipeline_rebuild
643+
)
644+
641645
post(
642646
"/workflows/:workflow_id/pipelines/:pipeline_id/swithes/:switch_id/targets/:name",
643647
TargetController,

β€Žfront/lib/front_web/templates/workflow/status/_interactive_pipeline.html.eex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
<span class="gray mh2">&middot;</span>
2222
<%= link "Stop Pipeline", to: pipeline_stop_path(@conn, :stop, @workflow.id, @pipeline.id), class: "btn btn-secondary btn-tiny", pipeline_stop_button: "true" %>
2323
<% end %>
24+
<%= if @conn.assigns.permissions["project.job.rerun"] && FrontWeb.PipelineView.pipeline_rebuildable?(@pipeline) && !FrontWeb.PipelineView.anonymous?(@conn) do %>
25+
<span class="gray mh2">&middot;</span>
26+
<%= link "Rebuild Pipeline", to: pipeline_rebuild_path(@conn, :rebuild, @workflow.id, @pipeline.id), class: "btn btn-secondary btn-tiny", pipeline_rebuild_button: "true", title: "Rerun only failed jobs in this pipeline" %>
27+
<% end %>
2428
<span class="child ml2">←</span>
2529
</div>
2630
</div>

β€Žfront/lib/front_web/views/pipeline_view.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ defmodule FrontWeb.PipelineView do
299299
pipeline.state != :DONE && pipeline.state != :STOPPING
300300
end
301301

302+
def pipeline_rebuildable?(pipeline) do
303+
pipeline.state == :DONE && pipeline.result != :PASSED
304+
end
305+
302306
def anonymous?(conn) do
303307
conn.assigns.anonymous
304308
end

β€Žfront/test/front/clients/pipeline_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ defmodule Front.Clients.PipelineTest do
77
DescribeManyRequest,
88
DescribeRequest,
99
DescribeTopologyRequest,
10+
PartialRebuildRequest,
1011
TerminateRequest
1112
}
1213

@@ -53,4 +54,15 @@ defmodule Front.Clients.PipelineTest do
5354
assert {:ok, response} == Pipeline.terminate(request)
5455
end
5556
end
57+
58+
describe "partial_rebuild" do
59+
test "returns PartialRebuildResponse for PartialRebuildRequest" do
60+
request = PartialRebuildRequest.new()
61+
62+
response = Factories.Pipeline.partial_rebuild_response()
63+
GrpcMock.stub(PipelineMock, :partial_rebuild, response)
64+
65+
assert {:ok, response} == Pipeline.partial_rebuild(request)
66+
end
67+
end
5668
end

β€Žfront/test/front_web/controllers/pipeline_controller_test.exs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,50 @@ defmodule FrontWeb.PipelineControllerTest do
262262
assert conn.status == 404
263263
end
264264
end
265+
266+
describe "rebuild" do
267+
test "sends partial rebuild request", %{
268+
conn: conn,
269+
workflow_id: workflow_id,
270+
pipeline_id: pipeline_id
271+
} do
272+
conn =
273+
conn
274+
|> post("/workflows/#{workflow_id}/pipelines/#{pipeline_id}/rebuild")
275+
276+
assert conn.status == 200
277+
assert json_response(conn, 200)["message"] == "Pipeline rebuild initiated successfully."
278+
assert json_response(conn, 200)["pipeline_id"] != nil
279+
end
280+
281+
test "returns 404 when organization_id mismatches", %{
282+
conn: conn,
283+
workflow_id: workflow_id,
284+
pipeline_id: pipeline_id
285+
} do
286+
conn =
287+
conn
288+
|> Plug.Conn.put_req_header("x-semaphore-org-id", Ecto.UUID.generate())
289+
|> post("/workflows/#{workflow_id}/pipelines/#{pipeline_id}/rebuild")
290+
291+
assert conn.status == 404
292+
end
293+
end
294+
295+
describe "rebuild => when user does not have permission to rerun jobs" do
296+
test "returns 404", %{conn: conn, workflow_id: workflow_id, pipeline_id: pipeline_id} do
297+
Support.Stubs.PermissionPatrol.remove_all_permissions()
298+
299+
org = Support.Stubs.DB.first(:organizations)
300+
user = Support.Stubs.DB.first(:users)
301+
302+
Support.Stubs.PermissionPatrol.allow_everything_except(org.id, user.id, "project.job.rerun")
303+
304+
conn =
305+
conn
306+
|> post("/workflows/#{workflow_id}/pipelines/#{pipeline_id}/rebuild")
307+
308+
assert conn.status == 404
309+
end
310+
end
265311
end

β€Žfront/test/front_web/views/pipeline_view_test.exs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,26 @@ defmodule FrontWeb.PipelineViewTest do
244244
assert action =~ "Triggered"
245245
end
246246
end
247+
248+
describe ".pipeline_rebuildable?" do
249+
test "returns true when pipeline is in DONE state" do
250+
pipeline = %Models.Pipeline{state: :DONE}
251+
assert PipelineView.pipeline_rebuildable?(pipeline) == true
252+
end
253+
254+
test "returns false when pipeline is in PENDING state" do
255+
pipeline = %Models.Pipeline{state: :PENDING}
256+
assert PipelineView.pipeline_rebuildable?(pipeline) == false
257+
end
258+
259+
test "returns false when pipeline is in RUNNING state" do
260+
pipeline = %Models.Pipeline{state: :RUNNING}
261+
assert PipelineView.pipeline_rebuildable?(pipeline) == false
262+
end
263+
264+
test "returns false when pipeline is in STOPPING state" do
265+
pipeline = %Models.Pipeline{state: :STOPPING}
266+
assert PipelineView.pipeline_rebuildable?(pipeline) == false
267+
end
268+
end
247269
end

0 commit comments

Comments
Β (0)