Skip to content

Commit 5c1a672

Browse files
authored
Merge pull request kroxylicious#2015 from robobario/test-crd-declarations
Add integration test for CRD apiserver validation
2 parents 3e7673d + bf5d93d commit 5c1a672

File tree

71 files changed

+1808
-31
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+1808
-31
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright Kroxylicious Authors.
3+
*
4+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
package io.kroxylicious.kubernetes.operator;
8+
9+
import java.io.IOException;
10+
import java.io.InputStream;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.util.stream.Stream;
14+
15+
import org.assertj.core.api.Assertions;
16+
import org.junit.jupiter.api.AfterAll;
17+
import org.junit.jupiter.api.BeforeAll;
18+
import org.junit.jupiter.api.condition.EnabledIf;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.Arguments;
21+
import org.junit.jupiter.params.provider.MethodSource;
22+
23+
import com.fasterxml.jackson.core.JsonProcessingException;
24+
import com.fasterxml.jackson.databind.ObjectMapper;
25+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
26+
27+
import io.fabric8.kubernetes.api.model.HasMetadata;
28+
import io.fabric8.kubernetes.api.model.Namespace;
29+
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
30+
import io.fabric8.kubernetes.api.model.Status;
31+
import io.fabric8.kubernetes.client.KubernetesClient;
32+
import io.fabric8.kubernetes.client.KubernetesClientException;
33+
import io.fabric8.kubernetes.client.dsl.NamespaceableResource;
34+
import io.fabric8.kubernetes.client.dsl.NonDeletingOperation;
35+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
36+
37+
import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
38+
import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
39+
import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
40+
import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
41+
import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
42+
43+
import static org.assertj.core.api.Assertions.assertThat;
44+
45+
@EnabledIf(value = "io.kroxylicious.kubernetes.operator.OperatorTestUtils#isKubeClientAvailable", disabledReason = "no viable kube client available")
46+
class CustomResourceValidationIT {
47+
48+
public static final Namespace NAMESPACE = new NamespaceBuilder().withNewMetadata().withName("proxy-ns").endMetadata().build();
49+
public static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory());
50+
51+
@BeforeAll
52+
static void beforeAll() {
53+
KubernetesClient client = OperatorTestUtils.kubeClient();
54+
LocallyRunOperatorExtension.applyCrd(KafkaProtocolFilter.class, client);
55+
LocallyRunOperatorExtension.applyCrd(KafkaProxy.class, client);
56+
LocallyRunOperatorExtension.applyCrd(VirtualKafkaCluster.class, client);
57+
LocallyRunOperatorExtension.applyCrd(KafkaService.class, client);
58+
LocallyRunOperatorExtension.applyCrd(KafkaProxyIngress.class, client);
59+
client.namespaces().resource(NAMESPACE).createOr(NonDeletingOperation::update);
60+
}
61+
62+
@AfterAll
63+
static void afterAll() {
64+
try (KubernetesClient kubernetesClient = OperatorTestUtils.kubeClient()) {
65+
kubernetesClient.namespaces().resource(NAMESPACE).delete();
66+
kubernetesClient.resources(KafkaProtocolFilter.class).delete();
67+
kubernetesClient.resources(KafkaProxyIngress.class).delete();
68+
kubernetesClient.resources(KafkaProxy.class).delete();
69+
kubernetesClient.resources(VirtualKafkaCluster.class).delete();
70+
kubernetesClient.resources(KafkaService.class).delete();
71+
}
72+
}
73+
74+
public static Stream<Path> testDerivedResourceInputsValid() {
75+
return TestFiles.recursiveFilesInDirectoryForTest(DerivedResourcesTest.class, "in-*.yaml").stream();
76+
}
77+
78+
public static Stream<Arguments> testResourceInvalid() {
79+
return TestFiles.recursiveFilesInDirectoryForTest(CustomResourceValidationIT.class, "invalid-*.yaml").stream().map(p -> {
80+
try {
81+
return Arguments.argumentSet(p.toString(), YAML_MAPPER.readValue(p.toFile(), InvalidResource.class));
82+
}
83+
catch (IOException e) {
84+
throw new RuntimeException(e);
85+
}
86+
});
87+
}
88+
89+
@MethodSource
90+
@ParameterizedTest
91+
void testDerivedResourceInputsValid(Path validYaml) {
92+
try (InputStream is = Files.newInputStream(validYaml)) {
93+
NamespaceableResource<HasMetadata> resource = OperatorTestUtils.kubeClient().resource(is);
94+
Assertions.assertThatCode(resource::create).doesNotThrowAnyException();
95+
Assertions.assertThatCode(resource::delete).doesNotThrowAnyException();
96+
}
97+
catch (IOException e) {
98+
throw new RuntimeException(e);
99+
}
100+
}
101+
102+
record InvalidResource(String expectFailureMessageToContain, Object resource) {
103+
String resourceAsString() {
104+
try {
105+
return YAML_MAPPER.writeValueAsString(resource);
106+
}
107+
catch (JsonProcessingException e) {
108+
throw new RuntimeException(e);
109+
}
110+
}
111+
}
112+
113+
@MethodSource
114+
@ParameterizedTest
115+
void testResourceInvalid(InvalidResource invalidYaml) {
116+
NamespaceableResource<HasMetadata> resource = OperatorTestUtils.kubeClient().resource(invalidYaml.resourceAsString());
117+
try {
118+
Assertions.assertThatThrownBy(resource::create).isInstanceOfSatisfying(KubernetesClientException.class, e -> {
119+
Status status = e.getStatus();
120+
assertThat(status).isNotNull();
121+
assertThat(status.getCode()).isEqualTo(422);
122+
assertThat(status.getMessage()).contains(invalidYaml.expectFailureMessageToContain);
123+
});
124+
}
125+
finally {
126+
try {
127+
resource.delete();
128+
}
129+
catch (KubernetesClientException e) {
130+
// ignored, redundantly deleting in case the resource was accidentally valid
131+
}
132+
}
133+
}
134+
135+
}

kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/DerivedResourcesTest.java

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@
2222
import java.util.Map;
2323
import java.util.Set;
2424
import java.util.TreeSet;
25-
import java.util.regex.Pattern;
2625
import java.util.stream.Collectors;
2726
import java.util.stream.Stream;
28-
import java.util.stream.StreamSupport;
2927

3028
import org.assertj.core.api.Assertions;
3129
import org.junit.jupiter.api.DynamicContainer;
@@ -193,22 +191,9 @@ Stream<DynamicContainer> dependentResourcesShouldEqual() {
193191
return dependentResourcesShouldEqual(list);
194192
}
195193

196-
static List<Path> filesInDir(Path dir, Pattern pattern) {
197-
var result = new ArrayList<Path>();
198-
try (var expected = Files.newDirectoryStream(dir, path -> pattern.matcher(path.getFileName().toString()).matches())) {
199-
for (Path f : expected) {
200-
result.add(f);
201-
}
202-
}
203-
catch (IOException e) {
204-
throw new UncheckedIOException(e);
205-
}
206-
return result;
207-
}
208-
209194
Stream<DynamicContainer> dependentResourcesShouldEqual(List<DesiredFn<KafkaProxy, ?>> list) {
210-
var dir = Path.of("target", "test-classes", DerivedResourcesTest.class.getSimpleName());
211-
return filesInDir(dir, Pattern.compile(".*")).stream()
195+
List<Path> paths = TestFiles.subDirectoriesForTest(DerivedResourcesTest.class);
196+
return paths.stream()
212197
.map(testDir -> {
213198
String testCase = fileName(testDir);
214199
try {
@@ -235,17 +220,18 @@ private static List<DynamicTest> testsForDir(List<DesiredFn<KafkaProxy, ?>> depe
235220
Path testDir)
236221
throws IOException {
237222
try {
238-
var unusedFiles = childFilesMatching(testDir, "*");
223+
var unusedFiles = TestFiles.childFilesMatching(testDir, "*");
239224
String inFileName = "in-KafkaProxy.yaml";
240225
Path input = testDir.resolve(inFileName);
241226
KafkaProxy kafkaProxy = kafkaProxyFromFile(input);
242-
List<VirtualKafkaCluster> virtualKafkaClusters = resourcesFromFiles(childFilesMatching(testDir, "in-VirtualKafkaCluster-*"), VirtualKafkaCluster.class);
243-
List<KafkaService> kafkaServiceRefs = resourcesFromFiles(childFilesMatching(testDir, "in-KafkaService-*"), KafkaService.class);
227+
List<VirtualKafkaCluster> virtualKafkaClusters = resourcesFromFiles(TestFiles.childFilesMatching(testDir, "in-VirtualKafkaCluster-*"),
228+
VirtualKafkaCluster.class);
229+
List<KafkaService> kafkaServiceRefs = resourcesFromFiles(TestFiles.childFilesMatching(testDir, "in-KafkaService-*"), KafkaService.class);
244230
assertMinimalMetadata(kafkaProxy.getMetadata(), inFileName);
245-
List<KafkaProxyIngress> ingresses = kafkaProxyIngressesFromFiles(childFilesMatching(testDir, "in-KafkaProxyIngress-*"));
231+
List<KafkaProxyIngress> ingresses = kafkaProxyIngressesFromFiles(TestFiles.childFilesMatching(testDir, "in-KafkaProxyIngress-*"));
246232

247233
unusedFiles.remove(input);
248-
unusedFiles.removeAll(childFilesMatching(testDir, "in-*"));
234+
unusedFiles.removeAll(TestFiles.childFilesMatching(testDir, "in-*"));
249235

250236
Context<KafkaProxy> context;
251237
try {
@@ -323,15 +309,6 @@ private static <T> void assertSameYaml(T actualResource, T expected) throws Json
323309
}
324310
}
325311

326-
@NonNull
327-
private static HashSet<Path> childFilesMatching(
328-
Path testDir,
329-
String glob)
330-
throws IOException {
331-
return StreamSupport.stream(Files.newDirectoryStream(testDir, glob).spliterator(), false)
332-
.collect(Collectors.toCollection(HashSet::new));
333-
}
334-
335312
@NonNull
336313
private static Context<KafkaProxy> buildContext(Path testDir,
337314
List<VirtualKafkaCluster> virtualKafkaClusters,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright Kroxylicious Authors.
3+
*
4+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
package io.kroxylicious.kubernetes.operator;
8+
9+
import java.io.IOException;
10+
import java.io.UncheckedIOException;
11+
import java.nio.file.FileSystems;
12+
import java.nio.file.FileVisitResult;
13+
import java.nio.file.Files;
14+
import java.nio.file.Path;
15+
import java.nio.file.PathMatcher;
16+
import java.nio.file.SimpleFileVisitor;
17+
import java.nio.file.attribute.BasicFileAttributes;
18+
import java.util.ArrayList;
19+
import java.util.HashSet;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
import java.util.stream.StreamSupport;
23+
24+
import io.kroxylicious.proxy.tag.VisibleForTesting;
25+
26+
import edu.umd.cs.findbugs.annotations.NonNull;
27+
28+
public class TestFiles {
29+
30+
@NonNull
31+
static HashSet<Path> childFilesMatching(
32+
Path testDir,
33+
String glob)
34+
throws IOException {
35+
return StreamSupport.stream(Files.newDirectoryStream(testDir, glob).spliterator(), false)
36+
.filter(Files::isRegularFile)
37+
.collect(Collectors.toCollection(HashSet::new));
38+
}
39+
40+
static List<Path> subDirectoriesForTest(Class<?> testClazz) {
41+
var dir = testDir(testClazz);
42+
return subDirectories(dir);
43+
}
44+
45+
private static @NonNull Path testDir(Class<?> testClazz) {
46+
return Path.of("target", "test-classes", testClazz.getSimpleName());
47+
}
48+
49+
@NonNull
50+
static List<Path> subDirectories(Path dir) {
51+
var directories = new ArrayList<Path>();
52+
try (var expected = Files.newDirectoryStream(dir, Files::isDirectory)) {
53+
for (Path f : expected) {
54+
directories.add(f);
55+
}
56+
}
57+
catch (IOException e) {
58+
throw new UncheckedIOException(e);
59+
}
60+
return directories;
61+
}
62+
63+
static List<Path> recursiveFilesInDirectoryForTest(Class<?> testClazz, String filenameGlob) {
64+
Path path = testDir(testClazz);
65+
return recursiveFilesInDirectory(filenameGlob, path);
66+
}
67+
68+
@VisibleForTesting
69+
static @NonNull List<Path> recursiveFilesInDirectory(String filenameGlob, Path path) {
70+
PathMatcher globMatcher = FileSystems.getDefault().getPathMatcher("glob:**/" + filenameGlob);
71+
List<Path> files = new ArrayList<>();
72+
try {
73+
Files.walkFileTree(path, new SimpleFileVisitor<>() {
74+
@Override
75+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
76+
if (globMatcher.matches(file)) {
77+
files.add(file);
78+
}
79+
return FileVisitResult.CONTINUE;
80+
}
81+
});
82+
}
83+
catch (IOException e) {
84+
throw new RuntimeException(e);
85+
}
86+
return files;
87+
}
88+
}

0 commit comments

Comments
 (0)