Hamburger Cross Icon
Min Kubectl Version
+
Kubernetes Collector

Min Kubectl Version + Kubernetes Collector

✓ Guardrail ↓ Collector Stable Deployment And Infrastructure

Enforce Min Kubectl Version using data collected by Kubernetes Collector. Automatically check deployment and infrastructure standards on every PR.

Guardrail: Ensures the kubectl version used in CI meets the minimum required version. Old kubectl clients may miss security fixes or lack support for current cluster APIs.
Data Source: Parse Kubernetes YAML manifests and render Helm charts to extract workloads, containers, resource limits, probes, PodDisruptionBudgets, autoscalers, and NetworkPolicies. Capture kubectl commands in CI for deployment traceability.

How Kubernetes Collector Powers This Guardrail

The Kubernetes Collector gathers metadata from your orchestration, containers, ci cd systems. This data flows into Lunar's Component JSON, where the Min Kubectl Version guardrail evaluates it against your standards.

When enabled, this check runs automatically on every PR and in AI coding workflows, providing real-time enforcement with actionable feedback.

1
↓ Kubernetes Collector Gathers Data Collector
Extracts metadata from code, configs, and tool outputs
2
{ } Component JSON
Data centralized in structured format for evaluation
3
✓ Min Kubectl Version Checks Guardrail
Pass/fail result with actionable feedback in PRs

Quick Start Configuration

Add both the collector and policy to your lunar-config.yml to enable this guardrail.

📄 lunar-config.yml
# Step 1: Enable the Kubernetes Collector
collectors:
  - uses: github://earthly/lunar-lib/collectors/k8s@v1.0.5
    # with: ...

# Step 2: Enable the Kubernetes Guardrails
policies:
  - uses: github://earthly/lunar-lib/policies/k8s@v1.0.5
    include: [min-kubectl-version]
    # with: ...

What Kubernetes Collector Collects

This collector gathers the following data that the Min Kubectl Version guardrail evaluates.

Collector code

k8s

Parses all Kubernetes manifests in the repository using kubeconform validation and collects:

  • Manifest validity and parse errors
  • Workloads (Deployments, StatefulSets, DaemonSets, Jobs, CronJobs)
  • Container specs with resource requests/limits, probes, and shutdown hooks
  • PodDisruptionBudgets (PDBs)
  • HorizontalPodAutoscalers (HPAs) and KEDA ScaledObjects
  • NetworkPolicies (pod selector, policy types, egress rules) Helm chart templates are rendered by the helm sub-collector instead.
Collector code

helm

Renders each Helm chart in the repository with helm template, once per values set in helm_values or derived from its file names with helm_values_chains, and parses the output the same way as a static manifest. Rendered objects land in the same .k8s arrays, tagged with the chart and values files that produced them. A chart no values set applies to is rendered with its own values.yaml only to check that it builds: it gets a .k8s.manifests[] entry and no objects. A chart that fails to render is recorded as an invalid manifest, so the valid check fails instead of every check skipping.

Collector ci-after-command

cicd

Records every kubectl command executed in CI pipelines along with the kubectl client version. Writes command strings and version info to .k8s.cicd for audit trails and deployment traceability (kubectl apply, kubectl rollout, etc.).

Example Data Flow

Here's an example of the data that Kubernetes Collector writes to the Component JSON, which Min Kubectl Version then evaluates.

{ } component.json From Kubernetes Collector
{
  "k8s": {
    "source": {
      "tool": "kubeconform",
      "version": "0.8.0"
    },
    "manifests": [
      {
        "path": "deploy/deployment.yaml",
        "valid": true,
        "resources": [
          {"kind": "Deployment", "name": "payment-api", "namespace": "payments", "api_version": "apps/v1"}
        ]
      },
      {
        "path": "charts/worker",
        "render": {"chart": "charts/worker", "values": ["values.yaml", "values-prod.yaml"], "validated_only": false},
        "valid": true,
        "resources": [
          {"kind": "Deployment", "name": "worker", "namespace": "default", "api_version": "apps/v1"},
          {"kind": "PodDisruptionBudget", "name": "worker", "namespace": "default", "api_version": "policy/v1"},
          {"kind": "ScaledObject", "name": "worker", "namespace": "default", "api_version": "keda.sh/v1alpha1"}
        ]
      },
      {
        "path": "charts/reports",
        "render": {"chart": "charts/reports", "values": ["values.yaml"], "validated_only": true},
        "valid": false,
        "error": "helm template: An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: found in Chart.yaml, but missing in charts/ directory: postgresql"
      }
    ],
    "workloads": [
      {
        "kind": "Deployment",
        "name": "payment-api",
        "namespace": "payments",
        "path": "deploy/deployment.yaml",
        "replicas": 1,
        "replicas_set": false,
        "pod_labels": {"app": "payment-api", "tier": "api"},
        "pod_annotations": {"prometheus.io/scrape": "true"},
        "host_users": false,
        "host_network": false,
        "host_pid": false,
        "host_ipc": false,
        "termination_grace_period_seconds": 60,
        "topology_spread_constraints": [
          {
            "topology_key": "topology.kubernetes.io/zone",
            "max_skew": 1,
            "when_unsatisfiable": "ScheduleAnyway",
            "label_selector": {"matchLabels": {"app": "payment-api"}},
            "match_label_keys": ["pod-template-hash"]
          }
        ],
        "containers": [
          {
            "name": "api",
            "image": "gcr.io/acme/payment-api:v1.2.3",
            "has_resources": true,
            "has_requests": true,
            "has_limits": true,
            "cpu_request": "100m",
            "cpu_limit": "500m",
            "memory_request": "128Mi",
            "memory_limit": "512Mi",
            "has_liveness_probe": true,
            "has_readiness_probe": true,
            "liveness_probe": {"handler": "httpGet", "port": 8080, "path": "/livez", "initial_delay_seconds": 0, "period_seconds": 10, "timeout_seconds": 2, "failure_threshold": 3},
            "readiness_probe": {"handler": "httpGet", "port": 8080, "path": "/readyz", "initial_delay_seconds": 0, "period_seconds": 5, "timeout_seconds": 2, "failure_threshold": 3},
            "startup_probe": null,
            "has_prestop": true,
            "runs_as_non_root": true,
            "read_only_root_fs": true,
            "privileged": false
          }
        ],
        "init_containers": []
      },
      {
        "kind": "Deployment",
        "name": "worker",
        "namespace": "default",
        "path": "charts/worker/templates/deployment.yaml",
        "render": {"chart": "charts/worker", "values": ["values.yaml", "values-prod.yaml"]},
        "replicas": 1,
        "replicas_set": false,
        "pod_labels": {"app.kubernetes.io/name": "worker"},
        "pod_annotations": {},
        "host_users": true,
        "host_network": false,
        "host_pid": false,
        "host_ipc": false,
        "termination_grace_period_seconds": 30,
        "topology_spread_constraints": [],
        "containers": [
          {
            "name": "worker",
            "image": "ghcr.io/acme/worker:2.4.0",
            "has_resources": true,
            "has_requests": true,
            "has_limits": true,
            "cpu_request": "250m",
            "cpu_limit": "1",
            "memory_request": "256Mi",
            "memory_limit": "512Mi",
            "has_liveness_probe": true,
            "has_readiness_probe": true,
            "liveness_probe": {"handler": "httpGet", "port": "http", "path": "/healthz", "initial_delay_seconds": 0, "period_seconds": 10, "timeout_seconds": 1, "failure_threshold": 3},
            "readiness_probe": {"handler": "httpGet", "port": "http", "path": "/healthz", "initial_delay_seconds": 0, "period_seconds": 10, "timeout_seconds": 1, "failure_threshold": 3},
            "startup_probe": null,
            "has_prestop": false,
            "runs_as_non_root": true,
            "read_only_root_fs": false,
            "privileged": false
          }
        ],
        "init_containers": [
          {
            "name": "migrate",
            "image": "ghcr.io/acme/worker:2.4.0",
            "has_resources": false,
            "has_requests": false,
            "has_limits": false,
            "cpu_request": null,
            "cpu_limit": null,
            "memory_request": null,
            "memory_limit": null,
            "has_liveness_probe": false,
            "has_readiness_probe": false,
            "liveness_probe": null,
            "readiness_probe": null,
            "startup_probe": null,
            "has_prestop": false,
            "runs_as_non_root": true,
            "read_only_root_fs": false,
            "privileged": false
          }
        ]
      }
    ],
    "pdbs": [
      {
        "name": "payment-api-pdb",
        "namespace": "payments",
        "path": "deploy/pdb.yaml",
        "selector": {"matchLabels": {"app": "payment-api"}},
        "target_workload": "payment-api",
        "min_available": 2,
        "max_unavailable": null
      },
      {
        "name": "worker",
        "namespace": "default",
        "path": "charts/worker/templates/pdb.yaml",
        "render": {"chart": "charts/worker", "values": ["values.yaml", "values-prod.yaml"]},
        "selector": {"matchLabels": {"app.kubernetes.io/name": "worker"}},
        "target_workload": "worker",
        "min_available": null,
        "max_unavailable": 1
      }
    ],
    "hpas": [
      {
        "name": "payment-api-hpa",
        "namespace": "payments",
        "path": "deploy/hpa.yaml",
        "target_workload": "payment-api",
        "target_kind": "Deployment",
        "min_replicas": 3,
        "max_replicas": 10
      }
    ],
    "scaled_objects": [
      {
        "name": "worker",
        "namespace": "default",
        "path": "charts/worker/templates/scaledobject.yaml",
        "render": {"chart": "charts/worker", "values": ["values.yaml", "values-prod.yaml"]},
        "target_workload": "worker",
        "target_kind": "Deployment",
        "min_replicas": 2,
        "max_replicas": 20
      }
    ],
    "network_policies": [
      {
        "name": "egress-no-metadata",
        "namespace": "payments",
        "path": "deploy/netpol.yaml",
        "pod_selector": {},
        "policy_types": ["Egress"],
        "egress": [
          {"to": [{"ipBlock": {"cidr": "0.0.0.0/0", "except": ["169.254.169.254/32"]}}]}
        ]
      }
    ],
    "cicd": {
      "cmds": [
        {"cmd": "kubectl apply -f deploy/", "version": "1.29.2"},
        {"cmd": "kubectl rollout status deployment/payment-api", "version": "1.29.2"}
      ],
      "source": {"tool": "kubectl", "integration": "ci"}
    }
  }
}

Configuration Options

Kubernetes Collector Inputs

Input Required Default Description
find_command Optional find . -type f \( -name '*.yaml' -o -name '*.yml' \) Command to find K8s manifest files (must output one file path per line)
helm_find_command Optional find . -type f \( -name 'Chart.yaml' -o -name 'Chart.yml' \) Command to find the Helm charts to render, as Chart.yaml paths (one per line). A subchart in another chart's charts/ directory is rendered with its parent, not on its own.
helm_values Required — Values sets to check each chart's objects with, one per line. A line lists values files, relative to the chart directory, applied in order on top of the chart's values.yaml; a glob gives one render per matching file, and a line of just `values.yaml` checks the defaults. A chart skips lines whose files it doesn't have. A chart no line or chain applies to is only checked to build: its values.yaml render records validity and no objects. `#` starts a comment.
helm_values_chains Required — Derive values sets from each chart's file names: `overlays` or `all`. Empty (the default) is off. An overlay extends the ones its name continues at a dash (values-prod-eu.yaml extends values-prod.yaml), and each values-*.yaml that nothing extends is one chain: values.yaml, the overlays it extends, then itself. A chart with no values-*.yaml stays build-only under `overlays`, and is checked with values.yaml alone under `all`, which is only right where every chart is a deployed app, not a chart library. Adds to the sets in helm_values.
helm_dependency_build Optional false Run `helm dependency build` before rendering a chart whose remote dependencies aren't vendored in its charts/ directory (true/false). Needs network access to the chart repositories. When off, such a chart fails to render and is reported as invalid. Local file:// dependencies are always built.
helm_api_versions Required — Comma-separated API versions to render charts as if the cluster served them (`helm template --api-versions`), for charts that only render an object when its CRD is installed (e.g. "keda.sh/v1alpha1"). Core Kubernetes APIs are always available.

Kubernetes Guardrails Inputs

Input Required Default Description
min_replicas Optional 3 Minimum replicas required for HPAs (default 3)
max_limit_to_request_ratio Optional 4 Maximum ratio of limits to requests for CPU/memory (default 4)
min_kubectl_version Optional 1.28 Minimum kubectl version required in CI (semver, e.g. "1.28")
metadata_ips Optional 169.254.169.254 Comma-separated metadata endpoint IPs that metadata-egress-blocked keeps pods from reaching on TCP port 80 (add fd00:ec2::254 for IPv6 EKS clusters)
topology_key Optional topology.kubernetes.io/zone Node label topology-spread requires a constraint on (default topology.kubernetes.io/zone)
topology_max_skew Optional 1 Largest maxSkew topology-spread accepts on that constraint (default 1)
topology_when_unsatisfiable Required — whenUnsatisfiable value topology-spread requires on that constraint (DoNotSchedule or ScheduleAnyway). Empty accepts either.
topology_require_match_label_keys Optional false Also require the constraint to set matchLabelKeys with pod-template-hash, so a rollout's skew counts only the new revision's pods (true/false)
min_termination_grace_period_seconds Optional 30 Smallest terminationGracePeriodSeconds graceful-shutdown accepts. An unset value counts as the Kubernetes default, 30 (default 30)
require_prestop Optional true Whether graceful-shutdown requires a preStop hook on at least one container (true/false)
min_probe_timeout_seconds Optional 1 Smallest probe timeoutSeconds probe-timeouts accepts. An unset value counts as the Kubernetes default, 1 (default 1)
allowed_registries Required — Comma-separated registries, or registry/path prefixes, images may come from (e.g. "ghcr.io/acme,123456789012.dkr.ecr.us-east-1.amazonaws.com"). An image with no registry counts as docker.io. Empty skips allowed-registries.
deprecated_api_versions Required — Comma-separated apiVersions deprecated-api-versions fails, each optionally narrowed to one kind (e.g. "policy/v1beta1,networking.k8s.io/v1beta1:Ingress"). Empty skips the check.
required_pod_annotations Required — Comma-separated annotation keys every workload's pod template must set. Empty requires none.
forbidden_pod_annotations Required — Comma-separated annotation keys no workload's pod template may set (e.g. "karpenter.sh/do-not-disrupt"). Empty forbids none.

Ready to Automate Your Standards?

See how Lunar can turn your AGENTS.md, engineering wiki, compliance docs, or postmortem action items into automated guardrails with our 200+ built-in guardrails.

Works with any process
check AI agent rules & prompt files
check Post-mortem action items
check Security & compliance policies
check Testing & quality requirements
Auto-ID My Guardrails
Paste your AGENTS.md or manual process doc and get guardrails in minutes
Book a Demo