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
|
||||
# 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>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<java.version>20</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
|
||||
<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>
|
||||
|
||||
<dependencyManagement>
|
||||
@ -30,6 +32,16 @@
|
||||
<artifactId>vavr</artifactId>
|
||||
<version>${vavr.version}</version>
|
||||
</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>
|
||||
</dependencyManagement>
|
||||
|
||||
@ -38,10 +50,27 @@
|
||||
<groupId>io.vavr</groupId>
|
||||
<artifactId>vavr</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vavr</groupId>
|
||||
<artifactId>vavr-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.kubernetes</groupId>
|
||||
<artifactId>client-java</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- dev -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
@ -49,4 +78,18 @@
|
||||
</dependency>
|
||||
</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>
|
||||
|
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