github.com/kiali/kiali@v1.84.0/business/namespaces.go (about)

     1  package business
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  	"sync"
     9  
    10  	osproject_v1 "github.com/openshift/api/project/v1"
    11  	core_v1 "k8s.io/api/core/v1"
    12  	"k8s.io/apimachinery/pkg/api/errors"
    13  	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/labels"
    15  
    16  	"github.com/kiali/kiali/config"
    17  	"github.com/kiali/kiali/kubernetes"
    18  	"github.com/kiali/kiali/kubernetes/cache"
    19  	"github.com/kiali/kiali/log"
    20  	"github.com/kiali/kiali/models"
    21  	"github.com/kiali/kiali/observability"
    22  )
    23  
    24  // NamespaceService deals with fetching k8sClients namespaces / OpenShift projects and convert to kiali model
    25  type NamespaceService struct {
    26  	conf                   *config.Config
    27  	hasProjects            bool
    28  	homeClusterUserClient  kubernetes.ClientInterface
    29  	isAccessibleNamespaces map[string]bool
    30  	kialiCache             cache.KialiCache
    31  	kialiSAClients         map[string]kubernetes.ClientInterface
    32  	userClients            map[string]kubernetes.ClientInterface
    33  }
    34  
    35  type AccessibleNamespaceError struct {
    36  	msg string
    37  }
    38  
    39  func (in *AccessibleNamespaceError) Error() string {
    40  	return in.msg
    41  }
    42  
    43  func IsAccessibleError(err error) bool {
    44  	_, isAccessibleError := err.(*AccessibleNamespaceError)
    45  	return isAccessibleError
    46  }
    47  
    48  func NewNamespaceService(userClients map[string]kubernetes.ClientInterface, kialiSAClients map[string]kubernetes.ClientInterface, cache cache.KialiCache, conf *config.Config) NamespaceService {
    49  	var hasProjects bool
    50  
    51  	homeClusterName := conf.KubernetesConfig.ClusterName
    52  	if saClient, ok := kialiSAClients[homeClusterName]; ok && saClient.IsOpenShift() {
    53  		hasProjects = true
    54  	} else {
    55  		hasProjects = false
    56  	}
    57  
    58  	ans := conf.Deployment.AccessibleNamespaces
    59  	isAccessibleNamespaces := make(map[string]bool, len(ans))
    60  	for _, ns := range ans {
    61  		isAccessibleNamespaces[ns] = true
    62  	}
    63  
    64  	return NamespaceService{
    65  		conf:                   conf,
    66  		hasProjects:            hasProjects,
    67  		homeClusterUserClient:  userClients[homeClusterName],
    68  		isAccessibleNamespaces: isAccessibleNamespaces,
    69  		kialiCache:             cache,
    70  		kialiSAClients:         kialiSAClients,
    71  		userClients:            userClients,
    72  	}
    73  }
    74  
    75  // GetClusterList Returns a list of cluster names based on the user clients
    76  func (in *NamespaceService) GetClusterList() []string {
    77  	var clusterList []string
    78  	for cluster := range in.userClients {
    79  		clusterList = append(clusterList, cluster)
    80  	}
    81  	return clusterList
    82  }
    83  
    84  // Returns a list of the given namespaces / projects
    85  func (in *NamespaceService) GetNamespaces(ctx context.Context) ([]models.Namespace, error) {
    86  	var end observability.EndFunc
    87  	_, end = observability.StartSpan(ctx, "GetNamespaces",
    88  		observability.Attribute("package", "business"),
    89  	)
    90  	defer end()
    91  
    92  	// kiali cache saves namespaces per token + cluster. The same token can be
    93  	// used for multiple clusters.
    94  	clustersToCheck := make(map[string]kubernetes.ClientInterface)
    95  	namespaces := []models.Namespace{}
    96  	for cluster, client := range in.userClients {
    97  		cachedNamespaces, found := in.kialiCache.GetNamespaces(cluster, client.GetToken())
    98  		if !found {
    99  			clustersToCheck[cluster] = client
   100  		} else {
   101  			namespaces = append(namespaces, cachedNamespaces...)
   102  		}
   103  	}
   104  
   105  	// Cache hit for all namespaces.
   106  	if len(clustersToCheck) == 0 {
   107  		return namespaces, nil
   108  	}
   109  
   110  	var discoverySelectors []*meta_v1.LabelSelector
   111  	homeClusterCache, err := in.kialiCache.GetKubeCache(in.conf.KubernetesConfig.ClusterName)
   112  	if err != nil {
   113  		log.Errorf("Will not process discoverySelectors due to a failure to get the Kiali cache: %v", err)
   114  	} else {
   115  		// determine what the discoverySelectors are by examining the Istio ConfigMap
   116  		if icm, err := homeClusterCache.GetConfigMap(in.conf.IstioNamespace, IstioConfigMapName(*in.conf, "")); err == nil {
   117  			if ic, err2 := kubernetes.GetIstioConfigMap(icm); err2 == nil {
   118  				discoverySelectors = ic.DiscoverySelectors
   119  			} else {
   120  				log.Errorf("Will not process discoverySelectors due to a failure to get the Istio ConfigMap: %v", err2)
   121  			}
   122  		} else {
   123  			log.Errorf("Will not process discoverySelectors due to a failure to parse the Istio ConfigMap: %v", err)
   124  		}
   125  	}
   126  	if len(discoverySelectors) > 0 {
   127  		log.Tracef("Istio discovery selectors: %+v", discoverySelectors)
   128  	} else {
   129  		log.Tracef("No Istio discovery selectors defined.")
   130  	}
   131  
   132  	// Let's explain the four different filters along with accessible namespaces (aka AN).
   133  	//
   134  	// First, we look at AN. AN is either ["**"] or it is not.
   135  	//
   136  	// If AN is ["**"], then the entire cluster of namespaces is accessible to Kiali.
   137  	// In this case, the user can further filter what namespaces this function should return using both includes and excludes.
   138  	// 1. LabelSelectorInclude is used to obtain an initial set of namespaces, if specified.
   139  	// 2. Added to that initial list will be the namespaces named in the Include list, if those namespaces actually exist.
   140  	// 3. If no LabelSelectorInclude or Include list is specified, then all namespaces are in the list.
   141  	// 4. Remove from that list those namespaces that match LabelSelectorExclude, as well as those namespaces found in the Exclude list.
   142  	// (Side note: You might ask: Why have an Include list when we already have the AN list?
   143  	// The difference is if you specify AN (not ["**"]), only those namespaces that exist __at install time__ will get a Role
   144  	// and hence are accessible to Kiali. The Include list is evaluated at the time this function is called, thus it
   145  	// allows Kiali to see those namespaces even if they are created after Kiali is installed).
   146  	//
   147  	// If AN is not ["**"], then only a subset of namespaces in the cluster is accessible to Kiali.
   148  	// When installed by the operator, Kiali will be given access to a set of namespaces (as defined in AN) via Roles that
   149  	// are created by the operator. Those namespaces that Kiali has access to (as defined in AN) will be labeled with the label
   150  	// selector defined in LabelSelectorInclude (Kiali CR "spec.api.namespaces.label_selector_include").
   151  	// 1. All of those namespaces are retrieved with the LabelSelectorInclude to obtain a set of namespaces.
   152  	// 2. Remove from that list those namespaces that match LabelSelectorExclude, as well as those namespaces found in the Exclude list.
   153  	// The Include option is ignored in this case - you cannot Include more namespaces over and above what AN specifies.
   154  	// (Side note 1: It probably doesn't make sense to set LabelSelectorExclude and Excludes when AN is not ["**"]. This is because
   155  	// you already have defined what namespaces you want to give Kiali access to (the AN list itself). However, for consistency,
   156  	// this function will still use those additional filters to filter out namespaces. So it is possible this function returns
   157  	// a subset of namespaces that are listed in AN.)
   158  	// (Side note 2: Notice the difference here between when AN is set to ["**"] and when it is not. When AN is not set to ["**"],
   159  	// LabelSelectorInclude does not tell the operator which namespaces are included - AN does that. Instead, the operator will
   160  	// create that label as defined by LabelSelectorInclude on each namespace defined in AN. Thus, after the operator installs
   161  	// Kiali, the Kiali Server can then use LabelSelectorInclude in this function in order to select all namespaces as defined in AN.
   162  	// If installed via the server helm chart, none of that is done, and it is up to the user to ensure
   163  	// LabelSelectorInclude (if defined) selects all namespaces in AN. It is a user-error if they do not configure that correctly.
   164  	// The server helm chart will not assume they did it correctly. The user therefore normally should not set LabelSelectorInclude
   165  	// if they also set AN to something not ["**"]. This is one reason why we recommend using the Kiali operator, and why we say
   166  	// the server helm chart is only provided as a convenience.)
   167  	// (Side note 3: The control plane namespace is always included via api.namespaces.include and
   168  	// never excluded via api.namespaces.exclude or api.namespaces.label_selector_exclude.)
   169  
   170  	// determine if we are to exclude namespaces by label - if so, set the label name and value for use later
   171  	labelSelectorExclude := in.conf.API.Namespaces.LabelSelectorExclude
   172  	var labelSelectorExcludeName string
   173  	var labelSelectorExcludeValue string
   174  	if labelSelectorExclude != "" {
   175  		excludeLabelList := strings.Split(labelSelectorExclude, "=")
   176  		if len(excludeLabelList) != 2 {
   177  			return nil, fmt.Errorf("api.namespaces.label_selector_exclude is invalid: %v", labelSelectorExclude)
   178  		}
   179  		labelSelectorExcludeName = excludeLabelList[0]
   180  		labelSelectorExcludeValue = excludeLabelList[1]
   181  	}
   182  
   183  	wg := &sync.WaitGroup{}
   184  	type result struct {
   185  		cluster string
   186  		ns      []models.Namespace
   187  		err     error
   188  	}
   189  	resultsCh := make(chan result)
   190  
   191  	// TODO: Use a context to define a timeout. The context should be passed to the k8s client
   192  	go func() {
   193  		for cluster := range clustersToCheck {
   194  			wg.Add(1)
   195  			go func(c string) {
   196  				defer wg.Done()
   197  				list, error := in.getNamespacesByCluster(ctx, c)
   198  				if error != nil {
   199  					resultsCh <- result{cluster: c, ns: nil, err: error}
   200  				} else {
   201  					resultsCh <- result{cluster: c, ns: list, err: nil}
   202  				}
   203  			}(cluster)
   204  		}
   205  		wg.Wait()
   206  		close(resultsCh)
   207  	}()
   208  
   209  	// Combine namespace data
   210  	for resultCh := range resultsCh {
   211  		if resultCh.err != nil {
   212  			if resultCh.cluster == in.conf.KubernetesConfig.ClusterName {
   213  				log.Errorf("Error fetching Namespaces for local cluster [%s]: %s", resultCh.cluster, resultCh.err)
   214  				return nil, resultCh.err
   215  			} else {
   216  				log.Infof("Error fetching Namespaces for cluster [%s]: %s", resultCh.cluster, resultCh.err)
   217  				continue
   218  			}
   219  		}
   220  		namespaces = append(namespaces, resultCh.ns...)
   221  	}
   222  
   223  	resultns := namespaces
   224  
   225  	// Filter out those namespaces that do not match discoverySelectors.
   226  	// Follow the semantics that Istio follows, which is:
   227  	//   If there is no discoverySelectors section in the config, skip this entirely.
   228  	//   If there is an empty discoverySelectors section, that means all namespaces are to be used.
   229  	//   If there are one or more discoverySelectors specified, the filter namespaces based on what they select.
   230  	if len(discoverySelectors) > 0 {
   231  		// 1. convert LabelSelectors to Selectors
   232  		selectors := make([]labels.Selector, 0)
   233  		for _, selector := range discoverySelectors {
   234  			ls, err := meta_v1.LabelSelectorAsSelector(selector)
   235  			if err != nil {
   236  				return nil, fmt.Errorf("error initializing discovery selectors filter, invalid discovery selector: %v", err)
   237  			}
   238  			selectors = append(selectors, ls)
   239  		}
   240  
   241  		// 2. range over all namespaces to get discovery namespaces, notice each selector result is ORed (as per Istio convention)
   242  		selectedNamespaces := make([]models.Namespace, 0)
   243  		for _, ns := range resultns {
   244  			if ns.Name == in.conf.IstioNamespace {
   245  				selectedNamespaces = append(selectedNamespaces, ns) // we always want to return the control plane namespace
   246  			} else {
   247  				for _, selector := range selectors {
   248  					if selector.Matches(labels.Set(ns.Labels)) {
   249  						selectedNamespaces = append(selectedNamespaces, ns)
   250  						break
   251  					}
   252  				}
   253  			}
   254  		}
   255  		namespaces = selectedNamespaces
   256  		resultns = namespaces
   257  	}
   258  
   259  	// exclude namespaces that are:
   260  	// 1. to be filtered out via the exclude list
   261  	// 2. to be filtered out via the label selector
   262  	// Note that the control plane namespace is never excluded
   263  	excludes := in.conf.API.Namespaces.Exclude
   264  	if len(excludes) > 0 || labelSelectorExclude != "" {
   265  		resultns = []models.Namespace{}
   266  	NAMESPACES:
   267  		for _, namespace := range namespaces {
   268  			if namespace.Name != in.conf.IstioNamespace {
   269  				if len(excludes) > 0 {
   270  					for _, excludePattern := range excludes {
   271  						if match, _ := regexp.MatchString(excludePattern, namespace.Name); match {
   272  							continue NAMESPACES
   273  						}
   274  					}
   275  				}
   276  				if labelSelectorExclude != "" {
   277  					if namespace.Labels[labelSelectorExcludeName] == labelSelectorExcludeValue {
   278  						continue NAMESPACES
   279  					}
   280  				}
   281  			}
   282  			resultns = append(resultns, namespace)
   283  		}
   284  	}
   285  
   286  	// store only the filtered set of namespaces in cache for the token
   287  	namespacesPerCluster := make(map[string][]models.Namespace)
   288  	for _, ns := range resultns {
   289  		namespacesPerCluster[ns.Cluster] = append(namespacesPerCluster[ns.Cluster], ns)
   290  	}
   291  	for cluster, ns := range namespacesPerCluster {
   292  		in.kialiCache.SetNamespaces(in.userClients[cluster].GetToken(), ns)
   293  	}
   294  
   295  	return resultns, nil
   296  }
   297  
   298  func (in *NamespaceService) getNamespacesByCluster(ctx context.Context, cluster string) ([]models.Namespace, error) {
   299  	configObject := in.conf
   300  
   301  	labelSelectorInclude := configObject.API.Namespaces.LabelSelectorInclude
   302  
   303  	var namespaces []models.Namespace
   304  	_, queryAllNamespaces := in.isAccessibleNamespaces["**"]
   305  	// If we are running in OpenShift, we will use the project names since these are the list of accessible namespaces
   306  	if in.hasProjects {
   307  		projects, err := in.userClients[cluster].GetProjects(ctx, labelSelectorInclude)
   308  		if err != nil {
   309  			return nil, err
   310  		}
   311  		if queryAllNamespaces {
   312  			namespaces = models.CastProjectCollection(projects, cluster)
   313  			// add the namespaces explicitly included in the include list.
   314  			includes := configObject.API.Namespaces.Include
   315  			if len(includes) > 0 {
   316  				var allNamespaces []models.Namespace
   317  				var seedNamespaces []models.Namespace
   318  
   319  				if labelSelectorInclude == "" {
   320  					// we have already retrieved all the namespaces, but we want only those in the Include list
   321  					allNamespaces = namespaces
   322  					seedNamespaces = make([]models.Namespace, 0)
   323  				} else {
   324  					// we have already got those namespaces that match the LabelSelectorInclude - that is our seed list.
   325  					// but we need ALL namespaces so we can look for more that match the Include list.
   326  					allProjectList, err := in.userClients[cluster].GetProjects(ctx, "")
   327  					if err != nil {
   328  						return nil, err
   329  					}
   330  
   331  					allNamespaces = models.CastProjectCollection(allProjectList, cluster)
   332  					seedNamespaces = namespaces
   333  				}
   334  				namespaces = in.addIncludedNamespaces(allNamespaces, seedNamespaces)
   335  			}
   336  		} else {
   337  			filteredProjects := make([]osproject_v1.Project, 0)
   338  			for _, project := range projects {
   339  				if _, isAccessible := in.isAccessibleNamespaces[project.Name]; isAccessible {
   340  					filteredProjects = append(filteredProjects, project)
   341  				}
   342  			}
   343  			namespaces = models.CastProjectCollection(filteredProjects, cluster)
   344  		}
   345  	} else {
   346  		// if the accessible namespaces define a distinct list of namespaces, use only those.
   347  		// If accessible namespaces include the special "**" (meaning all namespaces) ask k8sClients for them.
   348  		// Note that "**" requires cluster role permission to list all namespaces.
   349  		accessibleNamespaces := configObject.Deployment.AccessibleNamespaces
   350  		if queryAllNamespaces {
   351  
   352  			nss, err := in.userClients[cluster].GetNamespaces(labelSelectorInclude)
   353  			if err != nil {
   354  				// Fallback to using the Kiali service account, if needed
   355  				if errors.IsForbidden(err) {
   356  					if nss, err = in.getNamespacesUsingKialiSA(cluster, labelSelectorInclude, err); err != nil {
   357  						return nil, err
   358  					}
   359  				} else {
   360  					return nil, err
   361  				}
   362  			}
   363  
   364  			namespaces = models.CastNamespaceCollection(nss, cluster)
   365  
   366  			// add the namespaces explicitly included in the includes list.
   367  			includes := configObject.API.Namespaces.Include
   368  			if len(includes) > 0 {
   369  				var allNamespaces []models.Namespace
   370  				var seedNamespaces []models.Namespace
   371  
   372  				if labelSelectorInclude == "" {
   373  					// we have already retrieved all the namespaces, but we want only those in the Include list
   374  					allNamespaces = namespaces
   375  					seedNamespaces = make([]models.Namespace, 0)
   376  				} else {
   377  					// we have already got those namespaces that match the LabelSelectorInclude - that is our seed list.
   378  					// but we need ALL namespaces so we can look for more that match the Include list.
   379  					allK8sNamespaces, errGetNs := in.userClients[cluster].GetNamespaces("")
   380  					if errGetNs != nil {
   381  						// Fallback to using the Kiali service account, if needed
   382  						if errors.IsForbidden(errGetNs) {
   383  							if allK8sNamespaces, errGetNs = in.getNamespacesUsingKialiSA(cluster, "", errGetNs); errGetNs != nil {
   384  								return nil, errGetNs
   385  							}
   386  						} else {
   387  							return nil, errGetNs
   388  						}
   389  					}
   390  					allNamespaces = models.CastNamespaceCollection(allK8sNamespaces, cluster)
   391  					seedNamespaces = namespaces
   392  				}
   393  				namespaces = in.addIncludedNamespaces(allNamespaces, seedNamespaces)
   394  			}
   395  		} else {
   396  			k8sNamespaces := make([]core_v1.Namespace, 0)
   397  			for _, ans := range accessibleNamespaces {
   398  				k8sNs, err := in.userClients[cluster].GetNamespace(ans)
   399  				if err != nil {
   400  					if errors.IsNotFound(err) {
   401  						// If a namespace is not found, then we skip it from the list of namespaces
   402  						log.Warningf("Kiali has an accessible namespace [%s] which doesn't exist", ans)
   403  					} else if errors.IsForbidden(err) {
   404  						// Also, if namespace isn't readable, skip it.
   405  						log.Warningf("Kiali has an accessible namespace [%s] which is forbidden", ans)
   406  					} else {
   407  						// On any other error, abort and return the error.
   408  						return nil, err
   409  					}
   410  				} else {
   411  					k8sNamespaces = append(k8sNamespaces, *k8sNs)
   412  				}
   413  			}
   414  			namespaces = models.CastNamespaceCollection(k8sNamespaces, cluster)
   415  		}
   416  	}
   417  
   418  	return namespaces, nil
   419  }
   420  
   421  // GetClusterNamespaces is just a convenience routine that filters GetNamespaces for a particular cluster
   422  func (in *NamespaceService) GetClusterNamespaces(ctx context.Context, cluster string) ([]models.Namespace, error) {
   423  	tokenNamespaces, err := in.GetNamespaces(ctx)
   424  	if err != nil {
   425  		return nil, err
   426  	}
   427  
   428  	clusterNamespaces := []models.Namespace{}
   429  	for _, ns := range tokenNamespaces {
   430  		if ns.Cluster == cluster {
   431  			clusterNamespaces = append(clusterNamespaces, ns)
   432  		}
   433  	}
   434  
   435  	return clusterNamespaces, nil
   436  }
   437  
   438  // addIncludedNamespaces will look at all the namespaces and return all of them that match the Include list.
   439  // The returned results will be guaranteed to include the namespaces found in the given seed list.
   440  // There will be no duplicate namespaces in the returned list.
   441  func (in *NamespaceService) addIncludedNamespaces(all []models.Namespace, seed []models.Namespace) []models.Namespace {
   442  	var controlPlaneNamespace models.Namespace
   443  	hasNamespace := make(map[string]bool, len(seed))
   444  	results := make([]models.Namespace, 0, len(seed))
   445  	configObject := in.conf
   446  
   447  	// seed with the initial set of namespaces - this ensures there are no duplicates in the seed list
   448  	for _, ns := range seed {
   449  		if _, exists := hasNamespace[ns.Name]; !exists {
   450  			hasNamespace[ns.Name] = true
   451  			results = append(results, ns)
   452  		}
   453  	}
   454  
   455  	// go through the list of all namespaces and add to the results list those that match a regex found in the Include list
   456  	includes := configObject.API.Namespaces.Include
   457  NAMESPACES:
   458  	for _, ns := range all {
   459  		if _, exists := hasNamespace[ns.Name]; exists {
   460  			continue
   461  		}
   462  		for _, includePattern := range includes {
   463  			if match, _ := regexp.MatchString(includePattern, ns.Name); match {
   464  				hasNamespace[ns.Name] = true
   465  				results = append(results, ns)
   466  				continue NAMESPACES
   467  			}
   468  		}
   469  		if ns.Name == configObject.IstioNamespace {
   470  			controlPlaneNamespace = ns // squirrel away the control plane namepace in case we need to add it
   471  		}
   472  	}
   473  
   474  	// Kiali needs the control plane namespace, so it should always be included.
   475  	// If the user did not configure the include list to explicitly include the control plane namespace, then we need to include it now.
   476  	if _, exists := hasNamespace[configObject.IstioNamespace]; !exists {
   477  		if controlPlaneNamespace.Name != "" {
   478  			results = append(results, controlPlaneNamespace)
   479  		} else {
   480  			log.Errorf("Kiali needs to include the control plane namespace. Make sure you configured Kiali so it can access and include the namespace [%s].", configObject.IstioNamespace)
   481  		}
   482  	}
   483  	return results
   484  }
   485  
   486  func (in *NamespaceService) isAccessibleNamespace(namespace string) bool {
   487  	_, queryAllNamespaces := in.isAccessibleNamespaces["**"]
   488  	if queryAllNamespaces {
   489  		return true
   490  	}
   491  	_, isAccessible := in.isAccessibleNamespaces[namespace]
   492  	return isAccessible
   493  }
   494  
   495  func (in *NamespaceService) isExcludedNamespace(namespace string) bool {
   496  	configObject := in.conf
   497  	excludes := configObject.API.Namespaces.Exclude
   498  	if len(excludes) == 0 {
   499  		return false
   500  	}
   501  	if namespace == configObject.IstioNamespace {
   502  		return false // the control plane namespace is never excluded
   503  	}
   504  	for _, excludePattern := range excludes {
   505  		if match, _ := regexp.MatchString(excludePattern, namespace); match {
   506  			return true
   507  		}
   508  	}
   509  	return false
   510  }
   511  
   512  func (in *NamespaceService) isIncludedNamespace(namespace string) bool {
   513  	_, queryAllNamespaces := in.isAccessibleNamespaces["**"]
   514  	if !queryAllNamespaces {
   515  		return true // Include list is ignored if accessible namespaces is not **; for our purposes, when ignored we assume the Include list includes all.
   516  	}
   517  
   518  	configObject := in.conf
   519  	if namespace == configObject.IstioNamespace {
   520  		return true // the control plane namespace is always included
   521  	}
   522  
   523  	includes := configObject.API.Namespaces.Include
   524  	if len(includes) == 0 {
   525  		return true // if no Include list is specified, all namespaces are included
   526  	}
   527  	for _, includePattern := range includes {
   528  		if match, _ := regexp.MatchString(includePattern, namespace); match {
   529  			return true
   530  		}
   531  	}
   532  	return false
   533  }
   534  
   535  // GetNamespaceClusters is a convenience routine that filters GetNamespaces for a particular namespace
   536  func (in *NamespaceService) GetNamespaceClusters(ctx context.Context, namespace string) ([]models.Namespace, error) {
   537  	namespaces, err := in.GetNamespaces(ctx)
   538  	if err != nil {
   539  		return nil, err
   540  	}
   541  
   542  	result := []models.Namespace{}
   543  	for _, ns := range namespaces {
   544  		if ns.Name == namespace {
   545  			result = append(result, ns)
   546  		}
   547  	}
   548  
   549  	return result, nil
   550  }
   551  
   552  // GetClusterNamespace returns the definition of the specified namespace.
   553  func (in *NamespaceService) GetClusterNamespace(ctx context.Context, namespace string, cluster string) (*models.Namespace, error) {
   554  	var end observability.EndFunc
   555  	_, end = observability.StartSpan(ctx, "GetClusterNamespace",
   556  		observability.Attribute("package", "business"),
   557  		observability.Attribute("namespace", namespace),
   558  		observability.Attribute("cluster", cluster),
   559  	)
   560  	defer end()
   561  
   562  	client, ok := in.userClients[cluster]
   563  	if !ok {
   564  		return nil, fmt.Errorf("cluster [%s] is not found or is not accessible for Kiali", cluster)
   565  	}
   566  
   567  	// Cache already has included/excluded namespaces applied
   568  	if ns, found := in.kialiCache.GetNamespace(cluster, client.GetToken(), namespace); found {
   569  		return &ns, nil
   570  	}
   571  
   572  	if !in.isAccessibleNamespace(namespace) {
   573  		return nil, &AccessibleNamespaceError{msg: "Namespace [" + namespace + "] is not accessible for Kiali"}
   574  	}
   575  
   576  	if !in.isIncludedNamespace(namespace) {
   577  		return nil, &AccessibleNamespaceError{msg: "Namespace [" + namespace + "] is not included for Kiali"}
   578  	}
   579  
   580  	if in.isExcludedNamespace(namespace) {
   581  		return nil, &AccessibleNamespaceError{msg: "Namespace [" + namespace + "] is excluded for Kiali"}
   582  	}
   583  
   584  	var result models.Namespace
   585  	if in.hasProjects {
   586  		project, err := client.GetProject(ctx, namespace)
   587  		if err != nil {
   588  			return nil, err
   589  		}
   590  		result = models.CastProject(*project, cluster)
   591  	} else {
   592  		ns, err := client.GetNamespace(namespace)
   593  		if err != nil {
   594  			return nil, err
   595  		}
   596  		result = models.CastNamespace(*ns, cluster)
   597  	}
   598  
   599  	// Refresh namespace in cache since we've just fetched it from the API.
   600  	if _, err := in.GetClusterNamespaces(ctx, cluster); err != nil {
   601  		log.Errorf("Unable to refresh cache for cluster [%s]: %s", cluster, err)
   602  	}
   603  
   604  	return &result, nil
   605  }
   606  
   607  func (in *NamespaceService) UpdateNamespace(ctx context.Context, namespace string, jsonPatch string, cluster string) (*models.Namespace, error) {
   608  	var end observability.EndFunc
   609  	ctx, end = observability.StartSpan(ctx, "UpdateNamespace",
   610  		observability.Attribute("package", "business"),
   611  		observability.Attribute("namespace", namespace),
   612  		observability.Attribute("jsonPatch", jsonPatch),
   613  	)
   614  	defer end()
   615  
   616  	// A first check to run the accessible/excluded logic and not run the Update operation on filtered namespaces
   617  	_, err := in.GetClusterNamespace(ctx, namespace, cluster)
   618  	if err != nil {
   619  		return nil, err
   620  	}
   621  
   622  	userClient, found := in.userClients[cluster]
   623  	if !found {
   624  		return nil, fmt.Errorf("cluster [%s] is not found or is not accessible for Kiali", cluster)
   625  	}
   626  
   627  	if _, err := userClient.UpdateNamespace(namespace, jsonPatch); err != nil {
   628  		return nil, err
   629  	}
   630  
   631  	// Cache is stopped after a Create/Update/Delete operation to force a refresh
   632  	kubeCache, err := in.kialiCache.GetKubeCache(cluster)
   633  	if err != nil {
   634  		return nil, err
   635  	}
   636  	kubeCache.Refresh(namespace)
   637  	in.kialiCache.RefreshTokenNamespaces(cluster)
   638  
   639  	// Call GetClusterNamespaces to update the cache for this cluster.
   640  	if _, err := in.GetClusterNamespaces(ctx, cluster); err != nil {
   641  		return nil, err
   642  	}
   643  
   644  	return in.GetClusterNamespace(ctx, namespace, cluster)
   645  }
   646  
   647  func (in *NamespaceService) getNamespacesUsingKialiSA(cluster string, labelSelector string, forwardedError error) ([]core_v1.Namespace, error) {
   648  	// Check if we already are using the Kiali ServiceAccount token. If we are, no need to do further processing, since
   649  	// this would just circle back to the same results.
   650  	kialiToken := in.kialiSAClients[cluster].GetToken()
   651  	if in.userClients[cluster].GetToken() == kialiToken {
   652  		return nil, forwardedError
   653  	}
   654  
   655  	// Let's get the namespaces list using the Kiali Service Account
   656  	nss, err := in.kialiSAClients[cluster].GetNamespaces(labelSelector)
   657  	if err != nil {
   658  		return nil, err
   659  	}
   660  
   661  	// Only take namespaces where the user has privileges
   662  	var namespaces []core_v1.Namespace
   663  	for _, item := range nss {
   664  		if _, getNsErr := in.userClients[cluster].GetNamespace(item.Name); getNsErr == nil {
   665  			// Namespace is accessible
   666  			namespaces = append(namespaces, item)
   667  		} else if !errors.IsForbidden(getNsErr) {
   668  			// Since the returned error is NOT "forbidden", something bad happened
   669  			return nil, getNsErr
   670  		}
   671  	}
   672  
   673  	// Return the list of namespaces where the user has the 'get namespace' read privilege.
   674  	return namespaces, nil
   675  }