Repository: kiali/kiali

Issue:
Refactor out global variables from business package
### What do you want to improve?

This is a "tech debt" issue. The business package has a number of global variables that must be set in order to use the package: https://github.com/kiali/kiali/blob/9a11e24d993ef34be0de7fbad3d6df946a530bfa/business/layer.go#L38-L44

Most of the api handlers call `business.Get` which uses these globals and then calls `NewWithBackends` that also uses these globals:
https://github.com/kiali/kiali/blob/9a11e24d993ef34be0de7fbad3d6df946a530bfa/business/layer.go#L83-L115

The problems for this are apparent when writing unit tests for and with the business package. When you write a test that doesn't set all the right globals, your test will panic and fail. It's not always obvious what you need to set and how. This makes it much harder to write tests and discourages folks from writing tests. Another issue is that having global variables lends itself to having global locks alongside those variables to prevent race conditions with multiple different actors trying to use those global variables.

### What is the new behavior?

Instead of having global variables in the business package, those variables can be passed into the business package when creating a new layer. Create a new function to create a `business.Layer` and migrate to the new method over time. Something like
```
NewLayer(cf kubernetes.ClientFactory, cache cache.KialiCache, conf config.Config, prom prometheus.ClientInterface) *Layer {
  	temporaryLayer := &Layer{}

        userClients := cf.GetClients()
	temporaryLayer.App = AppService{prom: prom, userClients: userClients, businessLayer: temporaryLayer}
        ...

	return temporaryLayer
```

Notice that almost all the arguments are interfaces so the "fake" implementations can easily be passed in testing.

The http handlers would need to pass these into the `NewLayer` function when they access the business layer. Although the handlers need to conform to `http.HandlerFunc`, it is a common pattern to wrap handler methods in a closure to pass dependencies into them. So this:
```
func AppList(w http.ResponseWriter, r *http.Request) {
  // Uses globals
  business.Get()
}
```
would become:
```
func AppList(cf kubernetes.ClientFactory, cache cache.KialiCache, conf config.Config, prom prometheus.ClientInterface) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    // Does not use globals
    business.NewLayer(cf, cache, conf, prom)
  }
}
```

Adding the `AppList` handler to the route becomes:
```
		{
			"AppList",
			"GET",
			"/api/namespaces/{namespace}/apps",
			handlers.AppList(cf, cache, conf, prom),
			true,
		},
```

Using this approach we can also migrate each handler individually rather than all at once.

Hints from issue comments:

Solution patch:
diff --git a/hack/README.adoc b/hack/README.adoc
index 703760666..a3728d7ca 100644
--- a/hack/README.adoc
+++ b/hack/README.adoc
@@ -37,7 +37,7 @@ Runs OpenShift on the local machine using link:https://github.com/code-ready/crc
 
 * `link:docker-io-auth.sh[]`
 
-Creates the necessary resources allowing your cluster to authenticate itself with docker.io when pulling Docker Hub images. This helps avoid the annoying docker.io rate limiting errors.
+Creates the necessary resources allowing your cluster to authenticate itself with Docker Hub when pulling Docker Hub images. This helps avoid the annoying Docker Hub rate limiting errors.
 
 * `link:fix_imports.sh[]`
 
diff --git a/hack/istio/bookinfo-traffic/bookinfo-service-entry.yaml b/hack/istio/bookinfo-traffic/bookinfo-service-entry.yaml
index 6536b8dcb..813f2ca5b 100644
--- a/hack/istio/bookinfo-traffic/bookinfo-service-entry.yaml
+++ b/hack/istio/bookinfo-traffic/bookinfo-service-entry.yaml
@@ -10,7 +10,7 @@
 # To create this simulated external ratings:v1 service, you must run a ratings service
 # by executing this command:
 #
-#   $ docker run -t -i --network=host docker.io/istio/examples-bookinfo-ratings-v1
+#   $ docker run -t -i --network=host quay.io/sail-dev/examples-bookinfo-ratings-v1
 #
 # At this point, you have a ratings service running outside of your local cluster bound
 # to your host's IP on port 9080. To test, make sure you get a response (albeit a 404)
diff --git a/hack/istio/install-bookinfo-demo.sh b/hack/istio/install-bookinfo-demo.sh
index 15a26e450..1467f0b0a 100755
--- a/hack/istio/install-bookinfo-demo.sh
+++ b/hack/istio/install-bookinfo-demo.sh
@@ -315,6 +315,7 @@ if [ "${MONGO_ENABLED}" == "true" ]; then
   MONGO_DB_YAML="${ISTIO_DIR}/samples/bookinfo/platform/kube/bookinfo-db.yaml"
   MONGO_SERVICE_YAML="${ISTIO_DIR}/samples/bookinfo/platform/kube/bookinfo-ratings-v2.yaml"
 
+  # Leaving these alone since they involve IBM arch testing. This is moving off docker.io anyway (https://github.com/kiali/kiali/issues/8278)
   if [ "${ARCH}" == "ppc64le" ]; then
     sed  "s;docker.io/istio.*;quay.io/maistra/examples-bookinfo-mongodb:2.0.0-ibm-p;g" ${MONGO_DB_YAML} > ${ISTIO_DIR}/samples/bookinfo/platform/kube/bookinfo-db-ppc64le.yaml
     MONGO_DB_YAML="${ISTIO_DIR}/samples/bookinfo/platform/kube/bookinfo-db-ppc64le.yaml"
@@ -352,6 +353,7 @@ if [ "${MYSQL_ENABLED}" == "true" ]; then
   MYSQL_DB_YAML="${ISTIO_DIR}/samples/bookinfo/platform/kube/bookinfo-mysql.yaml"
   MYSQL_SERVICE_YAML="${ISTIO_DIR}/samples/bookinfo/platform/kube/bookinfo-ratings-v2-mysql.yaml"
 
+  # Leaving these alone since they involve IBM arch testing. This is moving off docker.io anyway (https://github.com/kiali/kiali/issues/8278)
   if [ "${ARCH}" == "ppc64le" ]; then
     sed  "s;docker.io/istio.*;quay.io/maistra/examples-bookinfo-mysqldb:2.0.0-ibm-p;g" ${MYSQL_DB_YAML} > ${ISTIO_DIR}/samples/bookinfo/platform/kube/bookinfo-mysql-ppc64le.yaml
     MYSQL_DB_YAML="${ISTIO_DIR}/samples/bookinfo/platform/kube/bookinfo-mysql-ppc64le.yaml"
diff --git a/hack/istio/install-bookinfo-workload-entry.sh b/hack/istio/install-bookinfo-workload-entry.sh
index 2b4c744a7..309233f12 100755
--- a/hack/istio/install-bookinfo-workload-entry.sh
+++ b/hack/istio/install-bookinfo-workload-entry.sh
@@ -90,7 +90,7 @@ function create_workload_pod {
     inject_sidecar "${POD_NAME}" "${WORKLOAD_NAME}"
     sudo podman run --name ${WORKLOAD_NAME} \
         -d --rm --pod "${POD_NAME}" \
-        docker.io/istio/examples-bookinfo-${WORKLOAD_NAME}:latest
+        quay.io/sail-dev/examples-bookinfo-${WORKLOAD_NAME}:latest
     # Wait for pod to be running to ensure it has been assigned an IP address.
     sudo podman wait "${WORKLOAD_NAME}" --condition "running"
     cat ${TEMPLATE_DIR}/bookinfo-service-entry.yaml | \
diff --git a/hack/istio/install-istio-via-istioctl.sh b/hack/istio/install-istio-via-istioctl.sh
index a43eb3f30..bf7833753 100755
--- a/hack/istio/install-istio-via-istioctl.sh
+++ b/hack/istio/install-istio-via-istioctl.sh
@@ -247,7 +247,7 @@ Valid command line arguments:
   -ih|--image-hub <hub id>
        The hub where the Istio images will be pulled from.
        You can set this to "default" in order to use the default hub that the Istio charts use but
-       this may be using docker.io and docker hub rate limiting may cause the installation to fail.
+       this may be using Docker Hub, and Docker Hub rate limiting may cause the installation to fail.
        Default: gcr.io/istio-release
   -it|--image-tag <tag>
        The tag of the Istio images. Leave this as "default" (which means the default images are pulled)
diff --git a/hack/istio/multicluster/deploy-kiali.sh b/hack/istio/multicluster/deploy-kiali.sh
index ba2770e0e..2315de87b 100755
--- a/hack/istio/multicluster/deploy-kiali.sh
+++ b/hack/istio/multicluster/deploy-kiali.sh
@@ -210,7 +210,7 @@ deploy_kiali() {
   # If using openid auth strategy, create the keycloak realm and the kiali user.
 
   if [ "${KIALI_AUTH_STRATEGY}" == "openid" ]; then
-    # Get a token from keycloak to use the admin api
+    echo "## Kiali auth strategy is openid; get a token from keycloak at [${KEYCLOAK_ADDRESS}] to use the admin api"
     TOKEN_KEY=$(curl -k -X POST https://"${KEYCLOAK_ADDRESS}"/realms/master/protocol/openid-connect/token \
                 -d grant_type=password \
                 -d client_id=admin-cli \
diff --git a/hack/istio/multicluster/env.sh b/hack/istio/multicluster/env.sh
index 897fb3e91..754685641 100755
--- a/hack/istio/multicluster/env.sh
+++ b/hack/istio/multicluster/env.sh
@@ -57,7 +57,7 @@ ISTIO_NAMESPACE="${ISTIO_NAMESPACE:-istio-system}"
 # If you want to pull Istio images from a different image repository than what the hack script
 # will tell Istio to pull from, then set that hub name here. If you set this to "default",
 # the images will come from the repo that the Istio distro pulls from.
-# Suffice it to say, this setting is needed to avoid docker.io (and its throttling behavior) for
+# Suffice it to say, this setting is needed to avoid Docker Hub (and its throttling behavior) for
 # Istio production releases but allows you to run with Istio dev builds (set this to "default" for dev builds).
 ISTIO_HUB=""
 
diff --git a/hack/istio/multicluster/install-multi-primary.sh b/hack/istio/multicluster/install-multi-primary.sh
index 97d25209a..d0f9eab39 100755
--- a/hack/istio/multicluster/install-multi-primary.sh
+++ b/hack/istio/multicluster/install-multi-primary.sh
@@ -17,13 +17,14 @@ source ${SCRIPT_DIR}/env.sh $*
 install_istio() {
   local clustername="${1}"
   local network="${2}"
+  local pf_yaml="${3}"
   if [ ! -z "${ISTIO_TAG}" ]; then
     local image_tag_arg="--image-tag ${ISTIO_TAG}"
   fi
   if [ ! -z "${ISTIO_HUB}" ]; then
     local image_hub_arg="--image-hub ${ISTIO_HUB}"
   fi
-  "${ISTIO_INSTALL_SCRIPT}" ${image_tag_arg:-} ${image_hub_arg:-} --client-exe-path "${CLIENT_EXE}" --cluster-name "${clustername}" --istioctl "${ISTIOCTL}" --istio-dir "${ISTIO_DIR}" --mesh-id "${MESH_ID}" --namespace "${ISTIO_NAMESPACE}" --network "${network}"
+  "${ISTIO_INSTALL_SCRIPT}" -pf "${pf_yaml}"
   if [ "$?" != "0" ]; then
     echo "Failed to install Istio on cluster [${clustername}]"
     exit 1
@@ -93,8 +94,31 @@ create_remote_secret() {
   fi
 }
 
+MC_EAST_YAML=$(mktemp)
+cat <<EOF > "$MC_EAST_YAML"
+spec:
+  values:
+    global:
+      meshID: ${MESH_ID}
+      multiCluster:
+        clusterName: ${CLUSTER1_NAME}
+      network: ${NETWORK1_ID}
+EOF
+
+MC_WEST_YAML=$(mktemp)
+cat <<EOF > "$MC_WEST_YAML"
+spec:
+  values:
+    global:
+      meshID: ${MESH_ID}
+      multiCluster:
+        clusterName: ${CLUSTER2_NAME}
+      network: ${NETWORK2_ID}
+EOF
+
 # Find the hack script to be used to install istio
-ISTIO_INSTALL_SCRIPT="${SCRIPT_DIR}/../install-istio-via-istioctl.sh"
+ISTIO_INSTALL_SCRIPT="${SCRIPT_DIR}/../install-istio-via-sail.sh"
+
 if [ -x "${ISTIO_INSTALL_SCRIPT}" ]; then
   echo "Istio install script: ${ISTIO_INSTALL_SCRIPT}"
 else
@@ -211,11 +235,15 @@ source ${SCRIPT_DIR}/setup-ca.sh
 
 echo "==== INSTALL ISTIO ON CLUSTER #1 [${CLUSTER1_NAME}] - ${CLUSTER1_CONTEXT}"
 switch_cluster "${CLUSTER1_CONTEXT}" "${CLUSTER1_USER}" "${CLUSTER1_PASS}"
-install_istio "${CLUSTER1_NAME}" "${NETWORK1_ID}"
+install_istio "${CLUSTER1_NAME}" "${NETWORK1_ID}" "${MC_EAST_YAML}"
 
 echo "==== INSTALL ISTIO ON CLUSTER #2 [${CLUSTER2_NAME}] - ${CLUSTER2_CONTEXT}"
 switch_cluster "${CLUSTER2_CONTEXT}" "${CLUSTER2_USER}" "${CLUSTER2_PASS}"
-install_istio "${CLUSTER2_NAME}" "${NETWORK2_ID}"
+install_istio "${CLUSTER2_NAME}" "${NETWORK2_ID}" "${MC_WEST_YAML}"
+
+echo "==== DEPLOY ISTIO INGRESS GATEWAY ON CLUSTER #1 [${CLUSTER1_NAME}] - ${CLUSTER1_CONTEXT}"
+switch_cluster "${CLUSTER1_CONTEXT}" "${CLUSTER1_USER}" "${CLUSTER1_PASS}"
+kubectl apply -n istio-system -f "${SCRIPT_DIR}/../istio-gateway.yaml"
 
 if [ "${CROSSNETWORK_GATEWAY_REQUIRED}" == "true" ]; then
   echo "==== CREATE CROSSNETWORK GATEWAY ON CLUSTER #1 [${CLUSTER1_NAME}] - ${CLUSTER1_CONTEXT}"
diff --git a/hack/k8s-minikube.sh b/hack/k8s-minikube.sh
index 6a3721af8..ba8708335 100755
--- a/hack/k8s-minikube.sh
+++ b/hack/k8s-minikube.sh
@@ -168,7 +168,7 @@ EOF
 26c26
 <       - image: dexidp/dex:v2.27.0 #or quay.io/dexidp/dex:v2.26.0
 ---
->       - image: docker.io/dexidp/dex:${DEX_VERSION}
+>       - image: ghcr.io/dexidp/dex:${DEX_VERSION}
 41c41
 <         - name: GITHUB_CLIENT_ID
 ---
diff --git a/hack/run-integration-tests.sh b/hack/run-integration-tests.sh
index 2dc106841..193a8c3e4 100755
--- a/hack/run-integration-tests.sh
+++ b/hack/run-integration-tests.sh
@@ -12,8 +12,6 @@ FRONTEND_AMBIENT="frontend-ambient"
 FRONTEND_PRIMARY_REMOTE="frontend-primary-remote"
 FRONTEND_MULTI_PRIMARY="frontend-multi-primary"
 FRONTEND_TEMPO="frontend-tempo"
-#
-
 ISTIO_VERSION=""
 HELM_CHARTS_DIR=""
 TEST_SUITE="${BACKEND}"
diff --git a/handlers/istio_config.go b/handlers/istio_config.go
index 7c466c35b..41a41c902 100644
--- a/handlers/istio_config.go
+++ b/handlers/istio_config.go
@@ -421,3 +421,52 @@ func IstioConfigPermissions(
 		RespondWithJSON(w, http.StatusOK, istioConfigPermissions)
 	}
 }
+
+// IstioConfigValidationSummary is the API handler to fetch validations summary to be displayed.
+// It is related to all the Istio Objects within given namespaces
+func IstioConfigValidationSummary(
+	conf *config.Config,
+	kialiCache cache.KialiCache,
+	clientFactory kubernetes.ClientFactory,
+	prom prometheus.ClientInterface,
+	cpm business.ControlPlaneMonitor,
+	traceClientLoader func() tracing.ClientInterface,
+	grafana *grafana.Service,
+	discovery *istio.Discovery,
+) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		params := r.URL.Query()
+		namespaces := params.Get("namespaces") // csl of namespaces
+		nss := []string{}
+		if len(namespaces) > 0 {
+			nss = strings.Split(namespaces, ",")
+		}
+		cluster := clusterNameFromQuery(conf, params)
+
+		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
+		if err != nil {
+			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
+			return
+		}
+
+		if len(nss) == 0 {
+			loadedNamespaces, _ := business.Namespace.GetClusterNamespaces(r.Context(), cluster)
+			for _, ns := range loadedNamespaces {
+				nss = append(nss, ns.Name)
+			}
+		}
+
+		validationSummaries := []models.IstioValidationSummary{}
+		for _, ns := range nss {
+			istioConfigValidationResults, errValidations := business.Validations.GetValidationsForNamespace(r.Context(), cluster, ns)
+			if errValidations != nil {
+				log.Error(errValidations)
+				RespondWithError(w, http.StatusInternalServerError, errValidations.Error())
+				return
+			}
+			validationSummaries = append(validationSummaries, *istioConfigValidationResults.SummarizeValidation(ns, cluster))
+		}
+
+		RespondWithJSON(w, http.StatusOK, validationSummaries)
+	}
+}
diff --git a/handlers/namespaces.go b/handlers/namespaces.go
index e94013033..5ccfbe473 100644
--- a/handlers/namespaces.go
+++ b/handlers/namespaces.go
@@ -3,71 +3,89 @@ package handlers
 import (
 	"io"
 	"net/http"
-	"strings"
 
 	"github.com/gorilla/mux"
 
+	"github.com/kiali/kiali/business"
 	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/grafana"
 	"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/prometheus"
+	"github.com/kiali/kiali/tracing"
 )
 
-func NamespaceList(w http.ResponseWriter, r *http.Request) {
-	business, err := getBusiness(r)
-	if err != nil {
-		log.Error(err)
-		RespondWithError(w, http.StatusInternalServerError, err.Error())
-		return
-	}
+func NamespaceList(conf *config.Config, kialiCache cache.KialiCache, clientFactory kubernetes.ClientFactory, discovery istio.MeshDiscovery) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		userClients, err := getUserClients(r, clientFactory)
+		if err != nil {
+			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
+			return
+		}
 
-	namespaces, err := business.Namespace.GetNamespaces(r.Context())
-	if err != nil {
-		log.Error(err)
-		RespondWithError(w, http.StatusInternalServerError, err.Error())
-		return
-	}
+		namespace := business.NewNamespaceService(kialiCache, conf, discovery, clientFactory.GetSAClients(), userClients)
 
-	RespondWithJSON(w, http.StatusOK, namespaces)
-}
+		namespaces, err := namespace.GetNamespaces(r.Context())
+		if err != nil {
+			log.Error(err)
+			RespondWithError(w, http.StatusInternalServerError, err.Error())
+			return
+		}
 
-func NamespaceInfo(w http.ResponseWriter, r *http.Request) {
-	query := r.URL.Query()
-	vars := mux.Vars(r)
-	namespace := vars["namespace"]
-	cluster := clusterNameFromQuery(config.Get(), query)
-
-	business, err := getBusiness(r)
-	if err != nil {
-		log.Error(err)
-		RespondWithError(w, http.StatusInternalServerError, err.Error())
-		return
+		RespondWithJSON(w, http.StatusOK, namespaces)
 	}
+}
 
-	namespaceInfo, err := business.Namespace.GetClusterNamespace(r.Context(), namespace, cluster)
-	if err != nil {
-		log.Error(err)
-		RespondWithError(w, http.StatusInternalServerError, err.Error())
-		return
-	}
+func NamespaceInfo(conf *config.Config, cache cache.KialiCache, clientFactory kubernetes.ClientFactory, discovery istio.MeshDiscovery) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		query := r.URL.Query()
+		vars := mux.Vars(r)
+		namespace := vars["namespace"]
+		cluster := clusterNameFromQuery(conf, query)
+
+		userClients, err := getUserClients(r, clientFactory)
+		if err != nil {
+			RespondWithError(w, http.StatusInternalServerError, err.Error())
+			return
+		}
+
+		namespaceService := business.NewNamespaceService(cache, conf, discovery, clientFactory.GetSAClients(), userClients)
+		namespaceInfo, err := namespaceService.GetClusterNamespace(r.Context(), namespace, cluster)
+		if err != nil {
+			log.Error(err)
+			RespondWithError(w, http.StatusInternalServerError, err.Error())
+			return
+		}
 
-	RespondWithJSON(w, http.StatusOK, namespaceInfo)
+		RespondWithJSON(w, http.StatusOK, namespaceInfo)
+	}
 }
 
 // NamespaceValidationSummary is the API handler to fetch validations summary to be displayed.
 // It is related to all the Istio Objects within the namespace
-func NamespaceValidationSummary(discovery *istio.Discovery) http.HandlerFunc {
+func NamespaceValidationSummary(
+	conf *config.Config,
+	kialiCache cache.KialiCache,
+	clientFactory kubernetes.ClientFactory,
+	prom prometheus.ClientInterface,
+	cpm business.ControlPlaneMonitor,
+	traceClientLoader func() tracing.ClientInterface,
+	grafana *grafana.Service,
+	discovery *istio.Discovery,
+) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		query := r.URL.Query()
 		vars := mux.Vars(r)
 		namespace := vars["namespace"]
 
-		cluster := clusterNameFromQuery(config.Get(), query)
+		cluster := clusterNameFromQuery(conf, query)
 
-		business, err := getBusiness(r)
+		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
-			log.Error(err)
-			RespondWithError(w, http.StatusInternalServerError, err.Error())
+			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
 			return
 		}
 
@@ -100,68 +118,36 @@ func NamespaceValidationSummary(discovery *istio.Discovery) http.HandlerFunc {
 	}
 }
 
-// ConfigValidationSummary is the API handler to fetch validations summary to be displayed.
-// It is related to all the Istio Objects within given namespaces
-func ConfigValidationSummary(w http.ResponseWriter, r *http.Request) {
-	params := r.URL.Query()
-	namespaces := params.Get("namespaces") // csl of namespaces
-	nss := []string{}
-	if len(namespaces) > 0 {
-		nss = strings.Split(namespaces, ",")
-	}
-	cluster := clusterNameFromQuery(config.Get(), params)
-
-	business, err := getBusiness(r)
-	if err != nil {
-		log.Error(err)
-		RespondWithError(w, http.StatusInternalServerError, err.Error())
-		return
-	}
-
-	if len(nss) == 0 {
-		loadedNamespaces, _ := business.Namespace.GetClusterNamespaces(r.Context(), cluster)
-		for _, ns := range loadedNamespaces {
-			nss = append(nss, ns.Name)
-		}
-	}
-
-	validationSummaries := []models.IstioValidationSummary{}
-	for _, ns := range nss {
-		istioConfigValidationResults, errValidations := business.Validations.GetValidationsForNamespace(r.Context(), cluster, ns)
-		if errValidations != nil {
-			log.Error(errValidations)
-			RespondWithError(w, http.StatusInternalServerError, errValidations.Error())
+// NamespaceUpdate is the API to perform a patch on a Namespace configuration
+func NamespaceUpdate(conf *config.Config, kialiCache cache.KialiCache, clientFactory kubernetes.ClientFactory, discovery istio.MeshDiscovery) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		params := mux.Vars(r)
+		namespace := params["namespace"]
+		body, err := io.ReadAll(r.Body)
+		if err != nil {
+			RespondWithError(w, http.StatusBadRequest, "Update request with bad update patch: "+err.Error())
 			return
 		}
-		validationSummaries = append(validationSummaries, *istioConfigValidationResults.SummarizeValidation(ns, cluster))
-	}
+		defer r.Body.Close()
 
-	RespondWithJSON(w, http.StatusOK, validationSummaries)
-}
+		jsonPatch := string(body)
 
-// NamespaceUpdate is the API to perform a patch on a Namespace configuration
-func NamespaceUpdate(w http.ResponseWriter, r *http.Request) {
-	params := mux.Vars(r)
-	business, err := getBusiness(r)
-	if err != nil {
-		RespondWithError(w, http.StatusInternalServerError, "Namespace initialization error: "+err.Error())
-		return
-	}
-	namespace := params["namespace"]
-	body, err := io.ReadAll(r.Body)
-	if err != nil {
-		RespondWithError(w, http.StatusBadRequest, "Update request with bad update patch: "+err.Error())
-	}
-	jsonPatch := string(body)
+		query := r.URL.Query()
+		cluster := clusterNameFromQuery(conf, query)
 
-	query := r.URL.Query()
-	cluster := clusterNameFromQuery(config.Get(), query)
+		userClients, err := getUserClients(r, clientFactory)
+		if err != nil {
+			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
+			return
+		}
 
-	ns, err := business.Namespace.UpdateNamespace(r.Context(), namespace, jsonPatch, cluster)
-	if err != nil {
-		handleErrorResponse(w, err)
-		return
+		namespaceService := business.NewNamespaceService(kialiCache, conf, discovery, clientFactory.GetSAClients(), userClients)
+		ns, err := namespaceService.UpdateNamespace(r.Context(), namespace, jsonPatch, cluster)
+		if err != nil {
+			handleErrorResponse(w, err)
+			return
+		}
+		audit(r, "UPDATE on Namespace: "+namespace+" Patch: "+jsonPatch)
+		RespondWithJSON(w, http.StatusOK, ns)
 	}
-	audit(r, "UPDATE on Namespace: "+namespace+" Patch: "+jsonPatch)
-	RespondWithJSON(w, http.StatusOK, ns)
 }
diff --git a/handlers/proxy_logging.go b/handlers/proxy_logging.go
index 67b7d4358..501da2a69 100644
--- a/handlers/proxy_logging.go
+++ b/handlers/proxy_logging.go
@@ -9,43 +9,59 @@ import (
 
 	"github.com/kiali/kiali/business"
 	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/grafana"
+	"github.com/kiali/kiali/istio"
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/cache"
+	"github.com/kiali/kiali/prometheus"
+	"github.com/kiali/kiali/tracing"
 )
 
-func LoggingUpdate(w http.ResponseWriter, r *http.Request) {
-	params := mux.Vars(r)
+func LoggingUpdate(
+	conf *config.Config,
+	kialiCache cache.KialiCache,
+	clientFactory kubernetes.ClientFactory,
+	cpm business.ControlPlaneMonitor,
+	prom prometheus.ClientInterface,
+	traceClientLoader func() tracing.ClientInterface,
+	grafana *grafana.Service,
+	discovery istio.MeshDiscovery,
+) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		params := mux.Vars(r)
 
-	if config.Get().Deployment.ViewOnlyMode {
-		RespondWithError(w, http.StatusForbidden, "Log level cannot be changed in view-only mode")
-		return
-	}
+		if conf.Deployment.ViewOnlyMode {
+			RespondWithError(w, http.StatusForbidden, "Log level cannot be changed in view-only mode")
+			return
+		}
 
-	// Get business layer
-	businessLayer, err := getBusiness(r)
-	if err != nil {
-		RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
-		return
-	}
+		businessLayer, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
+		if err != nil {
+			RespondWithError(w, http.StatusInternalServerError, "Apps initialization error: "+err.Error())
+			return
+		}
 
-	namespace := params["namespace"]
-	pod := params["pod"]
-	query := r.URL.Query()
-	level := query.Get("level")
-	switch {
-	case level == "":
-		RespondWithError(w, 400, "level query param is not set")
-		return
-	case !business.IsValidProxyLogLevel(level):
-		msg := fmt.Sprintf("%s is an invalid log level. Valid log levels are: %s", level, strings.Join(business.ValidProxyLogLevels, ", "))
-		RespondWithError(w, 400, msg)
-		return
-	}
+		namespace := params["namespace"]
+		pod := params["pod"]
+		query := r.URL.Query()
+		level := query.Get("level")
+		switch {
+		case level == "":
+			RespondWithError(w, 400, "level query param is not set")
+			return
+		case !business.IsValidProxyLogLevel(level):
+			msg := fmt.Sprintf("%s is an invalid log level. Valid log levels are: %s", level, strings.Join(business.ValidProxyLogLevels, ", "))
+			RespondWithError(w, 400, msg)
+			return
+		}
 
-	cluster := clusterNameFromQuery(config.Get(), query)
+		cluster := clusterNameFromQuery(conf, query)
 
-	if err := businessLayer.ProxyLogging.SetLogLevel(cluster, namespace, pod, level); err != nil {
-		handleErrorResponse(w, err)
-		return
+		if err := businessLayer.ProxyLogging.SetLogLevel(cluster, namespace, pod, level); err != nil {
+			handleErrorResponse(w, err)
+			return
+		}
+		audit(r, "UPDATE Envoy log. Cluster: "+cluster+" Namespace: "+namespace+" Pod: "+pod+" Log level:"+level)
+		RespondWithCode(w, 200)
 	}
-	audit(r, "UPDATE Envoy log. Cluster: "+cluster+" Namespace: "+namespace+" Pod: "+pod+" Log level:"+level)
-	RespondWithCode(w, 200)
 }
diff --git a/routing/routes.go b/routing/routes.go
index d500c1907..508115d8f 100644
--- a/routing/routes.go
+++ b/routing/routes.go
@@ -721,7 +721,7 @@ func NewRoutes(
 			"NamespaceList",
 			"GET",
 			"/api/namespaces",
-			handlers.NamespaceList,
+			handlers.NamespaceList(conf, kialiCache, clientFactory, discovery),
 			true,
 		},
 		// swagger:route PATCH /namespaces/{namespace} namespaces namespaceUpdate
@@ -746,7 +746,7 @@ func NewRoutes(
 			"NamespaceUpdate",
 			"PATCH",
 			"/api/namespaces/{namespace}",
-			handlers.NamespaceUpdate,
+			handlers.NamespaceUpdate(conf, kialiCache, clientFactory, discovery),
 			true,
 		},
 		// swagger:route GET /namespaces/{namespace}/info namespaces namespaceInfo
@@ -766,7 +766,7 @@ func NewRoutes(
 			"NamespaceInfo",
 			"GET",
 			"/api/namespaces/{namespace}/info",
-			handlers.NamespaceInfo,
+			handlers.NamespaceInfo(conf, kialiCache, clientFactory, discovery),
 			true,
 		},
 		// swagger:route GET /namespaces/{namespace}/services/{service}/metrics services serviceMetrics
@@ -1060,7 +1060,7 @@ func NewRoutes(
 			"NamespaceValidationSummary",
 			"GET",
 			"/api/namespaces/{namespace}/validations",
-			handlers.NamespaceValidationSummary(discovery),
+			handlers.NamespaceValidationSummary(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
 			true,
 		},
 		// swagger:route GET /istio/validations namespaces namespacesValidations
@@ -1081,7 +1081,7 @@ func NewRoutes(
 			"ConfigValidationSummary",
 			"GET",
 			"/api/istio/validations",
-			handlers.ConfigValidationSummary,
+			handlers.IstioConfigValidationSummary(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
 			true,
 		},
 		// swagger:route GET /mesh/tls tls meshTls
@@ -1524,7 +1524,7 @@ func NewRoutes(
 			"PodProxyLogging",
 			"POST",
 			"/api/namespaces/{namespace}/pods/{pod}/logging",
-			handlers.LoggingUpdate,
+			handlers.LoggingUpdate(conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery),
 			true,
 		},
 		// swagger:route GET /clusters/metrics clusterName namespaces clustersMetrics



Test patch:
diff --git a/handlers/metrics_test.go b/handlers/metrics_test.go
index 6fcaee718..5b8e6b0e6 100644
--- a/handlers/metrics_test.go
+++ b/handlers/metrics_test.go
@@ -729,3 +729,158 @@ func setupWorkloadMetricsEndpoint(t *testing.T) (*httptest.Server, *prometheuste
 
 	return ts, xapi
 }
+
+// TestNamespaceMetricsDefault is unit test (testing request handling, not the prometheus client behaviour)
+func TestNamespaceMetricsDefault(t *testing.T) {
+	ts, api := setupNamespaceMetricsEndpoint(t)
+
+	url := ts.URL + "/api/namespaces/ns/metrics"
+	now := time.Now()
+	delta := 15 * time.Second
+	var gaugeSentinel uint32
+
+	api.SpyArgumentsAndReturnEmpty(func(args mock.Arguments) {
+		query := args[1].(string)
+		assert.IsType(t, prom_v1.Range{}, args[2])
+		r := args[2].(prom_v1.Range)
+		assert.Contains(t, query, "_namespace=\"ns\"")
+		assert.Contains(t, query, "[1m]")
+		assert.NotContains(t, query, "histogram_quantile")
+		atomic.AddUint32(&gaugeSentinel, 1)
+		assert.Equal(t, 15*time.Second, r.Step)
+		assert.WithinDuration(t, now, r.End, delta)
+		assert.WithinDuration(t, now.Add(-30*time.Minute), r.Start, delta)
+	})
+
+	resp, err := http.Get(url)
+	if err != nil {
+		t.Fatal(err)
+	}
+	actual, _ := io.ReadAll(resp.Body)
+
+	assert.NotEmpty(t, actual)
+	assert.Equal(t, 200, resp.StatusCode, string(actual))
+	// Assert branch coverage
+	assert.NotZero(t, gaugeSentinel)
+}
+
+func TestNamespaceMetricsWithParams(t *testing.T) {
+	ts, api := setupNamespaceMetricsEndpoint(t)
+
+	req, err := http.NewRequest("GET", ts.URL+"/api/namespaces/ns/metrics", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	q := req.URL.Query()
+	q.Add("rateInterval", "5h")
+	q.Add("rateFunc", "rate")
+	q.Add("step", "2")
+	q.Add("queryTime", "1523364075")
+	q.Add("duration", "1000")
+	q.Add("byLabels[]", "response_code")
+	q.Add("quantiles[]", "0.5")
+	q.Add("quantiles[]", "0.95")
+	q.Add("filters[]", "request_count")
+	q.Add("filters[]", "request_size")
+	req.URL.RawQuery = q.Encode()
+
+	queryTime := time.Unix(1523364075, 0)
+	delta := 2 * time.Second
+	var histogramSentinel, gaugeSentinel uint32
+
+	api.SpyArgumentsAndReturnEmpty(func(args mock.Arguments) {
+		query := args[1].(string)
+		assert.IsType(t, prom_v1.Range{}, args[2])
+		r := args[2].(prom_v1.Range)
+		assert.Contains(t, query, "rate(")
+		assert.Contains(t, query, "[5h]")
+		if strings.Contains(query, "histogram_quantile") {
+			// Histogram specific queries
+			assert.Contains(t, query, " by (le,response_code)")
+			assert.Contains(t, query, "istio_request_bytes")
+			atomic.AddUint32(&histogramSentinel, 1)
+		} else {
+			assert.Contains(t, query, " by (response_code)")
+			atomic.AddUint32(&gaugeSentinel, 1)
+		}
+		assert.Equal(t, 2*time.Second, r.Step)
+		assert.WithinDuration(t, queryTime, r.End, delta)
+		assert.WithinDuration(t, queryTime.Add(-1000*time.Second), r.Start, delta)
+	})
+
+	httpclient := &http.Client{}
+	resp, err := httpclient.Do(req)
+	if err != nil {
+		t.Fatal(err)
+	}
+	actual, _ := io.ReadAll(resp.Body)
+
+	assert.NotEmpty(t, actual)
+	assert.Equal(t, 200, resp.StatusCode, string(actual))
+	// Assert branch coverage
+	assert.NotZero(t, histogramSentinel)
+	assert.NotZero(t, gaugeSentinel)
+}
+
+func TestNamespaceMetricsInaccessibleNamespace(t *testing.T) {
+	ts, _ := setupNamespaceMetricsEndpoint(t)
+	k8s := kubetest.NewFakeK8sClient(kubetest.FakeNamespace("my_namespace"))
+	business.SetupBusinessLayer(t, &noPrivClient{k8s}, *config.NewConfig())
+
+	url := ts.URL + "/api/namespaces/my_namespace/metrics"
+
+	resp, err := http.Get(url)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
+}
+
+func setupNamespaceMetricsEndpoint(t *testing.T) (*httptest.Server, *prometheustest.PromAPIMock) {
+	client, xapi := setupMocked(t)
+
+	authInfo := map[string]*api.AuthInfo{config.Get().KubernetesConfig.ClusterName: {Token: "test"}}
+	mr := mux.NewRouter()
+	mr.HandleFunc("/api/namespaces/{namespace}/metrics", http.HandlerFunc(
+		WithAuthInfo(
+			authInfo,
+			NamespaceMetrics(func() (*prometheus.Client, error) { return client, nil }),
+		),
+	))
+
+	ts := httptest.NewServer(mr)
+	t.Cleanup(ts.Close)
+	return ts, xapi
+}
+
+// Setup mock
+
+func setupMocked(t *testing.T) (*prometheus.Client, *prometheustest.PromAPIMock) {
+	t.Helper()
+
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	k := kubetest.NewFakeK8sClient(
+		kubetest.FakeNamespace("bookinfo"),
+		kubetest.FakeNamespace("tutorial"),
+		kubetest.FakeNamespace("ns"),
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "bookinfo"}},
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "tutorial"}},
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "ns"}},
+	)
+	k.OpenShift = true
+
+	api := new(prometheustest.PromAPIMock)
+	client, err := prometheus.NewClient()
+	if err != nil {
+		t.Fatal(err)
+		return nil, nil
+	}
+	client.Inject(api)
+
+	business.SetupBusinessLayer(t, k, *conf)
+
+	return client, api
+}
diff --git a/handlers/namespaces_test.go b/handlers/namespaces_test.go
index 77cce3f31..7bbb9f470 100644
--- a/handlers/namespaces_test.go
+++ b/handlers/namespaces_test.go
@@ -1,142 +1,37 @@
-package handlers
+package handlers_test
 
 import (
 	"io"
 	"net/http"
 	"net/http/httptest"
-	"strings"
-	"sync/atomic"
 	"testing"
-	"time"
 
 	"github.com/gorilla/mux"
 	osproject_v1 "github.com/openshift/api/project/v1"
-	prom_v1 "github.com/prometheus/client_golang/api/prometheus/v1"
 	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
 	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/tools/clientcmd/api"
 
-	"github.com/kiali/kiali/business"
 	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/handlers"
+	"github.com/kiali/kiali/istio"
+	"github.com/kiali/kiali/kubernetes/cache"
 	"github.com/kiali/kiali/kubernetes/kubetest"
 	"github.com/kiali/kiali/prometheus"
 	"github.com/kiali/kiali/prometheus/prometheustest"
 )
 
-// TestNamespaceMetricsDefault is unit test (testing request handling, not the prometheus client behaviour)
-func TestNamespaceMetricsDefault(t *testing.T) {
-	ts, api := setupNamespaceMetricsEndpoint(t)
-
-	url := ts.URL + "/api/namespaces/ns/metrics"
-	now := time.Now()
-	delta := 15 * time.Second
-	var gaugeSentinel uint32
-
-	api.SpyArgumentsAndReturnEmpty(func(args mock.Arguments) {
-		query := args[1].(string)
-		assert.IsType(t, prom_v1.Range{}, args[2])
-		r := args[2].(prom_v1.Range)
-		assert.Contains(t, query, "_namespace=\"ns\"")
-		assert.Contains(t, query, "[1m]")
-		assert.NotContains(t, query, "histogram_quantile")
-		atomic.AddUint32(&gaugeSentinel, 1)
-		assert.Equal(t, 15*time.Second, r.Step)
-		assert.WithinDuration(t, now, r.End, delta)
-		assert.WithinDuration(t, now.Add(-30*time.Minute), r.Start, delta)
-	})
-
-	resp, err := http.Get(url)
-	if err != nil {
-		t.Fatal(err)
-	}
-	actual, _ := io.ReadAll(resp.Body)
-
-	assert.NotEmpty(t, actual)
-	assert.Equal(t, 200, resp.StatusCode, string(actual))
-	// Assert branch coverage
-	assert.NotZero(t, gaugeSentinel)
-}
-
-func TestNamespaceMetricsWithParams(t *testing.T) {
-	ts, api := setupNamespaceMetricsEndpoint(t)
-
-	req, err := http.NewRequest("GET", ts.URL+"/api/namespaces/ns/metrics", nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	q := req.URL.Query()
-	q.Add("rateInterval", "5h")
-	q.Add("rateFunc", "rate")
-	q.Add("step", "2")
-	q.Add("queryTime", "1523364075")
-	q.Add("duration", "1000")
-	q.Add("byLabels[]", "response_code")
-	q.Add("quantiles[]", "0.5")
-	q.Add("quantiles[]", "0.95")
-	q.Add("filters[]", "request_count")
-	q.Add("filters[]", "request_size")
-	req.URL.RawQuery = q.Encode()
-
-	queryTime := time.Unix(1523364075, 0)
-	delta := 2 * time.Second
-	var histogramSentinel, gaugeSentinel uint32
-
-	api.SpyArgumentsAndReturnEmpty(func(args mock.Arguments) {
-		query := args[1].(string)
-		assert.IsType(t, prom_v1.Range{}, args[2])
-		r := args[2].(prom_v1.Range)
-		assert.Contains(t, query, "rate(")
-		assert.Contains(t, query, "[5h]")
-		if strings.Contains(query, "histogram_quantile") {
-			// Histogram specific queries
-			assert.Contains(t, query, " by (le,response_code)")
-			assert.Contains(t, query, "istio_request_bytes")
-			atomic.AddUint32(&histogramSentinel, 1)
-		} else {
-			assert.Contains(t, query, " by (response_code)")
-			atomic.AddUint32(&gaugeSentinel, 1)
-		}
-		assert.Equal(t, 2*time.Second, r.Step)
-		assert.WithinDuration(t, queryTime, r.End, delta)
-		assert.WithinDuration(t, queryTime.Add(-1000*time.Second), r.Start, delta)
-	})
-
-	httpclient := &http.Client{}
-	resp, err := httpclient.Do(req)
-	if err != nil {
-		t.Fatal(err)
-	}
-	actual, _ := io.ReadAll(resp.Body)
-
-	assert.NotEmpty(t, actual)
-	assert.Equal(t, 200, resp.StatusCode, string(actual))
-	// Assert branch coverage
-	assert.NotZero(t, histogramSentinel)
-	assert.NotZero(t, gaugeSentinel)
-}
-
-func TestNamespaceMetricsInaccessibleNamespace(t *testing.T) {
-	ts, _ := setupNamespaceMetricsEndpoint(t)
-	k8s := kubetest.NewFakeK8sClient(kubetest.FakeNamespace("my_namespace"))
-	business.SetupBusinessLayer(t, &noPrivClient{k8s}, *config.NewConfig())
-
-	url := ts.URL + "/api/namespaces/my_namespace/metrics"
+func TestNamespaceInfo(t *testing.T) {
+	conf := config.NewConfig()
+	_, _, k8s := setupMocked(t)
 
-	resp, err := http.Get(url)
-	if err != nil {
-		t.Fatal(err)
-	}
+	cf := kubetest.NewFakeClientFactoryWithClient(conf, k8s)
+	cache := cache.NewTestingCacheWithFactory(t, cf, *conf)
+	discovery := istio.NewDiscovery(cf.Clients, cache, conf)
 
-	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
-}
+	handler := handlers.WithFakeAuthInfo(conf, handlers.NamespaceInfo(conf, cache, cf, discovery))
 
-func TestNamespaceInfo(t *testing.T) {
-	setupMocked(t)
-
-	authInfo := map[string]*api.AuthInfo{config.Get().KubernetesConfig.ClusterName: {Token: "test"}}
 	mr := mux.NewRouter()
-	mr.HandleFunc("/api/namespaces/{namespace}/info", WithAuthInfo(authInfo, NamespaceInfo))
+	mr.HandleFunc("/api/namespaces/{namespace}/info", handler)
 
 	ts := httptest.NewServer(mr)
 	t.Cleanup(ts.Close)
@@ -157,31 +52,9 @@ func TestNamespaceInfo(t *testing.T) {
 	assert.Equal(t, 200, resp.StatusCode, string(actual))
 }
 
-func setupNamespaceMetricsEndpoint(t *testing.T) (*httptest.Server, *prometheustest.PromAPIMock) {
-	client, xapi := setupMocked(t)
-
-	authInfo := map[string]*api.AuthInfo{config.Get().KubernetesConfig.ClusterName: {Token: "test"}}
-	mr := mux.NewRouter()
-	mr.HandleFunc("/api/namespaces/{namespace}/metrics", http.HandlerFunc(
-		WithAuthInfo(
-			authInfo,
-			NamespaceMetrics(func() (*prometheus.Client, error) { return client, nil }),
-		),
-	))
-
-	ts := httptest.NewServer(mr)
-	t.Cleanup(ts.Close)
-	return ts, xapi
-}
-
-// Setup mock
-
-func setupMocked(t *testing.T) (*prometheus.Client, *prometheustest.PromAPIMock) {
+func setupMocked(t *testing.T) (*prometheus.Client, *prometheustest.PromAPIMock, *kubetest.FakeK8sClient) {
 	t.Helper()
 
-	conf := config.NewConfig()
-	config.Set(conf)
-
 	k := kubetest.NewFakeK8sClient(
 		kubetest.FakeNamespace("bookinfo"),
 		kubetest.FakeNamespace("tutorial"),
@@ -196,11 +69,9 @@ func setupMocked(t *testing.T) (*prometheus.Client, *prometheustest.PromAPIMock)
 	client, err := prometheus.NewClient()
 	if err != nil {
 		t.Fatal(err)
-		return nil, nil
+		return nil, nil, nil
 	}
 	client.Inject(api)
 
-	business.SetupBusinessLayer(t, k, *conf)
-
-	return client, api
+	return client, api, k
 }
diff --git a/handlers/proxy_logging_test.go b/handlers/proxy_logging_test.go
index 90ed333e2..407508b40 100644
--- a/handlers/proxy_logging_test.go
+++ b/handlers/proxy_logging_test.go
@@ -1,43 +1,48 @@
-package handlers
+package handlers_test
 
 import (
 	"fmt"
 	"io"
-	"net/http"
 	"net/http/httptest"
 	"testing"
 
 	"github.com/gorilla/mux"
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/client-go/tools/clientcmd/api"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/kiali/kiali/business"
 	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/grafana"
+	"github.com/kiali/kiali/handlers"
+	"github.com/kiali/kiali/istio"
+	"github.com/kiali/kiali/kubernetes/cache"
 	"github.com/kiali/kiali/kubernetes/kubetest"
+	"github.com/kiali/kiali/prometheus/prometheustest"
+	"github.com/kiali/kiali/tracing"
 )
 
 func setupTestLoggingServer(t *testing.T, namespace, pod string) *httptest.Server {
+	conf := config.NewConfig()
+	k8s := kubetest.NewFakeK8sClient(
+		&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}},
+		&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: pod, Namespace: namespace}, Status: corev1.PodStatus{Phase: corev1.PodRunning}},
+	)
+	prom := new(prometheustest.PromClientMock)
+	cf := kubetest.NewFakeClientFactoryWithClient(conf, k8s)
+	cache := cache.NewTestingCacheWithFactory(t, cf, *conf)
+	discovery := istio.NewDiscovery(cf.Clients, cache, conf)
+	cpm := &business.FakeControlPlaneMonitor{}
+	traceLoader := func() tracing.ClientInterface { return nil }
+	grafana := grafana.NewService(conf, cf.GetSAHomeClusterClient())
+
+	handler := handlers.WithFakeAuthInfo(conf, handlers.LoggingUpdate(conf, cache, cf, cpm, prom, traceLoader, grafana, discovery))
 	mr := mux.NewRouter()
-	path := "/api/namespaces/{namespace}/pods/{pod}/logging"
-	authInfo := map[string]*api.AuthInfo{config.Get().KubernetesConfig.ClusterName: {Token: "test"}}
-	mr.HandleFunc(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		WithAuthInfo(authInfo, LoggingUpdate)(w, r)
-	}))
+	mr.HandleFunc("/api/namespaces/{namespace}/pods/{pod}/logging", handler)
 
 	ts := httptest.NewServer(mr)
 	t.Cleanup(ts.Close)
 
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("SetProxyLogLevel").Return(nil)
-	var fakePod *corev1.Pod
-	k8s.On("GetPod", namespace, pod).Return(fakePod, nil)
-
-	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
-	business.SetWithBackends(mockClientFactory, nil)
-
 	return ts
 }
 
diff --git a/kubernetes/kubetest/fake.go b/kubernetes/kubetest/fake.go
index 725060784..54c437ca6 100644
--- a/kubernetes/kubetest/fake.go
+++ b/kubernetes/kubetest/fake.go
@@ -167,5 +167,8 @@ func (c *FakeK8sClient) IsGatewayAPI() bool                 { return c.GatewayAP
 func (c *FakeK8sClient) IsIstioAPI() bool                   { return c.IstioAPIEnabled }
 func (c *FakeK8sClient) GetToken() string                   { return c.Token }
 func (c *FakeK8sClient) ClusterInfo() kialikube.ClusterInfo { return c.KubeClusterInfo }
+func (c *FakeK8sClient) SetProxyLogLevel(namespace string, podName string, level string) error {
+	return nil
+}
 
 var _ kialikube.ClientInterface = &FakeK8sClient{}