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

     1  package cache
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"k8s.io/apimachinery/pkg/runtime/schema"
    10  
    11  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    12  	"github.com/argoproj/gitops-engine/pkg/utils/text"
    13  	"github.com/cespare/xxhash/v2"
    14  	v1 "k8s.io/api/core/v1"
    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/v2/common"
    20  	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    21  	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
    22  	"github.com/argoproj/argo-cd/v2/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  		switch gvk.Kind {
    62  		case kube.IngressKind:
    63  			populateIngressInfo(un, res)
    64  		}
    65  	case "networking.istio.io":
    66  		switch gvk.Kind {
    67  		case "VirtualService":
    68  			populateIstioVirtualServiceInfo(un, res)
    69  		}
    70  	}
    71  }
    72  
    73  func getIngress(un *unstructured.Unstructured) []v1.LoadBalancerIngress {
    74  	ingress, ok, err := unstructured.NestedSlice(un.Object, "status", "loadBalancer", "ingress")
    75  	if !ok || err != nil {
    76  		return nil
    77  	}
    78  	res := make([]v1.LoadBalancerIngress, 0)
    79  	for _, item := range ingress {
    80  		if lbIngress, ok := item.(map[string]interface{}); ok {
    81  			if hostname := lbIngress["hostname"]; hostname != nil {
    82  				res = append(res, v1.LoadBalancerIngress{Hostname: fmt.Sprintf("%s", hostname)})
    83  			} else if ip := lbIngress["ip"]; ip != nil {
    84  				res = append(res, v1.LoadBalancerIngress{IP: fmt.Sprintf("%s", ip)})
    85  			}
    86  		}
    87  	}
    88  	return res
    89  }
    90  
    91  func populateServiceInfo(un *unstructured.Unstructured, res *ResourceInfo) {
    92  	targetLabels, _, _ := unstructured.NestedStringMap(un.Object, "spec", "selector")
    93  	ingress := make([]v1.LoadBalancerIngress, 0)
    94  	if serviceType, ok, err := unstructured.NestedString(un.Object, "spec", "type"); ok && err == nil && serviceType == string(v1.ServiceTypeLoadBalancer) {
    95  		ingress = getIngress(un)
    96  	}
    97  
    98  	var urls []string
    99  	if res.NetworkingInfo != nil {
   100  		urls = res.NetworkingInfo.ExternalURLs
   101  	}
   102  
   103  	res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetLabels: targetLabels, Ingress: ingress, ExternalURLs: urls}
   104  }
   105  
   106  func getServiceName(backend map[string]interface{}, gvk schema.GroupVersionKind) (string, error) {
   107  	switch gvk.Group {
   108  	case "extensions":
   109  		return fmt.Sprintf("%s", backend["serviceName"]), nil
   110  	case "networking.k8s.io":
   111  		switch gvk.Version {
   112  		case "v1beta1":
   113  			return fmt.Sprintf("%s", backend["serviceName"]), nil
   114  		case "v1":
   115  			if service, ok, err := unstructured.NestedMap(backend, "service"); ok && err == nil {
   116  				return fmt.Sprintf("%s", service["name"]), nil
   117  			}
   118  		}
   119  	}
   120  	return "", errors.New("unable to resolve string")
   121  }
   122  
   123  func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
   124  	ingress := getIngress(un)
   125  	targetsMap := make(map[v1alpha1.ResourceRef]bool)
   126  	gvk := un.GroupVersionKind()
   127  	if backend, ok, err := unstructured.NestedMap(un.Object, "spec", "backend"); ok && err == nil {
   128  		if serviceName, err := getServiceName(backend, gvk); err == nil {
   129  			targetsMap[v1alpha1.ResourceRef{
   130  				Group:     "",
   131  				Kind:      kube.ServiceKind,
   132  				Namespace: un.GetNamespace(),
   133  				Name:      serviceName,
   134  			}] = true
   135  		}
   136  	}
   137  	urlsSet := make(map[string]bool)
   138  	if rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "rules"); ok && err == nil {
   139  		for i := range rules {
   140  			rule, ok := rules[i].(map[string]interface{})
   141  			if !ok {
   142  				continue
   143  			}
   144  			host := rule["host"]
   145  			if host == nil || host == "" {
   146  				for i := range ingress {
   147  					host = text.FirstNonEmpty(ingress[i].Hostname, ingress[i].IP)
   148  					if host != "" {
   149  						break
   150  					}
   151  				}
   152  			}
   153  			paths, ok, err := unstructured.NestedSlice(rule, "http", "paths")
   154  			if !ok || err != nil {
   155  				continue
   156  			}
   157  			for i := range paths {
   158  				path, ok := paths[i].(map[string]interface{})
   159  				if !ok {
   160  					continue
   161  				}
   162  
   163  				if backend, ok, err := unstructured.NestedMap(path, "backend"); ok && err == nil {
   164  					if serviceName, err := getServiceName(backend, gvk); err == nil {
   165  						targetsMap[v1alpha1.ResourceRef{
   166  							Group:     "",
   167  							Kind:      kube.ServiceKind,
   168  							Namespace: un.GetNamespace(),
   169  							Name:      serviceName,
   170  						}] = true
   171  					}
   172  				}
   173  
   174  				if host == nil || host == "" {
   175  					continue
   176  				}
   177  				stringPort := "http"
   178  				if tls, ok, err := unstructured.NestedSlice(un.Object, "spec", "tls"); ok && err == nil {
   179  					for i := range tls {
   180  						tlsline, ok := tls[i].(map[string]interface{})
   181  						secretName := tlsline["secretName"]
   182  						if ok && secretName != nil {
   183  							stringPort = "https"
   184  						}
   185  						tlshost := tlsline["host"]
   186  						if tlshost == host {
   187  							stringPort = "https"
   188  							continue
   189  						}
   190  						if hosts := tlsline["hosts"]; hosts != nil {
   191  							tlshosts, ok := tlsline["hosts"].(map[string]interface{})
   192  							if ok {
   193  								for j := range tlshosts {
   194  									if tlshosts[j] == host {
   195  										stringPort = "https"
   196  									}
   197  								}
   198  							}
   199  						}
   200  					}
   201  				}
   202  
   203  				externalURL := fmt.Sprintf("%s://%s", stringPort, host)
   204  
   205  				subPath := ""
   206  				if nestedPath, ok, err := unstructured.NestedString(path, "path"); ok && err == nil {
   207  					subPath = strings.TrimSuffix(nestedPath, "*")
   208  				}
   209  				externalURL += subPath
   210  				urlsSet[externalURL] = true
   211  			}
   212  		}
   213  	}
   214  	targets := make([]v1alpha1.ResourceRef, 0)
   215  	for target := range targetsMap {
   216  		targets = append(targets, target)
   217  	}
   218  
   219  	var urls []string
   220  	if res.NetworkingInfo != nil {
   221  		urls = res.NetworkingInfo.ExternalURLs
   222  	}
   223  	for url := range urlsSet {
   224  		urls = append(urls, url)
   225  	}
   226  	res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
   227  }
   228  
   229  func populateIstioVirtualServiceInfo(un *unstructured.Unstructured, res *ResourceInfo) {
   230  	targetsMap := make(map[v1alpha1.ResourceRef]bool)
   231  
   232  	if rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "http"); ok && err == nil {
   233  		for i := range rules {
   234  			rule, ok := rules[i].(map[string]interface{})
   235  			if !ok {
   236  				continue
   237  			}
   238  			routes, ok, err := unstructured.NestedSlice(rule, "route")
   239  			if !ok || err != nil {
   240  				continue
   241  			}
   242  			for i := range routes {
   243  				route, ok := routes[i].(map[string]interface{})
   244  				if !ok {
   245  					continue
   246  				}
   247  
   248  				if hostName, ok, err := unstructured.NestedString(route, "destination", "host"); ok && err == nil {
   249  					hostSplits := strings.Split(hostName, ".")
   250  					serviceName := hostSplits[0]
   251  
   252  					var namespace string
   253  					if len(hostSplits) >= 2 {
   254  						namespace = hostSplits[1]
   255  					} else {
   256  						namespace = un.GetNamespace()
   257  					}
   258  
   259  					targetsMap[v1alpha1.ResourceRef{
   260  						Kind:      kube.ServiceKind,
   261  						Name:      serviceName,
   262  						Namespace: namespace,
   263  					}] = true
   264  				}
   265  			}
   266  		}
   267  	}
   268  	targets := make([]v1alpha1.ResourceRef, 0)
   269  	for target := range targetsMap {
   270  		targets = append(targets, target)
   271  	}
   272  
   273  	var urls []string
   274  	if res.NetworkingInfo != nil {
   275  		urls = res.NetworkingInfo.ExternalURLs
   276  	}
   277  
   278  	res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, ExternalURLs: urls}
   279  }
   280  
   281  func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
   282  	pod := v1.Pod{}
   283  	err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &pod)
   284  	if err != nil {
   285  		return
   286  	}
   287  	restarts := 0
   288  	totalContainers := len(pod.Spec.Containers)
   289  	readyContainers := 0
   290  
   291  	reason := string(pod.Status.Phase)
   292  	if pod.Status.Reason != "" {
   293  		reason = pod.Status.Reason
   294  	}
   295  
   296  	imagesSet := make(map[string]bool)
   297  	for _, container := range pod.Spec.InitContainers {
   298  		imagesSet[container.Image] = true
   299  	}
   300  	for _, container := range pod.Spec.Containers {
   301  		imagesSet[container.Image] = true
   302  	}
   303  
   304  	res.Images = nil
   305  	for image := range imagesSet {
   306  		res.Images = append(res.Images, image)
   307  	}
   308  
   309  	initializing := false
   310  	for i := range pod.Status.InitContainerStatuses {
   311  		container := pod.Status.InitContainerStatuses[i]
   312  		restarts += int(container.RestartCount)
   313  		switch {
   314  		case container.State.Terminated != nil && container.State.Terminated.ExitCode == 0:
   315  			continue
   316  		case container.State.Terminated != nil:
   317  			// initialization is failed
   318  			if len(container.State.Terminated.Reason) == 0 {
   319  				if container.State.Terminated.Signal != 0 {
   320  					reason = fmt.Sprintf("Init:Signal:%d", container.State.Terminated.Signal)
   321  				} else {
   322  					reason = fmt.Sprintf("Init:ExitCode:%d", container.State.Terminated.ExitCode)
   323  				}
   324  			} else {
   325  				reason = "Init:" + container.State.Terminated.Reason
   326  			}
   327  			initializing = true
   328  		case container.State.Waiting != nil && len(container.State.Waiting.Reason) > 0 && container.State.Waiting.Reason != "PodInitializing":
   329  			reason = "Init:" + container.State.Waiting.Reason
   330  			initializing = true
   331  		default:
   332  			reason = fmt.Sprintf("Init:%d/%d", i, len(pod.Spec.InitContainers))
   333  			initializing = true
   334  		}
   335  		break
   336  	}
   337  	if !initializing {
   338  		restarts = 0
   339  		hasRunning := false
   340  		for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- {
   341  			container := pod.Status.ContainerStatuses[i]
   342  
   343  			restarts += int(container.RestartCount)
   344  			if container.State.Waiting != nil && container.State.Waiting.Reason != "" {
   345  				reason = container.State.Waiting.Reason
   346  			} else if container.State.Terminated != nil && container.State.Terminated.Reason != "" {
   347  				reason = container.State.Terminated.Reason
   348  			} else if container.State.Terminated != nil && container.State.Terminated.Reason == "" {
   349  				if container.State.Terminated.Signal != 0 {
   350  					reason = fmt.Sprintf("Signal:%d", container.State.Terminated.Signal)
   351  				} else {
   352  					reason = fmt.Sprintf("ExitCode:%d", container.State.Terminated.ExitCode)
   353  				}
   354  			} else if container.Ready && container.State.Running != nil {
   355  				hasRunning = true
   356  				readyContainers++
   357  			}
   358  		}
   359  
   360  		// change pod status back to "Running" if there is at least one container still reporting as "Running" status
   361  		if reason == "Completed" && hasRunning {
   362  			reason = "Running"
   363  		}
   364  	}
   365  
   366  	// "NodeLost" = https://github.com/kubernetes/kubernetes/blob/cb8ad64243d48d9a3c26b11b2e0945c098457282/pkg/util/node/node.go#L46
   367  	// But depending on the k8s.io/kubernetes package just for a constant
   368  	// is not worth it.
   369  	// See https://github.com/argoproj/argo-cd/issues/5173
   370  	// and https://github.com/kubernetes/kubernetes/issues/90358#issuecomment-617859364
   371  	if pod.DeletionTimestamp != nil && pod.Status.Reason == "NodeLost" {
   372  		reason = "Unknown"
   373  	} else if pod.DeletionTimestamp != nil {
   374  		reason = "Terminating"
   375  	}
   376  
   377  	if reason != "" {
   378  		res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
   379  	}
   380  
   381  	req, _ := resourcehelper.PodRequestsAndLimits(&pod)
   382  	res.PodInfo = &PodInfo{NodeName: pod.Spec.NodeName, ResourceRequests: req, Phase: pod.Status.Phase}
   383  
   384  	res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Node", Value: pod.Spec.NodeName})
   385  	res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
   386  	if restarts > 0 {
   387  		res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Restart Count", Value: fmt.Sprintf("%d", restarts)})
   388  	}
   389  
   390  	var urls []string
   391  	if res.NetworkingInfo != nil {
   392  		urls = res.NetworkingInfo.ExternalURLs
   393  	}
   394  
   395  	res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{Labels: un.GetLabels(), ExternalURLs: urls}
   396  }
   397  
   398  func populateHostNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
   399  	node := v1.Node{}
   400  	err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &node)
   401  	if err != nil {
   402  		return
   403  	}
   404  	res.NodeInfo = &NodeInfo{
   405  		Name:       node.Name,
   406  		Capacity:   node.Status.Capacity,
   407  		SystemInfo: node.Status.NodeInfo,
   408  	}
   409  }
   410  
   411  func generateManifestHash(un *unstructured.Unstructured, ignores []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts normalizers.IgnoreNormalizerOpts) (string, error) {
   412  	normalizer, err := normalizers.NewIgnoreNormalizer(ignores, overrides, opts)
   413  	if err != nil {
   414  		return "", fmt.Errorf("error creating normalizer: %w", err)
   415  	}
   416  
   417  	resource := un.DeepCopy()
   418  	err = normalizer.Normalize(resource)
   419  	if err != nil {
   420  		return "", fmt.Errorf("error normalizing resource: %w", err)
   421  	}
   422  
   423  	data, err := resource.MarshalJSON()
   424  	if err != nil {
   425  		return "", fmt.Errorf("error marshaling resource: %w", err)
   426  	}
   427  	hash := hash(data)
   428  	return hash, nil
   429  }
   430  
   431  func hash(data []byte) string {
   432  	return strconv.FormatUint(xxhash.Sum64(data), 16)
   433  }