{"id":51558502,"url":"https://github.com/git001/k8s-scale-app-rs","last_synced_at":"2026-07-10T08:01:46.329Z","repository":{"id":369669309,"uuid":"1230960036","full_name":"git001/k8s-scale-app-rs","owner":"git001","description":null,"archived":false,"fork":false,"pushed_at":"2026-07-06T11:29:30.000Z","size":86,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-06T12:05:00.559Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","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/git001.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-06T13:42:10.000Z","updated_at":"2026-07-06T11:28:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/git001/k8s-scale-app-rs","commit_stats":null,"previous_names":["git001/k8s-scale-app-rs"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/git001/k8s-scale-app-rs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/git001%2Fk8s-scale-app-rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/git001%2Fk8s-scale-app-rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/git001%2Fk8s-scale-app-rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/git001%2Fk8s-scale-app-rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/git001","download_url":"https://codeload.github.com/git001/k8s-scale-app-rs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/git001%2Fk8s-scale-app-rs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35325188,"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-07-10T02:00:06.465Z","response_time":60,"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":[],"created_at":"2026-07-10T08:01:44.931Z","updated_at":"2026-07-10T08:01:46.318Z","avatar_url":"https://github.com/git001.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# k8s-scale-app-rs\n\nSmall Rust CLI that either **scales** a Kubernetes Deployment to a fixed replica count **or** triggers a **rolling restart** on it, via the Kubernetes API. Designed to run as a `CronJob`, deployable via Helm **or** Kustomize.\n\n- Scale mode uses the `deployments/scale` subresource → minimal RBAC footprint\n- Restart mode patches `spec.template.metadata.annotations[\"kubectl.kubernetes.io/restartedAt\"]` — same mechanism as `kubectl rollout restart`\n- Configuration via environment variables, overridable by CLI flags\n- Optional server-side dry-run\n- Optional extra CA chain for the cluster API (e.g. corporate CA), provided via ConfigMap\n- Namespace is read at runtime from the pod via the Downward API — the CronJob always acts on a deployment in its own namespace\n\nIntroductory write-up: [k8s-scale-app-rs: Scale or Restart a Kubernetes Deployment from a CronJob](https://blog.none.at/blog/2026/2026-07-06-k8s-scale-app-rs/) on blog.none.at.\n\n## Requirements\n\n| Tool         | Version                                                                   |\n|--------------|---------------------------------------------------------------------------|\n| Rust         | 1.95 (edition 2024)                                                       |\n| Container    | UBI 10 micro runtime (default) or UBI 10 minimal (`-debug`), `rust:1.95` builder |\n| Kubernetes   | API ≥ v1.30 (k8s-openapi `v1_34` feature)                                 |\n| Helm         | v3                                                                        |\n| kustomize    | ≥ v5 (`kubectl kustomize` works)                                          |\n| cosign       | v2 (only for verifying published images / attestations)                   |\n\n## CLI\n\nTwo subcommands, sharing the same common arguments:\n\n```\nk8s-scale-app-rs scale   --deployment \u003cNAME\u003e --replicas \u003cN\u003e [--dry-run] [--extra-ca-bundle \u003cPATH\u003e]\nk8s-scale-app-rs restart --deployment \u003cNAME\u003e                [--dry-run] [--extra-ca-bundle \u003cPATH\u003e]\n```\n\nArguments and environment variables:\n\n| Flag                  | ENV                          | Subcommand(s)   | Required | Description                                                  |\n|-----------------------|------------------------------|-----------------|----------|--------------------------------------------------------------|\n| `--deployment`        | `K8S_SCALE_DEPLOYMENT`       | scale, restart  | yes      | Deployment name                                              |\n| `--namespace`         | `K8S_SCALE_NAMESPACE`        | scale, restart  | yes      | Namespace (set by Downward API in the CronJob)               |\n| `--replicas`          | `K8S_SCALE_REPLICAS`         | scale           | yes      | Target replica count (≥ 0)                                   |\n| `--dry-run`           | `K8S_SCALE_DRY_RUN`          | scale, restart  | no       | Server-side dry-run; no changes persisted                    |\n| `--extra-ca-bundle`   | `K8S_SCALE_EXTRA_CA_BUNDLE`  | scale, restart  | no       | Path to PEM file with additional CAs (chain is merged)       |\n\n`RUST_LOG` controls the log level (`info`, `debug`, …).\n\n## Build locally\n\n```bash\ncargo build --release\n./target/release/k8s-scale-app-rs --help\n./target/release/k8s-scale-app-rs scale --help\n./target/release/k8s-scale-app-rs restart --help\n```\n\nThe release binary uses `mimalloc` as the global allocator (`#[global_allocator]` in [src/main.rs](src/main.rs)).\n\n## Testing\n\nThree test surfaces, all invoked by `cargo test`:\n\n| Where                | Purpose                                                              | Needs cluster? |\n|----------------------|----------------------------------------------------------------------|----------------|\n| [src/main.rs](src/main.rs) `#[cfg(test)]` | Unit tests for `parse_pem_certificates`               | no             |\n| [tests/cli.rs](tests/cli.rs)              | CLI parsing, validation, help output (subprocess)     | no             |\n| [tests/cluster.rs](tests/cluster.rs)      | Auth + client-build + API roundtrip against real API  | auto-skip      |\n\nCluster tests auto-skip (they print a `SKIP:` line and pass) unless one of these is true:\n\n- `KUBECONFIG` env var is set and the file exists\n- `/var/run/secrets/kubernetes.io/serviceaccount/token` exists (in-cluster)\n\n```bash\n# Without cluster (unit + CLI tests only):\ncargo test --release\n\n# With cluster (all tests):\nKUBECONFIG=~/.kube/config cargo test --release\n```\n\n## Build the container image\n\nTwo variants live side by side. Both share the same builder stage but differ in the runtime base:\n\n| File                                  | Base image                            | Tag suffix   | Size    | Contains |\n|---------------------------------------|---------------------------------------|--------------|---------|----------|\n| [Containerfile](Containerfile)        | `registry.access.redhat.com/ubi10/ubi-micro` | (none) | ~35 MB  | Binary + CA trust store + `/licenses/` — no package manager, no `curl`/`dig` |\n| [Containerfile.debug](Containerfile.debug) | `registry.access.redhat.com/ubi10/ubi-minimal` | `-debug` | ~115 MB | Everything above + `bash`, `curl`, `bind-utils` (`dig`) |\n\nLocal build:\n\n```bash\n# Default (distroless-style):\npodman build -t ghcr.io/git001/k8s-scale-app-rs:local .\n\n# Debug variant:\npodman build -f Containerfile.debug -t ghcr.io/git001/k8s-scale-app-rs:local-debug .\n```\n\nBoth images run as UID `1001` and generate a `LICENSES.txt` bundle from `cargo-about` during build. The bundle plus the project `LICENSE` are placed under `/licenses/` inside the image (the path Red Hat expects for UBI compliance).\n\n## Deploy with Helm\n\n```bash\nhelm install scale-dev chart \\\n  --namespace my-app-dev \\\n  -f chart/values-dev.yaml\n```\n\nValues layering: `chart/values.yaml` is always loaded as the default; `-f values-{stage}.yaml` overrides individual keys.\n\nPer-stage example overrides:\n\n- [chart/values-dev.yaml](chart/values-dev.yaml)\n- [chart/values-preprod.yaml](chart/values-preprod.yaml)\n- [chart/values-prod.yaml](chart/values-prod.yaml)\n\nImportant toggles in `values.yaml`:\n\n| Key                         | Description                                                                  |\n|-----------------------------|------------------------------------------------------------------------------|\n| `mode`                      | `scale` (default) or `restart` — selects the subcommand                       |\n| `target.deployment`         | Required — name of the deployment to act on                                  |\n| `target.replicas`           | Target replica count (only used when `mode: scale`)                          |\n| `target.dryRun`             | Dry-run mode                                                                 |\n| `schedule`                  | Cron schedule of the CronJob                                                 |\n| `serviceAccount.create`     | Create the `deploy` ServiceAccount or assume it already exists               |\n| `rbac.create`               | Create the namespace-scoped Role + RoleBinding                               |\n| `extraCA.enabled`           | Mount an additional CA chain into the trust store                            |\n| `extraCA.existingConfigMap` | Reference an existing ConfigMap (alternative: inline `extraCA.bundle`)       |\n| `image.tag`                 | Container tag (empty → `Chart.AppVersion`)                                   |\n\n## Deploy with Kustomize\n\n```bash\nkubectl apply -k kustomize/overlays/dev\n# or render:\nkubectl kustomize kustomize/overlays/dev\n```\n\nLayout:\n\n```\nkustomize/\n├── base/                         # CronJob + volume mount for extra-ca-bundle (default args: [\"scale\"])\n├── components/\n│   ├── serviceaccount/           # opt-in: creates the \"deploy\" SA\n│   └── rbac/                     # opt-in: Role + RoleBinding (covers both scale and restart)\n└── overlays/{dev,preprod,prod}/  # patch schedule, image tag, env, optional CA CM\n```\n\nComponents replace Helm's `if` logic: an overlay only includes what it needs.\n\nTo switch a stage to restart mode, patch the container's `args` in the overlay:\n\n```yaml\n# overlays/\u003cstage\u003e/cronjob-patch.yaml\ncontainers:\n  - name: scaler\n    args: [\"restart\"]\n```\n\n`K8S_SCALE_REPLICAS` in the pod env is silently ignored by the `restart` subcommand.\n\n| Stage   | components            | configMapGenerator (CA) | mode  |\n|---------|-----------------------|-------------------------|-------|\n| dev     | serviceaccount + rbac | included                | scale |\n| preprod | rbac                  | template, commented out | scale |\n| prod    | rbac                  | template, commented out | scale |\n\n## Extra CA chain\n\nThe CLI merges PEM certificates from `K8S_SCALE_EXTRA_CA_BUNDLE` into `kube::Config.root_cert` **in addition to** the cluster CA from the ServiceAccount token. `pem::parse_many` reads any number of `CERTIFICATE` blocks from a single file.\n\n**Helm:**\n\n```yaml\nextraCA:\n  enabled: true\n  existingConfigMap: corporate-ca-bundle   # OR inline via `bundle:`\n  key: ca-bundle.pem\n  mountPath: /etc/ssl/extra-ca\n```\n\n**Kustomize:** the base CronJob always mounts a ConfigMap named `extra-ca-bundle` with key `ca-bundle.pem`. Generate it from an overlay:\n\n```yaml\nconfigMapGenerator:\n  - name: extra-ca-bundle\n    files:\n      - ca-bundle.pem\n    options:\n      disableNameSuffixHash: true\n```\n\n…or pre-create the ConfigMap in the target namespace.\n\n## RBAC\n\nThe CronJob's ServiceAccount permissions are kept minimal — one Role covering both modes:\n\n```yaml\nrules:\n  # get:   used in both modes\n  # patch: needed by the \"restart\" mode to update spec.template.metadata.annotations\n  - apiGroups: [\"apps\"]\n    resources: [\"deployments\"]\n    verbs: [\"get\", \"patch\"]\n  # scale subresource: used by the \"scale\" mode\n  - apiGroups: [\"apps\"]\n    resources: [\"deployments/scale\"]\n    verbs: [\"get\", \"patch\", \"update\"]\n```\n\nNamespace-scoped — the CronJob can only act on deployments in its **own** namespace.\n\n## Continuous integration\n\n[.github/workflows/build-publish.yaml](.github/workflows/build-publish.yaml) runs three jobs on every push to `main`, every tag `v*`, and (build-only) on pull requests:\n\n1. **`test`** — `cargo fmt --check`, `cargo clippy -D warnings`, `cargo test --release`. Cluster tests auto-skip in CI.\n2. **`build-image`** — matrix over both Containerfiles. Each variant is built with buildx, pushed to `ghcr.io/git001/k8s-scale-app-rs`, smoke-tested, then:\n   - signed with **cosign keyless** via GitHub OIDC (no key management, uses Sigstore Fulcio + Rekor),\n   - has an SPDX-JSON **SBOM** generated with syft attached via `cosign attest --type spdxjson`,\n   - has an **SLSA build provenance** attestation pushed with `actions/attest-build-provenance`.\n3. **`publish-chart`** — `helm lint` and `helm package` on every push; OCI push to `oci://ghcr.io/git001/charts` + cosign-sign + SLSA provenance only on tag events (avoids overwrite conflicts on GHCR).\n\n### Verifying a published image\n\n```bash\nIMG=ghcr.io/git001/k8s-scale-app-rs:latest\nIDENTITY='^https://github.com/git001/k8s-scale-app-rs/'\nISSUER=https://token.actions.githubusercontent.com\n\n# Signature\ncosign verify \\\n  --certificate-identity-regexp \"$IDENTITY\" \\\n  --certificate-oidc-issuer \"$ISSUER\" \\\n  \"$IMG\"\n\n# SBOM attestation\ncosign verify-attestation --type spdxjson \\\n  --certificate-identity-regexp \"$IDENTITY\" \\\n  --certificate-oidc-issuer \"$ISSUER\" \\\n  \"$IMG\"\n\n# SLSA provenance\ncosign verify-attestation --type slsaprovenance \\\n  --certificate-identity-regexp \"$IDENTITY\" \\\n  --certificate-oidc-issuer \"$ISSUER\" \\\n  \"$IMG\"\n```\n\nFor enforcement inside the cluster, use an admission controller that consumes cosign attestations — [Kyverno](https://kyverno.io/), [Sigstore policy-controller](https://github.com/sigstore/policy-controller), or [Connaisseur](https://sse-secure-systems.github.io/connaisseur/).\n\n## License\n\nMIT — see [LICENSE](LICENSE). The published container images also ship an aggregated `/licenses/LICENSES.txt` with the full text of every Rust crate license bundled into the release binary, generated from [about.toml](about.toml) via `cargo-about`. Accepted SPDX identifiers are whitelisted there — an unknown license in a new dependency will fail the container build on purpose.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgit001%2Fk8s-scale-app-rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgit001%2Fk8s-scale-app-rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgit001%2Fk8s-scale-app-rs/lists"}