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 }