Repository: kiali/kiali
Pull request #7923 (2024-11-13 21:24:50)

Allow setting trace limit in graph node-side-panel Trace tab
For completeness, we should also allow users the ability to set the trace fetch limit in the graph's node-side-panel Traces tab.

As part of this, remove the "use graph refresh" option and just have users manually refresh the traces list, as needed. This simplifies the tab header, given the new dropdown, and I doubt anyone will miss this option. If they do, we can always bring it back.

part-of kiali7769

Test
@josunect , I overlooked this in the original PR, but I think we need the option here as well. You can look at the network requests to ensure the initial request has limit 20, and that new requests are made if the limit changes.

josunect: I'm wondering if the user would need to increase the trace limit in this window? I see this trace table as a trace preview, because there is not a lot of space to show information. Also, it is not evident if the traces only cover a small portion of what the user has requested, because this is shown in a table and not in a chart. There is also a button "Show traces" to redirect the user to the traces tab for the selected app for a broader view.

Patch:
diff --git a/business/checkers/checker.go b/business/checkers/checker.go
index 29197f830..b72ca495d 100644
--- a/business/checkers/checker.go
+++ b/business/checkers/checker.go
@@ -14,6 +14,10 @@ type GroupChecker interface {
 	Check() models.IstioValidations
 }
 
+type ObjectChecker interface {
+	Check() models.IstioValidations
+}
+
 // EmptyValidValidation returns a stub validation object which can be used by checkers
 func EmptyValidValidations(name, namespace string, objectGVK schema.GroupVersionKind, cluster string) models.IstioValidations {
 	key, emptyValidation := EmptyValidValidation(name, namespace, objectGVK, cluster)
diff --git a/business/istio_config.go b/business/istio_config.go
index 9cf1d76dd..ae426b3f1 100644
--- a/business/istio_config.go
+++ b/business/istio_config.go
@@ -686,6 +686,9 @@ func (in *IstioConfigService) DeleteIstioConfigDetail(ctx context.Context, clust
 	// We need to refresh the kube cache though at least until waiting for the object to be updated is implemented.
 	kubeCache.Refresh(namespace)
 
+	// Remove validations for the object to refresh the validation cache.
+	in.kialiCache.Validations().Remove(models.IstioValidationKey{Name: name, Namespace: namespace, ObjectGVK: resourceType, Cluster: cluster})
+
 	return nil
 }
 
@@ -777,7 +780,13 @@ func (in *IstioConfigService) UpdateIstioConfigDetail(ctx context.Context, clust
 	// We need to refresh the kube cache though at least until waiting for the object to be updated is implemented.
 	kubeCache.Refresh(namespace)
 
-	return istioConfigDetail, err
+	// Re-run validations for that object to refresh the validation cache.
+	if _, _, err := in.businessLayer.Validations.GetIstioObjectValidations(ctx, cluster, namespace, resourceType, name); err != nil {
+		// Logging the error and swallowing it since the object was updated successfully.
+		log.Errorf("Error while validating Istio object: %s", err)
+	}
+
+	return istioConfigDetail, nil
 }
 
 func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, cluster, namespace string, resourceType schema.GroupVersionKind, body []byte) (models.IstioConfigDetails, error) {
@@ -797,6 +806,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 		return istioConfigDetail, nil
 	}
 
+	var name string
 	switch resourceType.String() {
 	case kubernetes.DestinationRules.String():
 		istioConfigDetail.DestinationRule = &networking_v1.DestinationRule{}
@@ -805,6 +815,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.DestinationRule, err = userClient.Istio().NetworkingV1().DestinationRules(namespace).Create(ctx, istioConfigDetail.DestinationRule, createOpts)
+		name = istioConfigDetail.DestinationRule.Name
 	case kubernetes.EnvoyFilters.String():
 		istioConfigDetail.EnvoyFilter = &networking_v1alpha3.EnvoyFilter{}
 		err = json.Unmarshal(body, istioConfigDetail.EnvoyFilter)
@@ -812,6 +823,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.EnvoyFilter, err = userClient.Istio().NetworkingV1alpha3().EnvoyFilters(namespace).Create(ctx, istioConfigDetail.EnvoyFilter, createOpts)
+		name = istioConfigDetail.EnvoyFilter.Name
 	case kubernetes.Gateways.String():
 		istioConfigDetail.Gateway = &networking_v1.Gateway{}
 		err = json.Unmarshal(body, istioConfigDetail.Gateway)
@@ -819,6 +831,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.Gateway, err = userClient.Istio().NetworkingV1().Gateways(namespace).Create(ctx, istioConfigDetail.Gateway, createOpts)
+		name = istioConfigDetail.Gateway.Name
 	case kubernetes.K8sGateways.String():
 		istioConfigDetail.K8sGateway = &k8s_networking_v1.Gateway{}
 		err = json.Unmarshal(body, istioConfigDetail.K8sGateway)
@@ -826,6 +839,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.K8sGateway, err = userClient.GatewayAPI().GatewayV1().Gateways(namespace).Create(ctx, istioConfigDetail.K8sGateway, createOpts)
+		name = istioConfigDetail.K8sGateway.Name
 	case kubernetes.K8sHTTPRoutes.String():
 		istioConfigDetail.K8sHTTPRoute = &k8s_networking_v1.HTTPRoute{}
 		err = json.Unmarshal(body, istioConfigDetail.K8sHTTPRoute)
@@ -833,6 +847,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.K8sHTTPRoute, err = userClient.GatewayAPI().GatewayV1().HTTPRoutes(namespace).Create(ctx, istioConfigDetail.K8sHTTPRoute, createOpts)
+		name = istioConfigDetail.K8sHTTPRoute.Name
 	case kubernetes.K8sGRPCRoutes.String():
 		istioConfigDetail.K8sGRPCRoute = &k8s_networking_v1.GRPCRoute{}
 		err = json.Unmarshal(body, istioConfigDetail.K8sGRPCRoute)
@@ -840,6 +855,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.K8sGRPCRoute, err = userClient.GatewayAPI().GatewayV1().GRPCRoutes(namespace).Create(ctx, istioConfigDetail.K8sGRPCRoute, createOpts)
+		name = istioConfigDetail.K8sGRPCRoute.Name
 	case kubernetes.K8sReferenceGrants.String():
 		istioConfigDetail.K8sReferenceGrant = &k8s_networking_v1beta1.ReferenceGrant{}
 		err = json.Unmarshal(body, istioConfigDetail.K8sReferenceGrant)
@@ -847,6 +863,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.K8sReferenceGrant, err = userClient.GatewayAPI().GatewayV1beta1().ReferenceGrants(namespace).Create(ctx, istioConfigDetail.K8sReferenceGrant, createOpts)
+		name = istioConfigDetail.K8sReferenceGrant.Name
 	case kubernetes.ServiceEntries.String():
 		istioConfigDetail.ServiceEntry = &networking_v1.ServiceEntry{}
 		err = json.Unmarshal(body, istioConfigDetail.ServiceEntry)
@@ -854,6 +871,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.ServiceEntry, err = userClient.Istio().NetworkingV1().ServiceEntries(namespace).Create(ctx, istioConfigDetail.ServiceEntry, createOpts)
+		name = istioConfigDetail.ServiceEntry.Name
 	case kubernetes.Sidecars.String():
 		istioConfigDetail.Sidecar = &networking_v1.Sidecar{}
 		err = json.Unmarshal(body, istioConfigDetail.Sidecar)
@@ -861,6 +879,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.Sidecar, err = userClient.Istio().NetworkingV1().Sidecars(namespace).Create(ctx, istioConfigDetail.Sidecar, createOpts)
+		name = istioConfigDetail.Sidecar.Name
 	case kubernetes.VirtualServices.String():
 		istioConfigDetail.VirtualService = &networking_v1.VirtualService{}
 		err = json.Unmarshal(body, istioConfigDetail.VirtualService)
@@ -868,6 +887,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.VirtualService, err = userClient.Istio().NetworkingV1().VirtualServices(namespace).Create(ctx, istioConfigDetail.VirtualService, createOpts)
+		name = istioConfigDetail.VirtualService.Name
 	case kubernetes.WorkloadEntries.String():
 		istioConfigDetail.WorkloadEntry = &networking_v1.WorkloadEntry{}
 		err = json.Unmarshal(body, istioConfigDetail.WorkloadEntry)
@@ -875,6 +895,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.WorkloadEntry, err = userClient.Istio().NetworkingV1().WorkloadEntries(namespace).Create(ctx, istioConfigDetail.WorkloadEntry, createOpts)
+		name = istioConfigDetail.WorkloadEntry.Name
 	case kubernetes.WorkloadGroups.String():
 		istioConfigDetail.WorkloadGroup = &networking_v1.WorkloadGroup{}
 		err = json.Unmarshal(body, istioConfigDetail.WorkloadGroup)
@@ -882,6 +903,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.WorkloadGroup, err = userClient.Istio().NetworkingV1().WorkloadGroups(namespace).Create(ctx, istioConfigDetail.WorkloadGroup, createOpts)
+		name = istioConfigDetail.WorkloadGroup.Name
 	case kubernetes.WasmPlugins.String():
 		istioConfigDetail.WasmPlugin = &extentions_v1alpha1.WasmPlugin{}
 		err = json.Unmarshal(body, istioConfigDetail.WasmPlugin)
@@ -889,6 +911,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.WasmPlugin, err = userClient.Istio().ExtensionsV1alpha1().WasmPlugins(namespace).Create(ctx, istioConfigDetail.WasmPlugin, createOpts)
+		name = istioConfigDetail.WasmPlugin.Name
 	case kubernetes.Telemetries.String():
 		istioConfigDetail.Telemetry = &telemetry_v1.Telemetry{}
 		err = json.Unmarshal(body, istioConfigDetail.Telemetry)
@@ -896,6 +919,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.Telemetry, err = userClient.Istio().TelemetryV1().Telemetries(namespace).Create(ctx, istioConfigDetail.Telemetry, createOpts)
+		name = istioConfigDetail.Telemetry.Name
 	case kubernetes.AuthorizationPolicies.String():
 		istioConfigDetail.AuthorizationPolicy = &security_v1.AuthorizationPolicy{}
 		err = json.Unmarshal(body, istioConfigDetail.AuthorizationPolicy)
@@ -903,6 +927,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.AuthorizationPolicy, err = userClient.Istio().SecurityV1().AuthorizationPolicies(namespace).Create(ctx, istioConfigDetail.AuthorizationPolicy, createOpts)
+		name = istioConfigDetail.AuthorizationPolicy.Name
 	case kubernetes.PeerAuthentications.String():
 		istioConfigDetail.PeerAuthentication = &security_v1.PeerAuthentication{}
 		err = json.Unmarshal(body, istioConfigDetail.PeerAuthentication)
@@ -910,6 +935,7 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.PeerAuthentication, err = userClient.Istio().SecurityV1().PeerAuthentications(namespace).Create(ctx, istioConfigDetail.PeerAuthentication, createOpts)
+		name = istioConfigDetail.PeerAuthentication.Name
 	case kubernetes.RequestAuthentications.String():
 		istioConfigDetail.RequestAuthentication = &security_v1.RequestAuthentication{}
 		err = json.Unmarshal(body, istioConfigDetail.RequestAuthentication)
@@ -917,9 +943,13 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
 		istioConfigDetail.RequestAuthentication, err = userClient.Istio().SecurityV1().RequestAuthentications(namespace).Create(ctx, istioConfigDetail.RequestAuthentication, createOpts)
+		name = istioConfigDetail.RequestAuthentication.Name
 	default:
 		err = fmt.Errorf("object type not found: %v", resourceType)
 	}
+	if err != nil {
+		return istioConfigDetail, err
+	}
 
 	if in.config.ExternalServices.Istio.IstioAPIEnabled {
 		// Refreshing the istio cache in case something has changed with the registry services. Not sure if this is really needed.
@@ -930,7 +960,13 @@ func (in *IstioConfigService) CreateIstioConfigDetail(ctx context.Context, clust
 	// We need to refresh the kube cache though at least until waiting for the object to be updated is implemented.
 	kubeCache.Refresh(namespace)
 
-	return istioConfigDetail, err
+	// Re-run validations for that object to refresh the validation cache.
+	if _, _, err := in.businessLayer.Validations.GetIstioObjectValidations(ctx, cluster, namespace, resourceType, name); err != nil {
+		// Logging the error and swallowing it since the object was created successfully.
+		log.Errorf("Error while validating Istio object: %s", err)
+	}
+
+	return istioConfigDetail, nil
 }
 
 func (in *IstioConfigService) IsGatewayAPI(cluster string) bool {
diff --git a/business/istio_validations.go b/business/istio_validations.go
index 01d3d0796..b887fcd1d 100644
--- a/business/istio_validations.go
+++ b/business/istio_validations.go
@@ -15,135 +15,167 @@ import (
 	"github.com/kiali/kiali/business/checkers"
 	"github.com/kiali/kiali/business/references"
 	"github.com/kiali/kiali/config"
-	"github.com/kiali/kiali/istio"
 	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/cache"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/observability"
 	"github.com/kiali/kiali/prometheus/internalmetrics"
 )
 
-type IstioValidationsService struct {
-	businessLayer *Layer
-	discovery     istio.MeshDiscovery
-	userClients   map[string]kubernetes.ClientInterface
+func NewValidationsService(
+	istioConfig *IstioConfigService,
+	kialiCache cache.KialiCache,
+	meshService *MeshService,
+	namespaceService *NamespaceService,
+	service *SvcService,
+	userClients map[string]kubernetes.ClientInterface,
+	workloadService *WorkloadService,
+) IstioValidationsService {
+	return IstioValidationsService{
+		istioConfig: istioConfig,
+		kialiCache:  kialiCache,
+		mesh:        meshService,
+		namespace:   namespaceService,
+		service:     service,
+		userClients: userClients,
+		workload:    workloadService,
+	}
 }
 
-type ObjectChecker interface {
-	Check() models.IstioValidations
+type IstioValidationsService struct {
+	istioConfig *IstioConfigService
+	kialiCache  cache.KialiCache
+	mesh        *MeshService
+	namespace   *NamespaceService
+	service     *SvcService
+	userClients map[string]kubernetes.ClientInterface
+	workload    *WorkloadService
 }
 
 type ReferenceChecker interface {
 	References() models.IstioReferencesMap
 }
 
-// GetValidations returns an IstioValidations object with all the checks found when running
-// all the enabled checkers. If service is "" then the whole namespace is validated.
-// If service is not empty string, then all of its associated Istio objects are validated.
-func (in *IstioValidationsService) GetValidations(ctx context.Context, cluster, namespace, service, workload string) (models.IstioValidations, error) {
-	var end observability.EndFunc
-	ctx, end = observability.StartSpan(ctx, "GetValidations",
-		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
-		observability.Attribute("namespace", namespace),
-		observability.Attribute("service", service),
-		observability.Attribute("workload", workload),
-	)
-	defer end()
+func validationsForCluster(validations models.IstioValidations, cluster string) models.IstioValidations {
+	clusterValidations := models.IstioValidations{}
+	for validationKey, validation := range validations {
+		if validationKey.Cluster == cluster {
+			clusterValidations[validationKey] = validation
+		}
+	}
+	return clusterValidations
+}
 
+func (in *IstioValidationsService) GetValidations(ctx context.Context, cluster string) (models.IstioValidations, error) {
+	return validationsForCluster(in.kialiCache.Validations().Items(), cluster), nil
+}
+
+func (in *IstioValidationsService) GetValidationsForNamespace(ctx context.Context, cluster, namespace string) (models.IstioValidations, error) {
 	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
 	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
-	if namespace != "" {
-		if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
-			return nil, err
+	if _, err := in.namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
+		return nil, err
+	}
+
+	namespaceValidations := models.IstioValidations{}
+	for validationKey, validation := range in.kialiCache.Validations().Items() {
+		if validationKey.Namespace == namespace && validationKey.Cluster == cluster {
+			namespaceValidations[validationKey] = validation
 		}
-	} else {
-		return nil, fmt.Errorf("Namespace param should be set for Validations in cluster %s", cluster)
+	}
+	return namespaceValidations, nil
+}
+
+func (in *IstioValidationsService) GetValidationsForService(ctx context.Context, cluster, namespace, service string) (models.IstioValidations, error) {
+	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
+	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
+	if _, err := in.namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
+		return nil, err
 	}
 
 	// Ensure the service exists
-	if service != "" {
-		_, err := in.businessLayer.Svc.GetService(ctx, cluster, namespace, service)
+	_, err := in.service.GetService(ctx, cluster, namespace, service)
+	if err != nil {
 		if err != nil {
-			if err != nil {
-				log.Warningf("Error invoking GetService %s", err)
-			}
-			return nil, fmt.Errorf("Service [namespace: %s] [name: %s] doesn't exist for Validations.", namespace, service)
+			log.Warningf("Error invoking GetService %s", err)
 		}
+		return nil, fmt.Errorf("Service [namespace: %s] [name: %s] doesn't exist for Validations.", namespace, service)
 	}
 
-	// time this function execution so we can capture how long it takes to fully validate this namespace/service
-	timer := internalmetrics.GetValidationProcessingTimePrometheusTimer(namespace, service)
-	defer timer.ObserveDuration()
+	return models.IstioValidations(validationsForCluster(in.kialiCache.Validations().Items(), cluster)).FilterBySingleType(schema.GroupVersionKind{Group: "", Version: "", Kind: "service"}, service), nil
+}
 
-	wg := sync.WaitGroup{}
-	errChan := make(chan error, 1)
+func (in *IstioValidationsService) GetValidationsForWorkload(ctx context.Context, cluster, namespace, workload string) (models.IstioValidations, error) {
+	if namespace == "" {
+		return nil, fmt.Errorf("Namespace param should be set for Validations in cluster %s", cluster)
+	}
+	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
+	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
+	if _, err := in.namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
+		return nil, err
+	}
+
+	return models.IstioValidations(validationsForCluster(in.kialiCache.Validations().Items(), cluster)).FilterBySingleType(schema.GroupVersionKind{Group: "", Version: "", Kind: "workload"}, workload), nil
+}
+
+// CreateValidations returns an IstioValidations object with all the checks found when running
+// all the enabled checkers. If service is "" then the whole namespace is validated.
+// If service is not empty string, then all of its associated Istio objects are validated.
+func (in *IstioValidationsService) CreateValidations(ctx context.Context, cluster string) (models.IstioValidations, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(ctx, "getValidations",
+		observability.Attribute("package", "business"),
+		observability.Attribute("cluster", cluster),
+	)
+	defer end()
+
+	timer := internalmetrics.GetValidationProcessingTimePrometheusTimer("", "")
+	defer timer.ObserveDuration()
 
-	var istioConfigList models.IstioConfigList
-	var services models.ServiceList
 	var serviceAccounts map[string][]string
 	var namespaces models.Namespaces
-	var workloadsPerNamespace map[string]models.WorkloadList
-	var mtlsDetails kubernetes.MTLSDetails
-	var rbacDetails kubernetes.RBACDetails
 	var registryServices []*kubernetes.RegistryService
+	var workloadsPerNamespace map[string]models.WorkloadList
 
-	wg.Add(3) // We need to add these here to make sure we don't execute wg.Wait() before scheduler has started goroutines
-	if service != "" {
-		wg.Add(1)
-	}
-
-	// We fetch without target service as some validations will require full-namespace details
-	go in.fetchIstioConfigList(ctx, &istioConfigList, &mtlsDetails, &rbacDetails, cluster, namespace, errChan, &wg)
-
-	if workload != "" {
-		// load only requested workload
-		go in.fetchWorkload(ctx, &workloadsPerNamespace, cluster, workload, namespace, errChan, &wg)
-	} else {
-		go in.fetchAllWorkloads(ctx, &workloadsPerNamespace, cluster, &namespaces, errChan, &wg)
+	err := in.fetchAllWorkloads(ctx, &workloadsPerNamespace, cluster, &namespaces)
+	if err != nil {
+		return nil, err
 	}
-
-	go in.fetchServiceAccounts(ctx, &serviceAccounts, errChan, &wg)
-
-	if err := in.fetchNonLocalmTLSConfigs(&mtlsDetails, cluster); err != nil {
+	err = in.fetchServiceAccounts(ctx, &serviceAccounts)
+	if err != nil {
 		return nil, err
 	}
-	if service != "" {
-		go in.fetchServices(ctx, &services, cluster, namespace, errChan, &wg)
+
+	if registryStatus := in.kialiCache.GetRegistryStatus(cluster); registryStatus != nil {
+		registryServices = registryStatus.Services
 	}
 
-	criteria := RegistryCriteria{AllNamespaces: true, Cluster: cluster}
-	registryServices = in.businessLayer.RegistryStatus.GetRegistryServices(criteria)
+	validations := models.IstioValidations{}
 
-	wg.Wait()
-	close(errChan)
-	for e := range errChan {
-		if e != nil { // Check that default value wasn't returned
-			return nil, e
+	for _, namespace := range namespaces {
+		var istioConfigs models.IstioConfigList
+		var mtlsDetails kubernetes.MTLSDetails
+		var rbacDetails kubernetes.RBACDetails
+		err = in.fetchIstioConfigList(ctx, &istioConfigs, &mtlsDetails, &rbacDetails, cluster, namespace.Name)
+		if err != nil {
+			return nil, err
+		}
+		if err := in.fetchNonLocalmTLSConfigs(&mtlsDetails, cluster); err != nil {
+			return nil, err
 		}
-	}
-
-	objectCheckers := in.getAllObjectCheckers(istioConfigList, workloadsPerNamespace, mtlsDetails, rbacDetails, namespaces, registryServices, cluster, serviceAccounts)
 
-	// Get group validations for same kind istio objects
-	validations := runObjectCheckers(objectCheckers)
+		objectCheckers := in.getAllObjectCheckers(istioConfigs, workloadsPerNamespace, mtlsDetails, rbacDetails, namespaces, registryServices, cluster, serviceAccounts)
 
-	if service != "" {
-		// in.businessLayer.Svc.GetServiceList(criteria) on fetchServices performs the validations on the service
-		// No need to re-fetch deployments+pods for this
-		validations.MergeValidations(services.Validations)
-		validations = validations.FilterBySingleType(schema.GroupVersionKind{Group: "", Version: "", Kind: "service"}, service)
-	} else if workload != "" {
-		workloadList := workloadsPerNamespace[namespace]
-		validations.MergeValidations(workloadList.Validations)
-		validations = validations.FilterBySingleType(schema.GroupVersionKind{Group: "", Version: "", Kind: "workload"}, workload)
+		// Get group validations for same kind istio objects
+		validations.MergeValidations(runObjectCheckers(objectCheckers))
 	}
 
 	return validations, nil
 }
 
-func (in *IstioValidationsService) getAllObjectCheckers(istioConfigList models.IstioConfigList, workloadsPerNamespace map[string]models.WorkloadList, mtlsDetails kubernetes.MTLSDetails, rbacDetails kubernetes.RBACDetails, namespaces []models.Namespace, registryServices []*kubernetes.RegistryService, cluster string, serviceAccounts map[string][]string) []ObjectChecker {
-	return []ObjectChecker{
+func (in *IstioValidationsService) getAllObjectCheckers(istioConfigList models.IstioConfigList, workloadsPerNamespace map[string]models.WorkloadList, mtlsDetails kubernetes.MTLSDetails, rbacDetails kubernetes.RBACDetails, namespaces []models.Namespace, registryServices []*kubernetes.RegistryService, cluster string, serviceAccounts map[string][]string) []checkers.ObjectChecker {
+	return []checkers.ObjectChecker{
 		checkers.NoServiceChecker{Namespaces: namespaces, IstioConfigList: &istioConfigList, WorkloadsPerNamespace: workloadsPerNamespace, AuthorizationDetails: &rbacDetails, RegistryServices: registryServices, PolicyAllowAny: in.isPolicyAllowAny(), Cluster: cluster},
 		checkers.VirtualServiceChecker{Namespaces: namespaces, VirtualServices: istioConfigList.VirtualServices, DestinationRules: istioConfigList.DestinationRules, Cluster: cluster},
 		checkers.DestinationRulesChecker{Namespaces: namespaces, DestinationRules: istioConfigList.DestinationRules, MTLSDetails: mtlsDetails, ServiceEntries: istioConfigList.ServiceEntries, Cluster: cluster},
@@ -154,7 +186,7 @@ func (in *IstioValidationsService) getAllObjectCheckers(istioConfigList models.I
 		checkers.SidecarChecker{Sidecars: istioConfigList.Sidecars, Namespaces: namespaces, WorkloadsPerNamespace: workloadsPerNamespace, ServiceEntries: istioConfigList.ServiceEntries, RegistryServices: registryServices, Cluster: cluster},
 		checkers.RequestAuthenticationChecker{RequestAuthentications: istioConfigList.RequestAuthentications, WorkloadsPerNamespace: workloadsPerNamespace, Cluster: cluster},
 		checkers.WorkloadChecker{AuthorizationPolicies: rbacDetails.AuthorizationPolicies, WorkloadsPerNamespace: workloadsPerNamespace, Cluster: cluster},
-		checkers.K8sGatewayChecker{K8sGateways: istioConfigList.K8sGateways, Cluster: cluster, GatewayClasses: in.businessLayer.IstioConfig.GatewayAPIClasses(cluster)},
+		checkers.K8sGatewayChecker{K8sGateways: istioConfigList.K8sGateways, Cluster: cluster, GatewayClasses: in.istioConfig.GatewayAPIClasses(cluster)},
 		checkers.K8sGRPCRouteChecker{K8sGRPCRoutes: istioConfigList.K8sGRPCRoutes, K8sGateways: istioConfigList.K8sGateways, K8sReferenceGrants: istioConfigList.K8sReferenceGrants, Namespaces: namespaces, RegistryServices: registryServices, Cluster: cluster},
 		checkers.K8sHTTPRouteChecker{K8sHTTPRoutes: istioConfigList.K8sHTTPRoutes, K8sGateways: istioConfigList.K8sGateways, K8sReferenceGrants: istioConfigList.K8sReferenceGrants, Namespaces: namespaces, RegistryServices: registryServices, Cluster: cluster},
 		checkers.K8sReferenceGrantChecker{K8sReferenceGrants: istioConfigList.K8sReferenceGrants, Namespaces: namespaces, Cluster: cluster},
@@ -178,20 +210,18 @@ func (in *IstioValidationsService) GetIstioObjectValidations(ctx context.Context
 	var istioConfigList models.IstioConfigList
 	var namespaces models.Namespaces
 	var workloadsPerNamespace map[string]models.WorkloadList
-	var mtlsDetails kubernetes.MTLSDetails
-	var rbacDetails kubernetes.RBACDetails
 	var registryServices []*kubernetes.RegistryService
 	var serviceAccounts map[string][]string
 	var err error
-	var objectCheckers []ObjectChecker
+	var objectCheckers []checkers.ObjectChecker
 	var referenceChecker ReferenceChecker
+	var mtlsDetails kubernetes.MTLSDetails
+	var rbacDetails kubernetes.RBACDetails
 	istioReferences := models.IstioReferencesMap{}
 
-	istioApiEnabled := config.Get().ExternalServices.Istio.IstioAPIEnabled
-
 	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
 	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
-	if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
+	if _, err = in.namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
 		return nil, istioReferences, err
 	}
 
@@ -205,16 +235,45 @@ func (in *IstioValidationsService) GetIstioObjectValidations(ctx context.Context
 	// Get all the Istio objects from a Namespace and all gateways from every namespace
 	wg.Add(3)
 
-	go in.fetchIstioConfigList(ctx, &istioConfigList, &mtlsDetails, &rbacDetails, cluster, namespace, errChan, &wg)
-	go in.fetchAllWorkloads(ctx, &workloadsPerNamespace, cluster, &namespaces, errChan, &wg)
-	go in.fetchServiceAccounts(ctx, &serviceAccounts, errChan, &wg)
+	go func() {
+		defer wg.Done()
+
+		if len(errChan) > 0 {
+			return
+		}
+		if fetchErr := in.fetchIstioConfigList(ctx, &istioConfigList, &mtlsDetails, &rbacDetails, cluster, namespace); fetchErr != nil {
+			errChan <- fetchErr
+		}
+	}()
+
+	go func() {
+		defer wg.Done()
+
+		if len(errChan) > 0 {
+			return
+		}
+		if fetchErr := in.fetchAllWorkloads(ctx, &workloadsPerNamespace, cluster, &namespaces); fetchErr != nil {
+			errChan <- fetchErr
+		}
+	}()
+
+	go func() {
+		defer wg.Done()
+
+		if len(errChan) > 0 {
+			return
+		}
+		if fetchErr := in.fetchServiceAccounts(ctx, &serviceAccounts); fetchErr != nil {
+			errChan <- fetchErr
+		}
+	}()
+
 	if err := in.fetchNonLocalmTLSConfigs(&mtlsDetails, cluster); err != nil {
 		return nil, nil, err
 	}
 
-	if istioApiEnabled {
-		criteria := RegistryCriteria{AllNamespaces: true, Cluster: cluster}
-		registryServices = in.businessLayer.RegistryStatus.GetRegistryServices(criteria)
+	if registryStatus := in.kialiCache.GetRegistryStatus(cluster); registryStatus != nil {
+		registryServices = registryStatus.Services
 	}
 
 	wg.Wait()
@@ -223,28 +282,28 @@ func (in *IstioValidationsService) GetIstioObjectValidations(ctx context.Context
 
 	switch objectGVK.String() {
 	case kubernetes.Gateways.String():
-		objectCheckers = []ObjectChecker{
+		objectCheckers = []checkers.ObjectChecker{
 			checkers.GatewayChecker{Cluster: cluster, Gateways: istioConfigList.Gateways, WorkloadsPerNamespace: workloadsPerNamespace, IsGatewayToNamespace: in.isGatewayToNamespace()},
 		}
 		referenceChecker = references.GatewayReferences{Gateways: istioConfigList.Gateways, VirtualServices: istioConfigList.VirtualServices, WorkloadsPerNamespace: workloadsPerNamespace}
 	case kubernetes.VirtualServices.String():
 		virtualServiceChecker := checkers.VirtualServiceChecker{Cluster: cluster, Namespaces: namespaces, VirtualServices: istioConfigList.VirtualServices, DestinationRules: istioConfigList.DestinationRules}
-		objectCheckers = []ObjectChecker{noServiceChecker, virtualServiceChecker}
+		objectCheckers = []checkers.ObjectChecker{noServiceChecker, virtualServiceChecker}
 		referenceChecker = references.VirtualServiceReferences{Namespace: namespace, Namespaces: namespaces, VirtualServices: istioConfigList.VirtualServices, DestinationRules: istioConfigList.DestinationRules, AuthorizationPolicies: rbacDetails.AuthorizationPolicies}
 	case kubernetes.DestinationRules.String():
 		destinationRulesChecker := checkers.DestinationRulesChecker{Cluster: cluster, Namespaces: namespaces, DestinationRules: istioConfigList.DestinationRules, MTLSDetails: mtlsDetails, ServiceEntries: istioConfigList.ServiceEntries}
-		objectCheckers = []ObjectChecker{noServiceChecker, destinationRulesChecker}
+		objectCheckers = []checkers.ObjectChecker{noServiceChecker, destinationRulesChecker}
 		referenceChecker = references.DestinationRuleReferences{Namespace: namespace, Namespaces: namespaces, DestinationRules: istioConfigList.DestinationRules, VirtualServices: istioConfigList.VirtualServices, WorkloadsPerNamespace: workloadsPerNamespace, ServiceEntries: istioConfigList.ServiceEntries, RegistryServices: registryServices}
 	case kubernetes.ServiceEntries.String():
 		serviceEntryChecker := checkers.ServiceEntryChecker{Cluster: cluster, ServiceEntries: istioConfigList.ServiceEntries, Namespaces: namespaces, WorkloadEntries: istioConfigList.WorkloadEntries}
-		objectCheckers = []ObjectChecker{serviceEntryChecker}
+		objectCheckers = []checkers.ObjectChecker{serviceEntryChecker}
 		referenceChecker = references.ServiceEntryReferences{AuthorizationPolicies: rbacDetails.AuthorizationPolicies, Namespace: namespace, Namespaces: namespaces, DestinationRules: istioConfigList.DestinationRules, ServiceEntries: istioConfigList.ServiceEntries, Sidecars: istioConfigList.Sidecars, RegistryServices: registryServices}
 	case kubernetes.Sidecars.String():
 		sidecarsChecker := checkers.SidecarChecker{
 			Cluster: cluster, Sidecars: istioConfigList.Sidecars, Namespaces: namespaces,
 			WorkloadsPerNamespace: workloadsPerNamespace, ServiceEntries: istioConfigList.ServiceEntries, RegistryServices: registryServices,
 		}
-		objectCheckers = []ObjectChecker{sidecarsChecker}
+		objectCheckers = []checkers.ObjectChecker{sidecarsChecker}
 		referenceChecker = references.SidecarReferences{Sidecars: istioConfigList.Sidecars, Namespace: namespace, Namespaces: namespaces, ServiceEntries: istioConfigList.ServiceEntries, RegistryServices: registryServices, WorkloadsPerNamespace: workloadsPerNamespace}
 	case kubernetes.AuthorizationPolicies.String():
 		authPoliciesChecker := checkers.AuthorizationPolicyChecker{
@@ -252,12 +311,12 @@ func (in *IstioValidationsService) GetIstioObjectValidations(ctx context.Context
 			Cluster:               cluster, Namespaces: namespaces, ServiceEntries: istioConfigList.ServiceEntries, ServiceAccounts: serviceAccounts,
 			WorkloadsPerNamespace: workloadsPerNamespace, MtlsDetails: mtlsDetails, VirtualServices: istioConfigList.VirtualServices, RegistryServices: registryServices, PolicyAllowAny: in.isPolicyAllowAny(),
 		}
-		objectCheckers = []ObjectChecker{authPoliciesChecker}
+		objectCheckers = []checkers.ObjectChecker{authPoliciesChecker}
 		referenceChecker = references.AuthorizationPolicyReferences{AuthorizationPolicies: rbacDetails.AuthorizationPolicies, Namespace: namespace, Namespaces: namespaces, VirtualServices: istioConfigList.VirtualServices, ServiceEntries: istioConfigList.ServiceEntries, RegistryServices: registryServices, WorkloadsPerNamespace: workloadsPerNamespace}
 	case kubernetes.PeerAuthentications.String():
 		// Validations on PeerAuthentications
 		peerAuthnChecker := checkers.PeerAuthenticationChecker{Cluster: cluster, PeerAuthentications: mtlsDetails.PeerAuthentications, MTLSDetails: mtlsDetails, WorkloadsPerNamespace: workloadsPerNamespace}
-		objectCheckers = []ObjectChecker{peerAuthnChecker}
+		objectCheckers = []checkers.ObjectChecker{peerAuthnChecker}
 		referenceChecker = references.PeerAuthReferences{MTLSDetails: mtlsDetails, WorkloadsPerNamespace: workloadsPerNamespace}
 	case kubernetes.WorkloadEntries.String():
 		// Validation on WorkloadEntries are not yet in place
@@ -266,7 +325,7 @@ func (in *IstioValidationsService) GetIstioObjectValidations(ctx context.Context
 	case kubernetes.RequestAuthentications.String():
 		// Validation on RequestAuthentications are not yet in place
 		requestAuthnChecker := checkers.RequestAuthenticationChecker{Cluster: cluster, RequestAuthentications: istioConfigList.RequestAuthentications, WorkloadsPerNamespace: workloadsPerNamespace}
-		objectCheckers = []ObjectChecker{requestAuthnChecker}
+		objectCheckers = []checkers.ObjectChecker{requestAuthnChecker}
 	case kubernetes.EnvoyFilters.String():
 		// Validation on EnvoyFilters are not yet in place
 	case kubernetes.WasmPlugins.String():
@@ -275,20 +334,20 @@ func (in *IstioValidationsService) GetIstioObjectValidations(ctx context.Context
 		// Validation on Telemetries is not expected
 	case kubernetes.K8sGateways.String():
 		// Validations on K8sGateways
-		objectCheckers = []ObjectChecker{
-			checkers.K8sGatewayChecker{Cluster: cluster, K8sGateways: istioConfigList.K8sGateways, GatewayClasses: in.businessLayer.IstioConfig.GatewayAPIClasses(cluster)},
+		objectCheckers = []checkers.ObjectChecker{
+			checkers.K8sGatewayChecker{Cluster: cluster, K8sGateways: istioConfigList.K8sGateways, GatewayClasses: in.istioConfig.GatewayAPIClasses(cluster)},
 		}
 		referenceChecker = references.K8sGatewayReferences{K8sGateways: istioConfigList.K8sGateways, K8sHTTPRoutes: istioConfigList.K8sHTTPRoutes, K8sGRPCRoutes: istioConfigList.K8sGRPCRoutes}
 	case kubernetes.K8sGRPCRoutes.String():
 		grpcRouteChecker := checkers.K8sGRPCRouteChecker{Cluster: cluster, K8sGRPCRoutes: istioConfigList.K8sGRPCRoutes, K8sGateways: istioConfigList.K8sGateways, K8sReferenceGrants: istioConfigList.K8sReferenceGrants, Namespaces: namespaces, RegistryServices: registryServices}
-		objectCheckers = []ObjectChecker{noServiceChecker, grpcRouteChecker}
+		objectCheckers = []checkers.ObjectChecker{noServiceChecker, grpcRouteChecker}
 		referenceChecker = references.K8sGRPCRouteReferences{K8sGRPCRoutes: istioConfigList.K8sGRPCRoutes, Namespaces: namespaces, K8sReferenceGrants: istioConfigList.K8sReferenceGrants}
 	case kubernetes.K8sHTTPRoutes.String():
 		httpRouteChecker := checkers.K8sHTTPRouteChecker{Cluster: cluster, K8sHTTPRoutes: istioConfigList.K8sHTTPRoutes, K8sGateways: istioConfigList.K8sGateways, K8sReferenceGrants: istioConfigList.K8sReferenceGrants, Namespaces: namespaces, RegistryServices: registryServices}
-		objectCheckers = []ObjectChecker{noServiceChecker, httpRouteChecker}
+		objectCheckers = []checkers.ObjectChecker{noServiceChecker, httpRouteChecker}
 		referenceChecker = references.K8sHTTPRouteReferences{K8sHTTPRoutes: istioConfigList.K8sHTTPRoutes, Namespaces: namespaces, K8sReferenceGrants: istioConfigList.K8sReferenceGrants}
 	case kubernetes.K8sReferenceGrants.String():
-		objectCheckers = []ObjectChecker{
+		objectCheckers = []checkers.ObjectChecker{
 			checkers.K8sReferenceGrantChecker{Cluster: cluster, K8sReferenceGrants: istioConfigList.K8sReferenceGrants, Namespaces: namespaces},
 		}
 	case kubernetes.K8sTCPRoutes.String():
@@ -314,10 +373,15 @@ func (in *IstioValidationsService) GetIstioObjectValidations(ctx context.Context
 		return models.IstioValidations{}, istioReferences, err
 	}
 
-	return runObjectCheckers(objectCheckers).FilterByKey(objectGVK, object), istioReferences, nil
+	validations := runObjectCheckers(objectCheckers).FilterByKey(objectGVK, object)
+	for k, v := range validations {
+		in.kialiCache.Validations().Set(k, v)
+	}
+
+	return validations, istioReferences, nil
 }
 
-func runObjectCheckers(objectCheckers []ObjectChecker) models.IstioValidations {
+func runObjectCheckers(objectCheckers []checkers.ObjectChecker) models.IstioValidations {
 	objectTypeValidations := models.IstioValidations{}
 
 	// Run checks for each IstioObject type
@@ -330,7 +394,7 @@ func runObjectCheckers(objectCheckers []ObjectChecker) models.IstioValidations {
 	return objectTypeValidations
 }
 
-func runObjectChecker(objectChecker ObjectChecker) models.IstioValidations {
+func runObjectChecker(objectChecker checkers.ObjectChecker) models.IstioValidations {
 	// tracking the time it takes to execute the Check
 	promtimer := internalmetrics.GetCheckerProcessingTimePrometheusTimer(fmt.Sprintf("%T", objectChecker))
 	defer promtimer.ObserveDuration()
@@ -344,132 +408,84 @@ func runObjectReferenceChecker(referenceChecker ReferenceChecker) models.IstioRe
 	return referenceChecker.References()
 }
 
-func (in *IstioValidationsService) fetchServices(ctx context.Context, rValue *models.ServiceList, cluster, namespace string, errChan chan error, wg *sync.WaitGroup) {
-	defer wg.Done()
-	if len(errChan) == 0 {
-		var services *models.ServiceList
-		var err error
-		criteria := ServiceCriteria{
-			IncludeHealth: false,
-			Namespace:     namespace,
-			Cluster:       cluster,
-		}
-		services, err = in.businessLayer.Svc.GetServiceList(ctx, criteria)
-		if err != nil {
-			select {
-			case errChan <- err:
-			default:
-			}
-		} else {
-			*rValue = *services
-		}
-	}
-}
-
-func (in *IstioValidationsService) fetchAllWorkloads(ctx context.Context, rValue *map[string]models.WorkloadList, cluster string, namespaces *models.Namespaces, errChan chan error, wg *sync.WaitGroup) {
-	defer wg.Done()
-	if len(errChan) == 0 {
-		nss, err := in.businessLayer.Namespace.GetClusterNamespaces(ctx, cluster)
-		if err != nil {
-			errChan <- err
-			return
-
-		}
-		*namespaces = nss
-
-		allWorkloads := map[string]models.WorkloadList{}
-		for _, ns := range nss {
-			criteria := WorkloadCriteria{Cluster: cluster, Namespace: ns.Name, IncludeIstioResources: false, IncludeHealth: false}
-			workloadList, err := in.businessLayer.Workload.GetWorkloadList(ctx, criteria)
-			if err != nil {
-				select {
-				case errChan <- err:
-				default:
-				}
-			} else {
-				allWorkloads[ns.Name] = workloadList
-			}
-		}
-		*rValue = allWorkloads
+func (in *IstioValidationsService) fetchAllWorkloads(
+	ctx context.Context,
+	rValue *map[string]models.WorkloadList,
+	cluster string,
+	namespaces *models.Namespaces,
+) error {
+	nss, err := in.namespace.GetClusterNamespaces(ctx, cluster)
+	if err != nil {
+		return err
 	}
-}
+	*namespaces = nss
 
-func (in *IstioValidationsService) fetchWorkload(ctx context.Context, rValue *map[string]models.WorkloadList, cluster, workload, namespace string, errChan chan error, wg *sync.WaitGroup) {
-	defer wg.Done()
-	if len(errChan) == 0 {
-		allWorkloads := map[string]models.WorkloadList{}
-		criteria := WorkloadCriteria{Cluster: cluster, WorkloadName: workload, Namespace: namespace, IncludeIstioResources: true, IncludeHealth: false}
-		workloadList, err := in.businessLayer.Workload.GetWorkloadList(ctx, criteria)
+	allWorkloads := map[string]models.WorkloadList{}
+	for _, ns := range nss {
+		criteria := WorkloadCriteria{Cluster: cluster, Namespace: ns.Name, IncludeIstioResources: false, IncludeHealth: false}
+		workloadList, err := in.workload.GetWorkloadList(ctx, criteria)
 		if err != nil {
-			select {
-			case errChan <- err:
-			default:
-			}
-		} else {
-			allWorkloads[namespace] = workloadList
+			return err
 		}
-		*rValue = allWorkloads
+		allWorkloads[ns.Name] = workloadList
 	}
+	*rValue = allWorkloads
+	return nil
 }
 
-// fetchServiceAccounts returns list of names of the ServiceAccounts retrieved from Registry Services in a map per cluster.
-func (in *IstioValidationsService) fetchServiceAccounts(ctx context.Context, rValue *map[string][]string, errChan chan error, wg *sync.WaitGroup) {
+func (in *IstioValidationsService) fetchServiceAccounts(
+	ctx context.Context,
+	rValue *map[string][]string,
+) error {
 	serviceAccounts := map[string][]string{}
-
 	istioDomain := strings.Replace(config.Get().ExternalServices.Istio.IstioIdentityDomain, "svc.", "", 1)
-	defer wg.Done()
-	if len(errChan) == 0 {
-		for _, cluster := range in.businessLayer.Namespace.GetClusterList() {
-			nss, err := in.businessLayer.Namespace.GetClusterNamespaces(ctx, cluster)
+
+	for _, cluster := range in.namespace.GetClusterList() {
+		nss, err := in.namespace.GetClusterNamespaces(ctx, cluster)
+		if err != nil {
+			return err
+		}
+		for _, ns := range nss {
+			criteria := WorkloadCriteria{Cluster: cluster, Namespace: ns.Name, IncludeIstioResources: false, IncludeHealth: false}
+			workloadList, err := in.workload.GetWorkloadList(ctx, criteria)
 			if err != nil {
-				errChan <- err
-				return
+				return err
 			}
-			for _, ns := range nss {
-				criteria := WorkloadCriteria{Cluster: cluster, Namespace: ns.Name, IncludeIstioResources: false, IncludeHealth: false}
-				workloadList, err := in.businessLayer.Workload.GetWorkloadList(ctx, criteria)
-				if err != nil {
-					select {
-					case errChan <- err:
-					default:
+			for _, wl := range workloadList.Workloads {
+				for _, sAccountName := range wl.ServiceAccountNames {
+					saFullName := fmt.Sprintf("%s/ns/%s/sa/%s", istioDomain, ns.Name, sAccountName)
+					found := false
+					if _, ok := serviceAccounts[cluster]; !ok {
+						serviceAccounts[cluster] = []string{}
 					}
-				} else {
-					for _, wl := range workloadList.Workloads {
-						for _, sAccountName := range wl.ServiceAccountNames {
-							saFullName := fmt.Sprintf("%s/ns/%s/sa/%s", istioDomain, ns.Name, sAccountName)
-							found := false
-							if _, ok := serviceAccounts[cluster]; !ok {
-								serviceAccounts[cluster] = []string{}
-							}
-							for _, name := range serviceAccounts[cluster] {
-								if name == saFullName {
-									found = true
-									break
-								}
-							}
-							if !found {
-								serviceAccounts[cluster] = append(serviceAccounts[cluster], saFullName)
-							}
+					for _, name := range serviceAccounts[cluster] {
+						if name == saFullName {
+							found = true
+							break
 						}
 					}
+					if !found {
+						serviceAccounts[cluster] = append(serviceAccounts[cluster], saFullName)
+					}
 				}
 			}
 		}
-		*rValue = serviceAccounts
 	}
+	*rValue = serviceAccounts
+	return nil
 }
 
-func (in *IstioValidationsService) fetchIstioConfigList(ctx context.Context, rValue *models.IstioConfigList, mtlsDetails *kubernetes.MTLSDetails, rbacDetails *kubernetes.RBACDetails, cluster, namespace string, errChan chan error, wg *sync.WaitGroup) {
-	defer wg.Done()
-	if len(errChan) > 0 {
-		return
-	}
-
+func (in *IstioValidationsService) fetchIstioConfigList(
+	ctx context.Context,
+	rValue *models.IstioConfigList,
+	mtlsDetails *kubernetes.MTLSDetails,
+	rbacDetails *kubernetes.RBACDetails,
+	cluster, namespace string,
+) error {
 	// all namespaces are necessary to check Ambient mode of each namespace
-	nss, err := in.businessLayer.Namespace.GetClusterNamespaces(ctx, cluster)
+	nss, err := in.namespace.GetClusterNamespaces(ctx, cluster)
 	if err != nil {
-		errChan <- err
-		return
+		return err
 	}
 
 	criteria := IstioConfigCriteria{
@@ -487,10 +503,9 @@ func (in *IstioValidationsService) fetchIstioConfigList(ctx context.Context, rVa
 		IncludeK8sGateways:            true,
 		IncludeK8sReferenceGrants:     true,
 	}
-	istioConfigMap, err := in.businessLayer.IstioConfig.GetIstioConfigMap(ctx, meta_v1.NamespaceAll, criteria)
+	istioConfigMap, err := in.istioConfig.GetIstioConfigMap(ctx, meta_v1.NamespaceAll, criteria)
 	if err != nil {
-		errChan <- err
-		return
+		return err
 	}
 	istioConfigList := istioConfigMap[cluster]
 
@@ -537,8 +552,9 @@ func (in *IstioValidationsService) fetchIstioConfigList(ctx context.Context, rVa
 	rValue.WorkloadEntries = append(rValue.WorkloadEntries, istioConfigList.WorkloadEntries...)
 
 	in.filterPeerAuths(namespace, mtlsDetails, istioConfigList.PeerAuthentications)
-
 	in.filterAuthPolicies(namespace, rbacDetails, istioConfigList.AuthorizationPolicies)
+
+	return nil
 }
 
 func (in *IstioValidationsService) filterPeerAuths(namespace string, mtlsDetails *kubernetes.MTLSDetails, peerAuths []*security_v1.PeerAuthentication) {
@@ -565,7 +581,7 @@ func (in *IstioValidationsService) filterVSExportToNamespaces(allNamespaces map[
 	if currentNamespace == "" {
 		return kubernetes.FilterAutogeneratedVirtualServices(vs)
 	}
-	meshExportTo := in.businessLayer.Mesh.GetMeshConfig().DefaultVirtualServiceExportTo
+	meshExportTo := in.mesh.GetMeshConfig().DefaultVirtualServiceExportTo
 	var result []*networking_v1.VirtualService
 	for _, v := range vs {
 		if kubernetes.IsAutogenerated(v.Name) {
@@ -582,7 +598,7 @@ func (in *IstioValidationsService) filterDRExportToNamespaces(allNamespaces map[
 	if currentNamespace == "" {
 		return dr
 	}
-	meshExportTo := in.businessLayer.Mesh.GetMeshConfig().DefaultDestinationRuleExportTo
+	meshExportTo := in.mesh.GetMeshConfig().DefaultDestinationRuleExportTo
 	var result []*networking_v1.DestinationRule
 	for _, d := range dr {
 		if in.isExportedObjectIncluded(d.Spec.ExportTo, meshExportTo, allNamespaces, d.Namespace, currentNamespace, cluster) {
@@ -596,7 +612,7 @@ func (in *IstioValidationsService) filterSEExportToNamespaces(allNamespaces map[
 	if currentNamespace == "" {
 		return se
 	}
-	meshExportTo := in.businessLayer.Mesh.GetMeshConfig().DefaultServiceExportTo
+	meshExportTo := in.mesh.GetMeshConfig().DefaultServiceExportTo
 	var result []*networking_v1.ServiceEntry
 	for _, s := range se {
 		if in.isExportedObjectIncluded(s.Spec.ExportTo, meshExportTo, allNamespaces, s.Namespace, currentNamespace, cluster) {
@@ -608,7 +624,7 @@ func (in *IstioValidationsService) filterSEExportToNamespaces(allNamespaces map[
 
 func (in *IstioValidationsService) isExportedObjectIncluded(exportTo []string, meshExportTo []string, allNamespaces map[string]bool, objectNamespace, currentNamespace string, cluster string) bool {
 	// Ambient mode namespace does not support ExportTo, so export only to own namespace
-	if in.businessLayer.IstioConfig.IsAmbientEnabled(cluster) && isAmbient(allNamespaces, objectNamespace) {
+	if in.istioConfig.IsAmbientEnabled(cluster) && isAmbient(allNamespaces, objectNamespace) {
 		return objectNamespace == currentNamespace
 	}
 	if len(exportTo) == 0 {
@@ -630,7 +646,7 @@ func (in *IstioValidationsService) isExportedObjectIncluded(exportTo []string, m
 }
 
 func (in *IstioValidationsService) fetchNonLocalmTLSConfigs(mtlsDetails *kubernetes.MTLSDetails, cluster string) error {
-	mesh, err := in.discovery.Mesh(context.TODO())
+	mesh, err := in.mesh.discovery.Mesh(context.TODO())
 	if err != nil {
 		return err
 	}
@@ -646,7 +662,7 @@ func (in *IstioValidationsService) fetchNonLocalmTLSConfigs(mtlsDetails *kuberne
 }
 
 func (in *IstioValidationsService) isGatewayToNamespace() bool {
-	mesh, err := in.discovery.Mesh(context.TODO())
+	mesh, err := in.mesh.discovery.Mesh(context.TODO())
 	if err != nil {
 		log.Errorf("Error getting mesh config: %s", err)
 		return false
@@ -663,7 +679,7 @@ func (in *IstioValidationsService) isGatewayToNamespace() bool {
 }
 
 func (in *IstioValidationsService) isPolicyAllowAny() bool {
-	mesh, err := in.discovery.Mesh(context.TODO())
+	mesh, err := in.mesh.discovery.Mesh(context.TODO())
 	if err != nil {
 		log.Errorf("Error getting mesh config: %s", err)
 		return false
diff --git a/business/layer.go b/business/layer.go
index 7ef180974..83a2b2e66 100644
--- a/business/layer.go
+++ b/business/layer.go
@@ -123,8 +123,8 @@ func newLayer(
 	temporaryLayer.RegistryStatus = RegistryStatusService{kialiCache: cache}
 	temporaryLayer.TLS = TLSService{discovery: discovery, userClients: userClients, kialiCache: cache, businessLayer: temporaryLayer}
 	temporaryLayer.Svc = SvcService{config: *conf, kialiCache: cache, businessLayer: temporaryLayer, prom: prom, userClients: userClients}
-	temporaryLayer.Validations = IstioValidationsService{discovery: discovery, userClients: userClients, businessLayer: temporaryLayer}
 	temporaryLayer.Workload = *NewWorkloadService(userClients, prom, cache, temporaryLayer, conf, grafana)
+	temporaryLayer.Validations = NewValidationsService(&temporaryLayer.IstioConfig, cache, &temporaryLayer.Mesh, &temporaryLayer.Namespace, &temporaryLayer.Svc, userClients, &temporaryLayer.Workload)
 
 	temporaryLayer.Tracing = NewTracingService(conf, traceClient, &temporaryLayer.Svc, &temporaryLayer.Workload)
 	temporaryLayer.IstioStatus = NewIstioStatusService(conf, kialiSAClients[homeClusterName], userClients, &temporaryLayer.Tracing, &temporaryLayer.Workload, discovery)
@@ -153,3 +153,21 @@ func NewLayer(
 	kialiSAClients := cf.GetSAClients()
 	return newLayer(userClients, kialiSAClients, prom, traceClient, cache, conf, grafana, discovery, cpm), nil
 }
+
+// NewLayer creates the business layer using the passed k8sClients and prom clients.
+// Note that the client passed here should *not* be the Kiali ServiceAccount client.
+// It should be the user client based on the logged in user's token.
+// TODO: Remove this when the services in the business layer are no longer coupled
+// to the business layer and can be used separately.
+func NewLayerWithSAClients(
+	conf *config.Config,
+	cache cache.KialiCache,
+	prom prometheus.ClientInterface,
+	traceClient tracing.ClientInterface,
+	cpm ControlPlaneMonitor,
+	grafana *grafana.Service,
+	discovery *istio.Discovery,
+	saClients map[string]kubernetes.ClientInterface,
+) (*Layer, error) {
+	return newLayer(saClients, saClients, prom, traceClient, cache, conf, grafana, discovery, cpm), nil
+}
diff --git a/business/workloads.go b/business/workloads.go
index 73efe843d..fa1f423bd 100644
--- a/business/workloads.go
+++ b/business/workloads.go
@@ -475,7 +475,7 @@ func (in *WorkloadService) UpdateWorkload(ctx context.Context, cluster string, n
 	// Cache is stopped after a Create/Update/Delete operation to force a refresh.
 	// Refresh once after all the updates have gone through since Update Workload will update
 	// every single workload type of that matches name/namespace and we only want to refresh once.
-	cache, err := kialiCache.GetKubeCache(cluster)
+	cache, err := in.cache.GetKubeCache(cluster)
 	if err != nil {
 		return nil, err
 	}
diff --git a/controller/controller.go b/controller/controller.go
new file mode 100644
index 000000000..af9c82e66
--- /dev/null
+++ b/controller/controller.go
@@ -0,0 +1,87 @@
+package controller
+
+/*
+	The controllers manage long running tasks in Kiali that take too long
+	to be completed within a single request.
+
+	As an example, the validations controller handles validations across namespaces or meshes.
+	Namespace/mesh wide validations can take too long for a single request.
+*/
+
+import (
+	"context"
+	"fmt"
+
+	networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
+	"k8s.io/apimachinery/pkg/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+
+	"github.com/kiali/kiali/business"
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/cache"
+	"github.com/kiali/kiali/log"
+)
+
+func NewScheme() (*runtime.Scheme, error) {
+	scheme := runtime.NewScheme()
+	addSchemeFuncs := []func(s *runtime.Scheme) error{
+		clientgoscheme.AddToScheme,
+		networkingv1beta1.AddToScheme,
+	}
+
+	for _, addToScheme := range addSchemeFuncs {
+		if err := addToScheme(scheme); err != nil {
+			return nil, err
+		}
+	}
+
+	return scheme, nil
+}
+
+// Start creates and starts all the controllers. They'll get cancelled when the context is cancelled.
+func Start(ctx context.Context, cf kubernetes.ClientFactory, kialiCache cache.KialiCache, validationsService *business.IstioValidationsService) error {
+	// TODO: Replace with kiali logging but if this isn't set some errors are thrown.
+	ctrl.SetLogger(zap.New())
+
+	// Combine the istio scheme and the kube scheme.
+	log.Debug("Setting up Validations Contoller")
+	scheme, err := NewScheme()
+	if err != nil {
+		return fmt.Errorf("error setting up ValidationsController when creating scheme: %s", err)
+	}
+
+	// In the future this could be any cluster and not just home cluster.
+	homeClusterInfo := cf.GetSAHomeClusterClient().ClusterInfo()
+
+	// Create a new controller manager
+	mgr, err := ctrl.NewManager(homeClusterInfo.ClientConfig, ctrl.Options{
+		// Disable metrics server since Kiali has its own metrics server.
+		Metrics: metricsserver.Options{BindAddress: "0"},
+		Scheme:  scheme,
+	})
+	if err != nil {
+		return fmt.Errorf("error setting up ValidationsController when creating manager: %s", err)
+	}
+
+	var clusters []string
+	// We want one manager/reconciler for all clusters.
+	for _, client := range cf.GetSAClients() {
+		clusters = append(clusters, client.ClusterInfo().Name)
+	}
+
+	if err := NewValidationsController(ctx, clusters, kialiCache, validationsService, mgr, nil); err != nil {
+		return fmt.Errorf("error setting up ValidationsController: %s", err)
+	}
+
+	go func() {
+		if err := mgr.Start(ctx); err != nil {
+			log.Errorf("error starting Validations Controller: %s", err)
+		}
+		log.Debug("Stopped Validations Controller")
+	}()
+
+	return nil
+}
diff --git a/controller/validations.go b/controller/validations.go
new file mode 100644
index 000000000..49f25090d
--- /dev/null
+++ b/controller/validations.go
@@ -0,0 +1,140 @@
+package controller
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	ctrlsource "sigs.k8s.io/controller-runtime/pkg/source"
+
+	"github.com/kiali/kiali/business"
+	"github.com/kiali/kiali/kubernetes/cache"
+	"github.com/kiali/kiali/log"
+	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/util"
+)
+
+const defaultReconcileInterval = time.Second * 10
+
+// newValidationsController creates and starts a new controller for the validations.
+// It stops when the ctx is cancelled. It creates a single controller for all clusters.
+// A single controller is needed if cross clusters validations are performed in the future.
+func NewValidationsController(
+	ctx context.Context,
+	clusters []string,
+	kialiCache cache.KialiCache,
+	validationsService *business.IstioValidationsService,
+	mgr ctrl.Manager,
+	reconcileInterval *time.Duration,
+) error {
+	if reconcileInterval == nil {
+		reconcileInterval = util.AsPtr(defaultReconcileInterval)
+	}
+
+	reconciler := NewValidationsReconciler(clusters, kialiCache, validationsService, *reconcileInterval)
+
+	validationsController, err := controller.New("validations-controller", mgr, controller.Options{
+		Reconciler: reconciler,
+	})
+	if err != nil {
+		return fmt.Errorf("error setting up ValidationsController when creating controller: %s", err)
+	}
+
+	events := make(chan event.GenericEvent)
+	ticker := time.NewTicker(reconciler.reconcileInterval)
+	// Dummy event object because the controller needs a real object.
+	// Setting name/namespace here so that all work items are the same.
+	// That way if validations are taking longer than the timer then we
+	// won't try to re-validate until the existing work is done.
+	emptyObject := &networkingv1beta1.VirtualService{ObjectMeta: metav1.ObjectMeta{Name: "queue", Namespace: "queue"}}
+	go func() {
+		// Prime the pump
+		select {
+		case <-ctx.Done():
+			ticker.Stop()
+			return
+		default:
+			events <- event.GenericEvent{Object: emptyObject}
+		}
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case <-ticker.C:
+				events <- event.GenericEvent{Object: emptyObject}
+			}
+		}
+	}()
+
+	if err := validationsController.Watch(ctrlsource.Channel(events, &handler.EnqueueRequestForObject{})); err != nil {
+		return fmt.Errorf("error setting up ValidationsController when creating controller watch: %s", err)
+	}
+
+	return nil
+}
+
+func NewValidationsReconciler(
+	clusters []string,
+	kialiCache cache.KialiCache,
+	validationsService *business.IstioValidationsService,
+	reconcileInterval time.Duration,
+) *ValidationsReconciler {
+	return &ValidationsReconciler{
+		clusters:           clusters,
+		kialiCache:         kialiCache,
+		reconcileInterval:  reconcileInterval,
+		validationsService: validationsService,
+	}
+}
+
+// validationsReconciler fetches Istio VirtualService objects and prints their names
+type ValidationsReconciler struct {
+	clusters           []string
+	kialiCache         cache.KialiCache
+	reconcileInterval  time.Duration
+	validationsService *business.IstioValidationsService
+}
+
+// Reconcile fetches the VirtualService and prints its name
+func (r *ValidationsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	log.Debug("[ValidationsReconciler] Started reconciling ")
+	startTime := time.Now()
+	defer func() {
+		totalReconcileTime := time.Since(startTime)
+		log.Debugf("[ValidationsReconciler] Finished reconciling in %dms", totalReconcileTime.Milliseconds())
+		if totalReconcileTime > r.reconcileInterval {
+			const warningLog = "[ValidationsReconciler] Reconcile took longer than the reconcile interval of [%s]. " +
+				"If this continues, validations will be increasingly stale. Please ensure your environment is healthy. " +
+				"If the issue still persists, please open an issue at www.github.com/kiali/kiali"
+			log.Warningf(warningLog, r.reconcileInterval)
+		}
+	}()
+
+	// Check version before performing replace.
+	version := r.kialiCache.Validations().Version()
+	allClusterValidations := make(models.IstioValidations)
+	for _, cluster := range r.clusters {
+		clusterValidations, err := r.validationsService.CreateValidations(ctx, cluster)
+		if err != nil {
+			log.Errorf("[ValidationsReconciler] Error creating validations for cluster %s: %s", cluster, err)
+			return ctrl.Result{}, err
+		}
+
+		allClusterValidations = allClusterValidations.MergeValidations(clusterValidations)
+	}
+
+	if r.kialiCache.Validations().Version() != version {
+		return ctrl.Result{}, fmt.Errorf("validations have been updated since reconciling started. Requeing to revalidate")
+	}
+
+	r.kialiCache.Validations().Replace(allClusterValidations)
+
+	return ctrl.Result{}, nil
+}
diff --git a/frontend/cypress/integration/common/istio_config.ts b/frontend/cypress/integration/common/istio_config.ts
index 17d6169d8..80a61ab4a 100644
--- a/frontend/cypress/integration/common/istio_config.ts
+++ b/frontend/cypress/integration/common/istio_config.ts
@@ -604,14 +604,24 @@ Then(
 );
 
 Then('the AuthorizationPolicy should have a {string}', function (healthStatus: string) {
-  cy.get(
-    `[data-test=VirtualItem_Ns${this.targetNamespace}_AuthorizationPolicy_${this.targetAuthorizationPolicy}] span.pf-v5-c-icon`
-  ).hasCssVar('color', `--pf-v5-global--${healthStatus}-color--100`);
+  waitUntilConfigIsVisible(
+    3,
+    this.targetAuthorizationPolicy,
+    'AuthorizationPolicy',
+    this.targetNamespace,
+    healthStatus
+  );
 });
 
-function waitUntilConfigIsVisible(attempt: number, crdInstanceName: string, crdName: string, namespace: string): void {
+function waitUntilConfigIsVisible(
+  attempt: number,
+  crdInstanceName: string,
+  crdName: string,
+  namespace: string,
+  healthStatus: string
+): void {
   if (attempt === 0) {
-    return;
+    throw new Error(`Condition not met after retries`);
   }
   cy.request({ method: 'GET', url: `${Cypress.config('baseUrl')}/api/istio/config?refresh=0` });
   cy.get('[data-test="refresh-button"]').click();
@@ -620,16 +630,26 @@ function waitUntilConfigIsVisible(attempt: number, crdInstanceName: string, crdN
   cy.get('tr')
     .each($row => {
       const dataTestAttr = $row[0].attributes.getNamedItem('data-test');
+      const hasNA = $row[0].innerText.includes('N/A');
       if (dataTestAttr !== null) {
-        if (dataTestAttr.value === `VirtualItem_Ns${namespace}_${crdName}_${crdInstanceName}`) {
-          found = true;
+        if (dataTestAttr.value === `VirtualItem_Ns${namespace}_${crdName}_${crdInstanceName}` && !hasNA) {
+          // Check if the health status icon is correct
+          cy.get(`[data-test=VirtualItem_Ns${namespace}_${crdName}_${crdInstanceName}] span.pf-v5-c-icon`)
+            .should('be.visible')
+            .then(icon => {
+              const colorVar = `--pf-v5-global--${healthStatus}-color--100`;
+              const color = getComputedStyle(icon[0]).getPropertyValue(colorVar);
+              if (color) {
+                found = true;
+              }
+            });
         }
       }
     })
     .then(() => {
       if (!found) {
-        cy.wait(40000);
-        waitUntilConfigIsVisible(attempt - 1, crdInstanceName, crdName, namespace);
+        cy.wait(10000);
+        waitUntilConfigIsVisible(attempt - 1, crdInstanceName, crdName, namespace, healthStatus);
       }
     });
 }
@@ -637,10 +657,7 @@ function waitUntilConfigIsVisible(attempt: number, crdInstanceName: string, crdN
 Then(
   'the {string} {string} of the {string} namespace should have a {string}',
   (crdInstanceName: string, crdName: string, namespace: string, healthStatus: string) => {
-    waitUntilConfigIsVisible(3, crdInstanceName, crdName, namespace);
-    cy.get(`[data-test=VirtualItem_Ns${namespace}_${crdName}_${crdInstanceName}] span.pf-v5-c-icon`)
-      .should('be.visible')
-      .hasCssVar('color', `--pf-v5-global--${healthStatus}-color--100`);
+    waitUntilConfigIsVisible(3, crdInstanceName, crdName, namespace, healthStatus);
   }
 );
 
diff --git a/frontend/cypress/integration/common/navigation.ts b/frontend/cypress/integration/common/navigation.ts
index 667f3a965..ff8e464dd 100644
--- a/frontend/cypress/integration/common/navigation.ts
+++ b/frontend/cypress/integration/common/navigation.ts
@@ -32,6 +32,16 @@ Given('user is at the {string} page for the {string} namespace', (page: string,
   cy.visit({ url: `${Cypress.config('baseUrl')}/console/${page}?refresh=0&namespaces=${namespace}` });
 });
 
+Given('autorefresh is enabled', () => {
+  // Forcing "Pause" to not cause unhandled promises from the browser when cypress is testing
+  cy.url().then(currentURL => {
+    const url = new URL(currentURL);
+    const tenSecondsInMiliSeconds = '10000';
+    url.searchParams.set('refresh', tenSecondsInMiliSeconds);
+    cy.visit(url.toString());
+  });
+});
+
 Given(
   'user is at the details page for the {string} {string} located in the {string} cluster',
   (detail: detailType, namespacedNamed: string, cluster: string) => {
diff --git a/frontend/cypress/integration/featureFiles/graph_display-pf.feature b/frontend/cypress/integration/featureFiles/graph_display-pf.feature
index 8bee5be6b..6e3b15d0d 100644
--- a/frontend/cypress/integration/featureFiles/graph_display-pf.feature
+++ b/frontend/cypress/integration/featureFiles/graph_display-pf.feature
@@ -256,6 +256,7 @@ Feature: Kiali Graph page - Display menu
     Given there are Istio objects in the "bookinfo" namespace for "east" cluster
     And there are Istio objects in the "bookinfo" namespace for "west" cluster
     When user graphs "bookinfo" namespaces
+    And autorefresh is enabled
     Then the Istio objects for the "bookinfo" namespace for both clusters should be grouped together in the panel
 
   @ambient
diff --git a/frontend/src/components/Metrics/TraceLimit.tsx b/frontend/src/components/Metrics/TraceLimit.tsx
index eef3eb429..b0505dd78 100644
--- a/frontend/src/components/Metrics/TraceLimit.tsx
+++ b/frontend/src/components/Metrics/TraceLimit.tsx
@@ -5,8 +5,6 @@ import { itemStyleWithoutInfo } from 'styles/DropdownStyles';
 import { ToolbarDropdown } from 'components/Dropdown/ToolbarDropdown';
 import { infoStyle } from 'styles/InfoStyle';
 
-export const TRACE_LIMIT_DEFAULT = 100;
-
 type TraceLimitProps = {
   asRadio?: boolean;
   initialLimit?: number;
@@ -15,6 +13,9 @@ type TraceLimitProps = {
   titleClassName?: string;
 };
 
+export const TRACE_LIMIT_DEFAULT = 100;
+export type TraceLimitOption = 20 | 100 | 500 | 1000;
+
 export const TraceLimit: React.FC<TraceLimitProps> = (props: TraceLimitProps) => {
   const initialLimit = props.initialLimit ?? TRACE_LIMIT_DEFAULT;
   const [limit, setLimit] = React.useState<number>(initialLimit);
diff --git a/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx b/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx
index b0d8deac1..20bc7b1a1 100644
--- a/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx
@@ -1,6 +1,6 @@
 import * as React from 'react';
 import { connect } from 'react-redux';
-import { SimpleList, SimpleListItem, Button, Checkbox, Divider, ButtonVariant } from '@patternfly/react-core';
+import { SimpleList, SimpleListItem, Button, Divider, ButtonVariant } from '@patternfly/react-core';
 import { kialiStyle } from 'styles/StyleUtils';
 
 import { KialiAppState } from 'store/Store';
@@ -20,6 +20,7 @@ import { isParentKiosk, kioskContextMenuAction } from '../../components/Kiosk/Ki
 import { KialiDispatch } from '../../types/Redux';
 import { isMultiCluster } from '../../config';
 import { KialiIcon } from 'config/KialiIcon';
+import { TraceLimit, TraceLimitOption } from 'components/Metrics/TraceLimit';
 
 type ReduxStateProps = {
   kiosk: string;
@@ -38,11 +39,8 @@ type Props = ReduxStateProps &
 
 type State = {
   traces: JaegerTrace[];
-  useGraphRefresh: boolean;
 };
 
-const tracesLimit = 15;
-
 const refreshDivStyle = kialiStyle({
   display: 'flex',
   alignItems: 'center',
@@ -51,15 +49,6 @@ const refreshDivStyle = kialiStyle({
   marginBottom: '0.5rem'
 });
 
-const checkboxStyle = kialiStyle({
-  $nest: {
-    '& > label': {
-      fontSize: 'var(--graph-side-panel--font-size)',
-      paddingTop: '0.25rem'
-    }
-  }
-});
-
 const refreshButtonStyle = kialiStyle({
   padding: '0.125rem 0.5rem',
   marginLeft: 'auto'
@@ -71,6 +60,7 @@ const dividerStyle = kialiStyle({
 
 class SummaryPanelNodeTracesComponent extends React.Component<Props, State> {
   private promises = new PromisesRegistry();
+  private currentLimit: TraceLimitOption = 20;
 
   static getDerivedStateFromProps(props: Props, state: State): State {
     // Update the selected trace within list because it may have more up-to-date data after being selected hence fetched again
@@ -87,7 +77,7 @@ class SummaryPanelNodeTracesComponent extends React.Component<Props, State> {
 
   constructor(props: Props) {
     super(props);
-    this.state = { traces: [], useGraphRefresh: false };
+    this.state = { traces: [] };
   }
 
   componentDidMount(): void {
@@ -96,7 +86,6 @@ class SummaryPanelNodeTracesComponent extends React.Component<Props, State> {
 
   componentDidUpdate(prevProps: Props): void {
     if (
-      (this.state.useGraphRefresh && prevProps.queryTime !== this.props.queryTime) ||
       prevProps.nodeData.namespace !== this.props.nodeData.namespace ||
       prevProps.nodeData.app !== this.props.nodeData.app ||
       prevProps.nodeData.workload !== this.props.nodeData.workload ||
@@ -110,53 +99,6 @@ class SummaryPanelNodeTracesComponent extends React.Component<Props, State> {
     this.promises.cancelAll();
   }
 
-  private loadTraces(): void {
-    // Convert seconds to microseconds
-    const params: TracingQuery = {
-      startMicros: this.props.queryTime * 1000000,
-      limit: tracesLimit
-    };
-
-    const d = this.props.nodeData;
-
-    const promise = d.workload
-      ? API.getWorkloadTraces(d.namespace, d.workload, params, d.cluster)
-      : d.service
-      ? API.getServiceTraces(d.namespace, d.service, params, d.cluster)
-      : API.getAppTraces(d.namespace, d.app!, params, d.cluster);
-
-    this.promises.cancelAll();
-
-    this.promises
-      .register('traces', promise)
-      .then(response => {
-        const traces = response.data.data
-          ? (response.data.data
-              .map(trace => transformTraceData(trace, this.props.nodeData.cluster))
-              .filter(trace => trace !== null) as JaegerTrace[])
-          : [];
-
-        if (this.props.selectedTrace && !traces.some(t => t.traceID === this.props.selectedTrace!.traceID)) {
-          // Put selected trace back in list
-          traces.push(this.props.selectedTrace);
-        }
-
-        this.setState({ traces: traces });
-      })
-      .catch(error => {
-        AlertUtils.addError('Could not fetch traces.', error);
-      });
-  }
-
-  private onClickTrace(trace: JaegerTrace): void {
-    if (this.props.selectedTrace?.traceID === trace.traceID) {
-      // Deselect
-      this.props.setTraceId(this.props.nodeData.cluster, undefined);
-    } else {
-      this.props.setTraceId(this.props.nodeData.cluster, trace.traceID);
-    }
-  }
-
   render(): React.ReactNode {
     const d = this.props.nodeData;
 
@@ -169,17 +111,12 @@ class SummaryPanelNodeTracesComponent extends React.Component<Props, State> {
     return (
       <div style={{ marginBottom: '0.5rem' }}>
         <div className={refreshDivStyle}>
-          <Checkbox
-            id="use-graph-refresh"
-            label="Use graph refresh"
-            className={checkboxStyle}
-            isChecked={this.state.useGraphRefresh}
-            onChange={(_event, checked) => this.setState({ useGraphRefresh: checked })}
-          />
+          <span style={{ marginLeft: '0.5rem' }}>
+            <TraceLimit initialLimit={this.currentLimit} onLimitChange={this.onLimitChange} />
+          </span>
 
           <Button
             id="manual-refresh"
-            isDisabled={this.state.useGraphRefresh}
             onClick={() => this.loadTraces()}
             aria-label="Refresh"
             variant={ButtonVariant.secondary}
@@ -223,6 +160,58 @@ class SummaryPanelNodeTracesComponent extends React.Component<Props, State> {
       </div>
     );
   }
+
+  private onLimitChange = (limit: number): void => {
+    this.currentLimit = limit as TraceLimitOption;
+    this.loadTraces();
+  };
+
+  private loadTraces(): void {
+    // Convert seconds to microseconds
+    const params: TracingQuery = {
+      startMicros: this.props.queryTime * 1000000,
+      limit: this.currentLimit
+    };
+
+    const d = this.props.nodeData;
+
+    const promise = d.workload
+      ? API.getWorkloadTraces(d.namespace, d.workload, params, d.cluster)
+      : d.service
+      ? API.getServiceTraces(d.namespace, d.service, params, d.cluster)
+      : API.getAppTraces(d.namespace, d.app!, params, d.cluster);
+
+    this.promises.cancelAll();
+
+    this.promises
+      .register('traces', promise)
+      .then(response => {
+        const traces = response.data.data
+          ? (response.data.data
+              .map(trace => transformTraceData(trace, this.props.nodeData.cluster))
+              .filter(trace => trace !== null) as JaegerTrace[])
+          : [];
+
+        if (this.props.selectedTrace && !traces.some(t => t.traceID === this.props.selectedTrace!.traceID)) {
+          // Put selected trace back in list
+          traces.push(this.props.selectedTrace);
+        }
+
+        this.setState({ traces: traces });
+      })
+      .catch(error => {
+        AlertUtils.addError('Could not fetch traces.', error);
+      });
+  }
+
+  private onClickTrace(trace: JaegerTrace): void {
+    if (this.props.selectedTrace?.traceID === trace.traceID) {
+      // Deselect
+      this.props.setTraceId(this.props.nodeData.cluster, undefined);
+    } else {
+      this.props.setTraceId(this.props.nodeData.cluster, trace.traceID);
+    }
+  }
 }
 
 const mapStateToProps = (state: KialiAppState): ReduxStateProps => ({
diff --git a/go.mod b/go.mod
index 8c08c5264..fdad18e0a 100644
--- a/go.mod
+++ b/go.mod
@@ -11,19 +11,21 @@ require (
 	github.com/gorilla/mux v1.8.1
 	github.com/mitchellh/mapstructure v1.4.3
 	github.com/nitishm/engarde v0.1.1
+	github.com/onsi/ginkgo/v2 v2.19.0
+	github.com/onsi/gomega v1.33.1
 	github.com/openshift/api v0.0.0-20240109042830-44756aa36879
 	github.com/openshift/client-go v0.0.0-20240109161853-2425b4b6d3b3
-	github.com/prometheus/client_golang v1.17.0
-	github.com/prometheus/common v0.45.0
+	github.com/prometheus/client_golang v1.19.1
+	github.com/prometheus/common v0.55.0
 	github.com/rs/zerolog v1.20.0
 	github.com/stretchr/testify v1.9.0
 	go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.47.0
-	go.opentelemetry.io/otel v1.22.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0
+	go.opentelemetry.io/otel v1.28.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0
-	go.opentelemetry.io/otel/sdk v1.22.0
-	go.opentelemetry.io/otel/trace v1.22.0
+	go.opentelemetry.io/otel/sdk v1.28.0
+	go.opentelemetry.io/otel/trace v1.28.0
 	go.uber.org/automaxprocs v1.5.3
 	golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f
 	golang.org/x/net v0.28.0
@@ -38,32 +40,38 @@ require (
 	k8s.io/api v0.31.1
 	k8s.io/apimachinery v0.31.1
 	k8s.io/client-go v0.31.1
+	sigs.k8s.io/controller-runtime v0.19.1
 	sigs.k8s.io/gateway-api v1.2.0
 )
 
 require (
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/emicklei/go-restful/v3 v3.12.0 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-logr/zapr v1.3.0 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/websocket v1.5.1 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
 	github.com/imdario/mergo v0.3.16 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
-	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
 	github.com/moby/spdystream v0.4.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -71,24 +79,29 @@ require (
 	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/client_model v0.5.0 // indirect
-	github.com/prometheus/procfs v0.12.0 // indirect
+	github.com/prometheus/client_model v0.6.1 // indirect
+	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/vjeantet/grok v1.0.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
-	go.opentelemetry.io/otel/metric v1.22.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.0.0 // indirect
+	go.opentelemetry.io/otel/metric v1.28.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.3.1 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.26.0 // indirect
 	golang.org/x/crypto v0.26.0 // indirect
 	golang.org/x/sys v0.24.0 // indirect
 	golang.org/x/term v0.23.0 // indirect
 	golang.org/x/text v0.17.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
+	golang.org/x/tools v0.24.0 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	k8s.io/apiextensions-apiserver v0.31.1 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108 // indirect
 	k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
diff --git a/go.sum b/go.sum
index 280757891..8ebc200cb 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
-github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@@ -15,8 +15,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
 github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
+github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
+github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/go-jose/go-jose v2.6.3+incompatible h1:eU70erXEHN0wZl7K7kBTRLel/hu4P09qqopkDaXiXso=
@@ -26,6 +32,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
 github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
 github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
@@ -36,8 +44,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
-github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
@@ -56,8 +64,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
 github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
 github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
 github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
 github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -74,8 +82,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
 github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@@ -96,8 +102,8 @@ github.com/nitishm/engarde v0.1.1 h1:+hT7EGP64L1wKnIv64xzu94oSR3n+SFqcsDHiGDcTVQ
 github.com/nitishm/engarde v0.1.1/go.mod h1:nlzhv5c2J4WL9e/Y8sebkZ2PJBY790US8d+Z/vS/qqY=
 github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
 github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
-github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
-github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
 github.com/openshift/api v0.0.0-20240109042830-44756aa36879 h1:9JfgDtOi0be+p2HsXwIahS8rXlt4MQRj6Vlpmx1DDg8=
 github.com/openshift/api v0.0.0-20240109042830-44756aa36879/go.mod h1:qNtV0315F+f8ld52TLtPvrfivZpdimOzTi3kn9IVbtU=
 github.com/openshift/client-go v0.0.0-20240109161853-2425b4b6d3b3 h1:XQv/z/1tedDGPeplmowftfUUrR3BtglN6Hv5kLzy1CU=
@@ -110,14 +116,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
-github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
-github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
-github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
-github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
-github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
-github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
@@ -139,26 +145,30 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.47.0 h1:yPWywmjyhn5C64Z7OLdIfjnbwOQF/Xz89HNqSVquC2E=
 go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.47.0/go.mod h1:jk2INQzOTr9e27FwMs2JVXXttZc/3bucJX/7l3YVfbw=
-go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
-go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 h1:H2JFgRcGiyHg7H7bwcwaQJYrNFqCqrbTQ8K4p1OvDu8=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0/go.mod h1:WfCWp1bGoYK8MeULtI15MmQVczfR+bFkk0DF3h06QmQ=
+go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
+go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
-go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
-go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
-go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
-go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
-go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
-go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
-go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
-go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
+go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
+go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
+go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
+go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
+go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
+go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
+go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
 go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
 go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
+go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -205,6 +215,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
+gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
 google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
 google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
@@ -233,6 +245,8 @@ istio.io/client-go v1.22.0 h1:TQ+Y7hqZVQHvaJXF99Q1jBqnVG7gYAHR9IvCK2nlwfE=
 istio.io/client-go v1.22.0/go.mod h1:1lAPr0DOVBbnRQqLAQKxWbEaxFk6b1CJTm+ypnP7sMo=
 k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
 k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
+k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=
+k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ=
 k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
 k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
 k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
@@ -243,6 +257,8 @@ k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108 h1:Q8Z7VlGhcJgBHJHYugJ/K/
 k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk=
+sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
 sigs.k8s.io/gateway-api v1.2.0 h1:LrToiFwtqKTKZcZtoQPTuo3FxhrrhTgzQG0Te+YGSo8=
 sigs.k8s.io/gateway-api v1.2.0/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
diff --git a/hack/istio/tempo/helm.yaml b/hack/istio/tempo/helm.yaml
new file mode 100644
index 000000000..579015b80
--- /dev/null
+++ b/hack/istio/tempo/helm.yaml
@@ -0,0 +1,33 @@
+storage:
+  trace:
+    backend: s3
+    s3:
+      access_key: 'minio'
+      secret_key: 'minio123'
+      bucket: 'tempo-data'
+      endpoint: 'minio:9000'
+      insecure: true
+#MinIO storage configuration
+minio:
+  enabled: true
+  mode: standalone
+  rootUser: minio
+  rootPassword: minio123
+  buckets:
+  # Default Tempo storage bucket
+  - name: tempo-data
+    policy: none
+    purge: false
+traces:
+  otlp:
+    grpc:
+      enabled: true
+    http:
+      enabled: true
+  zipkin:
+    enabled: true
+  jaeger:
+    thriftHttp:
+      enabled: false
+  opencensus:
+    enabled: false
diff --git a/hack/istio/tempo/install-tempo-env.sh b/hack/istio/tempo/install-tempo-env.sh
index 4c5b128a7..3c5adb380 100755
--- a/hack/istio/tempo/install-tempo-env.sh
+++ b/hack/istio/tempo/install-tempo-env.sh
@@ -15,9 +15,11 @@ DELETE_TEMPO="false"
 INSTALL_BOOKINFO="true"
 INSTALL_ISTIO="true"
 INSTALL_KIALI="false"
+METHOD="operator"
 ONLY_TEMPO="false"
 SECURE_DISTRIBUTOR="false"
 TEMPO_NS="tempo"
+TEMPO_PORT="3200"
 
 # process command line args
 while [[ $# -gt 0 ]]; do
@@ -47,6 +49,10 @@ while [[ $# -gt 0 ]]; do
       INSTALL_KIALI="$2"
       shift;shift
       ;;
+    -im|--install-method)
+      METHOD="$2"
+      shift;shift
+      ;;
     -ot|--only-tempo)
       ONLY_TEMPO="$2"
       shift;shift
@@ -74,6 +80,9 @@ Valid command line arguments:
        If istio should be installed. true by default.
   -ik|--install-kiali:
        If Kiali should be installed. false by default.
+  -im|--install-method:
+       If using "operator" or "helm". "operator" by default. Helm installation has other properties and uses a more updated version of Tempo.
+       The recommendation resources for using helm in minikube:
   -ot|--only-tempo:
        Install only tempo. false by default.
   -sd|--secure-distributor:
@@ -103,6 +112,11 @@ else
   exit 1
 fi
 
+if [ "${METHOD}" != "operator" ] && [ "${METHOD}" != "helm" ]; then
+  echo "method should be 'operator' or 'helm'"
+  exit 1
+fi
+
 if ${CLIENT_EXE} api-versions | grep --quiet "route.openshift.io"; then
   IS_OPENSHIFT="true"
   echo "You are connecting to an OpenShift cluster"
@@ -141,10 +155,14 @@ install_tempo() {
     --from-literal=access_key_id="minio" \
     --from-literal=access_key_secret="minio123"
 
-  if [ "${SECURE_DISTRIBUTOR}" == "true" ]; then
-    # Create ca and cert for tls for the distributor
-    echo -e "Creating ca and cert for tls for the distributor \n"
-    subj="
+  if [ "${METHOD}" == "operator" ]; then
+
+    echo -e "Installing Tempo with the operator \n"
+
+    if [ "${SECURE_DISTRIBUTOR}" == "true" ]; then
+      # Create ca and cert for tls for the distributor
+      echo -e "Creating ca and cert for tls for the distributor \n"
+      subj="
 C=ES
 ST=ST
 O=AR
@@ -156,9 +174,9 @@ emailAddress=not@mail
     openssl req -x509 -sha256 -nodes -newkey rsa:2048 -subj "$(echo -n "$subj" | tr "\n" "/")" -keyout /tmp/tls.key -out /tmp/service-ca.crt
     ${CLIENT_EXE} -n ${TEMPO_NS} create configmap tempo-ca --from-file=/tmp/service-ca.crt
     ${CLIENT_EXE} create secret tls tempo-cert -n ${TEMPO_NS} --key="/tmp/tls.key" --cert="/tmp/service-ca.crt"
-  # Install TempoStack CR
-  echo -e "Installing tempo with tls enabled \n"
-  ${CLIENT_EXE} apply -n ${TEMPO_NS} -f - <<EOF
+    # Install TempoStack CR
+    echo -e "Installing tempo with tls enabled \n"
+    ${CLIENT_EXE} apply -n ${TEMPO_NS} -f - <<EOF
 apiVersion: tempo.grafana.com/v1alpha1
 kind: TempoStack
 metadata:
@@ -183,10 +201,10 @@ spec:
       jaegerQuery:
         enabled: false
 EOF
-  else
-    # Install TempoStack CR
-    echo -e "Installing tempo \n"
-    ${CLIENT_EXE} apply -n ${TEMPO_NS} -f - <<EOF
+    else
+      # Install TempoStack CR
+      echo -e "Installing tempo \n"
+      ${CLIENT_EXE} apply -n ${TEMPO_NS} -f - <<EOF
 apiVersion: tempo.grafana.com/v1alpha1
 kind: TempoStack
 metadata:
@@ -207,6 +225,15 @@ spec:
       jaegerQuery:
         enabled: false
 EOF
+    fi
+
+  else
+    echo -e "Installing Tempo with Helm Charts \n"
+    TEMPO_PORT="3100"
+    helm repo add grafana https://grafana.github.io/helm-charts
+    helm repo update
+    helm install tempo-cr grafana/tempo-distributed -n tempo -f ${SCRIPT_DIR}/helm.yaml
+
   fi
 }
 
@@ -263,7 +290,7 @@ else
     echo -e "Installation finished. \n"
     if [ "${IS_OPENSHIFT}" != "true" ]; then
       echo "If you want to access Tempo from outside the cluster on your local machine, You can port forward the services with:
-  ./run-kiali.sh -pg 13000:3000 -pp 19090:9090 -pt 3200:3200 -app 8080 -es false -iu http://127.0.0.1:15014 -tr tempo-cr-query-frontend -ts tempo-cr-query-frontend -tn tempo
+  ./run-kiali.sh -pg 13000:3000 -pp 19090:9090 -pt 3200:${TEMPO_PORT} -app 8080 -es false -iu http://127.0.0.1:15014 -tr tempo-cr-query-frontend -ts tempo-cr-query-frontend -tn tempo
 
   To configure Kiali to use this, set the external_services.tracing section with the following settings:
   tracing:
diff --git a/handlers/istio_config.go b/handlers/istio_config.go
index 363d5c7d8..8d509b53e 100644
--- a/handlers/istio_config.go
+++ b/handlers/istio_config.go
@@ -75,29 +75,13 @@ func IstioConfigList(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if includeValidations {
-		// We don't filter by service and workload when calling validations, because certain validations require fetching all types to get the correct errors
-		if namespace == "" {
-			// when namespace is empty, validations should be done per namespaces to apply exportTo configs
-			loadedNamespaces, _ := business.Namespace.GetClusterNamespaces(r.Context(), cluster)
-			istioConfig.IstioValidations = models.IstioValidations{}
-			for _, ns := range loadedNamespaces {
-				nsValidations, nsErr := business.Validations.GetValidations(r.Context(), cluster, ns.Name, "", "")
-				if nsErr != nil {
-					handleErrorResponse(w, nsErr)
-					return
-				}
-				istioConfig.IstioValidations.MergeValidations(nsValidations)
-			}
-		} else {
-			// when namespace is provided, do validations for that namespace only
-			// this option is not called from Kiali UI
-			// @TODO consider exportTo namespaces
-			istioConfig.IstioValidations, err = business.Validations.GetValidations(r.Context(), cluster, namespace, "", "")
-			if err != nil {
-				handleErrorResponse(w, err)
-				return
-			}
+		istioConfig.IstioValidations, err = business.Validations.GetValidations(r.Context(), cluster)
+		if err != nil {
+			RespondWithError(w, http.StatusInternalServerError, "Error while getting validations: "+err.Error())
+			return
 		}
+
+		// We don't filter by objects when calling validations, because certain validations require fetching all types to get the correct errors
 		if len(parsedTypes) > 0 {
 			istioConfig.IstioValidations = istioConfig.IstioValidations.FilterByTypes(parsedTypes)
 		}
diff --git a/handlers/namespaces.go b/handlers/namespaces.go
index be3e919af..e308971e3 100644
--- a/handlers/namespaces.go
+++ b/handlers/namespaces.go
@@ -77,12 +77,12 @@ func NamespaceValidationSummary(discovery *istio.Discovery) http.HandlerFunc {
 		// If cluster is not set, is because we need a unified validations view (E.g. in the Summary graph)
 		clusters, _ := discovery.Clusters()
 		if len(clusters) == 1 {
-			istioConfigValidationResults, errValidations = business.Validations.GetValidations(r.Context(), cluster, namespace, "", "")
+			istioConfigValidationResults, errValidations = business.Validations.GetValidationsForNamespace(r.Context(), cluster, namespace)
 		} else {
 			for _, cl := range clusters {
 				_, errNs := business.Namespace.GetClusterNamespace(r.Context(), namespace, cl.Name)
 				if errNs == nil {
-					clusterIstioConfigValidationResults, _ := business.Validations.GetValidations(r.Context(), cl.Name, namespace, "", "")
+					clusterIstioConfigValidationResults, _ := business.Validations.GetValidationsForNamespace(r.Context(), cl.Name, namespace)
 					istioConfigValidationResults = istioConfigValidationResults.MergeValidations(clusterIstioConfigValidationResults)
 				}
 			}
@@ -126,7 +126,7 @@ func ConfigValidationSummary(w http.ResponseWriter, r *http.Request) {
 
 	validationSummaries := []models.IstioValidationSummary{}
 	for _, ns := range nss {
-		istioConfigValidationResults, errValidations := business.Validations.GetValidations(r.Context(), cluster, ns, "", "")
+		istioConfigValidationResults, errValidations := business.Validations.GetValidationsForNamespace(r.Context(), cluster, ns)
 		if errValidations != nil {
 			log.Error(errValidations)
 			RespondWithError(w, http.StatusInternalServerError, errValidations.Error())
diff --git a/handlers/services.go b/handlers/services.go
index 55209af18..0b777a253 100644
--- a/handlers/services.go
+++ b/handlers/services.go
@@ -159,7 +159,7 @@ func ServiceDetails(w http.ResponseWriter, r *http.Request) {
 		wg.Add(1)
 		go func() {
 			defer wg.Done()
-			istioConfigValidations, errValidations = business.Validations.GetValidations(r.Context(), cluster, namespace, service, "")
+			istioConfigValidations, errValidations = business.Validations.GetValidationsForService(r.Context(), cluster, namespace, service)
 		}()
 	}
 
@@ -227,7 +227,7 @@ func ServiceUpdate(w http.ResponseWriter, r *http.Request) {
 		wg.Add(1)
 		go func() {
 			defer wg.Done()
-			istioConfigValidations, errValidations = business.Validations.GetValidations(r.Context(), cluster, namespace, service, "")
+			istioConfigValidations, errValidations = business.Validations.GetValidationsForService(r.Context(), cluster, namespace, service)
 		}()
 	}
 
diff --git a/handlers/workloads.go b/handlers/workloads.go
index ac3605d78..15bbfa8a7 100644
--- a/handlers/workloads.go
+++ b/handlers/workloads.go
@@ -149,7 +149,7 @@ func WorkloadDetails(w http.ResponseWriter, r *http.Request) {
 		wg.Add(1)
 		go func() {
 			defer wg.Done()
-			istioConfigValidations, errValidations = business.Validations.GetValidations(r.Context(), criteria.Cluster, criteria.Namespace, "", criteria.WorkloadName)
+			istioConfigValidations, errValidations = business.Validations.GetValidationsForWorkload(r.Context(), criteria.Cluster, criteria.Namespace, criteria.WorkloadName)
 		}()
 	}
 
@@ -218,7 +218,7 @@ func WorkloadUpdate(w http.ResponseWriter, r *http.Request) {
 		wg.Add(1)
 		go func() {
 			defer wg.Done()
-			istioConfigValidations, errValidations = business.Validations.GetValidations(r.Context(), cluster, namespace, "", workload)
+			istioConfigValidations, errValidations = business.Validations.GetValidationsForWorkload(r.Context(), cluster, namespace, workload)
 		}()
 	}
 
diff --git a/kiali.go b/kiali.go
index d4a021416..f3f9c27ec 100644
--- a/kiali.go
+++ b/kiali.go
@@ -39,6 +39,8 @@ import (
 
 	"github.com/kiali/kiali/business"
 	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/controller"
+	"github.com/kiali/kiali/grafana"
 	"github.com/kiali/kiali/istio"
 	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/kubernetes/cache"
@@ -111,7 +113,7 @@ func main() {
 	}
 
 	log.Info("Initializing Kiali Cache")
-	cache, err := cache.NewKialiCache(clientFactory, *cfg)
+	cache, err := cache.NewKialiCache(clientFactory.GetSAClients(), *cfg)
 	if err != nil {
 		log.Fatalf("Error initializing Kiali Cache. Details: %s", err)
 	}
@@ -164,6 +166,8 @@ func main() {
 		log.Debug("Tracing is disabled")
 	}
 
+	grafana := grafana.NewService(cfg, clientFactory.GetSAHomeClusterClient())
+
 	// Start listening to requests
 	server, err := server.NewServer(cpm, clientFactory, cache, cfg, prom, tracingLoader, discovery)
 	if err != nil {
@@ -171,6 +175,15 @@ func main() {
 	}
 	server.Start()
 
+	// Needs to be started after the server so that the cache is started because the controllers use the cache.
+	layer, err := business.NewLayerWithSAClients(cfg, cache, prom, tracingClient, cpm, grafana, discovery, clientFactory.GetSAClients())
+	if err != nil {
+		log.Fatalf("Error creating business layer: %s", err)
+	}
+	if err := controller.Start(ctx, clientFactory, cache, &layer.Validations); err != nil {
+		log.Fatalf("Error creating validations controller: %s", err)
+	}
+
 	// wait forever, or at least until we are told to exit
 	waitForTermination()
 
diff --git a/kubernetes/cache/cache.go b/kubernetes/cache/cache.go
index a0d8827ae..b4e542619 100644
--- a/kubernetes/cache/cache.go
+++ b/kubernetes/cache/cache.go
@@ -79,6 +79,9 @@ type KialiCache interface {
 	RegistryStatusCache
 	ProxyStatusCache
 
+	// SetValidations caches validations for a cluster/namespace.
+	Validations() store.Store[models.IstioValidationKey, *models.IstioValidation]
+
 	// SetClusters sets the list of clusters that the cache knows about.
 	SetClusters([]models.KubeCluster)
 
@@ -101,7 +104,6 @@ type kialiCacheImpl struct {
 	cleanup                 func()
 	conf                    config.Config
 
-	clientFactory kubernetes.ClientFactory
 	// Maps a cluster name to a KubeCache
 	kubeCache map[string]KubeCache
 
@@ -126,22 +128,24 @@ type kialiCacheImpl struct {
 	registryStatusStore store.Store[string, *kubernetes.RegistryStatus]
 
 	waypointList models.WaypointStore
+	// validations key'd by the validation key
+	validations store.Store[models.IstioValidationKey, *models.IstioValidation]
 
 	// Info about the kube clusters that the cache knows about.
 	clusters    []models.KubeCluster
 	clusterLock sync.RWMutex
 }
 
-func NewKialiCache(clientFactory kubernetes.ClientFactory, cfg config.Config) (KialiCache, error) {
+func NewKialiCache(kialiSAClients map[string]kubernetes.ClientInterface, cfg config.Config) (KialiCache, error) {
 	ctx, cancel := context.WithCancel(context.Background())
 	namespaceKeyTTL := time.Duration(cfg.KubernetesConfig.CacheTokenNamespaceDuration) * time.Second
 	kialiCacheImpl := kialiCacheImpl{
 		ambientChecksPerCluster: store.NewExpirationStore(ctx, store.New[string, bool](), util.AsPtr(ambientCheckExpirationTime), nil),
 		canReadWebhookByCluster: make(map[string]bool),
 		cleanup:                 cancel,
-		clientFactory:           clientFactory,
 		conf:                    cfg,
 		kubeCache:               make(map[string]KubeCache),
+		validations:             store.New[models.IstioValidationKey, *models.IstioValidation](),
 		meshStore:               store.NewExpirationStore(ctx, store.New[string, *models.Mesh](), util.AsPtr(meshExpirationTime), nil),
 		namespaceStore:          store.NewExpirationStore(ctx, store.New[namespacesKey, map[string]models.Namespace](), &namespaceKeyTTL, nil),
 		refreshDuration:         time.Duration(cfg.KubernetesConfig.CacheDuration) * time.Second,
@@ -149,7 +153,7 @@ func NewKialiCache(clientFactory kubernetes.ClientFactory, cfg config.Config) (K
 		registryStatusStore:     store.New[string, *kubernetes.RegistryStatus](),
 	}
 
-	for cluster, client := range clientFactory.GetSAClients() {
+	for cluster, client := range kialiSAClients {
 		// we only need our deleteNamespace function called when an error occurs in a namespace-scoped cache
 		var errHandler ErrorHandler
 		if !cfg.Deployment.ClusterWideAccess {
@@ -245,6 +249,10 @@ func (c *kialiCacheImpl) SetMesh(mesh *models.Mesh) {
 	c.meshStore.Set(kialiCacheMeshKey, mesh)
 }
 
+func (c *kialiCacheImpl) Validations() store.Store[models.IstioValidationKey, *models.IstioValidation] {
+	return c.validations
+}
+
 // IsAmbientEnabled checks if the istio Ambient profile was enabled
 // by checking if the ztunnel daemonset exists on the cluster.
 func (in *kialiCacheImpl) IsAmbientEnabled(cluster string) bool {
diff --git a/kubernetes/client.go b/kubernetes/client.go
index 17b49642b..ff75d5891 100644
--- a/kubernetes/client.go
+++ b/kubernetes/client.go
@@ -107,7 +107,7 @@ func (client *K8SClient) ClusterInfo() ClusterInfo {
 	return client.clusterInfo
 }
 
-func newClientWithRemoteClusterInfo(config *rest.Config, remoteClusterInfo *RemoteClusterInfo) (*K8SClient, error) {
+func NewClientWithRemoteClusterInfo(config *rest.Config, remoteClusterInfo *RemoteClusterInfo) (*K8SClient, error) {
 	client, err := newClientFromConfig(config)
 	if err != nil {
 		return nil, err
diff --git a/kubernetes/client_factory.go b/kubernetes/client_factory.go
index 2ce684300..5d6390574 100644
--- a/kubernetes/client_factory.go
+++ b/kubernetes/client_factory.go
@@ -205,7 +205,7 @@ func (cf *clientFactory) newClient(authInfo *api.AuthInfo, expirationTime time.D
 
 	var newClient ClientInterface
 	if cluster == cf.homeCluster {
-		client, err := newClientWithRemoteClusterInfo(&config, nil)
+		client, err := NewClientWithRemoteClusterInfo(&config, nil)
 		if err != nil {
 			log.Errorf("Error creating client for cluster %s: %s", cluster, err.Error())
 			return nil, err
@@ -234,7 +234,7 @@ func (cf *clientFactory) newClient(authInfo *api.AuthInfo, expirationTime time.D
 		remoteConfig.BearerToken = authInfo.Token
 		remoteConfig.BearerTokenFile = ""
 
-		newClient, err = newClientWithRemoteClusterInfo(remoteConfig, &clusterInfo)
+		newClient, err = NewClientWithRemoteClusterInfo(remoteConfig, &clusterInfo)
 		if err != nil {
 			log.Errorf("Error getting remote client for cluster [%s]. Err: %s", cluster, err.Error())
 			return nil, err
@@ -259,7 +259,7 @@ func (cf *clientFactory) newSAClient(remoteClusterInfo *RemoteClusterInfo) (*K8S
 		return nil, err
 	}
 
-	client, err := newClientWithRemoteClusterInfo(config, remoteClusterInfo)
+	client, err := NewClientWithRemoteClusterInfo(config, remoteClusterInfo)
 	if err != nil {
 		return nil, err
 	}
diff --git a/make/Makefile.build.mk b/make/Makefile.build.mk
index fdc466c8c..2081feb78 100644
--- a/make/Makefile.build.mk
+++ b/make/Makefile.build.mk
@@ -70,7 +70,7 @@ build-system-test: go-check
 	  -o ${GOPATH}/bin/kiali -ldflags "-X main.version=${VERSION} -X main.commitHash=${COMMIT_HASH} -X main.goVersion=${GO_ACTUAL_VERSION}"
 
 ## test: Run tests, excluding third party tests under vendor and frontend. Runs `go test` internally
-test:
+test: .ensure-envtest-bin-dir-exists
 	@echo Running tests, excluding third party tests under vendor
 	${GO} test ${GO_TEST_FLAGS} $(shell ${GO} list ./... | grep -v -e /vendor/ -e /frontend/ -e /tests/integration/)
 
@@ -80,10 +80,16 @@ test-integration-setup:
 	go install github.com/jstemmer/go-junit-report@latest
 
 ## test-integration: Run Integration test suite
-test-integration: test-integration-setup
+test-integration: test-integration-setup .ensure-envtest-bin-dir-exists
 	@echo Running Integration tests
 	cd tests/integration/tests && ${GO} test ${GO_TEST_FLAGS} -v -timeout 30m 2>&1 | tee >(go-junit-report > ../junit-rest-report.xml) ../int-test.log
 	@echo Test results can be found here: $$(ls -1 ${ROOTDIR}/tests/integration/junit-rest-report.xml)
+	@echo Running controller integration tests
+	@cd tests/integration/controller && ${GO} test -v
+
+test-integration-controller: .ensure-envtest-bin-dir-exists
+	@echo Running controller integration tests
+	cd tests/integration/controller && ${GO} test -v
 
 #
 # Lint targets
@@ -97,3 +103,15 @@ lint-install:
 # doc.go is ommited for linting, because it generates lots of warnings.
 lint:
 	golangci-lint run -c ./.github/workflows/config/.golangci.yml
+
+# Assuming here that if the bin dir exists then the tools also exist inside of it.
+.ensure-envtest-bin-dir-exists: .ensure-envtest-exists
+	@if [ ! -d "${OUTDIR}/k8s" ]; then \
+		setup-envtest use --bin-dir "${OUTDIR}"; \
+	fi
+
+## Download setup-envtest locally if necessary.
+.ensure-envtest-exists:
+	@if [ ! -x envtest ]; then \
+	  ${GO} install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest; \
+	fi
\ No newline at end of file
diff --git a/store/store.go b/store/store.go
index 2d0c1871c..102f3fc77 100644
--- a/store/store.go
+++ b/store/store.go
@@ -21,6 +21,10 @@ type Store[K comparable, V any] interface {
 	// Set associates the given value with the given key. It will overwrite any existing value
 	// or create a new entry if the key does not exist.
 	Set(key K, value V)
+
+	// Version returns the current version of the store. The version is incremented every time the store is modified.
+	// It can be used to detect changes to the store.
+	Version() uint
 }
 
 // New returns a new store safe for concurrent use.
diff --git a/store/threadsafe_store.go b/store/threadsafe_store.go
index f0b47646f..b33d2f92f 100644
--- a/store/threadsafe_store.go
+++ b/store/threadsafe_store.go
@@ -8,8 +8,9 @@ import (
 
 // threadSafeStore implements the Store interface and is safe for concurrent use.
 type threadSafeStore[K comparable, V any] struct {
-	lock sync.RWMutex
-	data map[K]V
+	lock    sync.RWMutex
+	data    map[K]V
+	version uint
 }
 
 // Get returns the value associated with the given key or an error.
@@ -39,6 +40,7 @@ func (s *threadSafeStore[K, V]) Replace(items map[K]V) {
 		items = make(map[K]V)
 	}
 	s.data = items
+	s.version++
 }
 
 // Set associates the given value with the given key. It will overwrite any existing value
@@ -47,12 +49,14 @@ func (s *threadSafeStore[K, V]) Set(key K, value V) {
 	s.lock.Lock()
 	defer s.lock.Unlock()
 	s.data[key] = value
+	s.version++
 }
 
 func (s *threadSafeStore[K, V]) Remove(key K) {
 	s.lock.Lock()
 	defer s.lock.Unlock()
 	delete(s.data, key)
+	s.version++
 }
 
 // Keys returns all the keys in the store.
@@ -62,5 +66,9 @@ func (s *threadSafeStore[K, V]) Keys() []K {
 	return maps.Keys(s.data)
 }
 
+func (s *threadSafeStore[K, V]) Version() uint {
+	return s.version
+}
+
 // Interface guard to ensure threadSafeStore implements the Store.
-var _ Store[any, any] = &threadSafeStore[any, any]{}
+var _ Store[string, any] = &threadSafeStore[string, any]{}


Test patch:
diff --git a/business/istio_config_test.go b/business/istio_config_test.go
index 24f452000..43c6602fa 100644
--- a/business/istio_config_test.go
+++ b/business/istio_config_test.go
@@ -501,13 +501,18 @@ func TestHasCircuitBreaker(t *testing.T) {
 
 func TestDeleteIstioConfigDetails(t *testing.T) {
 	assert := assert.New(t)
-	k8s := kubetest.NewFakeK8sClient(data.CreateEmptyVirtualService("reviews-to-delete", "test", []string{"reviews"}))
+	k8s := kubetest.NewFakeK8sClient(
+		kubetest.FakeNamespace("test"),
+		data.CreateEmptyVirtualService("reviews-to-delete", "test", []string{"reviews"}),
+	)
 	cache := SetupBusinessLayer(t, k8s, *config.NewConfig())
 	conf := config.Get()
 
 	k8sclients := make(map[string]kubernetes.ClientInterface)
 	k8sclients[conf.KubernetesConfig.ClusterName] = k8s
-	configService := IstioConfigService{userClients: k8sclients, kialiCache: cache, controlPlaneMonitor: poller}
+
+	layer := NewWithBackends(k8sclients, k8sclients, nil, nil)
+	configService := IstioConfigService{userClients: k8sclients, kialiCache: cache, controlPlaneMonitor: poller, businessLayer: layer}
 
 	err := configService.DeleteIstioConfigDetail(context.Background(), conf.KubernetesConfig.ClusterName, "test", kubernetes.VirtualServices, "reviews-to-delete")
 	assert.Nil(err)
@@ -516,13 +521,17 @@ func TestDeleteIstioConfigDetails(t *testing.T) {
 func TestUpdateIstioConfigDetails(t *testing.T) {
 	assert := assert.New(t)
 	require := require.New(t)
-	k8s := kubetest.NewFakeK8sClient(data.CreateEmptyVirtualService("reviews-to-update", "test", []string{"reviews"}))
+	k8s := kubetest.NewFakeK8sClient(
+		kubetest.FakeNamespace("test"),
+		data.CreateEmptyVirtualService("reviews-to-update", "test", []string{"reviews"}),
+	)
 	cache := SetupBusinessLayer(t, k8s, *config.NewConfig())
 	conf := config.Get()
 
 	k8sclients := make(map[string]kubernetes.ClientInterface)
 	k8sclients[conf.KubernetesConfig.ClusterName] = k8s
-	configService := IstioConfigService{userClients: k8sclients, kialiCache: cache, controlPlaneMonitor: poller}
+	layer := NewWithBackends(k8sclients, k8sclients, nil, nil)
+	configService := IstioConfigService{userClients: k8sclients, kialiCache: cache, controlPlaneMonitor: poller, businessLayer: layer}
 
 	updatedVirtualService, err := configService.UpdateIstioConfigDetail(context.Background(), conf.KubernetesConfig.ClusterName, "test", kubernetes.VirtualServices, "reviews-to-update", "{}")
 	require.NoError(err)
@@ -533,13 +542,16 @@ func TestUpdateIstioConfigDetails(t *testing.T) {
 
 func TestCreateIstioConfigDetails(t *testing.T) {
 	assert := assert.New(t)
-	k8s := kubetest.NewFakeK8sClient()
+	k8s := kubetest.NewFakeK8sClient(
+		kubetest.FakeNamespace("test"),
+	)
 	cache := SetupBusinessLayer(t, k8s, *config.NewConfig())
 	conf := config.Get()
 
 	k8sclients := make(map[string]kubernetes.ClientInterface)
 	k8sclients[conf.KubernetesConfig.ClusterName] = k8s
-	configService := IstioConfigService{userClients: k8sclients, kialiCache: cache, controlPlaneMonitor: poller}
+	layer := NewWithBackends(k8sclients, k8sclients, nil, nil)
+	configService := IstioConfigService{userClients: k8sclients, kialiCache: cache, controlPlaneMonitor: poller, businessLayer: layer}
 
 	createVirtualService, err := configService.CreateIstioConfigDetail(context.Background(), conf.KubernetesConfig.ClusterName, "test", kubernetes.VirtualServices, []byte("{}"))
 	assert.Equal("test", createVirtualService.Namespace.Name)
diff --git a/business/istio_validations_perf_test.go b/business/istio_validations_perf_test.go
index 77cd3e9c4..9b67c989c 100644
--- a/business/istio_validations_perf_test.go
+++ b/business/istio_validations_perf_test.go
@@ -50,7 +50,7 @@ func TestGetValidationsPerf(t *testing.T) {
 		[]string{"details.test.svc.cluster.local", "product.test.svc.cluster.local", "product2.test.svc.cluster.local", "customer.test.svc.cluster.local"})
 
 	now := time.Now()
-	validations, err := vs.GetValidations(context.TODO(), conf.KubernetesConfig.ClusterName, "test", "", "")
+	validations, err := vs.CreateValidations(context.TODO(), conf.KubernetesConfig.ClusterName)
 	require.NoError(err)
 	log.Debugf("Validation Performance test took %f seconds for %d namespaces", time.Since(now).Seconds(), numNs)
 	assert.NotEmpty(validations)
diff --git a/business/istio_validations_test.go b/business/istio_validations_test.go
index 10bf6821d..d6f09e9b5 100644
--- a/business/istio_validations_test.go
+++ b/business/istio_validations_test.go
@@ -15,6 +15,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/istio"
 	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/kubernetes/kubetest"
 	"github.com/kiali/kiali/models"
@@ -60,7 +61,11 @@ func TestGetNamespaceValidations(t *testing.T) {
 	vs := mockCombinedValidationService(t, fakeIstioConfigList(),
 		[]string{"details.test.svc.cluster.local", "product.test.svc.cluster.local", "product2.test.svc.cluster.local", "customer.test.svc.cluster.local"})
 
-	validations, err := vs.GetValidations(context.TODO(), conf.KubernetesConfig.ClusterName, "test", "", "")
+	validations, err := vs.CreateValidations(context.Background(), conf.KubernetesConfig.ClusterName)
+	require.NoError(err)
+	vs.kialiCache.Validations().Replace(validations)
+
+	validations, err = vs.GetValidations(context.TODO(), conf.KubernetesConfig.ClusterName)
 	require.NoError(err)
 	require.NotEmpty(validations)
 	assert.True(validations[models.IstioValidationKey{ObjectGVK: kubernetes.VirtualServices, Namespace: "test", Name: "product-vs"}].Valid)
@@ -85,7 +90,8 @@ func TestGatewayValidation(t *testing.T) {
 	conf.Deployment.ClusterWideAccess = true
 	kubernetes.SetConfig(t, *conf)
 
-	v := mockMultiNamespaceGatewaysValidationService(t, *conf)
+	objs := mockMultiNamespaceGateways()
+	v := fakeValidationMeshService(t, *conf, objs...)
 	validations, _, _ := v.GetIstioObjectValidations(context.TODO(), conf.KubernetesConfig.ClusterName, "test", kubernetes.Gateways, "first")
 	assert.NotEmpty(validations)
 }
@@ -166,7 +172,15 @@ func TestGatewayValidationScopesToNamespaceWhenGatewayToNamespaceSet(t *testing.
 		},
 	}
 
-	v := mockMultiNamespaceGatewaysValidationService(t, *conf, revConfigMap, injectorConfigMap, istioSystemNamespace, istiod_1_19_0, gatewayDeployment, FakeCertificateConfigMap("istio-system"))
+	objs := []runtime.Object{
+		revConfigMap,
+		injectorConfigMap,
+		istioSystemNamespace,
+		istiod_1_19_0,
+		gatewayDeployment,
+	}
+	objs = append(objs, mockMultiNamespaceGateways()...)
+	v := fakeValidationMeshService(t, *conf, objs...)
 	validations, _, err := v.GetIstioObjectValidations(context.TODO(), conf.KubernetesConfig.ClusterName, "test", kubernetes.Gateways, "first")
 	require.NoError(err)
 	require.Len(validations, 1)
@@ -200,7 +214,8 @@ func TestFilterExportToNamespacesVS(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, vs3toall)
 	vs3towrong := loadVirtualService("vs_bookinfo3_to_wrong.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, vs3towrong)
-	v := mockEmptyValidationService(t)
+	objs := mockEmpty(t, conf)
+	v := fakeValidationMeshService(t, *conf, objs...)
 	filteredVSs := v.filterVSExportToNamespaces(map[string]bool{"bookinfo": false, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo", "", currentIstioObjects)
 	var expectedVS []*networking_v1.VirtualService
 	expectedVS = append(expectedVS, vs1tothis)
@@ -238,7 +253,8 @@ func TestAmbientFilterExportToNamespacesVS(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, vs3toall)
 	vs3towrong := loadVirtualService("vs_bookinfo3_to_wrong.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, vs3towrong)
-	v := mockAmbientValidationService(t)
+	objects := mockAmbient(t, conf)
+	v := fakeValidationMeshService(t, *conf, objects...)
 	filteredVSs := v.filterVSExportToNamespaces(map[string]bool{"bookinfo": true, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo2", "", currentIstioObjects)
 	var expectedVS []*networking_v1.VirtualService
 	expectedVS = append(expectedVS, vs2tothis)
@@ -270,7 +286,8 @@ func TestFilterVSMeshExportToThis(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, vs2not)
 	vs3toall := loadVirtualService("vs_bookinfo3_to_all.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, vs3toall)
-	v := mockEmptyValidationService(t, ".")
+	objs := mockEmpty(t, conf, ".")
+	v := fakeValidationMeshService(t, *conf, objs...)
 	filteredVSs := v.filterVSExportToNamespaces(map[string]bool{"bookinfo": false, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo", "", currentIstioObjects)
 	var expectedVS []*networking_v1.VirtualService
 	expectedVS = append(expectedVS, vs1tothis)
@@ -301,7 +318,8 @@ func TestFilterVSMeshExportToAll(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, vs2not)
 	vs3toall := loadVirtualService("vs_bookinfo3_to_all.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, vs3toall)
-	v := mockEmptyValidationService(t, "*")
+	objs := mockEmpty(t, conf, "*")
+	v := fakeValidationMeshService(t, *conf, objs...)
 	filteredVSs := v.filterVSExportToNamespaces(map[string]bool{"bookinfo": false, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo2", "", currentIstioObjects)
 	var expectedVS []*networking_v1.VirtualService
 	expectedVS = append(expectedVS, vs1not)
@@ -338,7 +356,8 @@ func TestFilterExportToNamespacesDR(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, dr3toall)
 	dr3towrong := loadDestinationRule("dr_bookinfo3_to_wrong.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, dr3towrong)
-	v := mockEmptyValidationService(t)
+	objs := mockEmpty(t, conf)
+	v := fakeValidationMeshService(t, *conf, objs...)
 	filteredDRs := v.filterDRExportToNamespaces(map[string]bool{"bookinfo": false, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo", "", currentIstioObjects)
 	var expectedDR []*networking_v1.DestinationRule
 	expectedDR = append(expectedDR, dr1tothis)
@@ -376,7 +395,8 @@ func TestAmbientFilterExportToNamespacesDR(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, dr3toall)
 	dr3towrong := loadDestinationRule("dr_bookinfo3_to_wrong.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, dr3towrong)
-	v := mockAmbientValidationService(t)
+	objects := mockAmbient(t, conf)
+	v := fakeValidationMeshService(t, *conf, objects...)
 	filteredDRs := v.filterDRExportToNamespaces(map[string]bool{"bookinfo": true, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo2", "", currentIstioObjects)
 	var expectedDR []*networking_v1.DestinationRule
 	expectedDR = append(expectedDR, dr2tothis)
@@ -408,7 +428,8 @@ func TestFilterDRMeshExportToThis(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, dr2not)
 	dr3toall := loadDestinationRule("dr_bookinfo3_to_all.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, dr3toall)
-	v := mockEmptyValidationService(t, ".")
+	objs := mockEmpty(t, conf, ".")
+	v := fakeValidationMeshService(t, *conf, objs...)
 	filteredDRs := v.filterDRExportToNamespaces(map[string]bool{"bookinfo": false, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo", "", currentIstioObjects)
 	var expectedDR []*networking_v1.DestinationRule
 	expectedDR = append(expectedDR, dr1tothis)
@@ -439,7 +460,8 @@ func TestFilterDRMeshExportToAll(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, dr2not)
 	dr3toall := loadDestinationRule("dr_bookinfo3_to_all.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, dr3toall)
-	v := mockEmptyValidationService(t, "*")
+	objs := mockEmpty(t, conf, "*")
+	v := fakeValidationMeshService(t, *conf, objs...)
 	filteredDRs := v.filterDRExportToNamespaces(map[string]bool{"bookinfo": false, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo2", "", currentIstioObjects)
 	var expectedDR []*networking_v1.DestinationRule
 	expectedDR = append(expectedDR, dr1not)
@@ -476,7 +498,8 @@ func TestFilterExportToNamespacesSE(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, se3toall)
 	se3towrong := loadServiceEntry("se_bookinfo3_to_wrong.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, se3towrong)
-	v := mockEmptyValidationService(t)
+	objs := mockEmpty(t, conf)
+	v := fakeValidationMeshService(t, *conf, objs...)
 	filteredSEs := v.filterSEExportToNamespaces(map[string]bool{"bookinfo": false, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo", "", currentIstioObjects)
 	var expectedSE []*networking_v1.ServiceEntry
 	expectedSE = append(expectedSE, se1tothis)
@@ -514,7 +537,8 @@ func TestAmbientFilterExportToNamespacesSE(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, se3toall)
 	se3towrong := loadServiceEntry("se_bookinfo3_to_wrong.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, se3towrong)
-	v := mockAmbientValidationService(t)
+	objects := mockAmbient(t, conf)
+	v := fakeValidationMeshService(t, *conf, objects...)
 	filteredSEs := v.filterSEExportToNamespaces(map[string]bool{"bookinfo": true, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo2", "", currentIstioObjects)
 	var expectedSE []*networking_v1.ServiceEntry
 	expectedSE = append(expectedSE, se2tothis)
@@ -546,7 +570,8 @@ func TestFilterSEMeshExportToThis(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, se2not)
 	se3toall := loadServiceEntry("se_bookinfo3_to_all.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, se3toall)
-	v := mockEmptyValidationService(t, ".")
+	objs := mockEmpty(t, conf, ".")
+	v := fakeValidationMeshService(t, *conf, objs...)
 	filteredSEs := v.filterSEExportToNamespaces(map[string]bool{"bookinfo": false, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo", "", currentIstioObjects)
 	var expectedSE []*networking_v1.ServiceEntry
 	expectedSE = append(expectedSE, se1tothis)
@@ -577,7 +602,8 @@ func TestFilterSEMeshExportToAll(t *testing.T) {
 	currentIstioObjects = append(currentIstioObjects, se2not)
 	se3toall := loadServiceEntry("se_bookinfo3_to_all.yaml", t)
 	currentIstioObjects = append(currentIstioObjects, se3toall)
-	v := mockEmptyValidationService(t, "*")
+	objs := mockEmpty(t, conf, "*")
+	v := fakeValidationMeshService(t, *conf, objs...)
 	filteredSEs := v.filterSEExportToNamespaces(map[string]bool{"bookinfo": false, "bookinfo2": false, "bookinfo3": false, "default": false}, "bookinfo2", "", currentIstioObjects)
 	var expectedSE []*networking_v1.ServiceEntry
 	expectedSE = append(expectedSE, se1not)
@@ -629,7 +655,75 @@ func TestGetVSReferencesNotExisting(t *testing.T) {
 	assert.Nil(references)
 }
 
-func mockMultiNamespaceGatewaysValidationService(t *testing.T, cfg config.Config, objects ...runtime.Object) IstioValidationsService {
+func TestValidatingSingleObjectUpdatesList(t *testing.T) {
+	assert := assert.New(t)
+	require := require.New(t)
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	vs := mockCombinedValidationService(t, fakeIstioConfigList(),
+		[]string{"details.test.svc.cluster.local", "product.test.svc.cluster.local", "product2.test.svc.cluster.local", "customer.test.svc.cluster.local"})
+
+	v, err := vs.userClients[conf.KubernetesConfig.ClusterName].Istio().NetworkingV1().VirtualServices("test").Get(context.Background(), "product-vs", v1.GetOptions{})
+	require.NoError(err)
+
+	validations, err := vs.CreateValidations(context.Background(), conf.KubernetesConfig.ClusterName)
+	require.NoError(err)
+	vs.kialiCache.Validations().Replace(validations)
+
+	currentValidations, err := vs.GetValidations(context.Background(), conf.KubernetesConfig.ClusterName)
+	require.NoError(err)
+
+	key := models.IstioValidationKey{ObjectGVK: kubernetes.VirtualServices, Namespace: "test", Name: "product-vs"}
+	require.True(currentValidations[key].Valid)
+
+	v.Spec.Gateways = []string{"nonexistant"}
+	_, err = vs.userClients[conf.KubernetesConfig.ClusterName].Istio().NetworkingV1().VirtualServices("test").Update(context.Background(), v, v1.UpdateOptions{})
+	require.NoError(err)
+
+	updatedValidations, _, err := vs.GetIstioObjectValidations(context.Background(), conf.KubernetesConfig.ClusterName, "test", kubernetes.VirtualServices, "product-vs")
+	require.NoError(err)
+	require.False(updatedValidations[key].Valid)
+
+	validations, err = vs.GetValidations(context.Background(), conf.KubernetesConfig.ClusterName)
+	require.NoError(err)
+	require.NotEmpty(validations)
+	assert.False(validations[key].Valid)
+}
+
+func fakeValidationMeshService(t *testing.T, cfg config.Config, objects ...runtime.Object) IstioValidationsService {
+	k8s := kubetest.NewFakeK8sClient(objects...)
+	cache := SetupBusinessLayer(t, k8s, cfg)
+
+	k8sclients := make(map[string]kubernetes.ClientInterface)
+	k8sclients[cfg.KubernetesConfig.ClusterName] = k8s
+	discovery := istio.NewDiscovery(k8sclients, cache, &cfg)
+	namespace := NewNamespaceService(k8sclients, k8sclients, cache, &cfg, discovery)
+	mesh := NewMeshService(k8sclients, discovery)
+	layer := NewWithBackends(k8sclients, k8sclients, nil, nil)
+	return NewValidationsService(&layer.IstioConfig, cache, &mesh, &namespace, &layer.Svc, k8sclients, &layer.Workload)
+}
+
+func fakeValidationMeshServiceWithRegistryStatus(t *testing.T, cfg config.Config, services []string, objects ...runtime.Object) IstioValidationsService {
+	k8s := kubetest.NewFakeK8sClient(objects...)
+	cache := SetupBusinessLayer(t, k8s, cfg)
+	conf := config.NewConfig()
+	cache.SetRegistryStatus(map[string]*kubernetes.RegistryStatus{
+		conf.KubernetesConfig.ClusterName: {
+			Services: data.CreateFakeMultiRegistryServices(services, "test", "*"),
+		},
+	})
+
+	k8sclients := make(map[string]kubernetes.ClientInterface)
+	k8sclients[cfg.KubernetesConfig.ClusterName] = k8s
+	discovery := istio.NewDiscovery(k8sclients, cache, conf)
+	namespace := NewNamespaceService(k8sclients, k8sclients, cache, conf, discovery)
+	mesh := NewMeshService(k8sclients, discovery)
+	layer := NewWithBackends(k8sclients, k8sclients, nil, nil)
+	return NewValidationsService(&layer.IstioConfig, cache, &mesh, &namespace, &layer.Svc, k8sclients, &layer.Workload)
+}
+
+func mockMultiNamespaceGateways() []runtime.Object {
 	fakeIstioObjects := []runtime.Object{
 		&core_v1.ConfigMap{ObjectMeta: v1.ObjectMeta{Name: "istio", Namespace: "istio-system"}},
 	}
@@ -652,22 +746,17 @@ func mockMultiNamespaceGatewaysValidationService(t *testing.T, cfg config.Config
 		fakeIstioObjects = append(fakeIstioObjects, p.DeepCopyObject())
 	}
 
-	fakeIstioObjects = append(fakeIstioObjects, objects...)
 	fakeIstioObjects = append(fakeIstioObjects, kubernetes.ToRuntimeObjects(getGateway("first", "test"))...)
 	fakeIstioObjects = append(fakeIstioObjects, kubernetes.ToRuntimeObjects(getGateway("second", "test2"))...)
 
-	k8s := kubetest.NewFakeK8sClient(fakeIstioObjects...)
-	SetupBusinessLayer(t, k8s, cfg)
-
-	k8sclients := make(map[string]kubernetes.ClientInterface)
-	k8sclients[cfg.KubernetesConfig.ClusterName] = k8s
-	return IstioValidationsService{discovery: discovery, userClients: k8sclients, businessLayer: NewWithBackends(k8sclients, k8sclients, nil, nil)}
+	return fakeIstioObjects
 }
 
 func mockCombinedValidationService(t *testing.T, istioConfigList *models.IstioConfigList, services []string) IstioValidationsService {
 	fakeIstioObjects := []runtime.Object{
 		&core_v1.ConfigMap{ObjectMeta: v1.ObjectMeta{Name: "istio", Namespace: "istio-system"}},
 		kubetest.FakeNamespace("wrong"),
+		kubetest.FakeNamespace("istio-system"),
 	}
 	for _, p := range fakeMeshPolicies() {
 		fakeIstioObjects = append(fakeIstioObjects, p.DeepCopyObject())
@@ -698,22 +787,10 @@ func mockCombinedValidationService(t *testing.T, istioConfigList *models.IstioCo
 	fakeIstioObjects = append(fakeIstioObjects, kubernetes.ToRuntimeObjects(istioConfigList.WorkloadEntries)...)
 	fakeIstioObjects = append(fakeIstioObjects, kubernetes.ToRuntimeObjects(istioConfigList.RequestAuthentications)...)
 
-	k8s := kubetest.NewFakeK8sClient(fakeIstioObjects...)
-
-	conf := config.NewConfig()
-	cache := SetupBusinessLayer(t, k8s, *conf)
-	cache.SetRegistryStatus(map[string]*kubernetes.RegistryStatus{
-		conf.KubernetesConfig.ClusterName: {
-			Services: data.CreateFakeMultiRegistryServices(services, "test", "*"),
-		},
-	})
-
-	k8sclients := make(map[string]kubernetes.ClientInterface)
-	k8sclients[conf.KubernetesConfig.ClusterName] = k8s
-	return IstioValidationsService{discovery: discovery, userClients: k8sclients, businessLayer: NewWithBackends(k8sclients, k8sclients, nil, nil)}
+	return fakeValidationMeshServiceWithRegistryStatus(t, *config.NewConfig(), services, fakeIstioObjects...)
 }
 
-func mockAmbientValidationService(t *testing.T) IstioValidationsService {
+func mockAmbient(t *testing.T, conf *config.Config) []runtime.Object {
 	objects := []runtime.Object{
 		fakeDaemonSetWithStatus("istio-ingressgateway", map[string]string{"app": "istio-ingressgateway", "istio": "ingressgateway"}, unhealthyDaemonSetStatus),
 		fakeDaemonSetWithStatus("ztunnel", map[string]string{"app": "ztunnel"}, unhealthyDaemonSetStatus),
@@ -721,9 +798,8 @@ func mockAmbientValidationService(t *testing.T) IstioValidationsService {
 		fakeDeploymentWithStatus("istiod", map[string]string{"app": "istiod", "istio": "pilot"}, healthyStatus),
 	}
 
-	k8s, _, _ := mockAddOnsCalls(t, objects, true, false)
+	mockAddOnsCalls(t, objects, true, false)
 
-	conf := config.Get()
 	conf.IstioLabels.AppLabelName = "app.kubernetes.io/name"
 	conf.ExternalServices.Istio.ComponentStatuses = config.ComponentStatuses{
 		Enabled: true,
@@ -733,17 +809,11 @@ func mockAmbientValidationService(t *testing.T) IstioValidationsService {
 			{AppLabel: "istio-ingressgateway", IsCore: false},
 		},
 	}
-	config.Set(conf)
-
-	// Set global cache var
-	SetupBusinessLayer(t, k8s, *conf)
 
-	clients := make(map[string]kubernetes.ClientInterface)
-	clients[conf.KubernetesConfig.ClusterName] = k8s
-	return IstioValidationsService{userClients: clients, businessLayer: NewWithBackends(clients, clients, nil, nil)}
+	return objects
 }
 
-func mockEmptyValidationService(t *testing.T, exportToValue ...string) IstioValidationsService {
+func mockEmpty(t *testing.T, conf *config.Config, exportToValue ...string) []runtime.Object {
 	configMapData := `accessLogFile: /dev/stdout
 enableAutoMtls: true
 rootNamespace: istio-system
@@ -769,7 +839,6 @@ defaultVirtualServiceExportTo:
 		},
 		Data: map[string]string{"mesh": configMapData},
 	}
-
 	objects := []runtime.Object{
 		fakeDaemonSetWithStatus("istio-ingressgateway", map[string]string{"app": "istio-ingressgateway", "istio": "ingressgateway"}, unhealthyDaemonSetStatus),
 		fakeDeploymentWithStatus("istio-egressgateway", map[string]string{"app": "istio-egressgateway", "istio": "egressgateway"}, unhealthyStatus),
@@ -778,9 +847,8 @@ defaultVirtualServiceExportTo:
 		FakeCertificateConfigMap("istio-system"),
 	}
 
-	k8s, _, _ := mockAddOnsCalls(t, objects, true, false)
+	mockAddOnsCalls(t, objects, true, false)
 
-	conf := config.Get()
 	conf.IstioLabels.AppLabelName = "app.kubernetes.io/name"
 	conf.ExternalServices.Istio.ComponentStatuses = config.ComponentStatuses{
 		Enabled: true,
@@ -792,12 +860,7 @@ defaultVirtualServiceExportTo:
 	}
 	config.Set(conf)
 
-	// Set global cache var
-	SetupBusinessLayer(t, k8s, *conf)
-
-	k8sclients := make(map[string]kubernetes.ClientInterface)
-	k8sclients[config.Get().KubernetesConfig.ClusterName] = k8s
-	return IstioValidationsService{userClients: k8sclients, businessLayer: NewWithBackends(k8sclients, k8sclients, nil, nil)}
+	return objects
 }
 
 func fakeEmptyIstioConfigList() *models.IstioConfigList {
diff --git a/controller/validations_test.go b/controller/validations_test.go
new file mode 100644
index 000000000..839a232a8
--- /dev/null
+++ b/controller/validations_test.go
@@ -0,0 +1,83 @@
+package controller_test
+
+/*
+	Unit tests for the validations controller.
+	Integration tests using envtest can be found in the 'tests/integration/controller' directory.
+
+	When should you write a unit test vs. writing an integration test? Favor integration tests over unit tests.
+	Write unit tests only when there's some specific state you need to test that you can't easily setup in an integration test.
+*/
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	"github.com/kiali/kiali/business"
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/controller"
+	"github.com/kiali/kiali/istio"
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/cache"
+	"github.com/kiali/kiali/kubernetes/kubetest"
+	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/store"
+)
+
+type incrementFirstVersionStore[K comparable, V any] struct {
+	store.Store[K, V]
+	version uint
+}
+
+func (s *incrementFirstVersionStore[K, V]) Version() uint {
+	currentVersion := s.version
+	if currentVersion == 0 {
+		s.version++
+	}
+	return currentVersion
+}
+
+type incrementFirstVersionCache struct {
+	cache.KialiCache
+	validations *incrementFirstVersionStore[models.IstioValidationKey, *models.IstioValidation]
+}
+
+func (s *incrementFirstVersionCache) Validations() store.Store[models.IstioValidationKey, *models.IstioValidation] {
+	return s.validations
+}
+
+func newIncrementFirstVersionCache(cache cache.KialiCache) *incrementFirstVersionCache {
+	return &incrementFirstVersionCache{
+		KialiCache:  cache,
+		validations: &incrementFirstVersionStore[models.IstioValidationKey, *models.IstioValidation]{Store: cache.Validations()},
+	}
+}
+
+func TestValidationsFailsToUpdateWithOldCache(t *testing.T) {
+	require := require.New(t)
+	conf := config.NewConfig()
+	client := kubetest.NewFakeK8sClient(
+		&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "istio-system"}},
+	)
+	cache := newIncrementFirstVersionCache(business.SetupBusinessLayer(t, client, *conf))
+	k8sclients := map[string]kubernetes.ClientInterface{conf.KubernetesConfig.ClusterName: client}
+	discovery := istio.NewDiscovery(k8sclients, cache, conf)
+	namespace := business.NewNamespaceService(k8sclients, k8sclients, cache, conf, discovery)
+	mesh := business.NewMeshService(k8sclients, discovery)
+	layer := business.NewWithBackends(k8sclients, k8sclients, nil, nil)
+	validations := business.NewValidationsService(&layer.IstioConfig, cache, &mesh, &namespace, &layer.Svc, k8sclients, &layer.Workload)
+	reconciler := controller.NewValidationsReconciler([]string{conf.KubernetesConfig.ClusterName}, cache, &validations, 0)
+
+	// We want to test that the reconciler won't update the cache if the version has changed.
+	// Going to test this by having an implementation of the store which increments the version
+	// when you call it the first time. That way the next time it's called it will have a different
+	// version and the reconciler should not update the cache.
+	req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "queue", Namespace: "queue"}}
+	_, err := reconciler.Reconcile(context.Background(), req)
+	require.Error(err)
+}
diff --git a/kubernetes/cache/cache_test.go b/kubernetes/cache/cache_test.go
index c92b95e31..930eb997e 100644
--- a/kubernetes/cache/cache_test.go
+++ b/kubernetes/cache/cache_test.go
@@ -20,16 +20,8 @@ func TestNoHomeClusterReturnsError(t *testing.T) {
 	conf := config.NewConfig()
 	config.Set(conf)
 
-	client := kubetest.NewFakeK8sClient()
-	clientFactory := kubetest.NewK8SClientFactoryMock(client)
-	clientFactory.SetClients(map[string]kubernetes.ClientInterface{"nothomecluster": client})
-
-	cache, err := cache.NewKialiCache(clientFactory, *conf)
-	defer func() {
-		if cache != nil {
-			cache.Stop()
-		}
-	}()
+	clients := map[string]kubernetes.ClientInterface{"nothomecluster": kubetest.NewFakeK8sClient()}
+	_, err := cache.NewKialiCache(clients, *conf)
 	require.Error(err, "no home cluster should return an error")
 }
 
@@ -43,13 +35,12 @@ func TestKubeCacheCreatedPerClient(t *testing.T) {
 	deploymentCluster2 := &apps_v1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "deployment2", Namespace: "test"}}
 	client := kubetest.NewFakeK8sClient(ns, deploymentCluster1)
 	client2 := kubetest.NewFakeK8sClient(ns, deploymentCluster2)
-	clientFactory := kubetest.NewK8SClientFactoryMock(nil)
-	clientFactory.SetClients(map[string]kubernetes.ClientInterface{
+	saClients := map[string]kubernetes.ClientInterface{
 		conf.KubernetesConfig.ClusterName: client,
 		"cluster2":                        client2,
-	})
+	}
 
-	kialiCache := cache.NewTestingCacheWithFactory(t, clientFactory, *conf)
+	kialiCache, _ := cache.NewKialiCache(saClients, *conf)
 
 	caches := kialiCache.GetKubeCaches()
 	require.Equal(2, len(caches))
@@ -255,3 +246,14 @@ func TestRefreshTokenNamespaces(t *testing.T) {
 	require.Equal(1, len(namespaces))
 	require.Equal("test", namespaces[0].Name)
 }
+
+func TestValidationsSetByConstructor(t *testing.T) {
+	require := require.New(t)
+	conf := config.NewConfig()
+
+	clients := map[string]kubernetes.ClientInterface{conf.KubernetesConfig.ClusterName: kubetest.NewFakeK8sClient()}
+	cache, err := cache.NewKialiCache(clients, *conf)
+	require.NoError(err)
+
+	require.NotNil(cache.Validations())
+}
diff --git a/kubernetes/cache/testing.go b/kubernetes/cache/testing.go
index 307639fe2..2cd3f9bf3 100644
--- a/kubernetes/cache/testing.go
+++ b/kubernetes/cache/testing.go
@@ -18,7 +18,7 @@ func newTestingCache(t *testing.T, cf kubernetes.ClientFactory, conf config.Conf
 	// when the cache is created.
 	conf.ExternalServices.Istio.IstioAPIEnabled = false
 
-	cache, err := NewKialiCache(cf, conf)
+	cache, err := NewKialiCache(cf.GetSAClients(), conf)
 	if err != nil {
 		t.Fatalf("Error creating KialiCache: %v", err)
 	}
diff --git a/store/store_test.go b/store/store_test.go
index 82e3bfb28..dc9b83d42 100644
--- a/store/store_test.go
+++ b/store/store_test.go
@@ -66,10 +66,13 @@ func TestSetNewKey(t *testing.T) {
 	_, found := testStore.Get("key1")
 	require.False(found)
 
+	currentVersion := testStore.Version()
+
 	testStore.Set("key1", 42)
 	val, found := testStore.Get("key1")
 	require.True(found)
 	require.Equal(42, val)
+	require.Equal(currentVersion+1, testStore.Version())
 }
 
 func TestSetExistingKey(t *testing.T) {
@@ -114,3 +117,40 @@ func TestRemove(t *testing.T) {
 	require.False(found)
 	require.Zero(v)
 }
+
+func TestReplaceIncrementsVersion(t *testing.T) {
+	require := require.New(t)
+
+	testStore := store.New[string, int]()
+	require.Equal(uint(0), testStore.Version())
+	testStore.Replace(map[string]int{"": 1})
+
+	require.Equal(uint(1), testStore.Version())
+}
+
+func TestItemsReturnsWholeMap(t *testing.T) {
+	require := require.New(t)
+
+	testStore := store.New[string, int]()
+	testStore.Replace(map[string]int{"key1": 42, "key2": 99})
+
+	contents := testStore.Items()
+	require.Len(contents, 2)
+	require.Equal(42, contents["key1"])
+	require.Equal(99, contents["key2"])
+}
+
+func TestDeleteKey(t *testing.T) {
+	require := require.New(t)
+
+	testStore := store.New[string, int]()
+	testStore.Replace(map[string]int{"key1": 42, "key2": 99})
+	require.Equal(uint(1), testStore.Version())
+
+	testStore.Remove("key1")
+	_, found := testStore.Get("key1")
+	require.False(found)
+	require.Equal(uint(2), testStore.Version())
+
+	testStore.Remove("nonexistent")
+}
diff --git a/tests/integration/controller/suite_test.go b/tests/integration/controller/suite_test.go
new file mode 100644
index 000000000..be2b3bb6b
--- /dev/null
+++ b/tests/integration/controller/suite_test.go
@@ -0,0 +1,160 @@
+/*
+Copyright 2024 The Kubernetes authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+Adapted from: https://github.com/kubernetes-sigs/kubebuilder/blob/8afeb403549fa87bf55a00e10a819a34719eec53/docs/book/src/cronjob-tutorial/testdata/project/internal/controller/suite_test.go
+*/
+
+package controller
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"testing"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"k8s.io/client-go/rest"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/envtest"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	"github.com/kiali/kiali/business"
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/controller"
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/cache"
+	kialicmd "github.com/kiali/kiali/tools/cmd"
+	"github.com/kiali/kiali/util"
+)
+
+var (
+	cfg        *rest.Config
+	k8sClient  client.Client
+	testEnv    *envtest.Environment
+	ctx        context.Context
+	cancel     context.CancelFunc
+	kialiCache cache.KialiCache
+)
+
+func TestControllers(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecs(t, "Controller Suite")
+}
+
+var (
+	kialiOutputFolder     = filepath.Join(kialicmd.KialiProjectRoot, "_output")
+	binaryAssetsDirectory = filepath.Join(kialiOutputFolder, "k8s")
+)
+
+var _ = BeforeSuite(func() {
+	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+	ctx, cancel = context.WithCancel(context.TODO())
+	var err error
+
+	istioVersion := os.Getenv("ISTIO_VERSION")
+	if istioVersion == "" {
+		files, err := os.ReadDir(kialiOutputFolder)
+		Expect(err).NotTo(HaveOccurred())
+		for _, file := range files {
+			// Simply pick one that beings with istio-
+			if strings.HasPrefix(file.Name(), "istio-") {
+				istioVersion = file.Name()
+				break
+			}
+		}
+	}
+
+	// Istio needs to be installed in the kiali/_output folder in order for these tests to run because
+	// these tests spin up a real API server and we need to find the Istio CRDs in the output folder
+	// so they can be installed in the API server.
+	if istioVersion == "" {
+		Fail(fmt.Sprintf("ISTIO_VERSION not set and could not be automatically determined. Have you installed istio in your kiali output directory: '%s'?", kialiOutputFolder))
+	}
+
+	kubeVersion := os.Getenv("KUBE_VERSION")
+	if kubeVersion == "" {
+		// Find one that ends with -<os>-<arch>.
+		files, err := os.ReadDir(binaryAssetsDirectory)
+		Expect(err).NotTo(HaveOccurred())
+		for _, file := range files {
+			if strings.HasSuffix(file.Name(), fmt.Sprintf("-%s-%s", runtime.GOOS, runtime.GOARCH)) {
+				kubeVersion = file.Name()
+				break
+			}
+		}
+	}
+
+	By("bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		// e.g. _output/istio-1.20.3/manifests/charts/base/crds/crd-all.gen.yaml
+		CRDDirectoryPaths:     []string{filepath.Join(kialiOutputFolder, istioVersion, "manifests", "charts", "base", "crds")},
+		ErrorIfCRDPathMissing: true,
+		// e.g. "_output/k8s/1.29.1-linux-amd64"
+		BinaryAssetsDirectory: filepath.Join(kialiOutputFolder, "k8s", kubeVersion),
+	}
+
+	cfg, err = testEnv.Start()
+	Expect(err).NotTo(HaveOccurred())
+	Expect(cfg).NotTo(BeNil())
+
+	scheme, err := controller.NewScheme()
+	Expect(err).NotTo(HaveOccurred())
+
+	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
+	Expect(err).NotTo(HaveOccurred())
+	Expect(k8sClient).NotTo(BeNil())
+
+	k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
+		Scheme: scheme,
+	})
+	Expect(err).ToNot(HaveOccurred())
+
+	conf := config.NewConfig()
+	kialiKubeClient, err := kubernetes.NewClientWithRemoteClusterInfo(cfg, nil)
+	Expect(err).ToNot(HaveOccurred())
+
+	saClients := map[string]kubernetes.ClientInterface{
+		conf.KubernetesConfig.ClusterName: kialiKubeClient,
+	}
+	kialiCache, err = cache.NewKialiCache(saClients, *conf)
+	Expect(err).ToNot(HaveOccurred())
+	layer, err := business.NewLayerWithSAClients(conf, kialiCache, nil, nil, nil, nil, nil, saClients)
+	Expect(err).ToNot(HaveOccurred())
+	err = controller.NewValidationsController(ctx, []string{conf.KubernetesConfig.ClusterName}, kialiCache, &layer.Validations, k8sManager, util.AsPtr(time.Millisecond*100))
+	Expect(err).ToNot(HaveOccurred())
+
+	go func() {
+		defer GinkgoRecover()
+		err = k8sManager.Start(ctx)
+		Expect(err).ToNot(HaveOccurred(), "failed to run manager")
+	}()
+})
+
+var _ = AfterSuite(func() {
+	cancel()
+	kialiCache.Stop()
+	By("tearing down the test environment")
+	err := testEnv.Stop()
+	Expect(err).NotTo(HaveOccurred())
+})
diff --git a/tests/integration/controller/validations_test.go b/tests/integration/controller/validations_test.go
new file mode 100644
index 000000000..c3527f074
--- /dev/null
+++ b/tests/integration/controller/validations_test.go
@@ -0,0 +1,114 @@
+package controller
+
+import (
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	apinetworkingv1beta1 "istio.io/api/networking/v1beta1"
+	networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/models"
+)
+
+var _ = Describe("Validations controller", func() {
+	const (
+		timeout  = time.Second * 10
+		interval = time.Millisecond * 250
+	)
+
+	Context("When validating a VirtualService", func() {
+		It("Should create validations in the kiali cache when a new VirtualService is created", func() {
+			Expect(kialiCache.Validations().Items()).Should(BeEmpty())
+
+			By("By creating a VirtualService")
+			istioSystemNamespace := &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "istio-system",
+				},
+			}
+			Expect(k8sClient.Create(ctx, istioSystemNamespace)).Should(Succeed())
+			vs := &networkingv1beta1.VirtualService{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-vs",
+					Namespace: "default",
+				},
+				Spec: apinetworkingv1beta1.VirtualService{
+					Hosts:    []string{"test.com"},
+					Gateways: []string{"test-gateway"},
+				},
+			}
+			Expect(k8sClient.Create(ctx, vs)).Should(Succeed())
+
+			vsKey := types.NamespacedName{Name: vs.Name, Namespace: vs.Namespace}
+			createdVS := &networkingv1beta1.VirtualService{}
+
+			Eventually(func() bool {
+				err := k8sClient.Get(ctx, vsKey, createdVS)
+				return err == nil
+			}, timeout, interval).Should(BeTrue())
+			Expect(createdVS.Spec.Hosts).Should(Equal([]string{"test.com"}))
+
+			By("By checking that the validations are created in the kiali cache")
+			Eventually(func() bool {
+				validations := kialiCache.Validations().Items()
+				return len(validations) > 0
+			}, timeout, interval).Should(BeTrue())
+		})
+
+		It("Should update validations in the kiali cache when an existing VirtualService is updated", func() {
+			validationKey := models.IstioValidationKey{Name: "test-vs", Namespace: "default", ObjectGVK: kubernetes.VirtualServices}
+			validation, err := kialiCache.Validations().Get(validationKey)
+			Expect(err).ShouldNot(HaveOccurred())
+			Expect(validation.Checks).Should(BeEmpty())
+			By("By updating a VirtualService")
+			vs := &networkingv1beta1.VirtualService{}
+			vsKey := types.NamespacedName{Name: "test-vs", Namespace: "default"}
+			Expect(k8sClient.Get(ctx, vsKey, vs)).Should(Succeed())
+			// Duplicate routes should be invalid.
+			vs.Spec.Http = []*apinetworkingv1beta1.HTTPRoute{
+				{
+					Route: []*apinetworkingv1beta1.HTTPRouteDestination{
+						{
+							Destination: &apinetworkingv1beta1.Destination{
+								Host: "test.com",
+							},
+						},
+						{
+							Destination: &apinetworkingv1beta1.Destination{
+								Host: "test.com",
+							},
+						},
+					},
+				},
+			}
+			Expect(k8sClient.Update(ctx, vs)).Should(Succeed())
+
+			By("By checking that the validations are then updated in the kiali cache")
+			Eventually(func() bool {
+				validation, err := kialiCache.Validations().Get(validationKey)
+				Expect(err).ShouldNot(HaveOccurred())
+				return len(validation.Checks) > 0
+			}, timeout, interval).Should(BeTrue())
+		})
+
+		It("Should delete validations in the kiali cache when the VirtualService is deleted", func() {
+			By("By deleting a VirtualService")
+			vs := &networkingv1beta1.VirtualService{}
+			vsKey := types.NamespacedName{Name: "test-vs", Namespace: "default"}
+			Expect(k8sClient.Get(ctx, vsKey, vs)).Should(Succeed())
+			Expect(k8sClient.Delete(ctx, vs)).Should(Succeed())
+
+			By("By checking that the validations are then deleted from the kiali cache")
+			Eventually(func() bool {
+				validationKey := models.IstioValidationKey{Name: "test-vs", Namespace: "default", ObjectGVK: kubernetes.VirtualServices}
+				_, result := kialiCache.Validations().Get(validationKey)
+				return result && len(kialiCache.Validations().Items()) == 0
+			}, timeout, interval).Should(BeTrue())
+		})
+	})
+})
diff --git a/tests/integration/tests/config_validations_test.go b/tests/integration/tests/config_validations_test.go
index 317bdf55c..fba894913 100644
--- a/tests/integration/tests/config_validations_test.go
+++ b/tests/integration/tests/config_validations_test.go
@@ -29,6 +29,11 @@ func TestDestinationRuleMultimatch(t *testing.T) {
 	require.NotNil(config)
 	assertConfigDetailsValidations(*config, kiali.BOOKINFO, kubernetes.DestinationRules, "all.googleapis.com", "KIA0201", true, require)
 
+	config, err = getConfigDetails(kiali.BOOKINFO, "all.googleapis.com2", kubernetes.DestinationRules, false, require)
+	require.NoError(err)
+	require.NotNil(config)
+	assertConfigDetailsValidations(*config, kiali.BOOKINFO, kubernetes.DestinationRules, "all.googleapis.com2", "KIA0201", true, require)
+
 	configList, err := kiali.IstioConfigsList(kiali.BOOKINFO)
 
 	require.NoError(err)
@@ -47,6 +52,11 @@ func TestDestinationRuleExportMultimatch(t *testing.T) {
 	require.NotNil(config)
 	assertConfigDetailsValidations(*config, kiali.BOOKINFO, kubernetes.DestinationRules, "all.googleapis.com", "KIA0201", true, require)
 
+	config, err = getConfigDetails(kiali.BOOKINFO, "all.googleapis.com2", kubernetes.DestinationRules, false, require)
+	require.NoError(err)
+	require.NotNil(config)
+	assertConfigDetailsValidations(*config, kiali.BOOKINFO, kubernetes.DestinationRules, "all.googleapis.com2", "KIA0201", true, require)
+
 	// load configs for all namespaces, single namespace config load will not return export to validations
 	// anyway Kiali UI does not load configs for a single namespace to avoid validation inconsistency and performance issues
 	configList, err := kiali.IstioConfigs()