Repository: kroxylicious/kroxylicious
Pull request #2035: Refactor dependency resolution
Author: login: robobario
Labels: name: kind/refactoring
name: kube/operator
description: Relates to the Kubernetes operator
Created: 2025-04-08T01:14:18Z | Merged: 2025-04-10T02:57:01Z

### Type of change

- Refactoring
- Enhancement

### Description

Use the DependencyResolver from the VirtualKafkaClusterReconciler, the logic for resolving the dependencies of a VKC is similar to what we have to do in the KafkaProxyReconciler so they can both use a DependencyResolver to handle the common task of extracting all the dependencies from the context and discovering all the resolution issues like missing dependencies and `ResolvedRefs=False` conditions on the dependencies.

Because the DependencyResolver is now aware of the ResolvedRefs=False cases we are pushed to make the KafkaProxyReconciler side handle those cases too. So there is an enhancement here that the KafkaProxyReconciler will not accept a VirtualKafkaCluster where any of it's dependencies have ResolvedRefs=false. Note that it doesn't consider the conditions of the VKC itself yet, just its dependencies.

### 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/api/common/Condition.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/api/common/Condition.java
index 00c99e8aa1..9150ee5cc8 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/api/common/Condition.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/api/common/Condition.java
@@ -200,4 +200,10 @@ public String getValue() {
             return value;
         }
     }
+
+    public static boolean isResolvedRefsFalse(Condition condition) {
+        Objects.requireNonNull(condition);
+        return Condition.Type.ResolvedRefs.equals(condition.getType())
+                && Condition.Status.FALSE.equals(condition.getStatus());
+    }
 }
\ No newline at end of file
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/OperatorMain.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/OperatorMain.java
index 2e745348ad..0ab7d3cf57 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/OperatorMain.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/OperatorMain.java
@@ -28,6 +28,7 @@
 import io.prometheus.metrics.exporter.httpserver.MetricsHandler;
 
 import io.kroxylicious.kubernetes.operator.management.UnsupportedHttpMethodFilter;
+import io.kroxylicious.kubernetes.operator.resolver.DependencyResolver;
 import io.kroxylicious.proxy.service.HostPort;
 import io.kroxylicious.proxy.tag.VisibleForTesting;
 
@@ -80,7 +81,7 @@ public static void main(String[] args) {
     void start() {
         operator.installShutdownHook(Duration.ofSeconds(10));
         operator.register(new KafkaProxyReconciler(Clock.systemUTC(), SecureConfigInterpolator.DEFAULT_INTERPOLATOR));
-        operator.register(new VirtualKafkaClusterReconciler(Clock.systemUTC()));
+        operator.register(new VirtualKafkaClusterReconciler(Clock.systemUTC(), DependencyResolver.create()));
         operator.register(new KafkaProxyIngressReconciler(Clock.systemUTC()));
         operator.register(new KafkaServiceReconciler(Clock.systemUTC()));
         operator.register(new KafkaProtocolFilterReconciler(Clock.systemUTC(), SecureConfigInterpolator.DEFAULT_INTERPOLATOR));
diff --git a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigReconcilePrecondition.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigReconcilePrecondition.java
index c3d3247f05..b9f6875f27 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigReconcilePrecondition.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigReconcilePrecondition.java
@@ -31,7 +31,7 @@
 import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterSpec;
 import io.kroxylicious.kubernetes.operator.model.ProxyModel;
 import io.kroxylicious.kubernetes.operator.model.ingress.ProxyIngressModel;
-import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
 import io.kroxylicious.proxy.config.Configuration;
 import io.kroxylicious.proxy.config.IllegalConfigurationException;
 import io.kroxylicious.proxy.config.NamedFilterDefinition;
@@ -116,7 +116,7 @@ private static String filterDefinitionName(FilterRef filterCrRef) {
         return filterCrRef.getName() + "." + filterCrRef.getKind() + "." + filterCrRef.getGroup();
     }
 
-    private List<NamedFilterDefinition> filterDefinitions(Context<KafkaProxy> context, VirtualKafkaCluster cluster, ResolutionResult resolutionResult) {
+    private List<NamedFilterDefinition> filterDefinitions(Context<KafkaProxy> context, VirtualKafkaCluster cluster, ProxyResolutionResult resolutionResult) {
 
         return Optional.ofNullable(cluster.getSpec().getFilterRefs()).orElse(List.of()).stream().map(filterCrRef -> {
 
@@ -165,4 +165,4 @@ private static VirtualCluster getVirtualCluster(VirtualKafkaCluster cluster,
                 filterNamesForCluster(cluster));
     }
 
-}
\ No newline at end of file
+}
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 6905257031..7e2a874eb4 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
@@ -20,7 +20,7 @@
 import io.kroxylicious.kubernetes.operator.model.ProxyModel;
 import io.kroxylicious.kubernetes.operator.model.ingress.IngressConflictException;
 import io.kroxylicious.kubernetes.operator.model.ingress.ProxyIngressModel;
-import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
 
 import static io.kroxylicious.kubernetes.operator.Labels.standardLabels;
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.namespace;
@@ -91,25 +91,33 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
     }
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigStateData data) {
-        proxyModel.resolutionResult().clusterResults().stream()
-                .filter(ResolutionResult.ClusterResolutionResult::isAnyDependencyUnresolved)
+        proxyModel.resolutionResult().clusterResolutionResults().stream()
+                .filter(result -> !result.isFullyResolved())
                 .forEach(clusterResolutionResult -> {
-
-                    Comparator<LocalRef<?>> comparator = Comparator.<LocalRef<?>, String> comparing(LocalRef::getGroup)
-                            .thenComparing(LocalRef::getKind)
-                            .thenComparing(LocalRef::getName);
-
-                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedDependencySet().stream()
-                            .sorted(comparator).findFirst()
-                            .orElseThrow();
-                    String message = String.format("Resource %s was not found.",
-                            ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
-
+                    VirtualKafkaCluster patch;
+                    if (!clusterResolutionResult.danglingReferences().isEmpty()) {
+                        Comparator<ClusterResolutionResult.DanglingReference> comparator = Comparator.<ClusterResolutionResult.DanglingReference, LocalRef> comparing(
+                                ClusterResolutionResult.DanglingReference::to);
+
+                        LocalRef<?> firstDanglingDependency = clusterResolutionResult.danglingReferences().stream()
+                                .sorted(comparator).map(ClusterResolutionResult.DanglingReference::to).findFirst()
+                                .orElseThrow();
+                        String message = String.format("Resource %s was not found.",
+                                ResourcesUtil.namespacedSlug(firstDanglingDependency, cluster));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
+                    }
+                    else {
+                        String message = String.format("Resource %s has ResolvedRefs=False.",
+                                ResourcesUtil.namespacedSlug(clusterResolutionResult.findResourcesWithResolvedRefsFalse().sorted().findFirst().orElseThrow(),
+                                        cluster));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
+                    }
                     data.addStatusPatchForCluster(
                             ResourcesUtil.name(cluster),
-                            statusFactory.newFalseConditionStatusPatch(cluster,
-                                    Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message));
+                            patch);
                 });
     }
 
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 9387d00e2a..63e99b5af5 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
@@ -7,11 +7,9 @@
 package io.kroxylicious.kubernetes.operator;
 
 import java.time.Clock;
-import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
-import java.util.TreeSet;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -20,7 +18,6 @@
 import org.slf4j.LoggerFactory;
 
 import io.fabric8.kubernetes.api.model.ConfigMap;
-import io.fabric8.kubernetes.client.CustomResource;
 import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration;
 import io.javaoperatorsdk.operator.api.reconciler.Context;
 import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl;
@@ -34,22 +31,22 @@
 import io.kroxylicious.kubernetes.api.common.AnyLocalRefBuilder;
 import io.kroxylicious.kubernetes.api.common.Condition;
 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.filter.api.v1alpha1.KafkaProtocolFilter;
-import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterStatus;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.DependencyResolver;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 
+import static io.fabric8.kubernetes.api.model.HasMetadata.getKind;
 import static io.kroxylicious.kubernetes.operator.ProxyConfigStateDependentResource.CONFIG_STATE_CONFIG_MAP_SUFFIX;
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.namespace;
+import static io.kroxylicious.kubernetes.operator.ResourcesUtil.toLocalRef;
 
 /**
  * Reconciles a {@link VirtualKafkaCluster} by checking whether the resources
@@ -68,117 +65,88 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String FILTERS_EVENT_SOURCE_NAME = "filters";
     public static final String TRANSITIVELY_REFERENCED_RESOURCES_NOT_FOUND = "TransitivelyReferencedResourcesNotFound";
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
+    private static final String KAFKA_PROXY_INGRESS_KIND = getKind(KafkaProxyIngress.class);
+    private static final String KAFKA_PROXY_KIND = getKind(KafkaProxy.class);
+    private static final String KAFKA_SERVICE_KIND = getKind(KafkaService.class);
+    private static final String KAFKA_PROTOCOL_FILTER_KIND = getKind(KafkaProtocolFilter.class);
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_STATE_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigStateData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ClusterResolutionResult resolutionResult = resolver.resolveClusterRefs(cluster, context);
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        if (resolutionResult.isFullyResolved()) {
+            updateControl = maybeCombineStatusWithClusterConfigMap(cluster, context);
         }
         else {
-            Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster, missingProxies);
-            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, missingServices);
-            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, missingIngresses);
-            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, missingFilters);
-
-            uc = UpdateControl.patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_REFS_NOT_FOUND,
-                    joiningMessages(proxyMsg, serviceMsg, ingressMsg, filterMsg)));
+            updateControl = handleResolutionProblems(cluster, resolutionResult);
         }
-
         if (LOGGER.isInfoEnabled()) {
             LOGGER.info("Completed reconciliation of {}/{}", namespace(cluster), name(cluster));
         }
-        return uc;
+        return updateControl;
+    }
+
+    private @NonNull UpdateControl<VirtualKafkaCluster> maybeCombineStatusWithClusterConfigMap(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        updateControl = context
+                .getSecondaryResource(ConfigMap.class)
+                .flatMap(cm -> Optional.ofNullable(cm.getData()))
+                .map(ProxyConfigStateData::new)
+                .flatMap(data -> data.getStatusPatchForCluster(name(cluster)))
+                .map(patch -> {
+                    var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
+                    var acc = ResourceState.fromList(patch.getStatus().getConditions());
+                    return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
+                })
+                .map(UpdateControl::patchStatus)
+                .orElse(UpdateControl.noUpdate());
+        return updateControl;
+    }
+
+    private @NonNull UpdateControl<VirtualKafkaCluster> handleResolutionProblems(VirtualKafkaCluster cluster,
+                                                                                 ClusterResolutionResult clusterResolutionResult) {
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
+        var unresovedIngressProxies = clusterResolutionResult.findDanglingReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND).collect(Collectors.toSet());
+        if (clusterResolutionResult.anyDependenciesNotFoundFor(clusterRef)) {
+            Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster,
+                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_PROXY_KIND));
+            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
+                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_SERVICE_KIND));
+            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster,
+                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_PROXY_INGRESS_KIND));
+            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
+                    clusterResolutionResult.findDanglingReferences(clusterRef, KAFKA_PROTOCOL_FILTER_KIND));
+            updateControl = UpdateControl.patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_REFS_NOT_FOUND,
+                    joiningMessages(proxyMsg, serviceMsg, ingressMsg, filterMsg)));
+        }
+        else if (clusterResolutionResult.anyResolvedRefsConditionsFalse() || !unresovedIngressProxies.isEmpty()) {
+            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
+                    clusterResolutionResult.findResourcesWithResolvedRefsFalse(KAFKA_SERVICE_KIND));
+            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster,
+                    clusterResolutionResult.findResourcesWithResolvedRefsFalse(KAFKA_PROXY_INGRESS_KIND));
+            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
+                    clusterResolutionResult.findResourcesWithResolvedRefsFalse(KAFKA_PROTOCOL_FILTER_KIND));
+            Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster,
+                    clusterResolutionResult.findDanglingReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND));
+            updateControl = UpdateControl
+                    .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
+                            joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
+        }
+        else {
+            updateControl = UpdateControl
+                    .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_INVALID,
+                            joiningMessages(Stream.of("unknown dependency resolution issue"))));
+        }
+        return updateControl;
     }
 
     @NonNull
@@ -187,20 +155,15 @@ private static String joiningMessages(
         return Stream.of(serviceMsg).flatMap(Function.identity()).collect(Collectors.joining("; "));
     }
 
-    private static boolean hasAnyResolvedRefsFalse(List<Condition> conditions) {
-        return conditions.stream()
-                .anyMatch(c -> Condition.Type.ResolvedRefs.equals(c.getType())
-                        && Condition.Status.FALSE.equals(c.getStatus()));
-    }
-
     @NonNull
-    private static <R extends CustomResource<?, ?>> Stream<String> refsMessage(
-                                                                               String prefix,
-                                                                               VirtualKafkaCluster cluster,
-                                                                               TreeSet<? extends LocalRef<R>> refs) {
-        return refs.isEmpty() ? Stream.of()
+    private static Stream<String> refsMessage(
+                                              String prefix,
+                                              VirtualKafkaCluster cluster,
+                                              Stream<LocalRef<?>> refs) {
+        List<LocalRef<?>> sortedRefs = refs.sorted().toList();
+        return sortedRefs.isEmpty() ? Stream.of()
                 : Stream.of(
-                        prefix + refs.stream()
+                        prefix + sortedRefs.stream()
                                 .map(ref -> ResourcesUtil.namespacedSlug(ref, cluster))
                                 .collect(Collectors.joining(", ")));
     }
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 990a0019eb..7afbb80b51 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
@@ -10,7 +10,7 @@
 
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
 import io.kroxylicious.kubernetes.operator.model.ingress.ProxyIngressModel;
-import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
 
 /**
  * This class aims to encapsulate the logical model of what we want to manifest (it is a work in progress).
@@ -20,7 +20,7 @@
  * @param ingressModel the ingress model for this reconciliation
  * @param clustersWithValidIngresses virtual kafka clusters that are fully resolved and have valid ingresses
  */
-public record ProxyModel(ResolutionResult resolutionResult,
+public record ProxyModel(ProxyResolutionResult resolutionResult,
                          ProxyIngressModel ingressModel,
                          List<VirtualKafkaCluster> 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 3c3bff6e55..529da9cf53 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
@@ -18,8 +18,7 @@
 import io.kroxylicious.kubernetes.operator.model.ingress.IngressAllocator;
 import io.kroxylicious.kubernetes.operator.model.ingress.ProxyIngressModel;
 import io.kroxylicious.kubernetes.operator.resolver.DependencyResolver;
-import io.kroxylicious.kubernetes.operator.resolver.DependencyResolverImpl;
-import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
 
 /**
  * Takes a KafkaProxy, resolves all its dependencies, and then computes a ProxyModel
@@ -36,7 +35,7 @@ public ProxyModelBuilder(DependencyResolver resolver) {
     }
 
     public ProxyModel build(KafkaProxy primary, Context<KafkaProxy> context) {
-        ResolutionResult resolutionResult = resolver.deepResolve(context);
+        ProxyResolutionResult resolutionResult = resolver.resolveProxyRefs(primary, context);
         Set<KafkaProxyIngress> ingresses = resolutionResult.ingresses();
         // 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.
@@ -48,7 +47,7 @@ public ProxyModel build(KafkaProxy primary, Context<KafkaProxy> context) {
     }
 
     public static ProxyModelBuilder contextBuilder() {
-        return new ProxyModelBuilder(DependencyResolverImpl.create());
+        return new ProxyModelBuilder(DependencyResolver.create());
     }
 
 }
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 6bb8650dea..6c68ca68e8 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
@@ -12,7 +12,7 @@
 
 import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
 import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
-import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
 
 import static io.kroxylicious.kubernetes.operator.ProxyDeploymentDependentResource.PROXY_PORT_START;
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
@@ -39,7 +39,7 @@ private IngressAllocator() {
      * @return non-null ProxyIngressModel
      */
     public static ProxyIngressModel allocateProxyIngressModel(KafkaProxy primary,
-                                                              ResolutionResult resolutionResult) {
+                                                              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();
@@ -51,7 +51,7 @@ public static ProxyIngressModel allocateProxyIngressModel(KafkaProxy primary,
     }
 
     private static List<ProxyIngressModel.IngressModel> allocateIngressModel(KafkaProxy primary, VirtualKafkaCluster it, AtomicInteger ports,
-                                                                             ResolutionResult resolutionResult) {
+                                                                             ProxyResolutionResult resolutionResult) {
         Stream<IngressDefinition> ingressStream = Ingresses.ingressesFor(primary, it, resolutionResult);
         return ingressStream.map(resource -> {
             int toAllocate = resource.numIdentifyingPortsRequired();
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
index 2affb7d687..cd6abb50db 100644
--- 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
@@ -13,17 +13,17 @@
 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.operator.resolver.ResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ProxyResolutionResult;
 
 class Ingresses {
     private Ingresses() {
     }
 
-    public static Stream<IngressDefinition> ingressesFor(KafkaProxy primary, VirtualKafkaCluster cluster, ResolutionResult resolutionResult) {
+    public static Stream<IngressDefinition> ingressesFor(KafkaProxy primary, VirtualKafkaCluster cluster, ProxyResolutionResult resolutionResult) {
         return cluster.getSpec().getIngressRefs().stream()
                 .flatMap(
                         ingressRef -> {
-                            // skip unresolved ingresses, we are working with VirtualClusters that may have unresolved references
+                            // skip unresolved ingresses, we are working with VirtualClusters that may have dangling references
                             Optional<KafkaProxyIngress> ingress = resolutionResult.ingress(ingressRef);
                             return ingress.stream().map(kafkaProxyIngress -> toIngress(primary, cluster, kafkaProxyIngress));
                         });
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
new file mode 100644
index 0000000000..e3ddf143c0
--- /dev/null
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ClusterResolutionResult.java
@@ -0,0 +1,72 @@
+/*
+ * 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.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+
+/**
+ * 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.
+ */
+public record ClusterResolutionResult(VirtualKafkaCluster cluster,
+                                      Set<DanglingReference> danglingReferences,
+                                      Set<LocalRef<?>> resourcesWithResolvedRefsFalse) {
+    public ClusterResolutionResult {
+        Objects.requireNonNull(danglingReferences);
+    }
+
+    public Stream<LocalRef<?>> findDanglingReferences(LocalRef<?> from, String kindTo) {
+        Objects.requireNonNull(from);
+        Objects.requireNonNull(kindTo);
+        return danglingReferences.stream().filter(r -> r.from().equals(from) && r.to.getKind().equals(kindTo)).map(DanglingReference::to);
+    }
+
+    public Stream<LocalRef<?>> findDanglingReferences(String fromKind, String toKind) {
+        Objects.requireNonNull(fromKind);
+        Objects.requireNonNull(toKind);
+        return danglingReferences.stream().filter(r -> r.from().getKind().equals(fromKind) && r.to().getKind().equals(toKind))
+                .map(DanglingReference::to);
+    }
+
+    public boolean isFullyResolved() {
+        return danglingReferences.isEmpty() && resourcesWithResolvedRefsFalse.isEmpty();
+    }
+
+    public boolean anyDependenciesNotFoundFor(LocalRef<?> fromResource) {
+        return danglingReferences.stream().anyMatch(u -> u.from().equals(fromResource));
+    }
+
+    public boolean anyResolvedRefsConditionsFalse() {
+        return !resourcesWithResolvedRefsFalse.isEmpty();
+    }
+
+    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse() {
+        return resourcesWithResolvedRefsFalse.stream();
+    }
+
+    public Stream<LocalRef<?>> findResourcesWithResolvedRefsFalse(String kind) {
+        return findResourcesWithResolvedRefsFalse().filter(r -> r.getKind().equals(kind));
+    }
+
+    /**
+     * Describes the case where an entity A references an entity B but we can not find B.
+     * @param from
+     * @param to
+     */
+    public record DanglingReference(LocalRef<?> from, LocalRef<?> to) {
+        public DanglingReference {
+            Objects.requireNonNull(from);
+            Objects.requireNonNull(to);
+        }
+    }
+}
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 18091bc7a7..3652089885 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
@@ -6,29 +6,223 @@
 
 package io.kroxylicious.kubernetes.operator.resolver;
 
+import java.util.List;
+import java.util.Map;
+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.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.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 edu.umd.cs.findbugs.annotations.NonNull;
+
+import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
+import static io.kroxylicious.kubernetes.operator.ResourcesUtil.toLocalRef;
 
 /**
- * DependencyResolver resolves the dependencies of a KafkaProxy. We use numerous Custom Resources
- * to model the virtual clusters, ingresses, filters and cluster references that will eventually
- * manifest as a single proxy Deployment. The custom resources reference each other by name, and
- * sometimes group/kind.
+ * DependencyResolver resolves the dependencies of a KafkaProxy or VirtualKafkaCluster. We use numerous
+ * Custom Resources to model the virtual clusters, ingresses, filters and clusters that will eventually
+ * manifest as a single proxy Deployment.
  * <p>
- * DependencyResolver is responsible for resolving all of these references, returning a result that
- * contains the resolved Custom Resource instances, and a description of which references could not
- * be resolved.
- * </p>
+ * DependencyResolver is responsible for resolving all of these references into Custom Resources, returning
+ * a result that contains the resolved Custom Resource instances, and a description of any problems encountered.
+ * Examples of problems are:
+ * <ul>
+ * <li>
+ *     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
+ * </li>
+ * </ul>
  */
-public interface DependencyResolver {
+public class DependencyResolver {
+
+    private static final ProxyResolutionResult EMPTY_RESOLUTION_RESULT = new ProxyResolutionResult(Map.of(), Map.of(), Map.of(), Set.of());
+
+    private DependencyResolver() {
+    }
+
+    public static DependencyResolver create() {
+        return new DependencyResolver();
+    }
+
     /**
      * Resolves all dependencies of a KafkaProxy recursively (if there are dependencies
-     * of dependencies, we resolve them too).
+     * of dependencies, we resolve them too). Makes the resolved dependencies available and
+     * reports any problems during resolution.
      *
+     * @param proxy proxy
      * @param context reconciliation context for a KafkaProxy
-     * @return a resolution result containing all resolved resources, and a description of which resources could not be resolved
+     * @return a resolution result containing all resolved resources, and a description of resolution problems, if any
+     */
+    public ProxyResolutionResult resolveProxyRefs(KafkaProxy proxy, Context<?> context) {
+        Objects.requireNonNull(proxy);
+        Objects.requireNonNull(context);
+        Set<VirtualKafkaCluster> virtualKafkaClusters = context.getSecondaryResources(VirtualKafkaCluster.class);
+        return resolve(virtualKafkaClusters, Set.of(proxy), context);
+    }
+
+    /**
+     * Resolves all dependencies of a VirtualKafkaCluster recursively (if there are dependencies
+     * of dependencies, we resolve them too) and reports any problems during resolution.
+     *
+     * @param cluster cluster being resolved
+     * @param context reconciliation context for a VirtualKafkaCluster
+     * @return cluster resolution result containing a description of resolution problems, if any
      */
-    ResolutionResult deepResolve(Context<KafkaProxy> context);
+    public ClusterResolutionResult resolveClusterRefs(VirtualKafkaCluster cluster, Context<?> context) {
+        Objects.requireNonNull(cluster);
+        Objects.requireNonNull(context);
+        Set<KafkaProxy> proxies = context.getSecondaryResources(KafkaProxy.class);
+        return resolve(Set.of(cluster), proxies, context).clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("resolution result for cluster not found in result, should be impossible"));
+    }
+
+    private @NonNull ProxyResolutionResult resolve(Set<VirtualKafkaCluster> virtualKafkaClusters, Set<KafkaProxy> proxies, Context<?> context) {
+        if (virtualKafkaClusters.isEmpty()) {
+            return EMPTY_RESOLUTION_RESULT;
+        }
+
+        Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses = context.getSecondaryResources(KafkaProxyIngress.class).stream()
+                .collect(ResourcesUtil.toByLocalRefMap());
+        Map<LocalRef<KafkaService>, KafkaService> clusterRefs = context.getSecondaryResources(KafkaService.class).stream()
+                .collect(ResourcesUtil.toByLocalRefMap());
+        Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters = context.getSecondaryResources(KafkaProtocolFilter.class).stream()
+                .collect(ResourcesUtil.toByLocalRefMap());
+        var clusterResolutionResults = virtualKafkaClusters.stream()
+                .map(cluster -> discoverProblemsAndBuildResolutionResult(cluster, ingresses, clusterRefs, filters, proxies)).collect(Collectors.toSet());
+        return new ProxyResolutionResult(filters, ingresses, clusterRefs, clusterResolutionResults);
+    }
+
+    private ClusterResolutionResult discoverProblemsAndBuildResolutionResult(VirtualKafkaCluster cluster,
+                                                                             Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses,
+                                                                             Map<LocalRef<KafkaService>, KafkaService> services,
+                                                                             Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters,
+                                                                             Set<KafkaProxy> proxies) {
+        VirtualKafkaClusterSpec spec = cluster.getSpec();
+        LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
+        Set<DanglingReference> danglingReferences = determineDanglingRefs(ingresses, services, filters, proxies, clusterRef, spec);
+        return new ClusterResolutionResult(cluster, danglingReferences, determineResolvedRefsFalse(ingresses, services, filters));
+    }
+
+    private static @NonNull Set<DanglingReference> determineDanglingRefs(Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses,
+                                                                         Map<LocalRef<KafkaService>, KafkaService> services,
+                                                                         Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters, Set<KafkaProxy> proxies,
+                                                                         LocalRef<VirtualKafkaCluster> clusterRef, VirtualKafkaClusterSpec spec) {
+        var danglingIngressRefs = determineDanglingIngressRefs(clusterRef, spec, ingresses);
+        var danglingProxyRefs = determineDanglingKafkaProxyRefs(clusterRef, spec, proxies).stream();
+        var danglingIngressProxyRefs = determineIngressesWithDanglingProxyRefs(ingresses, proxies);
+        var danglingServiceRefs = determineDanglingKafkaServiceRefs(clusterRef, spec, services).stream();
+        var danglingFilterRefs = determineDanglingFilterRefs(clusterRef, spec, filters);
+        return Stream.of(danglingIngressRefs, danglingProxyRefs, danglingIngressProxyRefs, danglingServiceRefs, danglingFilterRefs)
+                .flatMap(Function.identity()).collect(Collectors.toSet());
+    }
+
+    private static @NonNull Set<LocalRef<?>> determineResolvedRefsFalse(Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses,
+                                                                        Map<LocalRef<KafkaService>, KafkaService> services,
+                                                                        Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters) {
+        Stream<HasMetadata> ingressesWithResolvedRefsFalse = ingresses.values().stream()
+                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
+                .map(HasMetadata.class::cast);
+        Stream<HasMetadata> filtersWithResolvedRefsFalse = filters.values().stream()
+                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
+                .map(HasMetadata.class::cast);
+        Stream<HasMetadata> kafkaServicesWithResolvedRefsFalse = services.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();
+            }
+        });
+    }
+
+    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.getIngressRefs().stream()
+                .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/DependencyResolverImpl.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImpl.java
deleted file mode 100644
index 9bbcac6c61..0000000000
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImpl.java
+++ /dev/null
@@ -1,114 +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.resolver;
-
-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.stream.Collectors;
-import java.util.stream.Stream;
-
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-import io.kroxylicious.kubernetes.api.common.FilterRef;
-import io.kroxylicious.kubernetes.api.common.LocalRef;
-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.VirtualKafkaClusterSpec;
-import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
-import io.kroxylicious.kubernetes.operator.ResourcesUtil;
-import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.ClusterResolutionResult;
-
-import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
-
-public class DependencyResolverImpl implements DependencyResolver {
-
-    private static final ResolutionResult EMPTY_RESOLUTION_RESULT = new ResolutionResult(Map.of(), Map.of(), Map.of(), Set.of());
-
-    private DependencyResolverImpl() {
-    }
-
-    public static DependencyResolver create() {
-        return new DependencyResolverImpl();
-    }
-
-    @Override
-    public ResolutionResult deepResolve(Context<KafkaProxy> context) {
-        Objects.requireNonNull(context);
-        Set<VirtualKafkaCluster> virtualKafkaClusters = context.getSecondaryResources(VirtualKafkaCluster.class);
-        if (virtualKafkaClusters.isEmpty()) {
-            return EMPTY_RESOLUTION_RESULT;
-        }
-
-        // TODO Replace this with the code from the VKCReconciler
-        Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses = context.getSecondaryResources(KafkaProxyIngress.class).stream()
-                .collect(ResourcesUtil.toByLocalRefMap());
-        Map<LocalRef<KafkaService>, KafkaService> clusterRefs = context.getSecondaryResources(KafkaService.class).stream()
-                .collect(ResourcesUtil.toByLocalRefMap());
-        Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters = context.getSecondaryResources(KafkaProtocolFilter.class).stream()
-                .collect(ResourcesUtil.toByLocalRefMap());
-        var resolutionResult = virtualKafkaClusters.stream()
-                .map(cluster -> determineUnresolvedDependencies(cluster, ingresses, clusterRefs, filters))
-                .collect(Collectors.toSet());
-        return new ResolutionResult(filters, ingresses, clusterRefs, resolutionResult);
-    }
-
-    private ClusterResolutionResult determineUnresolvedDependencies(VirtualKafkaCluster cluster,
-                                                                    Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses,
-                                                                    Map<LocalRef<KafkaService>, KafkaService> clusterRefs,
-                                                                    Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters) {
-        VirtualKafkaClusterSpec spec = cluster.getSpec();
-        Set<LocalRef<?>> unresolvedDependencies = new HashSet<>();
-        determineUnresolvedIngresses(spec, ingresses).forEach(unresolvedDependencies::add);
-        determineUnresolvedKafkaService(spec, clusterRefs).ifPresent(unresolvedDependencies::add);
-        determineUnresolvedFilters(spec, filters).forEach(unresolvedDependencies::add);
-        return new ClusterResolutionResult(cluster, unresolvedDependencies);
-    }
-
-    private Stream<? extends LocalRef<?>> determineUnresolvedFilters(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)));
-        }
-    }
-
-    private Optional<LocalRef<?>> determineUnresolvedKafkaService(VirtualKafkaClusterSpec spec,
-                                                                  Map<LocalRef<KafkaService>, KafkaService> clusterRefs) {
-        var clusterRef = spec.getTargetKafkaServiceRef();
-        if (!clusterRefs.containsKey(clusterRef)) {
-            return Optional.of(clusterRef);
-        }
-        else {
-            return Optional.empty();
-        }
-    }
-
-    private static Stream<? extends LocalRef<?>> determineUnresolvedIngresses(VirtualKafkaClusterSpec spec,
-                                                                              Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses) {
-        return spec.getIngressRefs().stream()
-                .filter(ref -> !ingresses.containsKey(ref));
-    }
-
-    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/ResolutionResult.java b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ProxyResolutionResult.java
similarity index 76%
rename from kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
rename to kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ProxyResolutionResult.java
index 531c20eee8..c202d1c00d 100644
--- a/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
+++ b/kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ProxyResolutionResult.java
@@ -30,32 +30,20 @@
  * Filters, KafkaProxyIngresses and KafkaServices that were successfully resolved. It also
  * describes which dependencies could not be resolved per VirtualKafkaCluster.
  */
-public class ResolutionResult {
+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 record ClusterResolutionResult(VirtualKafkaCluster cluster, Set<LocalRef<?>> unresolvedDependencySet) {
-        public ClusterResolutionResult {
-            Objects.requireNonNull(cluster);
-            Objects.requireNonNull(unresolvedDependencySet);
-        }
-
-        public boolean isAnyDependencyUnresolved() {
-            return !unresolvedDependencySet.isEmpty();
-        }
-
-        public boolean isFullyResolved() {
-            return !isAnyDependencyUnresolved();
-        }
-
+    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
+        return clusterResolutionResults.stream().filter(result -> result.cluster().equals(cluster)).findFirst();
     }
 
-    ResolutionResult(Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters,
-                     Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> kafkaProxyIngresses,
-                     Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs,
-                     Set<ClusterResolutionResult> clusterResolutionResults) {
+    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);
@@ -71,27 +59,27 @@ public boolean isFullyResolved() {
      * @return non-null list of VirtualKafkaClusters sorted by metadata.name
      */
     public List<VirtualKafkaCluster> fullyResolvedClustersInNameOrder() {
-        return clusterResults(ClusterResolutionResult::isFullyResolved);
+        return clustersSatisfying(ClusterResolutionResult::isFullyResolved);
     }
 
     /**
-     * Get all VirtualKafkaClusters, even if they have unresolved dependencies, sorted by the VirtualKafkaCluster's metadata.name
+     * Get all VirtualKafkaClusters, even if they have dangling dependencies, sorted by the VirtualKafkaCluster's metadata.name
      * @return non-null list of VirtualKafkaClusters sorted by metadata.name
      */
     public List<VirtualKafkaCluster> allClustersInNameOrder() {
-        return clusterResults(result -> true);
+        return clustersSatisfying(result -> true);
     }
 
-    private List<VirtualKafkaCluster> clusterResults(Predicate<ClusterResolutionResult> include) {
-        return clusterResolutionResults.stream().filter(include).map(ClusterResolutionResult::cluster)
-                .sorted(comparing(ResourcesUtil::name)).toList();
+    private List<VirtualKafkaCluster> clustersSatisfying(Predicate<ClusterResolutionResult> include) {
+        return clusterResolutionResults.stream().filter(include)
+                .sorted(comparing(r -> ResourcesUtil.name(r.cluster()))).map(ClusterResolutionResult::cluster).toList();
     }
 
     /**
      * Get all ClusterResolutionResult
      * @return all ClusterResolutionResult
      */
-    public Collection<ClusterResolutionResult> clusterResults() {
+    public Set<ClusterResolutionResult> clusterResolutionResults() {
         return clusterResolutionResults;
     }
 
diff --git a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconcilerIT.java b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconcilerIT.java
index a44761557e..003ffc1e61 100644
--- a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconcilerIT.java
+++ b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconcilerIT.java
@@ -37,6 +37,7 @@
 import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterBuilder;
 import io.kroxylicious.kubernetes.operator.assertj.ConditionListAssert;
 import io.kroxylicious.kubernetes.operator.assertj.VirtualKafkaClusterStatusAssert;
+import io.kroxylicious.kubernetes.operator.resolver.DependencyResolver;
 
 import edu.umd.cs.findbugs.annotations.Nullable;
 
@@ -64,7 +65,7 @@ class VirtualKafkaClusterReconcilerIT {
 
     @RegisterExtension
     LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder()
-            .withReconciler(new VirtualKafkaClusterReconciler(Clock.systemUTC()))
+            .withReconciler(new VirtualKafkaClusterReconciler(Clock.systemUTC(), DependencyResolver.create()))
             .withReconciler(new KafkaProxyReconciler(Clock.systemUTC(), SecureConfigInterpolator.DEFAULT_INTERPOLATOR))
             .withKubernetesClient(rbacHandler.operatorClient())
             .withAdditionalCustomResourceDefinition(KafkaProxyIngress.class)
diff --git a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconcilerTest.java b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconcilerTest.java
index 6c711a036c..d73796adab 100644
--- a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconcilerTest.java
+++ b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconcilerTest.java
@@ -36,6 +36,7 @@
 import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterBuilder;
 import io.kroxylicious.kubernetes.operator.assertj.ConditionListAssert;
 import io.kroxylicious.kubernetes.operator.assertj.VirtualKafkaClusterStatusAssert;
+import io.kroxylicious.kubernetes.operator.resolver.DependencyResolver;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 
@@ -171,7 +172,7 @@ static List<Arguments> shouldSetResolvedRefsToTrueOrFalse() {
                         assertResolvedRefsFalse(
                                 VirtualKafkaClusterReconciler.TRANSITIVELY_REFERENCED_RESOURCES_NOT_FOUND,
                                 "a spec.ingressRef had an inconsistent or missing proxyRef kafkaproxy.kroxylicious.io/not-my-proxy in namespace 'my-namespace'")),
-                Arguments.argumentSet("service has unresolved refs",
+                Arguments.argumentSet("service has ResolvedRefs=False condition",
                         CLUSTER_NO_FILTERS,
                         Optional.of(PROXY),
                         Optional.empty(),
@@ -198,7 +199,7 @@ static List<Arguments> shouldSetResolvedRefsToTrueOrFalse() {
                         assertResolvedRefsFalse(
                                 VirtualKafkaClusterReconciler.REFERENCED_RESOURCES_NOT_FOUND,
                                 "spec.ingressRefs references kafkaproxyingress.kroxylicious.io/my-ingress in namespace 'my-namespace'")),
-                Arguments.argumentSet("ingress has unresolved refs",
+                Arguments.argumentSet("ingress has ResolvedRefs=False condition",
                         CLUSTER_NO_FILTERS,
                         Optional.of(PROXY),
                         Optional.empty(),
@@ -224,7 +225,7 @@ static List<Arguments> shouldSetResolvedRefsToTrueOrFalse() {
                         assertResolvedRefsFalse(
                                 VirtualKafkaClusterReconciler.REFERENCED_RESOURCES_NOT_FOUND,
                                 "spec.filterRefs references kafkaprotocolfilter.filter.kroxylicious.io/my-filter in namespace 'my-namespace'")),
-                Arguments.argumentSet("filter has unresolved refs",
+                Arguments.argumentSet("filter has ResolvedRefs=False condition",
                         CLUSTER_ONE_FILTER,
                         Optional.of(PROXY),
                         Optional.empty(),
@@ -285,12 +286,12 @@ void shouldSetResolvedRefsToTrueOrFalse(VirtualKafkaCluster cluster,
                                             BiConsumer<VirtualKafkaCluster, ConditionListAssert> asserter) {
         // given
         Clock z = TEST_CLOCK;
-        var reconciler = new VirtualKafkaClusterReconciler(z);
+        var reconciler = new VirtualKafkaClusterReconciler(z, DependencyResolver.create());
 
         Context<VirtualKafkaCluster> context = mock(Context.class);
-        when(context.getSecondaryResource(KafkaProxy.class, VirtualKafkaClusterReconciler.PROXY_EVENT_SOURCE_NAME)).thenReturn(existingProxy);
-        when(context.getSecondaryResource(ConfigMap.class, VirtualKafkaClusterReconciler.PROXY_CONFIG_STATE_SOURCE_NAME)).thenReturn(existingProxyConfigMap);
-        when(context.getSecondaryResource(KafkaService.class, VirtualKafkaClusterReconciler.SERVICES_EVENT_SOURCE_NAME)).thenReturn(existingService);
+        when(context.getSecondaryResources(KafkaProxy.class)).thenReturn(existingProxy.map(Set::of).orElse(Set.of()));
+        when(context.getSecondaryResource(ConfigMap.class)).thenReturn(existingProxyConfigMap);
+        when(context.getSecondaryResources(KafkaService.class)).thenReturn(existingService.map(Set::of).orElse(Set.of()));
         when(context.getSecondaryResources(KafkaProxyIngress.class)).thenReturn(existingIngresses);
         when(context.getSecondaryResources(KafkaProtocolFilter.class)).thenReturn(existingFilters);
 
@@ -311,7 +312,7 @@ void shouldSetResolvedRefsToTrueOrFalse(VirtualKafkaCluster cluster,
     @Test
     void shouldSetResolvedRefsToUnknown() {
         // given
-        var reconciler = new VirtualKafkaClusterReconciler(TEST_CLOCK);
+        var reconciler = new VirtualKafkaClusterReconciler(TEST_CLOCK, DependencyResolver.create());
 
         Context<VirtualKafkaCluster> context = mock(Context.class);
 
diff --git a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImplTest.java b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImplTest.java
deleted file mode 100644
index a0778aae65..0000000000
--- a/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImplTest.java
+++ /dev/null
@@ -1,344 +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.resolver;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-import io.kroxylicious.kubernetes.api.common.FilterRef;
-import io.kroxylicious.kubernetes.api.common.FilterRefBuilder;
-import io.kroxylicious.kubernetes.api.common.IngressRef;
-import io.kroxylicious.kubernetes.api.common.IngressRefBuilder;
-import io.kroxylicious.kubernetes.api.common.KafkaServiceRefBuilder;
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngressBuilder;
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
-import io.kroxylicious.kubernetes.api.v1alpha1.KafkaServiceBuilder;
-import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
-import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaClusterBuilder;
-import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
-import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterBuilder;
-import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.ClusterResolutionResult;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mock.Strictness.LENIENT;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-class DependencyResolverImplTest {
-
-    @Mock(strictness = LENIENT)
-    Context<KafkaProxy> mockContext;
-
-    @Test
-    void testNoDependencies() {
-        givenFiltersInContext();
-        givenClusterRefsInContext();
-        givenIngressesInContext();
-        givenVirtualKafkaClustersInContext();
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // then
-        assertThat(resolutionResult.allClustersInNameOrder()).isEmpty();
-        assertThat(resolutionResult.clusterResults()).isEmpty();
-        assertThat(resolutionResult.ingresses()).isEmpty();
-        assertThat(resolutionResult.fullyResolvedClustersInNameOrder()).isEmpty();
-        assertThat(resolutionResult.filter(filterRef("c"))).isEmpty();
-        assertThat(resolutionResult.filters()).isEmpty();
-    }
-
-    @Test
-    void testNullFiltersOnVirtualClusterTolerated() {
-        givenFiltersInContext();
-        givenClusterRefsInContext(kafkaServiceRef("cluster"));
-        givenIngressesInContext();
-        VirtualKafkaCluster cluster = virtualCluster(null, "cluster", List.of());
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // then
-        assertThat(resolutionResult.allClustersInNameOrder()).containsExactly(cluster);
-        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.unresolvedDependencySet()).isEmpty();
-    }
-
-    @Test
-    void testSingleFilterUnreferenced() {
-        KafkaProtocolFilter filter = protocolFilter("filterName");
-        givenFiltersInContext(filter);
-        givenClusterRefsInContext(kafkaServiceRef("cluster"));
-        givenIngressesInContext();
-        VirtualKafkaCluster cluster = virtualCluster(List.of(), "cluster", List.of());
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // then
-        assertThat(resolutionResult.filter(filterRef("filterName"))).contains(filter);
-        assertThat(resolutionResult.filters()).containsExactly(filter);
-        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.unresolvedDependencySet()).isEmpty();
-    }
-
-    @Test
-    void testSingleFilterReferenced() {
-        KafkaProtocolFilter filter = protocolFilter("filterName");
-        givenFiltersInContext(filter);
-        givenClusterRefsInContext(kafkaServiceRef("cluster"));
-        givenIngressesInContext();
-        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName")), "cluster", List.of());
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // then
-        assertThat(resolutionResult.filter(filterRef("filterName"))).contains(filter);
-        assertThat(resolutionResult.filters()).containsExactly(filter);
-        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.unresolvedDependencySet()).isEmpty();
-    }
-
-    @Test
-    void testMultipleFiltersReferenced() {
-        KafkaProtocolFilter filter = protocolFilter("filterName");
-        KafkaProtocolFilter filter2 = protocolFilter("filterName2");
-        givenFiltersInContext(filter, filter2);
-        givenClusterRefsInContext(kafkaServiceRef("cluster"));
-        givenIngressesInContext();
-        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName"), filterRef("filterName2")), "cluster", List.of());
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // 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.unresolvedDependencySet()).isEmpty();
-    }
-
-    @Test
-    void testSubsetOfFiltersReferenced() {
-        KafkaProtocolFilter filter = protocolFilter("filterName");
-        givenFiltersInContext(filter);
-        givenClusterRefsInContext(kafkaServiceRef("cluster"));
-        givenIngressesInContext();
-        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName"), filterRef("filterName2")), "cluster", List.of());
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // 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.unresolvedDependencySet()).containsExactly(new FilterRefBuilder().withName("filterName2").build());
-    }
-
-    @Test
-    void testUnresolvedFilter() {
-        KafkaProtocolFilter filter = protocolFilter("filterName");
-        givenFiltersInContext(filter);
-        givenClusterRefsInContext(kafkaServiceRef("clusterRef"));
-        givenIngressesInContext();
-        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("other")), "clusterRef", List.of());
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // then
-        assertThat(resolutionResult.filter(filterRef("other"))).isEmpty();
-        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.unresolvedDependencySet()).containsExactly(new FilterRefBuilder().withName("other").build());
-    }
-
-    @Test
-    void testUnresolvedIngress() {
-        givenFiltersInContext();
-        givenClusterRefsInContext(kafkaServiceRef("clusterRef"));
-        givenIngressesInContext();
-        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingressMissing")));
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // then
-        assertThat(resolutionResult.ingresses()).isEmpty();
-        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.unresolvedDependencySet()).containsExactly(new IngressRefBuilder().withName("ingressMissing").build());
-    }
-
-    @Test
-    void testUnresolvedKafkaService() {
-        givenFiltersInContext();
-        givenClusterRefsInContext();
-        givenIngressesInContext();
-        VirtualKafkaCluster cluster = virtualCluster(List.of(), "missing", List.of());
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // then
-        assertThat(resolutionResult.ingresses()).isEmpty();
-        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.unresolvedDependencySet()).containsExactly(new KafkaServiceRefBuilder().withName("missing").build());
-    }
-
-    @Test
-    void testSingleResolvedIngress() {
-        givenFiltersInContext();
-        givenClusterRefsInContext(kafkaServiceRef("clusterRef"));
-        KafkaProxyIngress ingress = ingress("ingress");
-        givenIngressesInContext(ingress);
-        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress")));
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // then
-        assertThat(resolutionResult.ingresses()).containsExactly(ingress);
-        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.unresolvedDependencySet()).isEmpty();
-    }
-
-    @Test
-    void testMultipleResolvedIngresses() {
-        givenFiltersInContext();
-        givenClusterRefsInContext(kafkaServiceRef("clusterRef"));
-        KafkaProxyIngress ingress = ingress("ingress");
-        KafkaProxyIngress ingress2 = ingress("ingress2");
-        givenIngressesInContext(ingress, ingress2);
-        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress"), ingressRef("ingress2")));
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // then
-        assertThat(resolutionResult.ingresses()).containsExactlyInAnyOrder(ingress, ingress2);
-        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isTrue();
-        assertThat(onlyResult.unresolvedDependencySet()).isEmpty();
-    }
-
-    @Test
-    void testSubsetOfIngressesResolved() {
-        givenFiltersInContext();
-        givenClusterRefsInContext(kafkaServiceRef("clusterRef"));
-        KafkaProxyIngress ingress = ingress("ingress");
-        givenIngressesInContext(ingress);
-        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress"), ingressRef("ingress2")));
-        givenVirtualKafkaClustersInContext(cluster);
-
-        // when
-        ResolutionResult resolutionResult = DependencyResolverImpl.create().deepResolve(mockContext);
-
-        // then
-        assertThat(resolutionResult.ingresses()).containsExactlyInAnyOrder(ingress);
-        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
-        assertThat(onlyResult.isFullyResolved()).isFalse();
-        assertThat(onlyResult.unresolvedDependencySet()).containsExactly(new IngressRefBuilder().withName("ingress2").build());
-    }
-
-    private IngressRef ingressRef(String name) {
-        return new IngressRefBuilder().withName(name).build();
-    }
-
-    private static KafkaProxyIngress ingress(String name) {
-        return new KafkaProxyIngressBuilder().withNewMetadata().withName(name).endMetadata().build();
-    }
-
-    private static ClusterResolutionResult assertSingleResult(ResolutionResult resolutionResult, VirtualKafkaCluster cluster) {
-        Collection<ClusterResolutionResult> result = resolutionResult.clusterResults();
-        assertThat(result).hasSize(1);
-        ClusterResolutionResult onlyResult = result.stream().findFirst().orElseThrow();
-        assertThat(onlyResult.cluster()).isEqualTo(cluster);
-        return onlyResult;
-    }
-
-    private static KafkaService kafkaServiceRef(String clusterRef) {
-        return new KafkaServiceBuilder().withNewMetadata().withName(clusterRef).endMetadata().build();
-    }
-
-    private static FilterRef filterRef(String name) {
-        return new FilterRefBuilder().withName(name).build();
-    }
-
-    private static KafkaProtocolFilter protocolFilter(String name) {
-        return new KafkaProtocolFilterBuilder()
-                .withApiVersion("filter.kroxylicious.io/v1alpha")
-                .withKind("KafkaProtocolFilter")
-                .withNewMetadata().withName(name)
-                .endMetadata()
-                .build();
-    }
-
-    private void givenFiltersInContext(KafkaProtocolFilter... resources) {
-        givenSecondaryResourcesInContext(KafkaProtocolFilter.class, resources);
-    }
-
-    private void givenIngressesInContext(KafkaProxyIngress... ingresses) {
-        givenSecondaryResourcesInContext(KafkaProxyIngress.class, ingresses);
-    }
-
-    private void givenVirtualKafkaClustersInContext(VirtualKafkaCluster... virtualKafkaClusters) {
-        givenSecondaryResourcesInContext(VirtualKafkaCluster.class, virtualKafkaClusters);
-    }
-
-    private void givenClusterRefsInContext(KafkaService... clusterRefs) {
-        givenSecondaryResourcesInContext(KafkaService.class, clusterRefs);
-    }
-
-    @SafeVarargs
-    private <T> void givenSecondaryResourcesInContext(Class<T> type, T... resources) {
-        when(mockContext.getSecondaryResources(type)).thenReturn(Arrays.stream(resources).collect(Collectors.toSet()));
-    }
-
-    private static VirtualKafkaCluster virtualCluster(List<FilterRef> filterRefs, String clusterRef, List<IngressRef> ingressRefs) {
-        return new VirtualKafkaClusterBuilder()
-                .withNewSpec()
-                .withIngressRefs(ingressRefs)
-                .withNewTargetKafkaServiceRef().withName(clusterRef).endTargetKafkaServiceRef()
-                .withFilterRefs(filterRefs)
-                .endSpec()
-                .build();
-    }
-
-}
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
new file mode 100644
index 0000000000..27d1c2ee54
--- /dev/null
+++ b/kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverTest.java
@@ -0,0 +1,739 @@
+/*
+ * 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.time.Instant;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+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.ConditionBuilder;
+import io.kroxylicious.kubernetes.api.common.FilterRef;
+import io.kroxylicious.kubernetes.api.common.FilterRefBuilder;
+import io.kroxylicious.kubernetes.api.common.IngressRef;
+import io.kroxylicious.kubernetes.api.common.IngressRefBuilder;
+import io.kroxylicious.kubernetes.api.common.KafkaServiceRef;
+import io.kroxylicious.kubernetes.api.common.KafkaServiceRefBuilder;
+import io.kroxylicious.kubernetes.api.common.LocalRef;
+import io.kroxylicious.kubernetes.api.common.ProxyRef;
+import io.kroxylicious.kubernetes.api.common.ProxyRefBuilder;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxy;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyBuilder;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngress;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaProxyIngressBuilder;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaService;
+import io.kroxylicious.kubernetes.api.v1alpha1.KafkaServiceBuilder;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaCluster;
+import io.kroxylicious.kubernetes.api.v1alpha1.VirtualKafkaClusterBuilder;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterBuilder;
+import io.kroxylicious.kubernetes.operator.ResourcesUtil;
+import io.kroxylicious.kubernetes.operator.resolver.ClusterResolutionResult.DanglingReference;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class DependencyResolverTest {
+
+    public static final String PROXY_NAME = "proxy";
+    public static final KafkaProxy PROXY = new KafkaProxyBuilder().withNewMetadata().withName(PROXY_NAME).endMetadata().build();
+    @Mock(strictness = LENIENT)
+    Context<KafkaProxy> mockProxyContext;
+    private final DependencyResolver dependencyResolver = DependencyResolver.create();
+
+    @BeforeEach
+    void setup() {
+        givenFiltersInContext();
+        givenClusterRefsInContext();
+        givenIngressesInContext();
+        givenVirtualKafkaClustersInContext();
+        givenProxiesInContext();
+    }
+
+    @Test
+    void resolveProxyRefsWhenNoEntitiesReferenceProxy() {
+        // given nothing in context
+
+        // when
+        ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
+
+        // 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
+    void resolveClusterRefsWithManyReferencesDangling() {
+        // given nothing in context
+
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("missing")), "cluster", List.of(ingressRef("ingressRef")), getProxyRef(PROXY_NAME));
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.danglingReferences()).containsExactlyInAnyOrder(danglingReference(cluster, getProxyRef(PROXY_NAME)),
+                danglingReference(cluster, getKafkaServiceRef("cluster")),
+                danglingReference(cluster, ingressRef("ingressRef")),
+                danglingReference(cluster, filterRef("missing")));
+    }
+
+    @Test
+    void resolveProxyRefsWithNullFiltersOnVirtualCluster() {
+        // given
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(null, "cluster", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
+
+        // then
+        assertThat(resolutionResult.allClustersInNameOrder()).containsExactly(cluster);
+        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
+        assertThat(onlyResult.isFullyResolved()).isTrue();
+        assertThat(onlyResult.danglingReferences()).isEmpty();
+    }
+
+    @Test
+    void resolveClusterRefsWithNullFilters() {
+        // given
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(null, "cluster", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
+        assertThat(clusterResolutionResult.danglingReferences()).isEmpty();
+    }
+
+    @Test
+    void resolveProxyRefsWithFilterHavingResolvedRefsFalseCondition() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName").edit().withNewStatus().addToConditions(resolvedRefsFalse()).endStatus().build();
+        givenFiltersInContext(filter);
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName")), "cluster", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        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"));
+    }
+
+    @Test
+    void resolveClusterRefsWithFilterHavingResolvedRefsFalseCondition() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName").edit().withNewStatus().addToConditions(resolvedRefsFalse())
+                .endStatus().build();
+        givenFiltersInContext(filter);
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName")), "cluster", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        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"));
+    }
+
+    private static @NonNull Condition resolvedRefsFalse() {
+        return new ConditionBuilder().withType(Condition.Type.ResolvedRefs)
+                .withStatus(Condition.Status.FALSE)
+                .withObservedGeneration(1L)
+                .withLastTransitionTime(Instant.EPOCH)
+                .withMessage("BadThings")
+                .withReason("BadThings").build();
+    }
+
+    @Test
+    void resolveProxyRefsWithServiceHavingResolvedRefsFalseCondition() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName");
+        givenFiltersInContext(filter);
+        KafkaService service = kafkaService("cluster").edit().withNewStatus().addToConditions(resolvedRefsFalse()).endStatus().build();
+        givenClusterRefsInContext(service);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName")), "cluster", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
+
+        // 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"));
+    }
+
+    @Test
+    void resolveClusterRefsWithServiceHavingResolvedRefsFalseCondition() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName");
+        givenFiltersInContext(filter);
+        KafkaService service = kafkaService("cluster").edit().withNewStatus().addToConditions(resolvedRefsFalse()).endStatus().build();
+        givenClusterRefsInContext(service);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName")), "cluster", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        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"));
+    }
+
+    @Test
+    void resolveProxyRefsWithIngressHavingResolvedRefsFalseCondition() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName");
+        givenFiltersInContext(filter);
+        givenClusterRefsInContext(kafkaService("cluster"));
+        KafkaProxyIngress ingress = ingress("ingress", PROXY_NAME).edit().withNewStatus().addToConditions(resolvedRefsFalse()).endStatus().build();
+        givenIngressesInContext(ingress);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName")), "cluster", List.of(ingressRef("ingress")), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
+
+        // then
+        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
+        assertThat(onlyResult.isFullyResolved()).isFalse();
+        assertThat(onlyResult.findResourcesWithResolvedRefsFalse()).containsExactly(ingressRef("ingress"));
+        assertThat(onlyResult.findResourcesWithResolvedRefsFalse(HasMetadata.getKind(KafkaProxyIngress.class)))
+                .containsExactly(ingressRef("ingress"));
+    }
+
+    @Test
+    void resolveClusterRefsWithIngressHavingResolvedRefsFalseCondition() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName");
+        givenFiltersInContext(filter);
+        givenClusterRefsInContext(kafkaService("cluster"));
+        KafkaProxyIngress ingress = ingress("ingress", PROXY_NAME).edit().withNewStatus().addToConditions(resolvedRefsFalse()).endStatus().build();
+        givenIngressesInContext(ingress);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName")), "cluster", List.of(ingressRef("ingress")), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.findResourcesWithResolvedRefsFalse()).containsExactly(ingressRef("ingress"));
+        assertThat(clusterResolutionResult.findResourcesWithResolvedRefsFalse(HasMetadata.getKind(KafkaProxyIngress.class))).containsExactly(ingressRef("ingress"));
+    }
+
+    @Test
+    void resolveProxyRefsWithSingleDanglingFilterRef() {
+        // given
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("another")), "cluster", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        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")));
+    }
+
+    @Test
+    void resolveClusterRefsWithSingleDanglingFilterRef() {
+        // given
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("another")), "cluster", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, filterRef("another")));
+    }
+
+    @Test
+    void resolveProxyRefsWithSingleResolvedFilter() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName");
+        givenFiltersInContext(filter);
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName")), "cluster", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        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();
+    }
+
+    @Test
+    void resolveClusterRefsWithSingleResolvedFilter() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName");
+        givenFiltersInContext(filter);
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName")), "cluster", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
+        assertThat(clusterResolutionResult.danglingReferences()).isEmpty();
+    }
+
+    @Test
+    void resolveProxyRefsWithMultipleResolvedFilter() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName");
+        KafkaProtocolFilter filter2 = protocolFilter("filterName2");
+        givenFiltersInContext(filter, filter2);
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName"), filterRef("filterName2")), "cluster", List.of(),
+                getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        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();
+    }
+
+    @Test
+    void resolveClusterRefsWithMultipleResolvedFilter() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName");
+        KafkaProtocolFilter filter2 = protocolFilter("filterName2");
+        givenFiltersInContext(filter, filter2);
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName"), filterRef("filterName2")), "cluster", List.of(),
+                getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
+        assertThat(clusterResolutionResult.danglingReferences()).isEmpty();
+    }
+
+    @Test
+    void resolveProxyRefsWithSubsetOfFiltersReferenced() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName");
+        givenFiltersInContext(filter);
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName"), filterRef("filterName2")), "cluster", List.of(),
+                getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        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")));
+    }
+
+    @Test
+    void resolveClusterRefsWithSubsetOfFiltersReferenced() {
+        // given
+        KafkaProtocolFilter filter = protocolFilter("filterName");
+        givenFiltersInContext(filter);
+        givenClusterRefsInContext(kafkaService("cluster"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(filterRef("filterName"), filterRef("filterName2")), "cluster", List.of(),
+                getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, filterRef("filterName2")));
+    }
+
+    @Test
+    void resolveProxyRefsWithDanglingIngressRef() {
+        // given
+        givenClusterRefsInContext(kafkaService("clusterRef"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingressMissing")), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
+
+        // then
+        assertThat(resolutionResult.ingresses()).isEmpty();
+        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
+        assertThat(onlyResult.isFullyResolved()).isFalse();
+        assertThat(onlyResult.danglingReferences()).containsExactly(danglingReference(cluster, ingressRef("ingressMissing")));
+    }
+
+    @Test
+    void resolveClusterRefsWithDanglingIngressRef() {
+        // given
+        givenClusterRefsInContext(kafkaService("clusterRef"));
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingressMissing")), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, ingressRef("ingressMissing")));
+    }
+
+    @Test
+    void resolveProxyRefsWithDanglingKafkaServiceRef() {
+        // given
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "missing", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        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")));
+    }
+
+    @Test
+    void resolveClusterRefsWithDanglingKafkaServiceRef() {
+        // given
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "missing", List.of(), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, getKafkaServiceRef("missing")));
+    }
+
+    @Test
+    void resolveProxyRefsWithSingleResolvedIngress() {
+        // given
+        givenClusterRefsInContext(kafkaService("clusterRef"));
+        KafkaProxyIngress ingress = ingress("ingress", PROXY_NAME);
+        givenIngressesInContext(ingress);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress")), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
+
+        // then
+        assertThat(resolutionResult.ingresses()).containsExactly(ingress);
+        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
+        assertThat(onlyResult.isFullyResolved()).isTrue();
+        assertThat(onlyResult.danglingReferences()).isEmpty();
+    }
+
+    @Test
+    void resolveClusterRefsWithSingleResolvedIngress() {
+        // given
+        givenClusterRefsInContext(kafkaService("clusterRef"));
+        KafkaProxyIngress ingress = ingress("ingress", PROXY_NAME);
+        givenIngressesInContext(ingress);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress")), getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
+        assertThat(clusterResolutionResult.danglingReferences()).isEmpty();
+    }
+
+    @Test
+    void resolveProxyRefsWithMultipleResolvedIngresses() {
+        // given
+        givenClusterRefsInContext(kafkaService("clusterRef"));
+        KafkaProxyIngress ingress = ingress("ingress", PROXY_NAME);
+        KafkaProxyIngress ingress2 = ingress("ingress2", PROXY_NAME);
+        givenIngressesInContext(ingress, ingress2);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress"), ingressRef("ingress2")),
+                getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        ProxyResolutionResult resolutionResult = resolveProxyRefs(PROXY);
+
+        // then
+        assertThat(resolutionResult.ingresses()).containsExactlyInAnyOrder(ingress, ingress2);
+        ClusterResolutionResult onlyResult = assertSingleResult(resolutionResult, cluster);
+        assertThat(onlyResult.isFullyResolved()).isTrue();
+        assertThat(onlyResult.danglingReferences()).isEmpty();
+    }
+
+    @Test
+    void resolveClusterRefsWithMultipleResolvedIngresses() {
+        // given
+        givenClusterRefsInContext(kafkaService("clusterRef"));
+        KafkaProxyIngress ingress = ingress("ingress", PROXY_NAME);
+        KafkaProxyIngress ingress2 = ingress("ingress2", PROXY_NAME);
+        givenIngressesInContext(ingress, ingress2);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress"), ingressRef("ingress2")),
+                getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isTrue();
+        assertThat(clusterResolutionResult.danglingReferences()).isEmpty();
+    }
+
+    @Test
+    void resolveProxyRefsWithSubsetOfIngressesResolved() {
+        // given
+        givenClusterRefsInContext(kafkaService("clusterRef"));
+        KafkaProxyIngress ingress = ingress("ingress", PROXY_NAME);
+        givenIngressesInContext(ingress);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress"), ingressRef("ingress2")),
+                getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+
+        // when
+        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, ingressRef("ingress2")));
+    }
+
+    @Test
+    void resolveClusterRefsWithSubsetOfIngressesResolved() {
+        // given
+        givenClusterRefsInContext(kafkaService("clusterRef"));
+        KafkaProxyIngress ingress = ingress("ingress", PROXY_NAME);
+        givenIngressesInContext(ingress);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress"), ingressRef("ingress2")),
+                getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, ingressRef("ingress2")));
+    }
+
+    @Test
+    void resolveClusterRefsWithIngressReferencingDifferentProxyThanCluster() {
+        // given
+        givenClusterRefsInContext(kafkaService("clusterRef"));
+        KafkaProxyIngress ingress = ingress("ingress", "another-proxy");
+        givenIngressesInContext(ingress);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress")),
+                getProxyRef(PROXY_NAME));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
+        LocalRef<?> proxyRef = getProxyRef("another-proxy");
+        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(new DanglingReference(ResourcesUtil.toLocalRef(ingress), proxyRef));
+    }
+
+    @Test
+    void resolveClusterRefsWithDanglingProxyRef() {
+        // given
+        givenClusterRefsInContext(kafkaService("clusterRef"));
+        KafkaProxyIngress ingress = ingress("ingress", PROXY_NAME);
+        givenIngressesInContext(ingress);
+        VirtualKafkaCluster cluster = virtualCluster(List.of(), "clusterRef", List.of(ingressRef("ingress")),
+                getProxyRef("another-proxy"));
+        givenVirtualKafkaClustersInContext(cluster);
+        givenProxiesInContext(PROXY);
+
+        // when
+        ClusterResolutionResult clusterResolutionResult = resolveClusterRefs(cluster);
+
+        // then
+        assertThat(clusterResolutionResult.isFullyResolved()).isFalse();
+        assertThat(clusterResolutionResult.danglingReferences()).containsExactly(danglingReference(cluster, getProxyRef("another-proxy")));
+    }
+
+    private @NonNull ClusterResolutionResult resolveClusterRefs(VirtualKafkaCluster cluster) {
+        return dependencyResolver.resolveClusterRefs(cluster, mockProxyContext);
+    }
+
+    private @NonNull ProxyResolutionResult resolveProxyRefs(KafkaProxy proxy) {
+        return dependencyResolver.resolveProxyRefs(proxy, mockProxyContext);
+    }
+
+    private static KafkaServiceRef getKafkaServiceRef(String name) {
+        return new KafkaServiceRefBuilder().withName(name).build();
+    }
+
+    private static ProxyRef getProxyRef(String name) {
+        return new ProxyRefBuilder().withName(name).build();
+    }
+
+    private IngressRef ingressRef(String name) {
+        return new IngressRefBuilder().withName(name).build();
+    }
+
+    private static KafkaProxyIngress ingress(String name, String proxyName) {
+        return new KafkaProxyIngressBuilder().withNewMetadata().withName(name).endMetadata()
+                .withNewSpec().withNewProxyRef().withName(proxyName).endProxyRef().endSpec().build();
+    }
+
+    private static ClusterResolutionResult assertSingleResult(ProxyResolutionResult resolutionResult, VirtualKafkaCluster cluster) {
+        Set<ClusterResolutionResult> result = resolutionResult.clusterResolutionResults();
+        assertThat(result).hasSize(1);
+        ClusterResolutionResult onlyResult = result.stream().findFirst().orElseThrow();
+        assertThat(onlyResult.cluster()).isEqualTo(cluster);
+        return onlyResult;
+    }
+
+    private static KafkaService kafkaService(String clusterRef) {
+        return new KafkaServiceBuilder().withNewMetadata().withName(clusterRef).endMetadata().build();
+    }
+
+    private static FilterRef filterRef(String name) {
+        return new FilterRefBuilder().withName(name).build();
+    }
+
+    private static KafkaProtocolFilter protocolFilter(String name) {
+        return new KafkaProtocolFilterBuilder()
+                .withApiVersion("filter.kroxylicious.io/v1alpha")
+                .withKind("KafkaProtocolFilter")
+                .withNewMetadata().withName(name)
+                .endMetadata()
+                .build();
+    }
+
+    private static @NonNull DanglingReference danglingReference(VirtualKafkaCluster fromCluster, LocalRef<?> toRef) {
+        return new DanglingReference(ResourcesUtil.toLocalRef(fromCluster), toRef);
+    }
+
+    private void givenFiltersInContext(KafkaProtocolFilter... resources) {
+        givenSecondaryResourcesInContext(KafkaProtocolFilter.class, resources);
+    }
+
+    private void givenIngressesInContext(KafkaProxyIngress... ingresses) {
+        givenSecondaryResourcesInContext(KafkaProxyIngress.class, ingresses);
+    }
+
+    private void givenVirtualKafkaClustersInContext(VirtualKafkaCluster... virtualKafkaClusters) {
+        givenSecondaryResourcesInContext(VirtualKafkaCluster.class, virtualKafkaClusters);
+    }
+
+    private void givenProxiesInContext(KafkaProxy... kafkaProxies) {
+        givenSecondaryResourcesInContext(KafkaProxy.class, kafkaProxies);
+    }
+
+    private void givenClusterRefsInContext(KafkaService... clusterRefs) {
+        givenSecondaryResourcesInContext(KafkaService.class, clusterRefs);
+    }
+
+    @SafeVarargs
+    private <T> void givenSecondaryResourcesInContext(Class<T> type, T... resources) {
+        when(mockProxyContext.getSecondaryResources(type)).thenReturn(Arrays.stream(resources).collect(Collectors.toSet()));
+    }
+
+    private static VirtualKafkaCluster virtualCluster(List<FilterRef> filterRefs, String clusterRef, List<IngressRef> ingressRefs, ProxyRef proxyRef) {
+        return new VirtualKafkaClusterBuilder()
+                .withNewSpec()
+                .withIngressRefs(ingressRefs)
+                .withNewTargetKafkaServiceRef().withName(clusterRef).endTargetKafkaServiceRef()
+                .withProxyRef(proxyRef)
+                .withFilterRefs(filterRefs)
+                .endSpec()
+                .build();
+    }
+
+}


Reviews:
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T01:16:16Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T01:44:54Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T01:54:44Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T02:37:47Z
user: login: SamBarker
body: A few initial thoughts but I think I need to checkout and explore things a bit more as I've obviously got a quite a different model for what a resolution results is. 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T05:00:11Z
user: login: k-wall
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T10:51:40Z
user: login: k-wall
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T10:56:44Z
user: login: k-wall
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T11:02:30Z
user: login: k-wall
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T11:03:54Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T21:18:22Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T21:47:55Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T23:30:30Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T23:30:57Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T23:49:39Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T01:58:55Z
user: login: SamBarker
body: I think this change represents something useful, but I'm not entirely sure its got the naming and thus concepts/boundaries right. 

I'm relaxed about merging and iterate on main, or continuing on this PR (So I guess I'm +0.5 at this point).
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T05:10:49Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:03:38Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:05:43Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:08:41Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:11:26Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:12:02Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:13:10Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:15:02Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:17:22Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T21:20:28Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T23:15:13Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T23:15:34Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T23:18:07Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T23:19:06Z
user: login: SamBarker
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T23:38:17Z
user: login: SamBarker
body: Things are making a lot more sense to me now. Thanks @robobario 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-09T23:43:48Z
user: login: robobario
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T02:37:36Z

Review comments:
diff_hunk: @@ -144,12 +145,10 @@ private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory s
                 .filter(ResolutionResult.ClusterResolutionResult::isAnyDependencyUnresolved)
                 .forEach(clusterResolutionResult -> {
 
-                    Comparator<LocalRef<?>> comparator = Comparator.<LocalRef<?>, String> comparing(LocalRef::getGroup)
-                            .thenComparing(LocalRef::getKind)
-                            .thenComparing(LocalRef::getName);
+                    Comparator<UnresolvedReference> comparator = Comparator.<UnresolvedReference, LocalRef> comparing(UnresolvedReference::to);
 
-                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedDependencySet().stream()
-                            .sorted(comparator).findFirst()
+                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedReferences().unresolved().stream()
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigConfigMap.java
user: login: robobario
body: this needs to change since the result can also have dependencies that are resolved, but have `ResolvedRefs:False` in the Conditions
created_at: 2025-04-08T01:16:16Z
author_association: MEMBER
diff_hunk: @@ -57,50 +81,95 @@ public ResolutionResult deepResolve(Context<KafkaProxy> context) {
         Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters = context.getSecondaryResources(KafkaProtocolFilter.class).stream()
                 .collect(ResourcesUtil.toByLocalRefMap());
         var resolutionResult = virtualKafkaClusters.stream()
-                .map(cluster -> determineUnresolvedDependencies(cluster, ingresses, clusterRefs, filters))
+                .map(cluster -> determineUnresolvedDependencies(cluster, ingresses, clusterRefs, filters, proxies))
                 .collect(Collectors.toSet());
         return new ResolutionResult(filters, ingresses, clusterRefs, resolutionResult);
     }
 
     private ClusterResolutionResult determineUnresolvedDependencies(VirtualKafkaCluster cluster,
                                                                     Map<LocalRef<KafkaProxyIngress>, KafkaProxyIngress> ingresses,
-                                                                    Map<LocalRef<KafkaService>, KafkaService> clusterRefs,
-                                                                    Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters) {
+                                                                    Map<LocalRef<KafkaService>, KafkaService> services,
+                                                                    Map<LocalRef<KafkaProtocolFilter>, KafkaProtocolFilter> filters,
+                                                                    Set<KafkaProxy> proxies) {
         VirtualKafkaClusterSpec spec = cluster.getSpec();
-        Set<LocalRef<?>> unresolvedDependencies = new HashSet<>();
-        determineUnresolvedIngresses(spec, ingresses).forEach(unresolvedDependencies::add);
-        determineUnresolvedKafkaService(spec, clusterRefs).ifPresent(unresolvedDependencies::add);
-        determineUnresolvedFilters(spec, filters).forEach(unresolvedDependencies::add);
-        return new ClusterResolutionResult(cluster, unresolvedDependencies);
+        LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
+        Set<UnresolvedReference> unresolved = new HashSet<>();
+        determineUnresolvedIngresses(clusterRef, spec, ingresses).forEach(unresolved::add);
+        determineIngressesWithUnresolvedProxies(ingresses, proxies).forEach(unresolved::add);
+        determineUnresolvedKafkaProxies(clusterRef, spec, proxies).ifPresent(unresolved::add);
+        determineUnresolvedKafkaService(clusterRef, spec, services).ifPresent(unresolved::add);
+        determineUnresolvedFilters(clusterRef, spec, filters).forEach(unresolved::add);
+        Set<KafkaProxyIngress> ingressesWithResolvedRefsFalse = ingresses.values().stream()
+                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
+                .collect(Collectors.toSet());
+        Set<KafkaProtocolFilter> filtersWithResolvedRefsFalse = filters.values().stream()
+                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
+                .collect(Collectors.toSet());
+        Set<KafkaService> kafkaServicesWithResolvedRefsFalse = services.values().stream()
+                .filter(i -> hasAnyResolvedRefsFalse(Optional.ofNullable(i.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
+                .collect(Collectors.toSet());
+        return new ClusterResolutionResult(cluster,
+                new ResolutionResult.UnresolvedReferences(unresolved, ingressesWithResolvedRefsFalse, filtersWithResolvedRefsFalse, kafkaServicesWithResolvedRefsFalse));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImpl.java
user: login: robobario
body: need to unit test this new Resolver functionality around detecting resolved refs false
created_at: 2025-04-08T01:44:54Z
author_association: MEMBER
diff_hunk: @@ -141,27 +143,38 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigData data) {
         proxyModel.resolutionResult().clusterResults().stream()
-                .filter(ResolutionResult.ClusterResolutionResult::isAnyDependencyUnresolved)
+                .filter(ResolutionResult.ClusterResolutionResult::hasAnyResolutionProblems)
                 .forEach(clusterResolutionResult -> {
-
-                    Comparator<LocalRef<?>> comparator = Comparator.<LocalRef<?>, String> comparing(LocalRef::getGroup)
-                            .thenComparing(LocalRef::getKind)
-                            .thenComparing(LocalRef::getName);
-
-                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedDependencySet().stream()
-                            .sorted(comparator).findFirst()
-                            .orElseThrow();
-                    String message = String.format("Resource %s was not found.",
-                            ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
-
+                    VirtualKafkaCluster patch;
+                    ResolutionResult.UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+                    if (!unresolvedReferences.unresolved().isEmpty()) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigConfigMap.java
user: login: robobario
body: here we've departed from a pure refactor, to support the VKC side I added the responsibility for finding ResolvedRefs: False conditions to the DependencyResolver, which meant the KafkaProxyReconciler side also needs to tolerate this case.
created_at: 2025-04-08T01:54:44Z
author_association: MEMBER
diff_hunk: @@ -36,18 +39,84 @@ public class ResolutionResult {
     private final Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs;
     private final Set<ClusterResolutionResult> clusterResolutionResults;
 
-    public record ClusterResolutionResult(VirtualKafkaCluster cluster, Set<LocalRef<?>> unresolvedDependencySet) {
+    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
+        return clusterResolutionResults.stream()
+                .filter(r -> r.cluster == cluster)
+                .findFirst();
+    }
+
+    enum DependencyType {
+        // from the virtual kafka cluster to another entity
+        DIRECT,
+        // from any other entity to another entity
+        TRANSITIVE
+    }
+
+    public record UnresolvedReferences(Set<UnresolvedReference> unresolved,
+                                       Set<KafkaProxyIngress> ingressesWithResolvedRefsFalse,
+                                       Set<KafkaProtocolFilter> filtersWithResolvedRefsFalse,
+                                       Set<KafkaService> kafkaServicesWithResolvedRefsFalse) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: robobario
body: this implementation was driven more by the VirtualKafkaCluster's current code, where we want to know about each type of problem to generate a different `reasons` on the `ResolvedRefs` condition. Maybe it would simplify things if we adopted the KafkaProxyReconciler style where we emit a message like "resource <slug> not found" or "resource <slug> has ResolvedRefs: False condition".
created_at: 2025-04-08T02:37:46Z
author_association: MEMBER
diff_hunk: @@ -191,4 +191,9 @@ public String getValue() {
             return value;
         }
     }
+
+    public static boolean isResolvedRefsFalse(Condition condition) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/api/common/Condition.java
user: login: SamBarker
body: This should probably `null` safe as a generic utility function, or maybe just annotated `@NonNull`
created_at: 2025-04-08T03:25:19Z
author_association: MEMBER
diff_hunk: @@ -67,115 +65,89 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.deepResolveCluster(cluster, context);
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: SamBarker
body: Not convinced by `deepResolveCluster` How about `resolver.resolveTransitiveRefs(cluster, context)`?
created_at: 2025-04-08T04:48:40Z
author_association: MEMBER
diff_hunk: @@ -67,115 +65,89 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.deepResolveCluster(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: SamBarker
body: Feels like a really funky API. `var result = deepResolve(cluster)` then `result.resultFor(the same thing as the previous line)`. Surely the result already know what cluster we are interested in? 
created_at: 2025-04-08T04:50:33Z
author_association: MEMBER
diff_hunk: @@ -184,17 +156,11 @@ private static String joiningMessages(
         return Stream.of(serviceMsg).flatMap(Function.identity()).collect(Collectors.joining("; "));
     }
 
-    private static boolean hasAnyResolvedRefsFalse(List<Condition> conditions) {
-        return conditions.stream()
-                .anyMatch(c -> Condition.Type.ResolvedRefs.equals(c.getType())
-                        && Condition.Status.FALSE.equals(c.getStatus()));
-    }
-
     @NonNull
-    private static <R extends CustomResource<?, ?>> Stream<String> refsMessage(
-                                                                               String prefix,
-                                                                               VirtualKafkaCluster cluster,
-                                                                               TreeSet<? extends LocalRef<R>> refs) {
+    private static Stream<String> refsMessage(
+                                              String prefix,
+                                              VirtualKafkaCluster cluster,
+                                              TreeSet<LocalRef<?>> refs) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: SamBarker
body: Why do we collect to a tree set to then test if its an empty stream and if not get the stream back from the set. Why not just accept `Stream<LocalRef<?>>` and then do `refs.sorted()`
created_at: 2025-04-08T04:56:30Z
author_association: MEMBER
diff_hunk: @@ -63,226 +78,658 @@ void testNoDependencies() {
         assertThat(resolutionResult.filters()).isEmpty();
     }
 
+    @Test
+    void deepResolveClusterNoDependenciesVirtualKafkaCluster() {
path: kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImplTest.java
user: login: k-wall
body: I struggled making sense of the name of the method.  I'd have found 

`deepResolveClusterWithMissingProxyAndServiceRefs`

better.  But, I'd split the test case into two, one focusing on the unresolved proxy ref and the other the unresolved service ref.  If there's the need, have a `deepResolveClusterWithManyMissingRefs` too.


created_at: 2025-04-08T10:51:40Z
author_association: MEMBER
diff_hunk: @@ -63,226 +78,658 @@ void testNoDependencies() {
         assertThat(resolutionResult.filters()).isEmpty();
     }
 
+    @Test
+    void deepResolveClusterNoDependenciesVirtualKafkaCluster() {
+        givenFiltersInContext();
path: kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImplTest.java
user: login: k-wall
body: How about making setup establish the context returning no filters, no cluster refs, and leaving the test to set just what it needs.
created_at: 2025-04-08T10:56:44Z
author_association: MEMBER
diff_hunk: @@ -36,18 +39,84 @@ public class ResolutionResult {
     private final Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs;
     private final Set<ClusterResolutionResult> clusterResolutionResults;
 
-    public record ClusterResolutionResult(VirtualKafkaCluster cluster, Set<LocalRef<?>> unresolvedDependencySet) {
+    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
+        return clusterResolutionResults.stream()
+                .filter(r -> r.cluster == cluster)
+                .findFirst();
+    }
+
+    enum DependencyType {
+        // from the virtual kafka cluster to another entity
+        DIRECT,
+        // from any other entity to another entity
+        TRANSITIVE
+    }
+
+    public record UnresolvedReferences(Set<UnresolvedReference> unresolved,
+                                       Set<KafkaProxyIngress> ingressesWithResolvedRefsFalse,
+                                       Set<KafkaProtocolFilter> filtersWithResolvedRefsFalse,
+                                       Set<KafkaService> kafkaServicesWithResolvedRefsFalse) {
+        public UnresolvedReferences {
+            Objects.requireNonNull(unresolved);
+        }
+
+        public Stream<LocalRef<?>> getUnresolvedReferences(LocalRef<?> from, String kindTo) {
+            Objects.requireNonNull(from);
+            Objects.requireNonNull(kindTo);
+            return unresolved.stream().filter(r -> r.from.equals(from) && r.to.getKind().equals(kindTo)).map(UnresolvedReference::to);
+        }
+
+        public Stream<LocalRef<?>> getUnresolvedReferences(String fromKind, String toKind) {
+            Objects.requireNonNull(fromKind);
+            Objects.requireNonNull(toKind);
+            return unresolved.stream().filter(r -> r.from.getKind().equals(fromKind) && r.to.getKind().equals(toKind)).map(UnresolvedReference::to);
+        }
+
+        public boolean hasUnresolvedReferences(String fromKind, String toKind) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: k-wall
body: I question whether you really need this method.
created_at: 2025-04-08T11:02:29Z
author_association: MEMBER
diff_hunk: @@ -36,18 +39,84 @@ public class ResolutionResult {
     private final Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs;
     private final Set<ClusterResolutionResult> clusterResolutionResults;
 
-    public record ClusterResolutionResult(VirtualKafkaCluster cluster, Set<LocalRef<?>> unresolvedDependencySet) {
+    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
+        return clusterResolutionResults.stream()
+                .filter(r -> r.cluster == cluster)
+                .findFirst();
+    }
+
+    enum DependencyType {
+        // from the virtual kafka cluster to another entity
+        DIRECT,
+        // from any other entity to another entity
+        TRANSITIVE
+    }
+
+    public record UnresolvedReferences(Set<UnresolvedReference> unresolved,
+                                       Set<KafkaProxyIngress> ingressesWithResolvedRefsFalse,
+                                       Set<KafkaProtocolFilter> filtersWithResolvedRefsFalse,
+                                       Set<KafkaService> kafkaServicesWithResolvedRefsFalse) {
+        public UnresolvedReferences {
+            Objects.requireNonNull(unresolved);
+        }
+
+        public Stream<LocalRef<?>> getUnresolvedReferences(LocalRef<?> from, String kindTo) {
+            Objects.requireNonNull(from);
+            Objects.requireNonNull(kindTo);
+            return unresolved.stream().filter(r -> r.from.equals(from) && r.to.getKind().equals(kindTo)).map(UnresolvedReference::to);
+        }
+
+        public Stream<LocalRef<?>> getUnresolvedReferences(String fromKind, String toKind) {
+            Objects.requireNonNull(fromKind);
+            Objects.requireNonNull(toKind);
+            return unresolved.stream().filter(r -> r.from.getKind().equals(fromKind) && r.to.getKind().equals(toKind)).map(UnresolvedReference::to);
+        }
+
+        public boolean hasUnresolvedReferences(String fromKind, String toKind) {
+            return getUnresolvedReferences(fromKind, toKind).findAny().isPresent();
+        }
+
+        public boolean hasAnyResolutionProblems() {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: k-wall
body: Naming how about `isFullyResolved`?
created_at: 2025-04-08T11:03:54Z
author_association: MEMBER
diff_hunk: @@ -67,115 +65,89 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.deepResolveCluster(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: robobario
body: Needs a refactor I think, `ResolutionResult` contains all the resolved resources and then a `ClusterResolutionResult` that describes any problems with each cluster's dependencies. It was made for the KafkaProxy so doesn't quite fit this resolve cluster side.

Maybe we should shuffle the relevant dependencies down onto `ClusterResolutionResult` so that it carries the dependencies for each cluster, and or the description of dependencie issues. `deepResolveCluster` would return a ClusterResolutionResult.
created_at: 2025-04-08T21:18:22Z
author_association: MEMBER
diff_hunk: @@ -67,115 +65,89 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.deepResolveCluster(cluster, context);
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: robobario
body: we already have the `transitive` word knocking around in the `ResolvedRefs` reason `TransitivelyReferencedResourcesNotFound` which is used when one of the dependencies has a `ResolvedRefs: False`  condition. As opposed to `ReferencedResourcesNotFound` where we cannot find a dependency the VKC refers to.

This resolver is looking at both types of dependencies, direct references from this VKC and then transitive dependencies.

It's resolving direct and transitive dependencies. Maybe `resolveRefs` is better. Definitely think you're right that you don't resolve a cluster, you resolve it's dependencies.
created_at: 2025-04-08T21:47:55Z
author_association: MEMBER
diff_hunk: @@ -36,18 +39,84 @@ public class ResolutionResult {
     private final Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs;
     private final Set<ClusterResolutionResult> clusterResolutionResults;
 
-    public record ClusterResolutionResult(VirtualKafkaCluster cluster, Set<LocalRef<?>> unresolvedDependencySet) {
+    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
+        return clusterResolutionResults.stream()
+                .filter(r -> r.cluster == cluster)
+                .findFirst();
+    }
+
+    enum DependencyType {
+        // from the virtual kafka cluster to another entity
+        DIRECT,
+        // from any other entity to another entity
+        TRANSITIVE
+    }
+
+    public record UnresolvedReferences(Set<UnresolvedReference> unresolved,
+                                       Set<KafkaProxyIngress> ingressesWithResolvedRefsFalse,
+                                       Set<KafkaProtocolFilter> filtersWithResolvedRefsFalse,
+                                       Set<KafkaService> kafkaServicesWithResolvedRefsFalse) {
+        public UnresolvedReferences {
+            Objects.requireNonNull(unresolved);
+        }
+
+        public Stream<LocalRef<?>> getUnresolvedReferences(LocalRef<?> from, String kindTo) {
+            Objects.requireNonNull(from);
+            Objects.requireNonNull(kindTo);
+            return unresolved.stream().filter(r -> r.from.equals(from) && r.to.getKind().equals(kindTo)).map(UnresolvedReference::to);
+        }
+
+        public Stream<LocalRef<?>> getUnresolvedReferences(String fromKind, String toKind) {
+            Objects.requireNonNull(fromKind);
+            Objects.requireNonNull(toKind);
+            return unresolved.stream().filter(r -> r.from.getKind().equals(fromKind) && r.to.getKind().equals(toKind)).map(UnresolvedReference::to);
+        }
+
+        public boolean hasUnresolvedReferences(String fromKind, String toKind) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: robobario
body: removed
created_at: 2025-04-08T23:30:29Z
author_association: MEMBER
diff_hunk: @@ -184,17 +156,11 @@ private static String joiningMessages(
         return Stream.of(serviceMsg).flatMap(Function.identity()).collect(Collectors.joining("; "));
     }
 
-    private static boolean hasAnyResolvedRefsFalse(List<Condition> conditions) {
-        return conditions.stream()
-                .anyMatch(c -> Condition.Type.ResolvedRefs.equals(c.getType())
-                        && Condition.Status.FALSE.equals(c.getStatus()));
-    }
-
     @NonNull
-    private static <R extends CustomResource<?, ?>> Stream<String> refsMessage(
-                                                                               String prefix,
-                                                                               VirtualKafkaCluster cluster,
-                                                                               TreeSet<? extends LocalRef<R>> refs) {
+    private static Stream<String> refsMessage(
+                                              String prefix,
+                                              VirtualKafkaCluster cluster,
+                                              TreeSet<LocalRef<?>> refs) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: robobario
body: replaced with Stream
created_at: 2025-04-08T23:30:57Z
author_association: MEMBER
diff_hunk: @@ -67,115 +65,89 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.deepResolveCluster(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: robobario
body: since we've already got enough going on here, I'd push that out to another PR, will add a TODO in code
created_at: 2025-04-08T23:49:39Z
author_association: MEMBER
diff_hunk: @@ -63,226 +78,658 @@ void testNoDependencies() {
         assertThat(resolutionResult.filters()).isEmpty();
     }
 
+    @Test
+    void deepResolveClusterNoDependenciesVirtualKafkaCluster() {
path: kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImplTest.java
user: login: robobario
body: renamed all the tests to have clearer names
created_at: 2025-04-09T01:58:55Z
author_association: MEMBER
diff_hunk: @@ -141,27 +143,38 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigData data) {
         proxyModel.resolutionResult().clusterResults().stream()
-                .filter(ResolutionResult.ClusterResolutionResult::isAnyDependencyUnresolved)
+                .filter(clusterResolutionResult1 -> !clusterResolutionResult1.isFullyResolved())
                 .forEach(clusterResolutionResult -> {
-
-                    Comparator<LocalRef<?>> comparator = Comparator.<LocalRef<?>, String> comparing(LocalRef::getGroup)
-                            .thenComparing(LocalRef::getKind)
-                            .thenComparing(LocalRef::getName);
-
-                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedDependencySet().stream()
-                            .sorted(comparator).findFirst()
-                            .orElseThrow();
-                    String message = String.format("Resource %s was not found.",
-                            ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
-
+                    VirtualKafkaCluster patch;
+                    ResolutionResult.UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+                    if (!unresolvedReferences.unresolved().isEmpty()) {
+                        Comparator<UnresolvedReference> comparator = Comparator.<UnresolvedReference, LocalRef> comparing(UnresolvedReference::to);
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigConfigMap.java
user: login: SamBarker
body: nit: The comparator should probably be a constant rather than being created each time this method is invoked.
created_at: 2025-04-09T04:21:17Z
author_association: MEMBER
diff_hunk: @@ -141,27 +143,38 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigData data) {
         proxyModel.resolutionResult().clusterResults().stream()
-                .filter(ResolutionResult.ClusterResolutionResult::isAnyDependencyUnresolved)
+                .filter(clusterResolutionResult1 -> !clusterResolutionResult1.isFullyResolved())
                 .forEach(clusterResolutionResult -> {
-
-                    Comparator<LocalRef<?>> comparator = Comparator.<LocalRef<?>, String> comparing(LocalRef::getGroup)
-                            .thenComparing(LocalRef::getKind)
-                            .thenComparing(LocalRef::getName);
-
-                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedDependencySet().stream()
-                            .sorted(comparator).findFirst()
-                            .orElseThrow();
-                    String message = String.format("Resource %s was not found.",
-                            ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
-
+                    VirtualKafkaCluster patch;
+                    ResolutionResult.UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+                    if (!unresolvedReferences.unresolved().isEmpty()) {
+                        Comparator<UnresolvedReference> comparator = Comparator.<UnresolvedReference, LocalRef> comparing(UnresolvedReference::to);
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigConfigMap.java
user: login: SamBarker
body: or even pushed into ResolutionResult.
created_at: 2025-04-09T04:23:14Z
author_association: MEMBER
diff_hunk: @@ -141,27 +143,38 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigData data) {
         proxyModel.resolutionResult().clusterResults().stream()
-                .filter(ResolutionResult.ClusterResolutionResult::isAnyDependencyUnresolved)
+                .filter(clusterResolutionResult1 -> !clusterResolutionResult1.isFullyResolved())
                 .forEach(clusterResolutionResult -> {
-
-                    Comparator<LocalRef<?>> comparator = Comparator.<LocalRef<?>, String> comparing(LocalRef::getGroup)
-                            .thenComparing(LocalRef::getKind)
-                            .thenComparing(LocalRef::getName);
-
-                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedDependencySet().stream()
-                            .sorted(comparator).findFirst()
-                            .orElseThrow();
-                    String message = String.format("Resource %s was not found.",
-                            ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
-
+                    VirtualKafkaCluster patch;
+                    ResolutionResult.UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+                    if (!unresolvedReferences.unresolved().isEmpty()) {
+                        Comparator<UnresolvedReference> comparator = Comparator.<UnresolvedReference, LocalRef> comparing(UnresolvedReference::to);
+
+                        LocalRef<?> firstUnresolvedDependency = unresolvedReferences.unresolved().stream()
+                                .sorted(comparator).map(UnresolvedReference::to).findFirst()
+                                .orElseThrow();
+                        String message = String.format("Resource %s was not found.",
+                                ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
+                    }
+                    else {
+                        String message = String.format("Resource %s has ResolvedRefs=False.",
+                                ResourcesUtil.namespacedSlug(firstLocalRef(unresolvedReferences.resourcesWithResolvedRefsFalse()), clusterResolutionResult.cluster()));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigConfigMap.java
user: login: SamBarker
body: I'm struggling to understand this branch. We get here only if `unresolvedReferences.unresolved()` is empty. So why are we saying its `ResolvedRefs=False`. Is this the branch for a transitive failure? The direct dependency is resolved but the things it points to are un-resolved?  

The naming is not helping.

On the naming point, to me at least, unresolved implies we haven't attempted to resolve but what we mean is there is noting at the the address. If I'm right about the usage I'd suggest `danglingRefs` or `invalidRefs`. 
created_at: 2025-04-09T04:30:29Z
author_association: MEMBER
diff_hunk: @@ -65,117 +63,91 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String FILTERS_EVENT_SOURCE_NAME = "filters";
     public static final String TRANSITIVELY_REFERENCED_RESOURCES_NOT_FOUND = "TransitivelyReferencedResourcesNotFound";
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
+    private static final String KAFKA_PROXY_INGRESS_KIND = getKind(KafkaProxyIngress.class);
+    private static final String KAFKA_PROXY_KIND = getKind(KafkaProxy.class);
+    private static final String KAFKA_SERVICE_KIND = getKind(KafkaService.class);
+    private static final String KAFKA_PROTOCOL_FILTER_KIND = getKind(KafkaProtocolFilter.class);
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.resolveClusterRefs(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: SamBarker
body: Why is this validation the callers responsibility? Shouldn't `clusterResult(cluster)` throw if its been asked for a cluster that it doesn't have results for?
created_at: 2025-04-09T04:32:06Z
author_association: MEMBER
diff_hunk: @@ -65,117 +63,91 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String FILTERS_EVENT_SOURCE_NAME = "filters";
     public static final String TRANSITIVELY_REFERENCED_RESOURCES_NOT_FOUND = "TransitivelyReferencedResourcesNotFound";
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
+    private static final String KAFKA_PROXY_INGRESS_KIND = getKind(KafkaProxyIngress.class);
+    private static final String KAFKA_PROXY_KIND = getKind(KafkaProxy.class);
+    private static final String KAFKA_SERVICE_KIND = getKind(KafkaService.class);
+    private static final String KAFKA_PROTOCOL_FILTER_KIND = getKind(KafkaProtocolFilter.class);
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.resolveClusterRefs(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        if (clusterResolutionResult.isFullyResolved()) {
+            updateControl = maybeCombineStatusWithClusterConfigMap(cluster, context);
         }
         else {
-            Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster, missingProxies);
-            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, missingServices);
-            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, missingIngresses);
-            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, missingFilters);
-
-            uc = UpdateControl.patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_REFS_NOT_FOUND,
-                    joiningMessages(proxyMsg, serviceMsg, ingressMsg, filterMsg)));
+            updateControl = createUnresolvedRefsStatus(cluster, clusterResolutionResult);
         }
-
         if (LOGGER.isInfoEnabled()) {
             LOGGER.info("Completed reconciliation of {}/{}", namespace(cluster), name(cluster));
         }
-        return uc;
+        return updateControl;
+    }
+
+    private @NonNull UpdateControl<VirtualKafkaCluster> maybeCombineStatusWithClusterConfigMap(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        updateControl = context
+                .getSecondaryResource(ConfigMap.class)
+                .flatMap(cm -> Optional.ofNullable(cm.getData()))
+                .map(ProxyConfigData::new)
+                .flatMap(data -> data.getStatusPatchForCluster(name(cluster)))
+                .map(patch -> {
+                    var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
+                    var acc = ResourceState.fromList(patch.getStatus().getConditions());
+                    return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
+                })
+                .map(UpdateControl::patchStatus)
+                .orElse(UpdateControl.noUpdate());
+        return updateControl;
+    }
+
+    private @NonNull UpdateControl<VirtualKafkaCluster> createUnresolvedRefsStatus(VirtualKafkaCluster cluster,
+                                                                                   ResolutionResult.ClusterResolutionResult clusterResolutionResult) {
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
+        UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+        var unresovedIngressProxies = unresolvedReferences.getUnresolvedReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND).collect(Collectors.toSet());
+        if (unresolvedReferences.anyDirectDependenciesUnresolved()) {
+            Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROXY_KIND));
+            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_SERVICE_KIND));
+            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROXY_INGRESS_KIND));
+            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROTOCOL_FILTER_KIND));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: SamBarker
body: I'd expect the resolution result to do this. for example:

`String message = unresolvedReferences.describeUnresolvedDirectDependencies()`

This just to generate the message, its responsibility of the reconciler to build the updateControl.
created_at: 2025-04-09T04:34:47Z
author_association: MEMBER
diff_hunk: @@ -65,117 +63,91 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String FILTERS_EVENT_SOURCE_NAME = "filters";
     public static final String TRANSITIVELY_REFERENCED_RESOURCES_NOT_FOUND = "TransitivelyReferencedResourcesNotFound";
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
+    private static final String KAFKA_PROXY_INGRESS_KIND = getKind(KafkaProxyIngress.class);
+    private static final String KAFKA_PROXY_KIND = getKind(KafkaProxy.class);
+    private static final String KAFKA_SERVICE_KIND = getKind(KafkaService.class);
+    private static final String KAFKA_PROTOCOL_FILTER_KIND = getKind(KafkaProtocolFilter.class);
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.resolveClusterRefs(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        if (clusterResolutionResult.isFullyResolved()) {
+            updateControl = maybeCombineStatusWithClusterConfigMap(cluster, context);
         }
         else {
-            Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster, missingProxies);
-            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, missingServices);
-            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, missingIngresses);
-            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, missingFilters);
-
-            uc = UpdateControl.patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_REFS_NOT_FOUND,
-                    joiningMessages(proxyMsg, serviceMsg, ingressMsg, filterMsg)));
+            updateControl = createUnresolvedRefsStatus(cluster, clusterResolutionResult);
         }
-
         if (LOGGER.isInfoEnabled()) {
             LOGGER.info("Completed reconciliation of {}/{}", namespace(cluster), name(cluster));
         }
-        return uc;
+        return updateControl;
+    }
+
+    private @NonNull UpdateControl<VirtualKafkaCluster> maybeCombineStatusWithClusterConfigMap(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        updateControl = context
+                .getSecondaryResource(ConfigMap.class)
+                .flatMap(cm -> Optional.ofNullable(cm.getData()))
+                .map(ProxyConfigData::new)
+                .flatMap(data -> data.getStatusPatchForCluster(name(cluster)))
+                .map(patch -> {
+                    var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
+                    var acc = ResourceState.fromList(patch.getStatus().getConditions());
+                    return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
+                })
+                .map(UpdateControl::patchStatus)
+                .orElse(UpdateControl.noUpdate());
+        return updateControl;
+    }
+
+    private @NonNull UpdateControl<VirtualKafkaCluster> createUnresolvedRefsStatus(VirtualKafkaCluster cluster,
+                                                                                   ResolutionResult.ClusterResolutionResult clusterResolutionResult) {
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
+        UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+        var unresovedIngressProxies = unresolvedReferences.getUnresolvedReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND).collect(Collectors.toSet());
+        if (unresolvedReferences.anyDirectDependenciesUnresolved()) {
+            Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROXY_KIND));
+            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_SERVICE_KIND));
+            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROXY_INGRESS_KIND));
+            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROTOCOL_FILTER_KIND));
+            updateControl = UpdateControl.patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_REFS_NOT_FOUND,
+                    joiningMessages(proxyMsg, serviceMsg, ingressMsg, filterMsg)));
+        }
+        else if (unresolvedReferences.anyResolvedRefsConditionsFalse() || !unresovedIngressProxies.isEmpty()) {
+            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
+                    unresolvedReferences.kafkaServicesWithResolvedRefsFalse().stream().map(ResourcesUtil::toLocalRef));
+            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster,
+                    unresolvedReferences.ingressesWithResolvedRefsFalse().stream().map(ResourcesUtil::toLocalRef));
+            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
+                    unresolvedReferences.filtersWithResolvedRefsFalse().stream().map(ResourcesUtil::toLocalRef));
+            Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: SamBarker
body: Similarly to the above 

`unresolvedReferences.describeUnresolvedTransitiveDependencies()`

modulo a deciding if transitive is the right describing word.
created_at: 2025-04-09T04:35:56Z
author_association: MEMBER
diff_hunk: @@ -42,9 +53,22 @@ public static DependencyResolver create() {
     }
 
     @Override
-    public ResolutionResult deepResolve(Context<KafkaProxy> context) {
+    public ResolutionResult resolveProxyRefs(KafkaProxy proxy, Context<?> context) {
+        Objects.requireNonNull(proxy);
         Objects.requireNonNull(context);
         Set<VirtualKafkaCluster> virtualKafkaClusters = context.getSecondaryResources(VirtualKafkaCluster.class);
+        return resolve(virtualKafkaClusters, Set.of(proxy), context);
+    }
+
+    @Override
+    public ResolutionResult resolveClusterRefs(VirtualKafkaCluster cluster, Context<?> context) {
+        Objects.requireNonNull(cluster);
+        Objects.requireNonNull(context);
+        Set<KafkaProxy> proxies = context.getSecondaryResources(KafkaProxy.class);
+        return resolve(Set.of(cluster), proxies, context);
+    }
+
+    private @NonNull ResolutionResult resolve(Set<VirtualKafkaCluster> virtualKafkaClusters, Set<KafkaProxy> proxies, Context<?> context) {
         if (virtualKafkaClusters.isEmpty()) {
             return EMPTY_RESOLUTION_RESULT;
         }
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImpl.java
user: login: SamBarker
body: Should we also be defensive against an empty set of Proxies? Given the set of proxies is determined in the same fashion as the set of `virtualKafaClusters`.

I'm also not sure if its this method or the callers that should be defensive about empty sets.
created_at: 2025-04-09T04:49:16Z
author_association: MEMBER
diff_hunk: @@ -36,18 +39,76 @@ public class ResolutionResult {
     private final Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs;
     private final Set<ClusterResolutionResult> clusterResolutionResults;
 
-    public record ClusterResolutionResult(VirtualKafkaCluster cluster, Set<LocalRef<?>> unresolvedDependencySet) {
-        public ClusterResolutionResult {
-            Objects.requireNonNull(cluster);
-            Objects.requireNonNull(unresolvedDependencySet);
+    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
+        return clusterResolutionResults.stream()
+                .filter(r -> r.cluster == cluster)
+                .findFirst();
+    }
+
+    enum DependencyType {
+        // from the virtual kafka cluster to another entity
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: SamBarker
body: Why is it special to the VKC? 
created_at: 2025-04-09T04:50:39Z
author_association: MEMBER
diff_hunk: @@ -36,18 +39,76 @@ public class ResolutionResult {
     private final Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs;
     private final Set<ClusterResolutionResult> clusterResolutionResults;
 
-    public record ClusterResolutionResult(VirtualKafkaCluster cluster, Set<LocalRef<?>> unresolvedDependencySet) {
-        public ClusterResolutionResult {
-            Objects.requireNonNull(cluster);
-            Objects.requireNonNull(unresolvedDependencySet);
+    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
+        return clusterResolutionResults.stream()
+                .filter(r -> r.cluster == cluster)
+                .findFirst();
+    }
+
+    enum DependencyType {
+        // from the virtual kafka cluster to another entity
+        DIRECT,
+        // from any other entity to another entity
+        TRANSITIVE
+    }
+
+    public record UnresolvedReferences(Set<UnresolvedReference> unresolved,
+                                       Set<KafkaProxyIngress> ingressesWithResolvedRefsFalse,
+                                       Set<KafkaProtocolFilter> filtersWithResolvedRefsFalse,
+                                       Set<KafkaService> kafkaServicesWithResolvedRefsFalse) {
+        public UnresolvedReferences {
+            Objects.requireNonNull(unresolved);
+        }
+
+        public Stream<LocalRef<?>> getUnresolvedReferences(LocalRef<?> from, String kindTo) {
+            Objects.requireNonNull(from);
+            Objects.requireNonNull(kindTo);
+            return unresolved.stream().filter(r -> r.from.equals(from) && r.to.getKind().equals(kindTo)).map(UnresolvedReference::to);
         }
 
-        public boolean isAnyDependencyUnresolved() {
-            return !unresolvedDependencySet.isEmpty();
+        public Stream<LocalRef<?>> getUnresolvedReferences(String fromKind, String toKind) {
+            Objects.requireNonNull(fromKind);
+            Objects.requireNonNull(toKind);
+            return unresolved.stream().filter(r -> r.from.getKind().equals(fromKind) && r.to.getKind().equals(toKind)).map(UnresolvedReference::to);
+        }
+
+        public boolean isFullyResolved() {
+            return unresolved.isEmpty() && filtersWithResolvedRefsFalse.isEmpty() && ingressesWithResolvedRefsFalse.isEmpty()
+                    && kafkaServicesWithResolvedRefsFalse.isEmpty();
+        }
+
+        public boolean anyDirectDependenciesUnresolved() {
+            return unresolved.stream().anyMatch(u -> u.dependencyType == DependencyType.DIRECT);
+        }
+
+        public boolean anyResolvedRefsConditionsFalse() {
+            return !ingressesWithResolvedRefsFalse.isEmpty() || !filtersWithResolvedRefsFalse.isEmpty() || !kafkaServicesWithResolvedRefsFalse.isEmpty();
+        }
+
+        public Set<HasMetadata> resourcesWithResolvedRefsFalse() {
+            HashSet<HasMetadata> hasMetadata = new HashSet<>();
+            hasMetadata.addAll(filtersWithResolvedRefsFalse);
+            hasMetadata.addAll(kafkaServicesWithResolvedRefsFalse);
+            hasMetadata.addAll(ingressesWithResolvedRefsFalse);
+            return hasMetadata;
+        }
+    }
+
+    public record UnresolvedReference(LocalRef<?> from, LocalRef<?> to, DependencyType dependencyType) {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: SamBarker
body: We should probably annotate the args as `@NonNull` 
created_at: 2025-04-09T04:53:49Z
author_association: MEMBER
diff_hunk: @@ -36,18 +39,76 @@ public class ResolutionResult {
     private final Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs;
     private final Set<ClusterResolutionResult> clusterResolutionResults;
 
-    public record ClusterResolutionResult(VirtualKafkaCluster cluster, Set<LocalRef<?>> unresolvedDependencySet) {
-        public ClusterResolutionResult {
-            Objects.requireNonNull(cluster);
-            Objects.requireNonNull(unresolvedDependencySet);
+    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
+        return clusterResolutionResults.stream()
+                .filter(r -> r.cluster == cluster)
+                .findFirst();
+    }
+
+    enum DependencyType {
+        // from the virtual kafka cluster to another entity
+        DIRECT,
+        // from any other entity to another entity
+        TRANSITIVE
+    }
+
+    public record UnresolvedReferences(Set<UnresolvedReference> unresolved,
+                                       Set<KafkaProxyIngress> ingressesWithResolvedRefsFalse,
+                                       Set<KafkaProtocolFilter> filtersWithResolvedRefsFalse,
+                                       Set<KafkaService> kafkaServicesWithResolvedRefsFalse) {
+        public UnresolvedReferences {
+            Objects.requireNonNull(unresolved);
+        }
+
+        public Stream<LocalRef<?>> getUnresolvedReferences(LocalRef<?> from, String kindTo) {
+            Objects.requireNonNull(from);
+            Objects.requireNonNull(kindTo);
+            return unresolved.stream().filter(r -> r.from.equals(from) && r.to.getKind().equals(kindTo)).map(UnresolvedReference::to);
         }
 
-        public boolean isAnyDependencyUnresolved() {
-            return !unresolvedDependencySet.isEmpty();
+        public Stream<LocalRef<?>> getUnresolvedReferences(String fromKind, String toKind) {
+            Objects.requireNonNull(fromKind);
+            Objects.requireNonNull(toKind);
+            return unresolved.stream().filter(r -> r.from.getKind().equals(fromKind) && r.to.getKind().equals(toKind)).map(UnresolvedReference::to);
+        }
+
+        public boolean isFullyResolved() {
+            return unresolved.isEmpty() && filtersWithResolvedRefsFalse.isEmpty() && ingressesWithResolvedRefsFalse.isEmpty()
+                    && kafkaServicesWithResolvedRefsFalse.isEmpty();
+        }
+
+        public boolean anyDirectDependenciesUnresolved() {
+            return unresolved.stream().anyMatch(u -> u.dependencyType == DependencyType.DIRECT);
+        }
+
+        public boolean anyResolvedRefsConditionsFalse() {
+            return !ingressesWithResolvedRefsFalse.isEmpty() || !filtersWithResolvedRefsFalse.isEmpty() || !kafkaServicesWithResolvedRefsFalse.isEmpty();
+        }
+
+        public Set<HasMetadata> resourcesWithResolvedRefsFalse() {
+            HashSet<HasMetadata> hasMetadata = new HashSet<>();
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: SamBarker
body: nit: 
```suggestion
            Set<HasMetadata> hasMetadata = new HashSet<>();
```
created_at: 2025-04-09T04:54:12Z
author_association: MEMBER
diff_hunk: @@ -17,18 +17,29 @@
 
 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.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.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterStatus;
 import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.ClusterResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.UnresolvedReference;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
 
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
+import static io.kroxylicious.kubernetes.operator.ResourcesUtil.toLocalRef;
+import static io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.DependencyType.DIRECT;
+import static io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.DependencyType.TRANSITIVE;
 
 public class DependencyResolverImpl implements DependencyResolver {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImpl.java
user: login: SamBarker
body: Whats special about this resolved? What makes it unique. Just calling it `impl` suggests to me there is no real need for an interface. 
created_at: 2025-04-09T04:55:50Z
author_association: MEMBER
diff_hunk: @@ -17,18 +17,29 @@
 
 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.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.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterStatus;
 import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.ClusterResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.UnresolvedReference;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
 
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
+import static io.kroxylicious.kubernetes.operator.ResourcesUtil.toLocalRef;
+import static io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.DependencyType.DIRECT;
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImpl.java
user: login: SamBarker
body: This suggests to me `DependencyType` is a first class member of the Resolver API.
created_at: 2025-04-09T04:56:39Z
author_association: MEMBER
diff_hunk: @@ -63,226 +85,614 @@ void testNoDependencies() {
         assertThat(resolutionResult.filters()).isEmpty();
     }
 
+    private @NonNull ResolutionResult resolveProxyRefs(KafkaProxy proxy) {
+        return DependencyResolverImpl.create().resolveProxyRefs(proxy, mockProxyContext);
path: kroxylicious-operator/src/test/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImplTest.java
user: login: SamBarker
body: Why not call `DependencyResolverImpl.create()` in `setup` and just use the field at call sites ?
created_at: 2025-04-09T04:58:36Z
author_association: MEMBER
diff_hunk: @@ -141,27 +143,38 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigData data) {
         proxyModel.resolutionResult().clusterResults().stream()
-                .filter(ResolutionResult.ClusterResolutionResult::isAnyDependencyUnresolved)
+                .filter(clusterResolutionResult1 -> !clusterResolutionResult1.isFullyResolved())
                 .forEach(clusterResolutionResult -> {
-
-                    Comparator<LocalRef<?>> comparator = Comparator.<LocalRef<?>, String> comparing(LocalRef::getGroup)
-                            .thenComparing(LocalRef::getKind)
-                            .thenComparing(LocalRef::getName);
-
-                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedDependencySet().stream()
-                            .sorted(comparator).findFirst()
-                            .orElseThrow();
-                    String message = String.format("Resource %s was not found.",
-                            ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
-
+                    VirtualKafkaCluster patch;
+                    ResolutionResult.UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+                    if (!unresolvedReferences.unresolved().isEmpty()) {
+                        Comparator<UnresolvedReference> comparator = Comparator.<UnresolvedReference, LocalRef> comparing(UnresolvedReference::to);
+
+                        LocalRef<?> firstUnresolvedDependency = unresolvedReferences.unresolved().stream()
+                                .sorted(comparator).map(UnresolvedReference::to).findFirst()
+                                .orElseThrow();
+                        String message = String.format("Resource %s was not found.",
+                                ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
+                    }
+                    else {
+                        String message = String.format("Resource %s has ResolvedRefs=False.",
+                                ResourcesUtil.namespacedSlug(firstLocalRef(unresolvedReferences.resourcesWithResolvedRefsFalse()), clusterResolutionResult.cluster()));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigConfigMap.java
user: login: robobario
body: I've been using `unresolved` to mean we attempted to retrieve a custom resource but could not find one.

Leaf resources like KafkaProtocolFilter may be resolved (obtainable from k8s), but have a `ResolvedRefs: False condition` because a Secret cannot be resolved.
created_at: 2025-04-09T21:03:38Z
author_association: MEMBER
diff_hunk: @@ -141,27 +143,38 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigData data) {
         proxyModel.resolutionResult().clusterResults().stream()
-                .filter(ResolutionResult.ClusterResolutionResult::isAnyDependencyUnresolved)
+                .filter(clusterResolutionResult1 -> !clusterResolutionResult1.isFullyResolved())
                 .forEach(clusterResolutionResult -> {
-
-                    Comparator<LocalRef<?>> comparator = Comparator.<LocalRef<?>, String> comparing(LocalRef::getGroup)
-                            .thenComparing(LocalRef::getKind)
-                            .thenComparing(LocalRef::getName);
-
-                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedDependencySet().stream()
-                            .sorted(comparator).findFirst()
-                            .orElseThrow();
-                    String message = String.format("Resource %s was not found.",
-                            ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
-
+                    VirtualKafkaCluster patch;
+                    ResolutionResult.UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+                    if (!unresolvedReferences.unresolved().isEmpty()) {
+                        Comparator<UnresolvedReference> comparator = Comparator.<UnresolvedReference, LocalRef> comparing(UnresolvedReference::to);
+
+                        LocalRef<?> firstUnresolvedDependency = unresolvedReferences.unresolved().stream()
+                                .sorted(comparator).map(UnresolvedReference::to).findFirst()
+                                .orElseThrow();
+                        String message = String.format("Resource %s was not found.",
+                                ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
+                    }
+                    else {
+                        String message = String.format("Resource %s has ResolvedRefs=False.",
+                                ResourcesUtil.namespacedSlug(firstLocalRef(unresolvedReferences.resourcesWithResolvedRefsFalse()), clusterResolutionResult.cluster()));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigConfigMap.java
user: login: robobario
body: Also it's possible the aggregator may be able to resolve/see some resources referred to by resource X by following the Refs, even though X has a status of ResolvedRefs: False
created_at: 2025-04-09T21:05:43Z
author_association: MEMBER
diff_hunk: @@ -65,117 +63,91 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String FILTERS_EVENT_SOURCE_NAME = "filters";
     public static final String TRANSITIVELY_REFERENCED_RESOURCES_NOT_FOUND = "TransitivelyReferencedResourcesNotFound";
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
+    private static final String KAFKA_PROXY_INGRESS_KIND = getKind(KafkaProxyIngress.class);
+    private static final String KAFKA_PROXY_KIND = getKind(KafkaProxy.class);
+    private static final String KAFKA_SERVICE_KIND = getKind(KafkaService.class);
+    private static final String KAFKA_PROTOCOL_FILTER_KIND = getKind(KafkaProtocolFilter.class);
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.resolveClusterRefs(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        if (clusterResolutionResult.isFullyResolved()) {
+            updateControl = maybeCombineStatusWithClusterConfigMap(cluster, context);
         }
         else {
-            Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster, missingProxies);
-            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, missingServices);
-            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, missingIngresses);
-            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, missingFilters);
-
-            uc = UpdateControl.patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_REFS_NOT_FOUND,
-                    joiningMessages(proxyMsg, serviceMsg, ingressMsg, filterMsg)));
+            updateControl = createUnresolvedRefsStatus(cluster, clusterResolutionResult);
         }
-
         if (LOGGER.isInfoEnabled()) {
             LOGGER.info("Completed reconciliation of {}/{}", namespace(cluster), name(cluster));
         }
-        return uc;
+        return updateControl;
+    }
+
+    private @NonNull UpdateControl<VirtualKafkaCluster> maybeCombineStatusWithClusterConfigMap(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        updateControl = context
+                .getSecondaryResource(ConfigMap.class)
+                .flatMap(cm -> Optional.ofNullable(cm.getData()))
+                .map(ProxyConfigData::new)
+                .flatMap(data -> data.getStatusPatchForCluster(name(cluster)))
+                .map(patch -> {
+                    var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
+                    var acc = ResourceState.fromList(patch.getStatus().getConditions());
+                    return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
+                })
+                .map(UpdateControl::patchStatus)
+                .orElse(UpdateControl.noUpdate());
+        return updateControl;
+    }
+
+    private @NonNull UpdateControl<VirtualKafkaCluster> createUnresolvedRefsStatus(VirtualKafkaCluster cluster,
+                                                                                   ResolutionResult.ClusterResolutionResult clusterResolutionResult) {
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
+        UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+        var unresovedIngressProxies = unresolvedReferences.getUnresolvedReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND).collect(Collectors.toSet());
+        if (unresolvedReferences.anyDirectDependenciesUnresolved()) {
+            Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROXY_KIND));
+            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_SERVICE_KIND));
+            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROXY_INGRESS_KIND));
+            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROTOCOL_FILTER_KIND));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: robobario
body: can do, to keep all tests passing I'd left it up to reconcilers as the VKCR and KPR produce different status messages. Though I think we're all in agreement that KPR shouldn't emit a ResolvedRefs condition, but something else.
created_at: 2025-04-09T21:08:41Z
author_association: MEMBER
diff_hunk: @@ -42,9 +53,22 @@ public static DependencyResolver create() {
     }
 
     @Override
-    public ResolutionResult deepResolve(Context<KafkaProxy> context) {
+    public ResolutionResult resolveProxyRefs(KafkaProxy proxy, Context<?> context) {
+        Objects.requireNonNull(proxy);
         Objects.requireNonNull(context);
         Set<VirtualKafkaCluster> virtualKafkaClusters = context.getSecondaryResources(VirtualKafkaCluster.class);
+        return resolve(virtualKafkaClusters, Set.of(proxy), context);
+    }
+
+    @Override
+    public ResolutionResult resolveClusterRefs(VirtualKafkaCluster cluster, Context<?> context) {
+        Objects.requireNonNull(cluster);
+        Objects.requireNonNull(context);
+        Set<KafkaProxy> proxies = context.getSecondaryResources(KafkaProxy.class);
+        return resolve(Set.of(cluster), proxies, context);
+    }
+
+    private @NonNull ResolutionResult resolve(Set<VirtualKafkaCluster> virtualKafkaClusters, Set<KafkaProxy> proxies, Context<?> context) {
         if (virtualKafkaClusters.isEmpty()) {
             return EMPTY_RESOLUTION_RESULT;
         }
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImpl.java
user: login: robobario
body: empty proxy set is a possibility on the VKC side that will cause the resolution result to describe problems, likely from the VKC proxyRef, possibly also from ingresses.

If we are reconciling a VKC and can't find the proxy we need to describe the problem.
created_at: 2025-04-09T21:11:26Z
author_association: MEMBER
diff_hunk: @@ -42,9 +53,22 @@ public static DependencyResolver create() {
     }
 
     @Override
-    public ResolutionResult deepResolve(Context<KafkaProxy> context) {
+    public ResolutionResult resolveProxyRefs(KafkaProxy proxy, Context<?> context) {
+        Objects.requireNonNull(proxy);
         Objects.requireNonNull(context);
         Set<VirtualKafkaCluster> virtualKafkaClusters = context.getSecondaryResources(VirtualKafkaCluster.class);
+        return resolve(virtualKafkaClusters, Set.of(proxy), context);
+    }
+
+    @Override
+    public ResolutionResult resolveClusterRefs(VirtualKafkaCluster cluster, Context<?> context) {
+        Objects.requireNonNull(cluster);
+        Objects.requireNonNull(context);
+        Set<KafkaProxy> proxies = context.getSecondaryResources(KafkaProxy.class);
+        return resolve(Set.of(cluster), proxies, context);
+    }
+
+    private @NonNull ResolutionResult resolve(Set<VirtualKafkaCluster> virtualKafkaClusters, Set<KafkaProxy> proxies, Context<?> context) {
         if (virtualKafkaClusters.isEmpty()) {
             return EMPTY_RESOLUTION_RESULT;
         }
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImpl.java
user: login: robobario
body: where empty VKC set in the proxy reconciler is a valid state, could be a proxy with no VKCs reffing it
created_at: 2025-04-09T21:12:02Z
author_association: MEMBER
diff_hunk: @@ -17,18 +17,29 @@
 
 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.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.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterStatus;
 import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.ClusterResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.UnresolvedReference;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
 
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
+import static io.kroxylicious.kubernetes.operator.ResourcesUtil.toLocalRef;
+import static io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.DependencyType.DIRECT;
+import static io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.DependencyType.TRANSITIVE;
 
 public class DependencyResolverImpl implements DependencyResolver {
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImpl.java
user: login: robobario
body: yeahp can drop the interface
created_at: 2025-04-09T21:13:10Z
author_association: MEMBER
diff_hunk: @@ -65,117 +63,91 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String FILTERS_EVENT_SOURCE_NAME = "filters";
     public static final String TRANSITIVELY_REFERENCED_RESOURCES_NOT_FOUND = "TransitivelyReferencedResourcesNotFound";
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
+    private static final String KAFKA_PROXY_INGRESS_KIND = getKind(KafkaProxyIngress.class);
+    private static final String KAFKA_PROXY_KIND = getKind(KafkaProxy.class);
+    private static final String KAFKA_SERVICE_KIND = getKind(KafkaService.class);
+    private static final String KAFKA_PROTOCOL_FILTER_KIND = getKind(KafkaProtocolFilter.class);
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.resolveClusterRefs(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        if (clusterResolutionResult.isFullyResolved()) {
+            updateControl = maybeCombineStatusWithClusterConfigMap(cluster, context);
         }
         else {
-            Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster, missingProxies);
-            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, missingServices);
-            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, missingIngresses);
-            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, missingFilters);
-
-            uc = UpdateControl.patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_REFS_NOT_FOUND,
-                    joiningMessages(proxyMsg, serviceMsg, ingressMsg, filterMsg)));
+            updateControl = createUnresolvedRefsStatus(cluster, clusterResolutionResult);
         }
-
         if (LOGGER.isInfoEnabled()) {
             LOGGER.info("Completed reconciliation of {}/{}", namespace(cluster), name(cluster));
         }
-        return uc;
+        return updateControl;
+    }
+
+    private @NonNull UpdateControl<VirtualKafkaCluster> maybeCombineStatusWithClusterConfigMap(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        updateControl = context
+                .getSecondaryResource(ConfigMap.class)
+                .flatMap(cm -> Optional.ofNullable(cm.getData()))
+                .map(ProxyConfigData::new)
+                .flatMap(data -> data.getStatusPatchForCluster(name(cluster)))
+                .map(patch -> {
+                    var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
+                    var acc = ResourceState.fromList(patch.getStatus().getConditions());
+                    return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
+                })
+                .map(UpdateControl::patchStatus)
+                .orElse(UpdateControl.noUpdate());
+        return updateControl;
+    }
+
+    private @NonNull UpdateControl<VirtualKafkaCluster> createUnresolvedRefsStatus(VirtualKafkaCluster cluster,
+                                                                                   ResolutionResult.ClusterResolutionResult clusterResolutionResult) {
+        UpdateControl<VirtualKafkaCluster> updateControl;
+        LocalRef<VirtualKafkaCluster> clusterRef = toLocalRef(cluster);
+        UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+        var unresovedIngressProxies = unresolvedReferences.getUnresolvedReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND).collect(Collectors.toSet());
+        if (unresolvedReferences.anyDirectDependenciesUnresolved()) {
+            Stream<String> proxyMsg = refsMessage("spec.proxyRef references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROXY_KIND));
+            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_SERVICE_KIND));
+            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROXY_INGRESS_KIND));
+            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(clusterRef, KAFKA_PROTOCOL_FILTER_KIND));
+            updateControl = UpdateControl.patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_REFS_NOT_FOUND,
+                    joiningMessages(proxyMsg, serviceMsg, ingressMsg, filterMsg)));
+        }
+        else if (unresolvedReferences.anyResolvedRefsConditionsFalse() || !unresovedIngressProxies.isEmpty()) {
+            Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster,
+                    unresolvedReferences.kafkaServicesWithResolvedRefsFalse().stream().map(ResourcesUtil::toLocalRef));
+            Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster,
+                    unresolvedReferences.ingressesWithResolvedRefsFalse().stream().map(ResourcesUtil::toLocalRef));
+            Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster,
+                    unresolvedReferences.filtersWithResolvedRefsFalse().stream().map(ResourcesUtil::toLocalRef));
+            Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster,
+                    unresolvedReferences.getUnresolvedReferences(KAFKA_PROXY_INGRESS_KIND, KAFKA_PROXY_KIND));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: robobario
body: wondering if the transitive/direct divide is really needed, since we have the option of asking for unresolved refs by Kind or LocalRef, we can control which kinds of problem we are interested in.
created_at: 2025-04-09T21:15:02Z
author_association: MEMBER
diff_hunk: @@ -65,117 +63,91 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String FILTERS_EVENT_SOURCE_NAME = "filters";
     public static final String TRANSITIVELY_REFERENCED_RESOURCES_NOT_FOUND = "TransitivelyReferencedResourcesNotFound";
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
+    private static final String KAFKA_PROXY_INGRESS_KIND = getKind(KafkaProxyIngress.class);
+    private static final String KAFKA_PROXY_KIND = getKind(KafkaProxy.class);
+    private static final String KAFKA_SERVICE_KIND = getKind(KafkaService.class);
+    private static final String KAFKA_PROTOCOL_FILTER_KIND = getKind(KafkaProtocolFilter.class);
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.resolveClusterRefs(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: robobario
body: For now I might change it from `resolveClusterRefs` to `ClusterResolutionResult validateClusterRefs()` since the VKC side doesn't operate on the dependencies, it just checks references and updates status. Will avoid this issue, and make it internal to the DependencyResolver
created_at: 2025-04-09T21:17:22Z
author_association: MEMBER
diff_hunk: @@ -36,18 +39,76 @@ public class ResolutionResult {
     private final Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs;
     private final Set<ClusterResolutionResult> clusterResolutionResults;
 
-    public record ClusterResolutionResult(VirtualKafkaCluster cluster, Set<LocalRef<?>> unresolvedDependencySet) {
-        public ClusterResolutionResult {
-            Objects.requireNonNull(cluster);
-            Objects.requireNonNull(unresolvedDependencySet);
+    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
+        return clusterResolutionResults.stream()
+                .filter(r -> r.cluster == cluster)
+                .findFirst();
+    }
+
+    enum DependencyType {
+        // from the virtual kafka cluster to another entity
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: robobario
body: the error messages are all relative to a VKC and we are trying to describe the resolution result per VKC. I'll see if I can remove this concept.
created_at: 2025-04-09T21:20:28Z
author_association: MEMBER
diff_hunk: @@ -17,18 +17,29 @@
 
 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.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.filter.api.v1alpha1.KafkaProtocolFilter;
+import io.kroxylicious.kubernetes.filter.api.v1alpha1.KafkaProtocolFilterStatus;
 import io.kroxylicious.kubernetes.operator.ResourcesUtil;
 import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.ClusterResolutionResult;
+import io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.UnresolvedReference;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
 
 import static io.kroxylicious.kubernetes.operator.ResourcesUtil.name;
+import static io.kroxylicious.kubernetes.operator.ResourcesUtil.toLocalRef;
+import static io.kroxylicious.kubernetes.operator.resolver.ResolutionResult.DependencyType.DIRECT;
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/DependencyResolverImpl.java
user: login: robobario
body: I've removed it, since it's possible to find danglingRefs by kind or by the referencing entity this classification was redundant.
created_at: 2025-04-09T23:15:12Z
author_association: MEMBER
diff_hunk: @@ -36,18 +39,76 @@ public class ResolutionResult {
     private final Map<LocalRef<KafkaService>, KafkaService> kafkaServiceRefs;
     private final Set<ClusterResolutionResult> clusterResolutionResults;
 
-    public record ClusterResolutionResult(VirtualKafkaCluster cluster, Set<LocalRef<?>> unresolvedDependencySet) {
-        public ClusterResolutionResult {
-            Objects.requireNonNull(cluster);
-            Objects.requireNonNull(unresolvedDependencySet);
+    public Optional<ClusterResolutionResult> clusterResult(VirtualKafkaCluster cluster) {
+        return clusterResolutionResults.stream()
+                .filter(r -> r.cluster == cluster)
+                .findFirst();
+    }
+
+    enum DependencyType {
+        // from the virtual kafka cluster to another entity
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/resolver/ResolutionResult.java
user: login: robobario
body: DependencyType is removed
created_at: 2025-04-09T23:15:34Z
author_association: MEMBER
diff_hunk: @@ -65,117 +63,91 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String FILTERS_EVENT_SOURCE_NAME = "filters";
     public static final String TRANSITIVELY_REFERENCED_RESOURCES_NOT_FOUND = "TransitivelyReferencedResourcesNotFound";
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
+    private static final String KAFKA_PROXY_INGRESS_KIND = getKind(KafkaProxyIngress.class);
+    private static final String KAFKA_PROXY_KIND = getKind(KafkaProxy.class);
+    private static final String KAFKA_SERVICE_KIND = getKind(KafkaService.class);
+    private static final String KAFKA_PROTOCOL_FILTER_KIND = getKind(KafkaProtocolFilter.class);
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.resolveClusterRefs(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: robobario
body: it's now internal to the `DependencyResolver`, we still go through a ProxyResolutionResult internally when getting the result for a virtual cluster, which I think we could refactor away from in future. It's backwards. A `ProxyResolutionResult` could instead be built up from multiple calls to `resolveClusterRefs`, just trying to limit the size of this branch.
created_at: 2025-04-09T23:18:07Z
author_association: MEMBER
diff_hunk: @@ -141,27 +143,38 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigData data) {
         proxyModel.resolutionResult().clusterResults().stream()
-                .filter(ResolutionResult.ClusterResolutionResult::isAnyDependencyUnresolved)
+                .filter(clusterResolutionResult1 -> !clusterResolutionResult1.isFullyResolved())
                 .forEach(clusterResolutionResult -> {
-
-                    Comparator<LocalRef<?>> comparator = Comparator.<LocalRef<?>, String> comparing(LocalRef::getGroup)
-                            .thenComparing(LocalRef::getKind)
-                            .thenComparing(LocalRef::getName);
-
-                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedDependencySet().stream()
-                            .sorted(comparator).findFirst()
-                            .orElseThrow();
-                    String message = String.format("Resource %s was not found.",
-                            ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
-
+                    VirtualKafkaCluster patch;
+                    ResolutionResult.UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+                    if (!unresolvedReferences.unresolved().isEmpty()) {
+                        Comparator<UnresolvedReference> comparator = Comparator.<UnresolvedReference, LocalRef> comparing(UnresolvedReference::to);
+
+                        LocalRef<?> firstUnresolvedDependency = unresolvedReferences.unresolved().stream()
+                                .sorted(comparator).map(UnresolvedReference::to).findFirst()
+                                .orElseThrow();
+                        String message = String.format("Resource %s was not found.",
+                                ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
+                    }
+                    else {
+                        String message = String.format("Resource %s has ResolvedRefs=False.",
+                                ResourcesUtil.namespacedSlug(firstLocalRef(unresolvedReferences.resourcesWithResolvedRefsFalse()), clusterResolutionResult.cluster()));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigConfigMap.java
user: login: robobario
body: have adopted `danglingRefs` naming instead of `unresolved` as unresolved is more ambiguous language, still need to kill off some usages of `unresolved` naming in the code though.
created_at: 2025-04-09T23:19:05Z
author_association: MEMBER
diff_hunk: @@ -141,27 +143,38 @@ private static void addAcceptedConditions(VirtualKafkaClusterStatusFactory statu
 
     private static void addResolvedRefsConditions(VirtualKafkaClusterStatusFactory statusFactory, ProxyModel proxyModel, ProxyConfigData data) {
         proxyModel.resolutionResult().clusterResults().stream()
-                .filter(ResolutionResult.ClusterResolutionResult::isAnyDependencyUnresolved)
+                .filter(clusterResolutionResult1 -> !clusterResolutionResult1.isFullyResolved())
                 .forEach(clusterResolutionResult -> {
-
-                    Comparator<LocalRef<?>> comparator = Comparator.<LocalRef<?>, String> comparing(LocalRef::getGroup)
-                            .thenComparing(LocalRef::getKind)
-                            .thenComparing(LocalRef::getName);
-
-                    LocalRef<?> firstUnresolvedDependency = clusterResolutionResult.unresolvedDependencySet().stream()
-                            .sorted(comparator).findFirst()
-                            .orElseThrow();
-                    String message = String.format("Resource %s was not found.",
-                            ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
                     VirtualKafkaCluster cluster = clusterResolutionResult.cluster();
-
+                    VirtualKafkaCluster patch;
+                    ResolutionResult.UnresolvedReferences unresolvedReferences = clusterResolutionResult.unresolvedReferences();
+                    if (!unresolvedReferences.unresolved().isEmpty()) {
+                        Comparator<UnresolvedReference> comparator = Comparator.<UnresolvedReference, LocalRef> comparing(UnresolvedReference::to);
+
+                        LocalRef<?> firstUnresolvedDependency = unresolvedReferences.unresolved().stream()
+                                .sorted(comparator).map(UnresolvedReference::to).findFirst()
+                                .orElseThrow();
+                        String message = String.format("Resource %s was not found.",
+                                ResourcesUtil.namespacedSlug(firstUnresolvedDependency, clusterResolutionResult.cluster()));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
+                    }
+                    else {
+                        String message = String.format("Resource %s has ResolvedRefs=False.",
+                                ResourcesUtil.namespacedSlug(firstLocalRef(unresolvedReferences.resourcesWithResolvedRefsFalse()), clusterResolutionResult.cluster()));
+                        patch = statusFactory.newFalseConditionStatusPatch(cluster,
+                                Condition.Type.ResolvedRefs, Condition.REASON_INVALID, message);
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/ProxyConfigConfigMap.java
user: login: SamBarker
body: Thanks Rob, I think that will really help
created_at: 2025-04-09T23:38:16Z
author_association: MEMBER
diff_hunk: @@ -67,115 +65,89 @@ public final class VirtualKafkaClusterReconciler implements
     public static final String REFERENCED_RESOURCES_NOT_FOUND = "ReferencedResourcesNotFound";
 
     private final VirtualKafkaClusterStatusFactory statusFactory;
+    private final DependencyResolver resolver;
 
-    public VirtualKafkaClusterReconciler(Clock clock) {
+    public VirtualKafkaClusterReconciler(Clock clock, DependencyResolver resolver) {
         this.statusFactory = new VirtualKafkaClusterStatusFactory(clock);
+        this.resolver = resolver;
     }
 
     @Override
     public UpdateControl<VirtualKafkaCluster> reconcile(VirtualKafkaCluster cluster, Context<VirtualKafkaCluster> context) {
-        var existingProxies = context.getSecondaryResource(KafkaProxy.class, PROXY_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        var existingProxyRefs = existingProxies.stream().map(ResourcesUtil::toLocalRef).toList();
-        TreeSet<LocalRef<KafkaProxy>> missingProxies = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getProxyRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingProxies.removeAll(existingProxyRefs);
-
-        var existingServices = context.getSecondaryResource(KafkaService.class, SERVICES_EVENT_SOURCE_NAME).stream().collect(Collectors.toSet());
-        TreeSet<LocalRef<KafkaService>> missingServices = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getTargetKafkaServiceRef)
-                .stream()
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingServices.removeAll(existingServices.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingIngresses = context.getSecondaryResources(KafkaProxyIngress.class);
-        TreeSet<LocalRef<KafkaProxyIngress>> missingIngresses = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getIngressRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingIngresses.removeAll(existingIngresses.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        var existingFilters = context.getSecondaryResources(KafkaProtocolFilter.class);
-        TreeSet<LocalRef<KafkaProtocolFilter>> missingFilters = Optional.ofNullable(cluster.getSpec())
-                .map(VirtualKafkaClusterSpec::getFilterRefs)
-                .stream().flatMap(Collection::stream)
-                .collect(Collectors.toCollection(TreeSet::new));
-        missingFilters.removeAll(existingFilters.stream()
-                .map(ResourcesUtil::toLocalRef)
-                .toList());
-
-        final UpdateControl<VirtualKafkaCluster> uc;
-        if (missingProxies.isEmpty()
-                && missingServices.isEmpty()
-                && missingIngresses.isEmpty()
-                && missingFilters.isEmpty()) {
-            var unresolvedServices = existingServices.stream()
-                    .filter(ks -> hasAnyResolvedRefsFalse(Optional.ofNullable(ks.getStatus()).map(KafkaServiceStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedIngresses = existingIngresses.stream()
-                    .filter(kafkaProxyIngress -> hasAnyResolvedRefsFalse(
-                            Optional.ofNullable(kafkaProxyIngress.getStatus()).map(KafkaProxyIngressStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-            var unresolvedFilters = existingFilters.stream()
-                    .filter(kpf -> hasAnyResolvedRefsFalse(Optional.ofNullable(kpf.getStatus()).map(KafkaProtocolFilterStatus::getConditions).orElse(List.of())))
-                    .map(ResourcesUtil::toLocalRef)
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            TreeSet<ProxyRef> ingressProxyRefs = existingIngresses.stream().map(i -> i.getSpec().getProxyRef()).collect(Collectors.toCollection(TreeSet::new));
-            // if the ingress references a different proxy to the virtual cluster, we will not have loaded the proxy.
-            // TODO can we discriminate between unresolved this mismatch?
-            TreeSet<ProxyRef> mismatchedIngressProxyRef = ingressProxyRefs.stream().filter(i -> !existingProxyRefs.contains(i))
-                    .collect(Collectors.toCollection(TreeSet::new));
-
-            if (unresolvedServices.isEmpty()
-                    && unresolvedIngresses.isEmpty()
-                    && unresolvedFilters.isEmpty()
-                    && mismatchedIngressProxyRef.isEmpty()) {
-                uc = context
-                        .getSecondaryResource(ConfigMap.class, PROXY_CONFIG_MAP_EVENT_SOURCE_NAME)
-                        .flatMap(cm -> Optional.ofNullable(cm.getData()))
-                        .map(ProxyConfigData::new)
-                        .flatMap(data -> data.getStatusPatchForCluster(ResourcesUtil.name(cluster)))
-                        .map(patch -> {
-                            var rr = ResourceState.of(statusFactory.newTrueCondition(cluster, Condition.Type.ResolvedRefs));
-                            var acc = ResourceState.fromList(patch.getStatus().getConditions());
-                            return statusFactory.clusterStatusPatch(cluster, rr.replacementFor(acc));
-                        })
-                        .map(UpdateControl::patchStatus)
-                        .orElse(UpdateControl.noUpdate());
-
-            }
-            else {
-                Stream<String> serviceMsg = refsMessage("spec.targetKafkaServiceRef references ", cluster, unresolvedServices);
-                Stream<String> ingressMsg = refsMessage("spec.ingressRefs references ", cluster, unresolvedIngresses);
-                Stream<String> filterMsg = refsMessage("spec.filterRefs references ", cluster, unresolvedFilters);
-                Stream<String> ingressProxyMessage = refsMessage("a spec.ingressRef had an inconsistent or missing proxyRef ", cluster, mismatchedIngressProxyRef);
-                uc = UpdateControl
-                        .patchStatus(statusFactory.newFalseConditionStatusPatch(cluster, Condition.Type.ResolvedRefs, Condition.REASON_TRANSITIVE_REFS_NOT_FOUND,
-                                joiningMessages(serviceMsg, ingressMsg, filterMsg, ingressProxyMessage)));
-            }
+        ResolutionResult resolutionResult = resolver.deepResolveCluster(cluster, context);
+        ResolutionResult.ClusterResolutionResult clusterResolutionResult = resolutionResult.clusterResult(cluster)
+                .orElseThrow(() -> new IllegalStateException("no resolution result for cluster " + name(cluster)));
path: kroxylicious-operator/src/main/java/io/kroxylicious/kubernetes/operator/VirtualKafkaClusterReconciler.java
user: login: robobario
body: this was fixed before merge
created_at: 2025-04-10T02:37:36Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-10T02:57:00Z
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=2035) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=2035&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=2035&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=2035&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [93.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=2035&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=2035&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=2035)


author_association: NONE