Skip to content

Commit

Permalink
Programming exercises: Fix missing build plan during import on Jenkin…
Browse files Browse the repository at this point in the history
…s setups (#7058)
  • Loading branch information
chrisknedl authored Dec 4, 2023
1 parent 91d27ba commit a85d19d
Show file tree
Hide file tree
Showing 17 changed files with 250 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,17 @@ default BuildPlan setBuildPlanForExercise(final String buildPlan, final Programm
buildPlanWrapper.addProgrammingExercise(exercise);
return save(buildPlanWrapper);
}

/**
* Copies the build plan from the source exercise to the target exercise.
*
* @param sourceExercise The exercise containing the build plan to be copied.
* @param targetExercise The exercise into which the build plan is copied.
*/
default void copyBetweenExercises(ProgrammingExercise sourceExercise, ProgrammingExercise targetExercise) {
findByProgrammingExercises_IdWithProgrammingExercises(sourceExercise.getId()).ifPresent(buildPlan -> {
buildPlan.addProgrammingExercise(targetExercise);
save(buildPlan);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -446,14 +446,14 @@ private ProgrammingExerciseStudentParticipation configureRepository(ProgrammingE
private ProgrammingExerciseStudentParticipation copyBuildPlan(ProgrammingExerciseStudentParticipation participation) {
// only execute this step if it has not yet been completed yet or if the build plan id is missing for some reason
if (!participation.getInitializationState().hasCompletedState(InitializationState.BUILD_PLAN_COPIED) || participation.getBuildPlanId() == null) {
final var projectKey = participation.getProgrammingExercise().getProjectKey();
final var exercise = participation.getProgrammingExercise();
final var planName = BuildPlanType.TEMPLATE.getName();
final var username = participation.getParticipantIdentifier();
final var buildProjectName = participation.getExercise().getCourseViaExerciseGroupOrCourseMember().getShortName().toUpperCase() + " "
+ participation.getExercise().getTitle();
final var targetPlanName = participation.addPracticePrefixIfTestRun(username.toUpperCase());
// the next action includes recovery, which means if the build plan has already been copied, we simply retrieve the build plan id and do not copy it again
final var buildPlanId = continuousIntegrationService.orElseThrow().copyBuildPlan(projectKey, planName, projectKey, buildProjectName, targetPlanName, true);
final var buildPlanId = continuousIntegrationService.orElseThrow().copyBuildPlan(exercise, planName, exercise, buildProjectName, targetPlanName, true);
participation.setBuildPlanId(buildPlanId);
participation.setInitializationState(InitializationState.BUILD_PLAN_COPIED);
return programmingExerciseStudentParticipationRepository.saveAndFlush(participation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,11 @@ private BambooBuildPlanDTO getBuildPlan(String planKey, boolean expand, boolean
}

@Override
public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
boolean targetProjectExists) {
String sourceProjectKey = sourceExercise.getProjectKey();
String targetProjectKey = targetExercise.getProjectKey();

final var cleanPlanName = getCleanPlanName(targetPlanName);
final var sourcePlanKey = sourceProjectKey + "-" + sourcePlanName;
final var targetPlanKey = targetProjectKey + "-" + cleanPlanName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,16 @@ void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, Vc
/**
* Clones an existing build plan. Illegal characters in the plan key, or name will be replaced.
*
* @param sourceProjectKey The key of the source project, normally the key of the exercise -> courseShortName + exerciseShortName.
* @param sourceExercise The exercise from which the build plan should be copied
* @param sourcePlanName The name of the source plan
* @param targetProjectKey The key of the project the plan should get copied to
* @param targetExercise The exercise to which the build plan is copied to
* @param targetProjectName The wanted name of the new project
* @param targetPlanName The wanted name of the new plan after copying it
* @param targetProjectExists whether the target project already exists or not
* @return The key of the new build plan
*/
String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName, boolean targetProjectExists);
String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
boolean targetProjectExists);

/**
* Configure the build plan with the given participation on the CI system. Common configurations: - update the repository in the build plan - set appropriate user permissions -
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,14 @@ public void recreateBuildPlansForExercise(ProgrammingExercise exercise) {
}

@Override
public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
boolean targetProjectExists) {
// In GitLab CI we don't have to copy the build plan.
// Instead, we configure a CI config path leading to the API when enabling the CI.

// When sending the build results back, the build plan key is used to identify the participation.
// Therefore, we return the key here even though GitLab CI does not need it.
return targetProjectKey + "-" + targetPlanName.toUpperCase().replaceAll("[^A-Z0-9]", "");
return targetExercise.getProjectKey() + "-" + targetPlanName.toUpperCase().replaceAll("[^A-Z0-9]", "");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ public void recreateBuildPlansForExercise(ProgrammingExercise exercise) {
}

@Override
public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
boolean targetProjectExists) {
return jenkinsBuildPlanService.copyBuildPlan(sourceProjectKey, sourcePlanName, targetProjectKey, targetPlanName);
return jenkinsBuildPlanService.copyBuildPlan(sourceExercise, sourcePlanName, targetExercise, targetPlanName);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import de.tum.in.www1.artemis.domain.enumeration.RepositoryType;
import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation;
import de.tum.in.www1.artemis.exception.JenkinsException;
import de.tum.in.www1.artemis.repository.BuildPlanRepository;
import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository;
import de.tum.in.www1.artemis.repository.UserRepository;
import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService;
Expand Down Expand Up @@ -78,9 +79,12 @@ public class JenkinsBuildPlanService {

private final ProgrammingExerciseRepository programmingExerciseRepository;

private final BuildPlanRepository buildPlanRepository;

public JenkinsBuildPlanService(@Qualifier("jenkinsRestTemplate") RestTemplate restTemplate, JenkinsServer jenkinsServer, JenkinsBuildPlanCreator jenkinsBuildPlanCreator,
JenkinsJobService jenkinsJobService, JenkinsJobPermissionsService jenkinsJobPermissionsService, JenkinsInternalUrlService jenkinsInternalUrlService,
UserRepository userRepository, ProgrammingExerciseRepository programmingExerciseRepository, JenkinsPipelineScriptCreator jenkinsPipelineScriptCreator) {
UserRepository userRepository, ProgrammingExerciseRepository programmingExerciseRepository, JenkinsPipelineScriptCreator jenkinsPipelineScriptCreator,
BuildPlanRepository buildPlanRepository) {
this.restTemplate = restTemplate;
this.jenkinsServer = jenkinsServer;
this.jenkinsBuildPlanCreator = jenkinsBuildPlanCreator;
Expand All @@ -90,6 +94,7 @@ public JenkinsBuildPlanService(@Qualifier("jenkinsRestTemplate") RestTemplate re
this.programmingExerciseRepository = programmingExerciseRepository;
this.jenkinsInternalUrlService = jenkinsInternalUrlService;
this.jenkinsPipelineScriptCreator = jenkinsPipelineScriptCreator;
this.buildPlanRepository = buildPlanRepository;
}

/**
Expand Down Expand Up @@ -191,6 +196,34 @@ public void updateBuildPlanRepositories(String buildProjectKey, String buildPlan
log.error("Pipeline Script not found", e);
}

postBuildPlanConfigChange(buildPlanKey, buildProjectKey, jobConfig);
}

/**
* Replaces the old build plan URL with a new one containing an updated exercise and access token.
*
* @param templateExercise The exercise containing the old build plan URL.
* @param newExercise The exercise of which the build plan URL is updated.
* @param jobConfig The job config in Jenkins for the new exercise.
*/
private void updateBuildPlanURLs(ProgrammingExercise templateExercise, ProgrammingExercise newExercise, Document jobConfig) {
final Long previousExerciseId = templateExercise.getId();
final String previousBuildPlanAccessSecret = templateExercise.getBuildPlanAccessSecret();
final Long newExerciseId = newExercise.getId();
final String newBuildPlanAccessSecret = newExercise.getBuildPlanAccessSecret();

String toBeReplaced = String.format("/%d/build-plan?secret=%s", previousExerciseId, previousBuildPlanAccessSecret);
String replacement = String.format("/%d/build-plan?secret=%s", newExerciseId, newBuildPlanAccessSecret);

try {
JenkinsBuildPlanUtils.replaceScriptParameters(jobConfig, toBeReplaced, replacement);
}
catch (IllegalArgumentException e) {
log.error("Pipeline Script not found", e);
}
}

private void postBuildPlanConfigChange(String buildPlanKey, String buildProjectKey, Document jobConfig) {
final var errorMessage = "Error trying to configure build plan in Jenkins " + buildPlanKey;
try {
URI uri = JenkinsEndpoints.PLAN_CONFIG.buildEndpoint(serverUrl.toString(), buildProjectKey, buildPlanKey).build(true).toUri();
Expand Down Expand Up @@ -233,17 +266,25 @@ public String getBuildPlanKeyFromTestResults(TestResultsDTO testResultsDTO) thro
/**
* Copies a build plan to another and replaces the old reference to the master and main branch with a reference to the default branch
*
* @param sourceProjectKey the source project key
* @param sourcePlanName the source plan name
* @param targetProjectKey the target project key
* @param targetPlanName the target plan name
* @param sourceExercise the source exercise
* @param sourcePlanName the source plan name
* @param targetExercise the target exercise
* @param targetPlanName the target plan name
* @return the key of the created build plan
*/
public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetPlanName) {
public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetPlanName) {
buildPlanRepository.copyBetweenExercises(sourceExercise, targetExercise);

String sourceProjectKey = sourceExercise.getProjectKey();
String targetProjectKey = targetExercise.getProjectKey();

final var cleanTargetName = getCleanPlanName(targetPlanName);
final var sourcePlanKey = sourceProjectKey + "-" + sourcePlanName;
final var targetPlanKey = targetProjectKey + "-" + cleanTargetName;
final var jobXml = jenkinsJobService.getJobConfigForJobInFolder(sourceProjectKey, sourcePlanKey);

updateBuildPlanURLs(sourceExercise, targetExercise, jobXml);

jenkinsJobService.createJobInFolder(jobXml, targetProjectKey, targetPlanKey);

return targetPlanKey;
Expand Down Expand Up @@ -362,7 +403,7 @@ public boolean buildPlanExists(String projectKey, String buildPlanId) {
/**
* Assigns access permissions to instructors and TAs for the specified build plan.
* This is done by getting all users that belong to the instructor and TA groups of
* the exercise' course and adding permissions to the Jenkins job.
* the exercises' course and adding permissions to the Jenkins job.
*
* @param programmingExercise the programming exercise
* @param planName the name of the build plan
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ public class JenkinsBuildPlanUtils {
private static final String PIPELINE_SCRIPT_DETECTION_COMMENT = "// ARTEMIS: JenkinsPipeline";

/**
* Replaces the base repository url written within the Jenkins pipeline script with the value specified by repoUrl
* Replaces either one of the previous repository urls or the build plan url written within the Jenkins pipeline
* script with the value specified by newUrl.
*
* @param jobXmlDocument the Jenkins pipeline
* @param repoUrl the new repository url
* @param baseRepoUrl the base repository url that will be replaced
* @param previousUrl the previous url that will be replaced
* @param newUrl the new repository or build plan url
* @throws IllegalArgumentException if the xml document isn't a Jenkins pipeline script
*/
public static void replaceScriptParameters(Document jobXmlDocument, String repoUrl, String baseRepoUrl) throws IllegalArgumentException {
public static void replaceScriptParameters(Document jobXmlDocument, String previousUrl, String newUrl) throws IllegalArgumentException {
final var scriptNode = findScriptNode(jobXmlDocument);
if (scriptNode == null || scriptNode.getFirstChild() == null) {
throw new IllegalArgumentException("Pipeline Script not found");
Expand All @@ -27,9 +28,9 @@ public static void replaceScriptParameters(Document jobXmlDocument, String repoU
if (!pipeLineScript.startsWith("pipeline") && !pipeLineScript.startsWith(PIPELINE_SCRIPT_DETECTION_COMMENT)) {
throw new IllegalArgumentException("Pipeline Script not found");
}
// Replace repo URL
// TODO: properly replace the baseRepoUrl with repoUrl by looking up the ciRepoName in the pipelineScript
pipeLineScript = pipeLineScript.replace(baseRepoUrl, repoUrl);
// Replace URL
// TODO: properly replace the previousUrl with newUrl by looking up the ciRepoName in the pipelineScript
pipeLineScript = pipeLineScript.replace(previousUrl, newUrl);

scriptNode.getFirstChild().setTextContent(pipeLineScript);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@ public BuildStatus getBuildStatus(ProgrammingExerciseParticipation participation
}

@Override
public String copyBuildPlan(String sourceProjectKey, String sourcePlanName, String targetProjectKey, String targetProjectName, String targetPlanName,
public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, ProgrammingExercise targetExercise, String targetProjectName, String targetPlanName,
boolean targetProjectExists) {
// No build plans exist for local CI. Only return a plan name.
final String cleanPlanName = getCleanPlanName(targetPlanName);
return targetProjectKey + "-" + cleanPlanName;
return targetExercise.getProjectKey() + "-" + cleanPlanName;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,7 @@ public ProgrammingExerciseImportBasicService(ExerciseHintService exerciseHintSer
@Transactional // TODO: apply the transaction on a smaller scope
// IMPORTANT: the transactional context only works if you invoke this method from another class
public ProgrammingExercise importProgrammingExerciseBasis(final ProgrammingExercise templateExercise, final ProgrammingExercise newExercise) {
// Set values we don't want to copy to null
setupExerciseForImport(newExercise);
newExercise.setBranch(versionControlService.orElseThrow().getDefaultBranchOfArtemis());
prepareBasicExerciseInformation(templateExercise, newExercise);

// Note: same order as when creating an exercise
programmingExerciseParticipationService.setupInitialTemplateParticipation(newExercise);
Expand Down Expand Up @@ -161,6 +159,25 @@ else if (Boolean.TRUE.equals(importedExercise.isStaticCodeAnalysisEnabled()) &&
return savedImportedExercise;
}

/**
* Prepares information directly stored in the exercise for the copy process.
* <p>
* Replaces attributes in the new exercise that should not be copied from the previous one.
*
* @param templateExercise Some exercise the information is copied from.
* @param newExercise The exercise that is prepared.
*/
private void prepareBasicExerciseInformation(final ProgrammingExercise templateExercise, final ProgrammingExercise newExercise) {
// Set values we don't want to copy to null
setupExerciseForImport(newExercise);

if (templateExercise.hasBuildPlanAccessSecretSet()) {
newExercise.generateAndSetBuildPlanAccessSecret();
}

newExercise.setBranch(versionControlService.orElseThrow().getDefaultBranchOfArtemis());
}

/**
* Sets up the test repository for a new exercise by setting the repository URL. This does not create the actual
* repository on the version control server!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,11 @@ private void cloneAndEnableAllBuildPlans(ProgrammingExercise templateExercise, P
final var targetExerciseProjectKey = newExercise.getProjectKey();
final var templatePlanName = BuildPlanType.TEMPLATE.getName();
final var solutionPlanName = BuildPlanType.SOLUTION.getName();
final var templateKey = templateExercise.getProjectKey();
final var targetKey = newExercise.getProjectKey();
final var targetName = newExercise.getCourseViaExerciseGroupOrCourseMember().getShortName().toUpperCase() + " " + newExercise.getTitle();
ContinuousIntegrationService continuousIntegration = continuousIntegrationService.orElseThrow();
continuousIntegration.createProjectForExercise(newExercise);
continuousIntegration.copyBuildPlan(templateKey, templatePlanName, targetKey, targetName, templatePlanName, false);
continuousIntegration.copyBuildPlan(templateKey, solutionPlanName, targetKey, targetName, solutionPlanName, true);
continuousIntegration.copyBuildPlan(templateExercise, templatePlanName, newExercise, targetName, templatePlanName, false);
continuousIntegration.copyBuildPlan(templateExercise, solutionPlanName, newExercise, targetName, solutionPlanName, true);
continuousIntegration.givePlanPermissions(newExercise, templatePlanName);
continuousIntegration.givePlanPermissions(newExercise, solutionPlanName);
programmingExerciseService.giveCIProjectPermissions(newExercise);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ public void mockUpdatePlanRepository(String projectKey, String planName, boolean
mockGetJobXmlForBuildPlanWith(projectKey, mockXml);

final var uri = UriComponentsBuilder.fromUri(jenkinsServerUrl.toURI()).pathSegment("job", projectKey, "job", planName, "config.xml").build().toUri();

// build plan URL is updated after the repository URLs, so in this case, the URI is used twice
mockServer.expect(requestTo(uri)).andExpect(method(HttpMethod.POST)).andRespond(withStatus(HttpStatus.OK));

mockTriggerBuild(projectKey, planName, false);
Expand Down
Loading

0 comments on commit a85d19d

Please sign in to comment.