Skip to content

Commit

Permalink
Integrated code lifecycle: Allow admins to pause all build agents (#9892
Browse files Browse the repository at this point in the history
)
  • Loading branch information
BBesrour authored and AjayvirS committed Dec 3, 2024
1 parent d4a3539 commit dffb3b4
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ public ResponseEntity<BuildJobsStatisticsDTO> getBuildJobStatistics(@RequestPara
}

/**
* {@code PUT /api/admin/agent/{agentName}/pause} : Pause the specified build agent.
* {@code PUT /api/admin/agents/{agentName}/pause} : Pause the specified build agent.
* This endpoint allows administrators to pause a specific build agent by its name.
* Pausing a build agent will prevent it from picking up any new build jobs until it is resumed.
*
Expand All @@ -207,15 +207,34 @@ public ResponseEntity<BuildJobsStatisticsDTO> getBuildJobStatistics(@RequestPara
* @return {@link ResponseEntity} with status code 204 (No Content) if the agent was successfully paused
* or an appropriate error response if something went wrong
*/
@PutMapping("agent/{agentName}/pause")
@PutMapping("agents/{agentName}/pause")
public ResponseEntity<Void> pauseBuildAgent(@PathVariable String agentName) {
log.debug("REST request to pause agent {}", agentName);
localCIBuildJobQueueService.pauseBuildAgent(agentName);
return ResponseEntity.noContent().build();
}

/**
* {@code PUT /api/admin/agent/{agentName}/resume} : Resume the specified build agent.
* {@code PUT /api/admin/agents/pause-all} : Pause all build agents.
* This endpoint allows administrators to pause all build agents.
* Pausing all build agents will prevent them from picking up any new build jobs until they are resumed.
*
* <p>
* <strong>Authorization:</strong> This operation requires admin privileges, enforced by {@code @EnforceAdmin}.
* </p>
*
* @return {@link ResponseEntity} with status code 204 (No Content) if all agents were successfully paused
* or an appropriate error response if something went wrong
*/
@PutMapping("agents/pause-all")
public ResponseEntity<Void> pauseAllBuildAgents() {
log.debug("REST request to pause all agents");
localCIBuildJobQueueService.pauseAllBuildAgents();
return ResponseEntity.noContent().build();
}

/**
* {@code PUT /api/admin/agents/{agentName}/resume} : Resume the specified build agent.
* This endpoint allows administrators to resume a specific build agent by its name.
* Resuming a build agent will allow it to pick up new build jobs again.
*
Expand All @@ -227,10 +246,29 @@ public ResponseEntity<Void> pauseBuildAgent(@PathVariable String agentName) {
* @return {@link ResponseEntity} with status code 204 (No Content) if the agent was successfully resumed
* or an appropriate error response if something went wrong
*/
@PutMapping("agent/{agentName}/resume")
@PutMapping("agents/{agentName}/resume")
public ResponseEntity<Void> resumeBuildAgent(@PathVariable String agentName) {
log.debug("REST request to resume agent {}", agentName);
localCIBuildJobQueueService.resumeBuildAgent(agentName);
return ResponseEntity.noContent().build();
}

/**
* {@code PUT /api/admin/agents/resume-all} : Resume all build agents.
* This endpoint allows administrators to resume all build agents.
* Resuming all build agents will allow them to pick up new build jobs again.
*
* <p>
* <strong>Authorization:</strong> This operation requires admin privileges, enforced by {@code @EnforceAdmin}.
* </p>
*
* @return {@link ResponseEntity} with status code 204 (No Content) if all agents were successfully resumed
* or an appropriate error response if something went wrong
*/
@PutMapping("agents/resume-all")
public ResponseEntity<Void> resumeAllBuildAgents() {
log.debug("REST request to resume all agents");
localCIBuildJobQueueService.resumeAllBuildAgents();
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,18 @@ public void pauseBuildAgent(String agent) {
pauseBuildAgentTopic.publish(agent);
}

public void pauseAllBuildAgents() {
getBuildAgentInformation().forEach(agent -> pauseBuildAgent(agent.buildAgent().name()));
}

public void resumeBuildAgent(String agent) {
resumeBuildAgentTopic.publish(agent);
}

public void resumeAllBuildAgents() {
getBuildAgentInformation().forEach(agent -> resumeBuildAgent(agent.buildAgent().name()));
}

/**
* Cancel a build job by removing it from the queue or stopping the build process.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
<div style="padding-bottom: 60px">
<h3 id="build-agents-heading" jhiTranslate="artemisApp.buildAgents.summary"></h3>
@if (buildAgents) {
<p>
{{ buildAgents.length }} <span jhiTranslate="artemisApp.buildAgents.onlineAgents"></span>: {{ currentBuilds }} <span jhiTranslate="artemisApp.buildAgents.of"></span>
{{ buildCapacity }} <span jhiTranslate="artemisApp.buildAgents.buildJobsRunning"></span>
</p>
<div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3"></div>
<div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3">
<div>
<h3 id="build-agents-heading" jhiTranslate="artemisApp.buildAgents.summary"></h3>
<p>
{{ buildAgents.length }} <span jhiTranslate="artemisApp.buildAgents.onlineAgents"></span>: {{ currentBuilds }}
<span jhiTranslate="artemisApp.buildAgents.of"></span> {{ buildCapacity }} <span jhiTranslate="artemisApp.buildAgents.buildJobsRunning"></span>
</p>
</div>
<div>
<button class="btn btn-success" (click)="resumeAllBuildAgents()">
<fa-icon [icon]="faPlay" />
<span jhiTranslate="artemisApp.buildAgents.resumeAll"></span>
</button>
<button class="btn btn-danger" (click)="displayPauseBuildAgentModal(content)">
<fa-icon [icon]="faPause" />
<span jhiTranslate="artemisApp.buildAgents.pauseAll"></span>
</button>
</div>
</div>
<jhi-data-table [showPageSizeDropdown]="false" [showSearchField]="false" entityType="buildAgent" [allEntities]="buildAgents!">
<ng-template let-settings="settings" let-controls="controls">
<ngx-datatable
Expand Down Expand Up @@ -115,3 +128,28 @@ <h3 id="build-agents-heading" jhiTranslate="artemisApp.buildAgents.summary"></h3
</jhi-data-table>
}
</div>

<!-- Modal -->
<ng-template #content let-modal>
<div class="modal-header">
<h5 class="modal-title">
<span jhiTranslate="artemisApp.buildAgents.pauseAll"></span>
</h5>
<button type="button" class="btn-close" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<div>
<span jhiTranslate="artemisApp.buildAgents.pauseAllWarning"></span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="modal.close()">
<fa-icon [icon]="faTimes" />
<span jhiTranslate="artemisApp.buildQueue.filter.close"></span>
</button>
<button type="button" class="btn btn-danger" (click)="pauseAllBuildAgents(modal)">
<fa-icon [icon]="faPause" />
<span jhiTranslate="artemisApp.buildAgents.pauseAll"></span>
</button>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { BuildAgentInformation } from 'app/entities/programming/build-agent-information.model';
import { BuildAgentInformation, BuildAgentStatus } from 'app/entities/programming/build-agent-information.model';
import { JhiWebsocketService } from 'app/core/websocket/websocket.service';
import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service';
import { Subscription } from 'rxjs';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { faPause, faPlay, faTimes } from '@fortawesome/free-solid-svg-icons';
import { BuildQueueService } from 'app/localci/build-queue/build-queue.service';
import { Router } from '@angular/router';
import { BuildAgent } from 'app/entities/programming/build-agent.model';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { AlertService, AlertType } from 'app/core/util/alert.service';

@Component({
selector: 'jhi-build-agents',
Expand All @@ -23,13 +25,17 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy {
routerLink: string;

//icons
faTimes = faTimes;
protected readonly faTimes = faTimes;
protected readonly faPause = faPause;
protected readonly faPlay = faPlay;

constructor(
private websocketService: JhiWebsocketService,
private buildAgentsService: BuildAgentsService,
private buildQueueService: BuildQueueService,
private router: Router,
private modalService: NgbModal,
private alertService: AlertService,
) {}

ngOnInit() {
Expand Down Expand Up @@ -59,7 +65,9 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy {

private updateBuildAgents(buildAgents: BuildAgentInformation[]) {
this.buildAgents = buildAgents;
this.buildCapacity = this.buildAgents.reduce((sum, agent) => sum + (agent.maxNumberOfConcurrentBuildJobs || 0), 0);
this.buildCapacity = this.buildAgents
.filter((agent) => agent.status !== BuildAgentStatus.PAUSED)
.reduce((sum, agent) => sum + (agent.maxNumberOfConcurrentBuildJobs || 0), 0);
this.currentBuilds = this.buildAgents.reduce((sum, agent) => sum + (agent.numberOfCurrentBuildJobs || 0), 0);
}

Expand All @@ -86,4 +94,47 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy {
this.buildQueueService.cancelAllRunningBuildJobsForAgent(buildAgentToCancel.buildAgent?.name).subscribe();
}
}

displayPauseBuildAgentModal(modal: any) {
this.modalService.open(modal);
}

pauseAllBuildAgents(modal?: any) {
this.buildAgentsService.pauseAllBuildAgents().subscribe({
next: () => {
this.load();
this.alertService.addAlert({
type: AlertType.SUCCESS,
message: 'artemisApp.buildAgents.alerts.buildAgentsPaused',
});
},
error: () => {
this.alertService.addAlert({
type: AlertType.DANGER,
message: 'artemisApp.buildAgents.alerts.buildAgentPauseFailed',
});
},
});
if (modal) {
modal.close();
}
}

resumeAllBuildAgents() {
this.buildAgentsService.resumeAllBuildAgents().subscribe({
next: () => {
this.load();
this.alertService.addAlert({
type: AlertType.SUCCESS,
message: 'artemisApp.buildAgents.alerts.buildAgentsResumed',
});
},
error: () => {
this.alertService.addAlert({
type: AlertType.DANGER,
message: 'artemisApp.buildAgents.alerts.buildAgentResumeFailed',
});
},
});
}
}
26 changes: 24 additions & 2 deletions src/main/webapp/app/localci/build-agents/build-agents.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,44 @@ export class BuildAgentsService {
*/
pauseBuildAgent(agentName: string): Observable<void> {
const encodedAgentName = encodeURIComponent(agentName);
return this.http.put<void>(`${this.adminResourceUrl}/agent/${encodedAgentName}/pause`, null).pipe(
return this.http.put<void>(`${this.adminResourceUrl}/agents/${encodedAgentName}/pause`, null).pipe(
catchError((err) => {
return throwError(() => new Error(`Failed to pause build agent ${agentName}\n${err.message}`));
}),
);
}

/**
* Pause All Build Agents
*/
pauseAllBuildAgents(): Observable<void> {
return this.http.put<void>(`${this.adminResourceUrl}/agents/pause-all`, null).pipe(
catchError((err) => {
return throwError(() => new Error(`Failed to pause build agents\n${err.message}`));
}),
);
}

/**
* Resume Build Agent
*/
resumeBuildAgent(agentName: string): Observable<void> {
const encodedAgentName = encodeURIComponent(agentName);
return this.http.put<void>(`${this.adminResourceUrl}/agent/${encodedAgentName}/resume`, null).pipe(
return this.http.put<void>(`${this.adminResourceUrl}/agents/${encodedAgentName}/resume`, null).pipe(
catchError((err) => {
return throwError(() => new Error(`Failed to resume build agent ${agentName}\n${err.message}`));
}),
);
}

/**
* Resume all Build Agents
*/
resumeAllBuildAgents(): Observable<void> {
return this.http.put<void>(`${this.adminResourceUrl}/agents/resume-all`, null).pipe(
catchError((err) => {
return throwError(() => new Error(`Failed to resume build agents\n${err.message}`));
}),
);
}
}
9 changes: 7 additions & 2 deletions src/main/webapp/i18n/de/buildAgents.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@
"buildAgentResumed": "Anfrage zum Fortsetzen des BuildJobs erfolgreich gesendet. Der Agent wird wieder neue BuildJobs annehmen.",
"buildAgentPauseFailed": "Anhalten des Build-Agenten fehlgeschlagen.",
"buildAgentResumeFailed": "Fortsetzen des Build-Agenten fehlgeschlagen.",
"buildAgentWithoutName": "Der Name des Build-Agenten ist erforderlich."
"buildAgentWithoutName": "Der Name des Build-Agenten ist erforderlich.",
"buildAgentsPaused": "Anfrage zum Anhalten aller Build-Agenten erfolgreich gesendet. Die Agenten akzeptieren keine neuen Build-Jobs und werden die aktuellen Build-Jobs entweder ordnungsgemäß beenden oder nach einer konfigurierbaren Anzahl von Sekunden abbrechen.",
"buildAgentsResumed": "Anfrage zum Fortsetzen aller Build-Agenten erfolgreich gesendet. Die Agenten werden wieder neue Build-Jobs annehmen."
},
"pause": "Anhalten",
"resume": "Fortsetzen"
"resume": "Fortsetzen",
"pauseAll": "Alle Agenten Anhalten",
"resumeAll": "Alle Agenten fortsetzen",
"pauseAllWarning": "Du bist dabei, alle Build-Agenten anzuhalten. Dies wird verhindern, dass sie neue Build-Jobs verarbeiten.\nBist du sicher, dass du fortfahren möchtest?"
}
}
}
13 changes: 9 additions & 4 deletions src/main/webapp/i18n/en/buildAgents.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@
"alerts": {
"buildAgentPaused": "Build agent pause request sent successfully. The agent will not accept new build jobs and will gracefully finish the current build jobs or will cancel them after a configurable seconds.",
"buildAgentResumed": "Build agent resume request sent successfully. The agent will start accepting new build jobs.",
"buildAgentPauseFailed": "Failed to pause the build agent.",
"buildAgentResumeFailed": "Failed to resume the build agent.",
"buildAgentWithoutName": "Build agent name is required."
"buildAgentPauseFailed": "Failed to pause build agent.",
"buildAgentResumeFailed": "Failed to resume build agent.",
"buildAgentWithoutName": "Build agent name is required.",
"buildAgentsPaused": "Pause request sent to all build agents. The agents will not accept new build jobs and will gracefully finish the current build jobs or will cancel them after a configurable seconds.",
"buildAgentsResumed": "Resume request sent to all build agents. The agents will start accepting new build jobs."
},
"pause": "Pause",
"resume": "Resume"
"resume": "Resume",
"pauseAll": "Pause All Agents",
"resumeAll": "Resume All Agents",
"pauseAllWarning": "You are about to pause all build agents. This will prevent them from processing any new build jobs.\nAre you sure you want to proceed?"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -364,10 +364,29 @@ void testPauseBuildAgent() throws Exception {
// We need to clear the processing jobs to avoid the agent being set to ACTIVE again
processingJobs.clear();

request.put("/api/admin/agent/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/pause", null, HttpStatus.NO_CONTENT);
request.put("/api/admin/agents/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/pause", null, HttpStatus.NO_CONTENT);
await().until(() -> buildAgentInformation.get(agent1.buildAgent().memberAddress()).status() == BuildAgentInformation.BuildAgentStatus.PAUSED);

request.put("/api/admin/agent/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/resume", null, HttpStatus.NO_CONTENT);
request.put("/api/admin/agents/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/resume", null, HttpStatus.NO_CONTENT);
await().until(() -> buildAgentInformation.get(agent1.buildAgent().memberAddress()).status() == BuildAgentInformation.BuildAgentStatus.IDLE);
}

@Test
@WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN")
void testPauseAllBuildAgents() throws Exception {
// We need to clear the processing jobs to avoid the agent being set to ACTIVE again
processingJobs.clear();

request.put("/api/admin/agents/pause-all", null, HttpStatus.NO_CONTENT);
await().until(() -> {
var agents = buildAgentInformation.values();
return agents.stream().allMatch(agent -> agent.status() == BuildAgentInformation.BuildAgentStatus.PAUSED);
});

request.put("/api/admin/agents/resume-all", null, HttpStatus.NO_CONTENT);
await().until(() -> {
var agents = buildAgentInformation.values();
return agents.stream().allMatch(agent -> agent.status() == BuildAgentInformation.BuildAgentStatus.IDLE);
});
}
}
Loading

0 comments on commit dffb3b4

Please sign in to comment.