From 49540744992c09833d49ea05336c3d5dc9526c2f Mon Sep 17 00:00:00 2001 From: Mohamed Bilel Besrour <58034472+BBesrour@users.noreply.github.com> Date: Sat, 2 Nov 2024 07:47:59 +0100 Subject: [PATCH] Integrated code lifecycle: Allow admins to set build timeout options via application properties (#9603) --- docs/admin/setup/distributed.rst | 3 +- docs/admin/setup/programming-exercises.rst | 21 ++++++++ .../exercises/programming-exercise-setup.inc | 4 +- .../service/BuildJobManagementService.java | 4 +- .../aet/artemis/core/config/Constants.java | 6 +++ .../localci/LocalCIInfoContributor.java | 15 ++++++ .../config/application-buildagent.yml | 5 +- .../resources/config/application-localci.yml | 5 +- ...xercise-build-configuration.component.html | 6 +-- ...-exercise-build-configuration.component.ts | 32 +++++++++++- .../layouts/profiles/profile-info.model.ts | 3 ++ .../layouts/profiles/profile.service.ts | 4 ++ ...cise-build-configuration.component.spec.ts | 49 ++++++++++++++++++- 13 files changed, 145 insertions(+), 12 deletions(-) diff --git a/docs/admin/setup/distributed.rst b/docs/admin/setup/distributed.rst index b2d1a12822d3..def7d2b7a980 100644 --- a/docs/admin/setup/distributed.rst +++ b/docs/admin/setup/distributed.rst @@ -608,7 +608,6 @@ These credentials are used to clone repositories via HTTPS. You must also add th specify-concurrent-builds: true # Set to false, if the number of concurrent build jobs should be chosen automatically based on system resources concurrent-build-size: 1 # If previous value is true: Set to desired value but keep available system resources in mind asynchronous: true - timeout-seconds: 240 # Time limit of a build before it will be cancelled build-container-prefix: local-ci- image-cleanup: enabled: true # If set to true (recommended), old Docker images will be deleted on a schedule. @@ -620,6 +619,8 @@ These credentials are used to clone repositories via HTTPS. You must also add th 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. + build-timeout-seconds: + max: 240 # (Optional, default 240) Maximum time in seconds a build job is allowed to run. If a build job exceeds this time, it will be cancelled. Please note that ``artemis.continuous-integration.build-agent.short-name`` must be provided. Otherwise, the build agent will not start. diff --git a/docs/admin/setup/programming-exercises.rst b/docs/admin/setup/programming-exercises.rst index f7f01ce1cad4..f51ce2f2a3a7 100644 --- a/docs/admin/setup/programming-exercises.rst +++ b/docs/admin/setup/programming-exercises.rst @@ -339,3 +339,24 @@ Adjust ``dockerFlags`` and ``mavenFlags`` only for student submissions, like thi dockerFlags += ' --network none' mavenFlags += ' --offline' } + + +Timeout Options +^^^^^^^^^^^^^^^ + +You can adjust possible :ref:`timeout options` for the build process in :ref:`Integrated Code Lifecycle Setup `. +These values will determine what is the minimum, maximum, and default value for the build timeout in seconds that can be set in the Artemis UI. +The max value is the upper limit for the timeout, if the value is set higher than the max value, the max value will be used. + +If you want to change these values, you need to change them in ``localci`` and ``buildagent`` nodes. +The corresponding configuration files are ``application-localci.yml`` and ``application-buildagent.yml``. + + + .. code-block:: yaml + + artemis: + continuous-integration: + build-timeout-seconds: + min: + max: + default: diff --git a/docs/user/exercises/programming-exercise-setup.inc b/docs/user/exercises/programming-exercise-setup.inc index 0d1adbff6297..8a2dc7cf9f78 100644 --- a/docs/user/exercises/programming-exercise-setup.inc +++ b/docs/user/exercises/programming-exercise-setup.inc @@ -398,7 +398,7 @@ You must then change the paths in the build script if necessary. Please refer to - Changing the checkout paths can lead to build errors if the build script is not adapted accordingly. - For C programming exercises, if used with the default docker image, changing the checkout paths will lead to build errors. The default docker image is configured to work with the default checkout paths. -.. _configure_static_code_analysis_tools: +.. _edit_build_duration: Edit Maximum Build Duration ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -412,6 +412,8 @@ You can change the maximum build duration by using the slider. .. figure:: programming/timeout-slider.png :align: center +.. _configure_static_code_analysis_tools: + Configure static code analysis ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java index b57d0a45323d..551b20f8bdd9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java @@ -58,7 +58,7 @@ public class BuildJobManagementService { private final ReentrantLock lock = new ReentrantLock(); - @Value("${artemis.continuous-integration.timeout-seconds:120}") + @Value("${artemis.continuous-integration.build-timeout-seconds.max:240}") private int timeoutSeconds; @Value("${artemis.continuous-integration.asynchronous:true}") @@ -152,7 +152,7 @@ public CompletableFuture executeBuildJob(BuildJobQueueItem buildJob } int buildJobTimeoutSeconds; - if (buildJobItem.buildConfig().timeoutSeconds() != 0 && buildJobItem.buildConfig().timeoutSeconds() < this.timeoutSeconds) { + if (buildJobItem.buildConfig().timeoutSeconds() > 0 && buildJobItem.buildConfig().timeoutSeconds() < this.timeoutSeconds) { buildJobTimeoutSeconds = buildJobItem.buildConfig().timeoutSeconds(); } else { diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java b/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java index c3f2ddc1c320..6ddd70dad841 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java @@ -275,6 +275,12 @@ public final class Constants { public static final String CONTINUOUS_INTEGRATION_NAME = "continuousIntegrationName"; + public static final String INSTRUCTOR_BUILD_TIMEOUT_MIN_OPTION = "buildTimeoutMin"; + + public static final String INSTRUCTOR_BUILD_TIMEOUT_MAX_OPTION = "buildTimeoutMax"; + + public static final String INSTRUCTOR_BUILD_TIMEOUT_DEFAULT_OPTION = "buildTimeoutDefault"; + public static final String USE_EXTERNAL = "useExternal"; public static final String EXTERNAL_CREDENTIAL_PROVIDER = "externalCredentialProvider"; diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIInfoContributor.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIInfoContributor.java index e43721594008..f44b2a481096 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIInfoContributor.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIInfoContributor.java @@ -2,6 +2,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALCI; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.info.Info; import org.springframework.boot.actuate.info.InfoContributor; import org.springframework.context.annotation.Profile; @@ -13,9 +14,23 @@ @Profile(PROFILE_LOCALCI) public class LocalCIInfoContributor implements InfoContributor { + @Value("${artemis.continuous-integration.build-timeout-seconds.min:10}") + private int minInstructorBuildTimeoutOption; + + @Value("${artemis.continuous-integration.build-timeout-seconds.max:240}") + private int maxInstructorBuildTimeoutOption; + + @Value("${artemis.continuous-integration.build-timeout-seconds.default:120}") + private int defaultInstructorBuildTimeoutOption; + @Override public void contribute(Info.Builder builder) { // Store name of the continuous integration system builder.withDetail(Constants.CONTINUOUS_INTEGRATION_NAME, "Local CI"); + + // Store the build timeout options for the instructor build + builder.withDetail(Constants.INSTRUCTOR_BUILD_TIMEOUT_MIN_OPTION, minInstructorBuildTimeoutOption); + builder.withDetail(Constants.INSTRUCTOR_BUILD_TIMEOUT_MAX_OPTION, maxInstructorBuildTimeoutOption); + builder.withDetail(Constants.INSTRUCTOR_BUILD_TIMEOUT_DEFAULT_OPTION, defaultInstructorBuildTimeoutOption); } } diff --git a/src/main/resources/config/application-buildagent.yml b/src/main/resources/config/application-buildagent.yml index fc3e4847f25e..2872d91575cc 100644 --- a/src/main/resources/config/application-buildagent.yml +++ b/src/main/resources/config/application-buildagent.yml @@ -18,7 +18,6 @@ artemis: specify-concurrent-builds: false concurrent-build-size: 1 asynchronous: true - timeout-seconds: 120 build-container-prefix: local-ci- proxies: use-system-proxy: false @@ -34,6 +33,10 @@ artemis: expiry-minutes: 5 cleanup-schedule-minutes: 60 pause-grace-period-seconds: 60 + build-timeout-seconds: + min: 10 + default: 120 + max: 240 git: name: Artemis email: artemis@xcit.tum.de diff --git a/src/main/resources/config/application-localci.yml b/src/main/resources/config/application-localci.yml index 38a630f0af44..5f687c9ccd5f 100644 --- a/src/main/resources/config/application-localci.yml +++ b/src/main/resources/config/application-localci.yml @@ -14,7 +14,6 @@ artemis: # If true, the CI jobs will be executed asynchronously. If false, the CI jobs will be executed synchronously (e.g. for debugging and tests). asynchronous: true # The maximum number of seconds that a CI job is allowed to run. If the job exceeds this time, it will be terminated. - timeout-seconds: 120 # The number of builds that can be in the local CI queue at the same time. Choosing a small value can prevent the CI system from being overloaded on slow machines. Jobs that are submitted when the queue is already full, will be discarded. queue-size-limit: 100 # The prefix that is used for the Docker containers that are created by the local CI system. @@ -32,5 +31,9 @@ artemis: expiry-days: 2 # Time of cleanup (cron expression) cleanup-schedule-time: 0 0 3 * * * + build-timeout-seconds: + min: 10 + default: 120 + max: 240 diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html index 6360bb509017..8460ca15d6fd 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.html @@ -28,10 +28,10 @@ class="w-100 mt-1" #timeoutField="ngModel" type="range" - min="0" - max="120" + min="{{ timeoutMinValue }}" + max="{{ timeoutMaxValue }}" step="1" - value="120" + value="{{ timeoutDefaultValue }}" id="field_timeout" [ngModel]="timeout()" (ngModelChange)="timeoutChange!.emit($event)" diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.ts index a506333c9d87..dedb28418571 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component.ts @@ -1,12 +1,15 @@ -import { Component, input, output, viewChild } from '@angular/core'; +import { Component, OnInit, inject, input, output, viewChild } from '@angular/core'; import { NgModel } from '@angular/forms'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; @Component({ selector: 'jhi-programming-exercise-build-configuration', templateUrl: './programming-exercise-build-configuration.component.html', styleUrls: ['../../../../programming-exercise-form.scss'], }) -export class ProgrammingExerciseBuildConfigurationComponent { +export class ProgrammingExerciseBuildConfigurationComponent implements OnInit { + private profileService = inject(ProfileService); + dockerImage = input.required(); dockerImageChange = output(); @@ -18,4 +21,29 @@ export class ProgrammingExerciseBuildConfigurationComponent { dockerImageField = viewChild('dockerImageField'); timeoutField = viewChild('timeoutField'); + + timeoutMinValue?: number; + timeoutMaxValue?: number; + timeoutDefaultValue?: number; + + ngOnInit() { + this.profileService.getProfileInfo().subscribe((profileInfo) => { + if (profileInfo) { + this.timeoutMinValue = profileInfo.buildTimeoutMin ?? 10; + + // Set the maximum timeout value to 240 if it is not set in the profile or if it is less than the minimum value + this.timeoutMaxValue = profileInfo.buildTimeoutMax && profileInfo.buildTimeoutMax > this.timeoutMinValue ? profileInfo.buildTimeoutMax : 240; + + // Set the default timeout value to 120 if it is not set in the profile or if it is not in the valid range + this.timeoutDefaultValue = 120; + if (profileInfo.buildTimeoutDefault && profileInfo.buildTimeoutDefault >= this.timeoutMinValue && profileInfo.buildTimeoutDefault <= this.timeoutMaxValue) { + this.timeoutDefaultValue = profileInfo.buildTimeoutDefault; + } + + if (!this.timeout) { + this.timeoutChange.emit(this.timeoutDefaultValue); + } + } + }); + } } diff --git a/src/main/webapp/app/shared/layouts/profiles/profile-info.model.ts b/src/main/webapp/app/shared/layouts/profiles/profile-info.model.ts index 293383532080..7039c00dc27e 100644 --- a/src/main/webapp/app/shared/layouts/profiles/profile-info.model.ts +++ b/src/main/webapp/app/shared/layouts/profiles/profile-info.model.ts @@ -37,6 +37,9 @@ export class ProfileInfo { public useVersionControlAccessToken?: boolean; public showCloneUrlWithoutToken?: boolean; public continuousIntegrationName?: string; + public buildTimeoutMin?: number; + public buildTimeoutMax?: number; + public buildTimeoutDefault?: number; public programmingLanguageFeatures: ProgrammingLanguageFeature[]; public saml2?: Saml2Config; public textAssessmentAnalyticsEnabled?: boolean; diff --git a/src/main/webapp/app/shared/layouts/profiles/profile.service.ts b/src/main/webapp/app/shared/layouts/profiles/profile.service.ts index f3a21ec5fbd6..14b5d5b2ce31 100644 --- a/src/main/webapp/app/shared/layouts/profiles/profile.service.ts +++ b/src/main/webapp/app/shared/layouts/profiles/profile.service.ts @@ -88,6 +88,10 @@ export class ProfileService { profileInfo.theiaPortalURL = data.theiaPortalURL ?? ''; + profileInfo.buildTimeoutMin = data.buildTimeoutMin; + profileInfo.buildTimeoutMax = data.buildTimeoutMax; + profileInfo.buildTimeoutDefault = data.buildTimeoutDefault; + return profileInfo; }), ) diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-build-configuration.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-build-configuration.component.spec.ts index 978f821fb406..fa62e609f38f 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-build-configuration.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-build-configuration.component.spec.ts @@ -3,15 +3,22 @@ import { ArtemisTestModule } from '../../test.module'; import { ProgrammingExerciseBuildConfigurationComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-build-configuration/programming-exercise-build-configuration.component'; import { FormsModule } from '@angular/forms'; import { ArtemisProgrammingExerciseUpdateModule } from 'app/exercises/programming/manage/update/programming-exercise-update.module'; +import { of } from 'rxjs'; +import { ProfileService } from '../../../../../main/webapp/app/shared/layouts/profiles/profile.service'; describe('ProgrammingExercise Docker Image', () => { let comp: ProgrammingExerciseBuildConfigurationComponent; + let profileServiceMock: { getProfileInfo: jest.Mock }; beforeEach(() => { + profileServiceMock = { + getProfileInfo: jest.fn(), + }; + TestBed.configureTestingModule({ imports: [ArtemisTestModule, FormsModule, ArtemisProgrammingExerciseUpdateModule], declarations: [ProgrammingExerciseBuildConfigurationComponent], - providers: [], + providers: [{ provide: ProfileService, useValue: profileServiceMock }], }) .compileComponents() .then(); @@ -36,4 +43,44 @@ describe('ProgrammingExercise Docker Image', () => { comp.timeoutChange.subscribe((value) => expect(value).toBe(20)); comp.timeoutChange.emit(20); }); + + it('should set timeout options', () => { + profileServiceMock.getProfileInfo.mockReturnValue( + of({ + buildTimeoutMin: undefined, + buildTimeoutMax: undefined, + buildTimeoutDefault: undefined, + }), + ); + + comp.ngOnInit(); + expect(comp.timeoutMinValue).toBe(10); + expect(comp.timeoutMaxValue).toBe(240); + expect(comp.timeoutDefaultValue).toBe(120); + + profileServiceMock.getProfileInfo.mockReturnValue( + of({ + buildTimeoutMin: 0, + buildTimeoutMax: 360, + buildTimeoutDefault: 60, + }), + ); + comp.ngOnInit(); + expect(comp.timeoutMinValue).toBe(0); + expect(comp.timeoutMaxValue).toBe(360); + expect(comp.timeoutDefaultValue).toBe(60); + + profileServiceMock.getProfileInfo.mockReturnValue( + of({ + buildTimeoutMin: 100, + buildTimeoutMax: 20, + buildTimeoutDefault: 10, + }), + ); + + comp.ngOnInit(); + expect(comp.timeoutMinValue).toBe(100); + expect(comp.timeoutMaxValue).toBe(240); + expect(comp.timeoutDefaultValue).toBe(120); + }); });