github.com/argoproj/argo-cd/v3@v3.2.1/controller/cache/info.go (about)

     1  package cache
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  
     9  	corev1 "k8s.io/api/core/v1"
    10  	"k8s.io/apimachinery/pkg/runtime/schema"
    11  
    12  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    13  	"github.com/argoproj/gitops-engine/pkg/utils/text"
    14  	"github.com/cespare/xxhash/v2"
    15  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    16  	"k8s.io/apimachinery/pkg/runtime"
    17  	resourcehelper "k8s.io/kubectl/pkg/util/resource"
    18  
    19  	"github.com/argoproj/argo-cd/v3/common"
    20  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    21  	"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
    22  	"github.com/argoproj/argo-cd/v3/util/resource"
    23  )
    24  
    25  func populateNodeInfo(un *unstructured.Unstructured, res *ResourceInfo, customLabels []string) {
    26  	gvk := un.GroupVersionKind()
    27  	revision := resource.GetRevision(un)
    28  	if revision > 0 {
    29  		res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Revision", Value: fmt.Sprintf("Rev:%v", revision)})
    30  	}
    31  	if len(customLabels) > 0 {
    32  		if labels := un.GetLabels(); labels != nil {
    33  			for _, customLabel := range customLabels {
    34  				if value, ok := labels[customLabel]; ok {
    35  					res.Info = append(res.Info, v1alpha1.InfoItem{Name: customLabel, Value: value})
    36  				}
    37  			}
    38  		}
    39  	}
    40  
    41  	for k, v := range un.GetAnnotations() {
    42  		if strings.HasPrefix(k, common.AnnotationKeyLinkPrefix) {
    43  			if res.NetworkingInfo == nil {
    44  				res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{}
    45  			}
    46  			res.NetworkingInfo.ExternalURLs = append(res.NetworkingInfo.ExternalURLs, v)
    47  		}
    48  	}
    49  
    50  	switch gvk.Group {
    51  	case "":
    52  		switch gvk.Kind {
    53  		case kube.PodKind:
    54  			populatePodInfo(un, res)
    55  		case kube.ServiceKind:
    56  			populateServiceInfo(un, res)
    57  		case "Node":
    58  			populateHostNodeInfo(un, res)
    59  		}
    60  	case "extensions", "networking.k8s.io":
    61  		if gvk.Kind == kube.IngressKind {
    62  			populateIngressInfo(un, res)
    63  		}
    64  	case "networking.istio.io":
    65  		switch gvk.Kind {
    66  		case "VirtualService":
    67  			populateIstioVirtualServiceInfo(un, res)
    68  		case "ServiceEntry":
    69  			populateIstioServiceEntryInfo(un, res)
    70  		}
    71  	}
    72  }
    73  
    74  func getIngress(un *unstructured.Unstructured) []corev1.LoadBalancerIngress {
    75  	ingress, ok, err := unstructured.NestedSlice(un.Object, "status", "loadBalancer", "ingress")
    76  	if !ok || err != nil {
    77  		return nil
    78  	}
    79  	res := make([]corev1.LoadBalancerIngress, 0)
    80  	for _, item := range ingress {
    81  		if lbIngress, ok := item.(map[string]any); ok {
    82  			if hostname := lbIngress["hostname"]; hostname != nil {
    83  				res = append(res, corev1.LoadBalancerIngress{Hostname: fmt.Sprintf("%s", hostname)})
    84  			} else if ip := lbIngress["ip"]; ip != nil {
    85  				res = append(res, corev1.LoadBalancerIngress{IP: fmt.Sprintf("%s", ip)})
    86  			}
    87  		}
    88  	}
    89  	return res
    90  }
    91  
    92  func populateServiceInfo(un *unstructured.Unstructured, res *ResourceInfo) {
    93  	targetLabels, _, _ := unstructured.NestedStringMap(un.Object, "spec", "selector")
    94  	ingress := make([]corev1.LoadBalancerIngress, 0)
    95  	if serviceType, ok, err := unstructured.NestedString(un.Object, "spec", "type"); ok && err == nil && serviceType == string(corev1.ServiceTypeLoadBalancer) {
    96  		ingress = getIngress(un)
    97  	}
    98  
    99  	var urls []string
   100  	if res.NetworkingInfo != nil {
   101  		urls = res.NetworkingInfo.ExternalURLs
   102  	}
   103  
   104  	res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetLabels: targetLabels, Ingress: ingress, ExternalURLs: urls}
   105  }
   106  
   107  func getServiceName(backend map[string]any, gvk schema.GroupVersionKind) (string, error) {
   108  	switch gvk.Group {
   109  	case "extensions":
   110  		return fmt.Sprintf("%s", backend["serviceName"]), nil
   111  	case "networking.k8s.io":
   112  		switch gvk.Version {
   113  		case "v1beta1":
   114  			return fmt.Sprintf("%s", backend["serviceName"]), nil
   115  		case "v1":
   116  			if service, ok, err := unstructured.NestedMap(backend, "service"); ok && err == nil {
   117  				return fmt.Sprintf("%s", service["name"]), nil
   118  			}
   119  		}
   120  	}
   121  	return "", errors.New("unable to resolve string")
   122  }
   123  
   124  func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
   125  	ingress := getIngress(un)
   126  	targetsMap := make(map[v1alpha1.ResourceRef]bool)
   127  	gvk := un.GroupVersionKind()
   128  	if backend, ok, err := unstructured.NestedMap(un.Object, "spec", "backend"); ok && err == nil {
   129  		if serviceName, err := getServiceName(backend, gvk); err == nil {
   130  			targetsMap[v1alpha1.ResourceRef{
   131  				Group:     "",
   132  				Kind:      kube.ServiceKind,
   133  				Namespace: un.GetNamespace(),
   134  				Name:      serviceName,
   135  			}] = true
   136  		}
   137  	}
   138  	urlsSet := make(map[string]bool)
   139  	if rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "rules"); ok && err == nil {
   140  		for i := range rules {
   141  			rule, ok := rules[i].(map[string]any)
   142  			if !ok {
   143  				continue
   144  			}
   145  			host := rule["host"]
   146  			if host == nil || host == "" {
   147  				for i := range ingress {
   148  					host = text.FirstNonEmpty(ingress[i].Hostname, ingress[i].IP)
   149  					if host != "" {
   150  						break
   151  					}
   152  				}
   153  			}
   154  			paths, ok, err := unstructured.NestedSlice(rule, "http", "paths")
   155  			if !ok || err != nil {
   156  				continue
   157  			}
   158  			for i := range paths {
   159  				path, ok := paths[i].(map[string]any)
   160  				if !ok {
   161  					continue
   162  				}
   163  
   164  				if backend, ok, err := unstructured.NestedMap(path, "backend"); ok && err == nil {
   165  					if serviceName, err := getServiceName(backend, gvk); err == nil {
   166  						targetsMap[v1alpha1.ResourceRef{
   167  							Group:     "",
   168  							Kind:      kube.ServiceKind,
   169  							Namespace: un.GetNamespace(),
   170  							Name:      serviceName,
   171  						}] = true
   172  					}
   173  				}
   174  
   175  				if host == nil || host == "" {
   176  					continue
   177  				}
   178  				stringPort := "http"
   179  				if tls, ok, err := unstructured.NestedSlice(un.Object, "spec", "tls"); ok && err == nil {
   180  					for i := range tls {
   181  						tlsline, ok := tls[i].(map[string]any)
   182  						secretName := tlsline["secretName"]
   183  						if ok && secretName != nil {
   184  							stringPort = "https"
   185  						}
   186  						tlshost := tlsline["host"]
   187  						if tlshost == host {
   188  							stringPort = "https"
   189  							continue
   190  						}
   191  						if hosts := tlsline["hosts"]; hosts != nil {
   192  							tlshosts, ok := tlsline["hosts"].(map[string]any)
   193  							if ok {
   194  								for j := range tlshosts {
   195  									if tlshosts[j] == host {
   196  										stringPort = "https"
   197  									}
   198  								}
   199  							}
   200  						}
   201  					}
   202  				}
   203  
   204  				externalURL := fmt.Sprintf("%s://%s", stringPort, host)
   205  
   206  				subPath := ""
   207  				if nestedPath, ok, err := unstructured.NestedString(path, "path"); ok && err == nil {
   208  					subPath = strings.TrimSuffix(nestedPath, "*")
   209  				}
   210  				externalURL += subPath
   211  				urlsSet[externalURL] = true
   212  			}
   213  		}
   214  	}
   215  	targets := make([]v1alpha1.ResourceRef, 0)
   216  	for target := range targetsMap {
   217  		targets = append(targets, target)
   218  	}
   219  
   220  	var urls []string
   221  	if res.NetworkingInfo != nil {
   222  		urls = res.NetworkingInfo.ExternalURLs
   223  	}
   224  	for url := range urlsSet {
   225  		urls = append(urls, url)
   226  	}
   227  	res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
   228  }
   229  
   230  func populateIstioVirtualServiceInfo(un *unstructured.Unstructured, res *ResourceInfo) {
   231  	targetsMap := make(map[v1alpha1.ResourceRef]bool)
   232  
   233  	if rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "http"); ok && err == nil {
   234  		for i := range rules {
   235  			rule, ok := rules[i].(map[string]any)
   236  			if !ok {
   237  				continue
   238  			}
   239  			routes, ok, err := unstructured.NestedSlice(rule, "route")
   240  			if !ok || err != nil {
   241  				continue
   242  			}
   243  			for i := range routes {
   244  				route, ok := routes[i].(map[string]any)
   245  				if !ok {
   246  					continue
   247  				}
   248  
   249  				if hostName, ok, err := unstructured.NestedString(route, "destination", "host"); ok && err == nil {
   250  					hostSplits := strings.Split(hostName, ".")
   251  					serviceName := hostSplits[0]
   252  
   253  					var namespace string
   254  					if len(hostSplits) >= 2 {
   255  						namespace = hostSplits[1]
   256  					} else {
   257  						namespace = un.GetNamespace()
   258  					}
   259  
   260  					targetsMap[v1alpha1.ResourceRef{
   261  						Kind:      kube.ServiceKind,
   262  						Name:      serviceName,
   263  						Namespace: namespace,
   264  					}] = true
   265  				}
   266  			}
   267  		}
   268  	}
   269  	targets := make([]v1alpha1.ResourceRef, 0)
   270  	for target := range targetsMap {
   271  		targets = append(targets, target)
   272  	}
   273  
   274  	var urls []string
   275  	if res.NetworkingInfo != nil {
   276  		urls = res.NetworkingInfo.ExternalURLs
   277  	}
   278  
   279  	res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, ExternalURLs: urls}
   280  }
   281  
   282  func populateIstioServiceEntryInfo(un *unstructured.Unstructured, res *ResourceInfo) {
   283  	targetLabels, ok, err := unstructured.NestedStringMap(un.Object, "spec", "workloadSelector", "labels")
   284  	if err != nil {
   285  		return
   286  	}
   287  	if !ok {
   288  		return
   289  	}
   290  	res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{
   291  		TargetLabels: targetLabels,
   292  		TargetRefs: []v1alpha1.ResourceRef{{
   293  			Kind: kube.PodKind,
   294  		}},
   295  	}
   296  }
   297  
   298  func isPodInitializedConditionTrue(status *corev1.PodStatus) bool {
   299  	for _, condition := range status.Conditions {
   300  		if condition.Type != corev1.PodInitialized {
   301  			continue
   302  		}
   303  
   304  		return condition.Status == corev1.ConditionTrue
   305  	}
   306  	return false
   307  }
   308  
   309  func isRestartableInitContainer(initContainer *corev1.Container) bool {
   310  	if initContainer == nil {
   311  		return false
   312  	}
   313  	if initContainer.RestartPolicy == nil {
   314  		return false
   315  	}
   316  
   317  	return *initContainer.RestartPolicy == corev1.ContainerRestartPolicyAlways
   318  }
   319  
   320  func isPodPhaseTerminal(phase corev1.PodPhase) bool {
   321  	return phase == corev1.PodFailed || phase == corev1.PodSucceeded
   322  }
   323  
   324  func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
   325  	pod := corev1.Pod{}
   326  	err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &pod)
   327  	if err != nil {
   328  		return
   329  	}
   330  	restarts := 0
   331  	totalContainers := len(pod.Spec.Containers)
   332  	readyContainers := 0
   333  
   334  	podPhase := pod.Status.Phase
   335  	reason := string(podPhase)
   336  	if pod.Status.Reason != "" {
   337  		reason = pod.Status.Reason
   338  	}
   339  
   340  	imagesSet := make(map[string]bool)
   341  	for _, container := range pod.Spec.InitContainers {
   342  		imagesSet[container.Image] = true
   343  	}
   344  	for _, container := range pod.Spec.Containers {
   345  		imagesSet[container.Image] = true
   346  	}
   347  
   348  	res.Images = nil
   349  	for image := range imagesSet {
   350  		res.Images = append(res.Images, image)
   351  	}
   352  
   353  	// If the Pod carries {type:PodScheduled, reason:SchedulingGated}, set reason to 'SchedulingGated'.
   354  	for _, condition := range pod.Status.Conditions {
   355  		if condition.Type == corev1.PodScheduled && condition.Reason == corev1.PodReasonSchedulingGated {
   356  			reason = corev1.PodReasonSchedulingGated
   357  		}
   358  	}
   359  
   360  	initContainers := make(map[string]*corev1.Container)
   361  	for i := range pod.Spec.InitContainers {
   362  		initContainers[pod.Spec.InitContainers[i].Name] = &pod.Spec.InitContainers[i]
   363  		if isRestartableInitContainer(&pod.Spec.InitContainers[i]) {
   364  			totalContainers++
   365  		}
   366  	}
   367  
   368  	initializing := false
   369  	for i := range pod.Status.InitContainerStatuses {
   370  		container := pod.Status.InitContainerStatuses[i]
   371  		restarts += int(container.RestartCount)
   372  		switch {
   373  		case container.State.Terminated != nil && container.State.Terminated.ExitCode == 0:
   374  			continue
   375  		case isRestartableInitContainer(initContainers[container.Name]) &&
   376  			container.Started != nil && *container.Started:
   377  			if container.Ready {
   378  				readyContainers++
   379  			}
   380  			continue
   381  		case container.State.Terminated != nil:
   382  			// initialization is failed
   383  			if container.State.Terminated.Reason == "" {
   384  				if container.State.Terminated.Signal != 0 {
   385  					reason = fmt.Sprintf("Init:Signal:%d", container.State.Terminated.Signal)
   386  				} else {
   387  					reason = fmt.Sprintf("Init:ExitCode:%d", container.State.Terminated.ExitCode)
   388  				}
   389  			} else {
   390  				reason = "Init:" + container.State.Terminated.Reason
   391  			}
   392  			initializing = true
   393  		case container.State.Waiting != nil && container.State.Waiting.Reason != "" && container.State.Waiting.Reason != "PodInitializing":
   394  			reason = "Init:" + container.State.Waiting.Reason
   395  			initializing = true
   396  		default:
   397  			reason = fmt.Sprintf("Init:%d/%d", i, len(pod.Spec.InitContainers))
   398  			initializing = true
   399  		}
   400  		break
   401  	}
   402  	if !initializing || isPodInitializedConditionTrue(&pod.Status) {
   403  		hasRunning := false
   404  		for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- {
   405  			container := pod.Status.ContainerStatuses[i]
   406  
   407  			restarts += int(container.RestartCount)
   408  			switch {
   409  			case container.State.Waiting != nil && container.State.Waiting.Reason != "":
   410  				reason = container.State.Waiting.Reason
   411  			case container.State.Terminated != nil && container.State.Terminated.Reason != "":
   412  				reason = container.State.Terminated.Reason
   413  			case container.State.Terminated != nil && container.State.Terminated.Reason == "":
   414  				if container.State.Terminated.Signal != 0 {
   415  					reason = fmt.Sprintf("Signal:%d", container.State.Terminated.Signal)
   416  				} else {
   417  					reason = fmt.Sprintf("ExitCode:%d", container.State.Terminated.ExitCode)
   418  				}
   419  			case container.Ready && container.State.Running != nil:
   420  				hasRunning = true
   421  				readyContainers++
   422  			}
   423  		}
   424  
   425  		// change pod status back to "Running" if there is at least one container still reporting as "Running" status
   426  		if reason == "Completed" && hasRunning {
   427  			reason = "Running"
   428  		}
   429  	}
   430  
   431  	// "NodeLost" = https://github.com/kubernetes/kubernetes/blob/cb8ad64243d48d9a3c26b11b2e0945c098457282/pkg/util/node/node.go#L46
   432  	// But depending on the k8s.io/kubernetes package just for a constant
   433  	// is not worth it.
   434  	// See https://github.com/argoproj/argo-cd/issues/5173
   435  	// and https://github.com/kubernetes/kubernetes/issues/90358#issuecomment-617859364
   436  	if pod.DeletionTimestamp != nil && pod.Status.Reason == "NodeLost" {
   437  		reason = "Unknown"
   438  		// If the pod is being deleted and the pod phase is not succeeded or failed, set the reason to "Terminating".
   439  		// See https://github.com/kubernetes/kubectl/issues/1595#issuecomment-2080001023
   440  	} else if pod.DeletionTimestamp != nil && !isPodPhaseTerminal(podPhase) {
   441  		reason = "Terminating"
   442  	}
   443  
   444  	if reason != "" {
   445  		res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
   446  	}
   447  
   448  	req, _ := resourcehelper.PodRequestsAndLimits(&pod)
   449  
   450  	res.PodInfo = &PodInfo{NodeName: pod.Spec.NodeName, ResourceRequests: req, Phase: pod.Status.Phase}
   451  
   452  	res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Node", Value: pod.Spec.NodeName})
   453  	res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
   454  	if restarts > 0 {
   455  		res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Restart Count", Value: strconv.Itoa(restarts)})
   456  	}
   457  
   458  	// Requests are relevant even for pods in the init phase or pending state (e.g., due to insufficient resources),
   459  	// as they help with diagnosing scheduling and startup issues.
   460  	// requests will be released for terminated pods either with success or failed state termination.
   461  	if !isPodPhaseTerminal(pod.Status.Phase) {
   462  		CPUReq := req[corev1.ResourceCPU]
   463  		MemoryReq := req[corev1.ResourceMemory]
   464  
   465  		res.Info = append(res.Info, v1alpha1.InfoItem{Name: common.PodRequestsCPU, Value: strconv.FormatInt(CPUReq.MilliValue(), 10)})
   466  		res.Info = append(res.Info, v1alpha1.InfoItem{Name: common.PodRequestsMEM, Value: strconv.FormatInt(MemoryReq.MilliValue(), 10)})
   467  	}
   468  
   469  	var urls []string
   470  	if res.NetworkingInfo != nil {
   471  		urls = res.NetworkingInfo.ExternalURLs
   472  	}
   473  
   474  	res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{Labels: un.GetLabels(), ExternalURLs: urls}
   475  }
   476  
   477  func populateHostNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
   478  	node := corev1.Node{}
   479  	err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &node)
   480  	if err != nil {
   481  		return
   482  	}
   483  	res.NodeInfo = &NodeInfo{
   484  		Name:       node.Name,
   485  		Capacity:   node.Status.Capacity,
   486  		SystemInfo: node.Status.NodeInfo,
   487  		Labels:     node.Labels,
   488  	}
   489  }
   490  
   491  func generateManifestHash(un *unstructured.Unstructured, ignores []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts normalizers.IgnoreNormalizerOpts) (string, error) {
   492  	normalizer, err := normalizers.NewIgnoreNormalizer(ignores, overrides, opts)
   493  	if err != nil {
   494  		return "", fmt.Errorf("error creating normalizer: %w", err)
   495  	}
   496  
   497  	resource := un.DeepCopy()
   498  	err = normalizer.Normalize(resource)
   499  	if err != nil {
   500  		return "", fmt.Errorf("error normalizing resource: %w", err)
   501  	}
   502  
   503  	data, err := resource.MarshalJSON()
   504  	if err != nil {
   505  		return "", fmt.Errorf("error marshaling resource: %w", err)
   506  	}
   507  	hash := hash(data)
   508  	return hash, nil
   509  }
   510  
   511  func hash(data []byte) string {
   512  	return strconv.FormatUint(xxhash.Sum64(data), 16)
   513  }