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

Integrated code lifecycle: Allow admins to pause all build agents #9892

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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();
}
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

/**
* {@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 @@ -154,10 +154,18 @@ public void pauseBuildAgent(String agent) {
pauseBuildAgentTopic.publish(agent);
}

public void pauseAllBuildAgents() {
getBuildAgentInformation().forEach(agent -> pauseBuildAgent(agent.buildAgent().name()));
}
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

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

public void resumeAllBuildAgents() {
getBuildAgentInformation().forEach(agent -> resumeBuildAgent(agent.buildAgent().name()));
}
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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>
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
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();
}
}
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

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',
});
},
});
}
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
}
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);
});
}
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
}
Loading
Loading