{"id":48750781,"url":"https://github.com/sholdee/crd-schema-publisher","last_synced_at":"2026-06-03T02:00:35.982Z","repository":{"id":350166316,"uuid":"1205545100","full_name":"sholdee/crd-schema-publisher","owner":"sholdee","description":"Browsable CRD docs and IDE validation schemas, straight from your Kubernetes cluster","archived":false,"fork":false,"pushed_at":"2026-06-02T20:05:09.000Z","size":2672,"stargazers_count":21,"open_issues_count":1,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-02T20:24:23.046Z","etag":null,"topics":["cloudflare-pages","custom-resource-definitions","helm-chart","json-schema","kubeconform","kubernetes","kubernetes-controller"],"latest_commit_sha":null,"homepage":"https://kube-schemas.shold.io","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sholdee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-09T04:01:09.000Z","updated_at":"2026-06-02T20:05:17.000Z","dependencies_parsed_at":null,"dependency_job_id":"a411f434-15ad-4efa-b1fe-b3c69e7e2b4c","html_url":"https://github.com/sholdee/crd-schema-publisher","commit_stats":null,"previous_names":["sholdee/crd-schema-publisher"],"tags_count":23,"template":false,"template_full_name":null,"purl":"pkg:github/sholdee/crd-schema-publisher","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sholdee%2Fcrd-schema-publisher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sholdee%2Fcrd-schema-publisher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sholdee%2Fcrd-schema-publisher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sholdee%2Fcrd-schema-publisher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sholdee","download_url":"https://codeload.github.com/sholdee/crd-schema-publisher/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sholdee%2Fcrd-schema-publisher/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33844687,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare-pages","custom-resource-definitions","helm-chart","json-schema","kubeconform","kubernetes","kubernetes-controller"],"created_at":"2026-04-12T18:31:20.521Z","updated_at":"2026-06-03T02:00:35.975Z","avatar_url":"https://github.com/sholdee.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/sholdee/crd-schema-publisher/main/docs/assets/logo.svg\" alt=\"crd-schema-publisher logo\" width=\"96\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003ecrd-schema-publisher\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  CRD docs and IDE validation, straight from the cluster.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://goreportcard.com/report/github.com/sholdee/crd-schema-publisher\"\u003e\u003cimg src=\"https://goreportcard.com/badge/github.com/sholdee/crd-schema-publisher\" alt=\"Go Report Card\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/sholdee/crd-schema-publisher/actions/workflows/ci.yaml\"\u003e\u003cimg src=\"https://github.com/sholdee/crd-schema-publisher/actions/workflows/ci.yaml/badge.svg\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/License-MIT-blue.svg\" alt=\"License: MIT\"\u003e\u003c/a\u003e\n  \u003ca href=\"go.mod\"\u003e\u003cimg src=\"https://img.shields.io/github/go-mod/go-version/sholdee/crd-schema-publisher\" alt=\"Go Version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://artifacthub.io/packages/helm/crd-schema-publisher/crd-schema-publisher\"\u003e\u003cimg src=\"https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/crd-schema-publisher\" alt=\"Artifact Hub\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\nExtracts CRD schemas from Kubernetes or YAML, converts Kubernetes built-in resource schemas from `/openapi/v2`, and publishes a searchable documentation site with interactive schema pages.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/sholdee/crd-schema-publisher/main/docs/screenshots/overview.gif\" alt=\"Installing crd-schema-publisher, extracting CRD schemas, and browsing the generated schema site\" width=\"720\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://kube-schemas.shold.io\"\u003eLive demo\u003c/a\u003e\n\u003c/p\u003e\n\nRun it as:\n\n- a Kubernetes-native controller for real-time CRD watching\n- a CronJob for scheduled extraction\n- a local CLI for extracting from a live cluster, converting CRD YAML, or rendering built-in schemas from Kubernetes OpenAPI\n\nExports schemas for IDE validation with yaml-language-server and CI linting with kubeconform. Cloudflare Pages and local serving are built in; S3, git repos, and custom web servers are supported via sidecar.\n\n\u003e **Upgrading direct-volume deployments:** the active site now lives at `OUTPUT_DIR/current`. Existing sidecars or scripts that read the shared output volume directly must be updated. Cloudflare Pages users do not need to change anything.\n\n## 💡 Why\n\nMost CRD schema solutions rely on static catalogs — community-maintained repositories that scrape schemas from popular Helm charts. Schemas go stale, internal CRDs are missing, and your validation pipeline depends on third-party infrastructure.\n\n- **Always accurate** — CRD schemas reflect what's installed in your cluster, including custom and internal CRDs, and built-in schemas can be pulled from the same cluster's OpenAPI document\n- **Self-hosted** — run in extract-only mode and serve schemas however you like, or publish directly to Cloudflare Pages\n- **Single static binary** — no runtime dependencies, no interpreters, no package managers. One binary in a distroless nonroot container with no shell\n- **Controller-grade runtime** — watch mode uses informers, leader election, debounced refresh cycles, and health probes. It's a proper workload, not a script on a timer\n- **No glue pipelines** — replaces multi-tool chains (CI runners, shell scripts, kubectl, CLI wrappers) with a single in-cluster binary. No external CI dependency, no cluster-admin runner pods, no scheduled workflow orchestration\n\nThe JSON Schema conversion is built for kubeconform and yaml-language-server compatibility — see [How It Works](#%EF%B8%8F-how-it-works) for details.\n\n## ⚡ Quickstart\n\n### Deploy to a Cluster\n\nInstall the Helm chart in controller mode for real-time CRD watching. Provide Cloudflare credentials to publish directly to Cloudflare Pages, or omit credentials to run extract-only and serve `OUTPUT_DIR/current` yourself.\n\n```bash\nhelm install crd-schema-publisher oci://ghcr.io/sholdee/charts/crd-schema-publisher \\\n  --namespace crd-schema-publisher \\\n  --create-namespace \\\n  --set existingSecret.name=crd-schema-publisher-cloudflare\n```\n\nSee [Deploying](#-deploying) for credentials, raw manifests, CronJob mode, alternative backends, and chart verification.\n\n### Install and Run the CLI\n\nInstall the standalone CLI:\n\n```bash\ncurl -fsSL https://crdsp.shold.io | bash\n```\n\nExtract schemas from a kubeconfig context, convert CRD YAML, or render Kubernetes built-ins from the cluster OpenAPI document:\n\n```bash\n# Extract from the current kubeconfig context\ncrd-schema-publisher extract -o ./schemas\n\n# Extract from a specific context\ncrd-schema-publisher extract --context my-cluster -o ./schemas\n\n# Convert CRD YAML without a cluster\ncrd-schema-publisher convert -f crd.yaml -o ./schemas\n\n# Convert Kubernetes built-in resources from the current cluster\nkubectl get --raw /openapi/v2 \u003e swagger.json\ncrd-schema-publisher convert --openapi swagger.json -o ./schemas --render\n```\n\nFor source builds, use `go run ./cmd/` in place of `crd-schema-publisher`. See [Standalone Binary](#standalone-binary) for installer options and manual downloads, and [Configuration and CLI Reference](#configuration-and-cli-reference) for flags and command behavior.\n\n### Use Published Schemas\n\nOnce published, schemas are available at `https://\u003cyour-pages-domain\u003e/\u003capigroup\u003e/\u003ckind\u003e_\u003cversion\u003e.json`, for example `cert-manager.io/certificate_v1.json` or `core/pod_v1.json`.\n\n```yaml\n# yaml-language-server: $schema=https://kube-schemas.example.com/cert-manager.io/certificate_v1.json\napiVersion: cert-manager.io/v1\nkind: Certificate\nmetadata:\n  name: example\n```\n\nSee [Using Your Schemas](#-using-your-schemas) for IDE and kubeconform examples.\n\n## 📦 Installation\n\n### Standalone Binary\n\nThe quick installer is the recommended path for local CLI use. Static binaries for Linux and macOS (amd64 + arm64) are also attached to each [GitHub Release](https://github.com/sholdee/crd-schema-publisher/releases).\n\nQuick install/update:\n\n```bash\ncurl -fsSL https://crdsp.shold.io | bash\n```\n\nNon-interactive install:\n\n```bash\ncurl -fsSL https://crdsp.shold.io | bash -s -- --yes\n```\n\nInstall a specific release:\n\n```bash\ncurl -fsSL https://crdsp.shold.io | bash -s -- --version vYYYY.MDD.HMMSS\n```\n\nThe installer detects Linux/macOS and amd64/arm64, verifies the selected binary against the release checksum manifest, optionally verifies the checksum Sigstore bundle when `cosign` is available, and installs to an existing `crd-schema-publisher` path or `/usr/local/bin/crd-schema-publisher`.\n\n```bash\n# Download the latest release (example: Linux amd64)\ncurl -LO https://github.com/sholdee/crd-schema-publisher/releases/latest/download/crd-schema-publisher-linux-amd64\nchmod +x crd-schema-publisher-linux-amd64\n```\n\nIf you manage project CLIs with [mise](https://mise.jdx.dev/), install through the aqua backend:\n\n```bash\nmise use aqua:sholdee/crd-schema-publisher@latest\n```\n\nOr pin a release in `mise.toml`:\n\n```toml\n[tools]\n\"aqua:sholdee/crd-schema-publisher\" = \"2026.519.317\"\n```\n\n### Verify Release Artifacts\n\n```bash\n# Verify the signed checksum manifest\ncurl -LO https://github.com/sholdee/crd-schema-publisher/releases/latest/download/checksums-sha256.txt\ncurl -LO https://github.com/sholdee/crd-schema-publisher/releases/latest/download/checksums-sha256.txt.sigstore.json\ncosign verify-blob checksums-sha256.txt \\\n  --bundle checksums-sha256.txt.sigstore.json \\\n  --certificate-identity 'https://github.com/sholdee/crd-schema-publisher/.github/workflows/release.yaml@refs/heads/main' \\\n  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com'\n\n# Verify the binary against the trusted checksum manifest\nsha256sum -c --ignore-missing checksums-sha256.txt\n\n# Optional: verify build provenance for the binary\ngh attestation verify ./crd-schema-publisher-linux-amd64 \\\n  --repo sholdee/crd-schema-publisher \\\n  --signer-workflow sholdee/crd-schema-publisher/.github/workflows/release.yaml \\\n  --source-ref refs/heads/main\n```\n\n## 🚀 Deploying\n\n### Helm Chart (recommended)\n\nThe chart is distributed as an OCI artifact and signed with cosign:\n\n```bash\nhelm install crd-schema-publisher oci://ghcr.io/sholdee/charts/crd-schema-publisher \\\n  --namespace crd-schema-publisher \\\n  --create-namespace \\\n  --set existingSecret.name=crd-schema-publisher-cloudflare\n```\n\nThis installs in **controller mode** by default (real-time watch with leader election). For scheduled runs, set `--set mode=cronjob`.\n\n#### Credentials\n\nCloudflare credentials are **optional in both controller and CronJob modes**. Without them, the workload runs in extract-only mode — site generations are written under the output directory and the active snapshot is exposed at `OUTPUT_DIR/current`, but nothing is uploaded. This is useful when serving schemas locally (e.g., via a sidecar web server) instead of Cloudflare Pages.\n\nTo publish to Cloudflare Pages, provide an API token with **Cloudflare Pages: Edit** permission and your account ID. Two secret management options are supported:\n\n- **`existingSecret`** — reference a pre-existing Secret containing `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`\n- **`externalSecret`** — create an [ExternalSecret](https://external-secrets.io) CR that syncs credentials from an external provider (Vault, AWS Secrets Manager, 1Password, etc.)\n\n```bash\n# Using External Secrets Operator\nhelm install crd-schema-publisher oci://ghcr.io/sholdee/charts/crd-schema-publisher \\\n  --namespace crd-schema-publisher \\\n  --create-namespace \\\n  --set externalSecret.enabled=true \\\n  --set externalSecret.secretStoreRef.name=my-store \\\n  --set externalSecret.secretStoreRef.kind=ClusterSecretStore\n```\n\nThe default remote ref points to a `crd-schema-publisher-cloudflare` key with `api-token` and `account-id` properties — override via `externalSecret.data` if your provider uses different paths.\n\n#### Schema filtering\n\nTo publish only part of the cluster CRD catalog, set `config.filter.group`, `config.filter.kind`, and/or `config.filter.version`. Values are comma-separated and case-insensitive.\n\n```bash\nhelm install crd-schema-publisher oci://ghcr.io/sholdee/charts/crd-schema-publisher \\\n  --namespace crd-schema-publisher \\\n  --create-namespace \\\n  --set config.filter.group=cert-manager.io \\\n  --set-string 'config.filter.kind=Certificate\\,Issuer'\n```\n\nController mode still watches all CRDs, then applies the filter to each generated output snapshot. If active filters match no CRDs or built-ins and Kustomize is not enabled, the next runtime build publishes an empty catalog instead of preserving a previous broader snapshot.\n\n#### Runtime built-ins and Kustomize\n\nRuntime modes publish CRDs only by default. Enable built-ins and Kustomize explicitly when you want one site for CRDs, Kubernetes built-in types, and kustomize's client-side `Kustomization` schema.\n\n```bash\nhelm upgrade --install crd-schema-publisher oci://ghcr.io/sholdee/charts/crd-schema-publisher \\\n  --namespace crd-schema-publisher \\\n  --set config.includeBuiltins=true \\\n  --set config.includeKustomize=true\n```\n\n`config.includeBuiltins=true` reads `/openapi/v2` from the API server. With chart RBAC enabled, it also adds the required ClusterRole permission; with `rbac.create=false`, provide that permission yourself. `config.includeKustomize=true` does not require extra Kubernetes permissions. Filters apply to CRDs and built-ins; Kustomize is an explicit unfiltered opt-in.\n\n#### Optional features\n\nPersistent output volume (`persistence`), built-in static serving (`serve`), Gateway API HTTPRoute (`serve.httpRoute`), extra volumes/volume mounts/containers (`extraVolumes`, `extraVolumeMounts`, `extraContainers`), PodMonitor, PrometheusRule, Grafana dashboard (sidecar ConfigMap or Grafana Operator `GrafanaDashboard`), NetworkPolicy, CiliumNetworkPolicy, PodDisruptionBudget, pod anti-affinity presets, topology spread constraints, and templated extra objects. See [`values.yaml`](charts/crd-schema-publisher/values.yaml) for all options.\n\n#### Built-in static serving\n\nFor simple in-cluster deployments, the controller can serve the active generated site directly from `OUTPUT_DIR/current`:\n\n```bash\nhelm upgrade --install crd-schema-publisher oci://ghcr.io/sholdee/charts/crd-schema-publisher \\\n  --namespace crd-schema-publisher \\\n  --set serve.enabled=true\n```\n\nThe site is exposed on the chart Service port named `site` and defaults to non-privileged port `8081`; health and metrics stay on the `health` port. Built-in serving is controller-only, requires `replicaCount=1`, and switches the Deployment strategy to `Recreate` so traffic is not routed to a new pod before it has published its first site.\n\nUse [`examples/built-in-server/values.yaml`](examples/built-in-server/values.yaml) for a complete values file with persistence and Gateway API `HTTPRoute` setup.\n\n#### Examples: Alternative backends via sidecar pattern\n\nThe chart's `extraContainers` and `extraObjects` values let you wire up any backend without changes to the tool. Each example runs in extract-only mode (no Cloudflare credentials) — schemas are written to generation snapshots under the output directory and the active site is exposed at `OUTPUT_DIR/current` for the sidecar to serve or sync. Examples that push to external storage run stateless with an emptyDir; the caddy example uses a persistent volume since it serves directly from the cluster.\n\n```bash\nhelm install crd-schema-publisher oci://ghcr.io/sholdee/charts/crd-schema-publisher \\\n  --namespace crd-schema-publisher --create-namespace \\\n  -f examples/\u003cexample\u003e/values.yaml\n```\n\n| Example                                               | Backend               | Description                                                                                                                                                                                  |\n| ----------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [`caddy-sidecar`](examples/caddy-sidecar/values.yaml) | Local HTTP            | Caddy serves schemas directly from the cluster with directory browsing and a Gateway API HTTPRoute. Adaptable to nginx or any web server.                                                    |\n| [`rclone-s3`](examples/rclone-s3/values.yaml)         | S3-compatible storage | rclone syncs schemas to any S3-compatible provider (AWS S3, Backblaze B2, MinIO, Cloudflare R2, GCS) on a 60-second interval. Provider-specific configuration documented in the file header. |\n| [`git-push`](examples/git-push/values.yaml)           | Git repository        | Commits and pushes schema changes to a GitHub repository for GitHub Pages hosting. Works with any git host (GitLab, Gitea, Bitbucket) by adjusting the remote URL.                           |\n\nEach example is a self-contained values file — copy it, fill in your credentials, and install. See the comments in each file for what to customize.\n\n#### GitHub Pages subpath deployments\n\nWhen serving schemas from a GitHub Pages project path such as `https://user.github.io/iac/`, set the base path so generated HTML links include the subpath:\n\n```bash\nBASE_PATH=/iac\n```\n\nOr in the Helm chart:\n\n```yaml\nconfig:\n  basePath: \"/iac\"\n```\n\nIf you already use the first-party `git-push` or `rclone-s3` examples, update them to read `/data/current`. Older example configs fail closed after upgrading to a new image: syncing stops, but existing remote content is not deleted or overwritten. Cloudflare Pages users do not need to change anything.\n\n#### Verification\n\nVerify the chart signature (substitute the version you installed — find it with `helm list`):\n\n```bash\ncosign verify ghcr.io/sholdee/charts/crd-schema-publisher:\u003cVERSION\u003e \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp github.com/sholdee/crd-schema-publisher\n```\n\n### Raw Manifests\n\nFor users who prefer raw YAML without Helm, deploy manifests are available in [`deploy/`](deploy/).\n\n### Watch Mode (recommended)\n\nReacts to CRD changes in real-time with debounced schema refreshes, uploading only when Cloudflare credentials are configured. Supports leader election for safe rolling updates. The container runs with `args: [\"watch\"]` — see [`deploy/deployment.yaml`](deploy/deployment.yaml).\n\n```bash\nkubectl apply -f deploy/common.yaml -f deploy/deployment.yaml\n```\n\n### CronJob Mode\n\nRuns scheduled schema extraction, uploading to Cloudflare Pages only when credentials are configured. Simpler, but schemas are only updated when the job runs. The example uses a daily schedule — adjust the `schedule` field as needed. Uses the default `run` command — see [`deploy/cronjob.yaml`](deploy/cronjob.yaml).\n\nWithout Cloudflare credentials, CronJob mode is extract-only. With the default `emptyDir` output volume, extracted schemas are discarded when the Job pod exits. Configure Cloudflare credentials, `persistence.enabled`/`persistence.existingClaim`, or an extra container backend if you want scheduled output to be retained.\n\n```bash\nkubectl apply -f deploy/common.yaml -f deploy/cronjob.yaml\n```\n\nBoth modes share [`deploy/common.yaml`](deploy/common.yaml) which provides namespace, ServiceAccount, RBAC (ClusterRole for CRD read access), and a hardened security context (nonroot, read-only rootfs, dropped capabilities).\n\nThe deploy manifests include an empty placeholder Secret named `crd-schema-publisher-cloudflare`. Fill in the values in `common.yaml` directly, or replace the Secret with your own secrets management (e.g., ExternalSecret, Sealed Secret). If Cloudflare credentials are empty or omitted, workloads run in extract-only mode (site generations written under `OUTPUT_DIR/.generations` with the active snapshot exposed at `OUTPUT_DIR/current`, but not uploaded). In raw CronJob mode, the default `emptyDir` output is discarded when the Job pod exits unless you replace it with retained storage or a backend sync.\n\n### Container Image\n\nPre-built multi-arch images (amd64 + arm64) are published to GHCR:\n\n```text\nghcr.io/sholdee/crd-schema-publisher:latest\n```\n\nReleases are triggered manually via the release workflow, producing a date-based tag (`vYYYY.MDD.HMMSS` — e.g. `v2026.413.65435`) and `latest`. Release notes include the image digest, OCI Helm chart reference, signed checksum manifest, binary provenance link, and standalone binary attachments. The workflow runs an internal smoke gate before release artifacts are promoted.\n\nImages use `gcr.io/distroless/static:nonroot` as the runtime base — no shell, no package manager, runs as UID 65534. Production images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/) keyless signing via GitHub Actions OIDC:\n\n```bash\ncosign verify ghcr.io/sholdee/crd-schema-publisher:latest \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp github.com/sholdee/crd-schema-publisher\n```\n\n## Configuration and CLI Reference\n\n### Environment Variables\n\nDeployment/runtime configuration is primarily via environment variables. For local CLI use, `extract`, `convert`, `run`, `watch`, `upload`, and `preview` also expose command-specific flags such as `--output-dir`/`-o`. Variables marked _(watch)_ apply only to watch mode deployment.\n\n| Variable                   | Required    | Default                | Description                                                                                                           |\n| -------------------------- | ----------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| `CLOUDFLARE_API_TOKEN`     | Upload only | —                      | Cloudflare API token with Pages permissions                                                                           |\n| `CLOUDFLARE_ACCOUNT_ID`    | Upload only | —                      | Cloudflare account ID                                                                                                 |\n| `CF_PAGES_PROJECT`         | No          | `kubernetes-schemas`   | Cloudflare Pages project name                                                                                         |\n| `OUTPUT_DIR`               | No          | `/output`              | Site output root. The active snapshot is exposed at `OUTPUT_DIR/current`                                              |\n| `KUBECTL_CONTEXT`          | No          | —                      | Kubernetes context name (local development only)                                                                      |\n| `DEBOUNCE_SECONDS`         | No          | `15`                   | Seconds to wait after last CRD event before publishing (watch mode)                                                   |\n| `POD_NAME`                 | Yes (watch) | —                      | Pod identity for leader election (set via downward API)                                                               |\n| `POD_NAMESPACE`            | Yes (watch) | —                      | Namespace for leader lease (set via downward API)                                                                     |\n| `LEASE_NAME`               | No          | `crd-schema-publisher` | Name of the Lease resource (watch mode)                                                                               |\n| `HEALTH_PORT`              | No          | `8080`                 | Port for liveness/readiness probes (watch mode)                                                                       |\n| `SERVE_SITE`               | No          | —                      | Set to `true` to serve `OUTPUT_DIR/current` from watch mode                                                           |\n| `SITE_PORT`                | No          | `8081`                 | Non-privileged static site server port when `SERVE_SITE=true`                                                         |\n| `SERVE_ACCESS_LOG`         | No          | —                      | Set to `true` to log each request served by the built-in static site server                                           |\n| `PREVIEW_ADDR`             | No          | `127.0.0.1:8989`       | Listen address for preview server (preview mode)                                                                      |\n| `SKIP_RENDER`              | No          | —                      | Set to `true` to skip HTML schema page rendering                                                                      |\n| `UPLOAD_BUCKET_SIZE_BYTES` | No          | `41943040`             | Cloudflare upload bucket size in bytes. Lower values reduce peak upload memory at the cost of more requests           |\n| `UPLOAD_CONCURRENCY`       | No          | `3`                    | Concurrent Cloudflare upload buckets. Lower values reduce peak upload memory at the cost of slower cache-miss uploads |\n| `BASE_PATH`                | No          | —                      | URL path prefix for subpath deployments (e.g., `/iac` for GitHub Pages at `user.github.io/iac/`)                      |\n| `SCHEMA_INCLUDE_BUILTINS`  | No          | —                      | Set to `true` to include Kubernetes built-ins from API server OpenAPI v2 (`run`, `extract`, `watch`)                  |\n| `SCHEMA_INCLUDE_KUSTOMIZE` | No          | —                      | Set to `true` to include kustomize's client-side `Kustomization` schema (`run`, `extract`, `watch`)                   |\n| `SCHEMA_FILTER_KIND`       | No          | —                      | Restrict generated schemas to matching CRD kinds, comma-separated and case-insensitive (`run`, `extract`, `watch`)    |\n| `SCHEMA_FILTER_GROUP`      | No          | —                      | Restrict generated schemas to matching API groups, comma-separated and case-insensitive (`run`, `extract`, `watch`)   |\n| `SCHEMA_FILTER_VERSION`    | No          | —                      | Restrict generated schemas to matching API versions, comma-separated and case-insensitive (`run`, `extract`, `watch`) |\n\nSchema filters limit generated output only. In watch mode, the controller still watches all cluster CRDs and applies the filters during each publish cycle. If active filters match no CRDs or built-ins and Kustomize is not enabled, runtime builds publish an empty catalog instead of leaving stale schemas in place.\n\nRuntime modes stay CRD-only unless opt-ins are enabled. `--include-builtins` reads `/openapi/v2` from the API server and publishes built-ins into the same generation. `--include-kustomize` adds kustomize's client-side `Kustomization` schema. Filters apply to CRDs and built-ins; Kustomize is explicit and unfiltered.\n\n### Command Behavior\n\n```text\ncrd-schema-publisher [command]\n\nCommands:\n  run       Extract schemas and upload to Cloudflare Pages when credentials are configured (default)\n  extract   Extract schemas from a Kubernetes cluster\n  convert   Convert CRD YAML files and Kubernetes OpenAPI built-ins to JSON Schema\n  upload    Upload the active site from OUTPUT_DIR/current to Cloudflare Pages\n  watch     Watch for CRD changes and upload when credentials are configured\n  preview   Serve a local preview of the documentation site\n```\n\n| Command(s)               | Output directory behavior                                                                                    |\n| ------------------------ | ------------------------------------------------------------------------------------------------------------ |\n| `extract`                | Requires explicit `--output-dir`/`-o` or `OUTPUT_DIR`; does not fall back to `/output`.                      |\n| `convert`                | Requires `--output-dir`/`-o`; does not read `OUTPUT_DIR`.                                                    |\n| `run`, `watch`, `upload` | Accept `--output-dir`/`-o`; output root must already exist.                                                  |\n| `preview`                | Uses sample data by default; reads real extracted output only when `--output-dir`/`-o` is passed explicitly. |\n\n| Command(s)                | Filters and command-specific flags                                                                                                                                                                   |\n| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `run`, `extract`, `watch` | Support comma-separated, case-insensitive `--kind`, `--group`, and `--version` filters. Defaults can also come from `SCHEMA_FILTER_KIND`, `SCHEMA_FILTER_GROUP`, and `SCHEMA_FILTER_VERSION`.        |\n| `run`, `extract`, `watch` | Support `--include-builtins`/`SCHEMA_INCLUDE_BUILTINS` to include built-ins from the API server OpenAPI v2 document.                                                                                 |\n| `run`, `extract`, `watch` | Support `--include-kustomize`/`SCHEMA_INCLUDE_KUSTOMIZE` to include kustomize's client-side `Kustomization` schema.                                                                                  |\n| `extract`                 | Supports `--context`, `--base-path`, and `--skip-render`.                                                                                                                                            |\n| `convert`                 | Supports comma-separated, case-insensitive `--kind`, `--group`, and `--version` filters for CRD YAML and OpenAPI inputs.                                                                             |\n| `convert`                 | Supports `--file`/`-f`, non-recursive `--dir`/`-d` YAML loading, optional `--render`, and `--base-path` for rendered links.                                                                          |\n| `convert`                 | `--openapi` converts a Kubernetes OpenAPI v2 (swagger) document of built-in types into self-contained per-kind schemas, combinable with `--file`/`--dir` to render CRDs and built-ins into one site. |\n| `convert`                 | `--kustomize` explicitly publishes kustomize's `Kustomization` schema, reflected from the pinned `sigs.k8s.io/kustomize/api` types. It is not filtered.                                              |\n\n## 📋 Using Your Schemas\n\nOnce published, your schemas are available at `https://\u003cyour-pages-domain\u003e/\u003capigroup\u003e/\u003ckind\u003e_\u003cversion\u003e.json`. Core built-ins use the `core` group path, such as `core/pod_v1.json`. The published site also includes a browsable index with search and interactive HTML documentation for each schema.\n\n### IDE Validation (yaml-language-server)\n\nAdd a modeline to any YAML file. Works in VS Code, Neovim, Helix, and any editor with yaml-language-server:\n\n```yaml\n# yaml-language-server: $schema=https://kube-schemas.example.com/cert-manager.io/certificate_v1.json\napiVersion: cert-manager.io/v1\nkind: Certificate\nmetadata:\n  name: example\n```\n\nOr configure schemas globally in VS Code:\n\n```jsonc\n// .vscode/settings.json\n{\n  \"yaml.schemas\": {\n    \"https://kube-schemas.example.com/cert-manager.io/certificate_v1.json\": [\"**/certificates/*.yaml\"],\n  },\n}\n```\n\n### CI Validation (kubeconform)\n\nIf your registry includes built-ins from runtime `--include-builtins` or offline `convert --openapi`, point kubeconform at both the `core` path and the grouped path:\n\n```bash\nkubeconform \\\n  -strict \\\n  -ignore-missing-schemas \\\n  -schema-location 'https://kube-schemas.example.com/core/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \\\n  -schema-location 'https://kube-schemas.example.com/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \\\n  manifests/*.yaml\n```\n\nThis project validates its own Helm chart manifests against its published schema registry in CI — see the `helm-lint` job in [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml) for a working example.\n\nThis validates Kubernetes built-ins and CRDs against the same published schema snapshot. If you publish only CRDs, keep `-schema-location default` before your custom registry path so kubeconform still validates built-ins.\n\n\u003e **Note:** Schema files are written as lowercase (e.g., `certificate_v1.json`) while `{{.ResourceKind}}` expands to the original case (e.g., `Certificate`). This works on Cloudflare Pages because it serves paths case-insensitively — the same convention used by [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog). If serving schemas from a case-sensitive host, use lowercase kind names in your template paths.\n\n## Operations\n\n### Monitoring\n\nIn watch mode, the health server exposes a `/metrics` endpoint on `HEALTH_PORT` (default 8080) in Prometheus text format.\n\n| Metric                                           | Type    | Description                                   |\n| ------------------------------------------------ | ------- | --------------------------------------------- |\n| `crdpublisher_publish_cycle_duration_seconds`    | gauge   | Duration of the most recent publish cycle     |\n| `crdpublisher_publish_cycle_total`               | counter | Publish cycles by result (`success`, `error`) |\n| `crdpublisher_crds_discovered`                   | gauge   | CRDs found in the most recent cycle           |\n| `crdpublisher_schemas_written`                   | gauge   | Schemas written in the most recent cycle      |\n| `crdpublisher_last_successful_publish_timestamp` | gauge   | Unix epoch of the last successful publish     |\n| `crdpublisher_watchdog_timestamp`                | gauge   | Unix epoch of the last debounce loop tick     |\n| `crdpublisher_publish_skipped_total`             | counter | Debounce skips (publish already in progress)  |\n| `crdpublisher_leader`                            | gauge   | Whether this pod is the current leader        |\n\nThe watchdog timestamp enables dead man's switch alerting — it updates on every debounce loop tick (regardless of whether a publish occurs), so `time() - crdpublisher_watchdog_timestamp` staying fresh proves the watcher is alive. The publish timestamp separately tracks when content was last pushed.\n\nThe Helm chart includes a PodMonitor — enable it with `--set metrics.podMonitor.enabled=true`. For raw manifests or vanilla Prometheus Operator, create a PodMonitor:\n\n```yaml\napiVersion: monitoring.coreos.com/v1\nkind: PodMonitor\nmetadata:\n  name: crd-schema-publisher\nspec:\n  selector:\n    matchLabels:\n      app: crd-schema-publisher\n  podMetricsEndpoints:\n    - port: health\n      path: /metrics\n```\n\nFor vanilla Prometheus, add `prometheus.io/*` annotations to the pod template or configure a static scrape target.\n\n#### Grafana dashboard\n\nThe Helm chart can install the included Grafana dashboard in either sidecar-discovery mode or Grafana Operator mode.\n\nFor Grafana sidecars that discover dashboard `ConfigMap` objects:\n\n```bash\nhelm upgrade --install crd-schema-publisher oci://ghcr.io/sholdee/charts/crd-schema-publisher \\\n  --namespace crd-schema-publisher \\\n  --set grafana.dashboard.enabled=true\n```\n\nFor Grafana Operator:\n\n```bash\nhelm upgrade --install crd-schema-publisher oci://ghcr.io/sholdee/charts/crd-schema-publisher \\\n  --namespace crd-schema-publisher \\\n  --set grafana.dashboard.operator.enabled=true\n```\n\nOperator mode renders a `GrafanaDashboard` that references the embedded dashboard `ConfigMap`. It selects Grafana instances with `dashboards: grafana` by default. Custom `grafana.dashboard.operator.instanceSelector` values replace that fallback instead of being merged with it:\n\n```yaml\ngrafana:\n  dashboard:\n    operator:\n      enabled: true\n      instanceSelector:\n        matchLabels:\n          grafana.internal/instance: home\n```\n\nTo intentionally match all Grafana instances visible to the operator, set `grafana.dashboard.operator.defaultInstanceSelector.enabled=false`.\n\nUse `grafana.dashboard.operator.folder` for a Grafana folder title, `grafana.dashboard.operator.folderRef` for a `GrafanaFolder` resource reference, or `grafana.dashboard.operator.folderUID` for an existing Grafana folder UID. Set only one folder option.\n\nIf your Grafana datasource name is not resolved automatically, map the bundled dashboard input with:\n\n```yaml\ngrafana:\n  dashboard:\n    operator:\n      datasources:\n        - inputName: DS_PROMETHEUS\n          datasourceName: Prometheus\n```\n\nGrafana Operator and its CRDs must already be installed in the cluster.\n\n### Output Structure\n\nCluster-backed site generation (`run`, `extract`, `watch`) and preview temp generations use this layout:\n\n```text\n\u003coutput-dir\u003e/\n  .generations/\n    \u003cgeneration\u003e/\n      \u003capigroup\u003e/\n        \u003ckind\u003e_\u003cversion\u003e.json          # JSON schema\n        \u003ckind\u003e_\u003cversion\u003e.html          # Interactive documentation page\n      _meta/\n        kinds.json                     # Internal Kind casing manifest\n        schema-metadata.json           # Internal index source manifest\n      master-standalone/\n        \u003capigroup\u003e-\u003ckind\u003e-stable-\u003cversion\u003e.json  # kubeval-compatible format\n      index.html                       # Browsable schema index\n      schema-search.js                 # Shared schema-page search/autocomplete module\n      favicon.svg                      # Constellation icon\n  current -\u003e .generations/\u003cgeneration\u003e # Stable read path for sidecars and local servers\n```\n\nDirect-volume consumers should read or serve `OUTPUT_DIR/current`, not the flat root of `OUTPUT_DIR`. `_meta/` is internal tool state; first-party Cloudflare, git, S3, and Caddy examples exclude it from published or served output.\n\n`convert` writes schema files directly into `--output-dir` instead of creating `.generations/current`. It records generated files in `_meta/convert-manifest.json` so reruns can remove stale generated artifacts while preserving files that existed before `convert` ran.\n\n## ⚙️ How It Works\n\nFor cluster-backed commands (`run`, `extract`, and `watch`), the pipeline is:\n\n1. Connects to the Kubernetes API (in-cluster or via kubeconfig)\n2. Lists all CRDs and extracts `.spec.versions[].schema.openAPIV3Schema`\n3. Applies three JSON Schema transforms:\n   - Adds `additionalProperties: false` to structural child objects with `properties` — recurses into schema-valued locations only, preserving validation overlays and literal `default`/`enum` data while fixing a bug in the original where CRD fields named `properties` or other JSON Schema keywords corrupt the output\n   - Replaces Kubernetes int-or-string markers with a non-conflicting `oneOf` union, preserving safe metadata and moving type-specific assertions into the matching string or integer branch\n   - Allows null for optional fields (per-field precision, including optional `$ref` fields as ref-or-null `anyOf` wrappers)\n\n   These transforms handle nullable fields, int-or-string types, root objects, and keyword-colliding property names. A frozen golden test locks converter output to prevent regressions.\n\n4. Writes schemas to both primary and kubeval-compatible directory formats inside a new generation snapshot\n5. Renders an interactive HTML documentation page for each schema with collapsible property trees, local `$ref` expansion, path-aware search, and autocomplete powered by a shared emitted `schema-search.js` asset\n6. Generates an HTML index grouped by schema source and API group with client-side search, schema statistics, and yaml-language-server usage examples\n7. Atomically switches `OUTPUT_DIR/current` to the completed generation so sidecars read a stable snapshot\n8. Uploads the active generation to Cloudflare Pages via the direct upload API (BLAKE3 content hashing, batched uploads with retry)\n\nThe `convert` command skips Kubernetes access and reads CRD YAML from `--file`/`-f`, stdin (`-f -`), and/or a non-recursive `--dir`/`-d`. It applies the same schema transforms and writes flat output directly to `--output-dir`/`-o`; with `--render`, it also renders HTML pages and an index.\n\nRuntime modes can include optional schemas in generated snapshots. `--include-builtins` fetches `/openapi/v2` from the API server and writes authorable built-in types into the same generation as CRDs. When OpenAPI also contains CRD-backed definitions, CRD schemas take precedence and those OpenAPI duplicates are skipped. `--include-kustomize` writes kustomize's client-side `Kustomization` schema. When more than one schema source is present, the index separates CRDs, built-ins, and Kustomize schemas; CRD-only output keeps the original API-group-only index. In the Helm chart, `config.includeBuiltins=true` adds `/openapi/v2` RBAC when `rbac.create=true`; `config.includeKustomize=true` does not require additional Kubernetes permissions.\n\n`--openapi \u003cswagger.json\u003e` converts Kubernetes' built-in (non-CRD) types from an OpenAPI v2 document (for example `kubectl get --raw /openapi/v2`). Each authorable type that declares a group/version/kind becomes a self-contained `\u003cgroup\u003e/\u003ckind\u003e_\u003cversion\u003e.json`; the empty API group is written under `core/`. Referenced definitions are bundled into each schema so validation and rendered child fields work without external references. When combined with CRD inputs, matching OpenAPI CRD definitions and their List types are skipped.\n\n```sh\nkubectl get --raw /openapi/v2 \u003e swagger.json\ncrd-schema-publisher convert --openapi swagger.json -o ./schemas --render\n```\n\nCombine `--openapi` with `--file` or `--dir` when you want one local site containing both built-ins and CRDs.\n\n`--kustomize` publishes a schema for kustomize's `Kustomization` at `kustomize.config.k8s.io/kustomization_v1beta1.json`. It's a client-side type with no usable upstream schema, so it's reflected from the `sigs.k8s.io/kustomize/api` Go types pinned in this module — bumping that dependency updates the schema. Combine it with the other inputs in a single run:\n\n`--kind`, `--group`, and `--version` filters limit CRD and OpenAPI inputs; `--kustomize` is a single explicit opt-in and always emits Kustomization when set.\n\n```sh\ncrd-schema-publisher convert -d ./crds --openapi swagger.json --kustomize -o ./schemas --render\n```\n\n## 🔧 Development\n\n### Run Locally\n\nUse the package path (`go run ./cmd/ \u003ccommand\u003e`) for local subcommands so Go compiles every file in `cmd/`. Single-file invocation (`go run ./cmd/main.go --help` or `--version`) is only kept for top-level smoke checks.\n\n```bash\n# Extract schemas from a local cluster (no upload)\nKUBECTL_CONTEXT=my-cluster OUTPUT_DIR=./output go run ./cmd/ extract\n# Writes the active snapshot under ./output/current\n\n# Full run with upload\nmkdir -p ./output\nKUBECTL_CONTEXT=my-cluster \\\n  CLOUDFLARE_API_TOKEN=xxx \\\n  CLOUDFLARE_ACCOUNT_ID=xxx \\\n  go run ./cmd/ --output-dir ./output\n\n# Convert CRD YAML files to JSON Schema (no cluster needed)\ngo run ./cmd/ convert -f crd.yaml -o ./schemas\n\n# Convert Kubernetes built-ins from a cluster OpenAPI document\nkubectl get --raw /openapi/v2 \u003e swagger.json\ngo run ./cmd/ convert --openapi swagger.json -o ./schemas --render\n\n# Convert all CRDs in a directory\ngo run ./cmd/ convert -d ./crds/ -o ./schemas\n\n# Pipe from kubectl\nkubectl get crds -o yaml | go run ./cmd/ convert -f - -o ./schemas\n\n# Filter by kind and group\ngo run ./cmd/ extract --output-dir ./schemas --kind certificate,issuer --group cert-manager.io\n\n# Filter a runtime extraction through env vars\nSCHEMA_FILTER_GROUP=cert-manager.io go run ./cmd/ --output-dir ./output\n```\n\n### Preview the Site Locally\n\nPreview is useful for UI development and local inspection. It needs no cluster or credentials when using sample data.\n\n```bash\n# Preview the index UI (no cluster or credentials needed)\ngo run ./cmd/ preview\n# open http://127.0.0.1:8989\n\n# Preview with real extracted schemas\ngo run ./cmd/ preview --output-dir ./output\n# Serves the active snapshot from ./output/current\n\n# Preview a subpath deployment locally\nBASE_PATH=/iac go run ./cmd/ preview\n# open http://127.0.0.1:8989/iac/\n```\n\n### Build\n\n```bash\n# Native build\ngo build -o crd-schema-publisher ./cmd/\n\n# Example static cross-compile\nCGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags=\"-s -w\" -o crd-schema-publisher ./cmd/\n\n# Build all release binaries locally\nfor pair in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do\n  GOOS=\"${pair%/*}\" GOARCH=\"${pair#*/}\"\n  CGO_ENABLED=0 GOOS=\"${GOOS}\" GOARCH=\"${GOARCH}\" \\\n    go build -ldflags=\"-s -w\" -o \"crd-schema-publisher-${GOOS}-${GOARCH}\" ./cmd/\ndone\n\n# Docker (multi-arch)\ndocker buildx build --platform linux/amd64,linux/arm64 -t crd-schema-publisher .\n```\n\n### Linting\n\nThis project uses [golangci-lint](https://golangci-lint.run/) with strict linters enabled:\n\n```bash\ngo install github.com/golangci/golangci-lint/cmd/golangci-lint@latest\ngolangci-lint run\n```\n\nEnable the pre-commit hook to enforce linting before each commit:\n\n```bash\ngit config core.hooksPath .githooks\n```\n\nIf you change the extracted schema search module or its tests, also run:\n\n```bash\nnode --test theme/schema_search.test.js\n```\n\n### Renovate\n\nDependencies are managed by [Renovate](https://docs.renovatebot.com/). Minor and patch updates for Go modules, GitHub Actions, Docker images, and CI tools are automerged after required status checks pass.\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for full contributor setup and guidelines.\n\n## 👥 Community\n\n- [Home Operations Discord](https://discord.gg/home-operations)\n- [Contributing](CONTRIBUTING.md)\n- [Code of Conduct](CODE_OF_CONDUCT.md)\n- [Security Policy](SECURITY.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsholdee%2Fcrd-schema-publisher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsholdee%2Fcrd-schema-publisher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsholdee%2Fcrd-schema-publisher/lists"}