package main # Kubernetes normalizes resource quantity values on write. ArgoCD diffs by # string comparison, so a non-canonical value in git will always differ from # the live object, causing permanent OutOfSync. # # Rules enforced here: # CPU integers — k8s converts integer 1 to string "1" (see PR #163) # CPU milliCPU — k8s converts 1000m → "1", 2000m → "2", etc. (PR #164) # Memory Mi→Gi — k8s converts 1024Mi → 1Gi, 2048Mi → 2Gi, etc. (PR #163) # clusterIP null — k8s assigns a real IP; null in git always differs # from the live assigned value (see PR #166) # ---- Container helpers ---- # Extracts containers from Deployment/StatefulSet/DaemonSet/Job/CronJob/Pod _containers contains c if { c := input.spec.template.spec.containers[_] } _containers contains c if { c := input.spec.template.spec.initContainers[_] } _containers contains c if { c := input.spec.containers[_] } _containers contains c if { c := input.spec.initContainers[_] } _containers contains c if { c := input.spec.jobTemplate.spec.template.spec.containers[_] } # ---- CPU: must not be an integer ---- # YAML `cpu: 1` (unquoted) parses as JSON integer; k8s stores as string "1". deny contains msg if { c := _containers[_] cpu := c.resources.limits.cpu is_number(cpu) msg := sprintf( "%s container %q: cpu limit %v is an unquoted integer — use \"%v\" (string) to prevent ArgoCD OutOfSync", [input.kind, c.name, cpu, cpu], ) } deny contains msg if { c := _containers[_] cpu := c.resources.requests.cpu is_number(cpu) msg := sprintf( "%s container %q: cpu request %v is an unquoted integer — use \"%v\" (string) to prevent ArgoCD OutOfSync", [input.kind, c.name, cpu, cpu], ) } deny contains msg if { input.kind == "Cluster" cpu := input.spec.resources.limits.cpu is_number(cpu) msg := sprintf( "Cluster %s/%s: cpu limit %v is an unquoted integer — use \"%v\" (string) to prevent ArgoCD OutOfSync", [input.metadata.namespace, input.metadata.name, cpu, cpu], ) } deny contains msg if { input.kind == "Cluster" cpu := input.spec.resources.requests.cpu is_number(cpu) msg := sprintf( "Cluster %s/%s: cpu request %v is an unquoted integer — use \"%v\" (string) to prevent ArgoCD OutOfSync", [input.metadata.namespace, input.metadata.name, cpu, cpu], ) } # ---- CPU: milliCPU divisible by 1000 normalizes to a whole number ---- # k8s converts 1000m → "1", 2000m → "2". Use the canonical whole-number form. _milli_whole(cpu) := n if { is_string(cpu) endswith(cpu, "m") val := to_number(substring(cpu, 0, count(cpu) - 1)) val % 1000 == 0 val > 0 n := val / 1000 } deny contains msg if { c := _containers[_] n := _milli_whole(c.resources.limits.cpu) msg := sprintf( "%s container %q: cpu limit %q normalizes to \"%v\" — use canonical form to prevent ArgoCD OutOfSync", [input.kind, c.name, c.resources.limits.cpu, n], ) } deny contains msg if { c := _containers[_] n := _milli_whole(c.resources.requests.cpu) msg := sprintf( "%s container %q: cpu request %q normalizes to \"%v\" — use canonical form to prevent ArgoCD OutOfSync", [input.kind, c.name, c.resources.requests.cpu, n], ) } deny contains msg if { input.kind == "Cluster" n := _milli_whole(input.spec.resources.limits.cpu) msg := sprintf( "Cluster %s/%s: cpu limit %q normalizes to \"%v\" — use canonical form to prevent ArgoCD OutOfSync", [input.metadata.namespace, input.metadata.name, input.spec.resources.limits.cpu, n], ) } deny contains msg if { input.kind == "Cluster" n := _milli_whole(input.spec.resources.requests.cpu) msg := sprintf( "Cluster %s/%s: cpu request %q normalizes to \"%v\" — use canonical form to prevent ArgoCD OutOfSync", [input.metadata.namespace, input.metadata.name, input.spec.resources.requests.cpu, n], ) } # ---- Memory: Mi values divisible by 1024 normalize to Gi ---- # k8s converts 1024Mi → 1Gi, 2048Mi → 2Gi, etc. _mi_canonical(mem) := canonical if { endswith(mem, "Mi") val := to_number(substring(mem, 0, count(mem) - 2)) val % 1024 == 0 val > 0 canonical := sprintf("%vGi", [val / 1024]) } deny contains msg if { c := _containers[_] canonical := _mi_canonical(c.resources.limits.memory) msg := sprintf( "%s container %q: memory limit %q normalizes to %q — use canonical form to prevent ArgoCD OutOfSync", [input.kind, c.name, c.resources.limits.memory, canonical], ) } deny contains msg if { c := _containers[_] canonical := _mi_canonical(c.resources.requests.memory) msg := sprintf( "%s container %q: memory request %q normalizes to %q — use canonical form to prevent ArgoCD OutOfSync", [input.kind, c.name, c.resources.requests.memory, canonical], ) } deny contains msg if { input.kind == "Cluster" canonical := _mi_canonical(input.spec.resources.limits.memory) msg := sprintf( "Cluster %s/%s: memory limit %q normalizes to %q — use canonical form to prevent ArgoCD OutOfSync", [input.metadata.namespace, input.metadata.name, input.spec.resources.limits.memory, canonical], ) } deny contains msg if { input.kind == "Cluster" canonical := _mi_canonical(input.spec.resources.requests.memory) msg := sprintf( "Cluster %s/%s: memory request %q normalizes to %q — use canonical form to prevent ArgoCD OutOfSync", [input.metadata.namespace, input.metadata.name, input.spec.resources.requests.memory, canonical], ) } # ---- Service: clusterIP must not be null ---- # k8s assigns a real IP on creation; clusterIP is immutable afterward. # Setting null in git means desired=null vs live=10.x.x.x → permanent OutOfSync. # Remove the field and let k8s own it. deny contains msg if { input.kind == "Service" input.spec.clusterIP == null msg := sprintf( "Service %s/%s has 'clusterIP: null' — remove this field; Kubernetes assigns the IP on creation and it causes ArgoCD OutOfSync", [input.metadata.namespace, input.metadata.name], ) }