Repository: kroxylicious/kroxylicious
Pull request #2137: Refactor reconcilers to operate on ClusterResolutionResult
Author: login: robobario
Labels: name: kube/operator
description: Relates to the Kubernetes operator
Created: 2025-05-07T04:56:02Z | Merged: 2025-05-13T03:55:00Z

### Type of change

- Refactoring

### Description

This extends ClusterResolutionResult to contains a full graph of the attempted resolutions, including the Referent resource if we could obtain it. We need to expose this partial result to generate the ingresses, to try and keep port allocation as stable as we can.

We remove the `Map<LocalRef, ReferentType>` maps from the ProxyResolutionResult as now all clients use ClusterResolutionResult to avoid having to dereference again.

Contributes towards #1920

### Why
Previously the `ProxyResolutionResult` contained referent maps and the client code had to dereference them again. It seemed ugly to have to go back in and out of the API when the DependencyResolver was already dereferencing everything. Why should the clients have to do it again?

The end result is the proxy reconciler does not have to dereference the resources again, it can operate on the ResolvedCluster.

We are about to add functionality to the VirtualKafkaClusterreconciler where we will need to access all referents to compute a checksum from their metadata. Until now we haven't needed to access the referents, so they aren't part of the ClusterResolutionResult, this structure will also make it easy to obtain all the Referents for
checksumming in a subsequent PR.

### 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`.



Diff:
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ClusterServiceDependentResource.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ClusterServiceDependentResource.java
index d23385b312..debac0e560 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ClusterServiceDependentResource.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ClusterServiceDependentResource.java
@@ -19,6 +19,7 @@
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
 import io.kroxylicious.kubernetes.operator.model.ingress.ProxyIngressModel;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
 
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.toByNameMap;
 
@@ -51,8 +52,11 @@ public Map<String, Service> desiredResources(
         KafkaProxyContext kafkaProxyContext = KafkaProxyContext.proxyContext(context);
         var model = kafkaProxyContext.model();
         Stream<Service> serviceStream = model.clustersWithValidIngresses().stream()
+                .map(ClusterResolutionResult::cluster)
                 .filter(cluster -> !kafkaProxyContext.isBroken(cluster))
-                .flatMap(cluster -> model.ingressModel().clusterIngressModel(cluster).map(ProxyIngressModel.VirtualClusterIngressModel::services).orElse(Stream.empty()));
+                .flatMap(cluster -> model.ingressModel().clusterIngressModel(cluster)
+                        .map(ProxyIngressModel.VirtualClusterIngressModel::services)
+                        .orElse(Stream.empty()));
         return serviceStream.collect(toByNameMap());
     }
 
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 c41061d537..91b9cb5a7e 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
@@ -50,7 +50,6 @@
 import io.kroxylicious.kubernetes.api.common.CertificateRef;
 import io.kroxylicious.kubernetes.api.common.CipherSuites;
 import io.kroxylicious.kubernetes.api.common.Condition;
-import io.kroxylicious.kubernetes.api.common.FilterRef;
 import io.kroxylicious.kubernetes.api.common.LocalRef;
 import io.kroxylicious.kubernetes.api.common.Protocols;
 import io.kroxylicious.kubernetes.api.common.TrustAnchorRef;
@@ -70,7 +69,8 @@
 import io.kroxylicious.kubernetes.operator.model.ingress.ClusterIPIngressDefinition;
 import io.kroxylicious.kubernetes.operator.model.ingress.ClusterIPIngressDefinition.ClusterIPIngressInstance;
 import io.kroxylicious.kubernetes.operator.model.ingress.ProxyIngressModel;
-import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult;
 import io.kroxylicious.proxy.config.Configuration;
 import io.kroxylicious.proxy.config.NamedFilterDefinition;
 import io.kroxylicious.proxy.config.NamedRange;
@@ -209,17 +209,17 @@ private ConfigurationFragment<Configuration> generateProxyConfig(ProxyModel mode
 
     private static List<ConfigurationFragment<VirtualCluster>> buildVirtualClusters(Set<String> successfullyBuiltFilterNames, ProxyModel model) {
         return model.clustersWithValidIngresses().stream()
-                .filter(cluster -> Optional.ofNullable(cluster.getSpec().getFilterRefs()).stream().flatMap(Collection::stream).allMatch(
-                        filters -> successfullyBuiltFilterNames.contains(filterDefinitionName(filters))))
-                .map(cluster -> buildVirtualCluster(cluster, model.resolutionResult().kafkaServiceRef(cluster).orElseThrow(), model.ingressModel()))
+                .filter(cluster -> cluster.filterResolutionResults().stream().allMatch(
+                        filterResult -> successfullyBuiltFilterNames.contains(filterDefinitionName(filterResult.reference()))))
+                .map(cluster -> buildVirtualCluster(cluster, model.ingressModel()))
                 .toList();
     }
 
     private List<ConfigurationFragment<NamedFilterDefinition>> buildFilterDefinitions(ProxyModel model) {
         List<ConfigurationFragment<NamedFilterDefinition>> filterDefinitions = new ArrayList<>();
         Set<NamedFilterDefinition> uniqueValues = new HashSet<>();
-        for (VirtualKafkaCluster cluster : model.clustersWithValidIngresses()) {
-            for (ConfigurationFragment<NamedFilterDefinition> namedFilterDefinitionAndFiles : filterDefinitions(cluster, model.resolutionResult())) {
+        for (ClusterResolutionResult cluster : model.clustersWithValidIngresses()) {
+            for (ConfigurationFragment<NamedFilterDefinition> namedFilterDefinitionAndFiles : filterDefinitions(cluster)) {
                 if (uniqueValues.add(namedFilterDefinitionAndFiles.fragment())) {
                     filterDefinitions.add(namedFilterDefinitionAndFiles);
                 }
@@ -229,34 +229,31 @@ private List<ConfigurationFragment<NamedFilterDefinition>> buildFilterDefinition
         return filterDefinitions;
     }
 
-    private static List<String> filterNamesForCluster(VirtualKafkaCluster cluster) {
-        return Optional.ofNullable(cluster.getSpec().getFilterRefs())
-                .orElse(List.of())
-                .stream()
+    private static List<String> filterNamesForCluster(ClusterResolutionResult cluster) {
+        return cluster.filterResolutionResults().stream()
+                .map(ResolutionResult::reference)
                 .map(KafkaProxyReconciler::filterDefinitionName)
                 .toList();
     }
 
-    private static String filterDefinitionName(FilterRef filterCrRef) {
+    private static String filterDefinitionName(LocalRef<?> filterCrRef) {
         return filterCrRef.getName() + "." + filterCrRef.getKind() + "." + filterCrRef.getGroup();
     }
 
-    private List<ConfigurationFragment<NamedFilterDefinition>> filterDefinitions(VirtualKafkaCluster cluster,
-                                                                                 ProxyResolutionResult resolutionResult) {
+    private List<ConfigurationFragment<NamedFilterDefinition>> filterDefinitions(ClusterResolutionResult cluster) {
 
-        return Optional.ofNullable(cluster.getSpec().getFilterRefs()).orElse(List.of()).stream().map(filterCrRef -> {
+        return cluster.filterResolutionResults().stream()
+                .map(ResolutionResult::referentResource)
+                .map(filterCr -> {
+                    String filterDefinitionName = filterDefinitionName(ResourcesUtil.toLocalRef(filterCr));
+                    var spec = filterCr.getSpec();
+                    String type = spec.getType();
+                    SecureConfigInterpolator.InterpolationResult interpolationResult = interpolateConfig(spec);
+                    return new ConfigurationFragment<>(new NamedFilterDefinition(filterDefinitionName, type, interpolationResult.config()),
+                            interpolationResult.volumes(),
+                            interpolationResult.mounts());
 
-            String filterDefinitionName = filterDefinitionName(filterCrRef);
-
-            var filterCr = resolutionResult.filter(filterCrRef).orElseThrow();
-            var spec = filterCr.getSpec();
-            String type = spec.getType();
-            SecureConfigInterpolator.InterpolationResult interpolationResult = interpolateConfig(spec);
-            return new ConfigurationFragment<>(new NamedFilterDefinition(filterDefinitionName, type, interpolationResult.config()),
-                    interpolationResult.volumes(),
-                    interpolationResult.mounts());
-
-        }).toList();
+                }).toList();
     }
 
     private SecureConfigInterpolator.InterpolationResult interpolateConfig(KafkaProtocolFilterSpec spec) {
@@ -264,11 +261,10 @@ private SecureConfigInterpolator.InterpolationResult interpolateConfig(KafkaProt
         return secureConfigInterpolator.interpolate(configTemplate);
     }
 
-    private static ConfigurationFragment<VirtualCluster> buildVirtualCluster(VirtualKafkaCluster cluster,
-                                                                             KafkaService kafkaServiceRef,
+    private static ConfigurationFragment<VirtualCluster> buildVirtualCluster(ClusterResolutionResult cluster,
                                                                              ProxyIngressModel ingressModel) {
 
-        ProxyIngressModel.VirtualClusterIngressModel virtualClusterIngressModel = ingressModel.clusterIngressModel(cluster).orElseThrow();
+        ProxyIngressModel.VirtualClusterIngressModel virtualClusterIngressModel = ingressModel.clusterIngressModel(cluster.cluster()).orElseThrow();
         var gatewayFragments = ConfigurationFragment.reduce(virtualClusterIngressModel.ingressModels().stream()
                 .map(ProxyIngressModel.IngressModel::ingressInstance)
                 .map(ingressInstance -> {
@@ -276,15 +272,17 @@ private static ConfigurationFragment<VirtualCluster> buildVirtualCluster(Virtual
                     return buildVirtualClusterGateway(instance);
                 }).toList());
 
-        var virtualClusterConfigurationFragment = gatewayFragments.flatMap(clusterCfs -> buildTargetCluster(kafkaServiceRef).map(targetCluster -> new VirtualCluster(
-                name(cluster),
-                targetCluster,
-                null,
-                Optional.empty(),
-                clusterCfs,
-                false,
-                false,
-                filterNamesForCluster(cluster))));
+        KafkaService kafkaServiceRef = cluster.serviceResolutionResult().referentResource();
+        var virtualClusterConfigurationFragment = gatewayFragments
+                .flatMap(clusterCfs -> buildTargetCluster(kafkaServiceRef).map(targetCluster -> new VirtualCluster(
+                        name(cluster.cluster()),
+                        targetCluster,
+                        null,
+                        Optional.empty(),
+                        clusterCfs,
+                        false,
+                        false,
+                        filterNamesForCluster(cluster))));
         return ConfigurationFragment.combine(virtualClusterConfigurationFragment,
                 gatewayFragments,
                 (virtualCluster, gateways) -> virtualCluster);
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigStateDependentResource.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigStateDependentResource.java
index 7e2a874eb4..58f8e2abfe 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigStateDependentResource.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigStateDependentResource.java
@@ -6,6 +6,7 @@
 package io.kroxylicious.kubernetes.operator;
 
 import java.util.Comparator;
+import java.util.stream.Stream;
 
 import io.fabric8.kubernetes.api.model.ConfigMap;
 import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
@@ -92,25 +93,33 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigStateData data) {
         proxyModel.resolutionResult().clusterResolutionResults().stream()
-                .filter(result -> !result.isFullyResolved())
+                .filter(result -> !result.allReferentsFullyResolved() || ResourcesUtil.hasResolvedRefsFalseCondition(result.cluster()))
                 .forEach(clusterResolutionResult -> {
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
                     VirtualKafkaCluster patch;
-                    if (!clusterResolutionResult.danglingReferences().isEmpty()) {
+                    if (clusterResolutionResult.allDanglingReferences().findAny().isPresent()) {
                         Comparator<ClusterResolutionResult.DanglingReference> comparator = Comparator.<ClusterResolutionResult.DanglingReference, LocalRef> comparing(
-                                ClusterResolutionResult.DanglingReference::to);
+                                ClusterResolutionResult.DanglingReference::absentRef);
 
-                        LocalRef<?> firstDanglingDependency = clusterResolutionResult.danglingReferences().stream()
-                                .sorted(comparator).map(ClusterResolutionResult.DanglingReference::to).findFirst()
+                        LocalRef<?> firstDanglingReference = clusterResolutionResult.allDanglingReferences()
+                                .sorted(comparator).map(ClusterResolutionResult.DanglingReference::absentRef).findFirst()
                                 .orElseThrow();
                         String message = String.format("Resource %s was not found.",
-                                ResourcesUtil.namespacedSlug(firstDanglingDependency, cluster));
+                                ResourcesUtil.namespacedSlug(firstDanglingReference, cluster));
                         patch = statusFactory.newFalseConditionStatusPatch(cluster,
                                 Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
                     }
                     else {
+                        Stream<LocalRef<?>> referentsWithResolvedRefsFalse = clusterResolutionResult.allResolvedReferents()
+                                .filter(ResourcesUtil::hasResolvedRefsFalseCondition)
+                                .map(ResourcesUtil::toLocalRef);
+                        Stream<LocalRef<?>> clusterWithResolvedRefsTrue = ResourcesUtil.hasResolvedRefsFalseCondition(clusterResolutionResult.cluster())
+                                ? Stream.of(ResourcesUtil.toLocalRef(
+                                        clusterResolutionResult.cluster()))
+                                : Stream.of();
                         String message = String.format("Resource %s has ResolvedRefs=False.",
-                                ResourcesUtil.namespacedSlug(clusterResolutionResult.findResourcesWithResolvedRefsFalse().sorted().findFirst().orElseThrow(),
+                                ResourcesUtil.namespacedSlug(
+                                        Stream.concat(referentsWithResolvedRefsFalse, clusterWithResolvedRefsTrue).sorted().findFirst().orElseThrow(),
                                         cluster));
                         patch = statusFactory.newFalseConditionStatusPatch(cluster,
                                 Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyDeploymentDependentResource.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyDeploymentDependentResource.java
index 675fa133c8..2c36a7f2a9 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyDeploymentDependentResource.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyDeploymentDependentResource.java
@@ -32,11 +32,11 @@
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxySpec;
-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.MetadataChecksumGenerator;
 import io.kroxylicious.kubernetes.operator.model.ingress.ProxyIngressModel;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
 import io.kroxylicious.proxy.tag.VisibleForTesting;
 
 import static io.kroxylicious.kubernetes.operator.Labels.standardLabels;
@@ -132,7 +132,7 @@ public static Map<String, String> podLabels(KafkaProxy primary) {
     private PodTemplateSpec podTemplate(KafkaProxy primary,
                                         KafkaProxyContext kafkaProxyContext,
                                         ProxyIngressModel ingressModel,
-                                        List<VirtualKafkaCluster> virtualKafkaClusters,
+                                        List<ClusterResolutionResult> clusterResolutionResults,
                                         String checksum) {
         PodTemplateSpecFluent<PodTemplateSpecBuilder>.MetadataNested<PodTemplateSpecBuilder> metadataBuilder = new PodTemplateSpecBuilder()
                 .editOrNewMetadata()
@@ -151,7 +151,7 @@ private PodTemplateSpec podTemplate(KafkaProxy primary,
                             .withType("RuntimeDefault")
                         .endSeccompProfile()
                     .endSecurityContext()
-                    .withContainers(proxyContainer(kafkaProxyContext, ingressModel, virtualKafkaClusters))
+                    .withContainers(proxyContainer(kafkaProxyContext, ingressModel, clusterResolutionResults))
                     .addNewVolume()
                         .withName(CONFIG_VOLUME)
                         .withNewConfigMap()
@@ -166,7 +166,7 @@ private PodTemplateSpec podTemplate(KafkaProxy primary,
 
     private Container proxyContainer(KafkaProxyContext kafkaProxyContext,
                                      ProxyIngressModel ingressModel,
-                                     List<VirtualKafkaCluster> virtualKafkaClusters) {
+                                     List<ClusterResolutionResult> clusterResolutionResults) {
         // @formatter:off
          var containerBuilder = new ContainerBuilder()
                 .withName("proxy")
@@ -203,9 +203,9 @@ private Container proxyContainer(KafkaProxyContext kafkaProxyContext,
                     .withName(MANAGEMENT_PORT_NAME)
                 .endPort();
         // broker ports
-        virtualKafkaClusters.forEach(virtualKafkaCluster -> {
-            if (!kafkaProxyContext.isBroken(virtualKafkaCluster)) {
-                ProxyIngressModel.VirtualClusterIngressModel virtualClusterIngressModel = ingressModel.clusterIngressModel(virtualKafkaCluster).orElseThrow();
+        clusterResolutionResults.forEach(resolutionResult -> {
+            if (!kafkaProxyContext.isBroken(resolutionResult.cluster())) {
+                ProxyIngressModel.VirtualClusterIngressModel virtualClusterIngressModel = ingressModel.clusterIngressModel(resolutionResult.cluster()).orElseThrow();
                 for (ProxyIngressModel.IngressModel ingress : virtualClusterIngressModel.ingressModels()) {
                     ingress.proxyContainerPorts().forEach(containerBuilder::addToPorts);
                 }
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ResourcesUtil.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ResourcesUtil.java
index ae1b737001..5594472fde 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ResourcesUtil.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ResourcesUtil.java
@@ -35,8 +35,10 @@
 import io.kroxylicious.kubernetes.api.common.Condition;
 import io.kroxylicious.kubernetes.api.common.LocalRef;
 import io.kroxylicious.kubernetes.api.common.TrustAnchorRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngressStatus;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyStatus;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaServiceStatus;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
@@ -312,41 +314,73 @@ private static String slug(LocalRef<?> ref) {
     /**
      * Checks that the status observedGeneration is equal to the metadata generation. Indicating
      * that the current {@code spec} of the resource has been reconciled.
-     * @param cluster cluster
+     * @param hasMetadata hasMetadata
+     * @throws IllegalStateException if hasMetadata is not a CustomResource type owned by the Kroxylicious Operator
      * @return true if status observedGeneration is equal to the metadata generation
      */
-    public static boolean isStatusFresh(VirtualKafkaCluster cluster) {
-        return isStatusFresh(cluster, c -> Optional.ofNullable(c.getStatus()).map(VirtualKafkaClusterStatus::getObservedGeneration).orElse(null));
+    public static boolean isStatusFresh(HasMetadata hasMetadata) {
+        Objects.requireNonNull(hasMetadata);
+        if (hasMetadata instanceof KafkaProtocolFilter filter) {
+            return isStatusFresh(filter, i -> Optional.ofNullable(i.getStatus()).map(KafkaProtocolFilterStatus::getObservedGeneration).orElse(null));
+        }
+        else if (hasMetadata instanceof KafkaService service) {
+            return isStatusFresh(service, i -> Optional.ofNullable(i.getStatus()).map(KafkaServiceStatus::getObservedGeneration).orElse(null));
+        }
+        else if (hasMetadata instanceof VirtualKafkaCluster cluster) {
+            return isStatusFresh(cluster, c -> Optional.ofNullable(c.getStatus()).map(VirtualKafkaClusterStatus::getObservedGeneration).orElse(null));
+        }
+        else if (hasMetadata instanceof KafkaProxyIngress ingress) {
+            return isStatusFresh(ingress, i -> Optional.ofNullable(i.getStatus()).map(KafkaProxyIngressStatus::getObservedGeneration).orElse(null));
+        }
+        else if (hasMetadata instanceof KafkaProxy kafkaProxy) {
+            return isStatusFresh(kafkaProxy, i -> Optional.ofNullable(i.getStatus()).map(KafkaProxyStatus::getObservedGeneration).orElse(null));
+        }
+        else {
+            throw new IllegalArgumentException("Unknown resource type: " + hasMetadata.getClass().getName());
+        }
     }
 
     /**
-     * Checks that the status observedGeneration is equal to the metadata generation. Indicating
-     * that the current {@code spec} of the resource has been reconciled.
-     * @param ingress ingress
-     * @return true if status observedGeneration is equal to the metadata generation
+     * Checks that the status contains a fresh ResolvedRefs=false condition
+     * @param hasMetadata hasMetadata
+     * @throws IllegalStateException if hasMetadata is not a CustomResource type owned by the Kroxylicious Operator which uses ResolvedRefs Conditions
+     * @return true if hasMetadata status contains a fresh ResolvedRefs=false condition
      */
-    public static boolean isStatusFresh(KafkaProxyIngress ingress) {
-        return isStatusFresh(ingress, i -> Optional.ofNullable(i.getStatus()).map(KafkaProxyIngressStatus::getObservedGeneration).orElse(null));
+    public static boolean hasResolvedRefsFalseCondition(HasMetadata hasMetadata) {
+        Objects.requireNonNull(hasMetadata);
+        List<Condition> conditions;
+        if (hasMetadata instanceof KafkaProtocolFilter filter) {
+            conditions = Optional.ofNullable(filter.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of());
+        }
+        else if (hasMetadata instanceof KafkaService service) {
+            conditions = Optional.ofNullable(service.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of());
+        }
+        else if (hasMetadata instanceof VirtualKafkaCluster cluster) {
+            conditions = Optional.ofNullable(cluster.getStatus()).map(VirtualKafkaClusterStatus::getConditions).orElse(List.of());
+        }
+        else if (hasMetadata instanceof KafkaProxyIngress ingress) {
+            conditions = Optional.ofNullable(ingress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of());
+        }
+        else {
+            throw new IllegalArgumentException("Resource kind '" + HasMetadata.getKind(hasMetadata.getClass()) + "' does not use ResolveRefs conditions");
+        }
+        return conditions.stream().anyMatch(Condition::isResolvedRefsFalse);
     }
 
-    /**
-     * Checks that the status observedGeneration is equal to the metadata generation. Indicating
-     * that the current {@code spec} of the resource has been reconciled.
-     * @param service service
-     * @return true if status observedGeneration is equal to the metadata generation
-     */
-    public static boolean isStatusFresh(KafkaService service) {
-        return isStatusFresh(service, i -> Optional.ofNullable(i.getStatus()).map(KafkaServiceStatus::getObservedGeneration).orElse(null));
+    public static Predicate<HasMetadata> isStatusFresh() {
+        return ResourcesUtil::isStatusFresh;
     }
 
-    /**
-     * Checks that the status observedGeneration is equal to the metadata generation. Indicating
-     * that the current {@code spec} of the resource has been reconciled.
-     * @param filter filter
-     * @return true if status observedGeneration is equal to the metadata generation
-     */
-    public static boolean isStatusFresh(KafkaProtocolFilter filter) {
-        return isStatusFresh(filter, i -> Optional.ofNullable(i.getStatus()).map(KafkaProtocolFilterStatus::getObservedGeneration).orElse(null));
+    public static Predicate<HasMetadata> isStatusStale() {
+        return isStatusFresh().negate();
+    }
+
+    public static Predicate<HasMetadata> hasResolvedRefsFalseCondition() {
+        return ResourcesUtil::hasResolvedRefsFalseCondition;
+    }
+
+    public static Predicate<HasMetadata> hasKind(String kind) {
+        return hasMetadata -> HasMetadata.getKind(hasMetadata.getClass()).equals(kind);
     }
 
     @SuppressWarnings("java:S4276") // ToLongFunction is not appropriate, since observedGeneration may be null
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
index 58960ac605..0dbecd5b9e 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
@@ -54,6 +54,7 @@
 import io.kroxylicious.kubernetes.api.v1alpha1.virtualkafkaclusterstatus.IngressesBuilder;
 import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
 import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult.DanglingReference;
 import io.kroxylicious.kubernetes.operator.resolver.DependencyResolver;
 import io.kroxylicious.proxy.tag.VisibleForTesting;
 
@@ -62,6 +63,8 @@
 import static io.fabric8.kubernetes.api.model.HasMetadata.getKind;
 import static io.kroxylicious.kubernetes.api.common.Condition.Type.ResolvedRefs;
 import static io.kroxylicious.kubernetes.operator.ProxyConfigStateDependentResource.CONFIG_STATE_CONFIG_MAP_SUFFIX;
+import static io.kroxylicious.kubernetes.operator.ResourcesUtil.hasKind;
+import static io.kroxylicious.kubernetes.operator.ResourcesUtil.hasResolvedRefsFalseCondition;
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.namespace;
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.toLocalRef;
@@ -102,7 +105,7 @@ public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster,
         ClusterResolutionResult clusterResolutionResult = resolver.resolveClusterRefs(cluster, context);
 
         VirtualKafkaCluster updatedCluster = null;
-        if (clusterResolutionResult.isFullyResolved()) {
+        if (clusterResolutionResult.allReferentsFullyResolved()) {
             updatedCluster = checkClusterIngressTlsSettings(cluster, context);
             if (updatedCluster == null) {
                 updatedCluster = maybeCombineStatusWithClusterConfigMap(cluster, context);
@@ -258,28 +261,43 @@ private Optional<OwnerReference> extractOwnerRefFromKubernetesService(Service se
     private VirtualKafkaCluster handleResolutionProblems(VirtualKafkaCluster cluster,
                                                          ClusterResolutionResult clusterResolutionResult) {
         LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
-        var unresolvedIngressProxies = clusterResolutionResult.findDanglingReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND).collect(Collectors.toSet());
-        if (clusterResolutionResult.anyDependenciesNotFoundFor(clusterRef)) {
+        var unresolvedIngressProxies = clusterResolutionResult.allDanglingReferences()
+                .filter(DanglingReference.hasReferrerKind(KAFKA_PROXY_INGRESS_KIND).and(DanglingReference.hasReferentKind(KAFKA_PROXY_KIND))).collect(Collectors.toSet());
+        if (clusterResolutionResult.allDanglingReferences().anyMatch(DanglingReference.hasReferrer(clusterRef))) {
             Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster,
-                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_PROXY_KIND));
+                    clusterResolutionResult.allDanglingReferences()
+                            .filter(DanglingReference.hasReferrer(clusterRef).and(DanglingReference.hasReferentKind(KAFKA_PROXY_KIND)))
+                            .map(DanglingReference::absentRef));
             Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
-                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_SERVICE_KIND));
+                    clusterResolutionResult.allDanglingReferences()
+                            .filter(DanglingReference.hasReferrer(clusterRef).and(DanglingReference.hasReferentKind(KAFKA_SERVICE_KIND)))
+                            .map(DanglingReference::absentRef));
             Stream<String> ingressMsg = refsMessage("spec.ingresses[].ingressRef references ", cluster,
-                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_PROXY_INGRESS_KIND));
+                    clusterResolutionResult.allDanglingReferences()
+                            .filter(DanglingReference.hasReferrer(clusterRef).and(DanglingReference.hasReferentKind(KAFKA_PROXY_INGRESS_KIND)))
+                            .map(DanglingReference::absentRef));
             Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
-                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_PROTOCOL_FILTER_KIND));
+                    clusterResolutionResult.allDanglingReferences()
+                            .filter(DanglingReference.hasReferrer(clusterRef).and(DanglingReference.hasReferentKind(KAFKA_PROTOCOL_FILTER_KIND)))
+                            .map(DanglingReference::absentRef));
             return statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_REFS_NOT_FOUND,
                     joiningMessages(proxyMsg, serviceMsg, ingressMsg, filterMsg));
         }
-        else if (clusterResolutionResult.anyResolvedRefsConditionsFalse() || !unresolvedIngressProxies.isEmpty()) {
+        else if (clusterResolutionResult.allResolvedReferents().anyMatch(ResourcesUtil::hasResolvedRefsFalseCondition) || !unresolvedIngressProxies.isEmpty()) {
             Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
-                    clusterResolutionResult.findResourcesWithResolvedRefsFalse(KAFKA_SERVICE_KIND));
+                    clusterResolutionResult.allResolvedReferents().filter(hasResolvedRefsFalseCondition().and(hasKind(KAFKA_SERVICE_KIND)))
+                            .map(ResourcesUtil::toLocalRef));
             Stream<String> ingressMsg = refsMessage("spec.ingresses[].ingressRef references ", cluster,
-                    clusterResolutionResult.findResourcesWithResolvedRefsFalse(KAFKA_PROXY_INGRESS_KIND));
+                    clusterResolutionResult.allResolvedReferents()
+                            .filter(hasResolvedRefsFalseCondition().and(hasKind(KAFKA_PROXY_INGRESS_KIND))).map(ResourcesUtil::toLocalRef));
             Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
-                    clusterResolutionResult.findResourcesWithResolvedRefsFalse(KAFKA_PROTOCOL_FILTER_KIND));
+                    clusterResolutionResult.allResolvedReferents()
+                            .filter(hasResolvedRefsFalseCondition().and(hasKind(KAFKA_PROTOCOL_FILTER_KIND)))
+                            .map(ResourcesUtil::toLocalRef));
             Stream<String> ingressProxyMessage = refsMessage("a spec.ingresses[].ingressRef had an inconsistent or missing proxyRef ", cluster,
-                    clusterResolutionResult.findDanglingReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND));
+                    clusterResolutionResult.allDanglingReferences()
+                            .filter(DanglingReference.hasReferrerKind(KAFKA_PROXY_INGRESS_KIND).and(DanglingReference.hasReferentKind(KAFKA_PROXY_KIND)))
+                            .map(DanglingReference::absentRef));
             return statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs,
                     Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
                     joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage));
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModel.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModel.java
index 7afbb80b51..00ad991d83 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModel.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModel.java
@@ -8,8 +8,8 @@
 
 import java.util.List;
 
-import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
 import io.kroxylicious.kubernetes.operator.model.ingress.ProxyIngressModel;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
 import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
 
 /**
@@ -22,6 +22,6 @@
  */
 public record ProxyModel(ProxyResolutionResult resolutionResult,
                          ProxyIngressModel ingressModel,
-                         List<VirtualKafkaCluster> clustersWithValidIngresses) {
+                         List<ClusterResolutionResult> clustersWithValidIngresses) {
 
 }
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModelBuilder.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModelBuilder.java
index 0a866e0adb..4335ea6ecf 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModelBuilder.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModelBuilder.java
@@ -13,11 +13,11 @@
 import io.javaoperatorsdk.operator.api.reconciler.Context;
 
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
-import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
 import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 import io.kroxylicious.kubernetes.operator.StaleReferentStatusException;
 import io.kroxylicious.kubernetes.operator.model.ingress.IngressAllocator;
 import io.kroxylicious.kubernetes.operator.model.ingress.ProxyIngressModel;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
 import io.kroxylicious.kubernetes.operator.resolver.DependencyResolver;
 import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
 
@@ -44,8 +44,11 @@ public ProxyModel build(KafkaProxy primary, Context<KafkaProxy> context) {
         // to try and produce the most stable allocation of ports we can, we attempt to consider all clusters in the ingress allocation, even those
         // that we know are unacceptable due to unresolved dependencies.
         ProxyIngressModel ingressModel = IngressAllocator.allocateProxyIngressModel(primary, resolutionResult);
-        List<VirtualKafkaCluster> clustersWithValidIngresses = resolutionResult.fullyResolvedClustersInNameOrder().stream()
-                .filter(cluster -> ingressModel.clusterIngressModel(cluster).map(i -> i.ingressExceptions().isEmpty()).orElse(false)).toList();
+        List<ClusterResolutionResult> clustersWithValidIngresses = resolutionResult.allResolutionResultsInClusterNameOrder()
+                .filter(clusterResolutionResult -> clusterResolutionResult.allReferentsFullyResolved() && !ResourcesUtil.hasResolvedRefsFalseCondition(
+                        clusterResolutionResult.cluster()))
+                .filter(result -> ingressModel.clusterIngressModel(result.cluster()).map(i -> i.ingressExceptions().isEmpty()).orElse(false))
+                .toList();
         return new ProxyModel(resolutionResult, ingressModel, clustersWithValidIngresses);
     }
 
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressAllocator.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressAllocator.java
index 6c68ca68e8..fe592953f7 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressAllocator.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressAllocator.java
@@ -11,7 +11,7 @@
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
-import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
 import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
 
 import static io.kroxylicious.kubernetes.operator.ProxyDeploymentDependentResource.PROXY_PORT_START;
@@ -42,17 +42,15 @@ public static ProxyIngressModel allocateProxyIngressModel(KafkaProxy primary,
                                                               ProxyResolutionResult resolutionResult) {
         AtomicInteger exclusivePorts = new AtomicInteger(PROXY_PORT_START);
         // include broken clusters in the model, so that if they are healed the ports will stay the same
-        Stream<VirtualKafkaCluster> virtualKafkaClusterStream = resolutionResult.allClustersInNameOrder().stream();
+        Stream<ClusterResolutionResult> virtualKafkaClusterStream = resolutionResult.allResolutionResultsInClusterNameOrder();
         List<ProxyIngressModel.VirtualClusterIngressModel> list = virtualKafkaClusterStream
-                .map(it -> new ProxyIngressModel.VirtualClusterIngressModel(it, allocateIngressModel(primary, it, exclusivePorts,
-                        resolutionResult)))
+                .map(it -> new ProxyIngressModel.VirtualClusterIngressModel(it.cluster(), allocateIngressModel(primary, it, exclusivePorts)))
                 .toList();
         return new ProxyIngressModel(list);
     }
 
-    private static List<ProxyIngressModel.IngressModel> allocateIngressModel(KafkaProxy primary, VirtualKafkaCluster it, AtomicInteger ports,
-                                                                             ProxyResolutionResult resolutionResult) {
-        Stream<IngressDefinition> ingressStream = Ingresses.ingressesFor(primary, it, resolutionResult);
+    private static List<ProxyIngressModel.IngressModel> allocateIngressModel(KafkaProxy primary, ClusterResolutionResult it, AtomicInteger ports) {
+        Stream<IngressDefinition> ingressStream = IngressDefinitionBuilder.buildIngressDefinitions(primary, it);
         return ingressStream.map(resource -> {
             int toAllocate = resource.numIdentifyingPortsRequired();
             IngressConflictException exception = null;
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressDefinition.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressDefinition.java
index 7e473d323a..a53ca9e841 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressDefinition.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressDefinition.java
@@ -9,10 +9,10 @@
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
 
 /**
- * Stateless Ingress model that:
+ * Ingress definition for a single VirtualKafkaCluster, KafkaProxyIngress pair that:
  * <ol>
- *     <li>describes the requirements of the ingress (e.g. how many exclusive ports it requires)</li>
- *     <li>can instantiate IngressInstances, allowing multiple virtual clusters to use the same IngressDefinition</li>
+ *     <li>declares the requirements of the ingress (e.g. how many identifying ports it requires)</li>
+ *     <li>can instantiate IngressInstances</li>
  * </ol>
  */
 public interface IngressDefinition {
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressDefinitionBuilder.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressDefinitionBuilder.java
new file mode 100644
index 0000000000..7caf739085
--- /dev/null
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressDefinitionBuilder.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.model.ingress;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.api.v1alpha1.kafkaproxyingressspec.ClusterIP;
+import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRanges;
+import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRangesBuilder;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
+
+/**
+ * IngressDefinitionBuilder is responsible for creating richer IngressDefinition objects from the raw
+ * Custom Resources that define it.
+ */
+class IngressDefinitionBuilder {
+
+    public static final List<NodeIdRanges> DEFAULT_NODE_ID_RANGES = List.of(new NodeIdRangesBuilder().withName("default").withStart(0L).withEnd(2L).build());
+
+    private IngressDefinitionBuilder() {
+    }
+
+    public static Stream<IngressDefinition> buildIngressDefinitions(KafkaProxy primary, ClusterResolutionResult clusterResult) {
+        VirtualKafkaCluster cluster = clusterResult.cluster();
+        return clusterResult.ingressResolutionResults().stream()
+                .flatMap(
+                        ingressResolutionResult -> {
+                            Optional<KafkaProxyIngress> maybeIngress = ingressResolutionResult.ingressResolutionResult().maybeReferentResource();
+                            if (maybeIngress.isPresent()) {
+                                Optional<KafkaService> maybeService = clusterResult.serviceResolutionResult().maybeReferentResource();
+                                // todo, maybe we should not include the case where the service does not resolve, it's optimistic to assume its using the default node id range
+                                List<NodeIdRanges> nodeIdRanges = maybeService.map(s -> s.getSpec().getNodeIdRanges()).orElse(DEFAULT_NODE_ID_RANGES);
+                                return Stream.of(buildIngressDefinition(primary, cluster, maybeIngress.get(), nodeIdRanges));
+                            }
+                            else {
+                                // skip unresolved ingresses
+                                return Stream.empty();
+                            }
+                        });
+    }
+
+    private static IngressDefinition buildIngressDefinition(KafkaProxy primary, VirtualKafkaCluster cluster, KafkaProxyIngress ingress, List<NodeIdRanges> nodeIdRanges) {
+        ClusterIP clusterIP = ingress.getSpec().getClusterIP();
+        if (clusterIP != null) {
+            return new ClusterIPIngressDefinition(ingress, cluster, primary, nodeIdRanges);
+        }
+        else {
+            throw new IllegalStateException("ingress must have clusterIP specified");
+        }
+    }
+}
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressInstance.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressInstance.java
index 7dfaa40e98..ece8818ba1 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressInstance.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressInstance.java
@@ -12,7 +12,7 @@
 import io.fabric8.kubernetes.api.model.ServiceBuilder;
 
 /**
- * Stateful Ingress model that provides:
+ * Ingress model that provides:
  * <ol>
  *     <li>
  *         The Gateway Config, that will be added to the virtual clusters gateways
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/Ingresses.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/Ingresses.java
deleted file mode 100644
index 6dae36a1d2..0000000000
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/Ingresses.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright Kroxylicious Authors.
- *
- * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
- */
-
-package io.kroxylicious.kubernetes.operator.model.ingress;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Stream;
-
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
-import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
-import io.kroxylicious.kubernetes.api.v1alpha1.kafkaproxyingressspec.ClusterIP;
-import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRanges;
-import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRangesBuilder;
-import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
-
-class Ingresses {
-
-    public static final List<NodeIdRanges> DEFAULT_NODE_ID_RANGES = List.of(new NodeIdRangesBuilder().withName("default").withStart(0L).withEnd(2L).build());
-
-    private Ingresses() {
-    }
-
-    public static Stream<IngressDefinition> ingressesFor(KafkaProxy primary, VirtualKafkaCluster cluster, ProxyResolutionResult resolutionResult) {
-        return cluster.getSpec().getIngresses().stream()
-                .map(io.kroxylicious.kubernetes.api.v1alpha1.virtualkafkaclusterspec.Ingresses::getIngressRef)
-                .flatMap(
-                        ingressRef -> {
-                            // skip unresolved ingresses, we are working with VirtualClusters that may have dangling references
-                            Optional<KafkaProxyIngress> ingress = resolutionResult.ingress(ingressRef);
-                            List<NodeIdRanges> nodeIdRanges = resolutionResult.kafkaServiceRef(cluster).map(s -> s.getSpec().getNodeIdRanges())
-                                    .orElse(DEFAULT_NODE_ID_RANGES);
-                            return ingress.stream().map(kafkaProxyIngress -> toIngress(primary, cluster, kafkaProxyIngress, nodeIdRanges));
-                        });
-    }
-
-    private static IngressDefinition toIngress(KafkaProxy primary, VirtualKafkaCluster cluster, KafkaProxyIngress ingress, List<NodeIdRanges> nodeIdRanges) {
-        ClusterIP clusterIP = ingress.getSpec().getClusterIP();
-        if (clusterIP != null) {
-            return new ClusterIPIngressDefinition(ingress, cluster, primary, nodeIdRanges);
-        }
-        else {
-            throw new IllegalStateException("ingress must have clusterIP specified");
-        }
-    }
-}
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
index 3682371267..044abed0ab 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
@@ -6,109 +6,95 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 /**
- * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
- * whether there were any resolution problems like dangling references, or referenced resources
- * that have a ResolvedRefs: False condition.
+ * The result of a deep resolution of the transitive references of a single VirtualKafkaCluster. Describes
+ * whether there were any resolution problems like dangling references, or Referents that have a ResolvedRefs=False
+ * condition, or Referents that have a stale status.
+ *
+ * @param cluster the cluster resource that was resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec,
+ * note that the same Filter can be in this list multiple times
+ * @param serviceResolutionResult the resolution result for the kafka service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling references)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has dangling references</li>
      * </ol>
+     * @return true if fully resolved
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean allReferentsFullyResolved() {
+        return allDanglingReferences().findAny().isEmpty() && allResolvedReferents()
+                .noneMatch(ResourcesUtil::hasResolvedRefsFalseCondition);
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * @return a stream of all dangling references
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
-    }
-
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    public Stream<DanglingReference> allDanglingReferences() {
+        return allResolutionResults().filter(ResolutionResult::dangling)
+                .map(result -> new DanglingReference(result.referrer(), result.reference()));
     }
 
-    public boolean anyResolvedRefsConditionsFalse() {
-        return !resourcesWithResolvedRefsFalse.isEmpty();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
-        return resourcesWithResolvedRefsFalse.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findReferentsWithStaleStatus() {
-        return referentsWithStaleStatus.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
-        return findResourcesWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
-    }
-
-    ClusterResolutionResult addAllResourcesHavingResolvedRefsFalse(Set<LocalRef<?>> resolvedRefsFalse) {
-        return new ClusterResolutionResult(cluster, danglingReferences, Stream.concat(resourcesWithResolvedRefsFalse.stream(), resolvedRefsFalse.stream()).collect(
-                Collectors.toSet()), referentsWithStaleStatus);
+    /**
+     * @return a stream of all resolved referents of this Cluster, note that KafkaProxy is not considered a Referent of VirtualKafkaCluster to prevent cycles
+     */
+    public Stream<HasMetadata> allResolvedReferents() {
+        return allResolutionResults()
+                .filter(resolutionResult -> !Objects.equals(resolutionResult.reference().getKind(), HasMetadata.getKind(KafkaProxy.class)))
+                .flatMap(resolutionResult -> resolutionResult.maybeReferentResource().stream());
     }
 
-    ClusterResolutionResult addReferentsWithStaleStatus(Set<LocalRef<?>> referentsWithStaleStatus) {
-        return new ClusterResolutionResult(cluster, danglingReferences, resourcesWithResolvedRefsFalse,
-                Stream.concat(this.referentsWithStaleStatus.stream(), referentsWithStaleStatus.stream()).collect(
-                        Collectors.toSet()));
+    private Stream<ResolutionResult<? extends HasMetadata>> allResolutionResults() {
+        Stream<ResolutionResult<? extends HasMetadata>> proxyResults = Stream.of(proxyResolutionResult);
+        Stream<ResolutionResult<? extends HasMetadata>> filterResults = filterResolutionResults.stream().map(Function.identity());
+        Stream<ResolutionResult<? extends HasMetadata>> serviceResults = Stream.of(serviceResolutionResult);
+        Stream<ResolutionResult<? extends HasMetadata>> ingressResults = ingressResolutionResults.stream()
+                .flatMap(i -> Stream.concat(Stream.of(i.ingressResolutionResult()), Optional.ofNullable(i.proxyResolutionResult()).stream()));
+        return Stream.of(proxyResults, filterResults, serviceResults, ingressResults).flatMap(Function.identity());
     }
 
     /**
-     * Describes the case where an entity A references an entity B but we can not find B.
-     * @param from
-     * @param to
+     * Describes the case where an entity A references an entity B, but we can not find B.
+     * @param referrer the Referrer
+     * @param absentRef the ref that could not be found
      */
-    public record DanglingReference(LocalRef<?> from, LocalRef<?> to) {
-        public DanglingReference {
-            Objects.requireNonNull(from);
-            Objects.requireNonNull(to);
+    public record DanglingReference(LocalRef<?> referrer, LocalRef<?> absentRef) {
+        public static Predicate<DanglingReference> hasReferrerKind(String kind) {
+            return p -> Objects.equals(p.referrer.getKind(), kind);
+        }
+
+        public static Predicate<DanglingReference> hasReferentKind(String kind) {
+            return p -> Objects.equals(p.absentRef.getKind(), kind);
+        }
+
+        public static Predicate<DanglingReference> hasReferrer(LocalRef<?> referrer) {
+            return p -> p.referrer.equals(referrer);
         }
     }
 }
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolver.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolver.java
index 72f76c0257..31acae9e5e 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolver.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolver.java
@@ -11,32 +11,21 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
-import java.util.function.Function;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
-import io.fabric8.kubernetes.api.model.HasMetadata;
 import io.javaoperatorsdk.operator.api.reconciler.Context;
 
-import io.kroxylicious.kubernetes.api.common.Condition;
-import io.kroxylicious.kubernetes.api.common.FilterRef;
+import io.kroxylicious.kubernetes.api.common.IngressRef;
+import io.kroxylicious.kubernetes.api.common.KafkaServiceRef;
 import io.kroxylicious.kubernetes.api.common.LocalRef;
 import io.kroxylicious.kubernetes.api.common.ProxyRef;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngressStatus;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaServiceStatus;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
-import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaClusterSpec;
-import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaClusterStatus;
-import io.kroxylicious.kubernetes.api.v1alpha1.virtualkafkaclusterspec.Ingresses;
 import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
-import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterStatus;
 import io.kroxylicious.kubernetes.operator.ResourcesUtil;
-import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult.DanglingReference;
 
-import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.toLocalRef;
 
 /**
@@ -52,13 +41,13 @@
  *     Dangling References - an entity refers to another entity that cannot be found
  * </li>
  * <li>
- *     ResolvedRefs=False condition - an entity has a condition declaring that it's Ref's are not resolved
+ *     ResolvedRefs=False condition - an entity has a condition declaring that its Refs are not resolved
  * </li>
  * </ul>
  */
 public class DependencyResolver {
 
-    public static final ProxyResolutionResult EMPTY_RESOLUTION_RESULT = new ProxyResolutionResult(Map.of(), Map.of(), Map.of(), Set.of());
+    public static final ProxyResolutionResult EMPTY_RESOLUTION_RESULT = new ProxyResolutionResult(Set.of());
 
     private DependencyResolver() {
     }
@@ -87,18 +76,8 @@ public ProxyResolutionResult resolveProxyRefs(KafkaProxy proxy, Context<?> conte
         CommonDependencies commonDependencies = getCommonDependenciesFrom(context);
         var clusterResolutionResults = virtualKafkaClusters.stream()
                 .map(cluster -> discoverProblemsAndBuildResolutionResult(cluster, commonDependencies, Set.of(proxy)))
-                .map(DependencyResolver::checkClusterConditions)
                 .collect(Collectors.toSet());
-        return new ProxyResolutionResult(commonDependencies.filters(), commonDependencies.ingresses(), commonDependencies.kafkaServices(), clusterResolutionResults);
-    }
-
-    private static ClusterResolutionResult checkClusterConditions(ClusterResolutionResult result) {
-        VirtualKafkaCluster cluster = result.cluster();
-        boolean clusterHasResolvedRefsFalse = hasAnyResolvedRefsFalse(
-                Optional.ofNullable(cluster.getStatus()).map(VirtualKafkaClusterStatus::getConditions).orElse(List.of()));
-        Set<LocalRef<?>> resolvedRefsFalse = clusterHasResolvedRefsFalse ? Set.of(toLocalRef(cluster)) : Set.of();
-        Set<LocalRef<?>> referentsWithStaleStatus = determineReferentsWithStaleStatus(cluster);
-        return result.addAllResourcesHavingResolvedRefsFalse(resolvedRefsFalse).addReferentsWithStaleStatus(referentsWithStaleStatus);
+        return new ProxyResolutionResult(clusterResolutionResults);
     }
 
     private static CommonDependencies getCommonDependenciesFrom(Context<?> context) {
@@ -139,133 +118,59 @@ public ClusterResolutionResult resolveClusterRefs(VirtualKafkaCluster cluster, C
     }
 
     private ClusterResolutionResult discoverProblemsAndBuildResolutionResult(VirtualKafkaCluster cluster,
-                                                                             CommonDependencies result,
+                                                                             CommonDependencies commonDependencies,
                                                                              Set<KafkaProxy> proxies) {
-        Set<DanglingReference> danglingReferences = determineDanglingRefs(result, proxies, cluster);
-        Set<LocalRef<?>> resolvedRefsFalse = determineResolvedRefsFalse(result);
-        Set<LocalRef<?>> referentsWithStaleStatus = determineReferentsWithStaleStatus(result);
-        return new ClusterResolutionResult(cluster, danglingReferences, resolvedRefsFalse, referentsWithStaleStatus);
-    }
-
-    private static Set<LocalRef<?>> determineReferentsWithStaleStatus(CommonDependencies cluster) {
-        Stream<LocalRef<?>> ingressesWithStaleStatus = cluster.ingresses().entrySet().stream().filter(e -> !ResourcesUtil.isStatusFresh(e.getValue()))
-                .map(Map.Entry::getKey);
-        Stream<LocalRef<?>> servicesWithStaleStatus = cluster.kafkaServices().entrySet().stream().filter(e -> !ResourcesUtil.isStatusFresh(e.getValue()))
-                .map(Map.Entry::getKey);
-        Stream<LocalRef<?>> filtersWithStaleStatus = cluster.filters().entrySet().stream().filter(e -> !ResourcesUtil.isStatusFresh(e.getValue()))
-                .map(Map.Entry::getKey);
-        return Stream.of(ingressesWithStaleStatus, servicesWithStaleStatus, filtersWithStaleStatus)
-                .flatMap(Function.identity())
-                .collect(Collectors.toSet());
-    }
-
-    private static Set<LocalRef<?>> determineReferentsWithStaleStatus(VirtualKafkaCluster cluster) {
-        if (ResourcesUtil.isStatusFresh(cluster)) {
-            return Set.of();
-        }
-        else {
-            return Set.of(toLocalRef(cluster));
-        }
-    }
-
-    private static Set<DanglingReference> determineDanglingRefs(CommonDependencies dependencies, Set<KafkaProxy> proxies,
-                                                                VirtualKafkaCluster cluster) {
         LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
-        VirtualKafkaClusterSpec spec = cluster.getSpec();
-        var danglingIngressRefs = determineDanglingIngressRefs(clusterRef, spec, dependencies.ingresses());
-        var danglingProxyRefs = determineDanglingKafkaProxyRefs(clusterRef, spec, proxies).stream();
-        var danglingIngressProxyRefs = determineIngressesWithDanglingProxyRefs(dependencies.ingresses(), proxies);
-        var danglingServiceRefs = determineDanglingKafkaServiceRefs(clusterRef, spec, dependencies.kafkaServices()).stream();
-        var danglingFilterRefs = determineDanglingFilterRefs(clusterRef, spec, dependencies.filters());
-        return Stream.of(danglingIngressRefs, danglingProxyRefs, danglingIngressProxyRefs, danglingServiceRefs, danglingFilterRefs)
-                .flatMap(Function.identity()).collect(Collectors.toSet());
-    }
-
-    private static Set<LocalRef<?>> determineResolvedRefsFalse(CommonDependencies dependencies) {
-        Stream<HasMetadata> ingressesWithResolvedRefsFalse = dependencies.ingresses().values().stream()
-                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                .map(HasMetadata.class::cast);
-        Stream<HasMetadata> filtersWithResolvedRefsFalse = dependencies.filters().values().stream()
-                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                .map(HasMetadata.class::cast);
-        Stream<HasMetadata> kafkaServicesWithResolvedRefsFalse = dependencies.kafkaServices().values().stream()
-                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                .map(HasMetadata.class::cast);
-        Stream<LocalRef<?>> localRefStream = Stream
-                .of(ingressesWithResolvedRefsFalse, filtersWithResolvedRefsFalse, kafkaServicesWithResolvedRefsFalse)
-                .flatMap(Function.identity())
-                .map(ResourcesUtil::toLocalRef);
-        return localRefStream.collect(Collectors.toSet());
-    }
-
-    private static boolean hasAnyResolvedRefsFalse(List<Condition> conditions) {
-        return conditions.stream().anyMatch(Condition::isResolvedRefsFalse);
-    }
-
-    private static Stream<DanglingReference> determineIngressesWithDanglingProxyRefs(Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses,
-                                                                                     Set<KafkaProxy> proxies) {
-        Set<LocalRef<KafkaProxy>> localRefs = proxies.stream().map(ResourcesUtil::toLocalRef).collect(Collectors.toSet());
-        return ingresses.values().stream().flatMap(ingress -> {
-            ProxyRef proxyRef = ingress.getSpec().getProxyRef();
-            if (!localRefs.contains(proxyRef)) {
-                return Stream.of(new DanglingReference(toLocalRef(ingress), proxyRef));
-            }
-            else {
-                return Stream.of();
-            }
-        });
+        return new ClusterResolutionResult(cluster,
+                resolveProxy(clusterRef, cluster, proxies),
+                resolveFilters(clusterRef, cluster, commonDependencies),
+                resolveService(clusterRef, cluster, commonDependencies),
+                resolveIngresses(clusterRef, cluster, commonDependencies, proxies));
+    }
+
+    private ResolutionResult<KafkaProxy> resolveProxy(LocalRef<?> referrer, VirtualKafkaCluster cluster, Set<KafkaProxy> proxies) {
+        ProxyRef proxyRef = cluster.getSpec().getProxyRef();
+        return resolveProxy(referrer, proxies, proxyRef);
+    }
+
+    private static ResolutionResult<KafkaProxy> resolveProxy(LocalRef<?> referrer, Set<KafkaProxy> proxies, ProxyRef proxyRef) {
+        return proxies.stream().filter(p -> ResourcesUtil.toLocalRef(p).equals(proxyRef)).findFirst()
+                .map(p -> new ResolutionResult<>(referrer, proxyRef, p)).orElse(new ResolutionResult<>(referrer, proxyRef, null));
+    }
+
+    private List<ResolutionResult<KafkaProtocolFilter>> resolveFilters(LocalRef<VirtualKafkaCluster> clusterRef,
+                                                                       VirtualKafkaCluster cluster,
+                                                                       CommonDependencies commonDependencies) {
+        return Optional.ofNullable(cluster.getSpec().getFilterRefs()).orElse(List.of()).stream()
+                .map(ref -> {
+                    Optional<KafkaProtocolFilter> filter = Optional.ofNullable(commonDependencies.filters().get(ref));
+                    return new ResolutionResult<>(clusterRef, ref, filter.orElse(null));
+                }).toList();
+    }
+
+    private ResolutionResult<KafkaService> resolveService(LocalRef<VirtualKafkaCluster> clusterRef,
+                                                          VirtualKafkaCluster cluster,
+                                                          CommonDependencies commonDependencies) {
+        KafkaServiceRef serviceRef = cluster.getSpec().getTargetKafkaServiceRef();
+        KafkaService service = commonDependencies.kafkaServices().get(serviceRef);
+        Optional<KafkaService> optionalKafkaService = Optional.ofNullable(service);
+        return new ResolutionResult<>(clusterRef, serviceRef, optionalKafkaService.orElse(null));
+    }
+
+    private List<IngressResolutionResult> resolveIngresses(LocalRef<VirtualKafkaCluster> clusterRef,
+                                                           VirtualKafkaCluster cluster,
+                                                           CommonDependencies commonDependencies,
+                                                           Set<KafkaProxy> proxies) {
+        return cluster.getSpec().getIngresses().stream().map(ingress -> {
+            IngressRef ingressRef = ingress.getIngressRef();
+            KafkaProxyIngress kafkaProxyIngress = commonDependencies.ingresses().get(ingressRef);
+            Optional<KafkaProxyIngress> optionalKafkaProxyIngress = Optional.ofNullable(kafkaProxyIngress);
+            ResolutionResult<KafkaProxyIngress> resolvedIngress = new ResolutionResult<>(clusterRef, ingressRef,
+                    optionalKafkaProxyIngress.orElse(null));
+            ResolutionResult<KafkaProxy> kafkaProxyResolutionResult = optionalKafkaProxyIngress
+                    .map(i -> resolveProxy(ResourcesUtil.toLocalRef(i), proxies, i.getSpec().getProxyRef()))
+                    .orElse(null);
+            return new IngressResolutionResult(resolvedIngress, kafkaProxyResolutionResult);
+        }).toList();
     }
-
-    private static Optional<DanglingReference> determineDanglingKafkaProxyRefs(LocalRef<VirtualKafkaCluster> clusterRef, VirtualKafkaClusterSpec spec,
-                                                                               Set<KafkaProxy> proxies) {
-        Set<LocalRef<KafkaProxy>> localRefs = proxies.stream().map(ResourcesUtil::toLocalRef).collect(Collectors.toSet());
-        ProxyRef proxyRef = spec.getProxyRef();
-        if (!localRefs.contains(proxyRef)) {
-            return Optional.of(new DanglingReference(clusterRef, proxyRef));
-        }
-        else {
-            return Optional.empty();
-        }
-    }
-
-    private static Stream<DanglingReference> determineDanglingFilterRefs(LocalRef<VirtualKafkaCluster> clusterRef, VirtualKafkaClusterSpec spec,
-                                                                         Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters) {
-        List<FilterRef> filtersList = spec.getFilterRefs();
-        if (filtersList == null) {
-            return Stream.empty();
-        }
-        else {
-            return filtersList.stream()
-                    .filter(filterRef -> filters.values().stream().noneMatch(filterResource -> filterResourceMatchesRef(filterRef, filterResource)))
-                    .map(ref -> new DanglingReference(clusterRef, ref));
-        }
-    }
-
-    private static Optional<DanglingReference> determineDanglingKafkaServiceRefs(LocalRef<VirtualKafkaCluster> clusterRef, VirtualKafkaClusterSpec spec,
-                                                                                 Map<LocalRef<KafkaService>, KafkaService> clusterRefs) {
-        var serviceRef = spec.getTargetKafkaServiceRef();
-        if (!clusterRefs.containsKey(serviceRef)) {
-            return Optional.of(new DanglingReference(clusterRef, serviceRef));
-        }
-        else {
-            return Optional.empty();
-        }
-    }
-
-    private static Stream<DanglingReference> determineDanglingIngressRefs(LocalRef<?> clusterRef, VirtualKafkaClusterSpec spec,
-                                                                          Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses) {
-        return spec.getIngresses().stream()
-                .map(Ingresses::getIngressRef)
-                .filter(ref -> !ingresses.containsKey(ref))
-                .map(ingressRef -> new DanglingReference(clusterRef, ingressRef));
-    }
-
-    private static boolean filterResourceMatchesRef(FilterRef filterRef, KafkaProtocolFilter filterResource) {
-        String apiVersion = filterResource.getApiVersion();
-        var filterResourceGroup = apiVersion.substring(0, apiVersion.indexOf("/"));
-        return filterResourceGroup.equals(filterRef.getGroup())
-                && filterResource.getKind().equals(filterRef.getKind())
-                && name(filterResource).equals(filterRef.getName());
-    }
-
 }
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/IngressResolutionResult.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/IngressResolutionResult.java
new file mode 100644
index 0000000000..bff5d903dc
--- /dev/null
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/IngressResolutionResult.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of resolving a VKC's {@code spec.ingresses.ingressRef} and {@code spec.ingresses.proxyRef}
+ * @param ingressResolutionResult the resolved KafkaProxyIngress result
+ * @param proxyResolutionResult the resolved KafkaProxy result
+ */
+public record IngressResolutionResult(ResolutionResult<KafkaProxyIngress> ingressResolutionResult,
+                                      @Nullable ResolutionResult<KafkaProxy> proxyResolutionResult) {}
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ProxyResolutionResult.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ProxyResolutionResult.java
index f1d82d9156..882265781b 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ProxyResolutionResult.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ProxyResolutionResult.java
@@ -6,65 +6,43 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
-import java.util.Collection;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
-import io.kroxylicious.kubernetes.api.common.FilterRef;
 import io.kroxylicious.kubernetes.api.common.LocalRef;
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
-import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
 import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 import static java.util.Comparator.comparing;
 
 /**
- * The result of a deep resolution of the dependencies of a single KafkaProxy. Contains all
+ * The result of a deep resolution of the transitive references of a single KafkaProxy. Contains all
  * Filters, KafkaProxyIngresses and KafkaServices that were successfully resolved. It also
- * describes which dependencies could not be resolved per VirtualKafkaCluster.
+ * describes which referents could not be resolved per VirtualKafkaCluster.
  */
 public class ProxyResolutionResult {
-    private final Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters;
-    private final Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> kafkaProxyIngresses;
-    private final Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs;
     private final Set<ClusterResolutionResult> clusterResolutionResults;
 
-    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
-        return clusterResolutionResults.stream().filter(result -> result.cluster().equals(cluster)).findFirst();
-    }
-
-    ProxyResolutionResult(Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters,
-                          Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> kafkaProxyIngresses,
-                          Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs,
-                          Set<ClusterResolutionResult> clusterResolutionResults) {
-        Objects.requireNonNull(filters);
-        Objects.requireNonNull(kafkaProxyIngresses);
-        Objects.requireNonNull(kafkaServiceRefs);
+    ProxyResolutionResult(Set<ClusterResolutionResult> clusterResolutionResults) {
         Objects.requireNonNull(clusterResolutionResults);
-        this.filters = filters;
-        this.kafkaProxyIngresses = kafkaProxyIngresses;
-        this.kafkaServiceRefs = kafkaServiceRefs;
         this.clusterResolutionResults = clusterResolutionResults;
     }
 
     /**
-     * Get all VirtualKafkaClusters that could have all their dependencies resolved sorted by the VirtualKafkaCluster's metadata.name
+     * Get all VirtualKafkaClusters that could have all their transitive references resolved, sorted by the VirtualKafkaCluster's metadata.name
      * @return non-null list of VirtualKafkaClusters sorted by metadata.name
      */
     public List<VirtualKafkaCluster> fullyResolvedClustersInNameOrder() {
-        return clustersSatisfying(ClusterResolutionResult::isFullyResolved);
+        return clustersSatisfying(
+                clusterResolutionResult -> clusterResolutionResult.allReferentsFullyResolved() && !ResourcesUtil.hasResolvedRefsFalseCondition(
+                        clusterResolutionResult.cluster()));
     }
 
     /**
-     * Get all VirtualKafkaClusters, even if they have dangling dependencies, sorted by the VirtualKafkaCluster's metadata.name
+     * Get all VirtualKafkaClusters, even if they have dangling references, sorted by the VirtualKafkaCluster's metadata.name
      * @return non-null list of VirtualKafkaClusters sorted by metadata.name
      */
     public List<VirtualKafkaCluster> allClustersInNameOrder() {
@@ -84,64 +62,29 @@ public Set<ClusterResolutionResult> clusterResolutionResults() {
         return clusterResolutionResults;
     }
 
-    /**
-     * Get all KafkaProxyIngresses
-     * @return all KafkaProxyIngresses
-     */
-    public Set<KafkaProxyIngress> ingresses() {
-        return new HashSet<>(kafkaProxyIngresses.values());
-    }
-
-    /**
-     * Get KafkaProxyIngress for this reference
-     * @param localRef reference
-     * @return optional containing ingress if present, else empty
-     */
-    public Optional<KafkaProxyIngress> ingress(LocalRef<KafkaProxyIngress> localRef) {
-        Objects.requireNonNull(localRef);
-        return Optional.ofNullable(kafkaProxyIngresses.get(localRef));
-    }
-
-    /**
-     * Get the resolved KafkaService for a cluster
-     * @return optional containing the cluster ref if resolved, else empty
-     */
-    public Optional<KafkaService> kafkaServiceRef(VirtualKafkaCluster cluster) {
-        var ref = cluster.getSpec().getTargetKafkaServiceRef();
-        return Optional.ofNullable(kafkaServiceRefs.get(ref));
-    }
-
-    /**
-     * Get all resolved Filters
-     * @return filters
-     */
-    public Collection<KafkaProtocolFilter> filters() {
-        return filters.values();
-    }
-
-    /**
-     * Get the resolved GenericKubernetesResource for a filterRef
-     * @return optional containing the resource if resolved, else empty
-     */
-    public Optional<KafkaProtocolFilter> filter(FilterRef filterRef) {
-        return filters().stream()
-                .filter(filterResource -> {
-                    String apiVersion = filterResource.getApiVersion();
-                    var filterResourceGroup = apiVersion.substring(0, apiVersion.indexOf("/"));
-                    return filterResourceGroup.equals(filterRef.getGroup())
-                            && filterResource.getKind().equals(filterRef.getKind())
-                            && ResourcesUtil.name(filterResource).equals(filterRef.getName());
-                })
-                .findFirst();
-    }
-
     public boolean allReferentsHaveFreshStatus() {
-        return clusterResolutionResults.stream().allMatch(ClusterResolutionResult::allReferentsHaveFreshStatus);
+        return clusterResolutionResults.stream().allMatch(
+                clusterResolutionResult -> clusterResolutionResult.allResolvedReferents()
+                        .allMatch(ResourcesUtil::isStatusFresh)
+                        && ResourcesUtil.isStatusFresh(
+                                clusterResolutionResult.cluster()));
     }
 
     // using wildcards intentionally
     @SuppressWarnings("java:S1452")
     public Stream<LocalRef<?>> allReferentsWithStaleStatus() {
-        return clusterResolutionResults.stream().flatMap(ClusterResolutionResult::findReferentsWithStaleStatus);
+        return clusterResolutionResults.stream().flatMap(clusterResolutionResult -> {
+            Stream<LocalRef<?>> referentsWithStaleStatus = clusterResolutionResult.allResolvedReferents()
+                    .filter(ResourcesUtil.isStatusStale())
+                    .map(ResourcesUtil::toLocalRef);
+            Stream<LocalRef<?>> clusterWithStaleStatus = !ResourcesUtil.isStatusFresh(clusterResolutionResult.cluster())
+                    ? Stream.of(ResourcesUtil.toLocalRef(clusterResolutionResult.cluster()))
+                    : Stream.of();
+            return Stream.concat(referentsWithStaleStatus, clusterWithStaleStatus);
+        });
+    }
+
+    public Stream<ClusterResolutionResult> allResolutionResultsInClusterNameOrder() {
+        return clusterResolutionResults.stream().sorted(comparing(r -> ResourcesUtil.name(r.cluster())));
     }
 }
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
new file mode 100644
index 0000000000..bac281a008
--- /dev/null
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import java.util.Optional;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
+import io.kroxylicious.kubernetes.api.common.LocalRef;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of attempting to resolve a reference
+ *
+ * @param <T> the referent type
+ * @param referrer the referring resource represented as a LocalRef
+ * @param reference the reference we attempted to resolve
+ */
+public record ResolutionResult<T extends HasMetadata>(LocalRef<?> referrer, LocalRef<T> reference, @Nullable T referentNew) {
+    /**
+     * If the resolution fails because we cannot locate a referent for a reference, we call this a dangling reference
+     * @return true iff no referent was found for this reference
+     */
+    boolean dangling() {
+        return referentNew == null;
+    }
+
+    public T referentResource() {
+        return maybeReferentResource().orElseThrow(() -> new NullPointerException("Referent resource for " + reference + " is null"));
+    }
+
+    public Optional<T> maybeReferentResource() {
+        return Optional.ofNullable(referentNew);
+    }
+}
diff --git a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/ResourcesUtilTest.java b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/ResourcesUtilTest.java
index a807c1f31a..950fd074f8 100644
--- a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/ResourcesUtilTest.java
+++ b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/ResourcesUtilTest.java
@@ -6,6 +6,7 @@
 
 package io.kroxylicious.kubernetes.operator;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -33,6 +34,8 @@
 import io.javaoperatorsdk.operator.processing.event.ResourceID;
 
 import io.kroxylicious.kubernetes.api.common.AnyLocalRefBuilder;
+import io.kroxylicious.kubernetes.api.common.Condition;
+import io.kroxylicious.kubernetes.api.common.ConditionBuilder;
 import io.kroxylicious.kubernetes.api.common.FilterRefBuilder;
 import io.kroxylicious.kubernetes.api.common.IngressRefBuilder;
 import io.kroxylicious.kubernetes.api.common.KafkaServiceRefBuilder;
@@ -48,6 +51,8 @@
 import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
 import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterBuilder;
 
+import edu.umd.cs.findbugs.annotations.NonNull;
+
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.findOnlyResourceNamed;
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.toByNameMap;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -393,6 +398,87 @@ void isStatusFresh_VirtualCluster(VirtualKafkaCluster cluster, boolean isReconci
         assertThat(ResourcesUtil.isStatusFresh(cluster)).isEqualTo(isReconciled);
     }
 
+    public static Stream<Arguments> hasResolvedRefsFalse() {
+        Condition resolvedRefsTrueCondition = resolvedRefsCondition(Condition.Status.TRUE);
+        Condition resolvedRefsFalseCondition = resolvedRefsCondition(Condition.Status.FALSE);
+        VirtualKafkaCluster vkcNoStatus = new VirtualKafkaClusterBuilder().build();
+        VirtualKafkaCluster vkcNoConditions = new VirtualKafkaClusterBuilder().withNewStatus().endStatus().build();
+        VirtualKafkaCluster vkcEmptyConditions = new VirtualKafkaClusterBuilder().withNewStatus().withConditions(List.of()).endStatus().build();
+        VirtualKafkaCluster vkcResolvedRefsTrue = new VirtualKafkaClusterBuilder().withNewStatus().withConditions(
+                resolvedRefsTrueCondition).endStatus().build();
+        VirtualKafkaCluster vkcResolvedRefsFalse = new VirtualKafkaClusterBuilder().withNewStatus().withConditions(
+                resolvedRefsFalseCondition).endStatus().build();
+
+        KafkaService ksNoStatus = new KafkaServiceBuilder().build();
+        KafkaService ksNoConditions = new KafkaServiceBuilder().withNewStatus().endStatus().build();
+        KafkaService ksEmptyConditions = new KafkaServiceBuilder().withNewStatus().withConditions(List.of()).endStatus().build();
+        KafkaService ksResolvedRefsTrue = new KafkaServiceBuilder().withNewStatus().withConditions(
+                resolvedRefsTrueCondition).endStatus().build();
+        KafkaService ksResolvedRefsFalse = new KafkaServiceBuilder().withNewStatus().withConditions(
+                resolvedRefsFalseCondition).endStatus().build();
+
+        KafkaProtocolFilter kpfNoStatus = new KafkaProtocolFilterBuilder().build();
+        KafkaProtocolFilter kpfNoConditions = new KafkaProtocolFilterBuilder().withNewStatus().endStatus().build();
+        KafkaProtocolFilter kpfEmptyConditions = new KafkaProtocolFilterBuilder().withNewStatus().withConditions(List.of()).endStatus().build();
+        KafkaProtocolFilter kpfResolvedRefsTrue = new KafkaProtocolFilterBuilder().withNewStatus().withConditions(
+                resolvedRefsTrueCondition).endStatus().build();
+        KafkaProtocolFilter kpfResolvedRefsFalse = new KafkaProtocolFilterBuilder().withNewStatus().withConditions(
+                resolvedRefsFalseCondition).endStatus().build();
+
+        KafkaProxyIngress kpiNoStatus = new KafkaProxyIngressBuilder().build();
+        KafkaProxyIngress kpiNoConditions = new KafkaProxyIngressBuilder().withNewStatus().endStatus().build();
+        KafkaProxyIngress kpiEmptyConditions = new KafkaProxyIngressBuilder().withNewStatus().withConditions(List.of()).endStatus().build();
+        KafkaProxyIngress kpiResolvedRefsTrue = new KafkaProxyIngressBuilder().withNewStatus().withConditions(
+                resolvedRefsTrueCondition).endStatus().build();
+        KafkaProxyIngress kpiResolvedRefsFalse = new KafkaProxyIngressBuilder().withNewStatus().withConditions(
+                resolvedRefsFalseCondition).endStatus().build();
+
+        return Stream.of(Arguments.argumentSet("virtualkafkacluster - no status", vkcNoStatus, false),
+                Arguments.argumentSet("virtualkafkacluster - no conditions on status", vkcNoConditions, false),
+                Arguments.argumentSet("virtualkafkacluster - empty conditions on status", vkcEmptyConditions, false),
+                Arguments.argumentSet("virtualkafkacluster - resolved refs true", vkcResolvedRefsTrue, false),
+                Arguments.argumentSet("virtualkafkacluster - resolved refs false", vkcResolvedRefsFalse, true),
+                Arguments.argumentSet("kafkaservice - no status", ksNoStatus, false),
+                Arguments.argumentSet("kafkaservice - no conditions on status", ksNoConditions, false),
+                Arguments.argumentSet("kafkaservice - empty conditions on status", ksEmptyConditions, false),
+                Arguments.argumentSet("kafkaservice - resolved refs true", ksResolvedRefsTrue, false),
+                Arguments.argumentSet("kafkaservice - resolved refs false", ksResolvedRefsFalse, true),
+                Arguments.argumentSet("kafkaprotocolfilter - no status", kpfNoStatus, false),
+                Arguments.argumentSet("kafkaprotocolfilter - no conditions on status", kpfNoConditions, false),
+                Arguments.argumentSet("kafkaprotocolfilter - empty conditions on status", kpfEmptyConditions, false),
+                Arguments.argumentSet("kafkaprotocolfilter - resolved refs true", kpfResolvedRefsTrue, false),
+                Arguments.argumentSet("kafkaprotocolfilter - resolved refs false", kpfResolvedRefsFalse, true),
+                Arguments.argumentSet("kafkaproxyingress - no status", kpiNoStatus, false),
+                Arguments.argumentSet("kafkaproxyingress - no conditions on status", kpiNoConditions, false),
+                Arguments.argumentSet("kafkaproxyingress - empty conditions on status", kpiEmptyConditions, false),
+                Arguments.argumentSet("kafkaproxyingress - resolved refs true", kpiResolvedRefsTrue, false),
+                Arguments.argumentSet("kafkaproxyingress - resolved refs false", kpiResolvedRefsFalse, true));
+    }
+
+    @NonNull
+    private static Condition resolvedRefsCondition(Condition.Status status) {
+        return new ConditionBuilder()
+                .withLastTransitionTime(Instant.EPOCH)
+                .withMessage("message")
+                .withObservedGeneration(1L)
+                .withType(Condition.Type.ResolvedRefs)
+                .withStatus(
+                        status)
+                .withReason("reason").build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void hasResolvedRefsFalse(HasMetadata cluster, boolean hasResolvedRefsFalse) {
+        assertThat(ResourcesUtil.hasResolvedRefsFalseCondition(cluster)).isEqualTo(hasResolvedRefsFalse);
+    }
+
+    @Test
+    void hasResolvedRefsFalseThrowsWhenResourceDoesntUseResolveRefs() {
+        assertThatThrownBy(() -> ResourcesUtil.hasResolvedRefsFalseCondition(new KafkaProxy())).isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("Resource kind 'KafkaProxy' does not use ResolveRefs conditions");
+    }
+
     public static Stream<Arguments> isStatusFresh_KafkaProxyIngress() {
         KafkaProxyIngress observedGenerationEqualsMetadataGeneration = new KafkaProxyIngressBuilder()
                 .withNewMetadata()
@@ -463,6 +549,41 @@ void isStatusFresh_KafkaProtocolFilter(KafkaProtocolFilter filter, boolean isRec
         assertThat(ResourcesUtil.isStatusFresh(filter)).isEqualTo(isReconciled);
     }
 
+    public static Stream<Arguments> isStatusFresh_KafkaProxy() {
+        KafkaProxy observedGenerationEqualsMetadataGeneration = new KafkaProxyBuilder()
+                .withNewMetadata()
+                .withGeneration(1L)
+                .endMetadata()
+                .editStatus().withObservedGeneration(1L)
+                .endStatus().build();
+        KafkaProxy observedGenerationLessThanMetadataGeneration = new KafkaProxyBuilder()
+                .withNewMetadata()
+                .withGeneration(2L)
+                .endMetadata()
+                .editStatus().withObservedGeneration(1L)
+                .endStatus().build();
+        KafkaProxy observedGenerationNull = new KafkaProxyBuilder()
+                .withNewMetadata()
+                .withGeneration(2L)
+                .endMetadata()
+                .editStatus().withObservedGeneration(null)
+                .endStatus().build();
+        KafkaProxy statusNull = new KafkaProxyBuilder()
+                .withNewMetadata()
+                .withGeneration(2L)
+                .endMetadata().build();
+        return Stream.of(Arguments.argumentSet("observed generation equals metadata generation", observedGenerationEqualsMetadataGeneration, true),
+                Arguments.argumentSet("observed generation less than metadata generation", observedGenerationLessThanMetadataGeneration, false),
+                Arguments.argumentSet("observed generation null", observedGenerationNull, false),
+                Arguments.argumentSet("status null", statusNull, false));
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void isStatusFresh_KafkaProxy(KafkaProxy proxy, boolean isReconciled) {
+        assertThat(ResourcesUtil.isStatusFresh(proxy)).isEqualTo(isReconciled);
+    }
+
     public static Stream<Arguments> isStatusFresh_KafkaService() {
         KafkaService observedGenerationEqualsMetadataGeneration = new KafkaServiceBuilder()
                 .withNewMetadata()
diff --git a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverTest.java b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverTest.java
index 3e70213603..e4236ccd4b 100644
--- a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverTest.java
+++ b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverTest.java
@@ -79,10 +79,7 @@ void resolveProxyRefsWhenNoEntitiesReferenceProxy() {
         // then
         assertThat(resolutionResult.allClustersInNameOrder()).isEmpty();
         assertThat(resolutionResult.clusterResolutionResults()).isEmpty();
-        assertThat(resolutionResult.ingresses()).isEmpty();
         assertThat(resolutionResult.fullyResolvedClustersInNameOrder()).isEmpty();
-        assertThat(resolutionResult.filter(filterRef("c"))).isEmpty();
-        assertThat(resolutionResult.filters()).isEmpty();
     }
 
     @Test
@@ -95,8 +92,8 @@ void resolveClusterRefsWithManyReferencesDangling() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
-        assertThat(clusterResolutionResult.danglingReferences()).containsExactlyInAnyOrder(danglingReference(cluster, getProxyRef(PROXY_NAME)),
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allDanglingReferences()).containsExactlyInAnyOrder(danglingReference(cluster, getProxyRef(PROXY_NAME)),
                 danglingReference(cluster, getKafkaServiceRef("cluster")),
                 danglingReference(cluster, ingress("ingressRef").getIngressRef()),
                 danglingReference(cluster, filterRef("missing")));
@@ -115,8 +112,9 @@ void resolveProxyRefsWithNullFiltersOnVirtualCluster() {
         // then
         assertThat(resolutionResult.allClustersInNameOrder()).containsExactly(cluster);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.danglingReferences()).isEmpty();
+        assertThat(ResourcesUtil.hasResolvedRefsFalseCondition(onlyResult.cluster())).isFalse();
+        assertThat(onlyResult.allReferentsFullyResolved()).isTrue();
+        assertThat(onlyResult.allDanglingReferences()).isEmpty();
     }
 
     @Test
@@ -131,8 +129,9 @@ void resolveClusterRefsWithNullFilters() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
-        assertThat(clusterResolutionResult.danglingReferences()).isEmpty();
+        assertThat(ResourcesUtil.hasResolvedRefsFalseCondition(clusterResolutionResult.cluster())).isFalse();
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isTrue();
+        assertThat(clusterResolutionResult.allDanglingReferences()).isEmpty();
     }
 
     @Test
@@ -149,8 +148,8 @@ void resolveProxyRefsWithResolvedRefsFalseCondition() {
         // then
         assertThat(resolutionResult.allClustersInNameOrder()).containsExactly(cluster);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.findResourcesWithResolvedRefsFalse("VirtualKafkaCluster")).containsExactly(ResourcesUtil.toLocalRef(cluster));
+        assertThat(onlyResult.allReferentsFullyResolved()).isTrue();
+        assertThat(ResourcesUtil.hasResolvedRefsFalseCondition(onlyResult.cluster())).isTrue();
     }
 
     @Test
@@ -167,9 +166,9 @@ void resolveProxyRefsWithStaleKafkaServiceStatus() {
         // then
         assertThat(resolutionResult.allClustersInNameOrder()).containsExactly(cluster);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.allReferentsHaveFreshStatus()).isFalse();
-        assertThat(onlyResult.findReferentsWithStaleStatus()).containsExactly(ResourcesUtil.toLocalRef(kafkaService));
+        assertThat(onlyResult.allResolvedReferents().allMatch(ResourcesUtil.isStatusStale())).isTrue();
+        assertThat(onlyResult.allResolvedReferents().filter(ResourcesUtil.isStatusStale()).<LocalRef<?>> map(ResourcesUtil::toLocalRef)).containsExactly(
+                ResourcesUtil.toLocalRef(kafkaService));
     }
 
     @Test
@@ -187,9 +186,8 @@ void resolveProxyRefsWithStaleKafkaProxyIngressStatus() {
         // then
         assertThat(resolutionResult.allClustersInNameOrder()).containsExactly(cluster);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.allReferentsHaveFreshStatus()).isFalse();
-        assertThat(onlyResult.findReferentsWithStaleStatus()).containsExactly(ResourcesUtil.toLocalRef(ingress));
+        assertThat(onlyResult.allResolvedReferents().allMatch(ResourcesUtil.isStatusStale())).isFalse();
+        assertThat(onlyResult.allResolvedReferents().filter(ResourcesUtil.isStatusStale())).containsExactly(ingress);
     }
 
     @Test
@@ -207,13 +205,12 @@ void resolveProxyRefsWithStaleKafkaProtocolFilterStatus() {
         // then
         assertThat(resolutionResult.allClustersInNameOrder()).containsExactly(cluster);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.allReferentsHaveFreshStatus()).isFalse();
-        assertThat(onlyResult.findReferentsWithStaleStatus()).containsExactly(ResourcesUtil.toLocalRef(filter));
+        assertThat(onlyResult.allResolvedReferents().allMatch(ResourcesUtil.isStatusStale())).isFalse();
+        assertThat(onlyResult.allResolvedReferents().filter(ResourcesUtil.isStatusStale())).containsExactly(filter);
     }
 
     @Test
-    void resolveProxyRefsWitStaleVirtualKafkaClusterStatus() {
+    void resolveProxyRefsWithStaleVirtualKafkaClusterStatus() {
         // given
         givenKafkaServicesInContext(kafkaService("cluster"));
         long oldGeneration = 1L;
@@ -230,16 +227,33 @@ void resolveProxyRefsWitStaleVirtualKafkaClusterStatus() {
         // then
         assertThat(resolutionResult.allClustersInNameOrder()).containsExactly(cluster);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.allReferentsHaveFreshStatus()).isFalse();
-        assertThat(onlyResult.findReferentsWithStaleStatus()).containsExactly(ResourcesUtil.toLocalRef(cluster));
+        // no referents are stale, only the cluster
+        assertThat(onlyResult.allResolvedReferents().noneMatch(ResourcesUtil.isStatusStale())).isTrue();
+        assertThat(!ResourcesUtil.isStatusFresh(onlyResult.cluster())).isTrue();
     }
 
-    // we want to ensure that when we are reconciling a VirtualKafkaCluster than we do not consider the status of that
-    // cluster. Ie if a previous reconciliation sets the cluster to ResolvedRefs: False, we do not want that status to
-    // be considered when calculating a fresh status.
     @Test
-    void resolveClusterShouldIgnoreConditionsForTargetCluster() {
+    void resolveClusterRefsWithStaleVirtualKafkaClusterStatus() {
+        // given
+        givenKafkaServicesInContext(kafkaService("cluster"));
+        long oldGeneration = 1L;
+        long generation = 2L;
+        VirtualKafkaCluster cluster = virtualCluster(null, "cluster", List.of(), getProxyRef(PROXY_NAME))
+                .edit().editMetadata().withGeneration(generation).endMetadata()
+                .editStatus().withObservedGeneration(oldGeneration).endStatus()
+                .build();
+
+        // when
+        ClusterResolutionResult resolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(resolutionResult.cluster()).isEqualTo(cluster);
+        assertThat(resolutionResult.allResolvedReferents().noneMatch(ResourcesUtil.isStatusStale())).isTrue();
+        assertThat(!ResourcesUtil.isStatusFresh(resolutionResult.cluster())).isTrue();
+    }
+
+    @Test
+    void resolveClusterRefsWhenClusterHasResolvedRefsFalse() {
         // given
         givenKafkaServicesInContext(kafkaService("cluster"));
         VirtualKafkaCluster cluster = virtualCluster(null, "cluster", List.of(), getProxyRef(PROXY_NAME))
@@ -251,7 +265,13 @@ void resolveClusterShouldIgnoreConditionsForTargetCluster() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
+
+        // we want to ensure that when we are reconciling a VirtualKafkaCluster than we do not consider the status of that
+        // cluster. Ie if a previous reconciliation sets the cluster to ResolvedRefs: False, we do not want that status to
+        // be considered when calculating a fresh status.
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isTrue();
+        assertThat(ResourcesUtil.hasResolvedRefsFalseCondition(clusterResolutionResult.cluster())).isTrue();
+        assertThat(clusterResolutionResult.allResolvedReferents().anyMatch(ResourcesUtil::hasResolvedRefsFalseCondition)).isFalse();
     }
 
     @Test
@@ -267,13 +287,14 @@ void resolveProxyRefsWithFilterHavingResolvedRefsFalseCondition() {
         ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
 
         // then
-        assertThat(resolutionResult.filter(filterRef("filterName"))).contains(filter);
-        assertThat(resolutionResult.filters()).containsExactly(filter);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.findResourcesWithResolvedRefsFalse()).containsExactly(filterRef("filterName"));
-        assertThat(onlyResult.findResourcesWithResolvedRefsFalse(HasMetadata.getKind(KafkaProtocolFilter.class)))
-                .containsExactly(filterRef("filterName"));
+        assertThat(onlyResult.allReferentsFullyResolved()).isFalse();
+        assertThat(onlyResult.allResolvedReferents().filter(ResourcesUtil::hasResolvedRefsFalseCondition)).containsExactly(filter);
+        String referentKind = HasMetadata.getKind(KafkaProtocolFilter.class);
+        assertThat(onlyResult.allResolvedReferents().filter(ResourcesUtil.hasResolvedRefsFalseCondition().and(ResourcesUtil.hasKind(referentKind))))
+                .containsExactly(filter);
+
+        assertThat(onlyResult.allResolvedReferents().anyMatch(ResourcesUtil::hasResolvedRefsFalseCondition)).isTrue();
     }
 
     @Test
@@ -291,9 +312,13 @@ void resolveClusterRefsWithFilterHavingResolvedRefsFalseCondition() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
-        assertThat(clusterResolutionResult.findResourcesWithResolvedRefsFalse()).containsExactly(filterRef("filterName"));
-        assertThat(clusterResolutionResult.findResourcesWithResolvedRefsFalse(HasMetadata.getKind(KafkaProtocolFilter.class))).containsExactly(filterRef("filterName"));
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allResolvedReferents().filter(ResourcesUtil.isStatusStale())).containsExactly(filter);
+        String referentKind = HasMetadata.getKind(KafkaProtocolFilter.class);
+        assertThat(clusterResolutionResult.allResolvedReferents().filter(ResourcesUtil.hasResolvedRefsFalseCondition().and(ResourcesUtil.hasKind(referentKind))))
+                .containsExactly(
+                        filter);
+        assertThat(clusterResolutionResult.allResolvedReferents().anyMatch(ResourcesUtil::hasResolvedRefsFalseCondition)).isTrue();
     }
 
     private static @NonNull Condition resolvedRefsFalse() {
@@ -320,10 +345,13 @@ void resolveProxyRefsWithServiceHavingResolvedRefsFalseCondition() {
 
         // then
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.findResourcesWithResolvedRefsFalse()).containsExactly(getKafkaServiceRef("cluster"));
-        assertThat(onlyResult.findResourcesWithResolvedRefsFalse(HasMetadata.getKind(KafkaService.class)))
-                .containsExactly(getKafkaServiceRef("cluster"));
+        assertThat(onlyResult.allReferentsFullyResolved()).isFalse();
+        assertThat(onlyResult.allResolvedReferents().filter(ResourcesUtil::hasResolvedRefsFalseCondition))
+                .containsExactly(service);
+        String referentKind = HasMetadata.getKind(KafkaService.class);
+        assertThat(onlyResult.allResolvedReferents().filter(ResourcesUtil.hasResolvedRefsFalseCondition().and(ResourcesUtil.hasKind(referentKind))))
+                .containsExactly(service);
+        assertThat(onlyResult.allResolvedReferents().anyMatch(ResourcesUtil::hasResolvedRefsFalseCondition)).isTrue();
     }
 
     @Test
@@ -341,9 +369,13 @@ void resolveClusterRefsWithServiceHavingResolvedRefsFalseCondition() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
-        assertThat(clusterResolutionResult.findResourcesWithResolvedRefsFalse()).containsExactly(getKafkaServiceRef("cluster"));
-        assertThat(clusterResolutionResult.findResourcesWithResolvedRefsFalse(HasMetadata.getKind(KafkaService.class))).containsExactly(getKafkaServiceRef("cluster"));
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allResolvedReferents().filter(ResourcesUtil::hasResolvedRefsFalseCondition))
+                .containsExactly(service);
+        String referentKind = HasMetadata.getKind(KafkaService.class);
+        assertThat(clusterResolutionResult.allResolvedReferents().filter(ResourcesUtil.hasResolvedRefsFalseCondition().and(ResourcesUtil.hasKind(referentKind))))
+                .containsExactly(service);
+        assertThat(clusterResolutionResult.allResolvedReferents().anyMatch(ResourcesUtil::hasResolvedRefsFalseCondition)).isTrue();
     }
 
     @Test
@@ -362,10 +394,13 @@ void resolveProxyRefsWithIngressHavingResolvedRefsFalseCondition() {
 
         // then
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.findResourcesWithResolvedRefsFalse()).containsExactly(ingress("ingress").getIngressRef());
-        assertThat(onlyResult.findResourcesWithResolvedRefsFalse(HasMetadata.getKind(KafkaProxyIngress.class)))
-                .containsExactly(ingress("ingress").getIngressRef());
+        assertThat(onlyResult.allReferentsFullyResolved()).isFalse();
+        assertThat(onlyResult.allResolvedReferents().filter(ResourcesUtil::hasResolvedRefsFalseCondition))
+                .containsExactly(ingress);
+        String referentKind = HasMetadata.getKind(KafkaProxyIngress.class);
+        assertThat(onlyResult.allResolvedReferents().filter(ResourcesUtil.hasResolvedRefsFalseCondition().and(ResourcesUtil.hasKind(referentKind))))
+                .containsExactly(ingress);
+        assertThat(onlyResult.allResolvedReferents().anyMatch(ResourcesUtil::hasResolvedRefsFalseCondition)).isTrue();
     }
 
     @Test
@@ -384,10 +419,13 @@ void resolveClusterRefsWithIngressHavingResolvedRefsFalseCondition() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
-        assertThat(clusterResolutionResult.findResourcesWithResolvedRefsFalse()).containsExactly(ingress("ingress").getIngressRef());
-        assertThat(clusterResolutionResult.findResourcesWithResolvedRefsFalse(HasMetadata.getKind(KafkaProxyIngress.class)))
-                .containsExactly(ingress("ingress").getIngressRef());
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allResolvedReferents().filter(ResourcesUtil::hasResolvedRefsFalseCondition))
+                .containsExactly(ingress);
+        String referentKind = HasMetadata.getKind(KafkaProxyIngress.class);
+        assertThat(clusterResolutionResult.allResolvedReferents().filter(ResourcesUtil.hasResolvedRefsFalseCondition().and(ResourcesUtil.hasKind(referentKind))))
+                .containsExactly(ingress);
+        assertThat(clusterResolutionResult.allResolvedReferents().anyMatch(ResourcesUtil::hasResolvedRefsFalseCondition)).isTrue();
     }
 
     @Test
@@ -401,11 +439,9 @@ void resolveProxyRefsWithSingleDanglingFilterRef() {
         ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
 
         // then
-        assertThat(resolutionResult.filter(filterRef("another"))).isEmpty();
-        assertThat(resolutionResult.filters()).isEmpty();
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.danglingReferences()).containsExactly(danglingReference(cluster, filterRef("another")));
+        assertThat(onlyResult.allReferentsFullyResolved()).isFalse();
+        assertThat(onlyResult.allDanglingReferences()).containsExactly(danglingReference(cluster, filterRef("another")));
     }
 
     @Test
@@ -420,8 +456,8 @@ void resolveClusterRefsWithSingleDanglingFilterRef() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
-        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, filterRef("another")));
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allDanglingReferences()).containsExactly(danglingReference(cluster, filterRef("another")));
     }
 
     @Test
@@ -437,11 +473,12 @@ void resolveProxyRefsWithSingleResolvedFilter() {
         ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
 
         // then
-        assertThat(resolutionResult.filter(filterRef("filterName"))).contains(filter);
-        assertThat(resolutionResult.filters()).containsExactly(filter);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.danglingReferences()).isEmpty();
+        assertThat(onlyResult.allReferentsFullyResolved()).isTrue();
+        assertThat(onlyResult.allDanglingReferences()).isEmpty();
+        assertThat(onlyResult.filterResolutionResults()).singleElement().satisfies(result -> {
+            assertThat(result.referentResource()).isEqualTo(filter);
+        });
     }
 
     @Test
@@ -458,8 +495,11 @@ void resolveClusterRefsWithSingleResolvedFilter() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
-        assertThat(clusterResolutionResult.danglingReferences()).isEmpty();
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isTrue();
+        assertThat(clusterResolutionResult.allDanglingReferences()).isEmpty();
+        assertThat(clusterResolutionResult.filterResolutionResults()).singleElement().satisfies(result -> {
+            assertThat(result.referentResource()).isEqualTo(filter);
+        });
     }
 
     @Test
@@ -477,12 +517,10 @@ void resolveProxyRefsWithMultipleResolvedFilter() {
         ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
 
         // then
-        assertThat(resolutionResult.filter(filterRef("filterName"))).contains(filter);
-        assertThat(resolutionResult.filter(filterRef("filterName2"))).contains(filter2);
-        assertThat(resolutionResult.filters()).containsExactlyInAnyOrder(filter, filter2);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.danglingReferences()).isEmpty();
+        assertThat(onlyResult.allReferentsFullyResolved()).isTrue();
+        assertThat(onlyResult.allDanglingReferences()).isEmpty();
+        assertThat(onlyResult.filterResolutionResults().stream().map(ResolutionResult::referentResource)).containsExactly(filter, filter2);
     }
 
     @Test
@@ -501,8 +539,8 @@ void resolveClusterRefsWithMultipleResolvedFilter() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
-        assertThat(clusterResolutionResult.danglingReferences()).isEmpty();
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isTrue();
+        assertThat(clusterResolutionResult.allDanglingReferences()).isEmpty();
     }
 
     @Test
@@ -519,12 +557,9 @@ void resolveProxyRefsWithSubsetOfFiltersReferenced() {
         ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
 
         // then
-        assertThat(resolutionResult.filter(filterRef("filterName"))).contains(filter);
-        assertThat(resolutionResult.filter(filterRef("filterName2"))).isEmpty();
-        assertThat(resolutionResult.filters()).containsExactlyInAnyOrder(filter);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.danglingReferences()).containsExactly(danglingReference(cluster, filterRef("filterName2")));
+        assertThat(onlyResult.allReferentsFullyResolved()).isFalse();
+        assertThat(onlyResult.allDanglingReferences()).containsExactly(danglingReference(cluster, filterRef("filterName2")));
     }
 
     @Test
@@ -542,8 +577,8 @@ void resolveClusterRefsWithSubsetOfFiltersReferenced() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
-        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, filterRef("filterName2")));
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allDanglingReferences()).containsExactly(danglingReference(cluster, filterRef("filterName2")));
     }
 
     @Test
@@ -557,10 +592,9 @@ void resolveProxyRefsWithDanglingIngressRef() {
         ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
 
         // then
-        assertThat(resolutionResult.ingresses()).isEmpty();
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.danglingReferences()).containsExactly(danglingReference(cluster, ingress("ingressMissing").getIngressRef()));
+        assertThat(onlyResult.allReferentsFullyResolved()).isFalse();
+        assertThat(onlyResult.allDanglingReferences()).containsExactly(danglingReference(cluster, ingress("ingressMissing").getIngressRef()));
     }
 
     @Test
@@ -575,8 +609,9 @@ void resolveClusterRefsWithDanglingIngressRef() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
-        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, ingress("ingressMissing").getIngressRef()));
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allDanglingReferences())
+                .containsExactly(danglingReference(cluster, ingress("ingressMissing").getIngressRef()));
     }
 
     @Test
@@ -589,10 +624,9 @@ void resolveProxyRefsWithDanglingKafkaServiceRef() {
         ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
 
         // then
-        assertThat(resolutionResult.ingresses()).isEmpty();
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.danglingReferences()).containsExactly(danglingReference(cluster, getKafkaServiceRef("missing")));
+        assertThat(onlyResult.allReferentsFullyResolved()).isFalse();
+        assertThat(onlyResult.allDanglingReferences()).containsExactly(danglingReference(cluster, getKafkaServiceRef("missing")));
     }
 
     @Test
@@ -606,8 +640,8 @@ void resolveClusterRefsWithDanglingKafkaServiceRef() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
-        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, getKafkaServiceRef("missing")));
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allDanglingReferences()).containsExactly(danglingReference(cluster, getKafkaServiceRef("missing")));
     }
 
     @Test
@@ -623,10 +657,9 @@ void resolveProxyRefsWithSingleResolvedIngress() {
         ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
 
         // then
-        assertThat(resolutionResult.ingresses()).containsExactly(ingress);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.danglingReferences()).isEmpty();
+        assertThat(onlyResult.allReferentsFullyResolved()).isTrue();
+        assertThat(onlyResult.allDanglingReferences()).isEmpty();
     }
 
     @Test
@@ -643,8 +676,8 @@ void resolveClusterRefsWithSingleResolvedIngress() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
-        assertThat(clusterResolutionResult.danglingReferences()).isEmpty();
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isTrue();
+        assertThat(clusterResolutionResult.allDanglingReferences()).isEmpty();
     }
 
     @Test
@@ -662,10 +695,9 @@ void resolveProxyRefsWithMultipleResolvedIngresses() {
         ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
 
         // then
-        assertThat(resolutionResult.ingresses()).containsExactlyInAnyOrder(ingress, ingress2);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.danglingReferences()).isEmpty();
+        assertThat(onlyResult.allReferentsFullyResolved()).isTrue();
+        assertThat(onlyResult.allDanglingReferences()).isEmpty();
     }
 
     @Test
@@ -684,8 +716,8 @@ void resolveClusterRefsWithMultipleResolvedIngresses() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
-        assertThat(clusterResolutionResult.danglingReferences()).isEmpty();
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isTrue();
+        assertThat(clusterResolutionResult.allDanglingReferences()).isEmpty();
     }
 
     @Test
@@ -702,10 +734,9 @@ void resolveProxyRefsWithSubsetOfIngressesResolved() {
         ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
 
         // then
-        assertThat(resolutionResult.ingresses()).containsExactlyInAnyOrder(ingress);
         ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.danglingReferences()).containsExactly(danglingReference(cluster, ingress("ingress2").getIngressRef()));
+        assertThat(onlyResult.allReferentsFullyResolved()).isFalse();
+        assertThat(onlyResult.allDanglingReferences()).containsExactly(danglingReference(cluster, ingress("ingress2").getIngressRef()));
     }
 
     @Test
@@ -723,8 +754,8 @@ void resolveClusterRefsWithSubsetOfIngressesResolved() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
-        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, ingress("ingress2").getIngressRef()));
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allDanglingReferences()).containsExactly(danglingReference(cluster, ingress("ingress2").getIngressRef()));
     }
 
     @Test
@@ -742,9 +773,9 @@ void resolveClusterRefsWithIngressReferencingDifferentProxyThanCluster() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
         LocalRef<?> proxyRef = getProxyRef("another-proxy");
-        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(new DanglingReference(ResourcesUtil.toLocalRef(ingress), proxyRef));
+        assertThat(clusterResolutionResult.allDanglingReferences()).containsExactly(new DanglingReference(ResourcesUtil.toLocalRef(ingress), proxyRef));
     }
 
     @Test
@@ -762,8 +793,8 @@ void resolveClusterRefsWithDanglingProxyRef() {
         ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
 
         // then
-        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
-        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, getProxyRef("another-proxy")));
+        assertThat(clusterResolutionResult.allReferentsFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.allDanglingReferences()).containsExactly(danglingReference(cluster, getProxyRef("another-proxy")));
     }
 
     private @NonNull ClusterResolutionResult resolveClusterRefs(VirtualKafkaCluster cluster) {


Reviews:
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T21:37:20Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T22:48:09Z
user: login: tombentley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T00:42:56Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T01:29:04Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T01:30:54Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T01:39:05Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T01:44:50Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T01:45:26Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T01:50:34Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:07:20Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:08:29Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:15:59Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:16:16Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:19:22Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:34:57Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:37:24Z
user: login: tombentley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:38:55Z
user: login: tombentley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:46:59Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T02:57:16Z
user: login: tombentley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T03:13:58Z
user: login: k-wall
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T14:00:11Z
user: login: k-wall
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T14:19:16Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-11T21:53:00Z
user: login: SamBarker
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-11T22:01:41Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-11T22:04:18Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-11T22:05:48Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-11T22:17:04Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-11T22:49:38Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-11T23:06:23Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T03:47:41Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T03:54:24Z
user: login: SamBarker
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T04:00:17Z
user: login: tombentley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T04:03:32Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T04:03:35Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T04:12:37Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T04:21:20Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T04:23:31Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T04:25:33Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T04:33:17Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T21:45:59Z
user: login: SamBarker
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T21:54:35Z
user: login: SamBarker
body: A bunch of nits and stylistic thoughts but I think this could be merged as is and the tidying up happen out of band.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-13T02:02:52Z

Review comments:
diff_hunk: @@ -6,42 +6,57 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param resolutionScope
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults,
+                                      ResolutionScope resolutionScope) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: This is smelly, ClusterResolutionResult behaves differently in different contexts.

1. When we are resolving the resources in the VKC Reconciler, we do not want to consider the cluster's ResolvedRefs and staleness because we are reconciling that resource.
2. When we are resolving the resources in the Proxy Reconciler, we want to report a stale or ResolvedRefs false VKC as a problem

Maybe this is suggesting it's not a `ClusterResolutionResult` level problem, which could be changed to be purely about the VKC's Referents.

Modelling it on some object associated with the cluster feels right, like I don't want to ask the `ProxyResolutionResult` `isClusterStale(cluster)`. Maybe it could be a type hierarchy instead so that there's a ProxyClusterResolutionResult that wraps the referent one and adds these modifications.
created_at: 2025-05-07T21:37:20Z
author_association: MEMBER
diff_hunk: @@ -6,42 +6,57 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param resolutionScope
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults,
+                                      ResolutionScope resolutionScope) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: after a chat with Sam I'm going to change it from being state of the ClusterResolutionResult to adding more explicit methods to the result so each context will be asking it for the problems it cares about
created_at: 2025-05-07T22:48:09Z
author_association: MEMBER
diff_hunk: @@ -12,11 +12,12 @@
 
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaproxyingressspec.ClusterIP;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRanges;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRangesBuilder;
-import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
 
 class Ingresses {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/Ingresses.java
user: login: tombentley
body: Please can we have Javadoc that explains what an `Ingresses` is, conceptually speaking?

We have:
* IngressDefinition
* IngressInstance
* Ingresses
* ProxyIngressModel

I have a real hard time understanding what's going on there.
created_at: 2025-05-08T00:17:24Z
author_association: MEMBER
diff_hunk: @@ -22,6 +21,6 @@
  */
 public record ProxyModel(ProxyResolutionResult resolutionResult,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModel.java
user: login: tombentley
body: Is a `ProxyModel` really a "`ResolvedProxy`"?
created_at: 2025-05-08T00:21:54Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: tombentley
body: What about Secrets, ConfigMaps referenced from a `VirtualKafkaCluster.spec.ingresses.tls` or `KafkaService.spec.tls`? If they're really not required then explain why in the Javadoc.
created_at: 2025-05-08T00:25:16Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: tombentley
body: And the condition is up to date?
created_at: 2025-05-08T00:26:29Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>The cluster does not have a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsAndClusterFullyResolved() {
+        return allReferentsFullyResolved() && !isClusterResolvedRefsFalse();
     }
 
-    // using wildcards intentionally
+    /**
+     * All referents are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
+    }
+
+    /**
+     * Transforms this result into a ResolvedCluster, it is assumed that the client has checked that this ClusterResolutionResult
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: tombentley
body: "Transforms this result into a ResolvedCluster"

I think you need to explain somewhere why you need a separate representations of the result from the resolved things.
created_at: 2025-05-08T00:28:27Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>The cluster does not have a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsAndClusterFullyResolved() {
+        return allReferentsFullyResolved() && !isClusterResolvedRefsFalse();
     }
 
-    // using wildcards intentionally
+    /**
+     * All referents are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
+    }
+
+    /**
+     * Transforms this result into a ResolvedCluster, it is assumed that the client has checked that this ClusterResolutionResult
+     * has no dangling references, therefore it is safe to transform it into a resolved form.
+     * @return a resolved cluster
+     * @throws IllegalStateException if there are any dangling references
+     */
+    public ResolvedCluster toResolvedCluster() {
+        if (anyDanglingReferences()) {
+            throw new IllegalStateException("cannot construct resolved cluster if any references are dangling");
+        }
+        return new ResolvedCluster(cluster,
+                proxyResolutionResult.referentResource(),
+                filterResolutionResults.stream().map(ResolutionResult::referentResource).toList(),
+                serviceResolutionResult.referentResource(), ingressResolutionResults.stream().map(IngressResolutionResult::toResolvedIngress).toList());
+    }
+
+    /**
+     * Find all dangling references (recursive) from this VirtualKafkaCluster
+     * @return a stream of all dangling references
+     */
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences() {
+        return allReferentResolutionResults().filter(ResolutionResult::dangling)
+                .map(resolutionResult1 -> new ClusterResolutionResult.DanglingReference(resolutionResult1.referrer(), resolutionResult1.reference()));
+    }
+
+    /**
+     * Find all dangling references (recursive) from the specified referrer to the specified kind
+     * @param fromReferrer a LocalRef identifying the Referrer
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified LocalRef to the specified kind
+     */
     @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
+    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> fromReferrer, String toKind) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: tombentley
body: It's very confusing to overload a method with one that has a different result type.
created_at: 2025-05-08T00:31:44Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>The cluster does not have a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsAndClusterFullyResolved() {
+        return allReferentsFullyResolved() && !isClusterResolvedRefsFalse();
     }
 
-    // using wildcards intentionally
+    /**
+     * All referents are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
+    }
+
+    /**
+     * Transforms this result into a ResolvedCluster, it is assumed that the client has checked that this ClusterResolutionResult
+     * has no dangling references, therefore it is safe to transform it into a resolved form.
+     * @return a resolved cluster
+     * @throws IllegalStateException if there are any dangling references
+     */
+    public ResolvedCluster toResolvedCluster() {
+        if (anyDanglingReferences()) {
+            throw new IllegalStateException("cannot construct resolved cluster if any references are dangling");
+        }
+        return new ResolvedCluster(cluster,
+                proxyResolutionResult.referentResource(),
+                filterResolutionResults.stream().map(ResolutionResult::referentResource).toList(),
+                serviceResolutionResult.referentResource(), ingressResolutionResults.stream().map(IngressResolutionResult::toResolvedIngress).toList());
+    }
+
+    /**
+     * Find all dangling references (recursive) from this VirtualKafkaCluster
+     * @return a stream of all dangling references
+     */
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences() {
+        return allReferentResolutionResults().filter(ResolutionResult::dangling)
+                .map(resolutionResult1 -> new ClusterResolutionResult.DanglingReference(resolutionResult1.referrer(), resolutionResult1.reference()));
+    }
+
+    /**
+     * Find all dangling references (recursive) from the specified referrer to the specified kind
+     * @param fromReferrer a LocalRef identifying the Referrer
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified LocalRef to the specified kind
+     */
     @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
+    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> fromReferrer, String toKind) {
+        Objects.requireNonNull(fromReferrer);
+        Objects.requireNonNull(toKind);
+        return findDanglingReferences().filter(r -> r.from().equals(fromReferrer) && toKind.equals(r.to().getKind())).map(
+                ClusterResolutionResult.DanglingReference::to);
     }
 
+    /**
+     * Find all dangling references (recursive) from the specified fromKind to the specified toKind
+     * @param fromKind a Referrer kind
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified fromKind to the specified toKind
+     */
     // using wildcards intentionally
     @SuppressWarnings("java:S1452")
     public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
         Objects.requireNonNull(fromKind);
         Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
+        return findDanglingReferences().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
+                .map(ClusterResolutionResult.DanglingReference::to);
     }
 
     /**
-     * A result is fully resolved if:
-     * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
-     * </ol>
+     * Find referents and cluster with stale status (status.observedGeneration != metadata.generation)
+     * @return LocalRefs to referents and cluster with stale status
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: tombentley
body: I don't understand what "referents and cluster" means, given this returns some `LocalRefs` (==referents). Where does the cluster part come from?
created_at: 2025-05-08T00:33:11Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>The cluster does not have a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsAndClusterFullyResolved() {
+        return allReferentsFullyResolved() && !isClusterResolvedRefsFalse();
     }
 
-    // using wildcards intentionally
+    /**
+     * All referents are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
+    }
+
+    /**
+     * Transforms this result into a ResolvedCluster, it is assumed that the client has checked that this ClusterResolutionResult
+     * has no dangling references, therefore it is safe to transform it into a resolved form.
+     * @return a resolved cluster
+     * @throws IllegalStateException if there are any dangling references
+     */
+    public ResolvedCluster toResolvedCluster() {
+        if (anyDanglingReferences()) {
+            throw new IllegalStateException("cannot construct resolved cluster if any references are dangling");
+        }
+        return new ResolvedCluster(cluster,
+                proxyResolutionResult.referentResource(),
+                filterResolutionResults.stream().map(ResolutionResult::referentResource).toList(),
+                serviceResolutionResult.referentResource(), ingressResolutionResults.stream().map(IngressResolutionResult::toResolvedIngress).toList());
+    }
+
+    /**
+     * Find all dangling references (recursive) from this VirtualKafkaCluster
+     * @return a stream of all dangling references
+     */
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences() {
+        return allReferentResolutionResults().filter(ResolutionResult::dangling)
+                .map(resolutionResult1 -> new ClusterResolutionResult.DanglingReference(resolutionResult1.referrer(), resolutionResult1.reference()));
+    }
+
+    /**
+     * Find all dangling references (recursive) from the specified referrer to the specified kind
+     * @param fromReferrer a LocalRef identifying the Referrer
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified LocalRef to the specified kind
+     */
     @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
+    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> fromReferrer, String toKind) {
+        Objects.requireNonNull(fromReferrer);
+        Objects.requireNonNull(toKind);
+        return findDanglingReferences().filter(r -> r.from().equals(fromReferrer) && toKind.equals(r.to().getKind())).map(
+                ClusterResolutionResult.DanglingReference::to);
     }
 
+    /**
+     * Find all dangling references (recursive) from the specified fromKind to the specified toKind
+     * @param fromKind a Referrer kind
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified fromKind to the specified toKind
+     */
     // using wildcards intentionally
     @SuppressWarnings("java:S1452")
     public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
         Objects.requireNonNull(fromKind);
         Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
+        return findDanglingReferences().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
+                .map(ClusterResolutionResult.DanglingReference::to);
     }
 
     /**
-     * A result is fully resolved if:
-     * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
-     * </ol>
+     * Find referents and cluster with stale status (status.observedGeneration != metadata.generation)
+     * @return LocalRefs to referents and cluster with stale status
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    // using wildcards intentionally
+    @SuppressWarnings("java:S1452")
+    public Stream<LocalRef<?>> findReferentsAndClusterWithStaleStatus() {
+        Stream<LocalRef<?>> referentsWithStaleStatus = referentsMatching(Referent::isStale);
+        Stream<LocalRef<?>> clusterWithStaleStatus = isClusterStale() ? Stream.of(ResourcesUtil.toLocalRef(cluster)) : Stream.of();
+        return Stream.concat(referentsWithStaleStatus, clusterWithStaleStatus);
+    }
+
+    // using wildcards intentionally
+    @SuppressWarnings("java:S1452")
+    public Stream<LocalRef<?>> findReferentsAndClusterWithResolvedRefsFalse() {
+        Stream<LocalRef<?>> referentsWithResolvedRefsFalse = findReferentsWithResolvedRefsFalse();
+        Stream<LocalRef<?>> clusterWithResolvedRefsTrue = isClusterResolvedRefsFalse() ? Stream.of(ResourcesUtil.toLocalRef(cluster)) : Stream.of();
+        return Stream.concat(referentsWithResolvedRefsFalse, clusterWithResolvedRefsTrue);
+    }
+
+    // using wildcards intentionally
+    @SuppressWarnings("java:S1452")
+    public Stream<LocalRef<?>> findReferentsWithResolvedRefsFalse(String kind) {
+        return findReferentsWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * @return true iff all referents and the cluster have fresh status (status.observedGeneration == metadata.generation)
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
+    public boolean allReferentsAndClusterHaveFreshStatus() {
+        return findReferentsAndClusterWithStaleStatus().findAny().isEmpty();
     }
 
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    public boolean anyDanglingRefsFrom(LocalRef<?> fromReferrer) {
+        return findDanglingReferences().anyMatch(u -> u.from().equals(fromReferrer));
     }
 
-    public boolean anyResolvedRefsConditionsFalse() {
-        return !resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean anyDanglingReferences() {
+        return findDanglingReferences().findAny().isPresent();
     }
 
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
-        return resourcesWithResolvedRefsFalse.stream();
+    /**
+     * @return true iff any Referent of the VirtualKafkaCluster has a ResolvedRefs=False condition
+     */
+    public boolean anyReferentResolvedRefsConditionsFalse() {
+        return findReferentsWithResolvedRefsFalse().findAny().isPresent();
     }
 
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findReferentsWithStaleStatus() {
-        return referentsWithStaleStatus.stream();
+    private boolean isClusterStale() {
+        return Referent.from(cluster).isStale();
+    }
+
+    private boolean isClusterResolvedRefsFalse() {
+        return Referent.from(cluster).hasResolvedRefsFalseCondition();
     }
 
     // using wildcards intentionally
     @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
-        return findResourcesWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
+    private Stream<LocalRef<?>> findReferentsWithResolvedRefsFalse() {
+        return referentsMatching(Referent::hasResolvedRefsFalseCondition);
     }
 
-    ClusterResolutionResult addAllResourcesHavingResolvedRefsFalse(Set<LocalRef<?>> resolvedRefsFalse) {
-        return new ClusterResolutionResult(cluster, danglingReferences, Stream.concat(resourcesWithResolvedRefsFalse.stream(), resolvedRefsFalse.stream()).collect(
-                Collectors.toSet()), referentsWithStaleStatus);
+    private Stream<LocalRef<?>> referentsMatching(Predicate<Referent<?>> predicate) {
+        return allReferentResolutionResults().filter(i -> (i.referent() != null) && predicate.test(i.referent()))
+                .map(ResolutionResult::reference);
     }
 
-    ClusterResolutionResult addReferentsWithStaleStatus(Set<LocalRef<?>> referentsWithStaleStatus) {
-        return new ClusterResolutionResult(cluster, danglingReferences, resourcesWithResolvedRefsFalse,
-                Stream.concat(this.referentsWithStaleStatus.stream(), referentsWithStaleStatus.stream()).collect(
-                        Collectors.toSet()));
+    private Stream<ResolutionResult<?>> allReferentResolutionResults() {
+        Stream<ResolutionResult<?>> proxyResults = Stream.of(proxyResolutionResult);
+        Stream<ResolutionResult<?>> filterResults = filterResolutionResults.stream().map(Function.identity());
+        Stream<ResolutionResult<?>> serviceResults = Stream.of(serviceResolutionResult);
+        Stream<ResolutionResult<?>> ingressResults = ingressResolutionResults.stream()
+                .flatMap(i -> Stream.concat(Stream.of(i.ingressResolutionResult()), Optional.ofNullable(i.proxyResolutionResult()).stream()));
+        return Stream.of(proxyResults, filterResults, serviceResults, ingressResults).flatMap(Function.identity());
     }
 
     /**
-     * Describes the case where an entity A references an entity B but we can not find B.
+     * Describes the case where an entity A references an entity B, but we can not find B.
      * @param from
      * @param to
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: tombentley
body: Maybe rename `to` to `missing` or `absent`? I suspect that would make call sites more readable.
created_at: 2025-05-08T00:35:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,31 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.virtualkafkaclusterspec.Ingresses;
+import io.kroxylicious.kubernetes.operator.model.ResolvedIngress;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of dereferencing an Ingresses ingressRef and proxyRef
+ * @param ingress the ingress
+ * @param ingressResolutionResult the dereferenced KafkaProxyIngress result
+ * @param proxyResolutionResult the dereferenced KafkaProxy result
+ */
+public record IngressResolutionResult(Ingresses ingress,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/IngressResolutionResult.java
user: login: tombentley
body: I don't know what to make of the plural type but singular parameter name.
created_at: 2025-05-08T00:36:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,31 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.virtualkafkaclusterspec.Ingresses;
+import io.kroxylicious.kubernetes.operator.model.ResolvedIngress;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of dereferencing an Ingresses ingressRef and proxyRef
+ * @param ingress the ingress
+ * @param ingressResolutionResult the dereferenced KafkaProxyIngress result
+ * @param proxyResolutionResult the dereferenced KafkaProxy result
+ */
+public record IngressResolutionResult(Ingresses ingress,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/IngressResolutionResult.java
user: login: tombentley
body: Again, I don't really understand why the need for a `Result` thing and a `Resolved` thing.
created_at: 2025-05-08T00:37:39Z
author_association: MEMBER
diff_hunk: @@ -22,6 +21,6 @@
  */
 public record ProxyModel(ProxyResolutionResult resolutionResult,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModel.java
user: login: robobario
body: I'm using `Resolved` to mean 100% of the recursive dependencies could be resolved (putting aside the fact it doesn't consider Secrets/Configmaps yet). 

`ProxyModel` contains information about problems/unresolved referents in the ProxyResolutionResult.

Maybe like you're getting at later we should have everything operate on the `*ResolutionResult` and drop the `Resolved*` conveniences.
created_at: 2025-05-08T01:29:04Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: Yeah it would make sense to resolve them here I think, as a dangling ref to a secret encountered in the aggregator should be reported back as a problem to the VKC. It just hasn't been done yet, so I'd be keen to leave it for another PR.
created_at: 2025-05-08T01:30:54Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: good point, I don't think our code considers that currently.
created_at: 2025-05-08T01:39:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,31 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.virtualkafkaclusterspec.Ingresses;
+import io.kroxylicious.kubernetes.operator.model.ResolvedIngress;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of dereferencing an Ingresses ingressRef and proxyRef
+ * @param ingress the ingress
+ * @param ingressResolutionResult the dereferenced KafkaProxyIngress result
+ * @param proxyResolutionResult the dereferenced KafkaProxy result
+ */
+public record IngressResolutionResult(Ingresses ingress,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/IngressResolutionResult.java
user: login: robobario
body: `Ingresses` is generated code from the CRD, I think this record-field is unused.
created_at: 2025-05-08T01:44:50Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: created #2146 
created_at: 2025-05-08T01:45:26Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>The cluster does not have a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsAndClusterFullyResolved() {
+        return allReferentsFullyResolved() && !isClusterResolvedRefsFalse();
     }
 
-    // using wildcards intentionally
+    /**
+     * All referents are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
+    }
+
+    /**
+     * Transforms this result into a ResolvedCluster, it is assumed that the client has checked that this ClusterResolutionResult
+     * has no dangling references, therefore it is safe to transform it into a resolved form.
+     * @return a resolved cluster
+     * @throws IllegalStateException if there are any dangling references
+     */
+    public ResolvedCluster toResolvedCluster() {
+        if (anyDanglingReferences()) {
+            throw new IllegalStateException("cannot construct resolved cluster if any references are dangling");
+        }
+        return new ResolvedCluster(cluster,
+                proxyResolutionResult.referentResource(),
+                filterResolutionResults.stream().map(ResolutionResult::referentResource).toList(),
+                serviceResolutionResult.referentResource(), ingressResolutionResults.stream().map(IngressResolutionResult::toResolvedIngress).toList());
+    }
+
+    /**
+     * Find all dangling references (recursive) from this VirtualKafkaCluster
+     * @return a stream of all dangling references
+     */
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences() {
+        return allReferentResolutionResults().filter(ResolutionResult::dangling)
+                .map(resolutionResult1 -> new ClusterResolutionResult.DanglingReference(resolutionResult1.referrer(), resolutionResult1.reference()));
+    }
+
+    /**
+     * Find all dangling references (recursive) from the specified referrer to the specified kind
+     * @param fromReferrer a LocalRef identifying the Referrer
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified LocalRef to the specified kind
+     */
     @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
+    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> fromReferrer, String toKind) {
+        Objects.requireNonNull(fromReferrer);
+        Objects.requireNonNull(toKind);
+        return findDanglingReferences().filter(r -> r.from().equals(fromReferrer) && toKind.equals(r.to().getKind())).map(
+                ClusterResolutionResult.DanglingReference::to);
     }
 
+    /**
+     * Find all dangling references (recursive) from the specified fromKind to the specified toKind
+     * @param fromKind a Referrer kind
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified fromKind to the specified toKind
+     */
     // using wildcards intentionally
     @SuppressWarnings("java:S1452")
     public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
         Objects.requireNonNull(fromKind);
         Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
+        return findDanglingReferences().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
+                .map(ClusterResolutionResult.DanglingReference::to);
     }
 
     /**
-     * A result is fully resolved if:
-     * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
-     * </ol>
+     * Find referents and cluster with stale status (status.observedGeneration != metadata.generation)
+     * @return LocalRefs to referents and cluster with stale status
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: `referents` means referents of the cluster. the cluster itself isn't a Referent but we want to treat it the same way sometimes. The ProxyReconciler want to ask "do any of the referents or the cluster have ResolvedRefs=false?". Where the VKC Reconciler only wants to ask "do any of the referents have ResolvedRefs=false?"
created_at: 2025-05-08T01:50:33Z
author_association: MEMBER
diff_hunk: @@ -22,6 +21,6 @@
  */
 public record ProxyModel(ProxyResolutionResult resolutionResult,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ProxyModel.java
user: login: robobario
body: have removed the `Resolved*` classes
created_at: 2025-05-08T02:07:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,31 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.virtualkafkaclusterspec.Ingresses;
+import io.kroxylicious.kubernetes.operator.model.ResolvedIngress;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of dereferencing an Ingresses ingressRef and proxyRef
+ * @param ingress the ingress
+ * @param ingressResolutionResult the dereferenced KafkaProxyIngress result
+ * @param proxyResolutionResult the dereferenced KafkaProxy result
+ */
+public record IngressResolutionResult(Ingresses ingress,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/IngressResolutionResult.java
user: login: robobario
body: have removed the `Resolved` classes
created_at: 2025-05-08T02:08:29Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>The cluster does not have a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsAndClusterFullyResolved() {
+        return allReferentsFullyResolved() && !isClusterResolvedRefsFalse();
     }
 
-    // using wildcards intentionally
+    /**
+     * All referents are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
+    }
+
+    /**
+     * Transforms this result into a ResolvedCluster, it is assumed that the client has checked that this ClusterResolutionResult
+     * has no dangling references, therefore it is safe to transform it into a resolved form.
+     * @return a resolved cluster
+     * @throws IllegalStateException if there are any dangling references
+     */
+    public ResolvedCluster toResolvedCluster() {
+        if (anyDanglingReferences()) {
+            throw new IllegalStateException("cannot construct resolved cluster if any references are dangling");
+        }
+        return new ResolvedCluster(cluster,
+                proxyResolutionResult.referentResource(),
+                filterResolutionResults.stream().map(ResolutionResult::referentResource).toList(),
+                serviceResolutionResult.referentResource(), ingressResolutionResults.stream().map(IngressResolutionResult::toResolvedIngress).toList());
+    }
+
+    /**
+     * Find all dangling references (recursive) from this VirtualKafkaCluster
+     * @return a stream of all dangling references
+     */
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences() {
+        return allReferentResolutionResults().filter(ResolutionResult::dangling)
+                .map(resolutionResult1 -> new ClusterResolutionResult.DanglingReference(resolutionResult1.referrer(), resolutionResult1.reference()));
+    }
+
+    /**
+     * Find all dangling references (recursive) from the specified referrer to the specified kind
+     * @param fromReferrer a LocalRef identifying the Referrer
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified LocalRef to the specified kind
+     */
     @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
+    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> fromReferrer, String toKind) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: fixed, they all return `Stream<DanglingReference>` now 
created_at: 2025-05-08T02:15:59Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>The cluster does not have a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsAndClusterFullyResolved() {
+        return allReferentsFullyResolved() && !isClusterResolvedRefsFalse();
     }
 
-    // using wildcards intentionally
+    /**
+     * All referents are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
+    }
+
+    /**
+     * Transforms this result into a ResolvedCluster, it is assumed that the client has checked that this ClusterResolutionResult
+     * has no dangling references, therefore it is safe to transform it into a resolved form.
+     * @return a resolved cluster
+     * @throws IllegalStateException if there are any dangling references
+     */
+    public ResolvedCluster toResolvedCluster() {
+        if (anyDanglingReferences()) {
+            throw new IllegalStateException("cannot construct resolved cluster if any references are dangling");
+        }
+        return new ResolvedCluster(cluster,
+                proxyResolutionResult.referentResource(),
+                filterResolutionResults.stream().map(ResolutionResult::referentResource).toList(),
+                serviceResolutionResult.referentResource(), ingressResolutionResults.stream().map(IngressResolutionResult::toResolvedIngress).toList());
+    }
+
+    /**
+     * Find all dangling references (recursive) from this VirtualKafkaCluster
+     * @return a stream of all dangling references
+     */
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences() {
+        return allReferentResolutionResults().filter(ResolutionResult::dangling)
+                .map(resolutionResult1 -> new ClusterResolutionResult.DanglingReference(resolutionResult1.referrer(), resolutionResult1.reference()));
+    }
+
+    /**
+     * Find all dangling references (recursive) from the specified referrer to the specified kind
+     * @param fromReferrer a LocalRef identifying the Referrer
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified LocalRef to the specified kind
+     */
     @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
+    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> fromReferrer, String toKind) {
+        Objects.requireNonNull(fromReferrer);
+        Objects.requireNonNull(toKind);
+        return findDanglingReferences().filter(r -> r.from().equals(fromReferrer) && toKind.equals(r.to().getKind())).map(
+                ClusterResolutionResult.DanglingReference::to);
     }
 
+    /**
+     * Find all dangling references (recursive) from the specified fromKind to the specified toKind
+     * @param fromKind a Referrer kind
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified fromKind to the specified toKind
+     */
     // using wildcards intentionally
     @SuppressWarnings("java:S1452")
     public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
         Objects.requireNonNull(fromKind);
         Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
+        return findDanglingReferences().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
+                .map(ClusterResolutionResult.DanglingReference::to);
     }
 
     /**
-     * A result is fully resolved if:
-     * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
-     * </ol>
+     * Find referents and cluster with stale status (status.observedGeneration != metadata.generation)
+     * @return LocalRefs to referents and cluster with stale status
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    // using wildcards intentionally
+    @SuppressWarnings("java:S1452")
+    public Stream<LocalRef<?>> findReferentsAndClusterWithStaleStatus() {
+        Stream<LocalRef<?>> referentsWithStaleStatus = referentsMatching(Referent::isStale);
+        Stream<LocalRef<?>> clusterWithStaleStatus = isClusterStale() ? Stream.of(ResourcesUtil.toLocalRef(cluster)) : Stream.of();
+        return Stream.concat(referentsWithStaleStatus, clusterWithStaleStatus);
+    }
+
+    // using wildcards intentionally
+    @SuppressWarnings("java:S1452")
+    public Stream<LocalRef<?>> findReferentsAndClusterWithResolvedRefsFalse() {
+        Stream<LocalRef<?>> referentsWithResolvedRefsFalse = findReferentsWithResolvedRefsFalse();
+        Stream<LocalRef<?>> clusterWithResolvedRefsTrue = isClusterResolvedRefsFalse() ? Stream.of(ResourcesUtil.toLocalRef(cluster)) : Stream.of();
+        return Stream.concat(referentsWithResolvedRefsFalse, clusterWithResolvedRefsTrue);
+    }
+
+    // using wildcards intentionally
+    @SuppressWarnings("java:S1452")
+    public Stream<LocalRef<?>> findReferentsWithResolvedRefsFalse(String kind) {
+        return findReferentsWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * @return true iff all referents and the cluster have fresh status (status.observedGeneration == metadata.generation)
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
+    public boolean allReferentsAndClusterHaveFreshStatus() {
+        return findReferentsAndClusterWithStaleStatus().findAny().isEmpty();
     }
 
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    public boolean anyDanglingRefsFrom(LocalRef<?> fromReferrer) {
+        return findDanglingReferences().anyMatch(u -> u.from().equals(fromReferrer));
     }
 
-    public boolean anyResolvedRefsConditionsFalse() {
-        return !resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean anyDanglingReferences() {
+        return findDanglingReferences().findAny().isPresent();
     }
 
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
-        return resourcesWithResolvedRefsFalse.stream();
+    /**
+     * @return true iff any Referent of the VirtualKafkaCluster has a ResolvedRefs=False condition
+     */
+    public boolean anyReferentResolvedRefsConditionsFalse() {
+        return findReferentsWithResolvedRefsFalse().findAny().isPresent();
     }
 
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findReferentsWithStaleStatus() {
-        return referentsWithStaleStatus.stream();
+    private boolean isClusterStale() {
+        return Referent.from(cluster).isStale();
+    }
+
+    private boolean isClusterResolvedRefsFalse() {
+        return Referent.from(cluster).hasResolvedRefsFalseCondition();
     }
 
     // using wildcards intentionally
     @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
-        return findResourcesWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
+    private Stream<LocalRef<?>> findReferentsWithResolvedRefsFalse() {
+        return referentsMatching(Referent::hasResolvedRefsFalseCondition);
     }
 
-    ClusterResolutionResult addAllResourcesHavingResolvedRefsFalse(Set<LocalRef<?>> resolvedRefsFalse) {
-        return new ClusterResolutionResult(cluster, danglingReferences, Stream.concat(resourcesWithResolvedRefsFalse.stream(), resolvedRefsFalse.stream()).collect(
-                Collectors.toSet()), referentsWithStaleStatus);
+    private Stream<LocalRef<?>> referentsMatching(Predicate<Referent<?>> predicate) {
+        return allReferentResolutionResults().filter(i -> (i.referent() != null) && predicate.test(i.referent()))
+                .map(ResolutionResult::reference);
     }
 
-    ClusterResolutionResult addReferentsWithStaleStatus(Set<LocalRef<?>> referentsWithStaleStatus) {
-        return new ClusterResolutionResult(cluster, danglingReferences, resourcesWithResolvedRefsFalse,
-                Stream.concat(this.referentsWithStaleStatus.stream(), referentsWithStaleStatus.stream()).collect(
-                        Collectors.toSet()));
+    private Stream<ResolutionResult<?>> allReferentResolutionResults() {
+        Stream<ResolutionResult<?>> proxyResults = Stream.of(proxyResolutionResult);
+        Stream<ResolutionResult<?>> filterResults = filterResolutionResults.stream().map(Function.identity());
+        Stream<ResolutionResult<?>> serviceResults = Stream.of(serviceResolutionResult);
+        Stream<ResolutionResult<?>> ingressResults = ingressResolutionResults.stream()
+                .flatMap(i -> Stream.concat(Stream.of(i.ingressResolutionResult()), Optional.ofNullable(i.proxyResolutionResult()).stream()));
+        return Stream.of(proxyResults, filterResults, serviceResults, ingressResults).flatMap(Function.identity());
     }
 
     /**
-     * Describes the case where an entity A references an entity B but we can not find B.
+     * Describes the case where an entity A references an entity B, but we can not find B.
      * @param from
      * @param to
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: done, `to` should be `referrer` as well.
created_at: 2025-05-08T02:16:16Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>The cluster does not have a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsAndClusterFullyResolved() {
+        return allReferentsFullyResolved() && !isClusterResolvedRefsFalse();
     }
 
-    // using wildcards intentionally
+    /**
+     * All referents are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
+    }
+
+    /**
+     * Transforms this result into a ResolvedCluster, it is assumed that the client has checked that this ClusterResolutionResult
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: removed `Resolved*` classes
created_at: 2025-05-08T02:19:22Z
author_association: MEMBER
diff_hunk: @@ -12,11 +12,12 @@
 
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaproxyingressspec.ClusterIP;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRanges;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRangesBuilder;
-import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
 
 class Ingresses {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/Ingresses.java
user: login: robobario
body: I don't want to go too far down tidying the ingress side of things up as it's a mess and will blow out this PR which has enough going on. I'll rename `Ingresses` to `IngressDefinitionBuilder` in this PR to give it a better name and disambiguate from the `Ingresses` class generated (with the annoying pluralization) from the CR.
created_at: 2025-05-08T02:34:57Z
author_association: MEMBER
diff_hunk: @@ -12,11 +12,12 @@
 
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaproxyingressspec.ClusterIP;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRanges;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRangesBuilder;
-import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
 
 class Ingresses {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/Ingresses.java
user: login: robobario
body: ah sorry, you were requesting javadoc, I'll have a shot at improving it
created_at: 2025-05-08T02:37:24Z
author_association: MEMBER
diff_hunk: @@ -12,11 +12,12 @@
 
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaproxyingressspec.ClusterIP;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRanges;
 import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRangesBuilder;
-import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
 
 class Ingresses {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/Ingresses.java
user: login: tombentley
body: Thanks! I hadn't even clocked that we had two `Ingresses`. Definitely distinct names would be good.
created_at: 2025-05-08T02:38:55Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>The cluster does not have a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsAndClusterFullyResolved() {
+        return allReferentsFullyResolved() && !isClusterResolvedRefsFalse();
     }
 
-    // using wildcards intentionally
+    /**
+     * All referents are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
+    }
+
+    /**
+     * Transforms this result into a ResolvedCluster, it is assumed that the client has checked that this ClusterResolutionResult
+     * has no dangling references, therefore it is safe to transform it into a resolved form.
+     * @return a resolved cluster
+     * @throws IllegalStateException if there are any dangling references
+     */
+    public ResolvedCluster toResolvedCluster() {
+        if (anyDanglingReferences()) {
+            throw new IllegalStateException("cannot construct resolved cluster if any references are dangling");
+        }
+        return new ResolvedCluster(cluster,
+                proxyResolutionResult.referentResource(),
+                filterResolutionResults.stream().map(ResolutionResult::referentResource).toList(),
+                serviceResolutionResult.referentResource(), ingressResolutionResults.stream().map(IngressResolutionResult::toResolvedIngress).toList());
+    }
+
+    /**
+     * Find all dangling references (recursive) from this VirtualKafkaCluster
+     * @return a stream of all dangling references
+     */
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences() {
+        return allReferentResolutionResults().filter(ResolutionResult::dangling)
+                .map(resolutionResult1 -> new ClusterResolutionResult.DanglingReference(resolutionResult1.referrer(), resolutionResult1.reference()));
+    }
+
+    /**
+     * Find all dangling references (recursive) from the specified referrer to the specified kind
+     * @param fromReferrer a LocalRef identifying the Referrer
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified LocalRef to the specified kind
+     */
     @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
+    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> fromReferrer, String toKind) {
+        Objects.requireNonNull(fromReferrer);
+        Objects.requireNonNull(toKind);
+        return findDanglingReferences().filter(r -> r.from().equals(fromReferrer) && toKind.equals(r.to().getKind())).map(
+                ClusterResolutionResult.DanglingReference::to);
     }
 
+    /**
+     * Find all dangling references (recursive) from the specified fromKind to the specified toKind
+     * @param fromKind a Referrer kind
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified fromKind to the specified toKind
+     */
     // using wildcards intentionally
     @SuppressWarnings("java:S1452")
     public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
         Objects.requireNonNull(fromKind);
         Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
+        return findDanglingReferences().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
+                .map(ClusterResolutionResult.DanglingReference::to);
     }
 
     /**
-     * A result is fully resolved if:
-     * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
-     * </ol>
+     * Find referents and cluster with stale status (status.observedGeneration != metadata.generation)
+     * @return LocalRefs to referents and cluster with stale status
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: tombentley
body: > we want to treat it the same way sometimes

That is not at all obvious, and is unintuitive for anyone with the CR model in their head (where clusters do not refer to clusters).
* Is it really necessary? I mean, can't the `ProxyReconciler` just make two calls? Or could it concatenate a stream of referents with a LocalRef for the cluster at the call site which want to tread things that way?
* If it is really necessary then you should at least explain this in the Javadoc.
created_at: 2025-05-08T02:46:59Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,175 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: tombentley
body: ```suggestion
 * @param serviceResolutionResult the resolution result for the kafka service referenced by the cluster
```
created_at: 2025-05-08T02:52:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,20 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of dereferencing an Ingresses ingressRef and proxyRef
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/IngressResolutionResult.java
user: login: tombentley
body: I _think_ this is what you mean?
```suggestion
 * The result of dereferencing an VKC's {@code spec.ingressss.ingressRef} and proxyRef
```

It's not obvious, because it could also refer to the result of resolving the KPI itself. Or maybe it's both?

In general we've got to be super careful when we use `ingress`, because it's so overloaded:

* The KafkaProxyIngress
* The `spec.ingresses` property on a VKC, or an element in that array
* The OpenShift resource
* Plus the model classes.
created_at: 2025-05-08T02:55:10Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
+
+    /**
+     * All referents and cluster are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>The cluster does not have a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsAndClusterFullyResolved() {
+        return allReferentsFullyResolved() && !isClusterResolvedRefsFalse();
     }
 
-    // using wildcards intentionally
+    /**
+     * All referents are fully resolved iff
+     * <ol>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     * </ol>
+     * @return true if fully resolved
+     */
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
+    }
+
+    /**
+     * Transforms this result into a ResolvedCluster, it is assumed that the client has checked that this ClusterResolutionResult
+     * has no dangling references, therefore it is safe to transform it into a resolved form.
+     * @return a resolved cluster
+     * @throws IllegalStateException if there are any dangling references
+     */
+    public ResolvedCluster toResolvedCluster() {
+        if (anyDanglingReferences()) {
+            throw new IllegalStateException("cannot construct resolved cluster if any references are dangling");
+        }
+        return new ResolvedCluster(cluster,
+                proxyResolutionResult.referentResource(),
+                filterResolutionResults.stream().map(ResolutionResult::referentResource).toList(),
+                serviceResolutionResult.referentResource(), ingressResolutionResults.stream().map(IngressResolutionResult::toResolvedIngress).toList());
+    }
+
+    /**
+     * Find all dangling references (recursive) from this VirtualKafkaCluster
+     * @return a stream of all dangling references
+     */
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences() {
+        return allReferentResolutionResults().filter(ResolutionResult::dangling)
+                .map(resolutionResult1 -> new ClusterResolutionResult.DanglingReference(resolutionResult1.referrer(), resolutionResult1.reference()));
+    }
+
+    /**
+     * Find all dangling references (recursive) from the specified referrer to the specified kind
+     * @param fromReferrer a LocalRef identifying the Referrer
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified LocalRef to the specified kind
+     */
     @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
+    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> fromReferrer, String toKind) {
+        Objects.requireNonNull(fromReferrer);
+        Objects.requireNonNull(toKind);
+        return findDanglingReferences().filter(r -> r.from().equals(fromReferrer) && toKind.equals(r.to().getKind())).map(
+                ClusterResolutionResult.DanglingReference::to);
     }
 
+    /**
+     * Find all dangling references (recursive) from the specified fromKind to the specified toKind
+     * @param fromKind a Referrer kind
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified fromKind to the specified toKind
+     */
     // using wildcards intentionally
     @SuppressWarnings("java:S1452")
     public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
         Objects.requireNonNull(fromKind);
         Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
+        return findDanglingReferences().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
+                .map(ClusterResolutionResult.DanglingReference::to);
     }
 
     /**
-     * A result is fully resolved if:
-     * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
-     * </ol>
+     * Find referents and cluster with stale status (status.observedGeneration != metadata.generation)
+     * @return LocalRefs to referents and cluster with stale status
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: Okay, I'll shuffle it around so we expose a couple of boolean methods and have the clients handle the concatenation
created_at: 2025-05-08T02:57:16Z
author_association: MEMBER
diff_hunk: @@ -32,26 +24,10 @@
  * describes which dependencies could not be resolved per VirtualKafkaCluster.
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ProxyResolutionResult.java
user: login: tombentley
body: The comment begins: 

> The result of a deep resolution of the dependencies of a single KafkaProxy.

I think this might be more consistent with the terminology we're using elsewhere: 

> The result of a deep resolution of the transitive references from a single KafkaProxy.

I note that you're using both "resolution" and "dereference". We should try to stick to one.
created_at: 2025-05-08T03:03:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
+import io.kroxylicious.kubernetes.api.common.Condition;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngressStatus;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyStatus;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaServiceStatus;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaClusterStatus;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterStatus;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+
+/**
+ * A single referent
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/Referent.java
user: login: tombentley
body: * This is really modelling the state of all the references from the referrer, I think? If so, perhaps there's a better name for this type.
* Could you give an example? I think that would make it much more concrete for the reader.
created_at: 2025-05-08T03:05:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import java.util.Optional;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
+import io.kroxylicious.kubernetes.api.common.LocalRef;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of attempting to dereference a reference
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: tombentley
body: ```suggestion
 * The result of attempting to resolve a reference
```
created_at: 2025-05-08T03:08:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import java.util.Optional;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
+import io.kroxylicious.kubernetes.api.common.LocalRef;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of attempting to dereference a reference
+ * @param referrer the referring resource's LocalRef
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: tombentley
body: ```suggestion
 * @param referrer the referring resource represented as a LocalRef
```
created_at: 2025-05-08T03:09:49Z
author_association: MEMBER
diff_hunk: @@ -141,131 +120,46 @@ public ClusterResolutionResult resolveClusterRefs(VirtualKafkaCluster cluster, C
     private ClusterResolutionResult discoverProblemsAndBuildResolutionResult(VirtualKafkaCluster cluster,
                                                                              CommonDependencies result,
                                                                              Set<KafkaProxy> proxies) {
-        Set<DanglingReference> danglingReferences = determineDanglingRefs(result, proxies, cluster);
-        Set<LocalRef<?>> resolvedRefsFalse = determineResolvedRefsFalse(result);
-        Set<LocalRef<?>> referentsWithStaleStatus = determineReferentsWithStaleStatus(result);
-        return new ClusterResolutionResult(cluster, danglingReferences, resolvedRefsFalse, referentsWithStaleStatus);
-    }
-
-    private static Set<LocalRef<?>> determineReferentsWithStaleStatus(CommonDependencies cluster) {
-        Stream<LocalRef<?>> ingressesWithStaleStatus = cluster.ingresses().entrySet().stream().filter(e -> !ResourcesUtil.isStatusFresh(e.getValue()))
-                .map(Map.Entry::getKey);
-        Stream<LocalRef<?>> servicesWithStaleStatus = cluster.kafkaServices().entrySet().stream().filter(e -> !ResourcesUtil.isStatusFresh(e.getValue()))
-                .map(Map.Entry::getKey);
-        Stream<LocalRef<?>> filtersWithStaleStatus = cluster.filters().entrySet().stream().filter(e -> !ResourcesUtil.isStatusFresh(e.getValue()))
-                .map(Map.Entry::getKey);
-        return Stream.of(ingressesWithStaleStatus, servicesWithStaleStatus, filtersWithStaleStatus)
-                .flatMap(Function.identity())
-                .collect(Collectors.toSet());
-    }
-
-    private static Set<LocalRef<?>> determineReferentsWithStaleStatus(VirtualKafkaCluster cluster) {
-        if (ResourcesUtil.isStatusFresh(cluster)) {
-            return Set.of();
-        }
-        else {
-            return Set.of(toLocalRef(cluster));
-        }
-    }
-
-    private static Set<DanglingReference> determineDanglingRefs(CommonDependencies dependencies, Set<KafkaProxy> proxies,
-                                                                VirtualKafkaCluster cluster) {
         LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
-        VirtualKafkaClusterSpec spec = cluster.getSpec();
-        var danglingIngressRefs = determineDanglingIngressRefs(clusterRef, spec, dependencies.ingresses());
-        var danglingProxyRefs = determineDanglingKafkaProxyRefs(clusterRef, spec, proxies).stream();
-        var danglingIngressProxyRefs = determineIngressesWithDanglingProxyRefs(dependencies.ingresses(), proxies);
-        var danglingServiceRefs = determineDanglingKafkaServiceRefs(clusterRef, spec, dependencies.kafkaServices()).stream();
-        var danglingFilterRefs = determineDanglingFilterRefs(clusterRef, spec, dependencies.filters());
-        return Stream.of(danglingIngressRefs, danglingProxyRefs, danglingIngressProxyRefs, danglingServiceRefs, danglingFilterRefs)
-                .flatMap(Function.identity()).collect(Collectors.toSet());
-    }
-
-    private static Set<LocalRef<?>> determineResolvedRefsFalse(CommonDependencies dependencies) {
-        Stream<HasMetadata> ingressesWithResolvedRefsFalse = dependencies.ingresses().values().stream()
-                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                .map(HasMetadata.class::cast);
-        Stream<HasMetadata> filtersWithResolvedRefsFalse = dependencies.filters().values().stream()
-                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                .map(HasMetadata.class::cast);
-        Stream<HasMetadata> kafkaServicesWithResolvedRefsFalse = dependencies.kafkaServices().values().stream()
-                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                .map(HasMetadata.class::cast);
-        Stream<LocalRef<?>> localRefStream = Stream
-                .of(ingressesWithResolvedRefsFalse, filtersWithResolvedRefsFalse, kafkaServicesWithResolvedRefsFalse)
-                .flatMap(Function.identity())
-                .map(ResourcesUtil::toLocalRef);
-        return localRefStream.collect(Collectors.toSet());
-    }
-
-    private static boolean hasAnyResolvedRefsFalse(List<Condition> conditions) {
-        return conditions.stream().anyMatch(Condition::isResolvedRefsFalse);
+        return new ClusterResolutionResult(cluster,
+                resolveProxy(clusterRef, cluster, proxies),
+                resolveFilters(clusterRef, cluster, result),
+                resolveService(clusterRef, cluster, result),
+                resolveIngresses(clusterRef, cluster, result, proxies));
     }
 
-    private static Stream<DanglingReference> determineIngressesWithDanglingProxyRefs(Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses,
-                                                                                     Set<KafkaProxy> proxies) {
-        Set<LocalRef<KafkaProxy>> localRefs = proxies.stream().map(ResourcesUtil::toLocalRef).collect(Collectors.toSet());
-        return ingresses.values().stream().flatMap(ingress -> {
-            ProxyRef proxyRef = ingress.getSpec().getProxyRef();
-            if (!localRefs.contains(proxyRef)) {
-                return Stream.of(new DanglingReference(toLocalRef(ingress), proxyRef));
-            }
-            else {
-                return Stream.of();
-            }
-        });
-    }
-
-    private static Optional<DanglingReference> determineDanglingKafkaProxyRefs(LocalRef<VirtualKafkaCluster> clusterRef, VirtualKafkaClusterSpec spec,
-                                                                               Set<KafkaProxy> proxies) {
-        Set<LocalRef<KafkaProxy>> localRefs = proxies.stream().map(ResourcesUtil::toLocalRef).collect(Collectors.toSet());
-        ProxyRef proxyRef = spec.getProxyRef();
-        if (!localRefs.contains(proxyRef)) {
-            return Optional.of(new DanglingReference(clusterRef, proxyRef));
-        }
-        else {
-            return Optional.empty();
-        }
+    private ResolutionResult<KafkaProxy> resolveProxy(LocalRef<?> referrer, VirtualKafkaCluster cluster, Set<KafkaProxy> proxies) {
+        ProxyRef proxyRef = cluster.getSpec().getProxyRef();
+        return resolveProxy(referrer, proxies, proxyRef);
     }
 
-    private static Stream<DanglingReference> determineDanglingFilterRefs(LocalRef<VirtualKafkaCluster> clusterRef, VirtualKafkaClusterSpec spec,
-                                                                         Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters) {
-        List<FilterRef> filtersList = spec.getFilterRefs();
-        if (filtersList == null) {
-            return Stream.empty();
-        }
-        else {
-            return filtersList.stream()
-                    .filter(filterRef -> filters.values().stream().noneMatch(filterResource -> filterResourceMatchesRef(filterRef, filterResource)))
-                    .map(ref -> new DanglingReference(clusterRef, ref));
-        }
+    private static ResolutionResult<KafkaProxy> resolveProxy(LocalRef<?> referrer, Set<KafkaProxy> proxies, ProxyRef proxyRef) {
+        return proxies.stream().filter(p -> ResourcesUtil.toLocalRef(p).equals(proxyRef)).findFirst()
+                .map(p -> new ResolutionResult<>(referrer, proxyRef, Referent.from(p))).orElse(new ResolutionResult<>(referrer, proxyRef, null));
     }
 
-    private static Optional<DanglingReference> determineDanglingKafkaServiceRefs(LocalRef<VirtualKafkaCluster> clusterRef, VirtualKafkaClusterSpec spec,
-                                                                                 Map<LocalRef<KafkaService>, KafkaService> clusterRefs) {
-        var serviceRef = spec.getTargetKafkaServiceRef();
-        if (!clusterRefs.containsKey(serviceRef)) {
-            return Optional.of(new DanglingReference(clusterRef, serviceRef));
-        }
-        else {
-            return Optional.empty();
-        }
+    private List<ResolutionResult<KafkaProtocolFilter>> resolveFilters(LocalRef<VirtualKafkaCluster> clusterRef, VirtualKafkaCluster cluster, CommonDependencies result) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolver.java
user: login: tombentley
body: Let's not call the parameter `result`, it's confusing.
created_at: 2025-05-08T03:14:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import java.util.Optional;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
+import io.kroxylicious.kubernetes.api.common.LocalRef;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of attempting to dereference a reference
+ * @param referrer the referring resource's LocalRef
+ * @param reference the reference we attempted to dereference
+ * @param referent the referent if the attempt to dereference was successful, null otherwise
+ * @param <T> the referent type
+ */
+public record ResolutionResult<T extends HasMetadata>(LocalRef<?> referrer, LocalRef<T> reference, @Nullable Referent<T> referent) {
+    /**
+     * If the dereference fails because we cannot locate a referent for a reference, we call this a dangling reference
+     * @return true iff no referent was found for this reference
+     */
+    boolean dangling() {
+        return referent == null;
+    }
+
+    public T referentResource() {
+        if (referent == null) {
+            throw new IllegalStateException("Referent is null!");
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: tombentley
body: This feels like a NPE at a higher level.
created_at: 2025-05-08T03:20:26Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,172 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
      *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
      *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
      * </ol>
+     * @return true if fully resolved
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * Find all dangling references (recursive) from this VirtualKafkaCluster
+     * @return a stream of all dangling references
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences() {
+        return allReferentResolutionResults().filter(ResolutionResult::dangling)
+                .map(resolutionResult1 -> new ClusterResolutionResult.DanglingReference(resolutionResult1.referrer(), resolutionResult1.reference()));
     }
 
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    /**
+     * Find all dangling references (recursive) from the specified referrer to the specified kind
+     * @param fromReferrer a LocalRef identifying the Referrer
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified LocalRef to the specified kind
+     */
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences(LocalRef<?> fromReferrer, String toKind) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: k-wall
body: This class has quite a big API surface.  It is quite a lot to absorb.  I wonder if you can reduce the number of methods by accepting a Predicate eg. findDanglingReferences(Predicate) rather than plethora overloads.

created_at: 2025-05-09T14:00:11Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: k-wall
body: +1.I started down that path when I did the the virtualcluster TLS work.  The PR got unwieldy and I stepped back to something smaller.

One unsolved issue I had was where to implement the Secret and ConfigMap content validations. By that I mean, the Kubernetes Secret has to be of type `kubernetes.io/tls or a particular data key has to end with `.pem`, `.p12` etc). Either the ClusterResolutionResult needs to expose the successfully resolved resources (so that the validation can be done in the Reconcilers), or the DependencyResolver needs to accept arbitrary validators (and have a mechanism to get back sufficient details to write a good condition message).   The former seemed more appealing.
created_at: 2025-05-09T14:19:16Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: +1, let resolver handle lookups, referential integrity (including considering the ResolvedRefs conditions of Referents), stale status check also feels closely related.

But once we are looking into the Referents, inspecting their spec/data (beyond following refs) I think that's some other component's job.
created_at: 2025-05-11T21:53:00Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: SamBarker
body: > But once we are looking into the Referents, inspecting their spec/data (beyond following refs) I think that's some other component's job.

Yeah I think that model works nicely. For adding checksums to Virtual clusters I was looking for something to give me the fully resolved and valid graph that I could just walk but it doesn't really exist on main and is a quite awkward to extract as things stand.
created_at: 2025-05-11T22:01:41Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: Yep, after this branch merges it should be easy to add a `Stream<HasMetadata> ClusterResolutionResult#referents()`
created_at: 2025-05-11T22:04:18Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: Or you can walk the different Lists of Referents if the client wants more control over order. 
created_at: 2025-05-11T22:05:48Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: > fully resolved and valid graph

I had this idea implemented earlier in this PR, but backed off it because there's already a lot of change/noise and classes to understand (ResolvedCluster vs VirtualKafkaCluster vs ClusterResolutionResult). Can revisit again in a future PR.
created_at: 2025-05-11T22:17:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
+import io.kroxylicious.kubernetes.api.common.Condition;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngressStatus;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyStatus;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaServiceStatus;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaClusterStatus;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterStatus;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+
+/**
+ * A single referent
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/Referent.java
user: login: robobario
body: Referent is a little wrapper class around the Referent resource so there is a common interface for finding stale or ResolvedRefs=False Referents. Ideally we'd be able to have these interfaces directly on the Resource classes.

Maybe calling it a `Referent` isn't that useful, it's describing something about what status/conditions the resource has. HasCommonStatus? KroxyliciousResource? Something saying it has some features common to all our CRs.
created_at: 2025-05-11T22:49:38Z
author_association: MEMBER
diff_hunk: @@ -32,26 +24,10 @@
  * describes which dependencies could not be resolved per VirtualKafkaCluster.
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ProxyResolutionResult.java
user: login: robobario
body: purged all `deref` mentions and removed most usages of `dependency/dependencies`. I'll put up a subsequent PR to rename `DependencyResolver` to try and get away from the `dependency` noun altogether.
created_at: 2025-05-11T23:06:23Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,95 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 /**
- * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
- * whether there were any resolution problems like dangling references, or referenced resources
- * that have a ResolvedRefs: False condition.
+ * The result of a deep resolution of the transitive references of a single VirtualKafkaCluster. Describes
+ * whether there were any resolution problems like dangling references, or Referents that have a ResolvedRefs=False
+ * condition, or Referents that have a stale status.
+ *
+ * @param cluster the cluster resource that was resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec,
+ * note that the same Filter can be in this list multiple times
+ * @param serviceResolutionResult the resolution result for the kafka service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling references)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has dangling references</li>
      * </ol>
+     * @return true if fully resolved
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean allReferentsFullyResolved() {
+        return allDanglingReferences().findAny().isEmpty() && allResolvedReferents()
+                .noneMatch(ResourcesUtil::hasResolvedRefsFalseCondition);
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * @return a stream of all dangling references
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
-    }
-
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    public Stream<DanglingReference> allDanglingReferences() {
+        return allResolutionResults().filter(ResolutionResult::dangling)
+                .map(result -> new DanglingReference(result.referrer(), result.reference()));
     }
 
-    public boolean anyResolvedRefsConditionsFalse() {
-        return !resourcesWithResolvedRefsFalse.isEmpty();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
-        return resourcesWithResolvedRefsFalse.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findReferentsWithStaleStatus() {
-        return referentsWithStaleStatus.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
-        return findResourcesWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
-    }
-
-    ClusterResolutionResult addAllResourcesHavingResolvedRefsFalse(Set<LocalRef<?>> resolvedRefsFalse) {
-        return new ClusterResolutionResult(cluster, danglingReferences, Stream.concat(resourcesWithResolvedRefsFalse.stream(), resolvedRefsFalse.stream()).collect(
-                Collectors.toSet()), referentsWithStaleStatus);
+    /**
+     * @return a stream of all resolved referents of this Cluster, note that KafkaProxy is not considered a Referent of VirtualKafkaCluster to prevent cycles
+     */
+    public Stream<HasMetadata> allResolvedReferents() {
+        return allResolutionResults()
+                .filter(resolutionResult -> !Objects.equals(resolutionResult.reference().getKind(), HasMetadata.getKind(KafkaProxy.class)))
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: This is the ugly code I mentioned in standup, `allResolutionResults` contains results for anything that refers to a KafkaProxy, so we can find dangling refs where we could not resolve a KafkaProxy .

Until now we have not considered the staleness of the KafkaProxy resources, I was unsure if we should require a fresh status on all KafkaProxy referents in case we create a deadlock where both the KafkaProxyReconciler requires a fresh status on the VKC, and the VKC Reconciler requires a fresh status on the KafkaProxy.
created_at: 2025-05-12T03:47:41Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,95 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 /**
- * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
- * whether there were any resolution problems like dangling references, or referenced resources
- * that have a ResolvedRefs: False condition.
+ * The result of a deep resolution of the transitive references of a single VirtualKafkaCluster. Describes
+ * whether there were any resolution problems like dangling references, or Referents that have a ResolvedRefs=False
+ * condition, or Referents that have a stale status.
+ *
+ * @param cluster the cluster resource that was resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec,
+ * note that the same Filter can be in this list multiple times
+ * @param serviceResolutionResult the resolution result for the kafka service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling references)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has dangling references</li>
      * </ol>
+     * @return true if fully resolved
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean allReferentsFullyResolved() {
+        return allDanglingReferences().findAny().isEmpty() && allResolvedReferents()
+                .noneMatch(ResourcesUtil::hasResolvedRefsFalseCondition);
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * @return a stream of all dangling references
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
-    }
-
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    public Stream<DanglingReference> allDanglingReferences() {
+        return allResolutionResults().filter(ResolutionResult::dangling)
+                .map(result -> new DanglingReference(result.referrer(), result.reference()));
     }
 
-    public boolean anyResolvedRefsConditionsFalse() {
-        return !resourcesWithResolvedRefsFalse.isEmpty();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
-        return resourcesWithResolvedRefsFalse.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findReferentsWithStaleStatus() {
-        return referentsWithStaleStatus.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
-        return findResourcesWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
-    }
-
-    ClusterResolutionResult addAllResourcesHavingResolvedRefsFalse(Set<LocalRef<?>> resolvedRefsFalse) {
-        return new ClusterResolutionResult(cluster, danglingReferences, Stream.concat(resourcesWithResolvedRefsFalse.stream(), resolvedRefsFalse.stream()).collect(
-                Collectors.toSet()), referentsWithStaleStatus);
+    /**
+     * @return a stream of all resolved referents of this Cluster, note that KafkaProxy is not considered a Referent of VirtualKafkaCluster to prevent cycles
+     */
+    public Stream<HasMetadata> allResolvedReferents() {
+        return allResolutionResults()
+                .filter(resolutionResult -> !Objects.equals(resolutionResult.reference().getKind(), HasMetadata.getKind(KafkaProxy.class)))
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: would suggest we merge without an answer, keeping the current behaviour, while we discuss
created_at: 2025-05-12T03:54:24Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,95 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 /**
- * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
- * whether there were any resolution problems like dangling references, or referenced resources
- * that have a ResolvedRefs: False condition.
+ * The result of a deep resolution of the transitive references of a single VirtualKafkaCluster. Describes
+ * whether there were any resolution problems like dangling references, or Referents that have a ResolvedRefs=False
+ * condition, or Referents that have a stale status.
+ *
+ * @param cluster the cluster resource that was resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec,
+ * note that the same Filter can be in this list multiple times
+ * @param serviceResolutionResult the resolution result for the kafka service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling references)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has dangling references</li>
      * </ol>
+     * @return true if fully resolved
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean allReferentsFullyResolved() {
+        return allDanglingReferences().findAny().isEmpty() && allResolvedReferents()
+                .noneMatch(ResourcesUtil::hasResolvedRefsFalseCondition);
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * @return a stream of all dangling references
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
-    }
-
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    public Stream<DanglingReference> allDanglingReferences() {
+        return allResolutionResults().filter(ResolutionResult::dangling)
+                .map(result -> new DanglingReference(result.referrer(), result.reference()));
     }
 
-    public boolean anyResolvedRefsConditionsFalse() {
-        return !resourcesWithResolvedRefsFalse.isEmpty();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
-        return resourcesWithResolvedRefsFalse.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findReferentsWithStaleStatus() {
-        return referentsWithStaleStatus.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
-        return findResourcesWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
-    }
-
-    ClusterResolutionResult addAllResourcesHavingResolvedRefsFalse(Set<LocalRef<?>> resolvedRefsFalse) {
-        return new ClusterResolutionResult(cluster, danglingReferences, Stream.concat(resourcesWithResolvedRefsFalse.stream(), resolvedRefsFalse.stream()).collect(
-                Collectors.toSet()), referentsWithStaleStatus);
+    /**
+     * @return a stream of all resolved referents of this Cluster, note that KafkaProxy is not considered a Referent of VirtualKafkaCluster to prevent cycles
+     */
+    public Stream<HasMetadata> allResolvedReferents() {
+        return allResolutionResults()
+                .filter(resolutionResult -> !Objects.equals(resolutionResult.reference().getKind(), HasMetadata.getKind(KafkaProxy.class)))
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: SamBarker
body: Setting aside the question of wether we should ignore Staleness of KafkaProxy for a moment. It feels strange that this type specific behaviour is buried in a generic class. Personally I think we should overload with a version that accepts the filter and ask the VKC Reconciler to pass its special sauce predicate. The default predicate would just accept everything. 

Should the KafkaProxy Reconciler have the reverse condition to break the cycle on that side too? 
created_at: 2025-05-12T04:00:17Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,95 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 /**
- * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
- * whether there were any resolution problems like dangling references, or referenced resources
- * that have a ResolvedRefs: False condition.
+ * The result of a deep resolution of the transitive references of a single VirtualKafkaCluster. Describes
+ * whether there were any resolution problems like dangling references, or Referents that have a ResolvedRefs=False
+ * condition, or Referents that have a stale status.
+ *
+ * @param cluster the cluster resource that was resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec,
+ * note that the same Filter can be in this list multiple times
+ * @param serviceResolutionResult the resolution result for the kafka service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling references)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has dangling references</li>
      * </ol>
+     * @return true if fully resolved
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean allReferentsFullyResolved() {
+        return allDanglingReferences().findAny().isEmpty() && allResolvedReferents()
+                .noneMatch(ResourcesUtil::hasResolvedRefsFalseCondition);
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * @return a stream of all dangling references
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
-    }
-
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    public Stream<DanglingReference> allDanglingReferences() {
+        return allResolutionResults().filter(ResolutionResult::dangling)
+                .map(result -> new DanglingReference(result.referrer(), result.reference()));
     }
 
-    public boolean anyResolvedRefsConditionsFalse() {
-        return !resourcesWithResolvedRefsFalse.isEmpty();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
-        return resourcesWithResolvedRefsFalse.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findReferentsWithStaleStatus() {
-        return referentsWithStaleStatus.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
-        return findResourcesWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
-    }
-
-    ClusterResolutionResult addAllResourcesHavingResolvedRefsFalse(Set<LocalRef<?>> resolvedRefsFalse) {
-        return new ClusterResolutionResult(cluster, danglingReferences, Stream.concat(resourcesWithResolvedRefsFalse.stream(), resolvedRefsFalse.stream()).collect(
-                Collectors.toSet()), referentsWithStaleStatus);
+    /**
+     * @return a stream of all resolved referents of this Cluster, note that KafkaProxy is not considered a Referent of VirtualKafkaCluster to prevent cycles
+     */
+    public Stream<HasMetadata> allResolvedReferents() {
+        return allResolutionResults()
+                .filter(resolutionResult -> !Objects.equals(resolutionResult.reference().getKind(), HasMetadata.getKind(KafkaProxy.class)))
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: tombentley
body: I think we can wave this away by chanting "eventual consistency" can't we? By which I mean we only care about staleness for resources which have refs of some kind. A `KafkaProxy` does not have any refs, so the KPRec shouldn't care that a VKC which references the KP is stale (KPRec should ignore the staleness and proceed as if if the VKC is fresh). That's because we'd expect the VKCRec to shortly reconcile the VKC, at which point the KP should be reconciled again.

Maybe that's what you were describing when I interrupted during standup, in which case sorry if I derailed things.
created_at: 2025-05-12T04:03:19Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,172 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
      *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
      *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
      * </ol>
+     * @return true if fully resolved
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean allReferentsFullyResolved() {
+        return !anyReferentResolvedRefsConditionsFalse() && !anyDanglingReferences();
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * Find all dangling references (recursive) from this VirtualKafkaCluster
+     * @return a stream of all dangling references
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences() {
+        return allReferentResolutionResults().filter(ResolutionResult::dangling)
+                .map(resolutionResult1 -> new ClusterResolutionResult.DanglingReference(resolutionResult1.referrer(), resolutionResult1.reference()));
     }
 
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    /**
+     * Find all dangling references (recursive) from the specified referrer to the specified kind
+     * @param fromReferrer a LocalRef identifying the Referrer
+     * @param toKind a Referent kind
+     * @return a stream of all dangling references from the specified LocalRef to the specified kind
+     */
+    public Stream<ClusterResolutionResult.DanglingReference> findDanglingReferences(LocalRef<?> fromReferrer, String toKind) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: I've reduced it down to a small amount of methods plus some static Predicates since it was all just conveniences around the streams API. So now clients would call `result.allDanglingReferences().filter(predicate)`
created_at: 2025-05-12T04:03:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import java.util.Optional;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
+import io.kroxylicious.kubernetes.api.common.LocalRef;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of attempting to dereference a reference
+ * @param referrer the referring resource's LocalRef
+ * @param reference the reference we attempted to dereference
+ * @param referent the referent if the attempt to dereference was successful, null otherwise
+ * @param <T> the referent type
+ */
+public record ResolutionResult<T extends HasMetadata>(LocalRef<?> referrer, LocalRef<T> reference, @Nullable Referent<T> referent) {
+    /**
+     * If the dereference fails because we cannot locate a referent for a reference, we call this a dangling reference
+     * @return true iff no referent was found for this reference
+     */
+    boolean dangling() {
+        return referent == null;
+    }
+
+    public T referentResource() {
+        if (referent == null) {
+            throw new IllegalStateException("Referent is null!");
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: robobario
body: yeah NPE is appropriate here
created_at: 2025-05-12T04:12:37Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,95 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 /**
- * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
- * whether there were any resolution problems like dangling references, or referenced resources
- * that have a ResolvedRefs: False condition.
+ * The result of a deep resolution of the transitive references of a single VirtualKafkaCluster. Describes
+ * whether there were any resolution problems like dangling references, or Referents that have a ResolvedRefs=False
+ * condition, or Referents that have a stale status.
+ *
+ * @param cluster the cluster resource that was resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec,
+ * note that the same Filter can be in this list multiple times
+ * @param serviceResolutionResult the resolution result for the kafka service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling references)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has dangling references</li>
      * </ol>
+     * @return true if fully resolved
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean allReferentsFullyResolved() {
+        return allDanglingReferences().findAny().isEmpty() && allResolvedReferents()
+                .noneMatch(ResourcesUtil::hasResolvedRefsFalseCondition);
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * @return a stream of all dangling references
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
-    }
-
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    public Stream<DanglingReference> allDanglingReferences() {
+        return allResolutionResults().filter(ResolutionResult::dangling)
+                .map(result -> new DanglingReference(result.referrer(), result.reference()));
     }
 
-    public boolean anyResolvedRefsConditionsFalse() {
-        return !resourcesWithResolvedRefsFalse.isEmpty();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
-        return resourcesWithResolvedRefsFalse.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findReferentsWithStaleStatus() {
-        return referentsWithStaleStatus.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
-        return findResourcesWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
-    }
-
-    ClusterResolutionResult addAllResourcesHavingResolvedRefsFalse(Set<LocalRef<?>> resolvedRefsFalse) {
-        return new ClusterResolutionResult(cluster, danglingReferences, Stream.concat(resourcesWithResolvedRefsFalse.stream(), resolvedRefsFalse.stream()).collect(
-                Collectors.toSet()), referentsWithStaleStatus);
+    /**
+     * @return a stream of all resolved referents of this Cluster, note that KafkaProxy is not considered a Referent of VirtualKafkaCluster to prevent cycles
+     */
+    public Stream<HasMetadata> allResolvedReferents() {
+        return allResolutionResults()
+                .filter(resolutionResult -> !Objects.equals(resolutionResult.reference().getKind(), HasMetadata.getKind(KafkaProxy.class)))
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: > VKC Reconciler to pass its special sauce predicate. The default predicate would just accept everything.

Both reconcilers do the same resolution work per VKC, resolving proxyRef on VKC and ingresses. It could be possible during KafkaProxy reconcilation that we observe an ingress that refers to a non-existent KafkaProxy and that should cause some failure. The difference is the source of the KafkaProxy, VKC Reconciler gets it from the JOSDK Context, KPReconciler just passes through the primary as the sole available KafkaProxy.

Both reconcilers currently want to exclude these resolved KafkaProxies from staleness checking
created_at: 2025-05-12T04:21:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
+import io.kroxylicious.kubernetes.api.common.Condition;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngressStatus;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyStatus;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaServiceStatus;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaClusterStatus;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterStatus;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+
+/**
+ * A single referent
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/Referent.java
user: login: robobario
body: Have removed `Referent`, instead the user can get at the resource directly and ask if it's stale/hasResolvedRefsFalseCondition via static methods on ResourcesUtil
created_at: 2025-05-12T04:23:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,61 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.model.ingress;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.api.v1alpha1.kafkaproxyingressspec.ClusterIP;
+import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRanges;
+import io.kroxylicious.kubernetes.api.v1alpha1.kafkaservicespec.NodeIdRangesBuilder;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
+
+/**
+ * IngressDefinitionBuilder is responsible for creating richer IngressDefinition objects from the raw
+ * Custom Resources that define it.
+ */
+class IngressDefinitionBuilder {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/model/ingress/IngressDefinitionBuilder.java
user: login: robobario
body: This is `Ingresses` renamed, the diff shows it as a create/delete
created_at: 2025-05-12T04:25:33Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,197 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.model.ResolvedCluster;
 
 /**
  * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
  * whether there were any resolution problems like dangling references, or referenced resources
  * that have a ResolvedRefs: False condition.
+ *
+ * @param cluster the cluster resource being recursively resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
+ * @param serviceResolutionResult the resolution result for service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: created #2169 for refactoring (imagine this is more off in the nice-to-haves while we get the essentials done)
created_at: 2025-05-12T04:33:17Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,95 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 /**
- * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
- * whether there were any resolution problems like dangling references, or referenced resources
- * that have a ResolvedRefs: False condition.
+ * The result of a deep resolution of the transitive references of a single VirtualKafkaCluster. Describes
+ * whether there were any resolution problems like dangling references, or Referents that have a ResolvedRefs=False
+ * condition, or Referents that have a stale status.
+ *
+ * @param cluster the cluster resource that was resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec,
+ * note that the same Filter can be in this list multiple times
+ * @param serviceResolutionResult the resolution result for the kafka service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling references)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has dangling references</li>
      * </ol>
+     * @return true if fully resolved
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean allReferentsFullyResolved() {
+        return allDanglingReferences().findAny().isEmpty() && allResolvedReferents()
+                .noneMatch(ResourcesUtil::hasResolvedRefsFalseCondition);
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * @return a stream of all dangling references
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
-    }
-
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    public Stream<DanglingReference> allDanglingReferences() {
+        return allResolutionResults().filter(ResolutionResult::dangling)
+                .map(result -> new DanglingReference(result.referrer(), result.reference()));
     }
 
-    public boolean anyResolvedRefsConditionsFalse() {
-        return !resourcesWithResolvedRefsFalse.isEmpty();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
-        return resourcesWithResolvedRefsFalse.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findReferentsWithStaleStatus() {
-        return referentsWithStaleStatus.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
-        return findResourcesWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
-    }
-
-    ClusterResolutionResult addAllResourcesHavingResolvedRefsFalse(Set<LocalRef<?>> resolvedRefsFalse) {
-        return new ClusterResolutionResult(cluster, danglingReferences, Stream.concat(resourcesWithResolvedRefsFalse.stream(), resolvedRefsFalse.stream()).collect(
-                Collectors.toSet()), referentsWithStaleStatus);
+    /**
+     * @return a stream of all resolved referents of this Cluster, note that KafkaProxy is not considered a Referent of VirtualKafkaCluster to prevent cycles
+     */
+    public Stream<HasMetadata> allResolvedReferents() {
+        return allResolutionResults()
+                .filter(resolutionResult -> !Objects.equals(resolutionResult.reference().getKind(), HasMetadata.getKind(KafkaProxy.class)))
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: robobario
body: @SamBarker is that a blocker for you? Maybe we could use a better term than `allReferents` maybe it's more accurately the `directReferents` of the cluster? Or we should't offer this stream of all referents munged together and clients should be forced to operate on the referents they are interested in. 

The hassle with that is that there are several places we just want to check that everything is resolved and we are able to proceed, so a bit of duplication required, or a shared static method, and at that point why not have the method on this result class.
created_at: 2025-05-12T21:45:59Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,95 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 /**
- * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
- * whether there were any resolution problems like dangling references, or referenced resources
- * that have a ResolvedRefs: False condition.
+ * The result of a deep resolution of the transitive references of a single VirtualKafkaCluster. Describes
+ * whether there were any resolution problems like dangling references, or Referents that have a ResolvedRefs=False
+ * condition, or Referents that have a stale status.
+ *
+ * @param cluster the cluster resource that was resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec,
+ * note that the same Filter can be in this list multiple times
+ * @param serviceResolutionResult the resolution result for the kafka service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling references)</li>
+     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has dangling references</li>
      * </ol>
+     * @return true if fully resolved
      */
-    public boolean isFullyResolved() {
-        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    public boolean allReferentsFullyResolved() {
+        return allDanglingReferences().findAny().isEmpty() && allResolvedReferents()
+                .noneMatch(ResourcesUtil::hasResolvedRefsFalseCondition);
     }
 
     /**
-     * A result is fully reconciled if all referents have been reconciled.
-     * @return true if all referents have been reconciled
+     * @return a stream of all dangling references
      */
-    public boolean allReferentsHaveFreshStatus() {
-        return referentsWithStaleStatus.isEmpty();
-    }
-
-    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
-        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    public Stream<DanglingReference> allDanglingReferences() {
+        return allResolutionResults().filter(ResolutionResult::dangling)
+                .map(result -> new DanglingReference(result.referrer(), result.reference()));
     }
 
-    public boolean anyResolvedRefsConditionsFalse() {
-        return !resourcesWithResolvedRefsFalse.isEmpty();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
-        return resourcesWithResolvedRefsFalse.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findReferentsWithStaleStatus() {
-        return referentsWithStaleStatus.stream();
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
-        return findResourcesWithResolvedRefsFalse().filter(r -> kind.equals(r.getKind()));
-    }
-
-    ClusterResolutionResult addAllResourcesHavingResolvedRefsFalse(Set<LocalRef<?>> resolvedRefsFalse) {
-        return new ClusterResolutionResult(cluster, danglingReferences, Stream.concat(resourcesWithResolvedRefsFalse.stream(), resolvedRefsFalse.stream()).collect(
-                Collectors.toSet()), referentsWithStaleStatus);
+    /**
+     * @return a stream of all resolved referents of this Cluster, note that KafkaProxy is not considered a Referent of VirtualKafkaCluster to prevent cycles
+     */
+    public Stream<HasMetadata> allResolvedReferents() {
+        return allResolutionResults()
+                .filter(resolutionResult -> !Objects.equals(resolutionResult.reference().getKind(), HasMetadata.getKind(KafkaProxy.class)))
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: SamBarker
body: No not a blocker for me.

Definitely a clean up we can deal with separately.
created_at: 2025-05-12T21:54:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,40 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package io.kroxylicious.kubernetes.operator.resolver;
+
+import java.util.Optional;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
+import io.kroxylicious.kubernetes.api.common.LocalRef;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+/**
+ * The result of attempting to resolve a reference
+ *
+ * @param <T> the referent type
+ * @param referrer the referring resource represented as a LocalRef
+ * @param reference the reference we attempted to resolve
+ */
+public record ResolutionResult<T extends HasMetadata>(LocalRef<?> referrer, LocalRef<T> reference, @Nullable T referentNew) {
+    /**
+     * If the resolution fails because we cannot locate a referent for a reference, we call this a dangling reference
+     * @return true iff no referent was found for this reference
+     */
+    boolean dangling() {
+        return referentNew == null;
+    }
+
+    public T referentResource() {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: SamBarker
body: Looking at usages of this I think it would be helpful to offer 
```
public LocalRef<T> referentResourceRef() {
        return ResourcesUtil.toLocalRef(referentResource());
    }
```

As part of the API (as 3 out of 4 usages convert to a local ref)
created_at: 2025-05-13T01:38:36Z
author_association: MEMBER
diff_hunk: @@ -51,8 +52,10 @@ public Map<String, Service> desiredResources(
         KafkaProxyContext kafkaProxyContext = KafkaProxyContext.proxyContext(context);
         var model = kafkaProxyContext.model();
         Stream<Service> serviceStream = model.clustersWithValidIngresses().stream()
+                .map(ClusterResolutionResult::cluster)
                 .filter(cluster -> !kafkaProxyContext.isBroken(cluster))
-                .flatMap(cluster -> model.ingressModel().clusterIngressModel(cluster).map(ProxyIngressModel.VirtualClusterIngressModel::services).orElse(Stream.empty()));
+                .flatMap(cluster -> model.ingressModel().clusterIngressModel(cluster)
+                        .map(ProxyIngressModel.VirtualClusterIngressModel::services).orElse(Stream.empty()));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ClusterServiceDependentResource.java
user: login: SamBarker
body: nit: 
```suggestion
                        .map(ProxyIngressModel.VirtualClusterIngressModel::services)
                        .orElse(Stream.empty()));
```
created_at: 2025-05-13T01:40:54Z
author_association: MEMBER
diff_hunk: @@ -92,25 +93,33 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigStateData data) {
         proxyModel.resolutionResult().clusterResolutionResults().stream()
-                .filter(result -> !result.isFullyResolved())
+                .filter(result -> !result.allReferentsFullyResolved() || ResourcesUtil.hasResolvedRefsFalseCondition(result.cluster()))
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigStateDependentResource.java
user: login: SamBarker
body: I want to extract this as:

```
    private static boolean isClusterInvalid(ClusterResolutionResult result) {
        return !result.allReferentsFullyResolved() || ResourcesUtil.hasResolvedRefsFalseCondition(result.cluster());
    
    }
```    

So that I can read the filter call as `.filter(ProxyConfigStateDependentResource::isClusterInvalid)` (its a bit verbose but I think it makes clearer what we are trying to do here.
created_at: 2025-05-13T01:49:22Z
author_association: MEMBER
diff_hunk: @@ -92,25 +93,33 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigStateData data) {
         proxyModel.resolutionResult().clusterResolutionResults().stream()
-                .filter(result -> !result.isFullyResolved())
+                .filter(result -> !result.allReferentsFullyResolved() || ResourcesUtil.hasResolvedRefsFalseCondition(result.cluster()))
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigStateDependentResource.java
user: login: SamBarker
body: Separately I wonder about the fact we have `!`  for one condition. I wonder would the API be more useful as `result.hasUnresolvedReferent()` or maybe `result.hasAnyUnresolvedReferents`
created_at: 2025-05-13T01:52:02Z
author_association: MEMBER
diff_hunk: @@ -92,25 +93,33 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigStateData data) {
         proxyModel.resolutionResult().clusterResolutionResults().stream()
-                .filter(result -> !result.isFullyResolved())
+                .filter(result -> !result.allReferentsFullyResolved() || ResourcesUtil.hasResolvedRefsFalseCondition(result.cluster()))
                 .forEach(clusterResolutionResult -> {
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
                     VirtualKafkaCluster patch;
-                    if (!clusterResolutionResult.danglingReferences().isEmpty()) {
+                    if (clusterResolutionResult.allDanglingReferences().findAny().isPresent()) {
                         Comparator<ClusterResolutionResult.DanglingReference> comparator = Comparator.<ClusterResolutionResult.DanglingReference, LocalRef> comparing(
-                                ClusterResolutionResult.DanglingReference::to);
+                                ClusterResolutionResult.DanglingReference::absentRef);
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigStateDependentResource.java
user: login: SamBarker
body: Can we pull the definition of the comparator out? Probably a constant so its easier to read what we are trying to do
created_at: 2025-05-13T01:55:06Z
author_association: MEMBER
diff_hunk: @@ -92,25 +93,33 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigStateData data) {
         proxyModel.resolutionResult().clusterResolutionResults().stream()
-                .filter(result -> !result.isFullyResolved())
+                .filter(result -> !result.allReferentsFullyResolved() || ResourcesUtil.hasResolvedRefsFalseCondition(result.cluster()))
                 .forEach(clusterResolutionResult -> {
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
                     VirtualKafkaCluster patch;
-                    if (!clusterResolutionResult.danglingReferences().isEmpty()) {
+                    if (clusterResolutionResult.allDanglingReferences().findAny().isPresent()) {
                         Comparator<ClusterResolutionResult.DanglingReference> comparator = Comparator.<ClusterResolutionResult.DanglingReference, LocalRef> comparing(
-                                ClusterResolutionResult.DanglingReference::to);
+                                ClusterResolutionResult.DanglingReference::absentRef);
 
-                        LocalRef<?> firstDanglingDependency = clusterResolutionResult.danglingReferences().stream()
-                                .sorted(comparator).map(ClusterResolutionResult.DanglingReference::to).findFirst()
+                        LocalRef<?> firstDanglingReference = clusterResolutionResult.allDanglingReferences()
+                                .sorted(comparator).map(ClusterResolutionResult.DanglingReference::absentRef).findFirst()
                                 .orElseThrow();
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigStateDependentResource.java
user: login: SamBarker
body: I know this is a refactor but why do we only report the first error (so not really a question for this PR)? Why not join the whole stream and report all the errors as we know them?
created_at: 2025-05-13T01:56:05Z
author_association: MEMBER
diff_hunk: @@ -132,7 +132,7 @@ public static Map<String, String> podLabels(KafkaProxy primary) {
     private PodTemplateSpec podTemplate(KafkaProxy primary,
                                         KafkaProxyContext kafkaProxyContext,
                                         ProxyIngressModel ingressModel,
-                                        List<VirtualKafkaCluster> virtualKafkaClusters,
+                                        List<ClusterResolutionResult> virtualKafkaClusters,
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyDeploymentDependentResource.java
user: login: SamBarker
body: name has got out of sync with what it is.
created_at: 2025-05-13T01:57:17Z
author_association: MEMBER
diff_hunk: @@ -204,8 +204,8 @@ private Container proxyContainer(KafkaProxyContext kafkaProxyContext,
                 .endPort();
         // broker ports
         virtualKafkaClusters.forEach(virtualKafkaCluster -> {
-            if (!kafkaProxyContext.isBroken(virtualKafkaCluster)) {
-                ProxyIngressModel.VirtualClusterIngressModel virtualClusterIngressModel = ingressModel.clusterIngressModel(virtualKafkaCluster).orElseThrow();
+            if (!kafkaProxyContext.isBroken(virtualKafkaCluster.cluster())) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyDeploymentDependentResource.java
user: login: SamBarker
body: `virtualKafkaCluster.cluster()` feels like an odd thing to read maybe `resolutionResult.cluster()` would work better. 
created_at: 2025-05-13T01:57:56Z
author_association: MEMBER
diff_hunk: @@ -312,41 +314,73 @@ private static String slug(LocalRef<?> ref) {
     /**
      * Checks that the status observedGeneration is equal to the metadata generation. Indicating
      * that the current {@code spec} of the resource has been reconciled.
-     * @param cluster cluster
+     * @param hasMetadata hasMetadata
+     * @throws IllegalStateException if hasMetadata is not a CustomResource type owned by the Kroxylicious Operator
      * @return true if status observedGeneration is equal to the metadata generation
      */
-    public static boolean isStatusFresh(VirtualKafkaCluster cluster) {
-        return isStatusFresh(cluster, c -> Optional.ofNullable(c.getStatus()).map(VirtualKafkaClusterStatus::getObservedGeneration).orElse(null));
+    public static boolean isStatusFresh(HasMetadata hasMetadata) {
+        Objects.requireNonNull(hasMetadata);
+        if (hasMetadata instanceof KafkaProtocolFilter filter) {
+            return isStatusFresh(filter, i -> Optional.ofNullable(i.getStatus()).map(KafkaProtocolFilterStatus::getObservedGeneration).orElse(null));
+        }
+        else if (hasMetadata instanceof KafkaService service) {
+            return isStatusFresh(service, i -> Optional.ofNullable(i.getStatus()).map(KafkaServiceStatus::getObservedGeneration).orElse(null));
+        }
+        else if (hasMetadata instanceof VirtualKafkaCluster cluster) {
+            return isStatusFresh(cluster, c -> Optional.ofNullable(c.getStatus()).map(VirtualKafkaClusterStatus::getObservedGeneration).orElse(null));
+        }
+        else if (hasMetadata instanceof KafkaProxyIngress ingress) {
+            return isStatusFresh(ingress, i -> Optional.ofNullable(i.getStatus()).map(KafkaProxyIngressStatus::getObservedGeneration).orElse(null));
+        }
+        else if (hasMetadata instanceof KafkaProxy kafkaProxy) {
+            return isStatusFresh(kafkaProxy, i -> Optional.ofNullable(i.getStatus()).map(KafkaProxyStatus::getObservedGeneration).orElse(null));
+        }
+        else {
+            throw new IllegalArgumentException("Unknown resource type: " + hasMetadata.getClass().getName());
+        }
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ResourcesUtil.java
user: login: SamBarker
body: Ohh for pattern matching switch. Need a JDK baseline upgrade for that though.
created_at: 2025-05-13T01:59:29Z
author_association: MEMBER
diff_hunk: @@ -258,28 +261,43 @@ private Optional<OwnerReference> extractOwnerRefFromKubernetesService(Service se
     private VirtualKafkaCluster handleResolutionProblems(VirtualKafkaCluster cluster,
                                                          ClusterResolutionResult clusterResolutionResult) {
         LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
-        var unresolvedIngressProxies = clusterResolutionResult.findDanglingReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND).collect(Collectors.toSet());
-        if (clusterResolutionResult.anyDependenciesNotFoundFor(clusterRef)) {
+        var unresolvedIngressProxies = clusterResolutionResult.allDanglingReferences()
+                .filter(DanglingReference.hasReferrerKind(KAFKA_PROXY_INGRESS_KIND).and(DanglingReference.hasReferentKind(KAFKA_PROXY_KIND))).collect(Collectors.toSet());
+        if (clusterResolutionResult.allDanglingReferences().anyMatch(DanglingReference.hasReferrer(clusterRef))) {
             Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster,
-                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_PROXY_KIND));
+                    clusterResolutionResult.allDanglingReferences()
+                            .filter(DanglingReference.hasReferrer(clusterRef).and(DanglingReference.hasReferentKind(KAFKA_PROXY_KIND)))
+                            .map(DanglingReference::absentRef));
             Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
-                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_SERVICE_KIND));
+                    clusterResolutionResult.allDanglingReferences()
+                            .filter(DanglingReference.hasReferrer(clusterRef).and(DanglingReference.hasReferentKind(KAFKA_SERVICE_KIND)))
+                            .map(DanglingReference::absentRef));
             Stream<String> ingressMsg = refsMessage("spec.ingresses[].ingressRef references ", cluster,
-                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_PROXY_INGRESS_KIND));
+                    clusterResolutionResult.allDanglingReferences()
+                            .filter(DanglingReference.hasReferrer(clusterRef).and(DanglingReference.hasReferentKind(KAFKA_PROXY_INGRESS_KIND)))
+                            .map(DanglingReference::absentRef));
             Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
-                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_PROTOCOL_FILTER_KIND));
+                    clusterResolutionResult.allDanglingReferences()
+                            .filter(DanglingReference.hasReferrer(clusterRef).and(DanglingReference.hasReferentKind(KAFKA_PROTOCOL_FILTER_KIND)))
+                            .map(DanglingReference::absentRef));
             return statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_REFS_NOT_FOUND,
                     joiningMessages(proxyMsg, serviceMsg, ingressMsg, filterMsg));
         }
-        else if (clusterResolutionResult.anyResolvedRefsConditionsFalse() || !unresolvedIngressProxies.isEmpty()) {
+        else if (clusterResolutionResult.allResolvedReferents().anyMatch(ResourcesUtil::hasResolvedRefsFalseCondition) || !unresolvedIngressProxies.isEmpty()) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: SamBarker
body: Again I'd like a method to wrap this compound condition to give it an intention revealing name.
created_at: 2025-05-13T02:00:31Z
author_association: MEMBER
diff_hunk: @@ -6,109 +6,95 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
 import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 
 /**
- * The result of a deep resolution of the dependencies of a single VirtualKafkaCluster. Describes
- * whether there were any resolution problems like dangling references, or referenced resources
- * that have a ResolvedRefs: False condition.
+ * The result of a deep resolution of the transitive references of a single VirtualKafkaCluster. Describes
+ * whether there were any resolution problems like dangling references, or Referents that have a ResolvedRefs=False
+ * condition, or Referents that have a stale status.
+ *
+ * @param cluster the cluster resource that was resolved
+ * @param proxyResolutionResult the resolution result for the KafkaProxy resource this cluster references
+ * @param filterResolutionResults resolution results for all filters referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec,
+ * note that the same Filter can be in this list multiple times
+ * @param serviceResolutionResult the resolution result for the kafka service referenced by the cluster
+ * @param ingressResolutionResults the resolution results for ingresses referenced by the cluster, in the same order they are declared in the VirtualKafkaCluster spec
  */
 public record ClusterResolutionResult(VirtualKafkaCluster cluster,
-                                      Set<DanglingReference> danglingReferences,
-                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse,
-                                      Set<LocalRef<?>> referentsWithStaleStatus) {
-    public ClusterResolutionResult {
-        Objects.requireNonNull(danglingReferences);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
-        Objects.requireNonNull(from);
-        Objects.requireNonNull(kindTo);
-        return danglingReferences.stream().filter(r -> r.from().equals(from) && kindTo.equals(r.to.getKind())).map(DanglingReference::to);
-    }
-
-    // using wildcards intentionally
-    @SuppressWarnings("java:S1452")
-    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
-        Objects.requireNonNull(fromKind);
-        Objects.requireNonNull(toKind);
-        return danglingReferences.stream().filter(r -> fromKind.equals(r.from().getKind()) && toKind.equals(r.to().getKind()))
-                .map(DanglingReference::to);
-    }
+                                      ResolutionResult<KafkaProxy> proxyResolutionResult,
+                                      List<ResolutionResult<KafkaProtocolFilter>> filterResolutionResults,
+                                      ResolutionResult<KafkaService> serviceResolutionResult,
+                                      List<IngressResolutionResult> ingressResolutionResults) {
 
     /**
-     * A result is fully resolved if:
+     * All referents are fully resolved iff
      * <ol>
-     *     <li>All references can be retrieved from kubernetes (we have no dangling refs)</li>
-     *     <li>No referent has a ResolvedRefs=False condition, which declares that it has unresolved dependencies</li>
+     *     <li>All references can be retrieved from kubernetes (we have no dangling references)</li>
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
user: login: SamBarker
body: is it clear to a reader what a `dangling reference` is?  
created_at: 2025-05-13T02:01:25Z
author_association: MEMBER

PR comments:
user: login: robobario
created_at: 2025-05-12T04:01:35Z
body: I've deleted `Referent` and removed most of the methods on `ClusterResolutionResult` in favour of offering `Predicate`s for use with the streaming API to filter DanglingReferences and referent resources. Since a lot of the methods were eventually just conveniences the streams API offers anyway like `anyMatch`, `allMatch` etc.
author_association: MEMBER
user: login: sonarqubecloud[bot]
created_at: 2025-05-13T03:54:40Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=2137) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [3 New issues](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=2137&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [1 Accepted issue](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=2137&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=2137&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [86.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=2137&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=2137&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=2137)


author_association: NONE