Compare commits

...

6 Commits

55 changed files with 1318 additions and 84 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
.idea/
*.iml
target/
testGit/
application-local.properties

View File

@ -2,6 +2,10 @@
NATIVE_VERSION=1.0.0
start-runner:
docker run -d --name gitlab-runner --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock \
-v gitlab-runner-config:/etc/gitlab-runner gitlab/gitlab-runner:latest
build:
./mvnw compile

View File

@ -1,10 +0,0 @@
# gitlab runner
command used to register new runner
```shell script
$ docker run -d --name gitlab-runner --restart always -v /var/run/docker.sock:/var/run/docker.sock -v gitlab-runner-config:/etc/gitlab-runner gitlab/gitlab-runner:latest
$ gitlab-runner register -non-interactive --description "manualy registered gitlab runner" --url "http://172.17.0.1
:8080/" --registration-token "registration_token" --tag-list "docker,manual"
```

40
misc/doc-gitlab-runner.md Normal file
View File

@ -0,0 +1,40 @@
# gitlab runner
## start gitlab runner
```shell script
docker run -d --name gitlab-runner --restart always -v /var/run/docker.sock:/var/run/docker.sock -v gitlab-runner-config:/etc/gitlab-runner gitlab/gitlab-runner:latest
```
## register new runner
```shell script
gitlab-runner register -non-interactive --description "manualy registered gitlab runner" --url "http://172.17.0.1:8080/" --registration-token "3b79eb1f-32f3-4db2-ad1b-6702e476d839" --tag-list "docker,manual" --executor shell
```
with curl
```shell script
curl --request POST -H 'Content-Type: application/json' "http://localhost:8080/api/v4/runners" -d '{"description":"manualy registered gitlab runner","tag_list":"docker,manual","run_untagged":false,"locked":true,"active":true,"info":{"name":"gitlab-runner","version":"13.3.1","revision":"738bbe5a","platform":"linux","architecture":"amd64","features":{"variables":false,"image":false,"services":false,"artifacts":false,"cache":false,"shared":false,"upload_multiple_artifacts":false,"upload_raw_artifacts":false,"session":false,"terminal":false,"refspecs":false,"masking":false,"proxy":false,"raw_variables":false,"artifacts_exclude":false,"multi_build_steps":false}},"token":"registration_token"}'
```
## unregister runner
```shell script
gitlab-runner unregister --url "http://172.17.0.1:8080/" --token "3b79eb1f-32f3-4db2-ad1b-6702e476d839"
```
## list runners
```shell script
gitlab-runner list
```
local listing :
cf => /etc/gitlab-runner/config.toml
## verify which runner is always alive
```shell script
gitlab-runner verify --delete
```

View File

@ -0,0 +1,16 @@
[PATCH /api/v4/jobs/0/trace HTTP/1.1
Host: 172.17.0.1:8080
User-Agent: gitlab-runner 13.3.1 (13-3-stable; go1.13.8; linux/amd64)
Content-Length: 363
Content-Range: 0-362
Content-Type: text/plain
Job-Token:
Accept-Encoding: gzip]
Running with gitlab-runner 13.3.1 (738bbe5a)
on manualy registered gitlab runner f336d593
Preparing the "shell" executor
Using Shell executor...
Preparing environment
Running on 94afd2dbc667...
ERROR: Job failed: panic: runtime error: slice bounds out of range [:8] with length 0

View File

@ -0,0 +1,37 @@
[POST /api/v4/jobs/request HTTP/1.1
Host: 172.17.0.1:8080
User-Agent: gitlab-runner 13.3.1 (13-3-stable; go1.13.8; linux/amd64)
Content-Length: 510
Accept: application/json
Content-Type: application/json
Accept-Encoding: gzip]
{
"info":{
"name":"gitlab-runner",
"version":"13.3.1",
"revision":"738bbe5a",
"platform":"linux",
"architecture":"amd64",
"executor":"shell",
"shell":"bash",
"features":{
"variables":true,
"image":false,
"services":false,
"artifacts":true,
"cache":true,
"shared":true,
"upload_multiple_artifacts":true,
"upload_raw_artifacts":true,
"session":true,
"terminal":true,
"refspecs":true,
"masking":true,
"proxy":false,
"raw_variables":true,
"artifacts_exclude":true,
"multi_build_steps":true
}
},
"token":"76a79b73-b211-48c6-a3da-6b99fb8b0612"
}

View File

@ -0,0 +1,40 @@
[POST /api/v4/runners HTTP/1.1
Host: 172.17.0.1:8080
User-Agent: gitlab-runner 13.3.1 (13-3-stable; go1.13.8; linux/amd64)
Content-Length: 510
Accept: application/json
Content-Type: application/json
Accept-Encoding: gzip]
{
"description": "manualy registered gitlab runner",
"tag_list": "docker,manual",
"run_untagged": false,
"locked": true,
"active": true,
"info": {
"name": "gitlab-runner",
"version": "13.3.1",
"revision": "738bbe5a",
"platform": "linux",
"architecture": "amd64",
"features": {
"variables": false,
"image": false,
"services": false,
"artifacts": false,
"cache": false,
"shared": false,
"upload_multiple_artifacts": false,
"upload_raw_artifacts": false,
"session": false,
"terminal": false,
"refspecs": false,
"masking": false,
"proxy": false,
"raw_variables": false,
"artifacts_exclude": false,
"multi_build_steps": false
}
},
"token": "registration_token"
}

View File

@ -0,0 +1,8 @@
[POST /api/v4/runners/verify HTTP/1.1
Host: 172.17.0.1:8080
User-Agent: gitlab-runner 13.3.1 (13-3-stable; go1.13.8; linux/amd64)
Content-Length: 48
Content-Type: application/json
Accept-Encoding: gzip]
{"token":"19b280e8-9c56-4566-8234-7a172d717be3"}

View File

@ -0,0 +1,115 @@
{
"secret": "toto",
"ref": "refs/heads/master",
"before": "1831a5d892851ec601c3f1b6c26245f18a9bcb58",
"after": "526300af0655f2eeb43c6dc553be4b64d96701c7",
"compare_url": "https://antoine-roux.ml/projects/antoine/testGit/compare/1831a5d892851ec601c3f1b6c26245f18a9bcb58...526300af0655f2eeb43c6dc553be4b64d96701c7",
"commits": [
{
"id": "526300af0655f2eeb43c6dc553be4b64d96701c7",
"message": "test trigger webhook\n",
"url": "https://antoine-roux.ml/projects/antoine/testGit/commit/526300af0655f2eeb43c6dc553be4b64d96701c7",
"author": {
"name": "Antoine",
"email": "antoinroux@hotmail.fr",
"username": "antoine"
},
"committer": {
"name": "Antoine",
"email": "antoinroux@hotmail.fr",
"username": "antoine"
},
"verification": null,
"timestamp": "2020-09-16T23:23:16+02:00",
"added": [
"toto"
],
"removed": [],
"modified": []
}
],
"head_commit": null,
"repository": {
"id": 124,
"owner": {
"id": 1,
"login": "antoine",
"full_name": "",
"email": "antoinroux@hotmail.fr",
"avatar_url": "https://antoine-roux.ml/projects/user/avatar/antoine/-1",
"language": "fr-FR",
"is_admin": true,
"last_login": "2020-09-15T21:45:21Z",
"created": "2018-08-26T06:16:47Z",
"username": "antoine"
},
"name": "testGit",
"full_name": "antoine/testGit",
"description": "repository for test some git feature",
"empty": false,
"private": true,
"fork": false,
"template": false,
"parent": null,
"mirror": false,
"size": 24,
"html_url": "https://antoine-roux.ml/projects/antoine/testGit",
"ssh_url": "gitolite@antoine-roux.ml:antoine/testGit.git",
"clone_url": "https://antoine-roux.ml/projects/antoine/testGit.git",
"original_url": "",
"website": "",
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "master",
"archived": false,
"created_at": "2018-11-05T22:35:38Z",
"updated_at": "2020-09-16T21:23:48Z",
"permissions": {
"admin": true,
"push": true,
"pull": true
},
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": true,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": true,
"allow_rebase": true,
"allow_rebase_explicit": false,
"allow_squash_merge": false,
"avatar_url": ""
},
"pusher": {
"id": 1,
"login": "antoine",
"full_name": "",
"email": "antoinroux@hotmail.fr",
"avatar_url": "https://antoine-roux.ml/projects/user/avatar/antoine/-1",
"language": "fr-FR",
"is_admin": true,
"last_login": "2020-09-15T21:45:21Z",
"created": "2018-08-26T06:16:47Z",
"username": "antoine"
},
"sender": {
"id": 1,
"login": "antoine",
"full_name": "",
"email": "antoinroux@hotmail.fr",
"avatar_url": "https://antoine-roux.ml/projects/user/avatar/antoine/-1",
"language": "fr-FR",
"is_admin": true,
"last_login": "2020-09-15T21:45:21Z",
"created": "2018-08-26T06:16:47Z",
"username": "antoine"
}
}

16
pom.xml
View File

@ -9,6 +9,7 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0-M2</version>
<!-- <version>2.3.2.RELEASE</version>-->
<relativePath/> <!-- lookup parent from repository -->
</parent>
@ -32,6 +33,8 @@
<start-class>tk.antoine_roux.wiki.MainLauncher</start-class>
<spring-graalvm-native.version>0.8.0</spring-graalvm-native.version>
<native-image-maven-plugin.version>20.2.0</native-image-maven-plugin.version>
<jackson-datatype.version>2.11.2</jackson-datatype.version>
<org.eclipse.jgit.version>5.9.0.202009080501-r</org.eclipse.jgit.version>
</properties>
<dependencies>
@ -58,6 +61,19 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>${org.eclipse.jgit.version}</version>
</dependency>
</dependencies>
<build>

5
readme.md Normal file
View File

@ -0,0 +1,5 @@
# Gitlab runner gateway
This aims of this repository is to catch gitea webhook, parse .gitlab-ci.yml file and run some job with gitlab-runner from docker container. Some documentation about [gitlab-runner](./misc/doc-gitlab-runner.md)
This API could be compile with graalVM to native image see [Makefile](./Makefile)

View File

@ -1,40 +1,51 @@
package tk.antoine_roux.wiki;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import tk.antoine_roux.wiki.annotation.ApiPrefix;
import tk.antoine_roux.wiki.annotation.ApiVersion;
import tk.antoine_roux.wiki.configuration.Exception.DeleteRunnerException;
import tk.antoine_roux.wiki.model.internal.GitlabCI;
import tk.antoine_roux.wiki.model.internal.Runner;
import tk.antoine_roux.wiki.model.request.AddRunner;
import tk.antoine_roux.wiki.model.request.HookEvent;
import tk.antoine_roux.wiki.model.request.JobRequest;
import tk.antoine_roux.wiki.model.request.TokenRunner;
import tk.antoine_roux.wiki.model.response.JobResponse;
import tk.antoine_roux.wiki.model.response.RegisterRunnerResponse;
import tk.antoine_roux.wiki.service.GitService;
import tk.antoine_roux.wiki.service.JobManager;
import tk.antoine_roux.wiki.service.RunnerRegistrar;
import tk.antoine_roux.wiki.utilitary.Boolean;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static tk.antoine_roux.wiki.Constant.*;
import static tk.antoine_roux.wiki.utilitary.Constant.*;
@RestController
@ApiPrefix(API_PREFIX)
public class ControllerHandlers {
private RunnerRegistrar runnerRegistrar;
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final RunnerRegistrar runnerRegistrar;
private final JobManager jobManager;
private final GitService gitService;
@Autowired
public ControllerHandlers(RunnerRegistrar runnerRegistrar) {
public ControllerHandlers(RunnerRegistrar runnerRegistrar, JobManager jobManager, GitService gitService) {
this.runnerRegistrar = runnerRegistrar;
}
/**
* add register new runners
*
* @param body
* @return
*/
@ResponseBody
@ApiVersion({API_VERSION})
@PostMapping(value = "/runners", produces = APPLICATION_JSON_VALUE)
public static ResponseEntity<String> addRunner(@RequestBody String body) {
System.out.println(body);
return ResponseEntity.ok(body);
this.jobManager = jobManager;
this.gitService = gitService;
}
/**
@ -44,7 +55,7 @@ public class ControllerHandlers {
*/
@ResponseBody
@GetMapping("/info")
public static ResponseEntity<TreeMap<String, String>> info() {
public ResponseEntity<TreeMap<String, String>> info() {
return ResponseEntity.ok(
// sort attribute by key name
new TreeMap<>(
@ -55,4 +66,76 @@ public class ControllerHandlers {
)
);
}
/**
* add register new runners
*
* @param body
* @return
*/
@ResponseBody
@ApiVersion({API_VERSION})
@PostMapping("/runners")
public ResponseEntity<RegisterRunnerResponse> addRunner(@RequestBody AddRunner body) {
logger.debug("Receive register runner request " + body);
Runner createdRunner = this.runnerRegistrar.addRunner(body);
RegisterRunnerResponse response = createdRunner.toRegisterResponse();
logger.debug("Response register runner " + response);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@ApiVersion(API_VERSION)
@DeleteMapping("/runners")
public ResponseEntity<Void> deleteRunner(@RequestBody TokenRunner body) throws Exception {
logger.debug("Receive delete runner request " + body);
Boolean.trueOrElseThrow(this.runnerRegistrar.removeRunnerByRegistrationToken(body), DeleteRunnerException::new);
logger.debug("Successfully delete runner ");
return ResponseEntity.noContent().build();
}
@ApiVersion(API_VERSION)
@PostMapping("/runners/verify")
public ResponseEntity<Void> runnerExist(@RequestBody TokenRunner body) {
boolean isValid = this.runnerRegistrar.checkRunner(body);
if (isValid) {
return ResponseEntity.ok().build();
} else {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
}
@ApiVersion(API_VERSION)
@PostMapping("/jobs/request")
public ResponseEntity<JobResponse> jobRequest(@RequestBody JobRequest jobRequest) {
Optional<JobResponse> currentJob = this.jobManager.popJob(jobRequest);
return currentJob
.map(job -> ResponseEntity.status(HttpStatus.CREATED).body(job))
.orElseGet(() -> ResponseEntity.noContent().build());
}
@ApiVersion(API_VERSION)
@PatchMapping("jobs/0/trace")
public ResponseEntity<Void> receiveTrace(@RequestBody String traceContent) {
logger.info(traceContent);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@ApiVersion(API_VERSION)
@PostMapping("/webhook")
public ResponseEntity<Void> webhook(@RequestBody HookEvent webHookData) throws IOException, GitAPIException {
ResponseEntity.BodyBuilder responseEntity;
Optional<GitlabCI> gitlabCI = this.gitService.getYMLGitlabCI(webHookData);
if (gitlabCI.isPresent()) {
JobResponse job = gitlabCI.get().buildJobResponse();
this.jobManager.stackJob(job);
responseEntity = ResponseEntity.ok();
} else {
responseEntity = ResponseEntity.unprocessableEntity();
}
return responseEntity.build();
}
}

View File

@ -2,15 +2,18 @@ package tk.antoine_roux.wiki;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;
import tk.antoine_roux.wiki.configuration.GitConfiguration;
/**
* Main class
*/
// force spring application to not use glibc or any non jdk code which is bad for graalvm
@SpringBootApplication(proxyBeanMethods = false)
@EnableConfigurationProperties(GitConfiguration.GitlabCIContextProperties.class)
public class MainLauncher {
/**

View File

@ -1,41 +0,0 @@
package tk.antoine_roux.wiki;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* Class use to perist registered gitlab runner
*/
@Service
public class RunnerRegistrar {
private final List<Runner> runners = new ArrayList<>();
public List<Runner> getRunners() {
return this.runners;
}
public void addRunner(Runner r) {
this.runners.add(r);
}
/**
* in memory representation of gitlab runner
*/
public static class Runner {
private static final String TAG_SEPARATOR = ",";
public String id;
public String description;
public String[] tags;
public String registrationToken;
public Runner(String id, String description, String tags, String registrationToken) {
this.id = id;
this.description = description;
this.tags = tags.split(TAG_SEPARATOR);
this.registrationToken = registrationToken;
}
}
}

View File

@ -52,8 +52,8 @@ public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandle
RequestMappingInfo requestMappingInfo = new RequestMappingInfo(
new PatternsRequestCondition(
new String[]{annotationApiPrefix.value()},
false,
this.getPathMatcher()
this.getUrlPathHelper(), this.getPathMatcher(),
false
),
new RequestMethodsRequestCondition(),
new ParamsRequestCondition(),
@ -76,7 +76,11 @@ public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandle
}
return new RequestMappingInfo(
new PatternsRequestCondition(patterns, false, this.getPathMatcher()),
new PatternsRequestCondition(
patterns, this.getUrlPathHelper(), this.getPathMatcher(),
false
// , this.useTrailingSlashMatch(), this.getFileExtensions()
),
new RequestMethodsRequestCondition(),
new ParamsRequestCondition(),
new HeadersRequestCondition(),

View File

@ -0,0 +1,39 @@
package tk.antoine_roux.wiki.configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import tk.antoine_roux.wiki.configuration.Exception.DeleteRunnerException;
import tk.antoine_roux.wiki.configuration.Exception.NoIdException;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Custom http handler for dealing with {@link Exception}
*/
@ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({NoIdException.class})
public ResponseEntity<Object> handleInternalException(NoIdException ex, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("message", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler({DeleteRunnerException.class})
public ResponseEntity<Object> handleNotFoundException(NoIdException ex, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("message", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
}
}

View File

@ -0,0 +1,9 @@
package tk.antoine_roux.wiki.configuration.Exception;
public class DeleteRunnerException extends RuntimeException {
private static final long serialVersionUID = 559919382888691526L;
public DeleteRunnerException() {
super("Fail to delete runner");
}
}

View File

@ -0,0 +1,9 @@
package tk.antoine_roux.wiki.configuration.Exception;
public class InvalidObjectIdException extends RuntimeException {
private static final long serialVersionUID = 6274838148439186894L;
public InvalidObjectIdException(String commitStr) {
super("Try to convert invalid string (" + commitStr + ") to commit id");
}
}

View File

@ -0,0 +1,9 @@
package tk.antoine_roux.wiki.configuration.Exception;
public class NoIdException extends RuntimeException {
private static final long serialVersionUID = 1470945508482780554L;
public NoIdException() {
super("Invalid id value");
}
}

View File

@ -0,0 +1,50 @@
package tk.antoine_roux.wiki.configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration(proxyBeanMethods = false)
public class GitConfiguration {
@Bean
public YAMLFactory yamlFactory() {
return YAMLFactory.builder().build();
}
@Bean(name = "YAMLObjectMapper")
public ObjectMapper ObjectMapper(YAMLFactory yamlFactory) {
return new ObjectMapper(yamlFactory);
}
@Bean
public UsernamePasswordCredentialsProvider credentialsProvider(GitlabCIContextProperties gitlabCIContextProperties, Environment environment) {
return new UsernamePasswordCredentialsProvider(gitlabCIContextProperties.getUsername(), gitlabCIContextProperties.getPassword());
}
@ConfigurationProperties("gitlab-ci")
public static class GitlabCIContextProperties {
private String username;
private String password;
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}
}

View File

@ -1,25 +1,40 @@
package tk.antoine_roux.wiki.configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.resource.ResourceUrlProvider;
import tk.antoine_roux.wiki.Constant;
import tk.antoine_roux.wiki.utilitary.Constant;
/**
* spring web configuration
*
* @see org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations
*/
@Configuration(proxyBeanMethods = false)
public class WebConfiguration extends WebMvcConfigurationSupport {
public class WebConfiguration implements WebMvcRegistrations {
@Bean
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping(ContentNegotiationManager contentNegotiationManager,
FormattingConversionService conversionService,
ResourceUrlProvider resourceUrlProvider) {
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping(Constant.VERSION_PREFIX);
}
/**
* build default Spring boot {@link ObjectMapper}
* this bean avoid spring boot to auto detect {@link GitConfiguration#ObjectMapper(YAMLFactory)}
* as default {@link ObjectMapper}
*
* @param jackson2ObjectMapperBuilder
* @return
*/
@Bean
@Primary
public ObjectMapper jackson2ObjectMapperBuilder(Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder) {
return jackson2ObjectMapperBuilder.build();
}
}

View File

@ -0,0 +1,46 @@
package tk.antoine_roux.wiki.model.internal;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import tk.antoine_roux.wiki.model.response.JobResponse;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class GitlabCI {
public String image;
@JsonProperty("before_script")
public List<String> beforeScript = Collections.emptyList();
@JsonProperty("after_script")
public List<String> afterScript = Collections.emptyList();
public Map<String, Job> jobs = Collections.emptyMap();
@JsonAnySetter
public void setJobs(String key, Job value) {
this.jobs.put(key, value);
}
public JobResponse buildJobResponse() {
return null;
}
public enum RuleEnum {
IF("if");
String content;
RuleEnum(String s) {
this.content = s;
}
}
private static class Job {
public String stage;
public String image;
public Map<String, String> variables = Collections.emptyMap();
public List<String> services = Collections.emptyList();
public Map<RuleEnum, String> rules = Collections.emptyMap();
public List<String> script = Collections.emptyList();
}
}

View File

@ -0,0 +1,21 @@
package tk.antoine_roux.wiki.model.internal;
import tk.antoine_roux.wiki.model.response.RegisterRunnerResponse;
import java.util.List;
import java.util.UUID;
/**
* in memory representation of gitlab runner
*/
public class Runner {
public String description;
public List<String> tags;
public UUID registrationToken;
public UUID authenticationToken;
public Integer id;
public RegisterRunnerResponse toRegisterResponse() {
return new RegisterRunnerResponse(String.valueOf(this.id), this.authenticationToken.toString());
}
}

View File

@ -0,0 +1,29 @@
package tk.antoine_roux.wiki.model.request;
import com.fasterxml.jackson.annotation.JsonProperty;
import tk.antoine_roux.wiki.model.request.secondary.RunnerInfo;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* Model use to add runner instance
*/
public class AddRunner {
private static final String TAG_SEPARATOR = ",";
public String description;
public List<String> tags;
public UUID token;
@JsonProperty("run_untagged")
public Boolean runUntagged;
public Boolean locked;
public Boolean active;
public RunnerInfo info;
@JsonProperty("tag_list")
public void setTags(String tags) {
this.tags = Arrays.asList(tags.split(TAG_SEPARATOR).clone());
}
}

View File

@ -0,0 +1,55 @@
package tk.antoine_roux.wiki.model.request;
import com.fasterxml.jackson.annotation.JsonProperty;
import tk.antoine_roux.wiki.model.request.secondary.Commit;
import tk.antoine_roux.wiki.model.request.secondary.Repository;
import tk.antoine_roux.wiki.model.request.secondary.User;
import java.util.List;
public class HookEvent {
public String secret;
public String ref;
public String before;
public String after;
@JsonProperty("compare_url")
public String compareUrl;
public List<Commit> commits;
@JsonProperty("head_commit")
public String headCommit;
public Repository repository;
public User pusher;
public User sender;
// AtomicInteger idIncrementer = new AtomicInteger();
//
// /**
// * convert {@link HookEvent} to {@link Job} if possible
// * else return and {@link Optional#empty()}
// */
// public Optional<Job> toJob() {
// Optional<Job> optJob;
//
// if (this.commits.isEmpty()) {
// optJob = Optional.empty();
// } else {
// // search for head commit or take first in event's list of commit
// Commit commit = this.commits.stream().filter(co -> co.id.equals(this.headCommit))
// .findFirst().orElse(this.commits.get(0));
//
// Job.Commit co = new Job.Commit(
// commit.author.email, commit.author.name, commit.timestamp,
// commit.id, commit.message, commit.id.substring(0, 8), commit.message
// );
//
// Job job = new Job(
// null, co, null, ZonedDateTime.now(), null,
// this.idIncrementer.getAndIncrement(), UUID.randomUUID().toString(), this.ref,
// null, "root", null, JobStatus.CREATED, false, this.pusher.toReducedUser()
// );
// optJob = Optional.of(job);
// }
//
// return optJob;
// }
}

View File

@ -0,0 +1,10 @@
package tk.antoine_roux.wiki.model.request;
import tk.antoine_roux.wiki.model.request.secondary.RunnerInfo;
/**
* Job Request compose from {@link RunnerInfo} and token field
*/
public class JobRequest extends TokenRunner {
public RunnerInfo info;
}

View File

@ -0,0 +1,10 @@
package tk.antoine_roux.wiki.model.request;
import java.util.UUID;
/**
* Model use to remove runner instance
*/
public class TokenRunner {
public UUID token;
}

View File

@ -0,0 +1,22 @@
package tk.antoine_roux.wiki.model.request.secondary;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.ZonedDateTime;
import java.util.List;
public class Commit {
public String id;
public String message;
public String url;
public UserReduced author;
public UserReduced committer;
public String verification;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
public ZonedDateTime timestamp;
public List<String> added;
public List<String> removed;
public List<String> modified;
}

View File

@ -0,0 +1,15 @@
package tk.antoine_roux.wiki.model.request.secondary;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* model representation of gitea tracker
*/
public class InternalTracker {
@JsonProperty("enable_time_tracker")
public boolean enableTimeTracker;
@JsonProperty("allow_only_contributors_to_track_time")
public boolean allowOnlyContributorsToTrackTime;
@JsonProperty("enable_issue_dependencies")
public boolean enableIssueDependencies;
}

View File

@ -0,0 +1,10 @@
package tk.antoine_roux.wiki.model.request.secondary;
/**
* model about repository permission
*/
public class Permission {
public boolean admin;
public boolean push;
public boolean pull;
}

View File

@ -0,0 +1,71 @@
package tk.antoine_roux.wiki.model.request.secondary;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.ZonedDateTime;
public class Repository {
public String id;
public User owner;
public String name;
@JsonProperty("full_name")
public String fullName;
public String description;
public boolean empty;
@JsonProperty("private")
public boolean isPrivate;
public boolean fork;
public boolean template;
public String parent;
public boolean mirror;
public int size;
@JsonProperty("html_url")
public String htmlUrl;
@JsonProperty("ssh_url")
public String sshUrl;
@JsonProperty("clone_url")
public String cloneUrl;
@JsonProperty("original_url")
public String originalUrl;
public String website;
@JsonProperty("stars_count")
public int starsCount;
@JsonProperty("forks_count")
public int forksCount;
@JsonProperty("watchers_count")
public int watchersCount;
@JsonProperty("open_issues_count")
public int openIssuesCount;
@JsonProperty("open_pr_count")
public int openPRCount;
@JsonProperty("release_counter")
public int releaseCounter;
@JsonProperty("default_branch")
public String defaultBranch;
public boolean archived;
@JsonProperty("created_at")
public ZonedDateTime createdAt;
@JsonProperty("updated_at")
public ZonedDateTime updatedAt;
public Permission permissions;
@JsonProperty("has_issues")
public boolean hasIssues;
@JsonProperty("internal_tracker")
public InternalTracker internalTracker;
@JsonProperty("has_wiki")
public boolean hasWiki;
@JsonProperty("has_pull_requests")
public boolean hasPullRequests;
@JsonProperty("ignore_whitespace_conflicts")
public boolean ignoreWhitespaceConflicts;
@JsonProperty("allow_merge_commits")
public boolean allowMergeCommits;
@JsonProperty("allow_rebase")
public boolean allowRebase;
@JsonProperty("allow_rebase_explicit")
public boolean allowRebaseExplicit;
@JsonProperty("allow_squash_merge")
public boolean allowSquashMerge;
@JsonProperty("avatar_url")
public String avatarUrl;
}

View File

@ -0,0 +1,37 @@
package tk.antoine_roux.wiki.model.request.secondary;
import java.util.Map;
public class RunnerInfo {
public String name;
public String version;
public String revision;
public String platform;
public String architecture;
public String executor;
public String shell;
/**
* features can contains :
* <ul>
* <li>variables</li>
* <li>image</li>
* <li>services</li>
* <li>artifacts</li>
* <li>cache</li>
* <li>shared</li>
* <li>upload_multiple_artifacts</li>
* <li>upload_raw_artifacts</li>
* <li>session</li>
* <li>terminal</li>
* <li>refspecs</li>
* <li>masking</li>
* <li>proxy</li>
* <li>raw_variables</li>
* <li>artifacts_exclude</li>
* <li>multi_build_steps</li>
* </ul>
*/
public Map<String, Boolean> features;
}

View File

@ -0,0 +1,29 @@
package tk.antoine_roux.wiki.model.request.secondary;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.ZonedDateTime;
/**
* pusher sender user information
*/
public class User {
public String id;
public String login;
public String username;
@JsonProperty("full_name")
public String fullName;
public String email;
@JsonProperty("avatar_url")
public String avatarUrl;
public String language;
@JsonProperty("is_admin")
public boolean isAdmin;
@JsonProperty("last_login")
public ZonedDateTime lastLogin;
public ZonedDateTime created;
public UserReduced toReducedUser() {
return new UserReduced(this.login, this.email, this.username);
}
}

View File

@ -0,0 +1,16 @@
package tk.antoine_roux.wiki.model.request.secondary;
/**
* Reduced user information
*/
public class UserReduced {
public String name;
public String email;
public String username;
public UserReduced(String name, String email, String username) {
this.name = name;
this.email = email;
this.username = username;
}
}

View File

@ -0,0 +1,33 @@
package tk.antoine_roux.wiki.model.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import tk.antoine_roux.wiki.model.response.secondary.*;
import java.util.List;
import java.util.Map;
/**
* job format use to submit
* information to gitlab-runner
*/
public class JobResponse {
public Integer id;
public String token;
@JsonProperty("allow_git_fetch")
public boolean allowGitFetch;
@JsonProperty("job_info")
public JobInfo jobInfo;
@JsonProperty("git_info")
public GitInfo gitInfo;
public RunnerInfo runnerInfo;
public List<JobVariable> variables;
public List<Step> steps;
public Image image;
public List<Image> services;
public List<Artifact> artifacts;
public List<Cache> cache;
public List<Credential> credentials;
public List<Dependency> dependencies;
public GitLabFeatures features;
public Map<String, Secret> secrets;
}

View File

@ -0,0 +1,18 @@
package tk.antoine_roux.wiki.model.response;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Json response object for register endpoint
*/
public class RegisterRunnerResponse {
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
String id;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
String token;
public RegisterRunnerResponse(String id, String token) {
this.id = id;
this.token = token;
}
}

View File

@ -0,0 +1,19 @@
package tk.antoine_roux.wiki.model.response.secondary;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class Artifact {
public String name;
public boolean untracked;
public List<String> paths;
public List<String> exclude;
public String when;
@JsonProperty("artifact_type")
public String artifactType;
@JsonProperty("artifact_format")
public String artifactFormat;
@JsonProperty("expire_in")
public String expireIn;
}

View File

@ -0,0 +1,10 @@
package tk.antoine_roux.wiki.model.response.secondary;
import java.util.List;
public class Cache {
public String key;
public boolean untracked;
public String policy;
public List<String> paths;
}

View File

@ -0,0 +1,8 @@
package tk.antoine_roux.wiki.model.response.secondary;
public class Credential {
public String type;
public String url;
public String username;
public String password;
}

View File

@ -0,0 +1,16 @@
package tk.antoine_roux.wiki.model.response.secondary;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Dependency {
public int id;
public String token;
public String name;
@JsonProperty("artifacts_file")
public DependencyArtifactsFile artifactsFile;
public static class DependencyArtifactsFile {
public String filename;
public int size;
}
}

View File

@ -0,0 +1,16 @@
package tk.antoine_roux.wiki.model.response.secondary;
import com.fasterxml.jackson.annotation.JsonProperty;
public class GitInfo {
@JsonProperty("repo_url")
public String repoURL;
public String ref;
public String sha;
@JsonProperty("before_sha")
public String beforeSha;
@JsonProperty("ref_type")
public String refType;
public String[] refspecs;
public int depth;
}

View File

@ -0,0 +1,8 @@
package tk.antoine_roux.wiki.model.response.secondary;
import com.fasterxml.jackson.annotation.JsonProperty;
public class GitLabFeatures {
@JsonProperty("trace_sections")
public boolean traceSections;
}

View File

@ -0,0 +1,26 @@
package tk.antoine_roux.wiki.model.response.secondary;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.List;
public class Image {
public String name;
@JsonInclude(JsonInclude.Include.NON_NULL)
public String alias;
@JsonInclude(JsonInclude.Include.NON_NULL)
public List<String> command;
@JsonInclude(JsonInclude.Include.NON_NULL)
public List<String> entrypoint;
@JsonInclude(JsonInclude.Include.NON_NULL)
public Port ports;
static class Port {
@JsonInclude(JsonInclude.Include.NON_NULL)
public String name;
@JsonInclude(JsonInclude.Include.NON_NULL)
public int number;
@JsonInclude(JsonInclude.Include.NON_NULL)
public String protocol;
}
}

View File

@ -0,0 +1,14 @@
package tk.antoine_roux.wiki.model.response.secondary;
import com.fasterxml.jackson.annotation.JsonProperty;
public class JobInfo {
public String name;
public String stage;
@JsonProperty("project_id")
public String projectID;
@JsonProperty("project_name")
public String projectName;
}

View File

@ -0,0 +1,15 @@
package tk.antoine_roux.wiki.model.response.secondary;
import com.fasterxml.jackson.annotation.JsonProperty;
public class JobVariable {
public String key;
public String value;
@JsonProperty("public")
public boolean isPublic;
@JsonProperty("-")
public boolean internal;
public boolean file;
public boolean masked;
public boolean raw;
}

View File

@ -0,0 +1,5 @@
package tk.antoine_roux.wiki.model.response.secondary;
public class RunnerInfo {
public int timeout;
}

View File

@ -0,0 +1,33 @@
package tk.antoine_roux.wiki.model.response.secondary;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Map;
public class Secret {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public VaultSecret vault;
public static class VaultSecret {
public VaultServer server;
public VaultEngine engine;
public String path;
public String field;
}
public static class VaultServer {
public String url;
public VaultAuth auth;
}
public static class VaultAuth {
public String name;
public String path;
public Map<String, Object> data;
}
public static class VaultEngine {
public String name;
public String path;
}
}

View File

@ -0,0 +1,14 @@
package tk.antoine_roux.wiki.model.response.secondary;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class Step {
public String name;
public List<String> script;
public int timeout;
public String when;
@JsonProperty("allow_failure")
public boolean allowFailure;
}

View File

@ -0,0 +1,75 @@
package tk.antoine_roux.wiki.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import tk.antoine_roux.wiki.model.internal.GitlabCI;
import tk.antoine_roux.wiki.model.request.HookEvent;
import tk.antoine_roux.wiki.model.request.secondary.Commit;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Optional;
@Service
public class GitService {
public static final String GITLAB_CI_FILE_PATH = ".gitlab-ci.yml";
private static final String GITLAB_RUNNER_CLONE_PREFIX = "gitlab-runner-clone";
public final UsernamePasswordCredentialsProvider credentialsProvider;
private final ObjectMapper objectMapper;
@Autowired
public GitService(UsernamePasswordCredentialsProvider credentialsProvider, @Qualifier("YAMLObjectMapper") ObjectMapper objectMapper) {
this.credentialsProvider = credentialsProvider;
this.objectMapper = objectMapper;
}
/**
* return .gitlab-ci.yml content for {@link HookEvent}
*/
public Optional<GitlabCI> getYMLGitlabCI(HookEvent hookEvent) throws IOException, GitAPIException {
Optional<GitlabCI> optJob;
if (hookEvent.commits.isEmpty()) {
optJob = Optional.empty();
} else {
// search for head commit or take first in event's list of commit
Commit commit = hookEvent.commits.stream().filter(co -> co.id.equals(hookEvent.headCommit))
.findFirst().orElse(hookEvent.commits.get(0));
optJob = Optional.of(this.getGitlabCIContent(hookEvent.repository.cloneUrl, commit));
}
return optJob;
}
/**
* return .gitlab-ci.yml content for given commit into cloneUrl repository
*/
private GitlabCI getGitlabCIContent(String cloneURL, Commit commit) throws IOException, GitAPIException {
Git call = Git.cloneRepository()
.setURI(cloneURL)
.setCredentialsProvider(this.credentialsProvider)
.setDirectory(Files.createTempDirectory(GITLAB_RUNNER_CLONE_PREFIX).toFile())
.call();
RevWalk revWalk = new RevWalk(call.getRepository());
RevCommit revCommit = revWalk.parseCommit(ObjectId.fromString(commit.id));
try (TreeWalk walk = TreeWalk.forPath(call.getRepository(), GITLAB_CI_FILE_PATH, revCommit.getTree())) {
if (walk != null) {
byte[] bytes = call.getRepository().open(walk.getObjectId(0)).getBytes();
return this.objectMapper.readValue(bytes, GitlabCI.class);
} else {
throw new IllegalArgumentException("No path found.");
}
}
}
}

View File

@ -0,0 +1,27 @@
package tk.antoine_roux.wiki.service;
import org.springframework.stereotype.Service;
import tk.antoine_roux.wiki.model.request.JobRequest;
import tk.antoine_roux.wiki.model.request.HookEvent;
import tk.antoine_roux.wiki.model.response.JobResponse;
import java.util.Optional;
import java.util.concurrent.ConcurrentLinkedQueue;
@Service
public class JobManager {
/**
* concurrent list of {@link JobResponse} fill by {@link tk.antoine_roux.wiki.ControllerHandlers#webhook(HookEvent)}
* and pop by {@link tk.antoine_roux.wiki.ControllerHandlers#jobRequest(JobRequest)}
*/
ConcurrentLinkedQueue<JobResponse> jobQueue = new ConcurrentLinkedQueue<>();
public void stackJob(JobResponse newJob) {
this.jobQueue.add(newJob);
}
public Optional<JobResponse> popJob(JobRequest jobRequest) {
return Optional.ofNullable(this.jobQueue.poll());
}
}

View File

@ -0,0 +1,44 @@
package tk.antoine_roux.wiki.service;
import org.springframework.stereotype.Service;
import tk.antoine_roux.wiki.model.internal.Runner;
import tk.antoine_roux.wiki.model.request.AddRunner;
import tk.antoine_roux.wiki.model.request.TokenRunner;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Class use to perist registered gitlab runner
*/
@Service
public class RunnerRegistrar {
private final List<Runner> runners = new ArrayList<>();
AtomicInteger idIncrementer = new AtomicInteger();
public List<Runner> getRunners() {
return this.runners;
}
public Runner addRunner(AddRunner addRunner) {
Runner r = new Runner();
r.id = this.idIncrementer.getAndIncrement();
r.authenticationToken = UUID.randomUUID();
r.registrationToken = addRunner.token;
r.tags = addRunner.tags;
r.description = addRunner.description;
this.runners.add(r);
return r;
}
public boolean removeRunnerByRegistrationToken(TokenRunner r) {
return this.runners.removeIf(runner -> runner.authenticationToken.equals(r.token));
}
public boolean checkRunner(TokenRunner body) {
return this.runners.stream().anyMatch(runner -> runner.authenticationToken.equals(body.token));
}
}

View File

@ -0,0 +1,19 @@
package tk.antoine_roux.wiki.utilitary;
import java.util.function.Supplier;
public final class Boolean {
/**
* utility method throw ex if in is false, else follow in value
*
* @param in
* @param ex
* @throws Exception exception thrown when in param is false
*/
public static <T extends Exception> void trueOrElseThrow(boolean in, Supplier<T> ex) throws Exception {
if (!in) {
throw ex.get();
}
}
}

View File

@ -1,4 +1,4 @@
package tk.antoine_roux.wiki;
package tk.antoine_roux.wiki.utilitary;
/**
* Application level constant
@ -8,4 +8,6 @@ public final class Constant {
public static final String API_NAME = "gitlab-runner-gateway";
public static final String API_VERSION = "4";
public static final String VERSION_PREFIX = "v";
public static final int OBJECT_ID_STRING_LENGTH = 40;
}

View File

@ -1,6 +1,7 @@
spring.main.banner-mode=off
spring.main.lazy-initialization=false
spring.output.ansi.enabled=ALWAYS
spring.jackson.serialization.write-dates-as-timestamps=false
server.port=^application.port^
@ -8,9 +9,15 @@ server.port=^application.port^
logging.level.root=INFO
# this log print request content
logging.level.org.apache.coyote.http11.Http11InputBuffer=DEBUG
# spring boot actuator
management.server.port=8080
info.name=gitlab-runner-gateway
info.more.detail=This is a REST API use to gateway gitlab runner call to gitlab instance
management.endpoints.web.exposure.include=mappings
gitlab-ci.username=
gitlab-ci.password=