feature: backend list nodes by name, regex
This commit is contained in:
parent
7840f8c73e
commit
2983242dde
@ -1,3 +1,3 @@
|
|||||||
# Enable auto-env through the sdkman_auto_env config
|
# Enable auto-env through the sdkman_auto_env config
|
||||||
# Add key=value pairs of SDKs to use below
|
# Add key=value pairs of SDKs to use below
|
||||||
java=21.ea.31-open
|
java=20.0.1-tem
|
||||||
|
45
pom.xml
45
pom.xml
@ -15,12 +15,14 @@
|
|||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>20</java.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||||
|
|
||||||
<vavr.version>1.0.0-alpha-4</vavr.version>
|
<vavr.version>1.0.0-alpha-4</vavr.version>
|
||||||
|
<vavr-jackson.version>1.0.0-alpha-3</vavr-jackson.version>
|
||||||
|
<kubernetes-client.version>15.0.</kubernetes-client.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@ -30,6 +32,16 @@
|
|||||||
<artifactId>vavr</artifactId>
|
<artifactId>vavr</artifactId>
|
||||||
<version>${vavr.version}</version>
|
<version>${vavr.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.vavr</groupId>
|
||||||
|
<artifactId>vavr-jackson</artifactId>
|
||||||
|
<version>${vavr-jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.kubernetes</groupId>
|
||||||
|
<artifactId>client-java</artifactId>
|
||||||
|
<version>${kubernetes-client.version}1</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
@ -38,10 +50,27 @@
|
|||||||
<groupId>io.vavr</groupId>
|
<groupId>io.vavr</groupId>
|
||||||
<artifactId>vavr</artifactId>
|
<artifactId>vavr</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.vavr</groupId>
|
||||||
|
<artifactId>vavr-jackson</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.kubernetes</groupId>
|
||||||
|
<artifactId>client-java</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- test -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- dev -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-devtools</artifactId>
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
@ -49,4 +78,18 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
<compilerArgs>--enable-preview</compilerArgs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
4
src/main/java/tk/antoine/roux/domain/Resource.java
Normal file
4
src/main/java/tk/antoine/roux/domain/Resource.java
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package tk.antoine.roux.domain;
|
||||||
|
|
||||||
|
public interface Resource {
|
||||||
|
}
|
15
src/main/java/tk/antoine/roux/domain/ResourceLister.java
Normal file
15
src/main/java/tk/antoine/roux/domain/ResourceLister.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package tk.antoine.roux.domain;
|
||||||
|
|
||||||
|
import io.vavr.collection.List;
|
||||||
|
import tk.antoine.roux.domain.model.Node;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public interface ResourceLister<T extends Resource> {
|
||||||
|
|
||||||
|
List<T> listAll();
|
||||||
|
|
||||||
|
List<Node> listAllByName(String namePattern);
|
||||||
|
|
||||||
|
List<Node> listAllByRegex(Pattern namePattern);
|
||||||
|
}
|
38
src/main/java/tk/antoine/roux/domain/model/Node.java
Normal file
38
src/main/java/tk/antoine/roux/domain/model/Node.java
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package tk.antoine.roux.domain.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
||||||
|
import tk.antoine.roux.domain.Resource;
|
||||||
|
|
||||||
|
@JsonAutoDetect(creatorVisibility = Visibility.ANY, fieldVisibility = Visibility.ANY)
|
||||||
|
public final class Node implements Resource {
|
||||||
|
private static final String NOT_DEFINED_NAME = "#NotDefined";
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private Node(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NodeBuilder builder() {
|
||||||
|
return new NodeBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NodeBuilder {
|
||||||
|
private String name = NOT_DEFINED_NAME;
|
||||||
|
|
||||||
|
public NodeBuilder withName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node build() {
|
||||||
|
return new Node(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package tk.antoine.roux.domain.usecases;
|
||||||
|
|
||||||
|
public record ByLabelCriteria() implements CriteriaCommand {
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package tk.antoine.roux.domain.usecases;
|
||||||
|
|
||||||
|
public record ByNameCriteria(String value) implements CriteriaCommand {
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package tk.antoine.roux.domain.usecases;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public record ByRegexCriteria(Pattern regex) implements CriteriaCommand {
|
||||||
|
}
|
14
src/main/java/tk/antoine/roux/domain/usecases/Command.java
Normal file
14
src/main/java/tk/antoine/roux/domain/usecases/Command.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package tk.antoine.roux.domain.usecases;
|
||||||
|
|
||||||
|
import tk.antoine.roux.domain.usecases.Command.EmptyCommand;
|
||||||
|
import tk.antoine.roux.domain.usecases.operation.UseCase;
|
||||||
|
|
||||||
|
public sealed interface Command extends Parameter permits EmptyCommand, CriteriaCommand {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link UseCase} {@link Command} without parameter
|
||||||
|
*/
|
||||||
|
record EmptyCommand() implements Command {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package tk.antoine.roux.domain.usecases;
|
||||||
|
|
||||||
|
public sealed interface CriteriaCommand extends Command permits ByLabelCriteria, ByNameCriteria, ByRegexCriteria {
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package tk.antoine.roux.domain.usecases;
|
||||||
|
|
||||||
|
import tk.antoine.roux.infrastructure.in.Criteria;
|
||||||
|
|
||||||
|
public class InvalidCriteriaException extends Exception {
|
||||||
|
public InvalidCriteriaException(String reason, Criteria criteria) {
|
||||||
|
super("Invalid criteria because " + reason + ", received criteria " + criteria);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package tk.antoine.roux.domain.usecases;
|
||||||
|
|
||||||
|
public sealed interface Parameter permits Command, Queries {
|
||||||
|
}
|
14
src/main/java/tk/antoine/roux/domain/usecases/Queries.java
Normal file
14
src/main/java/tk/antoine/roux/domain/usecases/Queries.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package tk.antoine.roux.domain.usecases;
|
||||||
|
|
||||||
|
import tk.antoine.roux.domain.usecases.Queries.EmptyQueries;
|
||||||
|
import tk.antoine.roux.domain.usecases.operation.UseCase;
|
||||||
|
|
||||||
|
public sealed interface Queries extends Parameter permits EmptyQueries {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link UseCase} {@link Queries} without parameter
|
||||||
|
*/
|
||||||
|
record EmptyQueries() implements Queries {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package tk.antoine.roux.domain.usecases.operation;
|
||||||
|
|
||||||
|
import io.vavr.collection.List;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import tk.antoine.roux.domain.model.Node;
|
||||||
|
import tk.antoine.roux.domain.usecases.ByLabelCriteria;
|
||||||
|
import tk.antoine.roux.domain.usecases.ByNameCriteria;
|
||||||
|
import tk.antoine.roux.domain.usecases.ByRegexCriteria;
|
||||||
|
import tk.antoine.roux.domain.usecases.Command;
|
||||||
|
import tk.antoine.roux.infrastructure.out.NodeLister;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public final class GetNodesUseCase implements UseCase<List<Node>, Command> {
|
||||||
|
|
||||||
|
private final NodeLister nodeLister;
|
||||||
|
|
||||||
|
public GetNodesUseCase(NodeLister nodeLister) {
|
||||||
|
this.nodeLister = nodeLister;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Node> invoke(Command command) {
|
||||||
|
return switch (command) {
|
||||||
|
case Command.EmptyCommand emptyCommand -> nodeLister.listAll();
|
||||||
|
case ByNameCriteria nameCriteria -> nodeLister.listAllByName(nameCriteria.value());
|
||||||
|
case ByRegexCriteria regexCriteria -> nodeLister.listAllByRegex(regexCriteria.regex());
|
||||||
|
case ByLabelCriteria byLabelCriteria -> nodeLister.listAll();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package tk.antoine.roux.domain.usecases.operation;
|
||||||
|
|
||||||
|
import tk.antoine.roux.domain.usecases.Parameter;
|
||||||
|
|
||||||
|
public sealed interface UseCase<T, C extends Parameter> permits GetNodesUseCase {
|
||||||
|
|
||||||
|
T invoke(C command);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package tk.antoine.roux.infrastructure;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
@ConfigurationProperties("backoffice.configuration")
|
||||||
|
public record BackofficeProperties(String apiPrefix, Kubernetes kubernetes) {
|
||||||
|
|
||||||
|
public record Kubernetes(Api api) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Api(String kubeconfig) {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package tk.antoine.roux.infrastructure;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationPropertiesScan
|
||||||
|
public class SpringConfiguration {
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package tk.antoine.roux.infrastructure.in;
|
||||||
|
|
||||||
|
import io.vavr.CheckedFunction1;
|
||||||
|
import io.vavr.control.Either;
|
||||||
|
import tk.antoine.roux.domain.usecases.ByLabelCriteria;
|
||||||
|
import tk.antoine.roux.domain.usecases.ByNameCriteria;
|
||||||
|
import tk.antoine.roux.domain.usecases.ByRegexCriteria;
|
||||||
|
import tk.antoine.roux.domain.usecases.CriteriaCommand;
|
||||||
|
import tk.antoine.roux.domain.usecases.InvalidCriteriaException;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public record Criteria(String rawCriteria) {
|
||||||
|
|
||||||
|
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
|
||||||
|
private static final String RESOURCE_NAME = "^[a-z0-9][a-z0-9-.]{0,252}[a-z0-9]?$";
|
||||||
|
private static final String LABEL_RESOURCE = "^[a-z0-9][a-z0-9-./]{0,252}[a-z0-9]?=[a-z0-9][a-z0-9-.]{0,252}[a-z0-9]?$";
|
||||||
|
|
||||||
|
public Either<Exception, CriteriaCommand> toCommand() {
|
||||||
|
String trimmedRawCriteria = rawCriteria.trim();
|
||||||
|
final Either<Exception, CriteriaCommand> resultingCriteria;
|
||||||
|
|
||||||
|
if (trimmedRawCriteria.matches(RESOURCE_NAME)) {
|
||||||
|
resultingCriteria = Either.right(new ByNameCriteria(trimmedRawCriteria));
|
||||||
|
} else if (trimmedRawCriteria.matches(LABEL_RESOURCE)) {
|
||||||
|
resultingCriteria = Either.right(new ByLabelCriteria());
|
||||||
|
} else if (isAValidRegex(trimmedRawCriteria)) {
|
||||||
|
resultingCriteria = Either.right(new ByRegexCriteria(Pattern.compile(trimmedRawCriteria)));
|
||||||
|
} else {
|
||||||
|
resultingCriteria = Either.left(new InvalidCriteriaException("not a word", this));
|
||||||
|
}
|
||||||
|
return resultingCriteria;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAValidRegex(String value) {
|
||||||
|
return CheckedFunction1.<String, Pattern>liftTry(Pattern::compile)
|
||||||
|
.apply(value)
|
||||||
|
.map(pattern -> true)
|
||||||
|
.getOrElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package tk.antoine.roux.infrastructure.in;
|
||||||
|
|
||||||
|
import io.vavr.control.Either;
|
||||||
|
import io.vavr.control.Option;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ProblemDetail;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tk.antoine.roux.domain.usecases.Command;
|
||||||
|
import tk.antoine.roux.domain.usecases.Command.EmptyCommand;
|
||||||
|
import tk.antoine.roux.domain.usecases.operation.GetNodesUseCase;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("${backoffice.configuration.api-prefix}/v1")
|
||||||
|
public class NodeController {
|
||||||
|
|
||||||
|
private static final EmptyCommand EMPTY_COMMAND = new EmptyCommand();
|
||||||
|
private static final String LIST_NODES_ERROR_MESSAGE = "List nodes failed";
|
||||||
|
private final GetNodesUseCase getNodesUseCase;
|
||||||
|
|
||||||
|
public NodeController(GetNodesUseCase getNodesUseCase) {
|
||||||
|
this.getNodesUseCase = getNodesUseCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/nodes")
|
||||||
|
ResponseEntity<?> listNode(@RequestParam(required = false, value = "criteria") Optional<Criteria> optionalCriteria) {
|
||||||
|
return Option.ofOptional(optionalCriteria)
|
||||||
|
.map(criteria -> criteria.toCommand().map(Command.class::cast))
|
||||||
|
.toEither(EMPTY_COMMAND)
|
||||||
|
.getOrElseGet(Either::right)
|
||||||
|
.map(getNodesUseCase::invoke)
|
||||||
|
.mapLeft(NodeController::exceptionToProblemDetail)
|
||||||
|
.fold(problemDetail -> ResponseEntity.of(problemDetail).build(), ResponseEntity::ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static ProblemDetail exceptionToProblemDetail(Exception e) {
|
||||||
|
ProblemDetail problemDetail = ProblemDetail
|
||||||
|
.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
|
||||||
|
problemDetail.setTitle(LIST_NODES_ERROR_MESSAGE);
|
||||||
|
return problemDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package tk.antoine.roux.infrastructure.in;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.Module;
|
||||||
|
import io.vavr.jackson.datatype.VavrModule;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Module vavrModule() {
|
||||||
|
return new VavrModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package tk.antoine.roux.infrastructure.out;
|
||||||
|
|
||||||
|
import io.kubernetes.client.openapi.ApiClient;
|
||||||
|
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||||
|
import io.kubernetes.client.util.ClientBuilder;
|
||||||
|
import io.kubernetes.client.util.KubeConfig;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import tk.antoine.roux.infrastructure.BackofficeProperties;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
public class KubernetesClientConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Profile("!dev")
|
||||||
|
public ApiClient prodClient() throws IOException {
|
||||||
|
return ClientBuilder.cluster().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Profile("dev")
|
||||||
|
public ApiClient devClient(BackofficeProperties backofficeProperties) throws IOException {
|
||||||
|
String kubeConfigPath = backofficeProperties.kubernetes().api().kubeconfig();
|
||||||
|
File kubeConfigFile = new File(kubeConfigPath);
|
||||||
|
try (BufferedReader kubeConfigReader =
|
||||||
|
new BufferedReader(
|
||||||
|
new InputStreamReader(
|
||||||
|
new FileInputStream(kubeConfigFile), StandardCharsets.UTF_8))) {
|
||||||
|
|
||||||
|
KubeConfig kubeConfig = KubeConfig.loadKubeConfig(kubeConfigReader);
|
||||||
|
kubeConfig.setFile(kubeConfigFile);
|
||||||
|
|
||||||
|
return ClientBuilder.kubeconfig(kubeConfig).setPingInterval(Duration.ofSeconds(2)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CoreV1Api getCoreApi(ApiClient apiClient) {
|
||||||
|
return new CoreV1Api(apiClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package tk.antoine.roux.infrastructure.out;
|
||||||
|
|
||||||
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||||
|
import io.kubernetes.client.openapi.models.V1Node;
|
||||||
|
import io.kubernetes.client.openapi.models.V1NodeList;
|
||||||
|
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||||
|
import io.vavr.collection.List;
|
||||||
|
import io.vavr.control.Option;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import tk.antoine.roux.domain.model.Node;
|
||||||
|
import tk.antoine.roux.domain.model.Node.NodeBuilder;
|
||||||
|
import tk.antoine.roux.domain.ResourceLister;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class NodeLister implements ResourceLister<Node> {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
|
||||||
|
|
||||||
|
private final CoreV1Api coreV1Api;
|
||||||
|
|
||||||
|
public NodeLister(CoreV1Api coreV1Api) {
|
||||||
|
this.coreV1Api = coreV1Api;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Node> listAll() {
|
||||||
|
try {
|
||||||
|
V1NodeList v1NodeList = coreV1Api.listNode(
|
||||||
|
null, null, null, null, null, null,
|
||||||
|
null, null, null, null
|
||||||
|
);
|
||||||
|
return List.ofAll(v1NodeList.getItems())
|
||||||
|
.map(NodeLister::buildNodeFromV1Node);
|
||||||
|
} catch (ApiException exception) {
|
||||||
|
log.warn("Node listing failed", exception);
|
||||||
|
return List.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Node> listAllByName(String namePrefix) {
|
||||||
|
try {
|
||||||
|
V1NodeList v1NodeList = coreV1Api.listNode(
|
||||||
|
null, null, null, null, null, null,
|
||||||
|
null, null, null, null
|
||||||
|
);
|
||||||
|
return List.ofAll(v1NodeList.getItems())
|
||||||
|
.map(NodeLister::buildNodeFromV1Node)
|
||||||
|
.filter(node -> node.name().startsWith(namePrefix));
|
||||||
|
} catch (ApiException exception) {
|
||||||
|
log.warn("Node listing failed", exception);
|
||||||
|
return List.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Node> listAllByRegex(Pattern regex) {
|
||||||
|
try {
|
||||||
|
V1NodeList v1NodeList = coreV1Api.listNode(
|
||||||
|
null, null, null, null, null, null,
|
||||||
|
null, null, null, null
|
||||||
|
);
|
||||||
|
return List.ofAll(v1NodeList.getItems())
|
||||||
|
.map(NodeLister::buildNodeFromV1Node)
|
||||||
|
.filter(node -> regex.matcher(node.name()).find());
|
||||||
|
} catch (ApiException exception) {
|
||||||
|
log.warn("Node listing failed", exception);
|
||||||
|
return List.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Node buildNodeFromV1Node(V1Node v1Node) {
|
||||||
|
NodeBuilder nodeBuilder = Node.builder();
|
||||||
|
|
||||||
|
Option.of(v1Node.getMetadata())
|
||||||
|
.map(V1ObjectMeta::getName)
|
||||||
|
.map(nodeBuilder::withName);
|
||||||
|
|
||||||
|
return nodeBuilder.build();
|
||||||
|
}
|
||||||
|
}
|
14
src/test/java/tk/antoine/roux/MainTest.java
Normal file
14
src/test/java/tk/antoine/roux/MainTest.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package tk.antoine.roux;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
@SpringBootTestWithProfile(activeProfiles = "dev")
|
||||||
|
class MainTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testApplicationContextLoading() {
|
||||||
|
assertTrue(true);
|
||||||
|
}
|
||||||
|
}
|
21
src/test/java/tk/antoine/roux/SpringBootTestWithProfile.java
Normal file
21
src/test/java/tk/antoine/roux/SpringBootTestWithProfile.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package tk.antoine.roux;
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.core.annotation.AliasFor;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface SpringBootTestWithProfile {
|
||||||
|
|
||||||
|
@AliasFor(annotation = ActiveProfiles.class, attribute = "profiles")
|
||||||
|
String[] activeProfiles() default {};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
|||||||
|
package tk.antoine.roux.infrastructure.in;
|
||||||
|
|
||||||
|
import io.vavr.control.Either;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import tk.antoine.roux.domain.usecases.ByLabelCriteria;
|
||||||
|
import tk.antoine.roux.domain.usecases.ByNameCriteria;
|
||||||
|
import tk.antoine.roux.domain.usecases.ByRegexCriteria;
|
||||||
|
import tk.antoine.roux.domain.usecases.CriteriaCommand;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class CriteriaTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("match a literal name")
|
||||||
|
void caseOne() {
|
||||||
|
// Given
|
||||||
|
Criteria worker = new Criteria("worker");
|
||||||
|
|
||||||
|
// When
|
||||||
|
Either<Exception, CriteriaCommand> command = worker.toCommand();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(command.isRight());
|
||||||
|
assertInstanceOf(ByNameCriteria.class, command.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("match literal name with dash")
|
||||||
|
void caseTwo() {
|
||||||
|
// Given
|
||||||
|
Criteria worker = new Criteria("worker-2");
|
||||||
|
|
||||||
|
// When
|
||||||
|
Either<Exception, CriteriaCommand> command = worker.toCommand();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(command.isRight());
|
||||||
|
assertInstanceOf(ByNameCriteria.class, command.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("match regex")
|
||||||
|
void caseThree() {
|
||||||
|
// Given
|
||||||
|
Criteria worker = new Criteria("worker*");
|
||||||
|
|
||||||
|
// When
|
||||||
|
Either<Exception, CriteriaCommand> command = worker.toCommand();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(command.isRight());
|
||||||
|
assertInstanceOf(ByRegexCriteria.class, command.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("match label")
|
||||||
|
void caseFour() {
|
||||||
|
// Given
|
||||||
|
Criteria worker = new Criteria("node=worker");
|
||||||
|
|
||||||
|
// When
|
||||||
|
Either<Exception, CriteriaCommand> command = worker.toCommand();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(command.isRight());
|
||||||
|
assertInstanceOf(ByLabelCriteria.class, command.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("match label with slash and dot")
|
||||||
|
void caseFive() {
|
||||||
|
// Given
|
||||||
|
Criteria worker = new Criteria("kubernetes.io/hostname=worker-2");
|
||||||
|
|
||||||
|
// When
|
||||||
|
Either<Exception, CriteriaCommand> command = worker.toCommand();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(command.isRight());
|
||||||
|
assertInstanceOf(ByLabelCriteria.class, command.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("match regex with wildcard")
|
||||||
|
void caseSix() {
|
||||||
|
// Given
|
||||||
|
Criteria worker = new Criteria("worker-*");
|
||||||
|
|
||||||
|
// When
|
||||||
|
Either<Exception, CriteriaCommand> command = worker.toCommand();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(command.isRight());
|
||||||
|
assertInstanceOf(ByRegexCriteria.class, command.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("don't match")
|
||||||
|
void errorCase() {
|
||||||
|
// Given
|
||||||
|
Criteria worker = new Criteria("worker [ltl");
|
||||||
|
|
||||||
|
// When
|
||||||
|
Either<Exception, CriteriaCommand> command = worker.toCommand();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(command.isLeft());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
package tk.antoine.roux.infrastructure.in;
|
||||||
|
|
||||||
|
import io.vavr.collection.List;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.ResultActions;
|
||||||
|
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||||
|
import tk.antoine.roux.domain.model.Node;
|
||||||
|
import tk.antoine.roux.domain.usecases.Command;
|
||||||
|
import tk.antoine.roux.domain.usecases.Command.EmptyCommand;
|
||||||
|
import tk.antoine.roux.domain.usecases.CriteriaCommand;
|
||||||
|
import tk.antoine.roux.domain.usecases.operation.GetNodesUseCase;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
@WebMvcTest(NodeController.class)
|
||||||
|
@Import(WebConfiguration.class)
|
||||||
|
class NodeControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Value("${backoffice.configuration.api-prefix}")
|
||||||
|
private String backendPrefix;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private GetNodesUseCase getNodesUseCase;
|
||||||
|
|
||||||
|
private static final EmptyCommand EMPTY_COMMAND = new EmptyCommand();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listNode() throws Exception {
|
||||||
|
// Given
|
||||||
|
MockHttpServletRequestBuilder request = get(backendPrefix + "/v1/nodes");
|
||||||
|
Mockito.when(getNodesUseCase.invoke(any(Command.class)))
|
||||||
|
.thenReturn(List.of(Node.builder().build(), Node.builder().build(), Node.builder().build()));
|
||||||
|
|
||||||
|
// When
|
||||||
|
ResultActions requestResult = mockMvc.perform(request);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
requestResult
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$").isArray())
|
||||||
|
.andExpect(jsonPath("$[0]").exists())
|
||||||
|
.andExpect(jsonPath("$[1]").exists())
|
||||||
|
.andExpect(jsonPath("$[2]").exists())
|
||||||
|
;
|
||||||
|
Mockito.verify(getNodesUseCase).invoke(eq(EMPTY_COMMAND));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listNodeWithCriteria() throws Exception {
|
||||||
|
// Given
|
||||||
|
MockHttpServletRequestBuilder request = get(backendPrefix + "/v1/nodes")
|
||||||
|
.param("criteria", "worker");
|
||||||
|
|
||||||
|
Mockito.when(getNodesUseCase.invoke(any(Command.class)))
|
||||||
|
.thenReturn(List.of(Node.builder().build(), Node.builder().build(), Node.builder().build()));
|
||||||
|
|
||||||
|
// When
|
||||||
|
ResultActions requestResult = mockMvc.perform(request);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
requestResult
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$").isArray());
|
||||||
|
|
||||||
|
Mockito.verify(getNodesUseCase).invoke(isA(CriteriaCommand.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Call nodes endpoint with criteria causing bad request")
|
||||||
|
void listNodeWithInvalidCriteria() throws Exception {
|
||||||
|
// Given
|
||||||
|
MockHttpServletRequestBuilder request = get(backendPrefix + "/v1/nodes")
|
||||||
|
.param("criteria", "worker [ltl");
|
||||||
|
|
||||||
|
Mockito.when(getNodesUseCase.invoke(any(Command.class)))
|
||||||
|
.thenReturn(List.of(Node.builder().build(), Node.builder().build(), Node.builder().build()));
|
||||||
|
|
||||||
|
// When
|
||||||
|
ResultActions requestResult = mockMvc.perform(request);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
requestResult
|
||||||
|
.andExpect(status().isBadRequest())
|
||||||
|
.andExpect(jsonPath("$").isMap())
|
||||||
|
.andExpect(jsonPath("$.title").exists())
|
||||||
|
.andExpect(jsonPath("$.detail").exists())
|
||||||
|
.andExpect(jsonPath("$.status").value(400))
|
||||||
|
;
|
||||||
|
|
||||||
|
Mockito.verifyNoInteractions(getNodesUseCase);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
package tk.antoine.roux.infrastructure.out;
|
||||||
|
|
||||||
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||||
|
import io.kubernetes.client.openapi.models.V1Node;
|
||||||
|
import io.kubernetes.client.openapi.models.V1NodeList;
|
||||||
|
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||||
|
import io.vavr.collection.List;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import tk.antoine.roux.domain.model.Node;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class NodeListerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CoreV1Api coreV1Api;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
NodeLister nodeLister;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listAll() throws ApiException {
|
||||||
|
// Given
|
||||||
|
when(coreV1Api.listNode(any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(new V1NodeList());
|
||||||
|
|
||||||
|
// When
|
||||||
|
List<Node> nodes = nodeLister.listAll();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
Assertions.assertEquals(0, nodes.size());
|
||||||
|
verify(coreV1Api).listNode(any(), any(), any(), any(), any(), any(), any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listAllByName() throws ApiException {
|
||||||
|
// Given
|
||||||
|
V1Node node1 = new V1Node().metadata(new V1ObjectMeta().name("worker-2"));
|
||||||
|
V1Node node2 = new V1Node().metadata(new V1ObjectMeta().name("worker-5"));
|
||||||
|
V1Node node3 = new V1Node().metadata(new V1ObjectMeta().name("worker-3"));
|
||||||
|
|
||||||
|
V1NodeList nodeList = new V1NodeList()
|
||||||
|
.addItemsItem(node1)
|
||||||
|
.addItemsItem(node2)
|
||||||
|
.addItemsItem(node3);
|
||||||
|
|
||||||
|
when(coreV1Api.listNode(any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(nodeList);
|
||||||
|
|
||||||
|
// When
|
||||||
|
List<Node> nodes = nodeLister.listAllByName("worker-2");
|
||||||
|
|
||||||
|
// Then
|
||||||
|
Assertions.assertEquals(1, nodes.size());
|
||||||
|
verify(coreV1Api).listNode(any(), any(), any(), any(), any(), any(), any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listAllByRegex() throws ApiException {
|
||||||
|
// Given
|
||||||
|
V1Node node1 = new V1Node().metadata(new V1ObjectMeta().name("worker-2"));
|
||||||
|
V1Node node2 = new V1Node().metadata(new V1ObjectMeta().name("worker-5"));
|
||||||
|
V1Node node3 = new V1Node().metadata(new V1ObjectMeta().name("worker-3"));
|
||||||
|
|
||||||
|
V1NodeList nodeList = new V1NodeList()
|
||||||
|
.addItemsItem(node1)
|
||||||
|
.addItemsItem(node2)
|
||||||
|
.addItemsItem(node3);
|
||||||
|
|
||||||
|
when(coreV1Api.listNode(any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(nodeList);
|
||||||
|
|
||||||
|
// When
|
||||||
|
List<Node> nodes = nodeLister.listAllByRegex(Pattern.compile("worker-[25]"));
|
||||||
|
|
||||||
|
// Then
|
||||||
|
Assertions.assertEquals(2, nodes.size());
|
||||||
|
verify(coreV1Api).listNode(any(), any(), any(), any(), any(), any(), any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user