Skip to content

Commit 7be7d32

Browse files
authored
Merge pull request #363 from mp-access/dev
Release v0.10.1
2 parents 62d9ae2 + eb8990d commit 7be7d32

48 files changed

Lines changed: 1658 additions & 534 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# Eclipse-specific files
2+
.classpath
3+
.project
4+
.settings/**/*
5+
bin/**/*
6+
7+
18
# Compiled class file
29
*.class
310

@@ -164,4 +171,5 @@ course_repositories/
164171
runner/
165172
courses_db
166173

167-
#load_testing/
174+
#load_testing/
175+
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package ch.uzh.ifi.access;
2+
3+
import lombok.Data;
4+
import org.springframework.boot.context.properties.ConfigurationProperties;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
import java.time.Instant;
12+
import java.time.ZoneId;
13+
import java.time.ZonedDateTime;
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
17+
@RestController
18+
public class ServerInfoController {
19+
20+
private final ServerInfo serverInfo;
21+
22+
public ServerInfoController(ServerInfo serverInfo) {
23+
this.serverInfo = serverInfo;
24+
}
25+
26+
@GetMapping("/info")
27+
public ResponseEntity<?> info() {
28+
Map<String, String> response = new HashMap<>();
29+
response.put("offsetDateTime", ZonedDateTime.now().toOffsetDateTime().toString());
30+
response.put("utcTime", Instant.now().toString());
31+
response.put("zoneId", ZoneId.systemDefault().toString());
32+
33+
if (serverInfo != null) {
34+
response.put("version", serverInfo.version);
35+
}
36+
return ResponseEntity.ok(response);
37+
}
38+
39+
@Component
40+
@Data
41+
@Configuration
42+
@ConfigurationProperties(prefix = "server.info")
43+
static class ServerInfo {
44+
private String version;
45+
}
46+
}

src/main/java/ch/uzh/ifi/access/coderunner/CodeRunner.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ private RunResult createAndRunContainer(ContainerConfig containerConfig, String
113113

114114
ContainerCreation creation = docker.createContainer(containerConfig);
115115
String containerId = creation.id();
116-
logger.trace(String.format("Created container %s", containerId));
116+
logger.debug("Created container {}", containerId);
117117

118118
if (creation.warnings() != null) {
119119
creation.warnings().forEach(logger::warn);
@@ -148,6 +148,8 @@ private RunResult createAndRunContainer(ContainerConfig containerConfig, String
148148

149149
stopAndRemoveContainer(containerId);
150150

151+
logger.trace("Code execution logs start --------------------\n{}\n-------------------- Code execution logs end", console);
152+
151153
return new RunResult(console, stdOut, stdErr, executionTime, didTimeout, isOomKilled);
152154
}
153155

@@ -184,7 +186,7 @@ private void copyDirectoryToContainer(String containerId, Path folder) throws In
184186
} catch (IOException e) {
185187
logger.warn(e.getMessage(), e);
186188
}
187-
logger.trace(joiner.toString());
189+
logger.debug(joiner.toString());
188190
}
189191

190192
private void startAndWaitContainer(String id) throws DockerException, InterruptedException {
@@ -205,7 +207,7 @@ private String readStdErr(String containerId) throws DockerException, Interrupte
205207
}
206208

207209
private void stopAndRemoveContainer(String id) throws DockerException, InterruptedException {
208-
logger.debug(String.format("Stopping and removing container %s", id));
210+
logger.debug("Stopping and removing container {}", id);
209211
docker.stopContainer(id, 1);
210212
docker.removeContainer(id);
211213
}

src/main/java/ch/uzh/ifi/access/config/AsyncConfig.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ public Executor getAsyncExecutor() {
4242
executor.setCorePoolSize(THREAD_POOL_SIZE);
4343
executor.setMaxPoolSize(MAX_POOL_SIZE);
4444
executor.setQueueCapacity(QUEUE_CAPACITY);
45+
executor.setWaitForTasksToCompleteOnShutdown(true);
46+
executor.setAwaitTerminationSeconds(60);
47+
executor.initialize();
48+
return executor;
49+
}
50+
51+
@Bean("courseUpdateWorkerExecutor")
52+
public Executor getCourseUpdateExecutor() {
53+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
54+
executor.setThreadNamePrefix("course-update-worker-");
55+
executor.setCorePoolSize(THREAD_POOL_SIZE);
56+
executor.setMaxPoolSize(MAX_POOL_SIZE);
57+
executor.setQueueCapacity(QUEUE_CAPACITY);
4558
executor.initialize();
4659
return executor;
4760
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package ch.uzh.ifi.access.config;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
import org.springframework.context.event.ContextClosedEvent;
6+
import org.springframework.context.event.EventListener;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
public class GracefulShutdown {
11+
12+
private static final Logger logger = LoggerFactory.getLogger(GracefulShutdown.class);
13+
14+
private boolean isShutdown = false;
15+
16+
@EventListener(ContextClosedEvent.class)
17+
public void rejectSubmissionsOnShutdown() {
18+
logger.warn("Received shutdown signal. Will reject new submissions");
19+
this.isShutdown = true;
20+
}
21+
22+
public boolean isShutdown() {
23+
return isShutdown;
24+
}
25+
}

src/main/java/ch/uzh/ifi/access/config/SecurityConfigurer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public void configure(ResourceServerSecurityConfigurer resources) {
4545

4646
@Override
4747
public void configure(final HttpSecurity http) throws Exception {
48-
final String[] swaggerPaths = new String[]{"/v2/api-docs", "/configuration/ui", "/swagger-resources/**", "/configuration/**", "/swagger-ui.html", "/webjars/**"};
48+
final String[] permittedPaths = new String[]{"/info", "/v2/api-docs", "/configuration/ui", "/swagger-resources/**", "/configuration/**", "/swagger-ui.html", "/webjars/**"};
4949

5050
http.cors()
5151
.configurationSource(corsConfigurationSource())
@@ -58,7 +58,7 @@ public void configure(final HttpSecurity http) throws Exception {
5858
.disable()
5959
.addFilterAfter(filter, AbstractPreAuthenticatedProcessingFilter.class)
6060
.authorizeRequests()
61-
.antMatchers(swaggerPaths)
61+
.antMatchers(permittedPaths)
6262
.permitAll()
6363
.antMatchers(securityProperties.getApiMatcher())
6464
.authenticated();

src/main/java/ch/uzh/ifi/access/course/controller/CourseController.java

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package ch.uzh.ifi.access.course.controller;
22

3-
import ch.uzh.ifi.access.config.ApiTokenAuthenticationProvider;
43
import ch.uzh.ifi.access.course.CheckCoursePermission;
54
import ch.uzh.ifi.access.course.FilterByPublishingDate;
65
import ch.uzh.ifi.access.course.config.CourseAuthentication;
@@ -14,8 +13,10 @@
1413
import org.slf4j.LoggerFactory;
1514
import org.springframework.http.ResponseEntity;
1615
import org.springframework.security.access.prepost.PreAuthorize;
17-
import org.springframework.security.authentication.BadCredentialsException;
18-
import org.springframework.web.bind.annotation.*;
16+
import org.springframework.web.bind.annotation.GetMapping;
17+
import org.springframework.web.bind.annotation.PathVariable;
18+
import org.springframework.web.bind.annotation.RequestMapping;
19+
import org.springframework.web.bind.annotation.RestController;
1920
import springfox.documentation.annotations.ApiIgnore;
2021

2122
import java.util.ArrayList;
@@ -87,18 +88,4 @@ public ResponseEntity<?> getCourseAssistants(@PathVariable String courseId) {
8788
UserService.UserQueryResult users = userService.getCourseAdmins(course);
8889
return ResponseEntity.ok(users.getUsersFound());
8990
}
90-
91-
@PostMapping(path = "{id}/update")
92-
public void updateCourse(@PathVariable("id") String id, @RequestBody String json,
93-
ApiTokenAuthenticationProvider.GithubHeaderAuthentication authentication) {
94-
logger.debug("Received web hook");
95-
96-
if (!authentication.matchesHmacSignature(json)) {
97-
throw new BadCredentialsException("Hmac signature does not match!");
98-
}
99-
100-
logger.debug("Updating courses");
101-
courseService.updateCourseById(id);
102-
}
103-
10491
}

src/main/java/ch/uzh/ifi/access/course/controller/ExerciseController.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import java.io.File;
1717
import java.io.IOException;
18-
import java.nio.file.Files;
1918
import java.util.Map;
2019
import java.util.Optional;
2120

@@ -64,7 +63,7 @@ public ResponseEntity<Resource> getFile(
6463
File fileHandle = file.get().getFile();
6564
FileSystemResource r = new FileSystemResource(fileHandle);
6665
return ResponseEntity.ok()
67-
.contentType(MediaType.parseMediaType(Files.probeContentType(fileHandle.toPath())))
66+
.contentType(MediaType.APPLICATION_OCTET_STREAM)
6867
.body(r);
6968
}
7069
}
@@ -94,7 +93,7 @@ public ResponseEntity<Resource> searchForFile(
9493
File fileHandle = file.get().getFile();
9594
FileSystemResource r = new FileSystemResource(fileHandle);
9695
return ResponseEntity.ok()
97-
.contentType(MediaType.parseMediaType(Files.probeContentType(fileHandle.toPath())))
96+
.contentType(MediaType.APPLICATION_OCTET_STREAM)
9897
.body(r);
9998
}
10099
}

src/main/java/ch/uzh/ifi/access/course/controller/WebhooksController.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package ch.uzh.ifi.access.course.controller;
22

33
import ch.uzh.ifi.access.config.ApiTokenAuthenticationProvider;
4+
import ch.uzh.ifi.access.course.model.Course;
45
import ch.uzh.ifi.access.course.service.CourseService;
6+
import com.fasterxml.jackson.databind.JsonNode;
7+
import lombok.Value;
58
import org.slf4j.Logger;
69
import org.slf4j.LoggerFactory;
10+
import org.springframework.http.ResponseEntity;
711
import org.springframework.security.authentication.BadCredentialsException;
812
import org.springframework.web.bind.annotation.*;
913

14+
import java.util.Optional;
15+
1016
@RestController
1117
@RequestMapping("/webhooks")
1218
public class WebhooksController {
@@ -32,6 +38,17 @@ public void updateCourse(@PathVariable("id") String id, @RequestBody String json
3238
courseService.updateCourseById(id);
3339
}
3440

41+
@PostMapping(path = "/courses/update/github")
42+
public ResponseEntity<?> updateCourse(@RequestBody JsonNode payload, ApiTokenAuthenticationProvider.GithubHeaderAuthentication authentication) {
43+
logger.info("Received github web hook");
44+
45+
if (!authentication.matchesHmacSignature(payload.toString())) {
46+
throw new BadCredentialsException("Hmac signature does not match!");
47+
}
48+
49+
return processWebhook(payload, false);
50+
}
51+
3552
@PostMapping(path = "/courses/{id}/update/gitlab")
3653
public void updateCourse(@PathVariable("id") String id,
3754
ApiTokenAuthenticationProvider.GitlabHeaderAuthentication authentication) {
@@ -44,4 +61,61 @@ public void updateCourse(@PathVariable("id") String id,
4461
logger.info("Updating courses");
4562
courseService.updateCourseById(id);
4663
}
64+
65+
@PostMapping(path = "/courses/update/gitlab")
66+
public ResponseEntity<?> updateCourse(@RequestBody JsonNode payload, ApiTokenAuthenticationProvider.GitlabHeaderAuthentication authentication) {
67+
logger.info("Received gitlab web hook");
68+
69+
if (!authentication.isMatchesSecret()) {
70+
throw new BadCredentialsException("Header secret does not match!");
71+
}
72+
73+
return processWebhook(payload, true);
74+
}
75+
76+
private ResponseEntity<String> processWebhook(JsonNode payload, boolean isGitlab) {
77+
logger.info("Updating course");
78+
WebhookPayload webhookPayload = new WebhookPayload(payload, isGitlab);
79+
Optional<Course> courseToUpdate = courseService.getAllCourses().stream().filter(course -> webhookPayload.matchesCourseUrl(course.getGitURL())).findFirst();
80+
courseToUpdate.ifPresent(c -> courseService.updateCourseById(c.getId()));
81+
return courseToUpdate.map(c -> ResponseEntity.accepted().body(c.getId())).orElse(ResponseEntity.notFound().build());
82+
}
83+
84+
@Value
85+
public static class WebhookPayload {
86+
87+
private JsonNode repository;
88+
89+
private boolean isGitlab;
90+
91+
public WebhookPayload(JsonNode root, boolean isGitlab) {
92+
this.repository = root.get("repository");
93+
this.isGitlab = isGitlab;
94+
}
95+
96+
public String getHtmlUrl() {
97+
if (isGitlab) {
98+
return repository.get("homepage").asText();
99+
}
100+
return repository.get("html_url").asText();
101+
}
102+
103+
public String getGitUrl() {
104+
if (isGitlab) {
105+
return repository.get("git_http_url").asText();
106+
}
107+
return repository.get("clone_url").asText();
108+
}
109+
110+
public String getSshUrl() {
111+
if (isGitlab) {
112+
return repository.get("git_ssh_url").asText();
113+
}
114+
return repository.get("ssh_url").asText();
115+
}
116+
117+
public boolean matchesCourseUrl(String courseUrl) {
118+
return courseUrl.equalsIgnoreCase(getHtmlUrl()) || courseUrl.equalsIgnoreCase(getGitUrl()) || courseUrl.equalsIgnoreCase(getSshUrl());
119+
}
120+
}
47121
}

src/main/java/ch/uzh/ifi/access/course/dao/CourseDAO.java

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.fasterxml.jackson.databind.ObjectMapper;
1111
import com.fasterxml.jackson.databind.ObjectWriter;
1212
import lombok.Data;
13+
import org.apache.commons.lang.SerializationUtils;
1314
import org.slf4j.Logger;
1415
import org.slf4j.LoggerFactory;
1516
import org.springframework.core.io.ClassPathResource;
@@ -40,15 +41,18 @@ public class CourseDAO {
4041

4142
private BreakingChangeNotifier breakingChangeNotifier;
4243

43-
public CourseDAO(BreakingChangeNotifier breakingChangeNotifier) {
44+
private RepoCacher repoCacher;
45+
46+
public CourseDAO(BreakingChangeNotifier breakingChangeNotifier, RepoCacher repoCacher) {
4447
this.breakingChangeNotifier = breakingChangeNotifier;
48+
this.repoCacher = repoCacher;
4549

4650
ClassPathResource resource = new ClassPathResource(CONFIG_FILE);
4751
if (resource.exists()) {
4852
try {
4953
ObjectMapper mapper = new ObjectMapper();
5054
URLList conf = mapper.readValue(resource.getFile(), URLList.class);
51-
courseList = RepoCacher.retrieveCourseData(conf.repositories);
55+
courseList = repoCacher.retrieveCourseData(conf.repositories);
5256
exerciseIndex = buildExerciseIndex(courseList);
5357
logger.info(String.format("Parsed %d courses", courseList.size()));
5458

@@ -95,14 +99,35 @@ protected Map<String, Exercise> buildExerciseIndex(List<Course> courses) {
9599
public Course updateCourseById(String id) {
96100
Course c = selectCourseById(id)
97101
.orElseThrow(() -> new ResourceNotFoundException("No course found"));
102+
103+
logger.info("Updating course {} {}", c.getTitle(), id);
104+
105+
return updateCourse(c);
106+
}
107+
108+
protected Course updateCourse(Course c) {
109+
Course clone = (Course) SerializationUtils.clone(c);
110+
Course newCourse;
111+
112+
// Try to pull new course
98113
try {
99-
Course courseUpdate = RepoCacher.retrieveCourseData(new String[]{c.getGitURL()}).get(0);
100-
updateCourse(c, courseUpdate);
101-
return c;
114+
newCourse = repoCacher.retrieveCourseData(new String[]{c.getGitURL()}).get(0);
102115
} catch (Exception e) {
116+
logger.error("Failed to generate new course", e);
117+
return null;
118+
}
119+
120+
// Try to update
121+
try {
122+
updateCourse(c, newCourse);
123+
} catch (Exception e) {
124+
// If we fail during updating we try to revert to original
103125
logger.error("Failed to update course", e);
126+
updateCourse(c, clone);
127+
return null;
104128
}
105-
return null;
129+
130+
return c;
106131
}
107132

108133
void updateCourse(Course before, Course after) {

0 commit comments

Comments
 (0)