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 }