Skip to content
Docs

Writing a Module

A module is a git repository (or directory) of operation files that hyve runs to manage one kind of cluster: create it, report its status, get a kubeconfig for it, scale it, and delete it. Hyve itself contains no cloud SDKs — everything provider-specific lives in a module, written in whatever tooling the provider already has (a CLI, Terraform, an HTTP API with curl, SSH).

This guide builds one from scratch. For the concepts behind it, see Modules.

1. Scaffold

Terminal window
hyve module init my-provider # module.yaml plus stub operations
hyve module init my-provider --auth-only # module.yaml + auth.sh only, for clusters hyve never provisions

A complete module looks like this:

my-provider/
module.yaml # manifest: name, version, type, params, requirements
status.yaml # required — reports whether the cluster exists
create.yaml # provision
delete.yaml # deprovision
auth.yaml # write a kubeconfig
scale.yaml # optional — apply param changes to a live cluster
describe.yaml # optional — report a live cluster's params (for `hyve cluster adopt`)
README.md

Each operation can be <op>.yaml (a Workflow, or ClusterAuth for auth), <op>.sh, or a bare <op> file — hyve uses the first one it finds, in that order.

2. Write the manifest

module.yaml
apiVersion: v1
kind: Module
metadata:
name: my-provider
version: 1.0.0
description: Kubernetes clusters on My Provider
type: driver # or authOnly — see "Auth-only modules" below
spec:
params:
- name: node_size
description: Node instance size
required: true
- name: node_count
description: Number of worker nodes
default: "3"
requirements:
env:
- name: MYPROVIDER_TOKEN
description: API token for My Provider
tools:
- name: myprovider
description: My Provider's CLI
- name: kubectl
description: Required for auth
FieldWhat hyve does with it
metadata.typedriver (default) or authOnly.
spec.paramsDocumentation only — shown by hyve module info. default, required, and choices are not applied or enforced; your scripts must handle missing values themselves (see below).
spec.requirements.toolsChecked on PATH before every operation in local mode, failing fast with the list of missing tools. In cluster mode operations run in a container, so the tools must be in its image instead.
spec.requirements.envChecked before every operation in cluster mode — a missing variable fails with a clear error instead of an opaque CLI failure. Not enforced in local mode, where credentials often come from somewhere else (a CLI’s own config file).
spec.requirements.mgmtClusterA default management cluster — another cluster hyve manages that this module drives, e.g. a Cluster API management cluster. A template’s or cluster’s spec.mgmtCluster overrides it; leave it unset in a module shared across installs. See Management clusters.

3. Know what your scripts receive

Every operation runs with the caller’s environment plus:

VariableValue
HYVE_CLUSTER_NAMEThe cluster’s metadata.name
HYVE_CLUSTER_REGIONThe cluster’s region
HYVE_PARAM_<KEY>Each spec.params entry, key uppercased — node_size → HYVE_PARAM_NODE_SIZE
Every driverOutputs keyValues create printed (see step 4), exactly as printed
HYVE_MGMT_KUBECONFIGOnly when the cluster has a management cluster (its spec.mgmtCluster, or the module’s requirements.mgmtCluster default): path to a kubeconfig for it. When it’s hyve’s own host (access.method: primary), cluster mode mints a fresh one; otherwise it’s the one the management cluster’s own auth last wrote, so it must have reconciled (or you must have run hyve cluster auth <name>) first.

Scripts run through /bin/sh with the working directory set to the consuming repository’s root, not your module’s directory. Don’t source ./lib/common.sh from another file in the module — it won’t be found once the module is installed from git. Inline shared helpers instead.

4. Follow the output contract

Hyve reads stdout line by line and keeps every line of the form HYVE_<NAME>=<value>. Everything else on stdout is ignored; send logs to stderr so they show in the reconcile output without being parsed.

  • Values printed by create are stored as the cluster’s driverOutputs (locally in cluster-state/<name>.state.yaml, in cluster mode on the ClusterDefinition’s status) and passed back into every later operation. This is the only way to carry state forward — for example, create prints HYVE_CLUSTER_ID=... and delete reads $HYVE_CLUSTER_ID. Other operations’ outputs are read for that run only (status’s HYVE_CLUSTER_STATUS) and not stored, so print everything later operations need from create.
  • A non-zero exit fails the operation.

Status values

status must print HYVE_CLUSTER_STATUS=<value>, uppercase, one of:

ValueWhat the reconciler does
NOT_FOUNDRuns create (or, if the cluster is marked delete: true, treats it as already gone and removes its definition)
ACTIVERuns auth, then scale if params changed, then applies spec.resources (or runs delete if marked for deletion)
FAILEDRetries create (or runs delete if marked for deletion)
CREATING / UPDATING / DELETINGWaits — skipped this cycle

Anything else, including printing nothing, is reported as an error.

5. Write the operations

YAML operations use the Workflow shape. All steps of all jobs are concatenated, in order, into one shell script run with set -e, so a variable or function defined in one step is visible in the next. Use the list form of jobs — the map form’s order isn’t guaranteed.

status.yaml
apiVersion: v1
kind: Workflow
metadata:
name: status
spec:
jobs:
- name: check
steps:
- name: query
script: |
if ! out=$(myprovider cluster show "$HYVE_CLUSTER_NAME" --region "$HYVE_CLUSTER_REGION" -o json 2>&1); then
case "$out" in
*"not found"*) echo "HYVE_CLUSTER_STATUS=NOT_FOUND"; exit 0 ;;
*) echo "status check failed: $out" >&2; exit 1 ;; # unsure → fail, never guess NOT_FOUND
esac
fi
case "$(echo "$out" | jq -r .state)" in
running) echo "HYVE_CLUSTER_STATUS=ACTIVE" ;;
provisioning) echo "HYVE_CLUSTER_STATUS=CREATING" ;;
deleting) echo "HYVE_CLUSTER_STATUS=DELETING" ;;
error) echo "HYVE_CLUSTER_STATUS=FAILED" ;;
*) echo "unexpected state" >&2; exit 1 ;;
esac
create.yaml
apiVersion: v1
kind: Workflow
metadata:
name: create
spec:
jobs:
- name: create
steps:
- name: provision
script: |
id=$(myprovider cluster create "$HYVE_CLUSTER_NAME" \
--region "$HYVE_CLUSTER_REGION" \
--size "$HYVE_PARAM_NODE_SIZE" \
--nodes "${HYVE_PARAM_NODE_COUNT:-3}" \
--wait -o json | jq -r .id)
echo "HYVE_CLUSTER_ID=$id"
echo "HYVE_CLUSTER_STATUS=ACTIVE"
delete.yaml
apiVersion: v1
kind: Workflow
metadata:
name: delete
spec:
jobs:
- name: delete
steps:
- name: deprovision
script: |
myprovider cluster delete "${HYVE_CLUSTER_ID:-$HYVE_CLUSTER_NAME}" --yes --wait

scale runs when an ACTIVE cluster’s params change; apply the new HYVE_PARAM_* values to the live cluster. Leave it out if changing params in place isn’t supported.

6. Write auth

auth.yaml has kind: ClusterAuth and one or more named methods; the first is the default, and hyve cluster auth <name> --method <name> picks another.

auth.yaml
apiVersion: v1
kind: ClusterAuth
metadata:
name: my-provider-auth
spec:
methods:
- name: cli
description: Fetch a kubeconfig with the provider CLI
deps: [myprovider]
auth:
script: |
myprovider cluster kubeconfig "${HYVE_CLUSTER_ID:-$HYVE_CLUSTER_NAME}" > "$KUBECONFIG"
exports: KUBECONFIG

With exports: KUBECONFIG, hyve sets $KUBECONFIG to a file of its own for this cluster before the script runs — write the kubeconfig to that path. Hyve then uses it to apply spec.resources, and hyve cluster auth merges it into ~/.kube/config. The same script works in cluster mode, where hyve relays the file back from the container for you.

7. Test it locally

Reference the module by local path while you develop — no publishing needed:

# clusters/test.yaml in any hyve directory
apiVersion: hyve.io/v1alpha1
kind: ClusterDefinition
metadata:
name: test
spec:
region: us-east-1
driver:
source: ../my-provider # ./, ../, or an absolute path
params:
node_size: small
Terminal window
hyve reconcile --dry-run # runs status, shows what would happen
hyve reconcile # create
hyve cluster auth test && kubectl get nodes
hyve cluster delete test && hyve reconcile

For an existing cluster, implement describe (print its live params as HYVE_PARAM_<KEY>=value, plus any IDs as other HYVE_* lines) and bring it under management without recreating it:

Terminal window
hyve cluster adopt existing-cluster --template my-template

8. Publish it

Push the module to a git repository and tag releases with semver (v1.0.0). Consumers reference it as host/org/repo, or host/org/repo//path/to/module when one repository holds several modules:

Terminal window
hyve module add github.com/your-org/hyve-my-provider # latest semver tag
hyve module add github.com/your-org/hyve-my-provider@v1.2.0 # exact tag
hyve module add github.com/your-org/hyve-my-provider@main # any branch, tag, or commit

hyve module add pins the resolved version and a SHA256 of its contents in the consumer’s hyve.lock; hyve module update <source> <version> re-resolves it (e.g. hyve module update github.com/your-org/hyve-my-provider latest after tagging a new release). With no semver tags, latest resolves to the default branch’s HEAD.

Private repositories work too. Hyve fetches with a real git clone, so any host works with whatever git credentials the machine already has (SSH key, credential helper). For github.com, a GITHUB_TOKEN in the environment is also used. In cluster mode, set GITHUB_TOKEN as a secret on hyve-api (hyve context secrets set GITHUB_TOKEN).

Running in cluster mode

When hyve runs as a controller, each operation runs in a short-lived Kubernetes Job instead of on a local machine. The container image is the cluster’s (or its template’s) spec.runner.image, falling back to HyveConfig.spec.defaultModuleImage. That image must contain every tool in your requirements.tools. Credentials come from hyve-api’s secrets (hyve context secrets set), and requirements.env is enforced against them.

Auth-only modules

For a cluster hyve should track and hand out kubeconfigs for, but never create or delete (a local k3d cluster, something provisioned elsewhere), set metadata.type: authOnly and implement only auth (plus status if you want hyve to notice it disappearing). With no status output the cluster is treated as ACTIVE, so spec.resources still apply. hyve module init <name> --auth-only scaffolds this shape.

Checklist

  • status exits non-zero when it can’t tell — it only prints NOT_FOUND when sure
  • Every status value is one of ACTIVE, NOT_FOUND, FAILED, CREATING, UPDATING, DELETING
  • Only HYVE_*=value lines go to stdout; logs go to stderr
  • Params are snake_case, and scripts supply their own defaults
  • IDs delete/scale need are printed by create as HYVE_* outputs
  • No cross-file source — scripts run from the consuming repo’s root
  • auth writes the kubeconfig to $KUBECONFIG, named after $HYVE_CLUSTER_NAME
  • Tools are listed in requirements.tools, and present in your runner image for cluster mode
  • Tested end to end from a local path; released with a semver tag