Getting a Kubernetes cluster shouldn't involve a wizard, a downloaded file and a kubeconfig you'll have lost by Friday. With the Kupe CLI it's four commands, and the last one is kubectl:
kupe auth login
kupe cluster create dev --cpu-limit 2 --memory-limit 8Gi --storage-limit 50Gi
kupe cluster kubeconfig dev --merge --exec
kubectl get pods
This post walks through what each of those does, why we built the CLI the way we did, and how the same commands hold up when a CI job runs them with nobody watching. If you already use kubectl, most of it should feel familiar. That was the plan.
Install and sign in
You'll need a Kupe Cloud account first, and sign-up is open during the alpha.
The quickest install is Homebrew (brew install kupecloud/tap/kupe) or the install script (curl -fsSL https://get.kupe.cloud | sh). The CLI docs cover Windows and every other route.
kupe auth login uses the OAuth device flow, the same pattern a smart TV uses when it shows you a code and tells you to finish on your phone. The CLI prints a short code and a URL, you approve in any browser, and the CLI collects the tokens. Your password never passes through it, and because there's no local web server involved it behaves the same over SSH or in a dev container. Tokens live in your operating system's keyring where one exists, and refresh themselves on later commands.
Create a cluster
kupe cluster create dev --cpu-limit 2 --memory-limit 8Gi --storage-limit 50Gi
The three limits have no defaults. They cap what workloads in the cluster can use and count against your plan's pool; kupe plan list shows what your plan allows.
Underneath, cluster creation is asynchronous. The API accepts the request straight away and the cluster moves through phases (pending, provisioning, running) while the platform builds it. The CLI watches that phase and only returns once the cluster is running, so the next line of a script can assume the cluster exists. If you'd rather have your prompt back immediately, pass --wait=false and come back later with kupe cluster wait dev --for running.
Connect kubectl
kupe cluster kubeconfig dev --merge --exec
Without --merge, the command prints a kubeconfig to stdout for you to redirect into a file. With it, the cluster is added to the kubeconfig you already use, under a context named kupe-<tenant>-<cluster>, and becomes your current context, so there's no second file to keep track of.
The --exec flag changes what goes in that kubeconfig. By default it embeds your token, which makes the file a secret and means it stops working when the token does. With --exec there is no token in the file at all. Instead, the file tells kubectl to run kupe auth get-token whenever it needs a credential, using Kubernetes' standard exec credential plugin mechanism (hence the flag name). The AWS, Google and Azure CLIs connect kubectl to their managed clusters the same way.
The trade-off is that kubectl now needs kupe on its PATH. That suits a laptop. For a throwaway container where you can't count on the binary being there, leave --exec off and treat the kubeconfig as the credential it is.
Why it looks like kubectl (mostly)
Our design notes start from two rules. The first is that a developer at a terminal and a CI job run the same commands, and the binary adapts to which one it is. Spinners, colour and confirmation prompts only appear on a terminal. What the CLI writes to stdout is the same either way, with progress and errors kept on stderr, so kupe cluster list -o json | jq never finds a spinner in its input.
The second rule is that we shouldn't surprise kubectl users, since you're probably switching between the two all day. So -o takes table, wide, json, yaml, name and go-template=..., and exit codes and error shapes follow kubectl wherever it has a convention.
We deliberately broke from kubectl in a few places, and each time there was an alternative we weighed:
-
Noun first.
kubectlputs the verb first (kubectl get pods) because it's a generic client for dozens of resource kinds that all share the same verbs. Kupe has a handful of nouns and you'll typekupe cluster <tab>far more often thankupe <verb>, sokupe cluster creategives completion a smaller list to offer.gh,flyandhcloudread the same way. -
-o json, not--json.ghandflyuse a--jsonflag. We kept kubectl's-obecauseyaml,name,wideandgo-templateall belong under one umbrella. - Our own printer. We looked at reusing the printing library from Kubernetes itself, but it expects Kubernetes objects. Our resources are plain structs, so we wrote a small printer instead.
Contexts keep you honest
Sign in to a second tenant and you get a second context, so staging and production can't blur together:
kupe auth login --tenant acme-staging --context staging
kupe config use-context staging
kupe config current-context
kupe config view shows the active configuration with the tokens redacted, which makes it safe to paste into a support request. Every command also takes --tenant and --context (or KUPE_TENANT and KUPE_CONTEXT), which is what you'll reach for in scripts.
Running it in CI
A pipeline can't open a browser, so it signs in with an API key instead. Mint one scoped to the tenant, give it an expiry, and store the output as a CI secret:
kupe apikey create --name ci --role admin --expires-at 90d
The raw key is printed once and can't be retrieved again. Pass it to the CLI as KUPE_API_TOKEN; the --token flag also works but leaves the value in shell history and process listings, which is why the help text itself warns against it.
Here is a job that creates a cluster, deploys to it, and always cleans up:
#!/usr/bin/env bash
set -euo pipefail
export KUPE_API_TOKEN="$CI_KUPE_TOKEN"
export KUPE_TENANT="acme"
cluster="ci-${GITHUB_SHA:0:7}"
trap 'kupe cluster delete "$cluster" --yes' EXIT
kupe cluster create "$cluster" \
--cpu-limit 2 --memory-limit 8Gi --storage-limit 50Gi \
--wait-timeout 10m
kupe cluster kubeconfig "$cluster" --merge --force
kubectl --context "kupe-${KUPE_TENANT}-${cluster}" apply -f example.yaml
Three details do the work here. create waits by default, so there's no sleep to guess at. Outside a terminal, delete refuses to run without --yes, so a stray redirect can't delete infrastructure. And delete waits as well, so a script that recreates the same name straight away isn't racing the old cluster on its way out. We learned that one from our own tests, when a destroy-and-recreate collided with the previous cluster's teardown. The platform copes with that now, but waiting is still the safer default.
The exit codes are part of the contract too, and they're stable across releases. 3 means authentication failed, 4 means not found, 5 means conflict, and 8 means a --wait-timeout ran out. That lets a script tell a bad token from a slow cluster without parsing error text. The full table is in the repository.
We rely on that contract ourselves. The CLI's live test suite builds the real binary and runs it against the deployed API: whoami, tenant and member commands, a bad token failing without leaking into the output, and an opt-in create-and-delete of a real cluster. If a command's output or exit code changes, that's where we find out.
-o name helps with housekeeping too. A cluster per commit counts against your plan's cluster limit, so this clears out any CI clusters a cancelled job left behind:
kupe cluster list -o name | grep '^ci-' | xargs -n1 kupe cluster delete --yes
Where to start
The CLI is imperative, which makes it good for trying things out and for the glue between CI steps, and a poor record of what exists. If you want clusters reviewable in a pull request, use the Terraform provider. If you're writing a service, call the API. If your team needs a shared view, use the console.
For a first run, create a throwaway cluster, merge its kubeconfig, run a few kubectl commands, then delete it and watch the delete wait for you. If you hit any issues while experimenting with the CLI, get in touch and we'll do our best to help.
Top comments (0)