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  }