Skip to content

Commit c319310

Browse files
[ACC-2066] Add tests for KubernetesResourceWaiter
1 parent c303c9f commit c319310

File tree

14 files changed

+572
-13
lines changed

14 files changed

+572
-13
lines changed

contentgrid-junit-jupiter-k8s/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ dependencies {
3434
testImplementation 'org.assertj:assertj-core'
3535
testImplementation 'org.mockito:mockito-core'
3636
testImplementation 'org.skyscreamer:jsonassert'
37+
testImplementation 'ch.qos.logback:logback-classic'
3738
testImplementation files(tasks.named("testResourcesJar"))
3839

3940
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
40-
testRuntimeOnly 'org.slf4j:slf4j-simple'
4141
}
4242

4343

contentgrid-junit-jupiter-k8s/src/main/java/com/contentgrid/junit/jupiter/k8s/K8sTestUtils.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import com.contentgrid.junit.jupiter.k8s.wait.KubernetesResourceWaiter;
66
import com.contentgrid.junit.jupiter.k8s.wait.ResourceMatcher;
7+
import io.fabric8.kubernetes.api.model.apps.ReplicaSet;
78
import io.fabric8.kubernetes.client.KubernetesClient;
89
import java.util.List;
910
import java.util.function.UnaryOperator;
@@ -50,4 +51,16 @@ public static void waitUntilStatefulSetsReady(int timeout, List<String> stateful
5051
.statefulSets(ResourceMatcher.named(statefulSets.toArray(String[]::new)).inNamespace(namespace))
5152
.await(createAwait(timeout));
5253
}
54+
55+
public static void waitUntilReplicaSetsReady(int timeout, List<String> replicaSets, KubernetesClient kubernetesClient) {
56+
new KubernetesResourceWaiter(kubernetesClient)
57+
.include(ReplicaSet.class, ResourceMatcher.named(replicaSets.toArray(String[]::new)))
58+
.await(createAwait(timeout));
59+
}
60+
61+
public static void waitUntilReplicaSetsReady(int timeout, List<String> replicaSets, KubernetesClient kubernetesClient, String namespace) {
62+
new KubernetesResourceWaiter(kubernetesClient)
63+
.include(ReplicaSet.class, ResourceMatcher.named(replicaSets.toArray(String[]::new)).inNamespace(namespace))
64+
.await(createAwait(timeout));
65+
}
5366
}

contentgrid-junit-jupiter-k8s/src/main/java/com/contentgrid/junit/jupiter/k8s/wait/KubernetesResourceWaiter.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ public class KubernetesResourceWaiter implements AutoCloseable {
6565
Deployment.class, client -> client.apps().deployments(),
6666
StatefulSet.class, client -> client.apps().statefulSets(),
6767
DaemonSet.class, client -> client.apps().daemonSets(),
68-
Job.class, client -> client.batch().v1().jobs()
68+
Job.class, client -> client.batch().v1().jobs(),
69+
ReplicaSet.class, client -> client.apps().replicaSets()
6970
);
7071

7172
static {
@@ -246,7 +247,7 @@ public void close() {
246247
* Custom awaitility {@link ConditionEvaluationListener} that prints events and logs for resources that are not ready after the timeout
247248
*/
248249
@RequiredArgsConstructor
249-
private static class ConditionEvaluationListenerImpl implements ConditionEvaluationListener<List<? extends AwaitableResource>> {
250+
private class ConditionEvaluationListenerImpl implements ConditionEvaluationListener<List<? extends AwaitableResource>> {
250251
private static final ConditionEvaluationListener<Object> LOGGER = new ConditionEvaluationLogger(log::info, TimeUnit.SECONDS);
251252
private final AtomicReference<List<? extends AwaitableResource>> lastFailingCondition = new AtomicReference<>();
252253

@@ -262,7 +263,7 @@ public void conditionEvaluated(EvaluatedCondition<List<? extends AwaitableResour
262263

263264
@Override
264265
public void beforeEvaluation(StartEvaluationEvent<List<? extends AwaitableResource>> startEvaluationEvent) {
265-
LOGGER.beforeEvaluation((StartEvaluationEvent) startEvaluationEvent);
266+
log.info("Waiting for <{}>", resources().toList());
266267
}
267268

268269
@Override

contentgrid-junit-jupiter-k8s/src/main/java/com/contentgrid/junit/jupiter/k8s/wait/ResourceMatcher.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ public interface ResourceMatcher<T extends HasMetadata> extends Predicate<T> {
1919
* @param <T> The type of the resource
2020
*/
2121
static <T extends HasMetadata> ResourceMatcher<T> labelled(@NonNull Map<String, String> labels) {
22-
return o -> o.getMetadata().getLabels().entrySet()
23-
.stream()
24-
.allMatch(entry -> !labels.containsKey(entry.getKey()) || Objects.equals(entry.getValue(), labels.get(entry.getKey())));
22+
return o -> labels.entrySet().stream()
23+
.allMatch(entry -> Objects.equals(o.getMetadata().getLabels().get(entry.getKey()), entry.getValue()));
2524
}
2625

2726
/**
@@ -30,9 +29,8 @@ static <T extends HasMetadata> ResourceMatcher<T> labelled(@NonNull Map<String,
3029
* @param <T> The type of the resource
3130
*/
3231
static <T extends HasMetadata> ResourceMatcher<T> annotated(@NonNull Map<String, String> annotations) {
33-
return o -> o.getMetadata().getAnnotations().entrySet()
34-
.stream()
35-
.allMatch(entry -> !annotations.containsKey(entry.getKey()) || Objects.equals(entry.getValue(), annotations.get(entry.getKey())));
32+
return o -> annotations.entrySet().stream()
33+
.allMatch(entry -> Objects.equals(o.getMetadata().getAnnotations().get(entry.getKey()), entry.getValue()));
3634
}
3735

3836
/**

contentgrid-junit-jupiter-k8s/src/main/java/com/contentgrid/junit/jupiter/k8s/wait/resource/AwaitableResourceFactory.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,4 @@ public AwaitableResource instantiate(KubernetesClient client, KubernetesResource
1616
return factories.get(resource.getClass()).instantiate(client, this, resource);
1717
}
1818

19-
public boolean supports(KubernetesResource resource) {
20-
return factories.containsKey(resource.getClass());
21-
}
2219
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package com.contentgrid.junit.jupiter.k8s.wait;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatCode;
5+
6+
import ch.qos.logback.classic.Level;
7+
import ch.qos.logback.classic.Logger;
8+
import ch.qos.logback.classic.spi.ILoggingEvent;
9+
import ch.qos.logback.core.AppenderBase;
10+
import com.contentgrid.helm.HelmInstallCommand.InstallOption;
11+
import com.contentgrid.junit.jupiter.helm.HelmChart;
12+
import com.contentgrid.junit.jupiter.helm.HelmChartHandle;
13+
import com.contentgrid.junit.jupiter.helm.HelmClient;
14+
import com.contentgrid.junit.jupiter.k8s.KubernetesTestCluster;
15+
import io.fabric8.kubernetes.api.model.Pod;
16+
import io.fabric8.kubernetes.api.model.apps.DaemonSet;
17+
import io.fabric8.kubernetes.api.model.apps.Deployment;
18+
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
19+
import io.fabric8.kubernetes.api.model.batch.v1.Job;
20+
import io.fabric8.kubernetes.client.KubernetesClient;
21+
import java.util.Collections;
22+
import java.util.LinkedList;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.concurrent.TimeUnit;
26+
import org.junit.jupiter.api.Test;
27+
import org.slf4j.LoggerFactory;
28+
29+
@KubernetesTestCluster
30+
@HelmClient
31+
class KubernetesResourceWaiterTest {
32+
33+
static KubernetesClient kubernetesClient;
34+
35+
@HelmChart(chart = "classpath:chart")
36+
HelmChartHandle resourceWaiterChart;
37+
38+
@Test
39+
void waitForChart() {
40+
var installResult = resourceWaiterChart.install();
41+
42+
var waiter = new KubernetesResourceWaiter(kubernetesClient)
43+
.include(installResult)
44+
.exclude(Deployment.class, ResourceMatcher.named("broken-deploy"));
45+
46+
assertThat(waiter.resources())
47+
.satisfiesExactlyInAnyOrder(
48+
deployment -> {
49+
assertThat(deployment.getApiType()).isEqualTo(Deployment.class);
50+
assertThat(deployment.getObjectReference().getNamespace()).isEqualTo("kube-system");
51+
assertThat(deployment.getObjectReference().getName()).isEqualTo("test-nginx-deploy");
52+
},
53+
daemonSet -> {
54+
assertThat(daemonSet.getApiType()).isEqualTo(DaemonSet.class);
55+
assertThat(daemonSet.getObjectReference().getNamespace()).isEqualTo(kubernetesClient.getNamespace());
56+
assertThat(daemonSet.getObjectReference().getName()).isEqualTo("test-nginx-daemonset");
57+
},
58+
statefulSet -> {
59+
assertThat(statefulSet.getApiType()).isEqualTo(StatefulSet.class);
60+
assertThat(statefulSet.getObjectReference().getNamespace()).isEqualTo(kubernetesClient.getNamespace());
61+
assertThat(statefulSet.getObjectReference().getName()).isEqualTo("test-nginx-sts");
62+
},
63+
job -> {
64+
assertThat(job.getApiType()).isEqualTo(Job.class);
65+
assertThat(job.getObjectReference().getNamespace()).isEqualTo(kubernetesClient.getNamespace());
66+
assertThat(job.getObjectReference().getName()).isEqualTo("test-job");
67+
}
68+
)
69+
;
70+
71+
waiter.await(await -> await.atMost(1, TimeUnit.MINUTES).pollInterval(5, TimeUnit.SECONDS));
72+
73+
// Check events and logs from a job
74+
assertThat(waiter.resources())
75+
.filteredOn(resource -> resource.getObjectReference().getName().equals("test-job"))
76+
.singleElement()
77+
.satisfies(job -> {
78+
assertThat(job.events())
79+
// Event of the job itself
80+
.anySatisfy(event -> {
81+
assertThat(event.resource().getApiType()).isEqualTo(Job.class);
82+
assertThat(event.type()).isEqualTo("Normal");
83+
assertThat(event.reason()).isEqualTo("SuccessfulCreate");
84+
})
85+
// Event of the pod started by the job
86+
.anySatisfy(event -> {
87+
assertThat(event.resource().getApiType()).isEqualTo(Pod.class);
88+
assertThat(event.type()).isEqualTo("Normal");
89+
assertThat(event.reason()).isEqualTo("Started");
90+
})
91+
;
92+
assertThat(job.logs()).satisfiesExactly(line -> {
93+
assertThat(line.resource().getApiType()).isEqualTo(Pod.class);
94+
assertThat(line.container()).isEqualTo("hello");
95+
assertThat(line.line()).isEqualTo("Hello from this job");
96+
}, line -> {
97+
assertThat(line.resource().getApiType()).isEqualTo(Pod.class);
98+
assertThat(line.container()).isEqualTo("goodbye");
99+
assertThat(line.line()).isEqualTo("Bye from this job");
100+
});
101+
});
102+
103+
waiter.close();
104+
}
105+
106+
@Test
107+
void waitForBrokenDeploy() {
108+
var installResult = resourceWaiterChart.install();
109+
110+
var waiter = new KubernetesResourceWaiter(kubernetesClient)
111+
.include(Deployment.class, ResourceMatcher.named("broken-deploy"));
112+
113+
Logger waiterLogger = (Logger) LoggerFactory.getLogger(KubernetesResourceWaiter.class);
114+
var testAppender = new TestListAppender();
115+
testAppender.start();
116+
var prevLevel = waiterLogger.getLevel();
117+
waiterLogger.setLevel(Level.INFO);
118+
waiterLogger.addAppender(testAppender);
119+
120+
121+
try {
122+
assertThatCode(() -> {
123+
waiter.await(await -> await.atMost(30, TimeUnit.SECONDS));
124+
}).hasMessageContaining("Deployment " + kubernetesClient.getNamespace() + "/broken-deploy");
125+
} finally {
126+
waiterLogger.setLevel(prevLevel);
127+
waiterLogger.detachAppender(testAppender);
128+
testAppender.stop();
129+
}
130+
131+
assertThat(testAppender.getEvents())
132+
.extracting(ILoggingEvent::toString)
133+
.anySatisfy(message -> assertThat(message).contains("[ERROR] 1 resources are not ready"))
134+
.anySatisfy(message -> assertThat(message).contains("[ERROR] - Deployment " + kubernetesClient.getNamespace() + "/broken-deploy"))
135+
// Event about failing readiness probe
136+
.anySatisfy(message -> assertThat(message).contains("Warning Unhealthy Readiness probe failed: HTTP probe failed with statuscode: 404"))
137+
// Log of nginx access log message (from readiness probe)
138+
.anySatisfy(message -> assertThat(message).contains("\"GET /ready HTTP/1.1\" 404"));
139+
;
140+
141+
waiter.close();
142+
}
143+
144+
@Test
145+
void matchOnLabels() {
146+
resourceWaiterChart.install(InstallOption.namespace("kube-system"));
147+
148+
try(var waiter = new KubernetesResourceWaiter(kubernetesClient)
149+
.include(Deployment.class, ResourceMatcher.labelled(Map.of(
150+
"type", "webserver"
151+
)).inNamespace("kube-system"))) {
152+
153+
assertThat(waiter.resources()).hasSize(2); // Both our deployments match label type=webserver
154+
}
155+
156+
try(var waiter = new KubernetesResourceWaiter(kubernetesClient)
157+
.include(Deployment.class, ResourceMatcher.labelled(Map.of(
158+
"app", "nginx"
159+
)).inNamespace("kube-system"))) {
160+
161+
assertThat(waiter.resources()).hasSize(1); // Only the nginx deployment matches
162+
}
163+
164+
}
165+
166+
/**
167+
* A simple Logback appender that stores logging events in a list for testing.
168+
*/
169+
public static class TestListAppender extends AppenderBase<ILoggingEvent> {
170+
private final List<ILoggingEvent> events = Collections.synchronizedList(new LinkedList<>());
171+
172+
@Override
173+
protected void append(ILoggingEvent eventObject) {
174+
events.add(eventObject);
175+
}
176+
177+
public List<ILoggingEvent> getEvents() {
178+
// Return a new list to prevent ConcurrentModificationException if the list is modified while iterating
179+
return new LinkedList<>(events);
180+
}
181+
182+
public void clearEvents() {
183+
events.clear();
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)