Files
41536738c8 refactor(cd): hard-fork - rename CRD API group argoproj.io -> deploy.hanzo.ai (#1)
* feat(api): hard-fork CRD API group argoproj.io -> deploy.hanzo.ai

Hanzo CD hard-fork, phase 1.1. Single source of truth is
pkg/apis/application/register.go's Group constant (Application/AppProject/
ApplicationSet all derive their FullName from it); every other Go call-site
that hardcoded the literal group string is updated to match: the 3 generated
informer GVRs, the app-of-apps nested-Application detector in types.go, the
resource-tree Application-kind cache populator, the 3 NotFound error
constructors in server/application, and the applicationset-controller's
leader-election Lease name/ID (kept in lockstep across Go source + RBAC
resourceNames, or leader election would get RBAC-denied on startup).

CRD YAMLs (application/appproject/applicationset) and every RBAC apiGroups
rule that actually governs our 3 CRDs are repointed to deploy.hanzo.ai, across
both manifests/base/* (namespaced Roles) and manifests/cluster-rbac/* (cluster
scope). Two adjacent things that share the "argoproj.io" string but are NOT
ours are deliberately left untouched: gitops-engine's health-check dispatch
for Argo Workflows' `Workflow` kind, and argocd-server-clusterrole's second
apiGroups block granting read on Argo Workflows' `workflows` resource — both
belong to a separate, unrelated Argoproj-family project we are not forking.

Also untouched, per explicit scope: annotation/label keys (argocd.argoproj.io/*)
and finalizers (*.argoproj.io) that track already-applied live resources, and
argocd-cm.yaml's resource.customizations.ignoreResourceUpdates keys (a narrow
app-of-apps diffing nicety, not build- or RBAC-relevant).

CGO_ENABLED=0 go build ./... verified green.

* ci: wire canonical hanzo.yml + cicd.yml (Hanzo CD image build)

Phase 1.3. Follows the same one-way pattern every hanzo/lux/zoo repo uses:
hanzo.yml declares the image (root Dockerfile -> ghcr.io/hanzoai/deploy) and a
fast go-build sanity test (the multi-stage Dockerfile build is the slow, real
gate); cicd.yml is the ~10-line trigger shim importing hanzoai/ci's reusable
build workflow on the arc pool, secrets from KMS only.

No `deploy:` block yet, matching the hanzoai/cms build-only-first-cutover
pattern: this repo's own live hanzo-cd namespace runs the whole Hanzo fleet's
GitOps, so the prod image/CRD swap is a deliberate separate action (see the
[HELD] universe PR), never an automatic rollout on merge. Every PR/push still
builds and pushes an immutable ghcr.io/hanzoai/deploy:sha-<7>-amd64 image.

* feat(ui): rebrand Argo CD -> Hanzo CD (title, favicon, logo, sidebar)

Phase 1.2. User-facing "Argo CD"/"ArgoCD" text replaced with "Hanzo CD"
across the login page, help modal, settings screens, version-info panel,
sync-options warning, fullscreen-logs title, app.tsx page titles, and
index.html's <title>/noscript text (dropped the now-wrong upstream CLI
doc link rather than invent a hanzo.ai URL that doesn't exist).

Logo/favicon assets replaced with the canonical Hanzo mark (never
hand-drawn, copied from ~/work/hanzo/logo/dist): the two real usages of
Argo's logo collapse onto one Hanzo glyph (mono + white variants) at the
same 5 call sites (login icon + footer, login.scss $logoPath/
$logoNegativePath, sidebar text-logo), preserving the existing CSS
invert() filters so both the light login panel and dark sidebar still
render correctly. Old argologo.svg/argo_o.svg/argo.png deleted (no
longer referenced). favicon-{16x16,32x32}.png added at
ui/src/assets/favicon/ (index.html's links to that path were previously
dead - the directory didn't exist in this checkout). package.json name
argo-cd-ui -> hanzo-cd-ui.

Left alone, deliberately: the two Kubernetes resource-kind icon sets
under ui/src/assets/images/resources/ (unrelated to branding), and
upstream doc/project links (argoproj.io, argo-cd.readthedocs.io) since
we have no equivalent hanzo.ai docs to point to yet - except the login
footer logo's outbound link, which now points to hanzo.ai instead of
argoproj.io (a Hanzo-branded mark linking to the upstream project it
was forked from read as an obvious leftover).

* feat(module): rename Go module github.com/argoproj/argo-cd/v3 -> github.com/hanzoai/deploy

Phase 2. No version suffix on the new path (org policy: never publish our
own Go modules above v1.x.x / with a v2+ suffix) — this fork starts a fresh
v0/v1 module identity rather than carrying the v3 the upstream fork point
had reached.

Mechanical, symmetric rewrite: 704 .go files (import statements only; two
disjoint substitutions applied specific-first so the nested gitops-engine
module path never double-matches), plus go.mod (module line, the
gitops-engine require+replace pair, version reset to v0.0.0-... since a
suffix-less path can't carry a v3 semver), gitops-engine/go.mod, Makefile
(ldflags PACKAGE + 2 TEST_MODULE filters), .golangci.yaml, .goreleaser.yaml,
.mockery.yaml (+ gitops-engine's), Procfile, the codegen/proto generation
scripts (hack/generate-proto.sh, update-codegen.sh, update-openapi.sh), and
20 .proto files' go_package options. Also one OpenTelemetry tracer name
string in gitops-engine/pkg/sync/sync_context.go (a cosmestic label, not an
import, but worth keeping consistent).

CGO_ENABLED=0 go build ./... verified green for both the root module and
the nested gitops-engine module; go vet clean on both; go mod tidy run
(network available) to reconcile go.sum, dependency set unchanged.

Left alone, deliberately: doc-only mentions (docs/snyk/*.html scan reports,
gitops-engine/README.md), a comment-only reference in
.github/workflows/release.yaml, and one Renovate bot config line — none are
build- or codegen-load-bearing. Also left alone: comments citing upstream
GitHub issue URLs (github.com/argoproj/argo-cd/issues/NNNNN) and a handful
of test fixtures using "https://github.com/argoproj/argo-cd.git" as a
representative example repo URL — both are accurate historical/test-data
references, not stale self-references.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 18:27:58 -07:00

816 lines
24 KiB
Go

package cache
import (
"errors"
"fmt"
"slices"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/hanzoai/deploy/gitops-engine/pkg/utils/kube"
"github.com/hanzoai/deploy/gitops-engine/pkg/utils/text"
"github.com/cespare/xxhash/v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
resourcehelper "k8s.io/component-helpers/resource"
"github.com/hanzoai/deploy/common"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
"github.com/hanzoai/deploy/util/argo/normalizers"
"github.com/hanzoai/deploy/util/resource"
"github.com/hanzoai/deploy/util/settings"
)
func populateNodeInfo(un *unstructured.Unstructured, res *ResourceInfo, customLabels []string) {
gvk := un.GroupVersionKind()
revision := resource.GetRevision(un)
if revision > 0 {
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Revision", Value: fmt.Sprintf("Rev:%v", revision)})
}
if len(customLabels) > 0 {
if labels := un.GetLabels(); labels != nil {
for _, customLabel := range customLabels {
if value, ok := labels[customLabel]; ok {
res.Info = append(res.Info, v1alpha1.InfoItem{Name: customLabel, Value: value})
}
}
}
}
for k, v := range un.GetAnnotations() {
if !strings.HasPrefix(k, common.AnnotationKeyLinkPrefix) {
continue
}
// Annotation values may be either a bare URL or "title|url"; validate
// the URL portion to prevent XSS via javascript:/data:/vbscript: URIs
// when the value is rendered as an href in the UI.
urlPart := v
if _, after, ok := strings.Cut(v, "|"); ok {
urlPart = after
}
if err := settings.ValidateExternalURL(urlPart); err != nil {
continue
}
if res.NetworkingInfo == nil {
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{}
}
res.NetworkingInfo.ExternalURLs = append(res.NetworkingInfo.ExternalURLs, v)
}
switch gvk.Group {
case "":
switch gvk.Kind {
case kube.PodKind:
populatePodInfo(un, res)
case kube.ServiceKind:
populateServiceInfo(un, res)
case "Node":
populateHostNodeInfo(un, res)
}
case "extensions", "networking.k8s.io":
if gvk.Kind == kube.IngressKind {
populateIngressInfo(un, res)
}
case "networking.istio.io":
switch gvk.Kind {
case "VirtualService":
populateIstioVirtualServiceInfo(un, res)
case "ServiceEntry":
populateIstioServiceEntryInfo(un, res)
}
case "gateway.networking.k8s.io":
switch gvk.Kind {
case "HTTPRoute", "GRPCRoute", "TCPRoute", "TLSRoute", "UDPRoute":
populateGatewayAPIRouteInfo(un, res)
case "Gateway":
populateGatewayInfo(un, res)
}
case "deploy.hanzo.ai":
if gvk.Kind == "Application" {
populateApplicationInfo(un, res)
}
}
}
func getIngress(un *unstructured.Unstructured) []corev1.LoadBalancerIngress {
ingress, ok, err := unstructured.NestedSlice(un.Object, "status", "loadBalancer", "ingress")
if !ok || err != nil {
return nil
}
res := make([]corev1.LoadBalancerIngress, 0)
for _, item := range ingress {
if lbIngress, ok := item.(map[string]any); ok {
if hostname := lbIngress["hostname"]; hostname != nil {
res = append(res, corev1.LoadBalancerIngress{Hostname: fmt.Sprintf("%s", hostname)})
} else if ip := lbIngress["ip"]; ip != nil {
res = append(res, corev1.LoadBalancerIngress{IP: fmt.Sprintf("%s", ip)})
}
}
}
return res
}
func populateServiceInfo(un *unstructured.Unstructured, res *ResourceInfo) {
targetLabels, _, _ := unstructured.NestedStringMap(un.Object, "spec", "selector")
ingress := make([]corev1.LoadBalancerIngress, 0)
if serviceType, ok, err := unstructured.NestedString(un.Object, "spec", "type"); ok && err == nil && serviceType == string(corev1.ServiceTypeLoadBalancer) {
ingress = getIngress(un)
}
var urls []string
if res.NetworkingInfo != nil {
urls = res.NetworkingInfo.ExternalURLs
}
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetLabels: targetLabels, Ingress: ingress, ExternalURLs: urls}
}
func getServiceName(backend map[string]any, gvk schema.GroupVersionKind) (string, error) {
switch gvk.Group {
case "extensions":
return fmt.Sprintf("%s", backend["serviceName"]), nil
case "networking.k8s.io":
switch gvk.Version {
case "v1beta1":
return fmt.Sprintf("%s", backend["serviceName"]), nil
case "v1":
if service, ok, err := unstructured.NestedMap(backend, "service"); ok && err == nil {
return fmt.Sprintf("%s", service["name"]), nil
}
}
}
return "", errors.New("unable to resolve string")
}
func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
ingress := getIngress(un)
targetsMap := make(map[v1alpha1.ResourceRef]bool)
gvk := un.GroupVersionKind()
if backend, ok, err := unstructured.NestedMap(un.Object, "spec", "backend"); ok && err == nil {
if serviceName, err := getServiceName(backend, gvk); err == nil {
targetsMap[v1alpha1.ResourceRef{
Group: "",
Kind: kube.ServiceKind,
Namespace: un.GetNamespace(),
Name: serviceName,
}] = true
}
}
urlsSet := make(map[string]bool)
if rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "rules"); ok && err == nil {
for i := range rules {
rule, ok := rules[i].(map[string]any)
if !ok {
continue
}
host := rule["host"]
if host == nil || host == "" {
for i := range ingress {
host = text.FirstNonEmpty(ingress[i].Hostname, ingress[i].IP)
if host != "" {
break
}
}
}
paths, ok, err := unstructured.NestedSlice(rule, "http", "paths")
if !ok || err != nil {
continue
}
for i := range paths {
path, ok := paths[i].(map[string]any)
if !ok {
continue
}
if backend, ok, err := unstructured.NestedMap(path, "backend"); ok && err == nil {
if serviceName, err := getServiceName(backend, gvk); err == nil {
targetsMap[v1alpha1.ResourceRef{
Group: "",
Kind: kube.ServiceKind,
Namespace: un.GetNamespace(),
Name: serviceName,
}] = true
}
}
if host == nil || host == "" {
continue
}
stringPort := "http"
if tls, ok, err := unstructured.NestedSlice(un.Object, "spec", "tls"); ok && err == nil {
for i := range tls {
tlsline, ok := tls[i].(map[string]any)
secretName := tlsline["secretName"]
if ok && secretName != nil {
stringPort = "https"
}
tlshost := tlsline["host"]
if tlshost == host {
stringPort = "https"
continue
}
if hosts := tlsline["hosts"]; hosts != nil {
tlshosts, ok := tlsline["hosts"].(map[string]any)
if ok {
for j := range tlshosts {
if tlshosts[j] == host {
stringPort = "https"
}
}
}
}
}
}
externalURL := fmt.Sprintf("%s://%s", stringPort, host)
subPath := ""
if nestedPath, ok, err := unstructured.NestedString(path, "path"); ok && err == nil {
subPath = strings.TrimSuffix(nestedPath, "*")
}
externalURL += subPath
urlsSet[externalURL] = true
}
}
}
targets := make([]v1alpha1.ResourceRef, 0)
for target := range targetsMap {
targets = append(targets, target)
}
var urls []string
if res.NetworkingInfo != nil {
urls = res.NetworkingInfo.ExternalURLs
}
enableDefaultExternalURLs := true
if ignoreVal, ok := un.GetAnnotations()[common.AnnotationKeyIgnoreDefaultLinks]; ok {
if ignoreDefaultLinks, err := strconv.ParseBool(ignoreVal); err == nil {
enableDefaultExternalURLs = !ignoreDefaultLinks
}
}
if enableDefaultExternalURLs {
for url := range urlsSet {
urls = append(urls, url)
}
}
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
}
func populateIstioVirtualServiceInfo(un *unstructured.Unstructured, res *ResourceInfo) {
targetsMap := make(map[v1alpha1.ResourceRef]bool)
if rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "http"); ok && err == nil {
for i := range rules {
rule, ok := rules[i].(map[string]any)
if !ok {
continue
}
routes, ok, err := unstructured.NestedSlice(rule, "route")
if !ok || err != nil {
continue
}
for i := range routes {
route, ok := routes[i].(map[string]any)
if !ok {
continue
}
if hostName, ok, err := unstructured.NestedString(route, "destination", "host"); ok && err == nil {
hostSplits := strings.Split(hostName, ".")
serviceName := hostSplits[0]
var namespace string
if len(hostSplits) >= 2 {
namespace = hostSplits[1]
} else {
namespace = un.GetNamespace()
}
targetsMap[v1alpha1.ResourceRef{
Kind: kube.ServiceKind,
Name: serviceName,
Namespace: namespace,
}] = true
}
}
}
}
targets := make([]v1alpha1.ResourceRef, 0)
for target := range targetsMap {
targets = append(targets, target)
}
var urls []string
if res.NetworkingInfo != nil {
urls = res.NetworkingInfo.ExternalURLs
}
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, ExternalURLs: urls}
}
func populateIstioServiceEntryInfo(un *unstructured.Unstructured, res *ResourceInfo) {
targetLabels, ok, err := unstructured.NestedStringMap(un.Object, "spec", "workloadSelector", "labels")
if err != nil {
return
}
if !ok {
return
}
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{
TargetLabels: targetLabels,
TargetRefs: []v1alpha1.ResourceRef{{
Kind: kube.PodKind,
}},
}
}
func getGatewayAddresses(un *unstructured.Unstructured) []corev1.LoadBalancerIngress {
addresses, ok, err := unstructured.NestedSlice(un.Object, "status", "addresses")
if !ok || err != nil {
if err != nil {
log.Warnf("Failed to get addresses for Gateway %s/%s: %v", un.GetNamespace(), un.GetName(), err)
}
return nil
}
res := make([]corev1.LoadBalancerIngress, 0)
for _, item := range addresses {
addr, ok := item.(map[string]any)
if !ok {
continue
}
addrType, _, errType := unstructured.NestedString(addr, "type")
if errType != nil {
log.Warnf("Failed to get address type for Gateway %s/%s: %v", un.GetNamespace(), un.GetName(), errType)
}
value, _, errValue := unstructured.NestedString(addr, "value")
if errValue != nil {
log.Warnf("Failed to get address value for Gateway %s/%s: %v", un.GetNamespace(), un.GetName(), errValue)
}
if value == "" {
continue
}
switch addrType {
case "Hostname":
res = append(res, corev1.LoadBalancerIngress{Hostname: value})
case "IPAddress", "":
// IPAddress is default per Gateway API spec
res = append(res, corev1.LoadBalancerIngress{IP: value})
}
}
return res
}
func generateGatewayURLs(un *unstructured.Unstructured, addresses []corev1.LoadBalancerIngress) []string {
urlsSet := make(map[string]bool)
listeners, ok, err := unstructured.NestedSlice(un.Object, "spec", "listeners")
if !ok || err != nil {
if err != nil {
log.Warnf("Failed to get listeners for Gateway %s/%s: %v", un.GetNamespace(), un.GetName(), err)
}
return nil
}
for _, listener := range listeners {
listenerMap, ok := listener.(map[string]any)
if !ok {
continue
}
hostname, _, errHostname := unstructured.NestedString(listenerMap, "hostname")
if errHostname != nil {
log.Warnf("Failed to get hostname for Gateway %s/%s listener: %v", un.GetNamespace(), un.GetName(), errHostname)
}
protocol, _, errProtocol := unstructured.NestedString(listenerMap, "protocol")
if errProtocol != nil {
log.Warnf("Failed to get protocol for Gateway %s/%s listener: %v", un.GetNamespace(), un.GetName(), errProtocol)
}
// Port can come as int64 or float64 depending on parsing
var port int64
if portVal, exists := listenerMap["port"]; exists {
switch v := portVal.(type) {
case int64:
port = v
case float64:
port = int64(v)
case int:
port = int64(v)
default:
log.Warnf("Unsupported port type %T for Gateway %s/%s listener", portVal, un.GetNamespace(), un.GetName())
}
}
scheme := "http"
if protocol == "HTTPS" || protocol == "TLS" {
scheme = "https"
}
// Use listener hostname if specified, otherwise use addresses
hosts := []string{}
if hostname != "" && hostname != "*" {
hosts = append(hosts, hostname)
} else {
for _, addr := range addresses {
if addr.Hostname != "" {
hosts = append(hosts, addr.Hostname)
} else if addr.IP != "" {
hosts = append(hosts, addr.IP)
}
}
}
for _, host := range hosts {
urlStr := fmt.Sprintf("%s://%s", scheme, host)
// Add non-standard ports
if (scheme == "http" && port != 80) || (scheme == "https" && port != 443) {
urlStr = fmt.Sprintf("%s:%d", urlStr, port)
}
if err := settings.ValidateExternalURL(urlStr); err != nil {
log.Warnf("Invalid URL generated for Gateway %s/%s: %s", un.GetNamespace(), un.GetName(), urlStr)
continue
}
urlsSet[urlStr] = true
}
}
urls := make([]string, 0, len(urlsSet))
for u := range urlsSet {
urls = append(urls, u)
}
return urls
}
func populateGatewayInfo(un *unstructured.Unstructured, res *ResourceInfo) {
ingress := getGatewayAddresses(un)
var urls []string
if res.NetworkingInfo != nil {
urls = res.NetworkingInfo.ExternalURLs
}
// Check for ignore default links annotation
enableDefaultExternalURLs := true
if ignoreVal, ok := un.GetAnnotations()[common.AnnotationKeyIgnoreDefaultLinks]; ok {
if ignoreDefaultLinks, err := strconv.ParseBool(ignoreVal); err == nil {
enableDefaultExternalURLs = !ignoreDefaultLinks
}
}
if enableDefaultExternalURLs {
generatedURLs := generateGatewayURLs(un, ingress)
urls = append(urls, generatedURLs...)
}
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{
Ingress: ingress,
ExternalURLs: urls,
}
}
func populateGatewayAPIRouteInfo(un *unstructured.Unstructured, res *ResourceInfo) {
targetsMap := make(map[v1alpha1.ResourceRef]bool)
// Extract parent refs (Gateway references)
parentRefs, ok, err := unstructured.NestedSlice(un.Object, "spec", "parentRefs")
if err != nil {
log.Warnf("Failed to get parentRefs for %s %s/%s: %v", un.GetKind(), un.GetNamespace(), un.GetName(), err)
}
if ok && err == nil {
for _, parentRef := range parentRefs {
parent, ok := parentRef.(map[string]any)
if !ok {
continue
}
name, _, errName := unstructured.NestedString(parent, "name")
if errName != nil {
log.Warnf("Failed to get parentRef name for %s %s/%s: %v", un.GetKind(), un.GetNamespace(), un.GetName(), errName)
}
if name == "" {
continue
}
// Namespace defaults to HTTPRoute's namespace
namespace, found, _ := unstructured.NestedString(parent, "namespace")
if !found || namespace == "" {
namespace = un.GetNamespace()
}
// Group defaults to "gateway.networking.k8s.io"
group, found, _ := unstructured.NestedString(parent, "group")
if !found || group == "" {
group = "gateway.networking.k8s.io"
}
// Kind defaults to "Gateway"
kind, found, _ := unstructured.NestedString(parent, "kind")
if !found || kind == "" {
kind = "Gateway"
}
targetsMap[v1alpha1.ResourceRef{
Group: group,
Kind: kind,
Namespace: namespace,
Name: name,
}] = true
}
}
// Extract backend refs from all rules
rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "rules")
if ok && err == nil {
for _, rule := range rules {
ruleMap, ok := rule.(map[string]any)
if !ok {
continue
}
backendRefs, ok, err := unstructured.NestedSlice(ruleMap, "backendRefs")
if !ok || err != nil {
continue
}
for _, backendRef := range backendRefs {
backend, ok := backendRef.(map[string]any)
if !ok {
continue
}
name, _, _ := unstructured.NestedString(backend, "name")
if name == "" {
continue
}
// Namespace defaults to HTTPRoute's namespace if not specified
namespace, found, _ := unstructured.NestedString(backend, "namespace")
if !found || namespace == "" {
namespace = un.GetNamespace()
}
// Group defaults to "" (core API) if not specified
group, _, _ := unstructured.NestedString(backend, "group")
// Kind defaults to "Service" if not specified
kind, found, _ := unstructured.NestedString(backend, "kind")
if !found || kind == "" {
kind = kube.ServiceKind
}
targetsMap[v1alpha1.ResourceRef{
Group: group,
Kind: kind,
Namespace: namespace,
Name: name,
}] = true
}
}
}
targets := make([]v1alpha1.ResourceRef, 0, len(targetsMap))
for target := range targetsMap {
targets = append(targets, target)
}
var urls []string
if res.NetworkingInfo != nil {
urls = res.NetworkingInfo.ExternalURLs
}
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{
TargetRefs: targets,
ExternalURLs: urls,
}
}
func isPodInitializedConditionTrue(status *corev1.PodStatus) bool {
for _, condition := range status.Conditions {
if condition.Type != corev1.PodInitialized {
continue
}
return condition.Status == corev1.ConditionTrue
}
return false
}
func isRestartableInitContainer(initContainer *corev1.Container) bool {
if initContainer == nil {
return false
}
if initContainer.RestartPolicy == nil {
return false
}
return *initContainer.RestartPolicy == corev1.ContainerRestartPolicyAlways
}
func isPodPhaseTerminal(phase corev1.PodPhase) bool {
return phase == corev1.PodFailed || phase == corev1.PodSucceeded
}
func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
pod := corev1.Pod{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &pod)
if err != nil {
return
}
restarts := 0
totalContainers := len(pod.Spec.Containers)
readyContainers := 0
podPhase := pod.Status.Phase
reason := string(podPhase)
if pod.Status.Reason != "" {
reason = pod.Status.Reason
}
imagesSet := make(map[string]bool)
for _, container := range pod.Spec.InitContainers {
imagesSet[container.Image] = true
}
for _, container := range pod.Spec.Containers {
imagesSet[container.Image] = true
}
res.Images = nil
for image := range imagesSet {
res.Images = append(res.Images, image)
}
// If the Pod carries {type:PodScheduled, reason:SchedulingGated}, set reason to 'SchedulingGated'.
for _, condition := range pod.Status.Conditions {
if condition.Type == corev1.PodScheduled && condition.Reason == corev1.PodReasonSchedulingGated {
reason = corev1.PodReasonSchedulingGated
}
}
initContainers := make(map[string]*corev1.Container)
for i := range pod.Spec.InitContainers {
initContainers[pod.Spec.InitContainers[i].Name] = &pod.Spec.InitContainers[i]
if isRestartableInitContainer(&pod.Spec.InitContainers[i]) {
totalContainers++
}
}
initializing := false
for i := range pod.Status.InitContainerStatuses {
container := pod.Status.InitContainerStatuses[i]
restarts += int(container.RestartCount)
switch {
case container.State.Terminated != nil && container.State.Terminated.ExitCode == 0:
continue
case isRestartableInitContainer(initContainers[container.Name]) &&
container.Started != nil && *container.Started:
if container.Ready {
readyContainers++
}
continue
case container.State.Terminated != nil:
// initialization is failed
if container.State.Terminated.Reason == "" {
if container.State.Terminated.Signal != 0 {
reason = fmt.Sprintf("Init:Signal:%d", container.State.Terminated.Signal)
} else {
reason = fmt.Sprintf("Init:ExitCode:%d", container.State.Terminated.ExitCode)
}
} else {
reason = "Init:" + container.State.Terminated.Reason
}
initializing = true
case container.State.Waiting != nil && container.State.Waiting.Reason != "" && container.State.Waiting.Reason != "PodInitializing":
reason = "Init:" + container.State.Waiting.Reason
initializing = true
default:
reason = fmt.Sprintf("Init:%d/%d", i, len(pod.Spec.InitContainers))
initializing = true
}
break
}
if !initializing || isPodInitializedConditionTrue(&pod.Status) {
hasRunning := false
for _, v := range slices.Backward(pod.Status.ContainerStatuses) {
container := v
restarts += int(container.RestartCount)
switch {
case container.State.Waiting != nil && container.State.Waiting.Reason != "":
reason = container.State.Waiting.Reason
case container.State.Terminated != nil && container.State.Terminated.Reason != "":
reason = container.State.Terminated.Reason
case container.State.Terminated != nil && container.State.Terminated.Reason == "":
if container.State.Terminated.Signal != 0 {
reason = fmt.Sprintf("Signal:%d", container.State.Terminated.Signal)
} else {
reason = fmt.Sprintf("ExitCode:%d", container.State.Terminated.ExitCode)
}
case container.Ready && container.State.Running != nil:
hasRunning = true
readyContainers++
}
}
// change pod status back to "Running" if there is at least one container still reporting as "Running" status
if reason == "Completed" && hasRunning {
reason = "Running"
}
}
// "NodeLost" = https://github.com/kubernetes/kubernetes/blob/cb8ad64243d48d9a3c26b11b2e0945c098457282/pkg/util/node/node.go#L46
// But depending on the k8s.io/kubernetes package just for a constant
// is not worth it.
// See https://github.com/argoproj/argo-cd/issues/5173
// and https://github.com/kubernetes/kubernetes/issues/90358#issuecomment-617859364
if pod.DeletionTimestamp != nil && pod.Status.Reason == "NodeLost" {
reason = "Unknown"
// If the pod is being deleted and the pod phase is not succeeded or failed, set the reason to "Terminating".
// See https://github.com/kubernetes/kubectl/issues/1595#issuecomment-2080001023
} else if pod.DeletionTimestamp != nil && !isPodPhaseTerminal(podPhase) {
reason = "Terminating"
}
if reason != "" {
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
}
req := resourcehelper.PodRequests(&pod, resourcehelper.PodResourcesOptions{UseStatusResources: true})
res.PodInfo = &PodInfo{NodeName: pod.Spec.NodeName, ResourceRequests: req, Phase: pod.Status.Phase}
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Node", Value: pod.Spec.NodeName})
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
if restarts > 0 {
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Restart Count", Value: strconv.Itoa(restarts)})
}
// Requests are relevant even for pods in the init phase or pending state (e.g., due to insufficient resources),
// as they help with diagnosing scheduling and startup issues.
// requests will be released for terminated pods either with success or failed state termination.
if !isPodPhaseTerminal(pod.Status.Phase) {
CPUReq := req[corev1.ResourceCPU]
MemoryReq := req[corev1.ResourceMemory]
res.Info = append(res.Info, v1alpha1.InfoItem{Name: common.PodRequestsCPU, Value: strconv.FormatInt(CPUReq.MilliValue(), 10)})
res.Info = append(res.Info, v1alpha1.InfoItem{Name: common.PodRequestsMEM, Value: strconv.FormatInt(MemoryReq.MilliValue(), 10)})
}
var urls []string
if res.NetworkingInfo != nil {
urls = res.NetworkingInfo.ExternalURLs
}
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{Labels: un.GetLabels(), ExternalURLs: urls}
}
func populateHostNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
node := corev1.Node{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &node)
if err != nil {
return
}
res.NodeInfo = &NodeInfo{
Name: node.Name,
Capacity: node.Status.Capacity,
SystemInfo: node.Status.NodeInfo,
Labels: node.Labels,
}
}
func populateApplicationInfo(un *unstructured.Unstructured, res *ResourceInfo) {
// Add managed-by-url annotation to info if present
if managedByURL, ok := un.GetAnnotations()[v1alpha1.AnnotationKeyManagedByURL]; ok {
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "managed-by-url", Value: managedByURL})
}
}
func generateManifestHash(un *unstructured.Unstructured, ignores []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts normalizers.IgnoreNormalizerOpts) (string, error) {
normalizer, err := normalizers.NewIgnoreNormalizer(ignores, overrides, opts)
if err != nil {
return "", fmt.Errorf("error creating normalizer: %w", err)
}
resource := un.DeepCopy()
err = normalizer.Normalize(resource)
if err != nil {
return "", fmt.Errorf("error normalizing resource: %w", err)
}
data, err := resource.MarshalJSON()
if err != nil {
return "", fmt.Errorf("error marshaling resource: %w", err)
}
hash := hash(data)
return hash, nil
}
func hash(data []byte) string {
return strconv.FormatUint(xxhash.Sum64(data), 16)
}