#!/usr/bin/env bash
#
# Enforce CPU and memory limits on all non-excluded Pods by patching their controllers.
# - Skips kube-system, kube-public, kube-node-lease
# - For each container with missing limits, sets:
# limits.cpu: "500m"
# limits.memory: "512Mi"
# - Patches Deployments, StatefulSets, DaemonSets, ReplicaSets, Jobs, CronJobs.
# - Standalone Pods are reported but NOT modified (edit their manifests/IaC).
#
# Run on: any machine with kubectl and jq access to the cluster.
# Idempotent: safe to re-run; existing limits are preserved.
set -euo pipefail
DEFAULT_CPU_LIMIT="500m"
DEFAULT_MEM_LIMIT="512Mi"
# Ensure kubectl and jq are available
command -v kubectl >/dev/null 2>&1 || { echo "kubectl not found in PATH" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq not found in PATH" >&2; exit 1; }
echo "Scanning pods for containers missing CPU or memory limits..."
# Get all relevant pods as JSON once
PODS_JSON="$(kubectl get pods --all-namespaces -o json)"
# Build a list of affected containers with their owner (if any)
# Output format (tab-separated):
# ns podName ownerKind ownerName ownerUID containerName hasCpu hasMem
AFFECTED_CONTAINERS="$(
echo "${PODS_JSON}" | jq -r '
.items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| (.spec.containers // [])[]
| ((.resources.limits.cpu != null) ) as $hasCpu
| ((.resources.limits.memory != null)) as $hasMem
| select( ( $hasCpu and $hasMem ) | not )
| [
$m.namespace,
$m.name,
(if $own == null then "" else $own.kind end),
(if $own == null then "" else $own.name end),
(if $own == null then "" else $own.uid end),
.name,
(if $hasCpu then "true" else "false" end),
(if $hasMem then "true" else "false" end)
]
| @tsv
'
)"
if [[ -z "${AFFECTED_CONTAINERS}" ]]; then
echo "All scanned containers already have CPU and memory limits set."
echo "Re-running verification command for confirmation..."
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| (.spec.containers // [])[]
| ((.resources.limits.cpu != null) and (.resources.limits.memory != null)) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " container=\(.name) image=\(.image)"
+ " limitsCpu=\(.resources.limits.cpu // "unset") limitsMemory=\(.resources.limits.memory // "unset")"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
exit 0
fi
echo "Found containers missing limits. Grouping by owning controller..."
# Build a unique list of owner controllers to patch
# Output: ns<TAB>ownerKind<TAB>ownerName
OWNERS_TO_PATCH="$(
printf "%s\n" "${AFFECTED_CONTAINERS}" | awk -F'\t' '
$3 != "" && $4 != "" {
key = $1 "\t" $3 "\t" $4
if (!(key in seen)) {
seen[key]=1
print key
}
}
'
)"
# Track standalone pods (no owner) so users can fix their manifests manually
STANDALONE_PODS_REPORTED=()
if [[ -z "${OWNERS_TO_PATCH}" ]]; then
echo "No controller-backed workloads to patch; only standalone Pods are non-compliant."
else
echo "Patching the following controllers with default CPU=${DEFAULT_CPU_LIMIT}, Memory=${DEFAULT_MEM_LIMIT}:"
printf "%s\n" "${OWNERS_TO_PATCH}" | sed $'s/\t/ /g'
# For each controller, construct and apply a strategic merge patch
while IFS=$'\t' read -r NS OWNER_KIND OWNER_NAME; do
[[ -z "${NS}" || -z "${OWNER_KIND}" || -z "${OWNER_NAME}" ]] && continue
# Map kind -> kubectl resource type
case "${OWNER_KIND}" in
Deployment) RES_TYPE="deployment" ;;
StatefulSet) RES_TYPE="statefulset" ;;
DaemonSet) RES_TYPE="daemonset" ;;
ReplicaSet) RES_TYPE="replicaset" ;;
Job) RES_TYPE="job" ;;
CronJob) RES_TYPE="cronjob" ;;
*)
echo "Skipping unsupported controller kind ${OWNER_KIND} (${NS}/${OWNER_NAME})" >&2
continue
;;
esac
echo "Processing ${OWNER_KIND} ${NS}/${OWNER_NAME}..."
# Get the current pod template spec for this controller
CTRL_JSON="$(kubectl -n "${NS}" get "${RES_TYPE}" "${OWNER_NAME}" -o json)"
# Build a patched pod template with limits added where missing and existing limits preserved
PATCH_TEMPLATE="$(
echo "${CTRL_JSON}" | jq --arg cpu "${DEFAULT_CPU_LIMIT}" --arg mem "${DEFAULT_MEM_LIMIT}" '
.spec.template.spec
|= (
.containers |= map(
.resources.limits.cpu |= (if . == null then $cpu else . end)
| .resources.limits.memory |= (if . == null then $mem else . end)
)
)
'
)"
# Extract just the spec.template part for a strategic merge patch
PATCH="$(
echo "${PATCH_TEMPLATE}" | jq '{ spec: { template: . } }'
)"
echo "${PATCH}" | kubectl -n "${NS}" patch "${RES_TYPE}" "${OWNER_NAME}" --type merge -p "$(cat)"
echo "Patched ${OWNER_KIND} ${NS}/${OWNER_NAME}"
done <<< "${OWNERS_TO_PATCH}"
fi
# Report standalone pods (no owning controller) requiring manual manifest/IaC updates
echo
echo "Checking for standalone Pods (no owning controller) with missing limits..."
while IFS=$'\t' read -r NS POD OWNER_KIND OWNER_NAME OWNER_UID CONTAINER HASCPU HASMEM; do
if [[ -z "${OWNER_KIND}" || -z "${OWNER_NAME}" ]]; then
ID="${NS}/${POD}"
if [[ " ${STANDALONE_PODS_REPORTED[*]-} " != *" ${ID} "* ]]; then
STANDALONE_PODS_REPORTED+=("${ID}")
echo "Standalone Pod requires manual fix: ${NS}/${POD} (container ${CONTAINER})"
fi
fi
done <<< "${AFFECTED_CONTAINERS}"
if ((${#STANDALONE_PODS_REPORTED[@]} > 0)); then
echo
echo "For each standalone Pod above, edit its manifest/IaC to set resources.limits.cpu and resources.limits.memory,"
echo "then recreate the Pod from the updated definition."
fi
echo
echo "Re-running verification command to confirm compliance..."
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| (.spec.containers // [])[]
| ((.resources.limits.cpu != null) and (.resources.limits.memory != null)) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " container=\(.name) image=\(.image)"
+ " limitsCpu=\(.resources.limits.cpu // "unset") limitsMemory=\(.resources.limits.memory // "unset")"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'