Skip to content

Repository files navigation

darkubectl — kubectl-like access to the Hamravesh Darkube platform

CI Release Go Reference Downloads License

Your Darkube apps, from the terminal. List, inspect, tail, shell into and create apps on the Hamravesh platform — without opening the console.

Why

Darkube is a good PaaS with one gap: everything runs through the web console. There is no kubectl for it, no way to grep a log, no way to script a deploy check, no way to see that a deleted app left its Helm release running in your cluster.

darkubectl is that missing CLI. It speaks the Hamravesh API (https://api.hamravesh.com) and its websockets directly, so the mental model is the one you already have: tenant = organization, namespace = project, app = workload.

darkubectl get apps -o wide          # instead of five clicks
darkubectl logs app api -f           # instead of the log panel
darkubectl terminal app api          # instead of the web terminal
darkubectl get orphans               # something the console cannot tell you at all

Highlights

🔎 Familiar verbs get, describe, logs, exec, create, delete — the kubectl muscle memory carries over.
🧭 Real terminals terminal app <name> opens an interactive shell over the exec websocket, resize and all. exec runs one-off commands.
📜 Logs that pipe logs -f to follow, --previous for the container that just crashed, --timestamps for correlation.
🚀 Deploys that wait set image changes the running image and --wait follows the rollout pod by pod, so a pipeline fails on a crash-looping release instead of passing because the old pod is still up. rollout restart and rollout status round it out.
📄 Apps as files get apps -o spec exports apps as YAML and apply -f makes apps match it — create what is missing, edit what differs, show the diff first. Round-tripped against 79 live apps with no drift.
🔌 Tunnels, not exposure tunnel up runs a chisel server as an app and tunnel connect forwards local ports through it, so you can reach a ClusterIP database from a laptop without a public LoadBalancer and without cluster credentials. The client is built in, and connect --host … --auth … works for someone with no Darkube account at all.
👻 Orphan detection get orphans reconciles your tenant against a live cluster and finds the Helm releases Darkube left behind on delete. Nothing else surfaces these.
📂 Files in and out cp copies files and directories to and from a pod over the exec terminal, with nothing to install on either side.
🎨 Readable output Colorized tables and a describe -i interactive viewer with search, degrading to plain text the moment you pipe it.
🤖 Scriptable -o json, -o yaml, -o name on everything, config via flags, env or file, and get deploy-token to wire a CI pipeline without the console.
🧠 Agent-ready Non-interactive, JSON-emitting and credentialed from the environment, so a coding agent can run it as a tool and manage Darkube for you.
🔐 Two auth modes An account API key for scripting, or a full 2FA Console login (TOTP) for terminals and app creation.
📦 One static binary Go, no client-go, no runtime deps. Linux, macOS and Windows on amd64 and arm64.

Namespaces are here too: create namespace <name> --cluster <name|id> and delete namespace <name|id>. Both need a Console JWT rather than an account key, which is why they live in the CLI and not in a curl one-liner. Two rules the API will not tell you: the name must start with the tenant slug (acme can create acme-production, never production), and every rejection — bad prefix, bad charset, bad length — comes back as the same opaque 400 نام انتخاب شده درست نیست. The prefix is checked locally so the error names the rule. Delete refuses while a namespace still holds apps; the API does not check, and deleting a populated project takes its workloads with it.

Install

Homebrew (macOS):

brew install rahacloud/tap/darkubectl

AUR (Arch Linux):

yay -S darkubectl-bin

Go:

go install github.com/rahacloud/darkubectl@latest

Binaries — grab a tarball for your platform from the latest release:

curl -sSfL https://github.com/rahacloud/darkubectl/releases/latest/download/darkubectl_$(curl -s https://api.github.com/repos/rahacloud/darkubectl/releases/latest | grep -o '"tag_name": "v[^"]*' | cut -d'v' -f2)_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz | tar xz darkubectl
sudo install darkubectl /usr/local/bin/

From source:

git clone https://github.com/rahacloud/darkubectl && cd darkubectl && go build -o darkubectl .

Shell completion is installed by the Homebrew and AUR packages. Otherwise, load it from the binary — it completes commands, flags, and app names:

source <(darkubectl completion bash)     # or: zsh, fish, pwsh

Quickstart

darkubectl config use-tenant <org-slug>     # e.g. rahacloud
darkubectl config set-token <your-api-key>  # from the Darkube console
darkubectl get apps

That covers everything read-only. For pod terminals and app creation, add a Console login:

darkubectl login                            # email + password + TOTP
darkubectl terminal app <name>

Let an AI agent run it

Darkube has no MCP server and no CLI of its own, which leaves an agent with nothing to hold. darkubectl closes that gap: it is one static binary, every subcommand is non-interactive, every read speaks JSON, and credentials come from the environment — the four properties a coding agent (Claude Code, Codex, Cursor, or anything that can run a shell) needs to use a tool unattended. Give it the binary and it can answer "is anything unhealthy in talaland-dev?" or "why did the api pod restart last night?" without a human opening the console.

export DARKUBE_TOKEN=<api-key> DARKUBE_ORG=<org-slug>

darkubectl get apps -o json | jq -r '.[] | select(.state.text != "Healthy") | "\(.namespace.name)/\(.name): \(.state.text)"'
darkubectl logs app api --previous --tail 200      # what the container said before it died
darkubectl get orphans --context <ctx>             # releases left behind by deletes

A few lines in your CLAUDE.md or AGENTS.md are usually enough to teach an agent the tool:

Our apps run on Darkube. Use darkubectl get apps -o json to inspect state, darkubectl describe app <name> -o yaml for configuration, and darkubectl logs app <name> --tail 200 to read logs. Run darkubectl get orphans before recreating an app whose name is taken. Never run delete and never pass -y.

Give the agent an API key rather than a Console login. The Api-key cannot open pod terminals or create apps, which bounds an agent's blast radius to reads. Keep delete out of its reach: the deletion is asynchronous and routinely orphans the Helm release, and no flag in the API can undo that — see Orphaned releases.

Authentication

Almost every request is scoped to an active tenant (organization) via the X-Organization: <tenant-slug> header, and carries one of two credentials:

  • an account API key — Authorization: Api-key <token>; or
  • a Console JWT from darkubectl login — Authorization: Bearer <jwt>.

Either credential drives the whole REST API. The Api-key is the simplest for scripting; a login is required additionally for the terminal/exec websocket (the Api-key cannot open it). If both are configured, the Api-key is used for REST and the JWT for the terminal.

Three commands need no tenant at all, because the endpoints behind them are account-wide rather than tenant-scoped: whoami (the account and its organizations), get notifications (a feed that spans every organization) and get plans (a global catalogue). That is what makes the first run work — whoami is how you find out which tenant slugs exist before selecting one. Everything else fails fast with no tenant selected rather than sending a request the API would answer with 403 permission_denied.

Config is stored at ~/.darkube/config.yaml (override with $DARKUBE_CONFIG), written 0600. Values can also be supplied via environment or flags, which take precedence:

Setting Flag Environment Config key
Token --token DARKUBE_TOKEN token
Tenant --org/-n DARKUBE_ORG current-tenant
Base URL --base-url DARKUBE_BASE_URL base-url
Config --config DARKUBE_CONFIG —

Usage

# Tenants (organizations)
darkubectl whoami                      # no tenant needed: the account, its tenants, their numeric ids
darkubectl get tenants                 # the tenants in the local config
darkubectl config use-tenant talaland

# Apps
darkubectl get apps                    # table
darkubectl get apps -o wide            # + type, cluster, RAM, CPU, domain, id
darkubectl get apps --managed          # only managed services (Redis, Postgres, Grafana, …)
darkubectl get apps --type redisnew    # only apps with this creation_method
darkubectl get apps -o json
darkubectl describe app <name|id>      # colorized key/value view
darkubectl describe app <name|id> -i   # interactive: scroll + / search
darkubectl describe app <name|id> -o yaml

# Other resources
darkubectl get namespaces              # projects (derived from apps)
darkubectl get certificates
darkubectl get plans                   # no tenant needed: the plan catalogue is global
darkubectl get disks                   # every persistent disk and the app it belongs to
darkubectl get disks --orphaned        # disks no app accounts for: a deleted app's disk stays billed
darkubectl get databases               # managed databases (DBaaS), which `get apps` does not list
darkubectl get services                # marketplace services (Jira, Rocket.Chat, n8n, …)
darkubectl get suggestions <name|id>   # the platform's configuration advice for an app
darkubectl get balance                 # cash and gift wallets
darkubectl get invoices                # monthly invoices; the current one is pending
darkubectl get members                 # the tenant's members and their roles
darkubectl get apikeys                 # no tenant needed: the account's API keys
darkubectl create apikey ci-pipeline   # prints the key once; the platform sets a two-year expiry
darkubectl delete apikey ci-pipeline

# App configuration
darkubectl get env <name|id>           # environment variables (secrets by name; --show-secrets for values)
darkubectl get domains <name|id>       # custom domains, DNS status, the platform's certificate, the CNAME target
darkubectl get domains <name|id> --tls # + the certificate each host actually serves
darkubectl get manifests <name|id>     # the Kubernetes objects the platform renders (--kind Ingress)
darkubectl get permissions <name|id>   # which members can view, change, delete it
darkubectl set permissions <name|id> dev@example.com --grant view   # or --revoke change, --none

# Notifications and monitoring
darkubectl get notifications           # no tenant needed: account feed, spans every tenant
darkubectl get notifications --unread
darkubectl get alerts                  # monitoring alerts for the current tenant
darkubectl get alerts --firing         # only what has not resolved
darkubectl get alerts --app n8n        # only alerts raised against one app or service

# Support tickets (the console's پشتیبانی page), for the current tenant
darkubectl ticket list                 # your tickets; --status open, --org-wide for every member's
darkubectl ticket view 33421           # the ticket and its thread, staff replies marked
darkubectl ticket types                # request types to file under
darkubectl ticket create --type darkube --summary 'Pod stuck in Pending' -f details.md --attach trace.log
darkubectl ticket reply 33421 -m 'Thanks, that fixed it.'
darkubectl ticket close 33421
darkubectl ticket rate 33421 5 --comment 'fast and clear'
darkubectl ticket download 9001        # an attachment id from `ticket view`

# Change history, builds and usage
darkubectl rollout history <name|id>             # who changed the app, when, which fields (last 7 days)
darkubectl rollout history <name|id> --since 30d --details   # with each field's old and new value
darkubectl get builds <name|id>                  # a git-backed app's builds, newest first
darkubectl build logs <name|id> [BUILD] -f       # the latest build's log, followed to the end
darkubectl build start <name|id> -f              # build the branch head and deploy it
darkubectl build retry <name|id> BUILD
darkubectl build stop <name|id> BUILD
darkubectl top app <name|id>                     # per pod: CPU, throttling, memory vs ram_limit, network
darkubectl top apps --sort-by memory             # every app in the tenant, busiest first

# Container registry (registry.hamdocker.ir)
darkubectl registry list                         # registries, push prefix, username, storage used
darkubectl registry images                       # image repositories
darkubectl registry tags my-api                  # manifests with their tags, newest first
darkubectl registry prune my-api --keep 10 --dry-run   # never deletes a tag an app is running
darkubectl registry delete my-api:old-tag        # or my-api@sha256:… for a manifest, or my-api for all
darkubectl registry password | docker login registry.hamdocker.ir -u <user> --password-stdin
darkubectl registry gc set --keep-count 20       # a retention rule the platform enforces itself

# CI credentials — the pair `darkube deploy` needs in a pipeline
darkubectl get deploy-token <name|id>          # app id + trigger deploy token
darkubectl get deploy-token <name|id> -o name  # the bare token, for a CI variable
darkubectl get ci-config <name|id>             # the .gitlab-ci.yml jobs the console generates (--for github|curl|env)
darkubectl deploy --app-id $ID --deploy-token $TOKEN --tag $SHA   # deploy from CI with no login or config

# Reconcile Darkube against a cluster (needs kubectl on PATH)
darkubectl get orphans                                  # every namespace in the current context
darkubectl get orphans --context <ctx> --namespace <ns>

# Mutations (all prompt for confirmation; pass -y to skip)
darkubectl scale app <name|id> --replicas 3
darkubectl set env <name|id> LOG_LEVEL=debug PORT=8080
darkubectl set env <name|id> --remove LOG_LEVEL
darkubectl set env <name|id> --secret DB_PASSWORD=s3cret   # a secret variable, kept across deploys
darkubectl api GET /api/v1/darkube/plans/     # one raw authenticated request, like kubectl get --raw
darkubectl set domain <name|id> --add api.example.com      # a domain you own
darkubectl set domain <name|id> --remove old.example.com
darkubectl set subdomain <name|id> my-api                  # -> my-api.darkube.app, with a cert
darkubectl set subdomain <name|id> --remove
darkubectl set svc-type <name|id> LoadBalancer            # expose it; ports are preserved
darkubectl set svc-type <name|id> ClusterIP --dry-run    # show the diff, send nothing
darkubectl set disk <name|id> --size 40                   # grow only; re-read to confirm it took
darkubectl set image <name|id> registry.hamdocker.ir/acme/api:1.4.2 --wait   # deploy, and follow it
darkubectl set command <name|id> --command "/bin/sh -c" --args 'cd /app && exec ./worker'
darkubectl set probe <name|id> --readiness /healthz --liveness /livez
darkubectl set resources <name|id> --plan 2                    # or a dynamic plan with --memory/--cpu
darkubectl autoscale app <name|id> --min 2 --max 6 --cpu-percent 70
darkubectl autoscale app <name|id> --disable
darkubectl patch app <name|id> -p '{"redirect_SSL": true}'
darkubectl patch app <name|id> -p '{"replicas": 3}' --dry-run   # show the diff, send nothing
darkubectl delete app <name|id>

# Block until a deploy has actually landed, instead of sleeping and hoping
darkubectl rollout restart <name|id> --wait     # new pods, old ones gone, all ready
darkubectl rollout status <name|id> --timeout 10m
darkubectl wait app <name|id> --for ready --timeout 10m
darkubectl wait app <name|id> --for deleted

# Apps as files: export, commit, apply
darkubectl get apps --namespace prod -o spec > prod.yaml
darkubectl apply -f prod.yaml --dry-run         # the diff, per app
darkubectl apply -f prod.yaml                   # create what is missing, edit what differs

# Reach a ClusterIP service from your laptop, without exposing it (the chisel
# client is built in, and the server side installs nothing)
darkubectl tunnel up --namespace talaland-dev --subdomain tld-tunnel
darkubectl tunnel connect 1433:mssql-dev.talaland-dev.svc:1433 5432:postgres-dev.talaland-dev.svc:5432
darkubectl tunnel down

# Create an app from a Docker image (needs a JWT login; see below)
darkubectl get plans                       # pick a plan (NAME column → --plan)
darkubectl get namespaces                  # pick a namespace (ID column → --namespace)
darkubectl create app my-api --namespace <ns> --plan 1 --image nginx:latest
darkubectl create app -f spec.yaml         # from a YAML spec (ports, disk, env)
darkubectl create app -i                   # interactive prompts

# Or let Darkube build the image from a repository (creation_method git_repo_url).
# Needs the git provider connected to the Hamravesh account under Integration.
darkubectl create app my-api --namespace <ns> --plan 1 \
  --git-repo https://github.com/acme/my-api --git-branch main

# Logs
darkubectl logs <name>                    # last 100 lines (pod auto-detected; `logs app <name>` also works)
darkubectl logs <name> --tail 500 -f      # follow
darkubectl logs <name> --previous         # the container instance that crashed
darkubectl logs <name> --timestamps --pod <p> -c <container>

# Terminal / exec — needs a JWT login (separate from the Api-key)
darkubectl login                          # email + password + TOTP → stores a refresh token
darkubectl get pods <name>                # an app's pods: READY, STATUS, RESTARTS, AGE
darkubectl get pods <name> -o wide        # plus the containers and why they last died
darkubectl get pods <name> -w             # a line per change, like kubectl get pods -w
darkubectl exec app <name> -- ls -la      # run a command in a pod
darkubectl cp ./site <name>:/usr/share/nginx/html   # files or directories, either way
darkubectl cp <name>:/var/lib/app/dump.sql ./
darkubectl terminal app <name>            # interactive shell (auto-detects the pod; alias: shell)
darkubectl terminal app <name> --pod <p> -c <container>

Output format is controlled by -o/--output: table (default), wide, json, yaml, or name — and, for get apps, spec, the file create -f and apply -f read. Scope any single command to a different tenant with -n <org>.

The app spec file

Flags cover the flat fields. Ports, persistent storage and environment variables are nested, so they are --file only:

name: masstransit-dev
namespace: "175864"        # id, or a name — ids always work, see below
plan: "1"                  # plan NAME from `get plans`, or its id
image: masstransit/rabbitmq:3.13.1
replicas: 1
svcType: ClusterIP         # or LoadBalancer; defaults to ClusterIP
ports:                     # keyed by name; "main" is the one ingress targets
  amqp: {containerPort: 5672, servicePort: 5672, protocol: TCP}
  main: {containerPort: 15672, servicePort: 15672, protocol: TCP}
disk:
  sizeInGi: 4
  setFsGroup: true
  storageClassName: rawfile-btrfs   # required — omitting it makes the API 500
  partitions:
    - {name: data, mountPath: /var/lib/rabbitmq/mnesia, subPath: data}
envs:
  - {name: RabbitMq__Host, value: masstransit-dev.talaland-dev.svc}
secretEnvs:
  - {name: RabbitMq__Password, value: hunter2}

command is split on whitespace; args is not. This is the sharpest edge in the API and it is not documented anywhere upstream:

command: /bin/sh -c      # SPLIT   -> ["/bin/sh", "-c"]
args:    echo hi         # NOT split -> ["echo hi"], one single argument

The arrangement everyone writes first — command: /bin/sh with args: -c echo hi — hands the container "-c echo hi" as one token, and busybox reads the space as another flag: /bin/sh: illegal option -, crash-looping, with nothing in the API response pointing at the cause. Put the words that need splitting in command, and the script whole in args: since args is never split, sh -c receives it as the single argument it wants, spaces and all. create app, set command and apply warn when args contains whitespace and command is not a shell ending in -c, and refuse a multi-element args list.

Both fields accept a string or a list, so command: ["/bin/sh", "-c"] works too.

A spec can also carry what the create call itself ignores; apply -f sets these with a follow-up write once the app exists:

autoscale: {min: 2, max: 6, cpuPercent: 70}   # replaces replicas: the autoscaler owns the count
probes: {readiness: /healthz, liveness: /livez}
plan: dynamic                                 # memory and cpu apply only to a dynamic plan
memory: 1500M
cpu: 750m

replicas left out means one at creation, and "leave it alone" on apply. That goes for every field: apply changes what the spec names and leaves the rest, so a trimmed file never resets anything by omission. get apps -o spec writes this format — secret values and domains are left out and named on stderr, since a spec should not carry secrets in the clear and set domain owns the second.

For an app Darkube builds itself, replace image with a git block:

git:
  repoUrl: https://github.com/acme/my-api
  branch: main
  dockerfile: ./Dockerfile     # default
  provider: Github             # default: inferred from the URL

These can also be changed after the fact. The API has no partial update — PATCH is unimplemented and returns 500 — so every mutation is a read-modify-write of the whole app, which darkubectl does for you. Most fields have a dedicated command (set env, set domain, set image, set command, set probe, set resources, set disk, autoscale), each of which reads the app back afterwards: the API answers 202 to a write it then discards, so a change it silently dropped is reported as an error. Anything else goes through patch app, which merges your JSON into the current object and writes it back. The one caveat of that approach: a console edit made between the read and the write is lost.

Namespaces resolve by name when they already contain an app; a brand-new empty project has to be referenced by id, which get namespaces prints.

A multi-port spec is not proven to work. The example above is the real masstransit-dev spec, and the app it produced has no container ports and no Service at all — darkube deploy reported success and said nothing. Every single-port app created alongside it got its Service, so main on its own is the shape known to work. Treat the second port as unverified rather than broken: that app was later deleted and its release orphaned (see below), which is a second candidate explanation nobody has separated from the first.

Reaching a service from outside

There are two ways, and they are not equivalent.

set svc-type <app> LoadBalancer asks the cluster for a public address. It is one command, needs nothing installed, and changes the running app in place — the type is a field on the app object like any other, so there is no delete-and-recreate and no risk of stranding the Helm release. Use it for something that is meant to be public.

Two things about it are worth knowing before you reach for it. The first is that the port that answers is the allocated nodePort, not the servicePort. Hamravesh fronts LoadBalancer services with a shared gateway, so an app whose servicePort is 5432 is reached on something like :30410, and the servicePort itself refuses connections — which reads as a broken exposure rather than a wrong port. The command prints the endpoint that actually works, and describe app reports it as svc.externalAddress alongside the per-port nodePort:

darkubectl set svc-type postgres-dev LoadBalancer
app/postgres-dev service type set to LoadBalancer

reachable at:
  c41ac067-….hsvc.ir:30410   (main, container port 5432)
note: that is the nodePort, not the service port. The gateway is shared, so the
      service port itself refuses connections.

The second is that there is now nothing between the internet and the app but whatever authentication the app itself asks for. For a database reachable with a superuser account, that is usually the wrong trade, which is what the other option is for.

tunnel runs a chisel server as an ordinary app and forwards local ports through it. Nothing is published: the tunnel rides the HTTP ingress the app already has, and only someone holding the credential minted at tunnel up can use it.

darkubectl tunnel up --namespace talaland-dev --subdomain tld-tunnel
darkubectl tunnel connect 1433:mssql-dev.talaland-dev.svc:1433
# -> localhost:1433 reaches the in-cluster database

REMOTEHOST is resolved inside the cluster, so it is the in-cluster address — describe app reports it as svc.internalAddress. Forwarding to localhost is rejected, because it would resolve to the tunnel pod's own (empty) loopback and produce a tunnel that connects and then refuses everything.

The credential is stored in the config file at creation, so connect needs no second lookup; get env --show-secrets on the tunnel app can recover it if the config is lost. --auth and $DARKUBE_TUNNEL_AUTH override it, which is what a CI job or a second machine wants.

The point of the tunnel is that it needs no cluster credentials at all. kubectl port-forward is the obvious alternative and often is not available: on Hamravesh it means an OIDC exec plugin and a browser login, and the RBAC a Darkube user is given is frequently read-only or absent — so the person who can deploy the app cannot necessarily reach it. Neither side of the tunnel installs anything: the server is an app, and the chisel client is linked into this binary. --chisel-binary runs an external one instead, for a different version or a patched build.

Handing a tunnel to someone else is --host, and it is the flag that makes this useful beyond your own laptop:

darkubectl tunnel connect --host tld-tunnel.darkube.app --auth tunnel:… \
    27017:mongodb-stage.talaland-stage.svc:27017

With --host the command makes no API call, so whoever runs it needs no Darkube account, no tenant and no login — only this binary, the hostname and the credential. Without it the tunnel app is looked up by name, which needs all three. tunnel up prints the exact --host line to pass on, credential included.

The hostname has to be one the other side can actually resolve. A platform subdomain (--subdomain, *.darkube.app) answers with a private address and is reachable only from inside Hamravesh's network, so for an outside collaborator the tunnel needs either a real domain CNAMEd at the cluster (--host on tunnel up, pointed at the ingress_cname_address from get domains) or a LoadBalancer on the tunnel app — which exposes chisel rather than the database, and chisel's own SSH layer keeps the forwarded traffic encrypted even over plain http://. --host accepts host, host:port or a full URL, and keeps an explicit scheme.

Orphaned releases

Deleting an app removes it from the API at once and tears its Helm release down separately — and the release is routinely left behind. It keeps running, keeps its darkube.hamravesh.com/app-id label, and keeps the name, so recreating under that name fails with SameHelmReleaseNameExists indefinitely. Neither side shows this on its own: get apps cannot list an app that no longer exists, and the cluster looks entirely normal.

get orphans is the reconciliation, comparing the tenant's apps against the Deployments in a cluster:

darkubectl get orphans --context <ctx> --namespace <ns>
NAME              NAMESPACE      KIND       APP-ID
masstransit-dev   talaland-dev   orphaned   a5895ea3-ebea-4356-af55-e60835fd47f0
redis-dev         talaland-dev   orphaned   72050779-4c43-4c29-90c1-86d0ca066d5f

orphaned is a workload whose app is gone; no-workload is the reverse, an app with nothing in the cluster carrying its id. Only namespaces the cluster returns are compared, because a tenant's apps span several clusters while one kubeconfig context reaches one of them.

Clearing an orphan needs the console or Hamravesh support — there is no force/adopt flag on create and no release endpoint in the API. The command tells you the name is taken and why; it cannot free it.

This shells out to kubectl rather than linking client-go, which keeps the dependency tree small and inherits whatever kubeconfig, context and OIDC exec-plugin credentials already work for you.

Logging in

darkubectl login obtains a Console JWT and stores the (long-lived) refresh token, from which access tokens are minted automatically. There are several ways to provide it — a refresh token is as powerful as a full login:

darkubectl login                              # interactive: email + password + TOTP (2FA)
darkubectl login --refresh-token <token>      # store an existing refresh token (no 2FA)
some-vault get token | darkubectl login --refresh-token-stdin
export DARKUBE_REFRESH_TOKEN=<token>          # refresh token from the environment
export DARKUBE_ACCESS_TOKEN=<jwt>             # a ready access token (used verbatim)

The account API key cannot open a pod terminal or create apps — the exec websocket (wss://…/ws/aexec/) and app creation require the JWT. Force the JWT even when an Api-key is configured by unsetting it: DARKUBE_TOKEN= darkubectl ….

What works, and what does not

The Darkube API has no public documentation; every endpoint here was reverse-engineered against a live account and is noted as confirmed or not in CLAUDE.md. Two things to know up front:

  • There is no partial update — PATCH is not implemented and returns a bodyless 500 on both API versions, so every write is a read-modify-write of the entire app via PUT. darkubectl handles that; the visible consequence is that a console edit racing your command is lost.
  • Delete is asynchronous and orphans releases — see Orphaned releases. get orphans exists because this is the common case, not the edge case. Editing an app in place is safer than delete-and-recreate, which is what strands releases.

Managed services are ordinary apps. A oneclick Redis, Postgres, Grafana or Prometheus is not a separate resource with a separate endpoint — it is an app in the same list, distinguished only by its creation_method (redisnew, postgresqlnew, grafana, …). Probing for a dedicated managed-services API in 2026-08 found nothing but 404s. get apps -o wide shows the type, --type and --managed filter on it, and logs, describe and get env work on them like any other app. The caveat is access: a service provisioned under an organization your account cannot see will not appear at all, no matter what you know its URL to be.

Two things the API will not let you do, whatever the CLI offers:

  • Secret environment values live in a vault, and the app read returns them blank. get env --show-secrets reads them from the vault and set env --secret NAME=VALUE (or --secret --remove NAME) changes them, both through the same routes the console uses. A secret write reads every current value and sends the whole list back, and is verified by reading the vault again. set env without --secret refuses to shadow a secret with a plain variable.
  • You cannot mount an arbitrary config file into a docker-image app. custom_config is the app's Helm chart values and is silently filtered against the chart's schema — for a docker-image app only hpa and container (probes) survive. The config.files mechanism that would render a ConfigMap exists only on marketplace charts like redis and postgres. Use environment variables, or bake the file into the image.

Everything else — listing, describing, logs, exec, terminals, create, edit, deploy tokens, notifications, alerts — is confirmed working.

rollout history, top, registry, deploy, get manifests, get ci-config, the permission commands, the domain checks, get disks, get databases, get services, get suggestions, get balance, get invoices, get members, the API key commands and the reads of build are confirmed working. build start, retry and stop are implemented from the console's own calls but not yet exercised.

Development

go build ./...
go test -race ./...
golangci-lint run ./...

The demo GIF is generated with VHS:

brew install vhs
vhs demo/demo.tape     # runs against your configured tenant → assets/demo.gif

Architecture, the reverse-engineered API/auth details, and contributor conventions live in CLAUDE.md.

Contributing

Issues and pull requests are welcome — especially new endpoint findings, since the API surface is mapped by observation. Keep golangci-lint at zero issues and go test -race ./... green, and the CI will agree with you.

If darkubectl saved you a trip to the console, a ⭐ helps other Darkube users find it.


The Raha Cloud panda at work
Built and maintained by Raha Cloud · GPL-3.0

Releases

Packages

Contributors

Languages