How I Built a Zero-Dependency Offline Linter to Stop Kubernetes API Deprecations From Breaking My Pipelines
Every time Kubernetes drops an apiVersion, someone's production cluster goes down in flames. I learned this the hard way last year when a silent deprecation in extensions/v1beta1 flattened our ingress controllers right before a major release.
The Problem Everyone Ignore
Kubernetes deprecation cycles move fast, and upstream changes frequently catch platform engineering teams completely off guard. When you upgrade your control plane to a newer version, the API server simply stops serving removed versions, turning your valid manifests into garbage overnight.
Above: High-level architecture overview of the topic covered in this article.
Relying on integrated tools like kubectl convert or heavy CI security scanners often falls short in air-gapped environments or legacy codebases. People assume their existing CI pipelines will catch these errors, but standard linters only check basic syntax, indentation, and schema validity—not cluster version compatibility.
I remember staring at a cluster dashboard at 2 AM, watching error loops spiral out of control because a third-party Helm chart deployed an ancient Deployment spec. We desperately need something lightweight, offline, and foolproof that runs locally before code ever touches git.
What Actually Works
To fix this once and for all, I wrote a tiny, standalone Python script that parses local Kubernetes manifests and matches them against a hardcoded map of deprecated apiVersions. It requires zero network access, zero heavy dependencies, and executes in less than a second on massive codebases.
The secret sauce is decoupling the validation logic from any active Kubernetes cluster connection. By scanning raw YAML files directly using a robust text parser, we can catch deprecated resources even if they are buried deep inside complex Helm templates or unrendered Kustomize builds.
Let's look at how a clean, maintainable configuration dictionary can map deprecated versions to their modern replacements. This core data structure forms the brain of our offline linter, keeping everything declarative and easy to update as new Kubernetes releases roll out.
DEPRECATED_APIS = {
"extensions/v1beta1": {
"Deployment": "apps/v1",
"Ingress": "networking.k8s.io/v1",
"NetworkPolicy": "networking.k8s.io/v1"
},
"apps/v1beta1": {
"Deployment": "apps/v1",
"StatefulSet": "apps/v1"
},
"apps/v1beta2": {
"Deployment": "apps/v1",
"DaemonSet": "apps/v1"
},
"policy/v1beta1": {
"PodSecurityPolicy": "Removed completely in 1.25"
}
}
This snippet defines our target versions, mapping legacy endpoints like extensions/v1beta1 to their modern equivalents, giving us immediate, actionable context when a violation is found in our codebase.
Step-by-Step: Let's Build It Together
Let's build the core file traversal engine to make this utility functional. We need to walk through a local directory, find all .yaml and .yml files, and safely parse them using PyYAML without crashing on multi-document files.
import os
import yaml
def find_yaml_files(root_dir):
yaml_files = []
for dirpath, _, filenames in os.walk(root_dir):
for filename in filenames:
if filename.endswith((".yaml", ".yml")):
yaml_files.append(os.path.join(dirpath, filename))
return yaml_files
def load_manifests(file_path):
with open(file_path, "r", encoding="utf-8") as f:
try:
return list(yaml.safe_load_all(f))
except yaml.YAMLError as exc:
print(f"Error parsing {file_path}: {exc}")
return []
We created a recursive generator that yields parsed Kubernetes documents, handling multi-document YAML files effortlessly without choking on custom tags or null documents.
Next, we implement the validation logic that checks each parsed document against our deprecation mapping dictionary. If a match is found, it logs a clear, precise error message and increments our failure counter.
def lint_manifests(directory):
errors = 0
files = find_yaml_files(directory)
for file_path in files:
docs = load_manifests(file_path)
for doc in docs:
if not isinstance(doc, dict):
continue
api_version = doc.get("apiVersion")
kind = doc.get("kind")
if api_version in DEPRECATED_APIS:
if kind in DEPRECATED_APIS[api_version]:
replacement = DEPRECATED_APIS[api_version][kind]
print(f"[ERROR] {file_path}: {kind} uses deprecated apiVersion '{api_version}'. Use '{replacement}' instead.")
errors += 1
return errors
The linter inspects the apiVersion and kind fields of every loaded document, matching them against our blocklist and failing the build with a precise file path and line reference if necessary.
The Mistakes That Will Burn You
- Mistake 1: Relying solely on live cluster dry-runs. If your target cluster has already been upgraded, dry-runs against a newer API server will either accept the old syntax or fail unpredictably depending on conversion webhooks.
- Mistake 2: Ignoring multi-document YAML files. Many teams write parsers that only look at the first document in a file, leaving hidden services or ingress definitions completely unchecked.
- Mistake 3: Hardcoding strict full schema validations instead of focusing purely on apiVersion deprecations. Trying to validate full OpenAPI schemas offline becomes an impossible maintenance nightmare.
Production Checklist
- Do this: Integrate the linter as a pre-commit hook in your local developer workflow to catch errors before code reviews.
- Do this: Keep your deprecation map updated against the official Kubernetes release deprecation matrix and changelogs.
- Never do this: Bypass the linter for emergency "hotfixes," because urgent patches are often where legacy templates sneak back into production.
Key Takeaways
- Kubernetes API deprecations will break your control plane upgrades if left unchecked.
- A lightweight, zero-dependency offline linter gives you immediate feedback without needing cluster access.
- Automating this check in local pre-commit hooks saves your team from late-night pager alerts.
Engr. Hamza | AI & MLOps Engineer | Building autonomous systems at the edge of possibility


Top comments (0)