Skip to content

Commit 7957f91

Browse files
committed
parse v2 yaml configuration
1 parent 2d01788 commit 7957f91

19 files changed

+656
-2
lines changed

pom.xml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1717
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
1818

19-
<spring-boot.version>3.3.4</spring-boot.version>
19+
<spring-boot.version>3.3.7</spring-boot.version>
2020
<instancio-junit.version>2.9.0</instancio-junit.version>
2121
<testcontainers.version>1.20.4</testcontainers.version>
2222
<springdoc-openapi.version>2.5.0</springdoc-openapi.version>
@@ -124,6 +124,16 @@
124124
<artifactId>micrometer-registry-prometheus</artifactId>
125125
</dependency>
126126

127+
<dependency>
128+
<groupId>com.fasterxml.jackson.dataformat</groupId>
129+
<artifactId>jackson-dataformat-yaml</artifactId>
130+
</dependency>
131+
132+
<dependency>
133+
<groupId>com.fasterxml.jackson.datatype</groupId>
134+
<artifactId>jackson-datatype-jsr310</artifactId>
135+
</dependency>
136+
127137
<dependency>
128138
<groupId>org.springframework.boot</groupId>
129139
<artifactId>spring-boot-starter-test</artifactId>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package com.flowci.user;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.flowci.yaml.business;
2+
3+
import com.flowci.yaml.model.FlowV2;
4+
5+
public interface ParseYamlV2 {
6+
FlowV2 invoke(String yaml);
7+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.flowci.yaml.business.impl;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
6+
import com.flowci.common.validator.ValidName;
7+
import com.flowci.yaml.business.ParseYamlV2;
8+
import com.flowci.yaml.exception.InvalidYamlException;
9+
import com.flowci.yaml.model.FlowV2;
10+
import com.flowci.yaml.model.StepV2;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.util.CollectionUtils;
14+
15+
import java.util.HashSet;
16+
import java.util.List;
17+
18+
import static java.lang.String.format;
19+
import static org.springframework.util.CollectionUtils.isEmpty;
20+
import static org.springframework.util.StringUtils.hasText;
21+
22+
@Slf4j
23+
@Component
24+
public class ParseYamlV2Impl implements ParseYamlV2 {
25+
26+
private static final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
27+
private static final ValidName.NameValidator nameValidator = new ValidName.NameValidator();
28+
29+
static {
30+
objectMapper.findAndRegisterModules();
31+
}
32+
33+
@Override
34+
public FlowV2 invoke(String yaml) {
35+
try {
36+
var flowV2 = objectMapper.readValue(yaml, FlowV2.class);
37+
for (var step : flowV2.getSteps()) {
38+
step.setParent(flowV2);
39+
}
40+
return validateYaml(flowV2);
41+
} catch (JsonProcessingException e) {
42+
log.error("invalid YAML configuration", e);
43+
throw new InvalidYamlException("invalid YAML configuration");
44+
}
45+
}
46+
47+
private FlowV2 validateYaml(FlowV2 flowV2) {
48+
var steps = flowV2.getSteps();
49+
validateSteps(steps);
50+
51+
return flowV2;
52+
}
53+
54+
private void validateSteps(List<StepV2> steps) {
55+
if (CollectionUtils.isEmpty(steps)) {
56+
throw new InvalidYamlException("at least one step is required");
57+
}
58+
59+
var stepNameSet = new HashSet<>(steps.size());
60+
for (var step : steps) {
61+
if (!stepNameSet.add(step.getName())) {
62+
throw new InvalidYamlException(format("step name '%s' already exists", step.getName()));
63+
}
64+
}
65+
66+
for (var step : steps) {
67+
if (!nameValidator.isValid(step.getName(), null)) {
68+
throw new InvalidYamlException(format("step name '%s' is invalid", step.getName()));
69+
}
70+
71+
if (!isEmpty(step.getDependsOn())) {
72+
for (var dependsOn : step.getDependsOn()) {
73+
if (!stepNameSet.contains(dependsOn)) {
74+
throw new InvalidYamlException(format("depends on '%s' is not found", dependsOn));
75+
}
76+
}
77+
}
78+
79+
validateCommands(step);
80+
}
81+
}
82+
83+
private void validateCommands(StepV2 step) {
84+
var commands = step.getCommands();
85+
86+
if (CollectionUtils.isEmpty(commands)) {
87+
throw new InvalidYamlException(format("at least one command under step '%s' is required", step.getName()));
88+
}
89+
90+
for (var command : commands) {
91+
if (!hasText(command.getBash()) || !hasText(command.getPwsh())) {
92+
throw new InvalidYamlException("bash or powershell is required");
93+
}
94+
}
95+
}
96+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.flowci.yaml.exception;
2+
3+
import com.flowci.common.exception.BusinessException;
4+
5+
public class InvalidYamlException extends BusinessException {
6+
public InvalidYamlException(String message) {
7+
super(message);
8+
}
9+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.flowci.yaml.model;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
6+
import java.util.LinkedHashMap;
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
@Getter
11+
@Setter
12+
public abstract class BaseV2 {
13+
14+
private static final Integer DEFAULT_TIMEOUT = 1800;
15+
16+
protected Map<String, String> variables = new LinkedHashMap<>();
17+
18+
protected Integer timeout = DEFAULT_TIMEOUT; // timeout in seconds
19+
20+
/**
21+
* Groovy script
22+
*/
23+
protected String condition;
24+
25+
protected DockerV2 docker;
26+
27+
protected List<DockerV2> dockers;
28+
29+
public String getCondition() {
30+
return condition.trim();
31+
}
32+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.flowci.yaml.model;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
import org.springframework.util.StringUtils;
6+
7+
import static org.springframework.util.StringUtils.hasText;
8+
9+
@Getter
10+
@Setter
11+
public class CommandV2 {
12+
13+
private String name; // optional
14+
15+
private String bash; // bash script
16+
17+
private String pwsh; // powershell script
18+
19+
public String getBash() {
20+
return hasText(bash) ? bash.trim() : null;
21+
}
22+
23+
public String getPwsh() {
24+
return hasText(pwsh) ? pwsh.trim() : null;
25+
}
26+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.flowci.yaml.model;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
7+
import java.util.Map;
8+
import java.util.List;
9+
10+
@Getter
11+
@Setter
12+
public class DockerV2 {
13+
14+
private String image;
15+
16+
private String auth; // auth secret for private docker registry
17+
18+
private String name;
19+
20+
private String network;
21+
22+
private List<String> ports;
23+
24+
private List<String> entrypoint;
25+
26+
private List<String> command;
27+
28+
private Map<String, String> environment;
29+
30+
@JsonProperty("is_runtime")
31+
private Boolean isRuntime;
32+
33+
@JsonProperty("stop_on_finish")
34+
private Boolean stopOnFinish;
35+
36+
@JsonProperty("delete_on_finish")
37+
private Boolean deleteOnFinish;
38+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.flowci.yaml.model;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
6+
import java.util.List;
7+
8+
@Setter
9+
@Getter
10+
public class FlowV2 extends BaseV2 {
11+
12+
/**
13+
* List of agent tags
14+
*/
15+
private List<String> agents;
16+
17+
private List<StepV2> steps;
18+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.flowci.yaml.model;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnore;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.flowci.common.model.Variables;
6+
import lombok.Data;
7+
import lombok.EqualsAndHashCode;
8+
9+
import java.util.List;
10+
11+
@Data
12+
@EqualsAndHashCode(of = "name", callSuper = false)
13+
public class StepV2 extends BaseV2 {
14+
15+
private static final Boolean DEFAULT_ALLOW_FAILURE = false;
16+
17+
private String name;
18+
19+
// dependency steps name
20+
@JsonProperty("depends_on")
21+
private List<String> dependsOn;
22+
23+
private String plugin;
24+
25+
private Integer retry; // num of retry
26+
27+
@JsonProperty("allow_failure")
28+
private Boolean allowFailure = DEFAULT_ALLOW_FAILURE;
29+
30+
private List<CommandV2> commands;
31+
32+
private List<String> output;
33+
34+
private List<String> secrets;
35+
36+
private List<String> configs;
37+
38+
// ref to parent flow
39+
@JsonIgnore
40+
private FlowV2 parent;
41+
42+
@Override
43+
public DockerV2 getDocker() {
44+
if (this.docker != null) {
45+
return this.docker;
46+
}
47+
48+
if (this.parent.docker != null) {
49+
return this.parent.docker;
50+
}
51+
52+
return null;
53+
}
54+
55+
@Override
56+
public List<DockerV2> getDockers() {
57+
if (this.dockers != null) {
58+
return this.dockers;
59+
}
60+
61+
if (this.parent.dockers != null) {
62+
return this.parent.dockers;
63+
}
64+
65+
return null;
66+
}
67+
68+
// merge variables from current step and parent flow
69+
@Override
70+
public Variables getVariables() {
71+
var variables = new Variables(parent.getVariables());
72+
variables.putAll(this.variables);
73+
return variables;
74+
}
75+
}

0 commit comments

Comments
 (0)