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.

Safe first stepsbuild 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 config

The 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.

SkillRuntimeCheckout launcher
package-doks-greenBabashka./green
package-doks-redBun / TypeScript./red/red
package-doks-blueuv / 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-run

For 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 check

check 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-kubernetes on build (writing its documents under .colors/<profile>/compute/managed-kubernetes/) and managed-kubernetes on a real create or delete. The library owns the journal, coordination, plan safety, the state key and the kubeconfig write. The request declares the pre-library key <profile>/cluster.tfstate as 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 check and kubeconfig, and lets delete carry on to the registry stage.
  • registry is a package-owned OpenTofu stage — one digitalocean_container_registry named after the profile with prevent_destroy bound to compute-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_enabled flag; unlinking a cluster that is not integrated is already the desired state.
  • check-nodes runs kubectl get nodes against 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.

KeyMeaningExample
profileDeployment identity: names the work directory, both state keys, the cluster and the registry. Safe identifier; never overlay it.doks-dev
workdirWhere generated output goes, relative to colors.yml..colors
provider-computeManaged Kubernetes provider: digitalocean or vultr.digitalocean
provider-backendRemote state backend: r2 or s3 (the library also accepts gcs and oci).r2
r2-bucket, r2-endpointR2 backend settings, with provider-backend: r2.doks-state, https://<account>.r2.cloudflarestorage.com
s3-bucket, s3-regionS3 backend settings, with provider-backend: s3.doks-state, eu-west-1
digitalocean-regionDOKS region slug.ams3
doks-versionDOKS version slug, x.y.z-do.n; must be currently offered (checked on a real create).1.36.3-do.2
digitalocean-node-sizeDroplet size of the worker pool.s-2vcpu-4gb
digitalocean-node-countWorker count, 1–1000.2
digitalocean-nameOptional cluster display name; absent, blank or REPLACE_ME uses the profile. Never changes state keys.dev-cluster
vultr-regionVKE region.ams
vultr-vke-versionVKE version, vX.Y.Z+n; must be currently offered.v1.33.0+1
vultr-node-planPlan of the node pool.vc2-2c-4gb
vultr-node-countNode count, 1–1000.2
vultr-nameOptional cluster label override, as digitalocean-name.dev-cluster
digitalocean-registry-tierOptional. 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-destroyMandatory 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

CommandEffectNeeds
./green buildValidate and render .colors/<profile>/; no provider calls.Nothing
./green create --dry-runValidate and walk the graph; skip rendering and every side effect.Nothing
./green createConverge the cluster, apply the registry, link the two. Writes the kubeconfig.Provider token, backend credentials
./green checkEvery 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 kubeconfigRe-materialize .colors/<profile>/kubeconfig from state, mode 0600.Backend credentials
./green registryOne-hour read-write docker config under .colors/<profile>/registry/push/.COLORS_PAR_DO_TOKEN
./green deleteGuarded 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>/kubeconfig
Hand over the path, never the contentsThe kubeconfig is a bearer credential: owner-only, generated, never committed, dead after delete. Consumers must not copy it into their own tracked files.

#Credentials and safety

  • DigitalOcean: COLORS_PAR_DO_TOKENcreate and delete on DigitalOcean, registry, and check when 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_KEYcreate and delete on Vultr.
  • R2: COLORS_PAR_R2_ACCESS_KEY_ID and COLORS_PAR_R2_SECRET_ACCESS_KEY for every verb that reads or writes state; S3 uses the ambient AWS credential chain.
  • Credentials live in the gitignored .envrc.private — never in colors.yml, generated output or documentation. build and --dry-run need none.
  • Never export COLORS_PAR_PROFILE. The profile keys remote state; the package refuses to run when it is set.
  • Keep compute-prevent-destroy: true committed. Lift it for one authorized delete with COLORS_PAR_COMPUTE_PREVENT_DESTROY=false; the guard refuses delete --dry-run too, 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.