Skip to content

Commit f53ce07

Browse files
authored
Add health probe for operator (kroxylicious#1966)
* Add a /livez endpoint to the operator wired up to the health of the informers. * Declare it in the operator Deployment. * Fixes kroxylicious#1574. Signed-off-by: Tom Bentley <tbentley@redhat.com>
1 parent 30c8e83 commit f53ce07

File tree

3 files changed

+84
-20
lines changed

3 files changed

+84
-20
lines changed

kroxylicious-operator/install/03.Deployment.kroxylicious-operator.yaml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,18 @@ spec:
3333
resources:
3434
limits:
3535
memory: 300M
36-
cpu: 1
36+
cpu: "1"
3737
requests:
3838
memory: 300M
39-
cpu: 1
39+
cpu: "1"
40+
ports:
41+
- containerPort: 8080
42+
name: http
43+
livenessProbe:
44+
initialDelaySeconds: 10
45+
periodSeconds: 10
46+
failureThreshold: 3
47+
timeoutSeconds: 5
48+
httpGet:
49+
port: http
50+
path: /livez

kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/OperatorMain.java

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.util.List;
1212
import java.util.Map;
1313
import java.util.Properties;
14+
import java.util.function.IntSupplier;
1415

1516
import org.slf4j.Logger;
1617
import org.slf4j.LoggerFactory;
@@ -43,6 +44,8 @@ public class OperatorMain {
4344
private static final Logger LOGGER = LoggerFactory.getLogger(OperatorMain.class);
4445
private static final String BIND_ADDRESS_VAR_NAME = "BIND_ADDRESS";
4546
private static final int DEFAULT_MANAGEMENT_PORT = 8080;
47+
static final String HTTP_PATH_LIVEZ = "/livez";
48+
static final String HTTP_PATH_METRICS = "/metrics";
4649
private final Operator operator;
4750
private final HttpServer managementServer;
4851

@@ -78,18 +81,37 @@ public static void main(String[] args) {
7881
*/
7982
void start() {
8083
operator.installShutdownHook(Duration.ofSeconds(10));
81-
var registeredController = operator.register(new ProxyReconciler(runtimeDecl()));
82-
// TODO couple the health of the registeredController to the operator's HTTP healthchecks
83-
managementServer.createContext("/", exchange -> {
84+
operator.register(new ProxyReconciler(runtimeDecl()));
85+
addHttpGetHandler("/", () -> 404);
86+
managementServer.start();
87+
operator.start();
88+
addHttpGetHandler(HTTP_PATH_LIVEZ, this::livezStatusCode);
89+
LOGGER.info("Operator started");
90+
}
91+
92+
private void addHttpGetHandler(
93+
String path,
94+
IntSupplier statusCodeSupplier) {
95+
managementServer.createContext(path, exchange -> {
8496
try (exchange) {
8597
// note while the JDK docs advise exchange.getRequestBody().transferTo(OutputStream.nullOutputStream()); we explicitly don't do that!
8698
// As a denial-of-service protection we don't expect anything other than GET requests so there should be no input to read.
87-
exchange.sendResponseHeaders(404, -1);
99+
exchange.sendResponseHeaders(statusCodeSupplier.getAsInt(), -1);
88100
}
89-
}).getFilters().add(UnsupportedHttpMethodFilter.INSTANCE); // This works as a request to `/metrics` has a more explicit match and thus gets served.
90-
managementServer.start();
91-
operator.start();
92-
LOGGER.info("Operator started.");
101+
}).getFilters().add(UnsupportedHttpMethodFilter.INSTANCE);
102+
}
103+
104+
private int livezStatusCode() {
105+
int sc;
106+
try {
107+
sc = operator.getRuntimeInfo().allEventSourcesAreHealthy() ? 200 : 400;
108+
}
109+
catch (Exception e) {
110+
sc = 400;
111+
LOGGER.error("Ignoring exception caught while getting operator health info", e);
112+
}
113+
LOGGER.trace("Responding {} to GET {}", sc, HTTP_PATH_LIVEZ);
114+
return sc;
93115
}
94116

95117
void stop() {
@@ -113,7 +135,7 @@ private MicrometerMetrics enablePrometheusMetrics() {
113135

114136
private void configurePrometheusMetrics(HttpServer managementServer) {
115137
final PrometheusMeterRegistry prometheusMeterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
116-
final HttpContext metricsContext = managementServer.createContext("/metrics",
138+
final HttpContext metricsContext = managementServer.createContext(HTTP_PATH_METRICS,
117139
new MetricsHandler(prometheusMeterRegistry.getPrometheusRegistry()));
118140
metricsContext.getFilters().add(UnsupportedHttpMethodFilter.INSTANCE);
119141
Metrics.globalRegistry.add(prometheusMeterRegistry);

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

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444

4545
import static org.assertj.core.api.Assertions.assertThat;
4646
import static org.mockito.ArgumentMatchers.any;
47-
import static org.mockito.ArgumentMatchers.anyString;
4847
import static org.mockito.ArgumentMatchers.eq;
4948
import static org.mockito.Mockito.mock;
5049
import static org.mockito.Mockito.verify;
@@ -63,12 +62,22 @@ class OperatorMainTest {
6362
HttpServer managementServer;
6463

6564
@Mock
66-
HttpContext httpContext;
65+
HttpContext rootHttpContext;
66+
ArgumentCaptor<HttpHandler> rootCaptor = ArgumentCaptor.forClass(HttpHandler.class);
67+
68+
@Mock
69+
HttpContext metricsHttpContext;
70+
71+
@Mock
72+
HttpContext livezHttpContext;
73+
ArgumentCaptor<HttpHandler> livezCaptor = ArgumentCaptor.forClass(HttpHandler.class);
6774

6875
@BeforeEach
6976
void setUp() {
7077
expectApiResources();
71-
when(managementServer.createContext(anyString(), any(HttpHandler.class))).thenReturn(httpContext);
78+
when(managementServer.createContext(eq("/"), rootCaptor.capture())).thenReturn(rootHttpContext);
79+
when(managementServer.createContext(eq(OperatorMain.HTTP_PATH_METRICS), any(HttpHandler.class))).thenReturn(metricsHttpContext);
80+
when(managementServer.createContext(eq(OperatorMain.HTTP_PATH_LIVEZ), livezCaptor.capture())).thenReturn(livezHttpContext);
7281
operatorMain = new OperatorMain(kubeClient, managementServer);
7382
}
7483

@@ -138,38 +147,60 @@ void shouldRegisterMetricsWithManagementServer() {
138147
operatorMain.start();
139148

140149
// Then
141-
verify(managementServer).createContext(eq("/metrics"), any(HttpHandler.class));
150+
verify(managementServer).createContext(eq(OperatorMain.HTTP_PATH_METRICS), any(HttpHandler.class));
151+
}
152+
153+
@Test
154+
void shouldRegisterLivezWithManagementServer() {
155+
// Given
156+
157+
// When
158+
operatorMain.start();
159+
160+
// Then
161+
verify(managementServer).createContext(eq(OperatorMain.HTTP_PATH_LIVEZ), any(HttpHandler.class));
142162
}
143163

144164
@Test
145165
void shouldRegisterUnsupportedMethodsHandlerWithManagementServer() {
146166
// Given
147167
final ArrayList<Filter> filters = new ArrayList<>();
148-
when(httpContext.getFilters()).thenReturn(filters);
168+
when(rootHttpContext.getFilters()).thenReturn(filters);
149169

150170
// When
151171
operatorMain.start();
152172

153173
// Then
154174
verify(managementServer).createContext(eq("/"), any(HttpHandler.class));
155-
assertThat(filters).isNotEmpty()
175+
assertThat(filters)
156176
.singleElement()
157177
.isInstanceOf(UnsupportedHttpMethodFilter.class);
178+
158179
}
159180

160181
@Test
161182
void shouldRespondWith404ForRequestsManagementServer() throws IOException {
183+
shouldRespondWithStatusCode(rootCaptor, 404);
184+
}
185+
186+
@Test
187+
void shouldRespondWith200ForRequestsLivez() throws IOException {
188+
// Given
189+
shouldRespondWithStatusCode(livezCaptor, 200);
190+
}
191+
192+
private void shouldRespondWithStatusCode(ArgumentCaptor<HttpHandler> captor,
193+
int statusCode)
194+
throws IOException {
162195
// Given
163-
final ArgumentCaptor<HttpHandler> captor = ArgumentCaptor.forClass(HttpHandler.class);
164-
when(managementServer.createContext(anyString(), captor.capture())).thenReturn(httpContext);
165196
operatorMain.start();
166197
final HttpExchange httpExchange = mock(HttpExchange.class);
167198

168199
// When
169200
captor.getValue().handle(httpExchange);
170201

171202
// Then
172-
verify(httpExchange).sendResponseHeaders(404, -1);
203+
verify(httpExchange).sendResponseHeaders(statusCode, -1);
173204
}
174205

175206
private void expectApiResources() {

0 commit comments

Comments
 (0)