diff --git a/docs/admin/setup/distributed.rst b/docs/admin/setup/distributed.rst index 1fa74024dc2d..b2d1a12822d3 100644 --- a/docs/admin/setup/distributed.rst +++ b/docs/admin/setup/distributed.rst @@ -617,8 +617,13 @@ These credentials are used to clone repositories via HTTPS. You must also add th container-cleanup: expiry-minutes: 5 # Time after a hanging container will automatically be removed cleanup-schedule-minutes: 60 # Schedule for container cleanup + build-agent: + short-name: "artemis-build-agent-X" # Short name of the build agent. This should be unique for each build agent. Only lowercase letters, numbers and hyphens are allowed. + display-name: "Artemis Build Agent X" # This value is optional. If omitted, the short name will be used as display name. Display name of the build agent. This is shown in the Artemis UI. +Please note that ``artemis.continuous-integration.build-agent.short-name`` must be provided. Otherwise, the build agent will not start. + Build agents run as `Hazelcast Lite Members `__ and require a full member, in our case a core node, to be running. Thus, before starting a build agent make sure that at least the primary node is running. You can then add and remove build agents to the cluster as desired. diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentDTO.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentDTO.java new file mode 100644 index 000000000000..2ffa0f2daa61 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentDTO.java @@ -0,0 +1,10 @@ +package de.tum.cit.aet.artemis.buildagent.dto; + +import java.io.Serial; +import java.io.Serializable; + +public record BuildAgentDTO(String name, String memberAddress, String displayName) implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentInformation.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentInformation.java index ab24012f51fa..40af5049060e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentInformation.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentInformation.java @@ -11,7 +11,7 @@ // in the future are migrated or cleared. Changes should be communicated in release notes as potentially breaking changes. @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record BuildAgentInformation(String name, int maxNumberOfConcurrentBuildJobs, int numberOfCurrentBuildJobs, List runningBuildJobs, +public record BuildAgentInformation(BuildAgentDTO buildAgent, int maxNumberOfConcurrentBuildJobs, int numberOfCurrentBuildJobs, List runningBuildJobs, BuildAgentStatus status, List recentBuildJobs, String publicSshKey) implements Serializable { @Serial @@ -24,7 +24,7 @@ public record BuildAgentInformation(String name, int maxNumberOfConcurrentBuildJ * @param recentBuildJobs The list of recent build jobs */ public BuildAgentInformation(BuildAgentInformation agentInformation, List recentBuildJobs) { - this(agentInformation.name(), agentInformation.maxNumberOfConcurrentBuildJobs(), agentInformation.numberOfCurrentBuildJobs(), agentInformation.runningBuildJobs, + this(agentInformation.buildAgent(), agentInformation.maxNumberOfConcurrentBuildJobs(), agentInformation.numberOfCurrentBuildJobs(), agentInformation.runningBuildJobs, agentInformation.status(), recentBuildJobs, agentInformation.publicSshKey()); } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java index d9bfff039c2e..7a4220399819 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java @@ -14,7 +14,7 @@ // in the future are migrated or cleared. Changes should be communicated in release notes as potentially breaking changes. @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record BuildJobQueueItem(String id, String name, String buildAgentAddress, long participationId, long courseId, long exerciseId, int retryCount, int priority, +public record BuildJobQueueItem(String id, String name, BuildAgentDTO buildAgent, long participationId, long courseId, long exerciseId, int retryCount, int priority, BuildStatus status, RepositoryInfo repositoryInfo, JobTimingInfo jobTimingInfo, BuildConfig buildConfig, ResultDTO submissionResult) implements Serializable { @Serial @@ -28,7 +28,7 @@ public record BuildJobQueueItem(String id, String name, String buildAgentAddress * @param status The status/result of the build job */ public BuildJobQueueItem(BuildJobQueueItem queueItem, ZonedDateTime buildCompletionDate, BuildStatus status) { - this(queueItem.id(), queueItem.name(), queueItem.buildAgentAddress(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), + this(queueItem.id(), queueItem.name(), queueItem.buildAgent(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), queueItem.priority(), status, queueItem.repositoryInfo(), new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), queueItem.jobTimingInfo.buildStartDate(), buildCompletionDate), queueItem.buildConfig(), null); } @@ -36,17 +36,16 @@ public BuildJobQueueItem(BuildJobQueueItem queueItem, ZonedDateTime buildComplet /** * Constructor used to create a new processing build job from a queued build job * - * @param queueItem The queued build job - * @param hazelcastMemberAddress The address of the hazelcast member that is processing the build job + * @param queueItem The queued build job + * @param buildAgent The build agent that will process the build job */ - public BuildJobQueueItem(BuildJobQueueItem queueItem, String hazelcastMemberAddress) { - this(queueItem.id(), queueItem.name(), hazelcastMemberAddress, queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), - queueItem.priority(), null, queueItem.repositoryInfo(), new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), ZonedDateTime.now(), null), - queueItem.buildConfig(), null); + public BuildJobQueueItem(BuildJobQueueItem queueItem, BuildAgentDTO buildAgent) { + this(queueItem.id(), queueItem.name(), buildAgent, queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), queueItem.priority(), + null, queueItem.repositoryInfo(), new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), ZonedDateTime.now(), null), queueItem.buildConfig(), null); } public BuildJobQueueItem(BuildJobQueueItem queueItem, ResultDTO submissionResult) { - this(queueItem.id(), queueItem.name(), queueItem.buildAgentAddress(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), + this(queueItem.id(), queueItem.name(), queueItem.buildAgent(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), queueItem.priority(), queueItem.status(), queueItem.repositoryInfo(), queueItem.jobTimingInfo(), queueItem.buildConfig(), submissionResult); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java index e73f8bac244b..0823ec5a4f9b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java @@ -25,6 +25,7 @@ import jakarta.annotation.PreDestroy; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; @@ -45,6 +46,7 @@ import com.hazelcast.map.IMap; import com.hazelcast.topic.ITopic; +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; import de.tum.cit.aet.artemis.buildagent.dto.BuildResult; @@ -116,9 +118,15 @@ public class SharedQueueProcessingService { */ private final AtomicBoolean processResults = new AtomicBoolean(true); - @Value("${artemis.continuous-integration.pause-grace-period-seconds:15}") + @Value("${artemis.continuous-integration.pause-grace-period-seconds:60}") private int pauseGracePeriodSeconds; + @Value("${artemis.continuous-integration.build-agent.short-name}") + private String buildAgentShortName; + + @Value("${artemis.continuous-integration.build-agent.display-name:}") + private String buildAgentDisplayName; + public SharedQueueProcessingService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, ExecutorService localCIBuildExecutorService, BuildJobManagementService buildJobManagementService, BuildLogsMap buildLogsMap, BuildAgentSshKeyService buildAgentSSHKeyService, TaskScheduler taskScheduler) { this.hazelcastInstance = hazelcastInstance; @@ -134,6 +142,17 @@ public SharedQueueProcessingService(@Qualifier("hazelcastInstance") HazelcastIns */ @EventListener(ApplicationReadyEvent.class) public void init() { + if (!buildAgentShortName.matches("^[a-z0-9-]+$")) { + String errorMessage = "Build agent short name must not be empty and only contain lowercase letters, numbers and hyphens." + + " Build agent short name should be changed in the application properties under 'artemis.continuous-integration.build-agent.short-name'."; + log.error(errorMessage); + throw new IllegalArgumentException(errorMessage); + } + + if (StringUtils.isBlank(buildAgentDisplayName)) { + buildAgentDisplayName = buildAgentShortName; + } + this.buildAgentInformation = this.hazelcastInstance.getMap("buildAgentInformation"); this.processingJobs = this.hazelcastInstance.getMap("processingJobs"); this.queue = this.hazelcastInstance.getQueue("buildJobQueue"); @@ -154,14 +173,14 @@ public void init() { ITopic pauseBuildAgentTopic = hazelcastInstance.getTopic("pauseBuildAgentTopic"); pauseBuildAgentTopic.addMessageListener(message -> { - if (message.getMessageObject().equals(hazelcastInstance.getCluster().getLocalMember().getAddress().toString())) { + if (buildAgentShortName.equals(message.getMessageObject())) { pauseBuildAgent(); } }); ITopic resumeBuildAgentTopic = hazelcastInstance.getTopic("resumeBuildAgentTopic"); resumeBuildAgentTopic.addMessageListener(message -> { - if (message.getMessageObject().equals(hazelcastInstance.getCluster().getLocalMember().getAddress().toString())) { + if (buildAgentShortName.equals(message.getMessageObject())) { resumeBuildAgent(); } }); @@ -201,6 +220,7 @@ public void updateBuildAgentInformation() { log.debug("There are only lite member in the cluster. Not updating build agent information."); return; } + // Remove build agent information of offline nodes removeOfflineNodes(); @@ -253,7 +273,7 @@ private void checkAvailabilityAndProcessNextBuild() { if (buildJob != null) { processingJobs.remove(buildJob.id()); - buildJob = new BuildJobQueueItem(buildJob, ""); + buildJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO("", "", "")); log.info("Adding build job back to the queue: {}", buildJob); queue.add(buildJob); localProcessingJobs.decrementAndGet(); @@ -275,7 +295,7 @@ private BuildJobQueueItem addToProcessingJobs() { if (buildJob != null) { String hazelcastMemberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString(); - BuildJobQueueItem processingJob = new BuildJobQueueItem(buildJob, hazelcastMemberAddress); + BuildJobQueueItem processingJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO(buildAgentShortName, hazelcastMemberAddress, buildAgentDisplayName)); processingJobs.put(processingJob.id(), processingJob); localProcessingJobs.incrementAndGet(); @@ -297,10 +317,10 @@ private void updateLocalBuildAgentInformationWithRecentJob(BuildJobQueueItem rec // Add/update BuildAgentInformation info = getUpdatedLocalBuildAgentInformation(recentBuildJob); try { - buildAgentInformation.put(info.name(), info); + buildAgentInformation.put(info.buildAgent().memberAddress(), info); } catch (Exception e) { - log.error("Error while updating build agent information for agent {}", info.name(), e); + log.error("Error while updating build agent information for agent {} with address {}", info.buildAgent().name(), info.buildAgent().memberAddress(), e); } } finally { @@ -334,11 +354,13 @@ private BuildAgentInformation getUpdatedLocalBuildAgentInformation(BuildJobQueue String publicSshKey = buildAgentSSHKeyService.getPublicKeyAsString(); - return new BuildAgentInformation(memberAddress, maxNumberOfConcurrentBuilds, numberOfCurrentBuildJobs, processingJobsOfMember, status, recentBuildJobs, publicSshKey); + BuildAgentDTO agentInfo = new BuildAgentDTO(buildAgentShortName, memberAddress, buildAgentDisplayName); + + return new BuildAgentInformation(agentInfo, maxNumberOfConcurrentBuilds, numberOfCurrentBuildJobs, processingJobsOfMember, status, recentBuildJobs, publicSshKey); } private List getProcessingJobsOfNode(String memberAddress) { - return processingJobs.values().stream().filter(job -> Objects.equals(job.buildAgentAddress(), memberAddress)).toList(); + return processingJobs.values().stream().filter(job -> Objects.equals(job.buildAgent().memberAddress(), memberAddress)).toList(); } private void removeOfflineNodes() { @@ -371,7 +393,7 @@ private void processBuild(BuildJobQueueItem buildJob) { log.debug("Build job completed: {}", buildJob); JobTimingInfo jobTimingInfo = new JobTimingInfo(buildJob.jobTimingInfo().submissionDate(), buildJob.jobTimingInfo().buildStartDate(), ZonedDateTime.now()); - BuildJobQueueItem finishedJob = new BuildJobQueueItem(buildJob.id(), buildJob.name(), buildJob.buildAgentAddress(), buildJob.participationId(), buildJob.courseId(), + BuildJobQueueItem finishedJob = new BuildJobQueueItem(buildJob.id(), buildJob.name(), buildJob.buildAgent(), buildJob.participationId(), buildJob.courseId(), buildJob.exerciseId(), buildJob.retryCount(), buildJob.priority(), BuildStatus.SUCCESSFUL, buildJob.repositoryInfo(), jobTimingInfo, buildJob.buildConfig(), null); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java index 33e7bd099155..27cd50a6e4b9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java @@ -4,6 +4,7 @@ import java.time.ZonedDateTime; import java.util.List; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,9 +96,12 @@ public ResponseEntity> getBuildAgentSummary() { @GetMapping("build-agent") public ResponseEntity getBuildAgentDetails(@RequestParam String agentName) { log.debug("REST request to get information on build agent {}", agentName); - BuildAgentInformation buildAgentDetails = localCIBuildJobQueueService.getBuildAgentInformation().stream().filter(agent -> agent.name().equals(agentName)).findFirst() - .orElse(null); - return ResponseEntity.ok(buildAgentDetails); + Optional buildAgentDetails = localCIBuildJobQueueService.getBuildAgentInformation().stream() + .filter(agent -> agent.buildAgent().name().equals(agentName)).findFirst(); + if (buildAgentDetails.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(buildAgentDetails.get()); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildJob.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildJob.java index 53f3b75305f2..7a6aeafdbd04 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildJob.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildJob.java @@ -88,7 +88,7 @@ public BuildJob(BuildJobQueueItem queueItem, BuildStatus buildStatus, Result res this.courseId = queueItem.courseId(); this.participationId = queueItem.participationId(); this.result = result; - this.buildAgentAddress = queueItem.buildAgentAddress(); + this.buildAgentAddress = queueItem.buildAgent().memberAddress(); this.buildStartDate = queueItem.jobTimingInfo().buildStartDate(); this.buildCompletionDate = queueItem.jobTimingInfo().buildCompletionDate(); this.repositoryType = queueItem.repositoryInfo().repositoryType(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java index bfd04f5ba49d..5a805ff54d03 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java @@ -84,7 +84,7 @@ private void sendBuildAgentSummaryOverWebsocket() { } private void sendBuildAgentDetailsOverWebsocket(String agentName) { - sharedQueueManagementService.getBuildAgentInformation().stream().filter(agent -> agent.name().equals(agentName)).findFirst() + sharedQueueManagementService.getBuildAgentInformation().stream().filter(agent -> agent.buildAgent().name().equals(agentName)).findFirst() .ifPresent(localCIWebsocketMessagingService::sendBuildAgentDetails); } @@ -127,19 +127,19 @@ private class BuildAgentListener @Override public void entryAdded(com.hazelcast.core.EntryEvent event) { log.debug("Build agent added: {}", event.getValue()); - sendBuildAgentInformationOverWebsocket(event.getValue().name()); + sendBuildAgentInformationOverWebsocket(event.getValue().buildAgent().name()); } @Override public void entryRemoved(com.hazelcast.core.EntryEvent event) { log.debug("Build agent removed: {}", event.getOldValue()); - sendBuildAgentInformationOverWebsocket(event.getOldValue().name()); + sendBuildAgentInformationOverWebsocket(event.getOldValue().buildAgent().name()); } @Override public void entryUpdated(com.hazelcast.core.EntryEvent event) { log.debug("Build agent updated: {}", event.getValue()); - sendBuildAgentInformationOverWebsocket(event.getValue().name()); + sendBuildAgentInformationOverWebsocket(event.getValue().buildAgent().name()); } } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java index a450fc545886..71edf64a3fa8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java @@ -225,8 +225,8 @@ public void processResult() { */ private void addResultToBuildAgentsRecentBuildJobs(BuildJobQueueItem buildJob, Result result) { try { - buildAgentInformation.lock(buildJob.buildAgentAddress()); - BuildAgentInformation buildAgent = buildAgentInformation.get(buildJob.buildAgentAddress()); + buildAgentInformation.lock(buildJob.buildAgent().memberAddress()); + BuildAgentInformation buildAgent = buildAgentInformation.get(buildJob.buildAgent().memberAddress()); if (buildAgent != null) { List recentBuildJobs = buildAgent.recentBuildJobs(); for (int i = 0; i < recentBuildJobs.size(); i++) { @@ -235,11 +235,11 @@ private void addResultToBuildAgentsRecentBuildJobs(BuildJobQueueItem buildJob, R break; } } - buildAgentInformation.put(buildJob.buildAgentAddress(), new BuildAgentInformation(buildAgent, recentBuildJobs)); + buildAgentInformation.put(buildJob.buildAgent().memberAddress(), new BuildAgentInformation(buildAgent, recentBuildJobs)); } } finally { - buildAgentInformation.unlock(buildJob.buildAgentAddress()); + buildAgentInformation.unlock(buildJob.buildAgent().memberAddress()); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java index 5da2860ba799..cb4e894c90f7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java @@ -22,6 +22,7 @@ import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildConfig; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; import de.tum.cit.aet.artemis.buildagent.dto.JobTimingInfo; @@ -196,8 +197,10 @@ else if (triggeredByPushTo.equals(RepositoryType.TESTS)) { BuildConfig buildConfig = getBuildConfig(participation, commitHashToBuild, assignmentCommitHash, testCommitHash, programmingExerciseBuildConfig); - BuildJobQueueItem buildJobQueueItem = new BuildJobQueueItem(buildJobId, participation.getBuildPlanId(), null, participation.getId(), courseId, programmingExercise.getId(), - 0, priority, null, repositoryInfo, jobTimingInfo, buildConfig, null); + BuildAgentDTO buildAgent = new BuildAgentDTO(null, null, null); + + BuildJobQueueItem buildJobQueueItem = new BuildJobQueueItem(buildJobId, participation.getBuildPlanId(), buildAgent, participation.getId(), courseId, + programmingExercise.getId(), 0, priority, null, repositoryInfo, jobTimingInfo, buildConfig, null); queue.add(buildJobQueueItem); log.info("Added build job {} to the queue", buildJobId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java index 7c527a155b49..e27ec440d5aa 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java @@ -100,7 +100,7 @@ public void sendBuildAgentSummary(List buildAgentInfo) { } public void sendBuildAgentDetails(BuildAgentInformation buildAgentDetails) { - String channel = "/topic/admin/build-agent/" + buildAgentDetails.name(); + String channel = "/topic/admin/build-agent/" + buildAgentDetails.buildAgent().name(); log.debug("Sending message on topic {}: {}", channel, buildAgentDetails); websocketMessagingService.sendMessage(channel, buildAgentDetails); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java index 9af5fe3b45c1..87b44d4872ba 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java @@ -137,7 +137,7 @@ public List getBuildAgentInformation() { } public List getBuildAgentInformationWithoutRecentBuildJobs() { - return buildAgentInformation.values().stream().map(agent -> new BuildAgentInformation(agent.name(), agent.maxNumberOfConcurrentBuildJobs(), + return buildAgentInformation.values().stream().map(agent -> new BuildAgentInformation(agent.buildAgent(), agent.maxNumberOfConcurrentBuildJobs(), agent.numberOfCurrentBuildJobs(), agent.runningBuildJobs(), agent.status(), null, null)).toList(); } @@ -208,7 +208,7 @@ public void cancelAllRunningBuildJobs() { * @param agentName name of the agent */ public void cancelAllRunningBuildJobsForAgent(String agentName) { - processingJobs.values().stream().filter(job -> Objects.equals(job.buildAgentAddress(), agentName)).forEach(job -> cancelBuildJob(job.id())); + processingJobs.values().stream().filter(job -> Objects.equals(job.buildAgent().name(), agentName)).forEach(job -> cancelBuildJob(job.id())); } /** diff --git a/src/main/resources/config/application-buildagent.yml b/src/main/resources/config/application-buildagent.yml index 1439567b2cc9..fc3e4847f25e 100644 --- a/src/main/resources/config/application-buildagent.yml +++ b/src/main/resources/config/application-buildagent.yml @@ -33,6 +33,7 @@ artemis: container-cleanup: expiry-minutes: 5 cleanup-schedule-minutes: 60 + pause-grace-period-seconds: 60 git: name: Artemis email: artemis@xcit.tum.de diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 85965bb400e0..adcd08605bdb 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -98,6 +98,15 @@ artemis: typescript: default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" + # The following properties are used to configure the Artemis build agent. + # The build agent is responsible for executing the buildJob to test student submissions. + build-agent: + # Name of the build agent. Only lowercase letters, numbers and hyphens are allowed. ([a-z0-9-]+) + short-name: "artemis-build-agent-1" + display-name: "Artemis Build Agent 1" + + + management: endpoints: web: diff --git a/src/main/webapp/app/entities/programming/build-agent-information.model.ts b/src/main/webapp/app/entities/programming/build-agent-information.model.ts new file mode 100644 index 000000000000..c9e5c3c9e71d --- /dev/null +++ b/src/main/webapp/app/entities/programming/build-agent-information.model.ts @@ -0,0 +1,19 @@ +import { BaseEntity } from 'app/shared/model/base-entity'; +import { BuildJob } from 'app/entities/programming/build-job.model'; +import { BuildAgent } from 'app/entities/programming/build-agent.model'; + +export enum BuildAgentStatus { + ACTIVE = 'ACTIVE', + PAUSED = 'PAUSED', + IDLE = 'IDLE', +} + +export class BuildAgentInformation implements BaseEntity { + public id?: number; + public buildAgent?: BuildAgent; + public maxNumberOfConcurrentBuildJobs?: number; + public numberOfCurrentBuildJobs?: number; + public runningBuildJobs?: BuildJob[]; + public status?: BuildAgentStatus; + public recentBuildJobs?: BuildJob[]; +} diff --git a/src/main/webapp/app/entities/programming/build-agent.model.ts b/src/main/webapp/app/entities/programming/build-agent.model.ts index 86c51ffc8b35..f67bfbcd9dd1 100644 --- a/src/main/webapp/app/entities/programming/build-agent.model.ts +++ b/src/main/webapp/app/entities/programming/build-agent.model.ts @@ -1,18 +1,5 @@ -import { BaseEntity } from 'app/shared/model/base-entity'; -import { BuildJob } from 'app/entities/programming/build-job.model'; - -export enum BuildAgentStatus { - ACTIVE = 'ACTIVE', - PAUSED = 'PAUSED', - IDLE = 'IDLE', -} - -export class BuildAgent implements BaseEntity { - public id?: number; +export class BuildAgent { public name?: string; - public maxNumberOfConcurrentBuildJobs?: number; - public numberOfCurrentBuildJobs?: number; - public runningBuildJobs?: BuildJob[]; - public status?: BuildAgentStatus; - public recentBuildJobs?: BuildJob[]; + public memberAddress?: string; + public displayName?: string; } diff --git a/src/main/webapp/app/entities/programming/build-job.model.ts b/src/main/webapp/app/entities/programming/build-job.model.ts index a36bb9756091..2c88534d2626 100644 --- a/src/main/webapp/app/entities/programming/build-job.model.ts +++ b/src/main/webapp/app/entities/programming/build-job.model.ts @@ -4,11 +4,12 @@ import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/programming/build-config.model'; import { Result } from 'app/entities/result.model'; import dayjs from 'dayjs/esm'; +import { BuildAgent } from 'app/entities/programming/build-agent.model'; export class BuildJob implements StringBaseEntity { public id?: string; public name?: string; - public buildAgentAddress?: string; + public buildAgent?: BuildAgent; public participationId?: number; public courseId?: number; public exerciseId?: number; diff --git a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html index 1a88dba6a6d8..ca64b85617f6 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html +++ b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html @@ -2,10 +2,13 @@ @if (buildAgent) {
-
-

- : -

{{ buildAgent.name }}

+
+
+

+ : +

{{ buildAgent.buildAgent?.displayName }}

+
+ {{ buildAgent.buildAgent?.name }} - {{ buildAgent.buildAgent?.memberAddress }}
@if (buildAgent.status === 'PAUSED') { diff --git a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts index de19544b01ad..1408b83c2495 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts +++ b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { BuildAgent } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation } from 'app/entities/programming/build-agent-information.model'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; import { Subscription } from 'rxjs'; import { faCircleCheck, faExclamationCircle, faExclamationTriangle, faPause, faPlay, faTimes } from '@fortawesome/free-solid-svg-icons'; @@ -17,7 +17,7 @@ import { AlertService, AlertType } from 'app/core/util/alert.service'; }) export class BuildAgentDetailsComponent implements OnInit, OnDestroy { protected readonly TriggeredByPushTo = TriggeredByPushTo; - buildAgent: BuildAgent; + buildAgent: BuildAgentInformation; agentName: string; websocketSubscription: Subscription; restSubscription: Subscription; @@ -77,7 +77,7 @@ export class BuildAgentDetailsComponent implements OnInit, OnDestroy { }); } - private updateBuildAgent(buildAgent: BuildAgent) { + private updateBuildAgent(buildAgent: BuildAgentInformation) { this.buildAgent = buildAgent; this.setRecentBuildJobsDuration(); } @@ -100,8 +100,8 @@ export class BuildAgentDetailsComponent implements OnInit, OnDestroy { } cancelAllBuildJobs() { - if (this.buildAgent.name) { - this.buildQueueService.cancelAllRunningBuildJobsForAgent(this.buildAgent.name).subscribe(); + if (this.buildAgent.buildAgent?.name) { + this.buildQueueService.cancelAllRunningBuildJobsForAgent(this.buildAgent.buildAgent?.name).subscribe(); } } @@ -111,8 +111,8 @@ export class BuildAgentDetailsComponent implements OnInit, OnDestroy { } pauseBuildAgent(): void { - if (this.buildAgent.name) { - this.buildAgentsService.pauseBuildAgent(this.buildAgent.name).subscribe({ + if (this.buildAgent.buildAgent?.name) { + this.buildAgentsService.pauseBuildAgent(this.buildAgent.buildAgent.name).subscribe({ next: () => { this.alertService.addAlert({ type: AlertType.SUCCESS, @@ -135,8 +135,8 @@ export class BuildAgentDetailsComponent implements OnInit, OnDestroy { } resumeBuildAgent(): void { - if (this.buildAgent.name) { - this.buildAgentsService.resumeBuildAgent(this.buildAgent.name).subscribe({ + if (this.buildAgent.buildAgent?.name) { + this.buildAgentsService.resumeBuildAgent(this.buildAgent.buildAgent.name).subscribe({ next: () => { this.alertService.addAlert({ type: AlertType.SUCCESS, diff --git a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html index 0982463d75a9..7021b3f79c4d 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html @@ -20,19 +20,27 @@

- + - + - + - - + + {{ value }} + + + + + + + + @@ -75,7 +83,7 @@

{{ value }} @if (value > 0) { - diff --git a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts index b7f232bf0fc4..711fa4a40dce 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts @@ -1,11 +1,12 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { BuildAgent } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation } 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 { BuildQueueService } from 'app/localci/build-queue/build-queue.service'; import { Router } from '@angular/router'; +import { BuildAgent } from 'app/entities/programming/build-agent.model'; @Component({ selector: 'jhi-build-agents', @@ -13,7 +14,7 @@ import { Router } from '@angular/router'; styleUrl: './build-agent-summary.component.scss', }) export class BuildAgentSummaryComponent implements OnInit, OnDestroy { - buildAgents: BuildAgent[] = []; + buildAgents: BuildAgentInformation[] = []; buildCapacity = 0; currentBuilds = 0; channel: string = '/topic/admin/build-agents'; @@ -56,7 +57,7 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy { }); } - private updateBuildAgents(buildAgents: BuildAgent[]) { + private updateBuildAgents(buildAgents: BuildAgentInformation[]) { this.buildAgents = buildAgents; this.buildCapacity = this.buildAgents.reduce((sum, agent) => sum + (agent.maxNumberOfConcurrentBuildJobs || 0), 0); this.currentBuilds = this.buildAgents.reduce((sum, agent) => sum + (agent.numberOfCurrentBuildJobs || 0), 0); @@ -75,10 +76,14 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy { this.buildQueueService.cancelBuildJob(buildJobId).subscribe(); } - cancelAllBuildJobs(buildAgentName: string) { - const buildAgent = this.buildAgents.find((agent) => agent.name === buildAgentName); - if (buildAgent && buildAgent.name) { - this.buildQueueService.cancelAllRunningBuildJobsForAgent(buildAgent.name).subscribe(); + cancelAllBuildJobs(buildAgent?: BuildAgent) { + if (!buildAgent?.name) { + return; + } + + const buildAgentToCancel = this.buildAgents.find((agent) => agent.buildAgent?.name === buildAgent.name); + if (buildAgentToCancel?.buildAgent?.name) { + this.buildQueueService.cancelAllRunningBuildJobsForAgent(buildAgentToCancel.buildAgent?.name).subscribe(); } } } diff --git a/src/main/webapp/app/localci/build-agents/build-agents.service.ts b/src/main/webapp/app/localci/build-agents/build-agents.service.ts index b701b85fe5d0..6aac582940d3 100644 --- a/src/main/webapp/app/localci/build-agents/build-agents.service.ts +++ b/src/main/webapp/app/localci/build-agents/build-agents.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; -import { BuildAgent } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation } from 'app/entities/programming/build-agent-information.model'; import { catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) @@ -13,15 +13,15 @@ export class BuildAgentsService { /** * Get all build agents */ - getBuildAgentSummary(): Observable { - return this.http.get(`${this.adminResourceUrl}/build-agents`); + getBuildAgentSummary(): Observable { + return this.http.get(`${this.adminResourceUrl}/build-agents`); } /** * Get build agent details */ - getBuildAgentDetails(agentName: string): Observable { - return this.http.get(`${this.adminResourceUrl}/build-agent`, { params: { agentName } }).pipe( + getBuildAgentDetails(agentName: string): Observable { + return this.http.get(`${this.adminResourceUrl}/build-agent`, { params: { agentName } }).pipe( catchError((err) => { return throwError(() => new Error(`Failed to fetch build agent details ${agentName}\n${err.message}`)); }), diff --git a/src/main/webapp/app/localci/build-queue/build-queue.component.html b/src/main/webapp/app/localci/build-queue/build-queue.component.html index 4e0e1e095754..a8f9a582cbf3 100644 --- a/src/main/webapp/app/localci/build-queue/build-queue.component.html +++ b/src/main/webapp/app/localci/build-queue/build-queue.component.html @@ -107,15 +107,15 @@

+ - + - + - - + + {{ value }} diff --git a/src/main/webapp/i18n/de/buildAgents.json b/src/main/webapp/i18n/de/buildAgents.json index 2cd4ca72def3..186bd1432540 100644 --- a/src/main/webapp/i18n/de/buildAgents.json +++ b/src/main/webapp/i18n/de/buildAgents.json @@ -5,6 +5,7 @@ "summary": "Build Agenten Zusammenfassung", "details": "Build Agenten Details", "name": "Name", + "memberAddress": "Agentenadresse", "maxNumberOfConcurrentBuildJobs": "Maximale Anzahl an parallelen Build Jobs", "numberOfCurrentBuildJobs": "Anzahl aktueller Build Jobs", "runningBuildJobs": "Laufende Build Jobs", diff --git a/src/main/webapp/i18n/en/buildAgents.json b/src/main/webapp/i18n/en/buildAgents.json index e1401d97e272..1610e1bd0847 100644 --- a/src/main/webapp/i18n/en/buildAgents.json +++ b/src/main/webapp/i18n/en/buildAgents.json @@ -5,6 +5,7 @@ "summary": "Build Agents Summary", "details": "Build Agents Details", "name": "Name", + "memberAddress": "Member Address", "maxNumberOfConcurrentBuildJobs": "Max # of concurrent build jobs", "numberOfCurrentBuildJobs": "# of current build jobs", "runningBuildJobs": "Running build jobs", diff --git a/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java index b4012d1f7a6f..eab5c2190d4c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java @@ -28,6 +28,7 @@ import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildConfig; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; import de.tum.cit.aet.artemis.core.exception.LocalCIException; @@ -92,7 +93,9 @@ void testPullDockerImage() { doReturn(inspectImageCmd).when(dockerClient).inspectImageCmd(anyString()); doThrow(new NotFoundException("")).when(inspectImageCmd).exec(); BuildConfig buildConfig = new BuildConfig("echo 'test'", "test-image-name", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); - var build = new BuildJobQueueItem("1", "job1", "address1", 1, 1, 1, 1, 1, BuildStatus.SUCCESSFUL, null, null, buildConfig, null); + + BuildAgentDTO buildAgent = new BuildAgentDTO("buildagent1", "address1", "buildagent1"); + var build = new BuildJobQueueItem("1", "job1", buildAgent, 1, 1, 1, 1, 1, BuildStatus.SUCCESSFUL, null, null, buildConfig, null); // Pull image try { buildAgentDockerService.pullDockerImage(build, new BuildLogsMap()); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java index 0be99f7c9aa1..bbaa18df71b1 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java @@ -486,8 +486,8 @@ void testCustomCheckoutPaths() { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testPauseAndResumeBuildAgent() { - String memberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString(); - hazelcastInstance.getTopic("pauseBuildAgentTopic").publish(memberAddress); + String buildAgentName = "artemis-build-agent-test"; + hazelcastInstance.getTopic("pauseBuildAgentTopic").publish(buildAgentName); ProgrammingExerciseStudentParticipation studentParticipation = localVCLocalCITestService.createParticipation(programmingExercise, student1Login); @@ -500,7 +500,7 @@ void testPauseAndResumeBuildAgent() { return buildJobQueueItem != null && buildJobQueueItem.buildConfig().commitHashToBuild().equals(commitHash) && !buildJobMap.containsKey(buildJobQueueItem.id()); }); - hazelcastInstance.getTopic("resumeBuildAgentTopic").publish(memberAddress); + hazelcastInstance.getTopic("resumeBuildAgentTopic").publish(buildAgentName); localVCLocalCITestService.testLatestSubmission(studentParticipation.getId(), commitHash, 1, false); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java index e5c7f52e1413..a4d157d1a690 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; @@ -23,6 +24,7 @@ import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; import de.tum.cit.aet.artemis.assessment.domain.Result; +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; import de.tum.cit.aet.artemis.buildagent.dto.BuildConfig; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; @@ -67,6 +69,14 @@ protected String getTestPrefix() { return TEST_PREFIX; } + @Value("${artemis.continuous-integration.build-agent.short-name}") + private String buildAgentShortName; + + @Value("${artemis.continuous-integration.build-agent.display-name:}") + private String buildAgentDisplayName; + + private BuildAgentDTO buildAgent; + @BeforeEach void createJobs() { // temporarily remove listener to avoid triggering build job processing @@ -79,17 +89,19 @@ void createJobs() { BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); RepositoryInfo repositoryInfo = new RepositoryInfo("test", null, RepositoryType.USER, "test", "test", "test", null, null); - job1 = new BuildJobQueueItem("1", "job1", "address1", 1, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo1, buildConfig, null); - job2 = new BuildJobQueueItem("2", "job2", "address1", 2, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo2, buildConfig, null); String memberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString(); - agent1 = new BuildAgentInformation(memberAddress, 1, 0, new ArrayList<>(List.of(job1)), BuildAgentInformation.BuildAgentStatus.IDLE, new ArrayList<>(List.of(job2)), null); - BuildJobQueueItem finishedJobQueueItem1 = new BuildJobQueueItem("3", "job3", "address1", 3, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo1, + buildAgent = new BuildAgentDTO(buildAgentShortName, memberAddress, buildAgentDisplayName); + + job1 = new BuildJobQueueItem("1", "job1", buildAgent, 1, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo1, buildConfig, null); + job2 = new BuildJobQueueItem("2", "job2", buildAgent, 2, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo2, buildConfig, null); + agent1 = new BuildAgentInformation(buildAgent, 1, 0, new ArrayList<>(List.of(job1)), BuildAgentInformation.BuildAgentStatus.IDLE, new ArrayList<>(List.of(job2)), null); + BuildJobQueueItem finishedJobQueueItem1 = new BuildJobQueueItem("3", "job3", buildAgent, 3, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo1, buildConfig, null); - BuildJobQueueItem finishedJobQueueItem2 = new BuildJobQueueItem("4", "job4", "address1", 4, course.getId() + 1, 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo2, + BuildJobQueueItem finishedJobQueueItem2 = new BuildJobQueueItem("4", "job4", buildAgent, 4, course.getId() + 1, 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo2, buildConfig, null); - BuildJobQueueItem finishedJobQueueItem3 = new BuildJobQueueItem("5", "job5", "address1", 5, course.getId() + 2, 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo3, + BuildJobQueueItem finishedJobQueueItem3 = new BuildJobQueueItem("5", "job5", buildAgent, 5, course.getId() + 2, 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo3, buildConfig, null); - BuildJobQueueItem finishedJobQueueItemForLogs = new BuildJobQueueItem("6", "job5", "address1", 5, course.getId(), programmingExercise.getId(), 1, 1, BuildStatus.FAILED, + BuildJobQueueItem finishedJobQueueItemForLogs = new BuildJobQueueItem("6", "job5", buildAgent, 5, course.getId(), programmingExercise.getId(), 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo3, buildConfig, null); var result1 = new Result().successful(true).rated(true).score(100D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); var result2 = new Result().successful(false).rated(true).score(0D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); @@ -187,8 +199,8 @@ void testGetBuildAgents_returnsAgents() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testGetBuildAgentDetails_returnsAgent() throws Exception { - var retrievedAgent = request.get("/api/admin/build-agent?agentName=" + agent1.name(), HttpStatus.OK, BuildAgentInformation.class); - assertThat(retrievedAgent.name()).isEqualTo(agent1.name()); + var retrievedAgent = request.get("/api/admin/build-agent?agentName=" + agent1.buildAgent().name(), HttpStatus.OK, BuildAgentInformation.class); + assertThat(retrievedAgent.buildAgent().name()).isEqualTo(agent1.buildAgent().name()); } @Test @@ -241,7 +253,7 @@ void testCancelAllRunningBuildJobsForCourse() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testCancelAllRunningBuildJobsForAgent() throws Exception { - request.delete("/api/admin/cancel-all-running-jobs-for-agent?agentName=" + agent1.name(), HttpStatus.NO_CONTENT); + request.delete("/api/admin/cancel-all-running-jobs-for-agent?agentName=" + agent1.buildAgent().name(), HttpStatus.NO_CONTENT); } @Test @@ -270,7 +282,7 @@ void testGetFinishedBuildJobs_returnsFilteredJobs() throws Exception { ZonedDateTime.now().plusDays(1).plusMinutes(10)); BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); RepositoryInfo repositoryInfo = new RepositoryInfo("test", null, RepositoryType.USER, "test", "test", "test", null, null); - var failedJob1 = new BuildJobQueueItem("5", "job5", "address1", 1, course.getId(), 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo, buildConfig, null); + var failedJob1 = new BuildJobQueueItem("5", "job5", buildAgent, 1, course.getId(), 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo, buildConfig, null); var jobResult = new Result().successful(false).rated(true).score(0D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); var failedFinishedJob = new BuildJob(failedJob1, BuildStatus.FAILED, jobResult); @@ -349,10 +361,13 @@ void testGetBuildJobStatistics() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testPauseBuildAgent() throws Exception { - request.put("/api/admin/agent/" + URLEncoder.encode(agent1.name(), StandardCharsets.UTF_8) + "/pause", null, HttpStatus.NO_CONTENT); - await().until(() -> buildAgentInformation.get(agent1.name()).status() == BuildAgentInformation.BuildAgentStatus.PAUSED); + // 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); + await().until(() -> buildAgentInformation.get(agent1.buildAgent().memberAddress()).status() == BuildAgentInformation.BuildAgentStatus.PAUSED); - request.put("/api/admin/agent/" + URLEncoder.encode(agent1.name(), StandardCharsets.UTF_8) + "/resume", null, HttpStatus.NO_CONTENT); - await().until(() -> buildAgentInformation.get(agent1.name()).status() == BuildAgentInformation.BuildAgentStatus.IDLE); + request.put("/api/admin/agent/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/resume", null, HttpStatus.NO_CONTENT); + await().until(() -> buildAgentInformation.get(agent1.buildAgent().memberAddress()).status() == BuildAgentInformation.BuildAgentStatus.IDLE); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java index a3fbfd1b2a7b..fe88b498bb47 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java @@ -20,6 +20,7 @@ import com.hazelcast.collection.IQueue; import com.hazelcast.map.IMap; +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildConfig; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; import de.tum.cit.aet.artemis.buildagent.dto.JobTimingInfo; @@ -73,9 +74,12 @@ void testReturnCorrectBuildStatus() { BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); RepositoryInfo repositoryInfo = new RepositoryInfo("test", null, RepositoryType.USER, "test", "test", "test", null, null); - BuildJobQueueItem job1 = new BuildJobQueueItem("1", "job1", "address1", participation.getId(), course.getId(), 1, 1, 1, + String memberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString(); + BuildAgentDTO buildAgent = new BuildAgentDTO("artemis-build-agent-test", memberAddress, "artemis-build-agent-test"); + + BuildJobQueueItem job1 = new BuildJobQueueItem("1", "job1", buildAgent, participation.getId(), course.getId(), 1, 1, 1, de.tum.cit.aet.artemis.programming.domain.build.BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo, buildConfig, null); - BuildJobQueueItem job2 = new BuildJobQueueItem("2", "job2", "address1", participation.getId(), course.getId(), 1, 1, 1, + BuildJobQueueItem job2 = new BuildJobQueueItem("2", "job2", buildAgent, participation.getId(), course.getId(), 1, 1, 1, de.tum.cit.aet.artemis.programming.domain.build.BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo, buildConfig, null); queuedJobs = hazelcastInstance.getQueue("buildJobQueue"); diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts index 2db5c31ac80b..0b14d136c97c 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts @@ -9,7 +9,7 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { DataTableComponent } from 'app/shared/data-table/data-table.component'; import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; import { NgxDatatableModule } from '@siemens/ngx-datatable'; -import { BuildAgent, BuildAgentStatus } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation, BuildAgentStatus } from '../../../../../../main/webapp/app/entities/programming/build-agent-information.model'; import { RepositoryInfo, TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/programming/build-config.model'; @@ -69,7 +69,7 @@ describe('BuildAgentDetailsComponent', () => { { id: '2', name: 'Build Job 2', - buildAgentAddress: 'agent2', + buildAgent: { name: 'agent2', memberAddress: 'localhost:8080', displayName: 'Agent 2' }, participationId: 102, courseId: 10, exerciseId: 100, @@ -82,7 +82,7 @@ describe('BuildAgentDetailsComponent', () => { { id: '4', name: 'Build Job 4', - buildAgentAddress: 'agent4', + buildAgent: { name: 'agent4', memberAddress: 'localhost:8080', displayName: 'Agent 4' }, participationId: 104, courseId: 10, exerciseId: 100, @@ -98,7 +98,7 @@ describe('BuildAgentDetailsComponent', () => { { id: '1', name: 'Build Job 1', - buildAgentAddress: 'agent1', + buildAgent: { name: 'agent1', memberAddress: 'localhost:8080', displayName: 'Agent 1' }, participationId: 101, courseId: 10, exerciseId: 100, @@ -110,9 +110,9 @@ describe('BuildAgentDetailsComponent', () => { }, ]; - const mockBuildAgent: BuildAgent = { + const mockBuildAgent: BuildAgentInformation = { id: 1, - name: 'buildagent1', + buildAgent: { name: 'agent1', memberAddress: 'localhost:8080', displayName: 'Agent 1' }, maxNumberOfConcurrentBuildJobs: 2, numberOfCurrentBuildJobs: 2, runningBuildJobs: mockRunningJobs1, @@ -139,7 +139,7 @@ describe('BuildAgentDetailsComponent', () => { fixture = TestBed.createComponent(BuildAgentDetailsComponent); component = fixture.componentInstance; activatedRoute = fixture.debugElement.injector.get(ActivatedRoute) as MockActivatedRoute; - activatedRoute.setParameters({ agentName: mockBuildAgent.name }); + activatedRoute.setParameters({ agentName: mockBuildAgent.buildAgent?.name }); alertService = TestBed.inject(AlertService); alertServiceAddAlertStub = jest.spyOn(alertService, 'addAlert'); })); @@ -164,8 +164,8 @@ describe('BuildAgentDetailsComponent', () => { component.ngOnInit(); expect(component.buildAgent).toEqual(mockBuildAgent); - expect(mockWebsocketService.subscribe).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.name); - expect(mockWebsocketService.receive).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.name); + expect(mockWebsocketService.subscribe).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.buildAgent?.name); + expect(mockWebsocketService.receive).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.buildAgent?.name); }); it('should unsubscribe from the websocket channel on destruction', () => { @@ -175,7 +175,7 @@ describe('BuildAgentDetailsComponent', () => { component.ngOnDestroy(); - expect(mockWebsocketService.unsubscribe).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.name); + expect(mockWebsocketService.unsubscribe).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.buildAgent?.name); }); it('should set recent build jobs duration', () => { @@ -213,7 +213,7 @@ describe('BuildAgentDetailsComponent', () => { }); it('should show an alert when pausing build agent without a name', () => { - component.buildAgent = { ...mockBuildAgent, name: '' }; + component.buildAgent = { ...mockBuildAgent, buildAgent: { ...mockBuildAgent.buildAgent, name: '' } }; component.pauseBuildAgent(); expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ @@ -223,7 +223,7 @@ describe('BuildAgentDetailsComponent', () => { }); it('should show an alert when resuming build agent without a name', () => { - component.buildAgent = { ...mockBuildAgent, name: '' }; + component.buildAgent = { ...mockBuildAgent, buildAgent: { ...mockBuildAgent.buildAgent, name: '' } }; component.resumeBuildAgent(); expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts index 9a4e94b1cb7e..fe898f69474a 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts @@ -10,7 +10,7 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { DataTableComponent } from 'app/shared/data-table/data-table.component'; import { MockComponent, MockPipe } from 'ng-mocks'; import { NgxDatatableModule } from '@siemens/ngx-datatable'; -import { BuildAgent, BuildAgentStatus } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation, BuildAgentStatus } from '../../../../../../main/webapp/app/entities/programming/build-agent-information.model'; import { RepositoryInfo, TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/programming/build-config.model'; @@ -63,7 +63,7 @@ describe('BuildAgentSummaryComponent', () => { { id: '2', name: 'Build Job 2', - buildAgentAddress: 'agent2', + buildAgent: { name: 'agent2', memberAddress: 'localhost:8080', displayName: 'Agent 2' }, participationId: 102, courseId: 10, exerciseId: 100, @@ -76,7 +76,7 @@ describe('BuildAgentSummaryComponent', () => { { id: '4', name: 'Build Job 4', - buildAgentAddress: 'agent4', + buildAgent: { name: 'agent4', memberAddress: 'localhost:8080', displayName: 'Agent 4' }, participationId: 104, courseId: 10, exerciseId: 100, @@ -92,7 +92,7 @@ describe('BuildAgentSummaryComponent', () => { { id: '1', name: 'Build Job 1', - buildAgentAddress: 'agent1', + buildAgent: { name: 'agent1', memberAddress: 'localhost:8080', displayName: 'Agent 1' }, participationId: 101, courseId: 10, exerciseId: 100, @@ -105,7 +105,7 @@ describe('BuildAgentSummaryComponent', () => { { id: '3', name: 'Build Job 3', - buildAgentAddress: 'agent3', + buildAgent: { name: 'agent3', memberAddress: 'localhost:8080', displayName: 'Agent 3' }, participationId: 103, courseId: 10, exerciseId: 100, @@ -117,10 +117,10 @@ describe('BuildAgentSummaryComponent', () => { }, ]; - const mockBuildAgents: BuildAgent[] = [ + const mockBuildAgents: BuildAgentInformation[] = [ { id: 1, - name: 'buildagent1', + buildAgent: { name: 'buildagent1', displayName: 'Build Agent 1', memberAddress: 'agent1' }, maxNumberOfConcurrentBuildJobs: 2, numberOfCurrentBuildJobs: 2, runningBuildJobs: mockRunningJobs1, @@ -128,7 +128,7 @@ describe('BuildAgentSummaryComponent', () => { }, { id: 2, - name: 'buildagent2', + buildAgent: { name: 'buildagent2', displayName: 'Build Agent 2', memberAddress: 'agent2' }, maxNumberOfConcurrentBuildJobs: 2, numberOfCurrentBuildJobs: 2, runningBuildJobs: mockRunningJobs2, @@ -196,9 +196,9 @@ describe('BuildAgentSummaryComponent', () => { const spy = jest.spyOn(component, 'cancelAllBuildJobs'); component.ngOnInit(); - component.cancelAllBuildJobs(buildAgent.name!); + component.cancelAllBuildJobs(buildAgent.buildAgent); - expect(spy).toHaveBeenCalledExactlyOnceWith(buildAgent.name!); + expect(spy).toHaveBeenCalledExactlyOnceWith(buildAgent.buildAgent); }); it('should calculate the build capacity and current builds', () => { diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts index c73f5d49ee15..261acf610ff2 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts @@ -8,7 +8,7 @@ import { BuildJob } from 'app/entities/programming/build-job.model'; import dayjs from 'dayjs/esm'; import { lastValueFrom } from 'rxjs'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; -import { BuildAgent } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation } from '../../../../../../main/webapp/app/entities/programming/build-agent-information.model'; import { RepositoryInfo, TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/programming/build-config.model'; @@ -16,7 +16,7 @@ import { BuildConfig } from 'app/entities/programming/build-config.model'; describe('BuildAgentsService', () => { let service: BuildAgentsService; let httpMock: HttpTestingController; - let element: BuildAgent; + let element: BuildAgentInformation; const repositoryInfo: RepositoryInfo = { repositoryName: 'repo2', @@ -50,7 +50,7 @@ describe('BuildAgentsService', () => { { id: '2', name: 'Build Job 2', - buildAgentAddress: 'agent2', + buildAgent: { name: 'agent2', memberAddress: 'localhost:8080', displayName: 'Agent 2' }, participationId: 102, courseId: 10, exerciseId: 100, @@ -63,7 +63,7 @@ describe('BuildAgentsService', () => { { id: '4', name: 'Build Job 4', - buildAgentAddress: 'agent4', + buildAgent: { name: 'agent4', memberAddress: 'localhost:8080', displayName: 'Agent 4' }, participationId: 104, courseId: 10, exerciseId: 100, @@ -82,9 +82,9 @@ describe('BuildAgentsService', () => { }); service = TestBed.inject(BuildAgentsService); httpMock = TestBed.inject(HttpTestingController); - element = new BuildAgent(); + element = new BuildAgentInformation(); element.id = 1; - element.name = 'BuildAgent1'; + element.buildAgent = { name: 'buildAgent1', memberAddress: 'localhost:8080', displayName: 'Build Agent 1' }; element.maxNumberOfConcurrentBuildJobs = 3; element.numberOfCurrentBuildJobs = 1; element.runningBuildJobs = mockRunningJobs; diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 484e5f300ba1..efe58275f415 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -76,6 +76,8 @@ artemis: default: "~~invalid~~" typescript: default: "~~invalid~~" + build-agent: + short-name: "artemis-build-agent-test" spring: application: