feature: backend list nodes by name, regex

This commit is contained in:
RouxAntoine 2023-07-24 08:23:07 +02:00
parent 7840f8c73e
commit 2983242dde
Signed by: antoine
GPG Key ID: 098FB66FC0475E70
27 changed files with 812 additions and 2 deletions

View File

@ -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
View File

@ -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>

View File

@ -0,0 +1,4 @@
package tk.antoine.roux.domain;
public interface Resource {
}

View 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);
}

View 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);
}
}
}

View File

@ -0,0 +1,4 @@
package tk.antoine.roux.domain.usecases;
public record ByLabelCriteria() implements CriteriaCommand {
}

View File

@ -0,0 +1,4 @@
package tk.antoine.roux.domain.usecases;
public record ByNameCriteria(String value) implements CriteriaCommand {
}

View File

@ -0,0 +1,6 @@
package tk.antoine.roux.domain.usecases;
import java.util.regex.Pattern;
public record ByRegexCriteria(Pattern regex) implements CriteriaCommand {
}

View 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 {
}
}

View File

@ -0,0 +1,4 @@
package tk.antoine.roux.domain.usecases;
public sealed interface CriteriaCommand extends Command permits ByLabelCriteria, ByNameCriteria, ByRegexCriteria {
}

View File

@ -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);
}
}

View File

@ -0,0 +1,4 @@
package tk.antoine.roux.domain.usecases;
public sealed interface Parameter permits Command, Queries {
}

View 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 {
}
}

View File

@ -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();
};
}
}

View File

@ -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);
}

View File

@ -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) {
}
}

View File

@ -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 {
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View 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);
}
}

View 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 {};
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}