GREEN PACKAGE SKILL · MANAGED KUBERNETES · DIGITALOCEAN · VULTR
doks
Provision and operate one managed Kubernetes cluster on DigitalOcean DOKS or Vultr VKE through colors-compute, with an optional deployment-owned DigitalOcean container registry integrated into every namespace.
- One cluster per profile
- DOKS or VKE
- colors-compute managed-kubernetes
- Optional DO registry
- Green only
#Introduction
The doks Package Skill provisions one managed Kubernetes cluster named after the profile — DigitalOcean Kubernetes (DOKS) or Vultr Kubernetes Engine (VKE) — through the pinned colors-compute library’s managed-kubernetes kind. It creates no machines of its own: the provider runs the control plane and the worker pool, so the Compute Provider Standard’s machine API, the cluster standard and the SSH standards do not apply.
Optionally, the deployment also owns a DigitalOcean container registry (digitalocean-registry-tier) and integrates it with the cluster. Once linked, DOKS injects an image-pull Secret named after the registry into every namespace, so workloads pull from registry.digitalocean.com/<profile>/… without any package-side credential rotation.
The cluster it materializes is what consumer deployments such as redis-operator-doks run on; they receive it as a kubeconfig path, never as copied content.
build renders local files and create --dry-run walks the graph without contacting providers. Neither needs a credential. Review both before approving a real operation.#Quick start
npx skills add getcolors/doks --skill package-doks-green
cp .agents/skills/package-doks-green/green ./green
./green build
./green create --dry-run
# after approval:
./green create
./green check # every node Ready; registry present and integrated
./green kubeconfig # refresh .colors/<profile>/kubeconfig
./green registry # one-hour docker push configThe root ./green in a deployment is a copy of the payload, not a symlink. After npx skills update -p the payload changes and the root copy does not; repeat the cp or the project keeps running the old pin.
Choose an implementation
Green, Red and Blue provide the same six verbs, desired state and generated documents. Each runs its native SDK workflow and the matching colors-compute library.
| Skill | Runtime | Checkout launcher |
|---|---|---|
| package-doks-green | Babashka | ./green |
| package-doks-red | Bun / TypeScript | ./red/red |
| package-doks-blue | uv / Python | ./blue/blue |
npx skills add getcolors/doks --skill package-doks-red
cp .agents/skills/package-doks-red/red ./red
./red build
./red create --dry-runFor Blue, select package-doks-blue and copy its blue launcher. The examples below use Green. Substitute ./red or ./blue in an installed deployment. All launchers locate the nearest colors.yml above the working directory.
Switch an existing deployment
Keep the same colors.yml, profile, backend and credentials. Every implementation uses the same cluster and registry state keys. Installing another colour does not migrate state or create a second cluster. Run lifecycle commands one at a time against that profile.
npx skills add getcolors/doks --skill package-doks-blue
cp .agents/skills/package-doks-blue/blue ./blue
cmp .agents/skills/package-doks-blue/blue ./blue
./blue build
./blue create --dry-run
./blue checkcheck reads existing owned state, refreshes the local kubeconfig and verifies node readiness and registry integration. It needs backend credentials and the DigitalOcean token when a registry is configured. If the cluster is absent, the check fails; review that result before an authorized create. For Red or Green, substitute the corresponding skill name and launcher. Repeat the copy and comparison after every skill update.
Runtime prerequisites
Install Babashka for Green, Bun for Red, or uv with Python 3.11 or newer for Blue. Copied launchers fetch pinned dependencies on first use and need network access for that first run. Blue uses uv’s script environment without a manually created virtual environment. Live state and lifecycle operations also need OpenTofu (tofu) and the backend’s command-line tools; check needs kubectl. Installing skills with npx requires Node.js and npm. Render-only builds need no provider credentials.
./scripts/parity.sh compares DigitalOcean with a registry and Vultr without a registry byte for byte across all colours. Unit tests check lifecycle refusals, registry integration, credential permissions and deletion order.
#Workflow architecture
The workflow is one wire-fn per verb over a handful of steps. Steps that touch a provider, state or the local credential files are marked side-effecting, so --dry-run skips them without writing generated files.
build / create start ─ infrastructure ─ registry ─ registry-link
check start ─ load ─ check-nodes ─ check-registry
kubeconfig start ─ load ─ kubeconfig
registry start ─ registry-credentials
delete start ─ load ─ registry-unlink ─ infrastructure ─ registry ─ cleanup- infrastructure calls the library’s
plan-managed-kubernetesonbuild(writing its documents under.colors/<profile>/compute/managed-kubernetes/) andmanaged-kuberneteson a realcreateordelete. The library owns the journal, coordination, plan safety, the state key and the kubeconfig write. The request declares the pre-library key<profile>/cluster.tfstateas legacy, so a create refuses while old state still owns a cluster. - load reads the cluster from owned state without mutating it and re-materializes the kubeconfig. An absent or destroyed cluster fails
checkandkubeconfig, and letsdeletecarry on to the registry stage. - registry is a package-owned OpenTofu stage — one
digitalocean_container_registrynamed after the profile withprevent_destroybound tocompute-prevent-destroy. Nothing renders when no registry is configured. - registry-link and registry-unlink call the DigitalOcean Kubernetes registry API with the cluster UUID. The link is verified through the cluster’s
registry_enabledflag; unlinking a cluster that is not integrated is already the desired state. - check-nodes runs
kubectl get nodesagainst the materialized kubeconfig and requires every node Ready, printing the cluster name and id and each node’s name and external IP. - cleanup removes the kubeconfig and the push config after a delete — both are dead bearer credentials — then the rest of
.colors/<profile>/registry/on a best-effort basis, reporting anything another user owns rather than failing.
On delete the order matters: the integration goes first while the cluster still exists to be unlinked, the cluster before the registry it pulled from, and the local access material last. Each stage prints one summary line (integration removed, cluster destroyed, registry destroyed, cleanup done) so a failure after destruction is unambiguous.
#Container registry
Presence of digitalocean-registry-tier is the one switch. Set to starter, basic or professional, the deployment owns a DigitalOcean container registry named after the profile (lowercased, other characters removed) and integrates it with the cluster on create. Absent, there is no registry and the registry verb refuses. It is a validation error unless provider-compute is digitalocean.
No region is set on the registry: a DOKS region is not necessarily a registry region, and DigitalOcean picks one when none is given.
Pulling
With the integration in place, DOKS injects an image-pull Secret named after the registry into every namespace. Workloads reference registry.digitalocean.com/<profile>/<image> and pull without further configuration. check verifies the registry exists in the account and that the cluster reports it as integrated.
Pushing
./green registry requests read-write docker credentials valid for one hour and writes them to .colors/<profile>/registry/push/config.json (0600 inside a 0700 directory). The credential is never printed; point a build at it with DOCKER_CONFIG=.colors/<profile>/registry/push.
#Desired state
colors.yml is a flat, kebab-case, non-secret map. Provider settings, version slugs and node counts are validated by the library from its managed-provider recipes; the package validates only what it adds. Every validation and usage failure exits 2 and lists every problem at once.
| Key | Meaning | Example |
|---|---|---|
profile | Deployment identity: names the work directory, both state keys, the cluster and the registry. Safe identifier; never overlay it. | doks-dev |
workdir | Where generated output goes, relative to colors.yml. | .colors |
provider-compute | Managed Kubernetes provider: digitalocean or vultr. | digitalocean |
provider-backend | Remote state backend: r2 or s3 (the library also accepts gcs and oci). | r2 |
r2-bucket, r2-endpoint | R2 backend settings, with provider-backend: r2. | doks-state, https://<account>.r2.cloudflarestorage.com |
s3-bucket, s3-region | S3 backend settings, with provider-backend: s3. | doks-state, eu-west-1 |
digitalocean-region | DOKS region slug. | ams3 |
doks-version | DOKS version slug, x.y.z-do.n; must be currently offered (checked on a real create). | 1.36.3-do.2 |
digitalocean-node-size | Droplet size of the worker pool. | s-2vcpu-4gb |
digitalocean-node-count | Worker count, 1–1000. | 2 |
digitalocean-name | Optional cluster display name; absent, blank or REPLACE_ME uses the profile. Never changes state keys. | dev-cluster |
vultr-region | VKE region. | ams |
vultr-vke-version | VKE version, vX.Y.Z+n; must be currently offered. | v1.33.0+1 |
vultr-node-plan | Plan of the node pool. | vc2-2c-4gb |
vultr-node-count | Node count, 1–1000. | 2 |
vultr-name | Optional cluster label override, as digitalocean-name. | dev-cluster |
digitalocean-registry-tier | Optional. Present, the deployment owns a profile-named DigitalOcean registry on this tier — starter, basic or professional — integrated with the cluster. Requires provider-compute: digitalocean. | basic |
compute-prevent-destroy | Mandatory committed true. Bound to prevent_destroy on the cluster and the registry. | true |
Settings of the unselected provider are accepted and ignored. See the configuration reference; the repository’s own colors.yml is a worked example.
#Command reference
| Command | Effect | Needs |
|---|---|---|
./green build | Validate and render .colors/<profile>/; no provider calls. | Nothing |
./green create --dry-run | Validate and walk the graph; skip rendering and every side effect. | Nothing |
./green create | Converge the cluster, apply the registry, link the two. Writes the kubeconfig. | Provider token, backend credentials |
./green check | Every node Ready with its external IP; registry present in the account and integrated with the cluster. | Backend credentials; COLORS_PAR_DO_TOKEN when a registry is configured |
./green kubeconfig | Re-materialize .colors/<profile>/kubeconfig from state, mode 0600. | Backend credentials |
./green registry | One-hour read-write docker config under .colors/<profile>/registry/push/. | COLORS_PAR_DO_TOKEN |
./green delete | Guarded destruction: unlink, destroy the cluster, destroy the registry, clean up local credentials. | COLORS_PAR_COMPUTE_PREVENT_DESTROY=false, provider token, backend credentials |
Worker external IPs printed by check are what other packages’ SSH allowlists need. After a delete, the provider removes worker machines and cluster firewalls asynchronously over the following minutes — on DigitalOcean the worker Droplet and the two k8s-<cluster-id>-* firewalls stay visible briefly — and check is expected to fail from then on.
#State and generated output
State lives in the configured R2 or S3 bucket under two keys:
<profile>/compute/managed-kubernetes.tfstate— the cluster, owned by the library.<profile>/registry.tfstate— the registry, owned by the package, on the same backend with the same lockfile and checksum settings.
Under .colors/<profile>/: compute/managed-kubernetes/ (the library’s rendered documents, build only), doks-registry/ (the registry stage), kubeconfig (owner-only, written by create, check and kubeconfig), and registry/push/config.json (owner-only, written by registry, valid one hour). It is generated, sensitive output — never edit it, never read it as source, never commit it.
#The kubeconfig handoff
A consumer deployment on this cluster reads .colors/<profile>/kubeconfig from the doks deployment directory, or runs ./green kubeconfig there to refresh it, and exports KUBECONFIG pointing at that file.
# in the consumer deployment's .envrc
export KUBECONFIG=/path/to/doks-deployment/.colors/<profile>/kubeconfigdelete. Consumers must not copy it into their own tracked files.#Credentials and safety
- DigitalOcean:
COLORS_PAR_DO_TOKEN—createanddeleteon DigitalOcean,registry, andcheckwhen a registry is configured. The token travels only as an HTTP header or in the tofu process environment, never in argv or a rendered file. - Vultr:
COLORS_PAR_VULTR_API_KEY—createanddeleteon Vultr. - R2:
COLORS_PAR_R2_ACCESS_KEY_IDandCOLORS_PAR_R2_SECRET_ACCESS_KEYfor every verb that reads or writes state; S3 uses the ambient AWS credential chain. - Credentials live in the gitignored
.envrc.private— never incolors.yml, generated output or documentation.buildand--dry-runneed none. - Never export
COLORS_PAR_PROFILE. The profile keys remote state; the package refuses to run when it is set. - Keep
compute-prevent-destroy: truecommitted. Lift it for one authorized delete withCOLORS_PAR_COMPUTE_PREVENT_DESTROY=false; the guard refusesdelete --dry-runtoo, since rehearsing a refused delete rehearses nothing.
#Troubleshooting
“managed cluster is not present; run create first”
check and kubeconfig read the cluster from owned state and need it present. After a delete this is the expected outcome.
The registry verb refuses
digitalocean-registry-tier is not set. The registry exists only when that key is present, and only with provider-compute: digitalocean.
A create refuses because of legacy state
The request declares the pre-library key <profile>/cluster.tfstate as legacy; while that state still owns a cluster, the library refuses so two layouts never own the same resources. Migrate or retire the old state first.
Cleanup reports leftovers
A container build run as root leaves buildx/ state beside the push config. Cleanup reports paths it cannot remove instead of failing; remove them by hand.
Worker Droplets still visible after delete
DigitalOcean removes the worker machines and the k8s-<cluster-id>-* firewalls asynchronously, several minutes after the library’s destroy returns. Wait rather than deleting them by hand.