koc is a statically-linked Go replacement for python-openstackclient, built
for the KeyStack cloud. It mirrors the upstream openstack client's
noun → verb → flags syntax so operators fluent in OSC need no retraining, but
ships as one dependency-free binary suitable for air-gapped / FSTEC-regulated
deployment. No Python at runtime.
Status: broad v1 surface. The cross-cutting foundation (auth, TLS, output, microversions) is in place, and the following service command trees are implemented, each with httptest-based unit tests:
- baremetal (ironic) — node lifecycle (create/delete/show/set/unset), provision transitions (manage/provide/deploy/undeploy/rebuild/inspect, with
--wait), maintenance, power, boot device, ports, drivers, conductors- server (nova) — full lifecycle, add/remove volume·floating-ip·security-group, console log/url, plus
compute service,hypervisor list(with color allocation gauges),quota show- compute — flavor, keypair
- identity (keystone) — endpoint, domain, project, user, role (+assignments), service, region, catalog, application credential, token, group
- volume (cinder) — volume (incl.
set --state/--attached/--detached), attachment (reserve → connect → complete), snapshot, backup, type, service- dns (designate) — zone, recordset
- image (glance) — image CRUD,
save, project sharing- network (neutron) — network, subnet, router, port, floating ip, security group (+rule), agent
- loadbalancer (octavia) — load balancer, listener, pool, member, health monitor, l7policy, l7rule, quota, amphora, provider, flavor and flavorprofile (62 of python-octaviaclient's 82 commands), plus failover, stats and a flattened status tree
- placement — resource provider (list/show/delete/trait), allocation, trait
- keyvrm (Keystack Virtual Resource Manager — in-house) — app-config, host-aggregate-config, availability-zone, event, recommendation
In addition to the standard Keystone flow, credentials can be sourced from a standalone Ironic in a Kubernetes namespace (
--creds-from-ns) or an openrc-style secret in Vault (--creds-from-vault).A few operations use raw
ServiceClientrequests where gophercloud v2 lacks a typed verb (server floating-IP actions, quota defaults, image activate/deactivate) — isolated behind small helpers and flagged in code. KeyVRM has no gophercloud package at all and uses the raw request layer end to end.
brew install ftarasenko/tap/kocNo --cask flag is needed — nothing else in the tap shares the name. The binary
is unsigned, so the cask strips the macOS quarantine flag on install; on Apple
Silicon Go already ad-hoc-signs the binary so it runs.
koc ships cobra's completion generator for bash, zsh, fish and powershell:
koc completion zsh > "${fpath[1]}/_koc" # zsh: then restart the shell
source <(koc completion bash) # bash: current shell onlyRelease archives also bundle completions/koc.{bash,zsh,fish}. (The Homebrew
cask installs only the binary — casks have no completion stanza — so brew
users wire it up with the command above.)
Each release publishes static binaries for linux/amd64, linux/arm64,
darwin/amd64, darwin/arm64, windows/amd64, windows/arm64 with a
checksums.txt, attached to the GitHub release.
Alongside them: .rpm and .deb packages for the two linux architectures
(for a local yum/apt mirror on an air-gapped node — these do install the shell
completions), an SPDX SBOM bundle (koc_<ver>_sboms.tar.gz, one
*.spdx.json per artifact inside), a keyless cosign signature over
checksums.txt (checksums.txt.bundle), and a GitHub
build-provenance attestation. Builds are byte-reproducible, so the checksums
can be re-derived independently from the tag.
cosign verify-blob checksums.txt --bundle checksums.txt.bundle \
--certificate-identity-regexp 'https://github.com/ftarasenko/go-openstackclient/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum -c checksums.txt
# What built it: repo, workflow, commit, runner.
gh attestation verify koc_<version>_linux_amd64.tar.gz \
--repo ftarasenko/go-openstackclientThe two identity flags are the part that matters — without them you have checked
only that somebody signed the file. Verify the signature first, then let
sha256sum -c carry that trust to each artifact.
checksums.txt.bundle is a Sigstore bundle: it carries the
signature, the signing certificate and the transparency-log inclusion proof in
one file, so nothing else has to be downloaded alongside it. Reading it needs
cosign v3 or newer (where the format is the default), or cosign v2.6+ with an
explicit --new-bundle-format. Releases up to and including v0.22.0 instead
carry a detached checksums.txt.sig + .pem pair; a v3 client still verifies
those — pass --certificate/--signature in place of --bundle and it falls
back to the legacy path automatically.
Both commands reach the network by default (cosign consults the public Rekor
log, gh calls the GitHub API). On an isolated network, verify the bundle
against a --trusted-root file captured on a connected host — no Rekor call, no
--offline flag. sha256sum -c checksums.txt is fully offline and is what you
gate the install on. See SECURITY.md for that procedure, the
supported-version policy, and how to report a vulnerability.
Fully static, stripped binary:
CGO_ENABLED=0 go build -trimpath \
-ldflags "-s -w -X main.version=$(git describe --tags --always --dirty)" \
-o koc ./cmd/kocor just make build.
Releases are cut by GoReleaser (.goreleaser.yaml): the release workflow builds
the six static binaries, publishes the GitHub release, and pushes the Homebrew
cask to ftarasenko/homebrew-tap. It is triggered by pushing a v* tag
(git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z) — see AGENTS.md
"Cutting a release".
All dependencies are vendored (vendor/ is committed). The build reproduces
offline with no module proxy:
GOFLAGS=-mod=vendor GOPROXY=off CGO_ENABLED=0 go build -trimpath \
-ldflags "-s -w -X main.version=$(git describe --tags --always --dirty)" \
-o koc ./cmd/kockoc baremetal node list -f json
koc baremetal node inspect cmp-039 --wait
koc server list --all-projects --long
koc server create --image ubuntu-cloudimage --flavor 1 --network private myvm
koc server create --image ubuntu-cloudimage --flavor 1 --nic net-id=<uuid> \
--boot-from-volume 20 --boot-volume-type ssd --config-drive myvm
koc server create --image ubuntu-cloudimage --flavor 1 --network private \
--server-group web --hint different_host=<uuid> --hint different_host=<uuid> myvm
koc server add floating ip myvm 192.0.2.5
koc flavor create --ram 512 --disk 1 --vcpus 1 m1.tiny
koc project create demo --domain example
koc volume create --size 1 test-volume
koc volume set stuck-volume --state available --detached
koc volume attachment create test-volume myvm --connect --initiator iqn.2026-08.local:node1
koc network list --long
koc resource provider show <uuid> --allocations -f json
koc hypervisor list --gauge --sort ram --aggregate compute-hp
koc keyvrm recommendation list
koc vault kv copy -r deployments/example/dev deployments/example/stagingkoc builds one authenticated ProviderClient per invocation and reuses it to
derive service clients. Credentials are resolved in this precedence order:
--os-cloud/OS_CLOUD— a named cloud fromclouds.yamlOS_*environment variables- Application credentials (
OS_APPLICATION_CREDENTIAL_ID/_SECRET), honored through either path above.
Project-, domain- and system-scoped tokens are all supported:
| Scope | Flags |
|---|---|
| project | --os-project-name + --os-project-domain-name (or --os-project-id) |
| domain | --os-domain-name |
| system | --os-system-scope all (env OS_SYSTEM_SCOPE) |
A token has exactly one scope, so --os-system-scope conflicts with an
explicit --os-project-name / --os-project-id / --os-domain-name and is
rejected up front. A project inherited from clouds.yaml, the environment or a
Vault openrc is treated as background configuration instead and is overridden,
so koc baremetal driver show ipmi --os-system-scope all works from a shell
that already has a project-scoped openrc sourced. all is the only value
Keystone defines.
Two koc-specific, mutually exclusive flags source credentials outside the normal
OS_* / clouds.yaml flow (both use minimal in-repo REST clients — no client-go
or Vault SDK — to preserve the air-gap invariant):
--creds-from-ns <namespace>reads a metal3 ironic-standalone-operator instance's basic-auth secret from a Kubernetes namespace and builds a standalone Ironic client (baremetal only, no Keystone).--kubeconfig/--kube-contextselect the cluster.--creds-from-vault <path>reads an openrc-style KV v2 secret from Vault and folds itsOS_*into the normal Keystone flow (all services). The path may start with the KV mount (secret_v2/…), a leading/(absolute), or be relative to--vault-kv-prefix. Vault is reached via--vault-*flags /VAULT_*env; when those are absent on a cluster node, the address, namespace, role_id, KV mount/prefix and AppRole secret-id are auto-discovered from the LCMk0s-system/lcm-configConfigMap and thecert-manager/vault-approlesecret. Vault TLS uses the system roots (or--vault-cacert); pass--insecure-vault(envVAULT_SKIP_VERIFY) to skip Vault TLS verification. The global--insecuregoverns only the OpenStack/Keystone TLS, not Vault.
TLS is wired explicitly into the provider so behavior matches OSC:
| Purpose | Flag | Env / clouds.yaml |
|---|---|---|
| Custom CA bundle | --os-cacert |
OS_CACERT / cacert |
| Client cert (mTLS) | --os-cert |
OS_CERT / cert |
| Client key (mTLS) | --os-key |
OS_KEY / key |
| Disable verification | --insecure |
OS_INSECURE / verify |
Hostname verification is on by default and the minimum TLS version is 1.2.
--insecure logs a warning to stderr. clouds.yaml verify: false is honored
unless overridden by an explicit flag/env.
| Purpose | Flag | Env / default |
|---|---|---|
| Whole-exchange HTTP cap | --timeout |
OS_TIMEOUT / 0 (unbounded) |
--timeout <duration> (e.g. --timeout 90s) caps a single HTTP request/response
exchange on every client koc builds — OpenStack, standalone Ironic, Vault
and Kubernetes. It is per request, not per command, so the --wait polling loops
are unaffected.
It defaults to 0, meaning unbounded, on purpose. A whole-exchange cap counts
the body transfer, so any default large enough for koc image save of a
multi-gigabyte image over a slow link is too large to be a useful guard, and any
default small enough to be useful would break those transfers. The failure a
default would be reaching for — an endpoint that accepts the connection and then
never answers — is bounded regardless by a fixed 60s response-header timeout
that always applies and cannot be disabled: it fires on silence without ever
capping a transfer that is making progress. Set --timeout when you want a hard
ceiling on a specific invocation (a CI step, a health probe).
There is no upstream equivalent: keystoneauth has a session timeout, but
python-openstackclient registers no global flag for it.
-f/--format selects the renderer; -c/--column selects columns (repeatable,
case-insensitive, order-preserving); --sort-column sorts list output
(repeatable, for tie-breaks):
table(default) — human-readable ASCII tablejson— array (list) / object (single resource)yamlvalue— plain, tab-separated, no headers, for scriptingcsv— RFC 4180 with a header row
-f value is tab-separated, where openstack uses a single space. Most
values that appear in it contain spaces (status strings, flavor names, fixed-IP
lists), so a space-joined row cannot be split back into its cells; a tab can.
The consequence for scripts: openstack … -f value | cut -d' ' -f2 picks the
wrong field under koc. Use cut -f2 (tab is cut's default), awk '{print $2}', or -c <column> to select one column outright. Cells are otherwise
unquoted in both clients, so a value that itself contains a tab or a newline
breaks the one-cell-per-tab, one-row-per-line contract — prefer -f csv or -f json for fields that may hold arbitrary text (image descriptions, properties,
server metadata). Newlines are passed through on purpose so that koc zone export showfile <id> -f value > zone.txt yields a zonefile zone import create can read
back; control characters and ANSI escapes are stripped regardless.
Table output fits the terminal width: over-long cells wrap across lines when
stdout is a TTY (piped output stays unbounded, matching openstack). --max-width <n> caps the width explicitly and --fit-width forces fitting even when piped.
A very large opaque cell (base64 user_data, cert bundles) is elided in the
table to a <N bytes; …> placeholder; the full value is always available via
-f json/yaml, -f value, or by naming it with -c <column>. server show --user-data prints just the base64-decoded user_data.
-f json renders the same view the table does, not the raw API object. Keys
are the column titles a list command displays ("Project ID", "Service Name"),
and a composite cell arrives pre-formatted — a port's fixed_ips is the string
ip_address='192.0.2.5', subnet_id='…' where openstack -f json gives an array
of objects. It is the right shape for -c-narrowed output and for reading, but
it is not a drop-in for upstream's JSON in a script that indexes into nested
fields. Timestamps are normalised: RFC 3339 when set, null when absent (never
Go's zero date).
--sort-column <col> sorts any list command's rows, in every format. It is a
client-side sort applied before -c narrows the columns, so the sort key does
not have to be one of the displayed columns, and it needs no support from the
API. Numeric columns compare numerically (--sort-column Size puts 9 before
10, not before 100), the sort is stable so repeated --sort-column flags break
ties, and column names are matched case-insensitively.
A few columns are opt-in: they are in neither the default nor the --long
table, and naming one in -c/--column (or --sort-column) materialises it.
server list carries eleven — Created At, Image ID, Flavor ID,
Availability Zone, Host, Task State, Power State, Project ID,
User ID, Security Groups, Properties — mirroring the extras upstream's
server list appends the same way. So a server's age comes from the listing:
koc server list --all-projects -c Name -c "Created At" --sort-column "Created At"without which reading creation time costs one server show per server. The
default and --long tables are unchanged, so nothing that reads either
positionally is affected. server list --help names the full set.
openstack is built on argparse, which accepts any unambiguous prefix of a
long option — so --all works for --all-projects and --fit for
--fit-width. pflag does not abbreviate, so koc normalises the command line
before parsing it and accepts the same abbreviations:
koc volume list --all # → --all-projects
koc server list --fit # → --fit-width
koc image list --form json # → --format jsonThe rules are deliberately conservative, so an abbreviation can never change the
meaning of a command line: only --long forms are considered (short flags and
clusters are untouched), a token that is already a real flag name is never
rewritten, and a prefix matching zero or more than one flag is left alone so
koc reports the usual "unknown flag" rather than guessing. Everything after a
bare -- is positional and is not examined.
--all is therefore not one flag. Where upstream defines it, it is a real flag
with its own meaning — image list --all lists every visibility, flavor list --all every flavor, quota show --all every service — and expansion does not
apply. Where the command has exactly one --all… flag it expands to that one
(volume list --all → --all-projects). Where it has several, as server unset
does with --all-properties and --all-tags, a bare --all is ambiguous and
rejected, which is what openstack does too.
Most OpenStack APIs filter on name by equality, so a partial name matches
nothing and you get an empty table rather than an error. Glance is the strictest:
its query builder accepts only in: and eq: on name and rejects every other
operator, so there is no wildcard to reach for.
koc image list --name distro-9.7-x86_64 # exact — the only thing glance filters on
koc image list --name in:distro-9.7,distro-9.6 # several exact names (glance's in: operator)
koc image list --name-contains distro # substring, case-insensitive (koc-native)
koc image list -f value -c ID -c Name | grep distro # same idea, when you want a real regex--name-contains is filtered client-side, because glance cannot do it; --name
deliberately keeps upstream's exact semantics so koc and openstack return the
same rows for the same command. The two are mutually exclusive.
Nova is the exception worth knowing: server list --name is a server-side
regular expression, so koc server list --name '^web-' works as written.
volume list, network list, port list and subnet list are exact-match with
no --name-contains yet — pipe through grep there.
Each service client sets its own microversion; defaults negotiate the latest the endpoint supports. Override per service:
--os-baremetal-api-version/OS_BAREMETAL_API_VERSION--os-compute-api-version/OS_COMPUTE_API_VERSION--os-volume-api-version/OS_VOLUME_API_VERSION
Ironic emits X-OpenStack-Ironic-API-Version; nova/cinder use the generic
OpenStack-API-Version header (gophercloud sets this from client.Type).
--debug logs each HTTP request/response to stderr with auth tokens redacted.
--timing prints one line per API call to stderr with its method, URL, status
and wall-clock duration — the signal for "why is this slow" without the body
dumps --debug produces. The two combine; passwords in a URL are redacted.
timing: GET https://nova.example/v2.1/servers/detail 200 in 412ms
koc hypervisor list --gauge renders vCPU/RAM/Disk allocation as color bars with
warning/critical thresholds (--warn-pct/--crit-pct), overcommit ratios, an
--aggregate filter and --sort/--reverse. Column profiles auto-fit the
terminal width (detected via golang.org/x/term; override with --width),
--ascii falls back to plain bars, and --color forces auto/always/never.
Allocation figures come from placement (nova dropped these fields at microversion
2.88); nova supplies VMs/type/state/cpu_model/host_ip. --check-actual compares
real CPU/RAM usage scraped from each host's node_exporter (--ne-* flags tune the
scheme/port/suffix/concurrency/timeout). -f json/csv emit the raw numbers via
the output layer.
A koc-specific command group — Vault is not an OpenStack service, and there is no
python-openstackclient equivalent. It authenticates with Vault credentials only
(never Keystone), so it works on a host that has no cloud credentials at all:
koc vault kv list deployments/example/dev
koc vault kv get deployments/example/dev/openrc # prints values in cleartext
koc vault kv copy -r deployments/example/dev deployments/example/staging
koc vault kv export deployments/example/dev --recipient koc-export.pub -o .junit/vault.xml
koc vault kv decrypt .junit/vault.xml -i koc-export.keycopy fills a gap in the Vault CLI itself, which has no kv copy — the
alternative is piping vault kv get -format=json into vault kv put per secret.
Without -r the source must be a single secret; with -r the whole subtree is
mirrored under the destination (nested folders included). --dry-run reports the
exact set of writes without performing any, --skip-existing leaves destination
secrets that already exist untouched, and --src-version pins a single copy to a
specific source version. Secret values are never printed by copy (or by
--debug, which logs only method/path/status) — koc vault kv get is the
explicit way to see them.
The Vault to talk to, and the destination of a copy, is the one described by
the global --vault-* flags (see Authentication), including
LCM cluster auto-discovery. The source is addressed by --src-vault-*
overrides; each one left unset is inherited from the destination, so copying
between two paths of a single Vault needs no extra flags:
| flag | env | default |
|---|---|---|
--src-vault-addr |
VAULT_SRC_ADDR |
the destination's |
--src-vault-namespace |
VAULT_SRC_NAMESPACE |
the destination's |
--src-vault-token |
VAULT_SRC_TOKEN |
the destination's credentials |
--src-vault-role-id / --src-vault-secret-id |
VAULT_SRC_ROLE_ID / VAULT_SRC_SECRET_ID |
the destination's credentials |
--src-vault-kv-mount |
VAULT_SRC_ENGINE |
the destination's |
--src-vault-kv-prefix |
VAULT_SRC_PREFIX |
the destination's |
--src-vault-cacert |
VAULT_SRC_CACERT |
the destination's |
--insecure-src-vault |
VAULT_SRC_SKIP_VERIFY |
the destination's |
Any explicit source credential replaces the destination's credentials as a group,
so an inherited token can never silently win over a source AppRole. The env names
match the variables the KeyStack e2e pipeline already exports for its
vault-helper.py, so a cross-Vault copy needs no flags at all there. The
destination side accepts the symmetric names too: VAULT_ENGINE and
VAULT_PREFIX are fallback aliases of VAULT_KV_MOUNT / VAULT_KV_PREFIX
(the VAULT_KV_* names win when both are set):
export VAULT_ADDR=… VAULT_TOKEN=… # destination
export VAULT_ENGINE=secret_v2 VAULT_PREFIX=deployments/example
export VAULT_SRC_ADDR=… VAULT_SRC_TOKEN=… # source
export VAULT_SRC_ENGINE=secret_v2 VAULT_SRC_PREFIX=deployments/old
koc vault kv copy -r dev devOnly secret data is copied. KV v2 custom_metadata, version history and
delete_version_after are not, so the result is a copy of the current values,
not a replica.
koc vault kv export <path> --recipient <pub.pem> writes the subtree as a JUnit
XML report for artifacts:reports:junit — one test case per secret, so a CI run
still shows which paths exist, which are empty (skipped) and which could not be
read (failure) — with every payload encrypted. There is no plaintext mode:
--recipient is required.
Each secret is sealed with a fresh AES-256-GCM key, itself wrapped to the
recipient's RSA public key with OAEP-SHA256 (standard library only, so vendor/
is unaffected). CI therefore holds only the public key: a leaked runner, artifact
or Pages copy yields nothing readable, and only the private-key holder can
recover the values.
# once, on the operator's machine
openssl genrsa -out koc-export.key 4096
openssl rsa -in koc-export.key -pubout -out koc-export.pub
# in CI, with the public key only (env KOC_EXPORT_RECIPIENT also works)
koc vault kv export deployments/example/dev --recipient koc-export.pub -o .junit/vault.xml
# later, by the key holder — prints Path/Key/Value rows, honours -f/-c
koc vault kv decrypt .junit/vault.xml -i koc-export.key
koc vault kv decrypt .junit/vault.xml -i koc-export.key -f jsonAn existing PKI-issued certificate is accepted as the recipient, so a
deployment's own cert can be used without extracting the key. -o - (or no -o)
writes to stdout, and decrypt - reads from stdin; decrypt needs no Vault
access at all. Payloads look like:
koc-enc:v1:rsa-oaep-sha256:aes-256-gcm
AgCt+YNs5aaiMNcYrgAFe7c0kI+DeSGtIWdjJmQOBxYYNvhf…
Secret paths stay readable — they are what makes the report useful — but each
path is the payload's GCM additional authenticated data, so moving a payload to
another test case makes decryption fail rather than silently succeed under the
wrong name. A secret named ssl_certificates is expanded one test case per key,
so each certificate is separately visible and separately encrypted. decrypt
never writes to a Vault: recovering a secret and re-injecting it stay separate
acts.
Another koc-specific group, for the LCM cluster's Garage — the S3 store that
holds GitLab's object storage and the MariaDB dumps the backup-db scheduled
pipeline uploads. Like koc vault kv it never authenticates against Keystone:
S3 credentials alone are used, so it works on a host with no cloud credentials.
koc s3 bucket list
koc s3 object list db-backups
koc s3 object show db-backups/<key> # HEAD only, no transfer
koc s3 download db-backups/<key> ./dump.mbs.gz.enc
koc s3 download db-backups/<key>.sha256 - # "-" streams to stdout, so it pipes
koc s3 upload ./dump.mbs.gz.enc db-backups/bucket list is scoped to the access key, not to the store: Garage answers
with the buckets that key is granted, so a key made for one bucket lists exactly
that one.
Credentials come from flags, from the environment, or from a Kubernetes Secret:
| flag | env | default |
|---|---|---|
--s3-endpoint |
AWS_ENDPOINT_URL, S3_ENDPOINT, s3_host |
— |
--s3-access-key |
AWS_ACCESS_KEY_ID, S3_ACCESS_KEY, s3_access_key |
— |
--s3-secret-key |
AWS_SECRET_ACCESS_KEY, S3_SECRET_KEY, s3_secret_key |
— |
--s3-region |
AWS_REGION, AWS_DEFAULT_REGION, S3_REGION, s3_region |
garage |
--s3-cacert |
AWS_CA_BUNDLE, S3_CACERT |
system roots |
--s3-creds-from-ns |
KOC_S3_CREDS_FROM_NS |
— |
--insecure-s3 |
S3_SKIP_VERIFY |
off (the global --insecure also applies) |
The three env families are deliberate: AWS_* so an existing aws/boto
environment works unchanged, S3_* as the neutral spelling, and the lower-case
s3_host / s3_access_key / s3_secret_key / s3_region that the KeyStack
installer writes as GitLab group CI/CD variables — so a pipeline job can call
koc s3 with no flags at all. Prefer the environment to --s3-secret-key: a
flag value is visible in the process list and in shell history.
--s3-creds-from-ns <namespace>[/<secret>][:<key>] reads them from a Kubernetes
Secret over the same read-only API --creds-from-ns uses (--kubeconfig /
--kube-context apply). The secret name defaults to gitlab-object-storage, so
on an LCM cluster node GitLab's own key needs nothing else:
koc s3 --s3-creds-from-ns lcm-gitlab bucket listIt handles the shapes S3 credentials actually appear in: one key per value
(access_key/secret_key, AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, …), and
a Secret holding a whole connection config as a single value — GitLab's config
key, an rclone.conf or an .s3cfg — from which the endpoint, region and
path-style setting are read as well. A Secret carrying only a key pair
supplies only that, and the endpoint still comes from --s3-endpoint or the
environment. Note that on a KeyStack LCM cluster only
GitLab's key is a Kubernetes Secret; the db-backup key is held by Garage itself
and by the masked GitLab group variables, so export it from one of those.
Addressing is path-style by default (<endpoint>/<bucket>/<key>), matching
the --host-bucket setting the backup pipeline gives s3cmd; --no-path-style
switches to <bucket>.<endpoint>, which needs a wildcard DNS record.
Uploads are a single signed PUT — there is no multipart support, so the
server's own single-part ceiling (5 GiB on Garage and on AWS) applies. Downloads
never overwrite an existing file without --force, and a transfer that fails
partway removes the partial file rather than leaving a truncated dump that looks
complete.
cmd/koc/main.go cobra root entrypoint, version
internal/auth/ clouds.Parse + provider + TLS + per-service clients
+ --creds-from-ns / --creds-from-vault sources
internal/kube/ minimal read-only k8s REST client (no client-go)
internal/vault/ minimal Vault REST client (AppRole/token + KV v2)
internal/s3/ minimal S3 REST client (SigV4, no aws-sdk/minio-go)
internal/cli/vault/ "koc vault kv" list/get/copy/export/decrypt, no Keystone auth
internal/cli/s3/ "koc s3" bucket/object/download/upload, no Keystone auth
internal/output/ -f/-c formatter (table/json/yaml/value/csv)
internal/cli/ root command wiring
internal/cli/resolve/ cross-service name→ID resolution
internal/cli/baremetal/ baremetal (ironic) command group
internal/cli/keyvrm/ KeyVRM in-house catalog service (raw request layer)
make test # go test ./...
make race # go test -race ./... (needs cgo; shipped binaries stay CGO_ENABLED=0)
make vet # go vet ./...
make lint # golangci-lint run ./...
make crossbuild # compile all six release targets (build-only, offline)
make completions # generate completions/koc.{bash,zsh,fish}
make size # print the built binary size
make tidy # go mod tidy && go mod vendorA multi-perspective correctness review hardened the initial surface (auth domain
scoping, nova floating-IP microversion, --wait semantics, --limit, metadata
unset, cross-service resolution, debug redaction, and more). A few lower-risk
items are deferred and worth noting:
- Name-not-found resolution is silent. A name→ID resolver that finds no match
passes the reference through as a literal ID, so a mistyped
--domain/--projectfilter yields an empty result rather than an error, andkoc network delete typo-namereports neutron's error for a malformed UUID rather than koc's "no such network". Theserverpackage is the exception and does error properly (no server found with name "…"); the other resolvers should be brought in line with it. UUIDs always short-circuit resolution. baremetal node setuses JSON-patchreplacefor scalar attributes; on some ironic buildsaddis needed for a previously-absent attribute.role assignment listwith both--projectand--domainsends both scope qualifiers; keystone treats them as mutually exclusive.--debugelides large/binary bodies (image up/downloads) and redacts tokens and credential fields; it does not pretty-print JSON.-f jsonmirrors the rendered table, not the API object — see "Output formats" above before consuming it from a script.baremetal port --namecan be set but not shown.port create/port setsend ironic'snameattribute (API 1.88, OpenStack 2024.1), but gophercloud's port result type has no field for it and no catch-all, soport show/port listcannot render it back. Reading it needs a koc-owned DTO for the port reads.- Cinder's pool capacity can mix raw and replicated figures.
koc volume backend pool listreports what cinder's scheduler was handed: onetotal_capacity_gband onefree_capacity_gbper pool. A driver that computes the total from the cluster's raw capacity while reporting free space after replication yields a pair that cannot both be right, and koc does not try to reconcile them — it has no way to know a given driver's replication factor.--longadds every remaining capability key the driver reported, andkoc volume backend capability show <host>shows the driver's own set, which is where a raw figure can be compared against cinder's normalised one. Gate on the driver's key when the backend publishes one. volume list --hostfilters client-side. Cinder's server-sidehostfilter is admin-only and absent from the defaultresource_filters.jsonallow-list, so a stock deployment ignores it silently — an empty table that reads as "no volumes on that host". koc therefore reads the page and matchesos-vol-host-attr:hostitself, which also means--limitcaps what survives the filter rather than what cinder returned.server delete/start/stop --all-projectsis accepted, not required. koc always resolves a server name across projects, so the flag upstream needs for that is a no-op here rather than a gate; it exists so anopenstackinvocation carrying it does not fail on an unknown flag.port list --all-projectsis presentational. Neutron has no cross-project switch: it scopes a listing to the caller's project only when the token is not an admin one, so an admin already sees every project's ports and there is noall_tenantsparameter to send (a cloud running neutron's filter-validation extension would reject one). Upstream OSC therefore givesport listno such flag. koc accepts it anyway — so anopenstack-shaped invocation carrying it does not fail on an unknown flag — and makes it useful by inserting theProject IDcolumn, without which the rows of a multi-project result cannot be told apart. It also honoursALL_PROJECTSand is refused together with--project, which narrows to one project.
Command and flag names should be verified against the KeyStack command
reference at https://docs.keystack.ru/. That site returns HTTP 403; the
documentation source is mirrored locally (Sphinx/reST) and is the working
reference. The KeyStack-specific extensions (compute service set --admin-state, server dynamic server groups / --availability-zone /
server list created/deleted filters, server evacuate --preserve-ephemeral)
are verified against it. Other surfaces (e.g. baremetal node list) still
follow upstream OSC semantics and remain unverified; where KeyStack later
proves to differ, KeyStack wins.
- Go (see
go.mod), gophercloud v2 (github.com/gophercloud/gophercloud/v2) - cobra + pflag
- Vendored dependencies for offline/air-gapped builds