diff --git a/docs/user/exercises/programming-exercise-setup.inc b/docs/user/exercises/programming-exercise-setup.inc index 8a2dc7cf9f78..563f65f48361 100644 --- a/docs/user/exercises/programming-exercise-setup.inc +++ b/docs/user/exercises/programming-exercise-setup.inc @@ -404,7 +404,8 @@ Edit Maximum Build Duration ^^^^^^^^^^^^^^^^^^^^^^^^^^^ **This option is only available when using** :ref:`integrated code lifecycle` -This section is optional. In most cases, the preconfigured build script does not need to be changed. + +This section is optional. In most cases, the default maximum build duration does not need to be changed. The maximum build duration is the time limit for the build plan to execute. If the build plan exceeds this time limit, it will be terminated. The default value is 120 seconds. You can change the maximum build duration by using the slider. @@ -412,6 +413,29 @@ You can change the maximum build duration by using the slider. .. figure:: programming/timeout-slider.png :align: center +Edit Container Configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**This option is only available when using** :ref:`integrated code lifecycle` + +This section is optional. In most cases, the default container configuration does not need to be changed. + +Currently, instructors can only change whether the container has internet access and add additional environment variables. +Disabling internet access can be useful if instructors want to prevent students from downloading additional dependencies during the build process. +If internet access is disabled, the container cannot access the internet during the build process. Thus, it will not be able to download additional dependencies. +The dependencies must then be included/cached in the docker image. + +Additional environment variables can be added to the container configuration. This can be useful if the build process requires additional environment variables to be set. + +.. figure:: programming/docker-flags-edit.png + :align: center + +We plan to add more options to the container configuration in the future. + +.. warning:: + - Disabling internet access is not currently supported for Swift and Haskell exercises. + + .. _configure_static_code_analysis_tools: Configure static code analysis diff --git a/docs/user/exercises/programming/docker-flags-edit.png b/docs/user/exercises/programming/docker-flags-edit.png new file mode 100644 index 000000000000..06a030f69f18 Binary files /dev/null and b/docs/user/exercises/programming/docker-flags-edit.png differ diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java index 9a41fc6fdc20..34d139aa6f19 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildConfig.java @@ -15,7 +15,8 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record BuildConfig(String buildScript, String dockerImage, String commitHashToBuild, String assignmentCommitHash, String testCommitHash, String branch, ProgrammingLanguage programmingLanguage, ProjectType projectType, boolean scaEnabled, boolean sequentialTestRunsEnabled, boolean testwiseCoverageEnabled, - List resultPaths, int timeoutSeconds, String assignmentCheckoutPath, String testCheckoutPath, String solutionCheckoutPath) implements Serializable { + List resultPaths, int timeoutSeconds, String assignmentCheckoutPath, String testCheckoutPath, String solutionCheckoutPath, DockerRunConfig dockerRunConfig) + implements Serializable { @Override public String dockerImage() { diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerFlagsDTO.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerFlagsDTO.java new file mode 100644 index 000000000000..bb10c5ddf313 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerFlagsDTO.java @@ -0,0 +1,6 @@ +package de.tum.cit.aet.artemis.buildagent.dto; + +import java.util.Map; + +public record DockerFlagsDTO(String network, Map env) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerRunConfig.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerRunConfig.java new file mode 100644 index 000000000000..2b45273e13fd --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/DockerRunConfig.java @@ -0,0 +1,7 @@ +package de.tum.cit.aet.artemis.buildagent.dto; + +import java.io.Serializable; +import java.util.List; + +public record DockerRunConfig(boolean isNetworkDisabled, List env) implements Serializable { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java index cfe5a1ab01e3..b68cc7a0c001 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java @@ -85,12 +85,13 @@ public BuildJobContainerService(DockerClient dockerClient, HostConfig hostConfig /** * Configure a container with the Docker image, the container name, optional proxy config variables, and set the command that runs when the container starts. * - * @param containerName the name of the container to be created - * @param image the Docker image to use for the container - * @param buildScript the build script to be executed in the container + * @param containerName the name of the container to be created + * @param image the Docker image to use for the container + * @param buildScript the build script to be executed in the container + * @param exerciseEnvVars the environment variables provided by the instructor * @return {@link CreateContainerResponse} that can be used to start the container */ - public CreateContainerResponse configureContainer(String containerName, String image, String buildScript) { + public CreateContainerResponse configureContainer(String containerName, String image, String buildScript, List exerciseEnvVars) { List envVars = new ArrayList<>(); if (useSystemProxy) { envVars.add("HTTP_PROXY=" + httpProxy); @@ -98,6 +99,9 @@ public CreateContainerResponse configureContainer(String containerName, String i envVars.add("NO_PROXY=" + noProxy); } envVars.add("SCRIPT=" + buildScript); + if (exerciseEnvVars != null && !exerciseEnvVars.isEmpty()) { + envVars.addAll(exerciseEnvVars); + } return dockerClient.createContainerCmd(image).withName(containerName).withHostConfig(hostConfig).withEnv(envVars) // Command to run when the container starts. This is the command that will be executed in the container's main process, which runs in the foreground and blocks the // container from exiting until it finishes. @@ -121,11 +125,23 @@ public void startContainer(String containerId) { /** * Run the script in the container and wait for it to finish before returning. * - * @param containerId the id of the container in which the script should be run - * @param buildJobId the id of the build job that is currently being executed + * @param containerId the id of the container in which the script should be run + * @param buildJobId the id of the build job that is currently being executed + * @param isNetworkDisabled whether the network should be disabled for the container */ + public void runScriptInContainer(String containerId, String buildJobId, boolean isNetworkDisabled) { + if (isNetworkDisabled) { + log.info("disconnecting container with id {} from network", containerId); + try { + dockerClient.disconnectFromNetworkCmd().withContainerId(containerId).withNetworkId("bridge").exec(); + } + catch (Exception e) { + log.error("Failed to disconnect container with id {} from network: {}", containerId, e.getMessage()); + buildLogsMap.appendBuildLogEntry(buildJobId, "Failed to disconnect container from default network 'bridge': " + e.getMessage()); + throw new LocalCIException("Failed to disconnect container from default network 'bridge': " + e.getMessage()); + } + } - public void runScriptInContainer(String containerId, String buildJobId) { log.info("Started running the build script for build job in container with id {}", containerId); // The "sh script.sh" execution command specified here is run inside the container as an additional process. This command runs in the background, independent of the // container's @@ -448,9 +464,4 @@ private Container getContainerForName(String containerName) { List containers = dockerClient.listContainersCmd().withShowAll(true).exec(); return containers.stream().filter(container -> container.getNames()[0].equals("/" + containerName)).findFirst().orElse(null); } - - private String getParentFolderPath(String path) { - Path parentPath = Paths.get(path).normalize().getParent(); - return parentPath != null ? parentPath.toString() : ""; - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java index 7c789cfafb28..c5e042b7f20e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java @@ -232,10 +232,18 @@ public BuildResult runBuildJob(BuildJobQueueItem buildJob, String containerName) index++; } - CreateContainerResponse container = buildJobContainerService.configureContainer(containerName, buildJob.buildConfig().dockerImage(), buildJob.buildConfig().buildScript()); + List envVars = null; + boolean isNetworkDisabled = false; + if (buildJob.buildConfig().dockerRunConfig() != null) { + envVars = buildJob.buildConfig().dockerRunConfig().env(); + isNetworkDisabled = buildJob.buildConfig().dockerRunConfig().isNetworkDisabled(); + } + + CreateContainerResponse container = buildJobContainerService.configureContainer(containerName, buildJob.buildConfig().dockerImage(), buildJob.buildConfig().buildScript(), + envVars); return runScriptAndParseResults(buildJob, containerName, container.getId(), assignmentRepoUri, testsRepoUri, solutionRepoUri, auxiliaryRepositoriesUris, - assignmentRepositoryPath, testsRepositoryPath, solutionRepositoryPath, auxiliaryRepositoriesPaths, assignmentCommitHash, testCommitHash); + assignmentRepositoryPath, testsRepositoryPath, solutionRepositoryPath, auxiliaryRepositoriesPaths, assignmentCommitHash, testCommitHash, isNetworkDisabled); } /** @@ -270,7 +278,7 @@ public BuildResult runBuildJob(BuildJobQueueItem buildJob, String containerName) private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String containerName, String containerId, VcsRepositoryUri assignmentRepositoryUri, VcsRepositoryUri testRepositoryUri, VcsRepositoryUri solutionRepositoryUri, VcsRepositoryUri[] auxiliaryRepositoriesUris, Path assignmentRepositoryPath, Path testsRepositoryPath, Path solutionRepositoryPath, Path[] auxiliaryRepositoriesPaths, @Nullable String assignmentRepoCommitHash, - @Nullable String testRepoCommitHash) { + @Nullable String testRepoCommitHash, boolean isNetworkDisabled) { long timeNanoStart = System.nanoTime(); @@ -292,7 +300,7 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String buildLogsMap.appendBuildLogEntry(buildJob.id(), msg); log.debug(msg); - buildJobContainerService.runScriptInContainer(containerId, buildJob.id()); + buildJobContainerService.runScriptInContainer(containerId, buildJob.id(), isNetworkDisabled); msg = "~~~~~~~~~~~~~~~~~~~~ Finished Executing Build Script for Build job " + buildJob.id() + " ~~~~~~~~~~~~~~~~~~~~"; buildLogsMap.appendBuildLogEntry(buildJob.id(), msg); 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 6ddd70dad841..1f3ae2a0ae8b 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 @@ -39,6 +39,8 @@ public final class Constants { public static final int QUIZ_GRACE_PERIOD_IN_SECONDS = 5; + public static final int MAX_ENVIRONMENT_VARIABLES_DOCKER_FLAG_LENGTH = 1000; + /** * This constant determines how many seconds after the exercise due dates submissions will still be considered rated. * Submissions after the grace period exceeded will be flagged as illegal. diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java index a5ada6708999..d28e21bb3ad1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExerciseBuildConfig.java @@ -60,7 +60,7 @@ public class ProgrammingExerciseBuildConfig extends DomainObject { @Column(name = "timeout_seconds") private int timeoutSeconds; - @Column(name = "docker_flags") + @Column(name = "docker_flags", columnDefinition = "longtext") private String dockerFlags; @OneToOne(mappedBy = "buildConfig") diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseBuildConfigService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseBuildConfigService.java new file mode 100644 index 000000000000..5ccf7f2045a6 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseBuildConfigService.java @@ -0,0 +1,90 @@ +package de.tum.cit.aet.artemis.programming.service; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import jakarta.annotation.Nullable; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.cit.aet.artemis.buildagent.dto.DockerFlagsDTO; +import de.tum.cit.aet.artemis.buildagent.dto.DockerRunConfig; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; + +@Profile(PROFILE_CORE) +@Service +public class ProgrammingExerciseBuildConfigService { + + private static final Logger log = org.slf4j.LoggerFactory.getLogger(ProgrammingExerciseBuildConfigService.class); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Converts a JSON string representing Docker flags (in JSON format) + * into a {@link DockerRunConfig} instance. + * + *

+ * The JSON string is expected to represent a {@link DockerFlagsDTO} object. + * Example JSON input: + * + *

+     * {"network":"none","env":{"key1":"value1","key2":"value2"}}
+     * 
+ * + * @param buildConfig the build config containing the Docker flags + * @return a {@link DockerRunConfig} object initialized with the parsed flags, or {@code null} if the JSON string is empty + */ + @Nullable + public DockerRunConfig getDockerRunConfig(ProgrammingExerciseBuildConfig buildConfig) { + DockerFlagsDTO dockerFlagsDTO = parseDockerFlags(buildConfig); + + return getDockerRunConfigFromParsedFlags(dockerFlagsDTO); + } + + DockerRunConfig getDockerRunConfigFromParsedFlags(DockerFlagsDTO dockerFlagsDTO) { + if (dockerFlagsDTO == null) { + return null; + } + List env = new ArrayList<>(); + boolean isNetworkDisabled = dockerFlagsDTO.network() != null && dockerFlagsDTO.network().equals("none"); + + if (dockerFlagsDTO.env() != null) { + for (Map.Entry entry : dockerFlagsDTO.env().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + env.add(key + "=" + value); + } + } + + return new DockerRunConfig(isNetworkDisabled, env); + } + + /** + * Parses the JSON string representing Docker flags into DockerFlagsDTO. (see {@link DockerFlagsDTO}) + * + * @return a list of key-value pairs, or {@code null} if the JSON string is empty + * @throws IllegalArgumentException if the JSON string is invalid + */ + @Nullable + DockerFlagsDTO parseDockerFlags(ProgrammingExerciseBuildConfig buildConfig) { + if (StringUtils.isBlank(buildConfig.getDockerFlags())) { + return null; + } + + try { + return objectMapper.readValue(buildConfig.getDockerFlags(), DockerFlagsDTO.class); + } + catch (Exception e) { + log.error("Failed to parse DockerRunConfig from JSON string: {}. Using default settings.", buildConfig.getDockerFlags()); + throw new IllegalArgumentException("Failed to parse DockerRunConfig from JSON string: " + buildConfig.getDockerFlags(), e); + } + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java index 20b546ad4a48..c60fbc5b9d34 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java @@ -1,6 +1,7 @@ package de.tum.cit.aet.artemis.programming.service; import static de.tum.cit.aet.artemis.core.config.Constants.ALLOWED_CHECKOUT_DIRECTORY; +import static de.tum.cit.aet.artemis.core.config.Constants.MAX_ENVIRONMENT_VARIABLES_DOCKER_FLAG_LENGTH; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import static de.tum.cit.aet.artemis.programming.domain.build.BuildPlanType.SOLUTION; import static de.tum.cit.aet.artemis.programming.domain.build.BuildPlanType.TEMPLATE; @@ -46,6 +47,8 @@ import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; import de.tum.cit.aet.artemis.atlas.api.CompetencyProgressApi; +import de.tum.cit.aet.artemis.buildagent.dto.DockerFlagsDTO; +import de.tum.cit.aet.artemis.buildagent.dto.DockerRunConfig; import de.tum.cit.aet.artemis.communication.service.conversation.ChannelService; import de.tum.cit.aet.artemis.communication.service.notifications.GroupNotificationScheduleService; import de.tum.cit.aet.artemis.core.domain.Course; @@ -185,6 +188,8 @@ public class ProgrammingExerciseService { private final CompetencyProgressApi competencyProgressApi; + private final ProgrammingExerciseBuildConfigService programmingExerciseBuildConfigService; + public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerciseRepository, GitService gitService, Optional versionControlService, Optional continuousIntegrationService, Optional continuousIntegrationTriggerService, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, @@ -199,7 +204,8 @@ public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerc ProgrammingSubmissionService programmingSubmissionService, Optional irisSettingsService, Optional aeolusTemplateService, Optional buildScriptGenerationService, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProfileService profileService, ExerciseService exerciseService, - ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, CompetencyProgressApi competencyProgressApi) { + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, CompetencyProgressApi competencyProgressApi, + ProgrammingExerciseBuildConfigService programmingExerciseBuildConfigService) { this.programmingExerciseRepository = programmingExerciseRepository; this.gitService = gitService; this.versionControlService = versionControlService; @@ -233,6 +239,7 @@ public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerc this.exerciseService = exerciseService; this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; this.competencyProgressApi = competencyProgressApi; + this.programmingExerciseBuildConfigService = programmingExerciseBuildConfigService; } /** @@ -372,6 +379,7 @@ public void validateNewProgrammingExerciseSettings(ProgrammingExercise programmi programmingExercise.validateProgrammingSettings(); programmingExercise.validateSettingsForFeedbackRequest(); validateCustomCheckoutPaths(programmingExercise); + validateDockerFlags(programmingExercise); auxiliaryRepositoryService.validateAndAddAuxiliaryRepositoriesOfProgrammingExercise(programmingExercise, programmingExercise.getAuxiliaryRepositories()); submissionPolicyService.validateSubmissionPolicyCreation(programmingExercise); @@ -1072,4 +1080,40 @@ public ProgrammingExercise loadProgrammingExerciseWithAuxiliaryRepositories(long final Set fetchOptions = Set.of(AuxiliaryRepositories); return programmingExerciseRepository.findByIdWithDynamicFetchElseThrow(exerciseId, fetchOptions); } + + /** + * Validates the network access feature for the given programming language. + * Currently, SWIFT and HASKELL do not support disabling the network access feature. + * + * @param programmingExercise the programming exercise to validate + */ + public void validateDockerFlags(ProgrammingExercise programmingExercise) { + ProgrammingExerciseBuildConfig buildConfig = programmingExercise.getBuildConfig(); + DockerFlagsDTO dockerFlagsDTO; + try { + dockerFlagsDTO = programmingExerciseBuildConfigService.parseDockerFlags(buildConfig); + } + catch (IllegalArgumentException e) { + throw new BadRequestAlertException("Error while parsing the docker flags", "Exercise", "dockerFlagsParsingError"); + } + + if (dockerFlagsDTO == null) { + return; + } + + if (dockerFlagsDTO.env() != null) { + for (var entry : dockerFlagsDTO.env().entrySet()) { + if (entry.getKey().length() > MAX_ENVIRONMENT_VARIABLES_DOCKER_FLAG_LENGTH || entry.getValue().length() > MAX_ENVIRONMENT_VARIABLES_DOCKER_FLAG_LENGTH) { + throw new BadRequestAlertException("The environment variables are too long. Max " + MAX_ENVIRONMENT_VARIABLES_DOCKER_FLAG_LENGTH + " chars", "Exercise", + "envVariablesTooLong"); + } + } + } + + DockerRunConfig dockerRunConfig = programmingExerciseBuildConfigService.getDockerRunConfigFromParsedFlags(dockerFlagsDTO); + + if (List.of(ProgrammingLanguage.SWIFT, ProgrammingLanguage.HASKELL).contains(programmingExercise.getProgrammingLanguage()) && dockerRunConfig.isNetworkDisabled()) { + throw new BadRequestAlertException("This programming language does not support disabling the network access feature", "Exercise", "networkAccessNotSupported"); + } + } } 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 d25304141c24..0e081a93728b 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 @@ -174,7 +174,7 @@ private static List removeUnnecessaryInformation(List versionControlService, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, LocalCIBuildConfigurationService localCIBuildConfigurationService, GitService gitService, ExerciseDateService exerciseDateService, - ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, BuildScriptProviderService buildScriptProviderService) { + ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, BuildScriptProviderService buildScriptProviderService, + ProgrammingExerciseBuildConfigService programmingExerciseBuildConfigService) { this.hazelcastInstance = hazelcastInstance; this.aeolusTemplateService = aeolusTemplateService; this.programmingLanguageConfiguration = programmingLanguageConfiguration; @@ -119,6 +124,7 @@ public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance h this.programmingExerciseBuildConfigRepository = programmingExerciseBuildConfigRepository; this.exerciseDateService = exerciseDateService; this.buildScriptProviderService = buildScriptProviderService; + this.programmingExerciseBuildConfigService = programmingExerciseBuildConfigService; } @PostConstruct @@ -310,6 +316,8 @@ private BuildConfig getBuildConfig(ProgrammingExerciseParticipation participatio dockerImage = programmingLanguageConfiguration.getImage(programmingExercise.getProgrammingLanguage(), Optional.ofNullable(programmingExercise.getProjectType())); } + DockerRunConfig dockerRunConfig = programmingExerciseBuildConfigService.getDockerRunConfig(buildConfig); + List resultPaths = getTestResultPaths(windfile); resultPaths = buildScriptProviderService.replaceResultPathsPlaceholders(resultPaths, buildConfig); @@ -319,7 +327,7 @@ private BuildConfig getBuildConfig(ProgrammingExerciseParticipation participatio return new BuildConfig(buildScript, dockerImage, commitHashToBuild, assignmentCommitHash, testCommitHash, branch, programmingLanguage, projectType, staticCodeAnalysisEnabled, sequentialTestRunsEnabled, testwiseCoverageEnabled, resultPaths, buildConfig.getTimeoutSeconds(), - buildConfig.getAssignmentCheckoutPath(), buildConfig.getTestCheckoutPath(), buildConfig.getSolutionCheckoutPath()); + buildConfig.getAssignmentCheckoutPath(), buildConfig.getTestCheckoutPath(), buildConfig.getSolutionCheckoutPath(), dockerRunConfig); } private ProgrammingExerciseBuildConfig loadBuildConfig(ProgrammingExercise programmingExercise) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java index 748645568dc6..da74833808c0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java @@ -78,6 +78,7 @@ import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseExportService; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseImportFromFileService; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseImportService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseService; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeature; import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeatureService; import de.tum.cit.aet.artemis.programming.service.SubmissionPolicyService; @@ -130,13 +131,15 @@ public class ProgrammingExerciseExportImportResource { private final Optional athenaModuleService; + private final ProgrammingExerciseService programmingExerciseService; + public ProgrammingExerciseExportImportResource(ProgrammingExerciseRepository programmingExerciseRepository, UserRepository userRepository, AuthorizationCheckService authCheckService, CourseService courseService, ProgrammingExerciseImportService programmingExerciseImportService, ProgrammingExerciseExportService programmingExerciseExportService, Optional programmingLanguageFeatureService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, SubmissionPolicyService submissionPolicyService, ProgrammingExerciseTaskRepository programmingExerciseTaskRepository, ExamAccessService examAccessService, CourseRepository courseRepository, ProgrammingExerciseImportFromFileService programmingExerciseImportFromFileService, ConsistencyCheckService consistencyCheckService, - Optional athenaModuleService, CompetencyProgressApi competencyProgressApi) { + Optional athenaModuleService, CompetencyProgressApi competencyProgressApi, ProgrammingExerciseService programmingExerciseService) { this.programmingExerciseRepository = programmingExerciseRepository; this.userRepository = userRepository; this.courseService = courseService; @@ -153,6 +156,7 @@ public ProgrammingExerciseExportImportResource(ProgrammingExerciseRepository pro this.consistencyCheckService = consistencyCheckService; this.athenaModuleService = athenaModuleService; this.competencyProgressApi = competencyProgressApi; + this.programmingExerciseService = programmingExerciseService; } /** @@ -199,6 +203,7 @@ public ResponseEntity importProgrammingExercise(@PathVariab newExercise.validateGeneralSettings(); newExercise.validateProgrammingSettings(); newExercise.validateSettingsForFeedbackRequest(); + programmingExerciseService.validateDockerFlags(newExercise); validateStaticCodeAnalysisSettings(newExercise); final User user = userRepository.getUserWithGroupsAndAuthorities(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java index 0a39cabc4e06..0eaab82ce448 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java @@ -329,6 +329,9 @@ public ResponseEntity updateProgrammingExercise(@RequestBod // Verify that the checkout directories have not been changed. This is required since the buildScript and result paths are determined during the creation of the exercise. programmingExerciseService.validateCheckoutDirectoriesUnchanged(programmingExerciseBeforeUpdate, updatedProgrammingExercise); + // Verify that the programming language supports the selected network access option + programmingExerciseService.validateDockerFlags(updatedProgrammingExercise); + // Verify that a theia image is provided when the online IDE is enabled if (updatedProgrammingExercise.isAllowOnlineIde() && updatedProgrammingExercise.getBuildConfig().getTheiaImage() == null) { throw new BadRequestAlertException("You need to provide a Theia image when the online IDE is enabled", ENTITY_NAME, "noTheiaImageProvided"); diff --git a/src/main/resources/config/liquibase/changelog/20241022120000_changelog.xml b/src/main/resources/config/liquibase/changelog/20241022120000_changelog.xml new file mode 100644 index 000000000000..193a6370c0ed --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241022120000_changelog.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index d331337ceef4..1c9c7d4236c6 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -32,6 +32,7 @@ + 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 8460ca15d6fd..bb59ed44df0d 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 @@ -16,7 +16,57 @@ /> @if (!isAeolus()) { -
+ @if (isLanguageSupported) { +
+ +
+ @if (isNetworkDisabled) { + + } + } + +
+ + + + + + + + + + + + + + + + + + + + +
+