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 }