Skip to content

Commit f8668cd

Browse files
authored
Merge pull request kroxylicious#1596 from robobario/pod-template
operator: enable users to supply labels to Kroxylicious podTemplate
2 parents f1afd41 + 71a952d commit f8668cd

File tree

8 files changed

+255
-63
lines changed

8 files changed

+255
-63
lines changed

kroxylicious-operator/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@
240240
-->
241241
<source>src/main/resources/META-INF/fabric8</source>
242242
<extraAnnotations>true</extraAnnotations>
243+
<existingJavaTypes>
244+
<io.kroxylicious.kubernetes.api.v1alpha1.kafkaproxyspec.PodTemplate>
245+
io.fabric8.kubernetes.api.model.PodTemplateSpec
246+
</io.kroxylicious.kubernetes.api.v1alpha1.kafkaproxyspec.PodTemplate>
247+
</existingJavaTypes>
243248
<packageOverrides>
244249
<!-- the default package name ($apiGroup.$apiVersion) doesn't work for us -->
245250
<io.kroxylicious.v1alpha1>io.kroxylicious.kubernetes.api.v1alpha1</io.kroxylicious.v1alpha1>

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
package io.kroxylicious.kubernetes.operator;
77

88
import java.util.Map;
9+
import java.util.Optional;
910

1011
import io.fabric8.kubernetes.api.model.Container;
1112
import io.fabric8.kubernetes.api.model.ContainerBuilder;
13+
import io.fabric8.kubernetes.api.model.ObjectMeta;
1214
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
1315
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
1416
import io.fabric8.kubernetes.api.model.apps.Deployment;
@@ -77,10 +79,15 @@ static Map<String, String> podLabels() {
7779

7880
private PodTemplateSpec podTemplate(KafkaProxy primary,
7981
Context<KafkaProxy> context) {
82+
Map<String, String> labelsFromSpec = Optional.ofNullable(primary.getSpec().getPodTemplate())
83+
.map(PodTemplateSpec::getMetadata)
84+
.map(ObjectMeta::getLabels)
85+
.orElse(Map.of());
8086
// @formatter:off
8187
return new PodTemplateSpecBuilder()
8288
.editOrNewMetadata()
83-
.withLabels(podLabels())
89+
.addToLabels(labelsFromSpec)
90+
.addToLabels(podLabels())
8491
.endMetadata()
8592
.editOrNewSpec()
8693
.withContainers(proxyContainer(primary, context))

kroxylicious-operator/src/main/resources/META-INF/fabric8/kafkaproxies.kroxylicious.io-v1.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,16 @@ spec:
8181
type: string
8282
name:
8383
type: string
84-
84+
podTemplate:
85+
type: object
86+
properties:
87+
metadata:
88+
properties:
89+
labels:
90+
additionalProperties:
91+
type: string
92+
type: object
93+
type: object
8594
status:
8695
type: object
8796
properties:

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

Lines changed: 73 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
3737
import io.fabric8.kubernetes.api.model.HasMetadata;
38+
import io.fabric8.kubernetes.api.model.ObjectMeta;
3839
import io.javaoperatorsdk.operator.api.reconciler.Context;
3940
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext;
4041
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
@@ -47,6 +48,7 @@
4748
import edu.umd.cs.findbugs.annotations.NonNull;
4849

4950
import static org.assertj.core.api.Assertions.assertThat;
51+
import static org.assertj.core.api.Assertions.entry;
5052
import static org.mockito.Mockito.doReturn;
5153
import static org.mockito.Mockito.mock;
5254

@@ -195,75 +197,84 @@ record ConditionStruct(ConditionType type,
195197
private static List<DynamicTest> testsForDir(List<DesiredFn<KafkaProxy, ?>> dependentResources,
196198
Path testDir)
197199
throws IOException {
198-
var unusedFiles = childFilesMatching(testDir, "*");
199-
Path input = testDir.resolve("in-KafkaProxy.yaml");
200-
KafkaProxy kafkaProxy = kafkaProxyFromFile(input);
201-
assertMinimalMetadataPresent(kafkaProxy);
202-
unusedFiles.remove(input);
203-
unusedFiles.remove(testDir.resolve("operator-config.yaml"));
204-
unusedFiles.removeAll(childFilesMatching(testDir, "in-*"));
205-
206-
Context<KafkaProxy> context;
207200
try {
208-
context = buildContext(kafkaProxy, testDir);
209-
}
210-
catch (IOException e) {
211-
throw new UncheckedIOException(e);
212-
}
213-
214-
List<DynamicTest> tests = new ArrayList<>();
215-
216-
var dr = dependentResources.stream()
217-
.flatMap(r -> r.invokeDesired(kafkaProxy, context).values().stream().map(x -> Map.entry(r.resourceType(), x)))
218-
.collect(Collectors.groupingBy(Map.Entry::getKey))
219-
.entrySet()
220-
.stream()
221-
.collect(Collectors.toMap(Map.Entry::getKey,
222-
e -> e.getValue().stream().map(Map.Entry::getValue).collect(Collectors.toCollection(() -> new TreeSet<>(
223-
Comparator.comparing(hasMetadata -> hasMetadata.getMetadata().getName()))))));
224-
for (var entry : dr.entrySet()) {
225-
var resourceType = entry.getKey();
226-
var actualResources = entry.getValue();
227-
for (var actualResource : actualResources) {
228-
String kind = resourceType.getSimpleName();
229-
String name = actualResource.getMetadata().getName();
230-
var expectedFile = testDir.resolve("out-" + kind + "-" + name + ".yaml");
231-
tests.add(DynamicTest.dynamicTest(kind + " '" + name + "' should have the same content as " + testDir.relativize(expectedFile),
232-
() -> {
233-
assertThat(Files.exists(expectedFile)).isTrue();
234-
var expected = loadExpected(expectedFile, resourceType);
235-
assertSameYaml(actualResource, expected);
236-
unusedFiles.remove(expectedFile);
237-
}));
201+
var unusedFiles = childFilesMatching(testDir, "*");
202+
String inFileName = "in-KafkaProxy.yaml";
203+
Path input = testDir.resolve(inFileName);
204+
KafkaProxy kafkaProxy = kafkaProxyFromFile(input);
205+
assertMinimalMetadata(kafkaProxy.getMetadata(), inFileName);
206+
207+
unusedFiles.remove(input);
208+
unusedFiles.remove(testDir.resolve("operator-config.yaml"));
209+
unusedFiles.removeAll(childFilesMatching(testDir, "in-*"));
210+
211+
Context<KafkaProxy> context;
212+
try {
213+
context = buildContext(kafkaProxy, testDir);
238214
}
239-
for (var cluster : kafkaProxy.getSpec().getClusters()) {
240-
ClusterCondition actualClusterCondition = SharedKafkaProxyContext.clusterCondition(context, cluster);
241-
if (actualClusterCondition.type() == ConditionType.Accepted && actualClusterCondition.status().equals(Conditions.Status.TRUE)) {
242-
continue;
243-
}
244-
else {
245-
var expectedFile = testDir.resolve("cond-" + actualClusterCondition.type() + "-" + actualClusterCondition.cluster() + ".yaml");
246-
tests.add(DynamicTest.dynamicTest(
247-
"Condition " + actualClusterCondition.type() + " for cluster " + actualClusterCondition.cluster() + " matches contents of expected file "
248-
+ expectedFile,
215+
catch (IOException e) {
216+
throw new UncheckedIOException(e);
217+
}
218+
219+
List<DynamicTest> tests = new ArrayList<>();
220+
221+
var dr = dependentResources.stream()
222+
.flatMap(r -> r.invokeDesired(kafkaProxy, context).values().stream().map(x -> Map.entry(r.resourceType(), x)))
223+
.collect(Collectors.groupingBy(Map.Entry::getKey))
224+
.entrySet()
225+
.stream()
226+
.collect(Collectors.toMap(Map.Entry::getKey,
227+
e -> e.getValue().stream().map(Map.Entry::getValue).collect(Collectors.toCollection(() -> new TreeSet<>(
228+
Comparator.comparing(hasMetadata -> hasMetadata.getMetadata().getName()))))));
229+
for (var entry : dr.entrySet()) {
230+
var resourceType = entry.getKey();
231+
var actualResources = entry.getValue();
232+
for (var actualResource : actualResources) {
233+
String kind = resourceType.getSimpleName();
234+
String name = actualResource.getMetadata().getName();
235+
var expectedFile = testDir.resolve("out-" + kind + "-" + name + ".yaml");
236+
tests.add(DynamicTest.dynamicTest(kind + " '" + name + "' should have the same content as " + testDir.relativize(expectedFile),
249237
() -> {
250-
var expected = loadExpected(expectedFile, ClusterCondition.class);
251-
assertSameYaml(actualClusterCondition, expected);
238+
assertThat(Files.exists(expectedFile)).isTrue();
239+
var expected = loadExpected(expectedFile, resourceType);
240+
assertSameYaml(actualResource, expected);
252241
unusedFiles.remove(expectedFile);
253242
}));
254243
}
244+
for (var cluster : kafkaProxy.getSpec().getClusters()) {
245+
ClusterCondition actualClusterCondition = SharedKafkaProxyContext.clusterCondition(context, cluster);
246+
if (actualClusterCondition.type() == ConditionType.Accepted && actualClusterCondition.status().equals(Conditions.Status.TRUE)) {
247+
continue;
248+
}
249+
else {
250+
var expectedFile = testDir.resolve("cond-" + actualClusterCondition.type() + "-" + actualClusterCondition.cluster() + ".yaml");
251+
tests.add(DynamicTest.dynamicTest(
252+
"Condition " + actualClusterCondition.type() + " for cluster " + actualClusterCondition.cluster() + " matches contents of expected file "
253+
+ expectedFile,
254+
() -> {
255+
var expected = loadExpected(expectedFile, ClusterCondition.class);
256+
assertSameYaml(actualClusterCondition, expected);
257+
unusedFiles.remove(expectedFile);
258+
}));
259+
}
260+
}
255261
}
256-
}
257262

258-
tests.add(DynamicTest.dynamicTest("There should be no unused files in " + testDir,
259-
() -> assertThat(unusedFiles).isEmpty()));
260-
return tests;
263+
tests.add(DynamicTest.dynamicTest("There should be no unused files in " + testDir,
264+
() -> assertThat(unusedFiles).isEmpty()));
265+
return tests;
266+
}
267+
catch (AssertionError e) {
268+
return List.of(DynamicTest.dynamicTest("failed to initialize test", () -> {
269+
throw e;
270+
}));
271+
}
261272
}
262273

263-
// sanity check since we can omit fields that the k8s API will ensure are present in reality
264-
private static void assertMinimalMetadataPresent(HasMetadata resource) {
265-
assertThat(resource.getMetadata().getName()).isNotNull().isNotEmpty();
266-
assertThat(resource.getMetadata().getNamespace()).isNotNull().isNotEmpty();
274+
private static void assertMinimalMetadata(ObjectMeta metadata, String inFileName) {
275+
// sanity check since we can omit fields that the k8s API will ensure are present in reality
276+
assertThat(metadata.getName()).describedAs("metadata.name in " + inFileName).isNotNull().isNotEmpty();
277+
assertThat(metadata.getNamespace()).describedAs("metadata.namespace in " + inFileName).isNotNull().isNotEmpty();
267278
}
268279

269280
private static <T> void assertSameYaml(T actualResource, T expected) throws JsonProcessingException {
@@ -305,10 +316,11 @@ private static Context<KafkaProxy> buildContext(KafkaProxy kafkaProxy, Path test
305316
var runtimeDecl = Files.exists(configFile) ? configFromFile(configFile) : new RuntimeDecl(List.of());
306317
Set<GenericKubernetesResource> filterInstances = new HashSet<>();
307318
for (var filterApi : runtimeDecl.filterApis()) {
308-
try (var dirStream = Files.newDirectoryStream(testDir, "in-" + filterApi.kind() + "-*.yaml")) {
319+
String fileName = "in-" + filterApi.kind() + "-*.yaml";
320+
try (var dirStream = Files.newDirectoryStream(testDir, fileName)) {
309321
for (Path p : dirStream) {
310322
GenericKubernetesResource resource = YAML_MAPPER.readValue(p.toFile(), GenericKubernetesResource.class);
311-
assertMinimalMetadataPresent(resource);
323+
assertMinimalMetadata(resource.getMetadata(), fileName);
312324
filterInstances.add(resource);
313325
}
314326
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
---
8+
kind: KafkaProxy
9+
apiVersion: kroxylicious.io/v1alpha1
10+
metadata:
11+
name: use-pod-template-spec
12+
namespace: proxy-ns
13+
spec:
14+
clusters:
15+
- name: "one"
16+
upstream:
17+
bootstrapServers: my-cluster-kafka-bootstrap.kafka.svc.cluster.local:9092
18+
podTemplate:
19+
metadata:
20+
labels:
21+
environment: "production-from-podTemplate"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
---
8+
apiVersion: "apps/v1"
9+
kind: "Deployment"
10+
metadata:
11+
labels:
12+
app: "kroxylicious"
13+
app.kubernetes.io/managed-by: "kroxylicious-operator"
14+
app.kubernetes.io/name: "kroxylicious-proxy"
15+
app.kubernetes.io/part-of: "kafka"
16+
app.kubernetes.io/instance: "use-pod-template-spec"
17+
app.kubernetes.io/component: "proxy"
18+
name: "use-pod-template-spec"
19+
namespace: proxy-ns
20+
ownerReferences:
21+
- apiVersion: "kroxylicious.io/v1alpha1"
22+
kind: "KafkaProxy"
23+
name: "use-pod-template-spec"
24+
spec:
25+
replicas: 1
26+
selector:
27+
matchLabels:
28+
app: "kroxylicious"
29+
template:
30+
metadata:
31+
labels:
32+
environment: "production-from-podTemplate"
33+
app: "kroxylicious"
34+
spec:
35+
containers:
36+
- name: "proxy"
37+
image: "quay.io/kroxylicious/kroxylicious:0.9.0-SNAPSHOT"
38+
args:
39+
- "--config"
40+
- "/opt/kroxylicious/config/proxy-config.yaml"
41+
ports:
42+
- containerPort: 9190
43+
name: "metrics"
44+
- containerPort: 9292
45+
- containerPort: 9293
46+
- containerPort: 9294
47+
- containerPort: 9295
48+
volumeMounts:
49+
- mountPath: "/opt/kroxylicious/config/proxy-config.yaml"
50+
name: "config-volume"
51+
subPath: "proxy-config.yaml"
52+
volumes:
53+
- name: "config-volume"
54+
secret:
55+
secretName: "use-pod-template-spec"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
---
8+
apiVersion: "v1"
9+
kind: "Secret"
10+
metadata:
11+
labels:
12+
app.kubernetes.io/managed-by: "kroxylicious-operator"
13+
app.kubernetes.io/name: "kroxylicious-proxy"
14+
app.kubernetes.io/part-of: "kafka"
15+
app.kubernetes.io/instance: "use-pod-template-spec"
16+
app.kubernetes.io/component: "proxy"
17+
name: "use-pod-template-spec"
18+
namespace: proxy-ns
19+
ownerReferences:
20+
- apiVersion: "kroxylicious.io/v1alpha1"
21+
kind: "KafkaProxy"
22+
name: "use-pod-template-spec"
23+
stringData:
24+
proxy-config.yaml: |
25+
---
26+
adminHttp:
27+
host: "0.0.0.0"
28+
port: 9190
29+
endpoints:
30+
prometheus: {}
31+
virtualClusters:
32+
one:
33+
targetCluster:
34+
bootstrap_servers: "my-cluster-kafka-bootstrap.kafka.svc.cluster.local:9092"
35+
clusterNetworkAddressConfigProvider:
36+
type: "PortPerBrokerClusterNetworkAddressConfigProvider"
37+
config:
38+
bootstrapAddress: "localhost:9292"
39+
brokerAddressPattern: "one.proxy-ns.svc.cluster.local"
40+
brokerStartPort: 9293
41+
numberOfBrokerPorts: 3
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
---
8+
apiVersion: "v1"
9+
kind: "Service"
10+
metadata:
11+
labels:
12+
app.kubernetes.io/managed-by: "kroxylicious-operator"
13+
app.kubernetes.io/name: "kroxylicious-proxy"
14+
app.kubernetes.io/part-of: "kafka"
15+
app.kubernetes.io/instance: "use-pod-template-spec"
16+
app.kubernetes.io/component: "proxy"
17+
name: "one"
18+
namespace: proxy-ns
19+
ownerReferences:
20+
- apiVersion: "kroxylicious.io/v1alpha1"
21+
kind: "KafkaProxy"
22+
name: "use-pod-template-spec"
23+
spec:
24+
ports:
25+
- name: "one-9292"
26+
port: 9292
27+
protocol: "TCP"
28+
targetPort: 9292
29+
- name: "one-9293"
30+
port: 9293
31+
protocol: "TCP"
32+
targetPort: 9293
33+
- name: "one-9294"
34+
port: 9294
35+
protocol: "TCP"
36+
targetPort: 9294
37+
- name: "one-9295"
38+
port: 9295
39+
protocol: "TCP"
40+
targetPort: 9295
41+
selector:
42+
app: "kroxylicious"

0 commit comments

Comments
 (0)