istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/bug-report/pkg/cluster/cluster.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cluster 16 17 import ( 18 "context" 19 "fmt" 20 "regexp" 21 "strings" 22 23 appsv1 "k8s.io/api/apps/v1" 24 corev1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/client-go/kubernetes" 27 28 "istio.io/istio/operator/pkg/name" 29 "istio.io/istio/pkg/kube/inject" 30 "istio.io/istio/tools/bug-report/pkg/common" 31 config2 "istio.io/istio/tools/bug-report/pkg/config" 32 "istio.io/istio/tools/bug-report/pkg/util/path" 33 ) 34 35 var versionRegex = regexp.MustCompile(`.*(\d\.\d\.\d).*`) 36 37 // ParsePath parses path into its components. Input must have the form namespace/deployment/pod/container. 38 func ParsePath(path string) (namespace string, deployment, pod string, container string, err error) { 39 pv := strings.Split(path, "/") 40 if len(pv) != 4 { 41 return "", "", "", "", fmt.Errorf("bad path %s, must be namespace/deployment/pod/container", path) 42 } 43 return pv[0], pv[1], pv[2], pv[3], nil 44 } 45 46 // shouldSkip means that current pod should be skip or not based on given --include and --exclude 47 func shouldSkipPod(pod *corev1.Pod, config *config2.BugReportConfig) bool { 48 for _, eld := range config.Exclude { 49 if len(eld.Namespaces) > 0 { 50 if isIncludeOrExcludeEntriesMatched(eld.Namespaces, pod.Namespace) { 51 return true 52 } 53 } 54 if len(eld.Pods) > 0 { 55 if isIncludeOrExcludeEntriesMatched(eld.Pods, pod.Name) { 56 return true 57 } 58 } 59 if len(eld.Containers) > 0 { 60 for _, c := range pod.Spec.Containers { 61 if isIncludeOrExcludeEntriesMatched(eld.Containers, c.Name) { 62 return true 63 } 64 } 65 } 66 if len(eld.Labels) > 0 { 67 for key, val := range eld.Labels { 68 if evLabel, exists := pod.Labels[key]; exists { 69 if isExactMatchedOrPatternMatched(val, evLabel) { 70 return true 71 } 72 } 73 } 74 } 75 if len(eld.Annotations) > 0 { 76 for key, val := range eld.Annotations { 77 if evAnnotation, exists := pod.Annotations[key]; exists { 78 if isExactMatchedOrPatternMatched(val, evAnnotation) { 79 return true 80 } 81 } 82 } 83 } 84 } 85 86 for _, ild := range config.Include { 87 if len(ild.Namespaces) > 0 { 88 if !isIncludeOrExcludeEntriesMatched(ild.Namespaces, pod.Namespace) { 89 continue 90 } 91 } 92 if len(ild.Pods) > 0 { 93 if !isIncludeOrExcludeEntriesMatched(ild.Pods, pod.Name) { 94 continue 95 } 96 } 97 98 if len(ild.Containers) > 0 { 99 isContainerMatch := false 100 for _, c := range pod.Spec.Containers { 101 if isIncludeOrExcludeEntriesMatched(ild.Containers, c.Name) { 102 isContainerMatch = true 103 } 104 } 105 if !isContainerMatch { 106 continue 107 } 108 } 109 110 if len(ild.Labels) > 0 { 111 isLabelsMatch := false 112 for key, val := range ild.Labels { 113 if evLabel, exists := pod.Labels[key]; exists { 114 if isExactMatchedOrPatternMatched(val, evLabel) { 115 isLabelsMatch = true 116 break 117 } 118 } 119 } 120 if !isLabelsMatch { 121 continue 122 } 123 } 124 125 if len(ild.Annotations) > 0 { 126 isAnnotationMatch := false 127 for key, val := range ild.Annotations { 128 if evAnnotation, exists := pod.Annotations[key]; exists { 129 if isExactMatchedOrPatternMatched(val, evAnnotation) { 130 isAnnotationMatch = true 131 break 132 } 133 } 134 } 135 if !isAnnotationMatch { 136 continue 137 } 138 } 139 // If we reach here, it means that all include entries are matched. 140 return false 141 } 142 // If we reach here, it means that no include entries are matched. 143 return true 144 } 145 146 func shouldSkipDeployment(deployment string, config *config2.BugReportConfig) bool { 147 for _, eld := range config.Exclude { 148 if len(eld.Deployments) > 0 { 149 if isIncludeOrExcludeEntriesMatched(eld.Deployments, deployment) { 150 return true 151 } 152 } 153 } 154 155 for _, ild := range config.Include { 156 if len(ild.Deployments) > 0 { 157 if !isIncludeOrExcludeEntriesMatched(ild.Deployments, deployment) { 158 return true 159 } 160 } 161 } 162 163 return false 164 } 165 166 func shouldSkipDaemonSet(daemonSet string, config *config2.BugReportConfig) bool { 167 for _, eld := range config.Exclude { 168 if len(eld.Daemonsets) > 0 { 169 if isIncludeOrExcludeEntriesMatched(eld.Daemonsets, daemonSet) { 170 return true 171 } 172 } 173 } 174 175 for _, ild := range config.Include { 176 if len(ild.Daemonsets) > 0 { 177 if !isIncludeOrExcludeEntriesMatched(ild.Daemonsets, daemonSet) { 178 return true 179 } 180 } 181 } 182 return false 183 } 184 185 func isExactMatchedOrPatternMatched(pattern string, term string) bool { 186 result, _ := regexp.MatchString(entryPatternToRegexp(pattern), term) 187 return result 188 } 189 190 func isIncludeOrExcludeEntriesMatched(entries []string, term string) bool { 191 for _, entry := range entries { 192 if isExactMatchedOrPatternMatched(entry, term) { 193 return true 194 } 195 } 196 return false 197 } 198 199 func entryPatternToRegexp(pattern string) string { 200 var reg string 201 for i, literal := range strings.Split(pattern, "*") { 202 if i > 0 { 203 reg += ".*" 204 } 205 reg += regexp.QuoteMeta(literal) 206 } 207 return reg 208 } 209 210 // GetClusterResources returns cluster resources for the given REST config and k8s Clientset. 211 func GetClusterResources(ctx context.Context, clientset *kubernetes.Clientset, config *config2.BugReportConfig) (*Resources, error) { 212 out := &Resources{ 213 Labels: make(map[string]map[string]string), 214 Annotations: make(map[string]map[string]string), 215 Pod: make(map[string]*corev1.Pod), 216 CniPod: make(map[string]*corev1.Pod), 217 } 218 219 pods, err := clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) 220 if err != nil { 221 return nil, err 222 } 223 224 replicasets, err := clientset.AppsV1().ReplicaSets("").List(ctx, metav1.ListOptions{}) 225 if err != nil { 226 return nil, err 227 } 228 229 daemonsets, err := clientset.AppsV1().DaemonSets("").List(ctx, metav1.ListOptions{}) 230 if err != nil { 231 return nil, err 232 } 233 234 for i, p := range pods.Items { 235 if p.Labels["k8s-app"] == "istio-cni-node" { 236 out.CniPod[PodKey(p.Namespace, p.Name)] = &pods.Items[i] 237 } 238 239 if inject.IgnoredNamespaces.Contains(p.Namespace) { 240 continue 241 } 242 if skip := shouldSkipPod(&p, config); skip { 243 continue 244 } 245 246 deployment := getOwnerDeployment(&p, replicasets.Items) 247 if skip := shouldSkipDeployment(deployment, config); skip { 248 continue 249 } 250 daemonset := getOwnerDaemonSet(&p, daemonsets.Items) 251 if skip := shouldSkipDaemonSet(daemonset, config); skip { 252 continue 253 } 254 255 if deployment != "" { 256 for _, c := range p.Spec.Containers { 257 out.insertContainer(p.Namespace, deployment, p.Name, c.Name) 258 } 259 for _, c := range p.Spec.InitContainers { 260 if c.Name == inject.ProxyContainerName { 261 out.insertContainer(p.Namespace, deployment, p.Name, c.Name) 262 } 263 } 264 } else if daemonset != "" { 265 for _, c := range p.Spec.Containers { 266 out.insertContainer(p.Namespace, daemonset, p.Name, c.Name) 267 } 268 for _, c := range p.Spec.InitContainers { 269 if c.Name == inject.ProxyContainerName { 270 out.insertContainer(p.Namespace, deployment, p.Name, c.Name) 271 } 272 } 273 } 274 275 out.Labels[PodKey(p.Namespace, p.Name)] = p.Labels 276 out.Annotations[PodKey(p.Namespace, p.Name)] = p.Annotations 277 out.Pod[PodKey(p.Namespace, p.Name)] = &pods.Items[i] 278 } 279 280 return out, nil 281 } 282 283 // Resources defines a tree of cluster resource names. 284 type Resources struct { 285 // Root is the first level in the cluster resource hierarchy. 286 // Each level in the hierarchy is a map[string]interface{} to the next level. 287 // The levels are: namespaces/deployments/pods/containers. 288 Root map[string]any 289 // Labels maps a pod name to a map of labels key-values. 290 Labels map[string]map[string]string 291 // Annotations maps a pod name to a map of annotation key-values. 292 Annotations map[string]map[string]string 293 // Pod maps a pod name to its Pod info. The key is namespace/pod-name. 294 Pod map[string]*corev1.Pod 295 // CniPod 296 CniPod map[string]*corev1.Pod 297 } 298 299 func (r *Resources) insertContainer(namespace, deployment, pod, container string) { 300 if r.Root == nil { 301 r.Root = make(map[string]any) 302 } 303 if r.Root[namespace] == nil { 304 r.Root[namespace] = make(map[string]any) 305 } 306 d := r.Root[namespace].(map[string]any) 307 if d[deployment] == nil { 308 d[deployment] = make(map[string]any) 309 } 310 p := d[deployment].(map[string]any) 311 if p[pod] == nil { 312 p[pod] = make(map[string]any) 313 } 314 c := p[pod].(map[string]any) 315 c[container] = nil 316 } 317 318 // ContainerRestarts returns the number of container restarts for the given container. 319 func (r *Resources) ContainerRestarts(namespace, pod, container string, isCniPod bool) int { 320 var podItem *corev1.Pod 321 if isCniPod { 322 podItem = r.CniPod[PodKey(namespace, pod)] 323 } else { 324 podItem = r.Pod[PodKey(namespace, pod)] 325 } 326 for _, cs := range podItem.Status.ContainerStatuses { 327 if cs.Name == container { 328 return int(cs.RestartCount) 329 } 330 } 331 return 0 332 } 333 334 // IsDiscoveryContainer reports whether the given container is the Istio discovery container. 335 func (r *Resources) IsDiscoveryContainer(clusterVersion, namespace, pod, container string) bool { 336 return common.IsDiscoveryContainer(clusterVersion, container, r.Labels[PodKey(namespace, pod)]) 337 } 338 339 // PodIstioVersion returns the Istio version for the given pod, if either the proxy or discovery are one of its 340 // containers and the tag is in a parseable format. 341 func (r *Resources) PodIstioVersion(namespace, pod string) string { 342 p := r.Pod[PodKey(namespace, pod)] 343 if p == nil { 344 return "" 345 } 346 347 for _, c := range p.Spec.Containers { 348 if c.Name == common.ProxyContainerName || c.Name == common.DiscoveryContainerName { 349 return imageToVersion(c.Image) 350 } 351 } 352 return "" 353 } 354 355 // String implements the Stringer interface. 356 func (r *Resources) String() string { 357 return resourcesStringImpl(r.Root, "") 358 } 359 360 func resourcesStringImpl(node any, prefix string) string { 361 out := "" 362 if node == nil { 363 return "" 364 } 365 nv := node.(map[string]any) 366 for k, n := range nv { 367 out += prefix + k + "\n" 368 out += resourcesStringImpl(n, prefix+" ") 369 } 370 371 return out 372 } 373 374 // PodKey returns a unique key based on the namespace and pod name. 375 func PodKey(namespace, pod string) string { 376 return path.Path{namespace, pod}.String() 377 } 378 379 func getOwnerDeployment(pod *corev1.Pod, replicasets []appsv1.ReplicaSet) string { 380 for _, o := range pod.OwnerReferences { 381 if o.Kind == name.ReplicaSetStr { 382 for _, rs := range replicasets { 383 if rs.Name == o.Name { 384 for _, oo := range rs.OwnerReferences { 385 if oo.Kind == name.DeploymentStr { 386 return oo.Name 387 } 388 } 389 } 390 } 391 } 392 } 393 return "" 394 } 395 396 func getOwnerDaemonSet(pod *corev1.Pod, daemonsets []appsv1.DaemonSet) string { 397 for _, o := range pod.OwnerReferences { 398 if o.Kind == name.DaemonSetStr { 399 for _, ds := range daemonsets { 400 if ds.Name == o.Name { 401 return ds.Name 402 } 403 } 404 } 405 } 406 return "" 407 } 408 409 func imageToVersion(imageStr string) string { 410 vs := versionRegex.FindStringSubmatch(imageStr) 411 if len(vs) != 2 { 412 return "" 413 } 414 return vs[0] 415 }