Repository: kroxylicious/kroxylicious (java)

Issue:
Operator: downstream-tls example not starting
**Describe the bug**
On main if I run:

```
export QUAY_ORG=myorghere
./scripts/build-operator.sh
./scripts/run-operator.sh
```

and then execute the instructions in `kroxylicious-operator/examples/downstream-tls/README.md` to install certmanager and the proxy configuration, the proxy pod fails to start with exception:

```
2025-05-13 01:25:48 <main> ERROR io.kroxylicious.proxy.StartupShutdownLogger - Exception on startup
java.lang.IllegalArgumentException: Couldn't parse configuration
	at io.kroxylicious.proxy.config.ConfigParser.parseConfiguration(ConfigParser.java:74) ~[kroxylicious-runtime-0.13.0-SNAPSHOT.jar:?]
	...
Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class io.kroxylicious.proxy.config.tls.TrustProvider]: Cannot deduce unique subtype of `io.kroxylicious.proxy.config.tls.TrustProvider` (2 candidates match)
 at [Source: (sun.nio.ch.ChannelInputStream); line: 22, column: 16] (through reference chain: io.kroxylicious.proxy.config.Configuration["virtualClusters"]->java.util.ArrayList[0]->io.kroxylicious.proxy.config.VirtualCluster["gateways"]->java.util.ArrayList[0]->io.kroxylicious.proxy.config.VirtualClusterGateway["tls"]->io.kroxylicious.proxy.config.tls.Tls["trust"])
```

the proxy config contains:

```
  data:
    proxy-config.yaml: |-
      ---
      ...
      - name: "my-cluster"
        ...
        gateways:
        - name: "default"
          ...
          tls:
            key:
              privateKeyFile: "/opt/kroxylicious/virtual-cluster/server-certs/server-certificate/tls.key"
              certificateFile: "/opt/kroxylicious/virtual-cluster/server-certs/server-certificate/tls.crt"
            trust: {}
```

I don't think our jackson config can handle the `trust: {}` which is the serialized output of `PlatformTrustProvider`, which cannot be deserialized using Deduction.

**Expected behavior**

If I configure a VKC/Ingress with downstream TLS server certs, but no client trust, then the proxy is configured appropriately and starts up.

**YAML files and logs**

Attach or copy paste your kroxylicious configuration YAML (with sensitive details
like broker address redacted).
Attach or copy and paste relevant logs.

**Additional context**
Add any other context about the problem here.


Pull request description:
bugfix: operator do not emit empty downstream trust object

### Type of change

- Bugfix

### Description

Fixes #2176

If you supply downstream TLS server certs but no trust, the operator was manifesting it as an empty object in the configuration YAML. This cannot be decoded using jackson deduction. For now lets change it to not set trust if it isn't supplied and we can improved the configuration API separately.

### Checklist

_Please go through this checklist and make sure all applicable tasks have been done_

- [ ] PR raised from a fork of this repository and made from a branch rather than main. 
- [ ] Write tests
- [ ] Update documentation
- [ ] Make sure all unit/integration tests pass
- [ ] Make sure all Sonarcloud warnings are addressed or are justifiably ignored.
- [ ] If applicable to the change, [trigger the system test suite](../blob/main/DEV_GUIDE.md#jenkins-pipeline-for-system-tests).  Make sure tests pass.
- [ ] If applicable to the change, [trigger the performance test suite](../blob/main/PERFORMANCE.md#jenkins-pipeline-for-performance). Ensure that any degradations to performance numbers are understood and justified.
- [ ] Ensure the PR references relevant issue(s) so they are closed on merging.
- [ ] For user facing changes, update CHANGELOG.md (remember to include changes affecting the API of the test artefacts too).

> **_NOTE:_**  You must be a member of `@kroxylicious/developers` to trigger the system test and performance test suites.  If you are not part of this group, comment on the PR requesting a trigger, tagging `@kroxylicious/developers`.


Patch:
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/KafkaProxyReconciler.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/KafkaProxyReconciler.java
index 91b9cb5a..9fa9e3d9 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/KafkaProxyReconciler.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/KafkaProxyReconciler.java
@@ -84,7 +84,6 @@ import io.kroxylicious.proxy.config.admin.PrometheusMetricsConfig;
 import io.kroxylicious.proxy.config.tls.AllowDeny;
 import io.kroxylicious.proxy.config.tls.KeyPair;
 import io.kroxylicious.proxy.config.tls.KeyProvider;
-import io.kroxylicious.proxy.config.tls.PlatformTrustProvider;
 import io.kroxylicious.proxy.config.tls.ServerOptions;
 import io.kroxylicious.proxy.config.tls.Tls;
 import io.kroxylicious.proxy.config.tls.TlsClientAuth;
@@ -321,7 +320,7 @@ public class KafkaProxyReconciler implements
                 buildTrustProvider(true, ingressTls.getTrustAnchorRef(), ingressTls.getTlsClientAuthentication(), SERVER_TRUSTED_CERTS_BASE_DIR),
                 (keyProviderOpt, trustProvider) -> Optional.of(
                         new Tls(keyProviderOpt.orElse(null),
-                                trustProvider,
+                                trustProvider.orElse(null),
                                 buildCipherSuites(ingressTls.getCipherSuites()).orElse(null),
                                 buildProtocols(ingressTls.getProtocols()).orElse(null))));
     }
@@ -343,7 +342,7 @@ public class KafkaProxyReconciler implements
                         buildTrustProvider(false, serviceTls.getTrustAnchorRef(), null, CLIENT_TRUSTED_CERTS_BASE_DIR),
                         (keyProviderOpt, trustProvider) -> Optional.of(
                                 new Tls(keyProviderOpt.orElse(null),
-                                        trustProvider,
+                                        trustProvider.orElse(null),
                                         buildCipherSuites(serviceTls.getCipherSuites()).orElse(null),
                                         buildProtocols(serviceTls.getProtocols()).orElse(null)))))
                 .orElse(ConfigurationFragment.empty());
@@ -374,10 +373,10 @@ public class KafkaProxyReconciler implements
                 }).orElse(ConfigurationFragment.empty());
     }
 
-    private static ConfigurationFragment<TrustProvider> buildTrustProvider(boolean forServer,
-                                                                           @Nullable TrustAnchorRef trustAnchorRef,
-                                                                           @Nullable TlsClientAuthentication clientAuthentication,
-                                                                           Path parent) {
+    private static ConfigurationFragment<Optional<TrustProvider>> buildTrustProvider(boolean forServer,
+                                                                                     @Nullable TrustAnchorRef trustAnchorRef,
+                                                                                     @Nullable TlsClientAuthentication clientAuthentication,
+                                                                                     Path parent) {
         return Optional.ofNullable(trustAnchorRef)
                 .filter(tar -> ResourcesUtil.isConfigMap(tar.getRef()))
                 .map(tar -> {
@@ -400,10 +399,10 @@ public class KafkaProxyReconciler implements
                             null,
                             "PEM",
                             forServer ? buildTlsServerOptions(clientAuthentication) : null);
-                    return new ConfigurationFragment<>(trustProvider,
+                    return new ConfigurationFragment<>(Optional.of(trustProvider),
                             Set.of(volume),
                             Set.of(mount));
-                }).orElse(new ConfigurationFragment<>(PlatformTrustProvider.INSTANCE, Set.of(), Set.of()));
+                }).orElse(ConfigurationFragment.empty());
     }
 
     /**


Test patch:
diff --git a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/DerivedResourcesTest.java b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/DerivedResourcesTest.java
index 56fa23c4..31ea2802 100644
--- a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/DerivedResourcesTest.java
+++ b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/DerivedResourcesTest.java
@@ -37,6 +37,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
 import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 
+import io.fabric8.kubernetes.api.model.ConfigMap;
 import io.fabric8.kubernetes.api.model.HasMetadata;
 import io.fabric8.kubernetes.api.model.ObjectMeta;
 import io.javaoperatorsdk.operator.api.reconciler.Context;
@@ -51,6 +52,8 @@ import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
 import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
 import io.kroxylicious.kubernetes.operator.checksum.Crc32ChecksumGenerator;
 import io.kroxylicious.kubernetes.operator.checksum.FixedChecksumGenerator;
+import io.kroxylicious.proxy.config.ConfigParser;
+import io.kroxylicious.proxy.config.Configuration;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 
@@ -58,6 +61,7 @@ import static io.kroxylicious.kubernetes.operator.ProxyDeploymentDependentResour
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.namespace;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 
@@ -244,6 +248,11 @@ class DerivedResourcesTest {
                                 assertThat(Files.exists(expectedFile)).describedAs(expectedFile + " does not exist").isTrue();
                                 var expected = loadExpected(expectedFile, resourceType);
                                 assertSameYaml(actualResource, expected);
+                                if (actualResource instanceof ConfigMap cm && cm.getData().containsKey("proxy-config.yaml")) {
+                                    assertThatCode(() -> {
+                                        parseConfig(cm.getData().get("proxy-config.yaml"));
+                                    }).describedAs("proxy-config in configmap should be parseable").doesNotThrowAnyException();
+                                }
                                 unusedFiles.remove(expectedFile);
 
                             }));
@@ -261,6 +270,15 @@ class DerivedResourcesTest {
         }
     }
 
+    private static void parseConfig(String content) {
+        try {
+            ConfigParser.createBaseObjectMapper().readValue(content, Configuration.class);
+        }
+        catch (IOException e) {
+            throw new IllegalArgumentException("Couldn't parse configuration", e);
+        }
+    }
+
     private static void assertMinimalMetadata(ObjectMeta metadata, String inFileName) {
         // sanity check since we can omit fields that the k8s API will ensure are present in reality
         assertThat(metadata.getName()).describedAs("metadata.name in " + inFileName).isNotNull().isNotEmpty();
diff --git a/kroxylicious-operator/src/test/resources/DerivedResourcesTest/virtualcluster-with-tls-controls/out-ConfigMap-minimal-proxy-config.yaml b/kroxylicious-operator/src/test/resources/DerivedResourcesTest/virtualcluster-with-tls-controls/out-ConfigMap-minimal-proxy-config.yaml
index 870fc152..281c17ba 100644
--- a/kroxylicious-operator/src/test/resources/DerivedResourcesTest/virtualcluster-with-tls-controls/out-ConfigMap-minimal-proxy-config.yaml
+++ b/kroxylicious-operator/src/test/resources/DerivedResourcesTest/virtualcluster-with-tls-controls/out-ConfigMap-minimal-proxy-config.yaml
@@ -43,7 +43,6 @@ data:
           key:
             privateKeyFile: "/opt/kroxylicious/virtual-cluster/server-certs/downstream-tls-cert/tls.key"
             certificateFile: "/opt/kroxylicious/virtual-cluster/server-certs/downstream-tls-cert/tls.crt"
-          trust: {}
           cipherSuites:
             denied:
             - "TLS_KRB5_WITH_3DES_EDE_CBC_MD5"
diff --git a/kroxylicious-operator/src/test/resources/DerivedResourcesTest/virtualcluster-with-tls/out-ConfigMap-minimal-proxy-config.yaml b/kroxylicious-operator/src/test/resources/DerivedResourcesTest/virtualcluster-with-tls/out-ConfigMap-minimal-proxy-config.yaml
index 09fe90f9..7f484474 100644
--- a/kroxylicious-operator/src/test/resources/DerivedResourcesTest/virtualcluster-with-tls/out-ConfigMap-minimal-proxy-config.yaml
+++ b/kroxylicious-operator/src/test/resources/DerivedResourcesTest/virtualcluster-with-tls/out-ConfigMap-minimal-proxy-config.yaml
@@ -42,5 +42,4 @@ data:
         tls:
           key:
             privateKeyFile: "/opt/kroxylicious/virtual-cluster/server-certs/downstream-tls-cert/tls.key"
-            certificateFile: "/opt/kroxylicious/virtual-cluster/server-certs/downstream-tls-cert/tls.crt"
-          trust: {}
\ No newline at end of file
+            certificateFile: "/opt/kroxylicious/virtual-cluster/server-certs/downstream-tls-cert/tls.crt"
\ No newline at end of file