Min Kubectl Version + Kubernetes Collector
Enforce Min Kubectl Version using data collected by Kubernetes Collector. Automatically check deployment and infrastructure standards on every PR.
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.
Quick Start Configuration
Add both the collector and policy to your lunar-config.yml to enable this guardrail.
# 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.
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
helmsub-collector instead.
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.
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.
{
"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.