Skip to content

Repository files navigation

installer

Kubernetes off-the-shelf component installer using configuration as data.

This tool is intended to play the role of an installer wizard and a package dependency manager.

This installer aims to present only the minimal number of high-level decisions, such as which components to install and where to install them. It is recommended to set reasonable defaults as much as possible.

There's a local "fact collection" extension point. This can be used to collect details about the workload being deployed, verify existence of dependencies, and/or to retrieve or generate secrets and convert them to Kubernetes secrets. For cases where installation decisions depend on hardware, operating system, networking, or other details of the target cluster, Target Facts can be used, and can also be collected (assuming permission and access to the cluster) using cub k8s collect <target>.

As with installer wizards for systems other than Kubernetes, changes to detailed default settings are deferred until after installation. Configuration as data makes this possible by storing the configuration data rather than re-rendering from scratch. That decouples configuration authoring from configuration editing. Changes can be merged.

This enables customizations that would need to all be combined a single generator in configuration as code to be decoupled. For example, configuration can be initially authored using whatever tools are most effective. Subsequently, installation can customize the configuration. Then additional changes can be made post-installation, during and after variant creation, and then operationally. Changes made at a later stage can be preserved while merging updates from upstream sources. Edits can be automated with tools, as well.

More explanation of the tool's approach can be found in Design principles.

With the configuration-as-data approach, code that operates on configuration is separate from the data, which is plain YAML (and other formats in the future). The "code" lives in the installer.

This tool leverages kustomize for its foundational composition and customization. It adds a Package manifest named installer.yaml that declares bases, available components, dependencies, and inputs. It also contains a sequence of ConfigHub functions executed at install (render) time locally via the SDK.

Output from the rendering process goes to plain YAML files that can be uploaded to ConfigHub for delivery via ArgoCD or Flux, or could be committed to git to be deployed by ArgoCD, Flux, or other Kubernetes applier or deployment tool.

For post-installation customization, ConfigHub's function suite includes functions for changing commonly changed Kubernetes resource properties, such as set-container-image, set-container-resources, set-replicas, set-env, and set-hostname, and general-purpose editing functions, such as yq-i, set-string-path, delete-path, and set-starlark.

Why not just kustomize, or kpt? Neither tool was really designed to be a full-blown installer, and package management was explicitly out of scope for kustomize and kubectl. As shown in the kpt guide, together they can enable simplifying the installation process, and ConfigHub's function suite provides more capabilities out than kustomize's built-in transformers and kpt's function catalog.

Additionally, a lot was learned from kustomize and kpt, but starting afresh made it easier to experiment with different design choices, such as pushing packages to OCI, storing rendered configuration somewhere other than git, and so on.

Status

Working:

  • Package authoring + distribution. installer package (deterministic bundle), push / pull / inspect / list / tag / login / logout (OCI artifacts), sign / verify (cosign keyed + keyless) with a ~/.config/installer/policy.yaml trust policy that gates pull and deps update.
  • Dependencies. SemVer resolver + lock (deps update, deps tree), multi-package render into per-dep subtrees, upload into per-dep Spaces, with the parent's Space recording its dependencies in a DependsOn annotation.
  • Install lifecycle. installer setup (the one-shot consumer entry point: optional pull, wizard with high-level component presets minimal / default / all / selected, render). Interactive + non-interactive. Prior-state re-entry from the work-dir's out/record/, or, for a fresh clone, from the installer record Unit an earlier upload left in ConfigHub. setup --output-oci can also write the rendered non-secret objects to a local OCI image layout or push them to a registry without a ConfigHub account.
  • Day-2 lifecycle. Re-running setup with --pull <new-ref> runs the schema-diff machinery (carry forward existing values, adopt new defaults, prompt for new required-without-default). installer upload sends each package's render to ConfigHub's upload API, which makes the package's Space match it: new resources become Units, changed ones are merged, dropped ones are emptied, and an unchanged work-dir writes nothing. Every upload is create-or-update, so the first and every later one run the same way. installer plan is the same requests as a dry run.
  • Image overrides. installer setup --set-image applies kustomize edit set image before render. Overrides round-trip via Inputs.Spec.ImageOverrides and carry forward across re-renders / upgrades. The chosen base must declare an images: block; render fails fast otherwise.

Stubbed: installer preflight — cluster-side constraint checks.

Build

make build       # writes bin/installer

The binary is named installer so the cub plugin protocol exposes it as cub installer ... (cub names plugins after the entrypoint basename in cub-plugin.yaml). installer shells out to kustomize for kustomize build. Install it from kubernetes-sigs/kustomize or brew install kustomize.

Quick start

End to end against the included example, no ConfigHub server required:

# 1. Inspect what's in the package.
bin/installer doc ./examples/hello-app

# 2. Setup: pull (here: a local dir), pick base + components, supply
#    inputs, and render. One command replaces pull + wizard + render.
mkdir -p /tmp/hello && cd /tmp/hello
bin/installer setup \
  --pull ./examples/hello-app \
  --non-interactive \
  --select monitoring --select ingress \
  --namespace demo

# 3. Upload to ConfigHub. Nothing local records the Space, so name it on
#    every upload and plan.
bin/installer upload --space my-greeter

# 4. Day-2: edit a rendered file, see what upload would do, apply.
$EDITOR out/manifests/deployment-demo-hello-app.yaml
bin/installer plan --space my-greeter    # dry run: what upload would change
bin/installer upload --space my-greeter

# 5. Upgrade: re-pull (atomic), re-render via setup, then upload.
bin/installer setup --pull ./examples/hello-app \
  --set-image nginxdemos/hello=nginxdemos/hello:plain-text-v2
bin/installer upload --space my-greeter

The wizard's --select is closed under each component's requires: list, so selecting ingress-tls automatically pulls in ingress. Conflicts and validForBases are enforced at solve time.

setup auto-detects whether the work-dir is a fresh install (no out/record/) or a re-entry (prior state present, possibly with a newer package). On re-entry it runs the schema-diff machinery: silently carry prior values, adopt new defaults, drop removed inputs, prompt or fail-fast on newly-required inputs.

The granular commands (pull, wizard, render) are still available when you want step-by-step control — see consumer-guide.md.

Working directory layout

After setup (or wizard + render), the working dir looks like:

<work-dir>/
├── package/                  # what 'pull' fetched
│   ├── installer.yaml
│   ├── bases/
│   └── components/
└── out/
    ├── manifests/            # per-resource YAML, ready to upload
    │   ├── deployment-<ns>-<name>.yaml
    │   ├── service-<ns>-<name>.yaml
    │   └── ...
    ├── compose/              # the synthesized kustomization driving render
    │   ├── kustomization.yaml    # references the chosen base + components
    │   ├── transformers.yaml     # resolved ConfigHubTransformers (chain)
    │   ├── validators.yaml       # resolved ConfigHubValidators (if any)
    │   └── installer-transformer.sh   # exec wrapper kustomize invokes
    └── record/               # the record of the render: selection, inputs, facts, chain, lock
        ├── selection.yaml    # base + closure-resolved components
        ├── inputs.yaml       # validated wizard answers
        ├── function-chain.yaml   # the resolved chain that ran (audit copy)
        └── manifest-index.yaml   # filename → kind/name/namespace

out/compose/ is durable, not a temp dir. You can cd out/compose && kustomize build --enable-exec --enable-alpha-plugins . to reproduce the render byte-for-byte outside the installer.

The two record docs (selection.yaml, inputs.yaml) are the load-bearing inputs to re-render: edit them, re-run installer render, get a deterministic new set of manifests.

Package format

A package is a kustomize tree wrapped with an installer.yaml:

apiVersion: installer.confighub.com/v1alpha1
kind: Package
metadata:
  name: my-package
installerMetadata:
  version: 0.1.0
spec:
  bases: # alternative top-level kustomize trees
    - { name: default, path: bases/default, default: true }
  components: # opt-in kustomize Components (kind: Component)
    - { name: monitoring, path: components/monitoring }
    - { name: ingress, path: components/ingress }
    - name: ingress-tls
      path: components/ingress-tls
      requires: [ingress]
      externalRequires:
        - {
            kind: WebhookCertProvider,
            name: cert-manager,
            issuerKind: ClusterIssuer,
          }
  externalRequires: [] # cluster preconditions not provided by this package
  provides: [] # cluster-scope resources this package installs (CRDs, etc.)
  clusterSingleton: [] # leader-election leases this package claims
  externalManifests: [] # remote release-tarball manifests to fetch + merge
  inputs: [] # wizard prompts
  transformers: # one or more groups of function invocations
    - toolchain: Kubernetes/YAML
      whereResource: ""
      invocations:
        - name: set-namespace
          args: ["{{ .Namespace }}"]

See examples/hello-app/ for a complete working package.

The transformers: list is resolved with Go text/template syntax — {{ .Namespace }}, {{ .Inputs.* }}, {{ .Selection.* }}, {{ .Facts.* }}, {{ .Package.* }} — then emitted as a ConfigHubTransformers KRM function config that kustomize invokes through the installer transformer exec plugin. Each group's toolchain and whereResource are applied per-group, so a single chain can mutate raw Kubernetes manifests with Kubernetes/YAML and AppConfig-carried files (AppConfig/Properties, AppConfig/Env, …) in the same render. Components can carry their own transformers: and validators: lists; they append to the package-wide chain only when the component is selected.

Plugin install

The binary doubles as a cub plugin. After publishing a release that includes a platform binary at the path bin/installer, install with:

cub plugin install confighub/installer

The cub-plugin.yaml at the repo root tells cub the entry point. Once installed, the same commands work via cub installer ....

Layout

.
├── cmd/installer/main.go       # CLI entry point
├── internal/
│   ├── cli/                    # cobra subcommands
│   ├── pkg/                    # package load + OCI pull (oras-go)
│   ├── bundle/                 # deterministic tarball for `installer package`
│   ├── selection/              # required-deps closure + conflict detection
│   ├── wizard/                 # interactive + non-interactive answer collection,
│   │                           # prior-state load, schema diff for upgrades
│   ├── collector/              # in-package fact collectors run by the wizard
│   ├── render/                 # kustomize compose + chain execution + split,
│   │                           # --set-image (kustomize edit) + image extraction
│   ├── deps/                   # SemVer resolver + lock writer
│   ├── upload/                 # discover packages + Spaces, build the installer
│   │                           # record and upload requests, report results,
│   │                           # image footer
│   ├── cubctx/                 # active cub context (org / server)
│   └── sign/                   # cosign sign + verify
├── pkg/api/                    # Package, Selection, Inputs, FunctionChain,
│                               # Lock schemas
├── packages/                   # "published" packages bundled in this repo
│   ├── kubernetes-resources/   # 11 canonical resource templates with
│   │                           # per-type defaults (used by `installer new`)
│   └── worker/                 # ConfigHub bridge worker
├── examples/                   # test fixtures for the e2e + unit tests
│   ├── hello-app/              # single-package end-to-end test package
│   ├── example-base/           # multi-package: shared base
│   └── example-stack/          # multi-package: depends on example-base
├── docs/                       # design + implementation plans (see below)
└── cub-plugin.yaml             # cub plugin manifest

The packages/ subdirectory holds packages that are intended for publication; we'll move them to a separate repo as the catalog grows. examples/ stays in this repo as test fixtures the e2e and unit tests exercise.

User docs

For people using the installer (writing packages, installing them, or managing day-2 changes). These are how-to and reference; the design docs below explain why the installer works the way it does.

  • Author guide — for package authors. Schema reference for installer.yaml, file organization, how the install pipeline consumes your declarations, authoring best practices, publishing, signing, and version-to-version evolution.
  • Author tutorial — hands-on walkthrough of building a small package from an empty directory to a signed OCI artifact. ~30 minutes. Read this before the author guide if you prefer learning by example.
  • Consumer guide — for operators consuming packages. Find a package, install it, make day-2 changes, upgrade, revert. Plus a troubleshooting section for the common errors.

Design docs

For contributors to the installer itself. If you're using or authoring packages, the user docs above are what you want.

  • Design principles — the seven principles the installer is anchored to (package files are read-only; spec is the round-trippable source of truth; two layers of override; optimize for the zero-override case; image management; defer to ConfigHub for what ConfigHub does well; configuration as data, not templates).
  • Package and dependency management + implementation plan — spec and phased build plan for bundling, OCI publish, dependency declaration and resolution, and signing. Phases 0–8 shipped.
  • Day-2 lifecycle: interactive wizard, plan, update, upgrade
    • implementation plan — spec and phased build plan for the interactive wizard, prior-state re-entry, plan vs ConfigHub, ChangeSet-wrapped update, staged upgrade with schema-diff, and --set-image overrides. Phases A–E shipped.
  • Kustomize transformer plugin and AppConfig support — design for the installer transformer exec plugin (folds the function chain into kustomize), durable out/compose/, component-scoped function-chain mixins, and AppConfig support via configMapGenerator round-trip. Not yet implemented.

Multi-package example

examples/example-stack depends on examples/example-base. To exercise the full pipeline locally:

test/e2e/package-and-deps.sh

That script starts registry:2, pushes example-base, runs wizard → deps update → render, asserts the output layout + digest stability, and (when INSTALLER_E2E_CONFIGHUB=1) drives the full day-2 flow against the live server: upload → plan (clean) → edit → plan → upload → upload (no-op) → setup --pull (edited source) → upload → setup --set-image → upload → setup (override carries forward) → upload (no-op) → --set-image preflight rejection. Spaces created with the installer-e2e-* prefix are cleaned on exit. test/e2e/upload-reconcile.sh drives the worker package, which has an AppConfig carrier and a collector, through upload, re-upload, emptying, a DestroyGate refusal, and recovering a fresh work-dir from ConfigHub.

Roadmap

  • Better Secrets support. Currently we generate secrets during fact collection. Explicit support for ESO and CSI, and possibly "drop" and "stub" options.
  • installer preflight — evaluate externalRequires against a live cluster.
  • More packages: Flux, llm-d, KServe, vLLM production stack (KubeRay and Gateway API Inference Extension shipped — see examples/).
  • TBD: More comprehensive hooks, in-cluster and local.

About

Kubernetes off-the-shelf component installer using configuration as data

Resources

Stars

16 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages