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