Merge pull request 'Rename vctl to vcm' (#2) from benvin/vcm-rename into main
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
+3
-3
@@ -1,6 +1,6 @@
|
||||
# cross-compiled release artifacts at repo root (e.g. vctl-linux-amd64).
|
||||
# NB: do NOT ignore /vctl or /vctx bare — those are the tool source directories.
|
||||
/vctl-*
|
||||
# cross-compiled release artifacts at repo root (e.g. vcm-linux-amd64).
|
||||
# NB: do NOT ignore /vcm or /vctx bare — those are the tool source directories.
|
||||
/vcm-*
|
||||
/vctx-*
|
||||
sha256sums.txt
|
||||
# build output: binaries, completions, RPM
|
||||
|
||||
@@ -28,7 +28,7 @@ steps:
|
||||
# for the shell instead of substituting them (as pipeline vars) at parse
|
||||
# time. ${CI_COMMIT_TAG} is a real Woodpecker var and stays single-$.
|
||||
- |
|
||||
for entry in "vctl:./vctl" "vctx:./vctx"; do
|
||||
for entry in "vcm:./vcm" "vctx:./vctx"; do
|
||||
name="$${entry%%:*}"; pkg="$${entry##*:}"
|
||||
for osarch in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do
|
||||
os="$${osarch%/*}"; arch="$${osarch#*/}"
|
||||
@@ -135,7 +135,7 @@ steps:
|
||||
# root; the package step writes the RPM to dist/. Generate a checksums
|
||||
# manifest over everything we attach so downloads can be verified.
|
||||
RPM=$$(ls dist/*.rpm 2>/dev/null | head -1)
|
||||
ASSETS="vctl-linux-amd64 vctl-linux-arm64 vctl-darwin-amd64 vctl-darwin-arm64 vctx-linux-amd64 vctx-linux-arm64 vctx-darwin-amd64 vctx-darwin-arm64"
|
||||
ASSETS="vcm-linux-amd64 vcm-linux-arm64 vcm-darwin-amd64 vcm-darwin-arm64 vctx-linux-amd64 vctx-linux-arm64 vctx-darwin-amd64 vctx-darwin-arm64"
|
||||
[ -n "$$RPM" ] && ASSETS="$$ASSETS $$RPM"
|
||||
sha256sum $$ASSETS > sha256sums.txt
|
||||
tea releases assets create "${CI_COMMIT_TAG}" $$ASSETS sha256sums.txt \
|
||||
|
||||
@@ -5,24 +5,24 @@
|
||||
`vault-tools` is a Go monorepo of small CLIs for managing Vault tokens across
|
||||
multiple vault instances ("contexts"):
|
||||
|
||||
- **`vctl`** — logs in to / renews Vault tokens per context (`login`, `renew`,
|
||||
- **`vcm`** — logs in to / renews Vault tokens per context (`login`, `renew`,
|
||||
`list`), with `--all`, `--method` and `--user` flags. Caches each token under
|
||||
`~/.cache/vault/<context>` as JSON (token + accessor + policies + TTL/expiry).
|
||||
`~/.cache/vcm/<context>` as JSON (token + accessor + policies + TTL/expiry).
|
||||
- **`vctx`** — thin wrapper around the real `vault` CLI:
|
||||
`vctx --context <ctx> <vault args...>` resolves the context, sets
|
||||
`VAULT_ADDR`/`VAULT_TOKEN`/`VAULT_NAMESPACE`, and execs `vault`.
|
||||
|
||||
Both tools share the `shared/` package (config parsing, token cache, Vault HTTP
|
||||
client) and read the SAME config file (`~/.config/vault/vctl.yaml` or
|
||||
`config.yaml`) and token cache so one `~/.config/vault/` configures everything.
|
||||
client) and read the SAME config file (`~/.config/vcm/config.yaml`)
|
||||
and token cache so one `~/.config/vcm/` configures everything.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
shared/ # config.go, cache.go, vault.go (+ *_test.go)
|
||||
vctl/main.go # vctl CLI (module main package)
|
||||
vcm/main.go # vcm CLI (module main package)
|
||||
vctx/main.go # vctx CLI (module main package)
|
||||
docs/vctl.md # per-command docs
|
||||
docs/vcm.md # per-command docs
|
||||
docs/vctx.md
|
||||
go.mod # module git.unkin.net/unkin/vault-tools
|
||||
Makefile # build / test / lint / completions / rpm / version-bump
|
||||
@@ -39,9 +39,9 @@ mains to one file).
|
||||
## Build / test
|
||||
|
||||
```bash
|
||||
make build # -> dist/vctl, dist/vctx
|
||||
make build # -> dist/vcm, dist/vctx
|
||||
make test # go test -race ./...
|
||||
make completions # -> dist/completions/{vctl,vctx}.{bash,fish}, _{vctl,vctx}
|
||||
make completions # -> dist/completions/{vcm,vctx}.{bash,fish}, _{vcm,vctx}
|
||||
make rpm # build + nfpm RPM (bundles completions)
|
||||
go build ./... && go test ./...
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vault-tools: a Go monorepo of Vault token CLIs (vctl + vctx) sharing shared/.
|
||||
# vault-tools: a Go monorepo of Vault token CLIs (vcm + vctx) sharing shared/.
|
||||
# Each tool is a separate main package under its own subfolder.
|
||||
BINARIES := vctl vctx
|
||||
BINARIES := vcm vctx
|
||||
DIST := dist
|
||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||
GOFLAGS := -ldflags="-s -w -X main.version=$(VERSION)"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# vault-tools
|
||||
|
||||
A Go monorepo of small CLI tools for working with multiple Vault instances
|
||||
("contexts"). Both tools share a single config file (`~/.config/vault/`) and
|
||||
token cache (`~/.cache/vault/`), so once a context is configured every tool
|
||||
("contexts"). Both tools share a single config file (`~/.config/vcm/`) and
|
||||
token cache (`~/.cache/vcm/`), so once a context is configured every tool
|
||||
knows about it.
|
||||
|
||||
| Tool | Purpose |
|
||||
| -------------------- | ----------------------------------------------------------------------- |
|
||||
| [`vctl`](docs/vctl.md) | Log in to / renew Vault tokens per context and cache them on disk. |
|
||||
| [`vcm`](docs/vcm.md) | Log in to / renew Vault tokens per context and cache them on disk. |
|
||||
| [`vctx`](docs/vctx.md) | Run the real `vault` CLI against a named context (sets `VAULT_ADDR`/`VAULT_TOKEN`/`VAULT_NAMESPACE`, then execs `vault`). |
|
||||
|
||||
See the per-command docs in [`docs/`](docs/) for full details.
|
||||
@@ -16,8 +16,8 @@ See the per-command docs in [`docs/`](docs/) for full details.
|
||||
|
||||
```bash
|
||||
# 1. Configure your vaults
|
||||
mkdir -p ~/.config/vault
|
||||
cat > ~/.config/vault/vctl.yaml <<'YAML'
|
||||
mkdir -p ~/.config/vcm
|
||||
cat > ~/.config/vcm/config.yaml <<'YAML'
|
||||
defaults:
|
||||
method: ldap
|
||||
user: ben
|
||||
@@ -29,32 +29,31 @@ contexts:
|
||||
namespace: staging
|
||||
YAML
|
||||
|
||||
# 2. Log in (prompts for password), caches a token under ~/.cache/vault/
|
||||
vctl login sydney
|
||||
vctl login --all # or log in to every context at once
|
||||
# 2. Log in (prompts for password), caches a token under ~/.cache/vcm/
|
||||
vcm login sydney
|
||||
vcm login --all # or log in to every context at once
|
||||
|
||||
# 3. Use the vault CLI against a context
|
||||
vctx --context sydney kv put kv/foo/bar secret=baz
|
||||
vctx --context staging/sydney kv list kv/
|
||||
|
||||
# 4. Keep tokens fresh
|
||||
vctl renew --all
|
||||
vcm renew --all
|
||||
|
||||
# See what's configured and which tokens are still valid
|
||||
vctl list
|
||||
vcm list
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Both tools read the first of `~/.config/vault/vctl.yaml` or
|
||||
`~/.config/vault/config.yaml` that exists (`$XDG_CONFIG_HOME` honoured). The
|
||||
Both tools read `~/.config/vcm/config.yaml` (`$XDG_CONFIG_HOME` honoured). The
|
||||
file maps context names to a vault address plus optional per-context overrides
|
||||
(`method`, `user`, `namespace`, `path`) with file-level `defaults`. Context
|
||||
names may contain slashes, which nest on disk in the token cache. See
|
||||
[docs/vctl.md](docs/vctl.md#configuration) for the full schema and resolution
|
||||
[docs/vcm.md](docs/vcm.md#configuration) for the full schema and resolution
|
||||
rules.
|
||||
|
||||
Tokens are cached under `~/.cache/vault/<context>` (`$XDG_CACHE_HOME` honoured)
|
||||
Tokens are cached under `~/.cache/vcm/<context>` (`$XDG_CACHE_HOME` honoured)
|
||||
as JSON with restrictive permissions (files `0600`, dirs `0700`), storing the
|
||||
token plus its accessor, policies, TTL/expiry and renewable flag.
|
||||
|
||||
@@ -62,7 +61,7 @@ token plus its accessor, policies, TTL/expiry and renewable flag.
|
||||
|
||||
```
|
||||
shared/ # config parsing, token cache, Vault HTTP client (shared lib + tests)
|
||||
vctl/ # vctl CLI (main package)
|
||||
vcm/ # vcm CLI (main package)
|
||||
vctx/ # vctx CLI (main package)
|
||||
docs/ # per-command documentation
|
||||
packaging/nfpm.yaml# nfpm spec (envsubst-templated) for the RPM (both binaries + completions)
|
||||
@@ -74,7 +73,7 @@ Makefile # build / test / completions / rpm / version-bump targets
|
||||
## Building
|
||||
|
||||
```bash
|
||||
make build # build vctl + vctx into dist/
|
||||
make build # build vcm + vctx into dist/
|
||||
make test # go test -race ./...
|
||||
make completions # generate bash/zsh/fish completions into dist/completions/
|
||||
make rpm # build + package an RPM (needs nfpm)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package vaulttools is the module root for the vault-tools monorepo. The
|
||||
// shipped commands live in their own subfolders (vctl/, vctx/) and share the
|
||||
// shipped commands live in their own subfolders (vcm/, vctx/) and share the
|
||||
// shared/ library; this file exists so the module root is itself a buildable Go
|
||||
// package (some tooling, e.g. the pre-commit go-vet hook, runs `go list` here).
|
||||
package vaulttools
|
||||
|
||||
+24
-28
@@ -1,19 +1,19 @@
|
||||
# vctl
|
||||
# vcm
|
||||
|
||||
`vctl` manages Vault tokens for multiple vault instances ("contexts"). It logs
|
||||
`vcm` manages Vault tokens for multiple vault instances ("contexts"). It logs
|
||||
in to (or renews tokens for) one or all configured contexts and caches each
|
||||
resulting token under `~/.cache/vault/<context>` for use by
|
||||
resulting token under `~/.cache/vcm/<context>` for use by
|
||||
[`vctx`](vctx.md) and other tooling.
|
||||
|
||||
## Synopsis
|
||||
|
||||
```
|
||||
vctl login <context> # log in to one context
|
||||
vctl login --all # log in to every configured context
|
||||
vctl renew <context> # renew one context's cached token
|
||||
vctl renew --all # renew every context that has a cached token
|
||||
vctl list # show contexts + cached-token status
|
||||
vctl version
|
||||
vcm login <context> # log in to one context
|
||||
vcm login --all # log in to every configured context
|
||||
vcm renew <context> # renew one context's cached token
|
||||
vcm renew --all # renew every context that has a cached token
|
||||
vcm list # show contexts + cached-token status
|
||||
vcm version
|
||||
```
|
||||
|
||||
## Flags
|
||||
@@ -30,15 +30,11 @@ A context name or `--all` is required (but not both).
|
||||
|
||||
## Configuration
|
||||
|
||||
`vctl` reads the first of these files that exists:
|
||||
|
||||
1. `~/.config/vault/vctl.yaml`
|
||||
2. `~/.config/vault/config.yaml`
|
||||
|
||||
(`$XDG_CONFIG_HOME` is honoured in place of `~/.config`.)
|
||||
`vcm` reads `~/.config/vcm/config.yaml` (`$XDG_CONFIG_HOME` is honoured in
|
||||
place of `~/.config`).
|
||||
|
||||
```yaml
|
||||
# ~/.config/vault/vctl.yaml
|
||||
# ~/.config/vcm/config.yaml
|
||||
defaults:
|
||||
method: ldap # applied to any context that doesn't set its own
|
||||
user: ben
|
||||
@@ -75,16 +71,16 @@ the auth path follows the new method.
|
||||
|
||||
## Auth methods
|
||||
|
||||
- **Password methods** (`ldap`, `userpass`, `okta`, `radius`): `vctl` prompts
|
||||
- **Password methods** (`ldap`, `userpass`, `okta`, `radius`): `vcm` prompts
|
||||
for a password (no echo) and POSTs to `auth/<path>/login/<user>`.
|
||||
- **`token`**: `vctl` prompts for a raw Vault token (no echo), verifies it with
|
||||
- **`token`**: `vcm` prompts for a raw Vault token (no echo), verifies it with
|
||||
`auth/token/lookup-self`, and caches its details.
|
||||
|
||||
## Token cache
|
||||
|
||||
Tokens are written to `~/.cache/vault/<context>` (honouring `$XDG_CACHE_HOME`)
|
||||
Tokens are written to `~/.cache/vcm/<context>` (honouring `$XDG_CACHE_HOME`)
|
||||
as JSON. Slash contexts nest: `staging/sydney` →
|
||||
`~/.cache/vault/staging/sydney`. Parent directories are created `0700` and token
|
||||
`~/.cache/vcm/staging/sydney`. Parent directories are created `0700` and token
|
||||
files are written `0600`.
|
||||
|
||||
Each file stores enough to inspect and later revoke the token:
|
||||
@@ -111,29 +107,29 @@ The `accessor` lets you revoke the token later without exposing the secret
|
||||
|
||||
```bash
|
||||
# Log in to one context (prompts for password)
|
||||
vctl login sydney
|
||||
vcm login sydney
|
||||
|
||||
# Log in everywhere at once
|
||||
vctl login --all
|
||||
vcm login --all
|
||||
|
||||
# Log in to a context overriding the method + user
|
||||
vctl login sydney --method okta --user someone.else
|
||||
vcm login sydney --method okta --user someone.else
|
||||
|
||||
# Renew a slash-named context
|
||||
vctl renew staging/sydney
|
||||
vcm renew staging/sydney
|
||||
|
||||
# Renew everything that currently has a cached token
|
||||
vctl renew --all
|
||||
vcm renew --all
|
||||
|
||||
# See what is configured and which tokens are still valid
|
||||
vctl list
|
||||
vcm list
|
||||
```
|
||||
|
||||
## Shell completion
|
||||
|
||||
```bash
|
||||
vctl completion bash > /etc/bash_completion.d/vctl
|
||||
vctl completion zsh > ~/.zsh/completions/_vctl
|
||||
vcm completion bash > /etc/bash_completion.d/vcm
|
||||
vcm completion zsh > ~/.zsh/completions/_vcm
|
||||
```
|
||||
|
||||
Context names complete dynamically from the config file (the RPM installs these
|
||||
+5
-6
@@ -1,7 +1,7 @@
|
||||
# vctx
|
||||
|
||||
`vctx` is a thin, context-aware wrapper around the real `vault` CLI. It resolves
|
||||
a context (using the same config file and token cache as [`vctl`](vctl.md)),
|
||||
a context (using the same config file and token cache as [`vcm`](vcm.md)),
|
||||
sets `VAULT_ADDR`, `VAULT_TOKEN` and `VAULT_NAMESPACE` for that single
|
||||
invocation only, and execs `vault` with the remaining arguments.
|
||||
|
||||
@@ -18,10 +18,9 @@ vctx version
|
||||
vctx --context sydney kv put kv/foo/bar secret=baz
|
||||
```
|
||||
|
||||
1. Resolves the `sydney` context from `~/.config/vault/vctl.yaml` (or
|
||||
`config.yaml`) — the exact file `vctl` uses.
|
||||
2. Loads the cached token from `~/.cache/vault/sydney` (errors with a hint to
|
||||
run `vctl login sydney` if none exists).
|
||||
1. Resolves the `sydney` context from `~/.config/vcm/config.yaml` — the exact file `vcm` uses.
|
||||
2. Loads the cached token from `~/.cache/vcm/sydney` (errors with a hint to
|
||||
run `vcm login sydney` if none exists).
|
||||
3. Sets, for this process only:
|
||||
- `VAULT_ADDR` = the context's address
|
||||
- `VAULT_TOKEN` = the cached token
|
||||
@@ -68,7 +67,7 @@ vctx --context sydney token lookup
|
||||
## Requirements
|
||||
|
||||
- The `vault` CLI must be installed and on `PATH`.
|
||||
- A token must already be cached for the context (`vctl login <context>`).
|
||||
- A token must already be cached for the context (`vcm login <context>`).
|
||||
|
||||
## Shell completion
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Example vault-tools config. Copy to ~/.config/vault/vctl.yaml (or config.yaml)
|
||||
# and edit. Read by both vctl and vctx.
|
||||
# Example vault-tools config. Copy to ~/.config/vcm/config.yaml
|
||||
# and edit. Read by both vcm and vctx.
|
||||
|
||||
# File-level defaults applied to any context that doesn't set its own value.
|
||||
defaults:
|
||||
@@ -12,7 +12,7 @@ contexts:
|
||||
sydney:
|
||||
address: https://vault.syd1.au.unkin.net
|
||||
|
||||
# Slash-named context nests on disk as ~/.cache/vault/staging/sydney.
|
||||
# Slash-named context nests on disk as ~/.cache/vcm/staging/sydney.
|
||||
staging/sydney:
|
||||
address: https://vault-staging.syd1.au.unkin.net
|
||||
namespace: staging
|
||||
+9
-9
@@ -1,5 +1,5 @@
|
||||
---
|
||||
# nfpm config for building the vault-tools RPM (vctl + vctx).
|
||||
# nfpm config for building the vault-tools RPM (vcm + vctx).
|
||||
# Rendered through envsubst (see scripts/build-rpm.sh) then fed to `nfpm pkg`.
|
||||
|
||||
name: ${PACKAGE_NAME}
|
||||
@@ -24,8 +24,8 @@ provides:
|
||||
|
||||
contents:
|
||||
# The CLI binaries.
|
||||
- src: dist/vctl
|
||||
dst: /usr/bin/vctl
|
||||
- src: dist/vcm
|
||||
dst: /usr/bin/vcm
|
||||
file_info:
|
||||
mode: 0755
|
||||
owner: root
|
||||
@@ -38,16 +38,16 @@ contents:
|
||||
group: root
|
||||
|
||||
# Shell completions (generated by scripts/build-rpm.sh before packaging).
|
||||
- src: dist/completions/vctl.bash
|
||||
dst: /usr/share/bash-completion/completions/vctl
|
||||
- src: dist/completions/vcm.bash
|
||||
dst: /usr/share/bash-completion/completions/vcm
|
||||
file_info:
|
||||
mode: 0644
|
||||
- src: dist/completions/_vctl
|
||||
dst: /usr/share/zsh/site-functions/_vctl
|
||||
- src: dist/completions/_vcm
|
||||
dst: /usr/share/zsh/site-functions/_vcm
|
||||
file_info:
|
||||
mode: 0644
|
||||
- src: dist/completions/vctl.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/vctl.fish
|
||||
- src: dist/completions/vcm.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/vcm.fish
|
||||
file_info:
|
||||
mode: 0644
|
||||
- src: dist/completions/vctx.bash
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Package the (already built) vctl and vctx binaries into an RPM with nfpm,
|
||||
# Package the (already built) vcm and vctx binaries into an RPM with nfpm,
|
||||
# bundling generated bash/zsh/fish shell completions.
|
||||
# Usage: scripts/build-rpm.sh [version] (version defaults to $CI_COMMIT_TAG)
|
||||
#
|
||||
@@ -12,7 +12,7 @@ cd "${ROOT_DIR}"
|
||||
VERSION="${1:-${CI_COMMIT_TAG:-0.0.0-dev}}"
|
||||
VERSION="${VERSION#v}" # strip a leading v
|
||||
PACKAGE="vault-tools"
|
||||
BINARIES=(vctl vctx)
|
||||
BINARIES=(vcm vctx)
|
||||
DIST="dist"
|
||||
|
||||
for b in "${BINARIES[@]}"; do
|
||||
@@ -37,7 +37,7 @@ export PACKAGE_VERSION="${VERSION}"
|
||||
export PACKAGE_RELEASE="1"
|
||||
export PACKAGE_ARCH="amd64"
|
||||
export PACKAGE_PLATFORM="linux"
|
||||
export PACKAGE_DESCRIPTION="CLI tools for managing Vault tokens across multiple vault instances: vctl (per-context login/renew) and vctx (context-aware vault CLI wrapper)"
|
||||
export PACKAGE_DESCRIPTION="CLI tools for managing Vault tokens across multiple vault instances: vcm (per-context login/renew) and vctx (context-aware vault CLI wrapper)"
|
||||
export PACKAGE_MAINTAINER="Ben Vincent <ben@unkin.net>"
|
||||
export PACKAGE_HOMEPAGE="https://git.unkin.net/unkin/vault-tools"
|
||||
export PACKAGE_LICENSE="MIT"
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ type Token struct {
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
// CacheDir returns the XDG_CACHE_HOME/vault directory that holds token files.
|
||||
// CacheDir returns the XDG_CACHE_HOME/vcm directory that holds token files.
|
||||
func CacheDir() string {
|
||||
base := os.Getenv("XDG_CACHE_HOME")
|
||||
if base == "" {
|
||||
@@ -122,7 +122,7 @@ func LoadToken(context string) (*Token, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("no cached token for context %q (run 'vctl login %s'): %w", context, context, os.ErrNotExist)
|
||||
return nil, fmt.Errorf("no cached token for context %q (run 'vcm login %s'): %w", context, context, os.ErrNotExist)
|
||||
}
|
||||
return nil, fmt.Errorf("reading token %s: %w", path, err)
|
||||
}
|
||||
|
||||
+24
-39
@@ -1,7 +1,7 @@
|
||||
// Package shared holds the plumbing common to the vault-tools CLIs (vctl and
|
||||
// Package shared holds the plumbing common to the vault-tools CLIs (vcm and
|
||||
// vctx): config-file parsing, the on-disk token cache, and a small Vault HTTP
|
||||
// API client. Both tools read the SAME config file and token cache so a single
|
||||
// ~/.config/vault/ configures every tool in the family.
|
||||
// ~/.config/vcm/ configures every tool in the family.
|
||||
package shared
|
||||
|
||||
import (
|
||||
@@ -19,12 +19,10 @@ const (
|
||||
DefaultMethod = "ldap"
|
||||
|
||||
// appDir is the per-user config/cache subdirectory both tools live under.
|
||||
appDir = "vault"
|
||||
)
|
||||
appDir = "vcm"
|
||||
|
||||
// configFileNames are the accepted config file basenames, tried in order. The
|
||||
// first one that exists wins.
|
||||
var configFileNames = []string{"vctl.yaml", "config.yaml"}
|
||||
configFileName = "config.yaml"
|
||||
)
|
||||
|
||||
// Context is a single vault instance the tools can target. Every field except
|
||||
// Address is optional and, when empty, falls back to the file-level Defaults
|
||||
@@ -72,7 +70,7 @@ type ResolvedContext struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// ConfigDir returns the XDG_CONFIG_HOME/vault directory.
|
||||
// ConfigDir returns the XDG_CONFIG_HOME/vcm directory.
|
||||
func ConfigDir() string {
|
||||
base := os.Getenv("XDG_CONFIG_HOME")
|
||||
if base == "" {
|
||||
@@ -82,45 +80,32 @@ func ConfigDir() string {
|
||||
return filepath.Join(base, appDir)
|
||||
}
|
||||
|
||||
// ConfigPath returns the path of the first existing config file, or the path
|
||||
// the file would take (the first candidate) when none exists yet.
|
||||
// ConfigPath returns the path of the config file, whether or not it exists.
|
||||
func ConfigPath() string {
|
||||
dir := ConfigDir()
|
||||
for _, name := range configFileNames {
|
||||
p := filepath.Join(dir, name)
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return filepath.Join(dir, configFileNames[0])
|
||||
return filepath.Join(ConfigDir(), configFileName)
|
||||
}
|
||||
|
||||
// Load reads and parses the first config file found in ConfigDir. A missing
|
||||
// config file is not an error: an empty Config is returned so callers can give
|
||||
// a helpful "no contexts configured" message.
|
||||
// Load reads and parses the config file. A missing config file is not an
|
||||
// error: an empty Config is returned so callers can give a helpful "no
|
||||
// contexts configured" message.
|
||||
func Load() (*Config, error) {
|
||||
cfg := &Config{Contexts: map[string]Context{}}
|
||||
|
||||
dir := ConfigDir()
|
||||
for _, name := range configFileNames {
|
||||
p := filepath.Join(dir, name)
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return cfg, fmt.Errorf("reading config %s: %w", p, err)
|
||||
p := ConfigPath()
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return cfg, nil
|
||||
}
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
return cfg, fmt.Errorf("parsing config %s: %w", p, err)
|
||||
}
|
||||
if cfg.Contexts == nil {
|
||||
cfg.Contexts = map[string]Context{}
|
||||
}
|
||||
cfg.path = p
|
||||
return cfg, nil
|
||||
return cfg, fmt.Errorf("reading config %s: %w", p, err)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
return cfg, fmt.Errorf("parsing config %s: %w", p, err)
|
||||
}
|
||||
if cfg.Contexts == nil {
|
||||
cfg.Contexts = map[string]Context{}
|
||||
}
|
||||
cfg.path = p
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
|
||||
+7
-18
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// withConfigDir points XDG_CONFIG_HOME at a temp dir and writes the given
|
||||
// config file into <tmp>/vault/<name>, returning the temp base.
|
||||
// config file into <tmp>/vcm/<name>, returning the temp base.
|
||||
func withConfigDir(t *testing.T, name, content string) string {
|
||||
t.Helper()
|
||||
base := t.TempDir()
|
||||
@@ -42,7 +42,7 @@ contexts:
|
||||
`
|
||||
|
||||
func TestLoadAndContextNames(t *testing.T) {
|
||||
withConfigDir(t, "vctl.yaml", sampleConfig)
|
||||
withConfigDir(t, "config.yaml", sampleConfig)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
@@ -63,18 +63,7 @@ func TestLoadAndContextNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPathPrefersVctlYaml(t *testing.T) {
|
||||
base := withConfigDir(t, "vctl.yaml", sampleConfig)
|
||||
// also write a config.yaml; vctl.yaml should win
|
||||
if err := os.WriteFile(filepath.Join(base, appDir, "config.yaml"), []byte(sampleConfig), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := ConfigPath(), filepath.Join(base, appDir, "vctl.yaml"); got != want {
|
||||
t.Errorf("ConfigPath = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPathFallsBackToConfigYaml(t *testing.T) {
|
||||
func TestConfigPath(t *testing.T) {
|
||||
base := withConfigDir(t, "config.yaml", sampleConfig)
|
||||
if got, want := ConfigPath(), filepath.Join(base, appDir, "config.yaml"); got != want {
|
||||
t.Errorf("ConfigPath = %q, want %q", got, want)
|
||||
@@ -93,7 +82,7 @@ func TestLoadMissingConfigIsNotError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveAppliesDefaults(t *testing.T) {
|
||||
withConfigDir(t, "vctl.yaml", sampleConfig)
|
||||
withConfigDir(t, "config.yaml", sampleConfig)
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -128,7 +117,7 @@ func TestResolveAppliesDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveUnknownContext(t *testing.T) {
|
||||
withConfigDir(t, "vctl.yaml", sampleConfig)
|
||||
withConfigDir(t, "config.yaml", sampleConfig)
|
||||
cfg, _ := Load()
|
||||
if _, err := cfg.Resolve("nope"); err == nil {
|
||||
t.Error("expected error for unknown context")
|
||||
@@ -136,7 +125,7 @@ func TestResolveUnknownContext(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveMethodDefaultWhenUnset(t *testing.T) {
|
||||
withConfigDir(t, "vctl.yaml", `
|
||||
withConfigDir(t, "config.yaml", `
|
||||
contexts:
|
||||
bare:
|
||||
address: https://vault.example.net
|
||||
@@ -156,7 +145,7 @@ contexts:
|
||||
}
|
||||
|
||||
func TestResolveWithOverrides(t *testing.T) {
|
||||
withConfigDir(t, "vctl.yaml", sampleConfig)
|
||||
withConfigDir(t, "config.yaml", sampleConfig)
|
||||
cfg, _ := Load()
|
||||
|
||||
// Overriding the method also moves the auth path (context did not pin one).
|
||||
|
||||
+10
-10
@@ -1,13 +1,13 @@
|
||||
// Command vctl manages Vault tokens for multiple vault instances ("contexts").
|
||||
// Command vcm manages Vault tokens for multiple vault instances ("contexts").
|
||||
//
|
||||
// It logs in to (or renews tokens for) one or all configured contexts and
|
||||
// caches the resulting tokens under ~/.cache/vault/<context> for use by vctx
|
||||
// caches the resulting tokens under ~/.cache/vcm/<context> for use by vctx
|
||||
// and other tooling.
|
||||
//
|
||||
// vctl login sydney
|
||||
// vctl login --all
|
||||
// vctl renew staging/sydney
|
||||
// vctl renew --all
|
||||
// vcm login sydney
|
||||
// vcm login --all
|
||||
// vcm renew staging/sydney
|
||||
// vcm renew --all
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -39,11 +39,11 @@ func newRootCmd() *cobra.Command {
|
||||
)
|
||||
|
||||
root := &cobra.Command{
|
||||
Use: "vctl",
|
||||
Use: "vcm",
|
||||
Short: "Manage Vault tokens for multiple vault instances (contexts).",
|
||||
Long: "vctl logs in to and renews Vault tokens for the contexts defined in\n" +
|
||||
"~/.config/vault/vctl.yaml (or config.yaml), caching each token under\n" +
|
||||
"~/.cache/vault/<context> for use by vctx and other tooling.",
|
||||
Long: "vcm logs in to and renews Vault tokens for the contexts defined in\n" +
|
||||
"~/.config/vcm/config.yaml, caching each token under\n" +
|
||||
"~/.cache/vcm/<context> for use by vctx and other tooling.",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
@@ -16,23 +16,23 @@ import (
|
||||
"git.unkin.net/unkin/vault-tools/shared"
|
||||
)
|
||||
|
||||
// setupVctl points XDG dirs at temp locations and writes a config file. It
|
||||
// setupVcm points XDG dirs at temp locations and writes a config file. It
|
||||
// returns the config dir base so tests can inspect cache writes.
|
||||
func setupVctl(t *testing.T, cfg string) {
|
||||
func setupVcm(t *testing.T, cfg string) {
|
||||
t.Helper()
|
||||
cfgHome := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", cfgHome)
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
cfgDir := filepath.Join(cfgHome, "vault")
|
||||
cfgDir := filepath.Join(cfgHome, "vcm")
|
||||
if err := os.MkdirAll(cfgDir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(cfgDir, "vctl.yaml"), []byte(cfg), 0o600); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(cfgDir, "config.yaml"), []byte(cfg), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const vctlConfig = `
|
||||
const vcmConfig = `
|
||||
defaults:
|
||||
method: ldap
|
||||
user: ben
|
||||
@@ -47,7 +47,7 @@ contexts:
|
||||
// --- runOverContexts: selection + iteration + aggregation ------------------
|
||||
|
||||
func TestRunOverContextsSingle(t *testing.T) {
|
||||
setupVctl(t, vctlConfig)
|
||||
setupVcm(t, vcmConfig)
|
||||
var seen []string
|
||||
fn := func(rc shared.ResolvedContext) (*shared.Token, error) {
|
||||
seen = append(seen, rc.Name)
|
||||
@@ -66,7 +66,7 @@ func TestRunOverContextsSingle(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunOverContextsAllIteratesEveryContext(t *testing.T) {
|
||||
setupVctl(t, vctlConfig)
|
||||
setupVcm(t, vcmConfig)
|
||||
var seen []string
|
||||
fn := func(rc shared.ResolvedContext) (*shared.Token, error) {
|
||||
seen = append(seen, rc.Name)
|
||||
@@ -88,7 +88,7 @@ func TestRunOverContextsAllIteratesEveryContext(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunOverContextsContextAndAllConflict(t *testing.T) {
|
||||
setupVctl(t, vctlConfig)
|
||||
setupVcm(t, vcmConfig)
|
||||
fn := func(rc shared.ResolvedContext) (*shared.Token, error) { return nil, nil }
|
||||
err := runOverContexts([]string{"sydney"}, true, "", "", "login", fn)
|
||||
if err == nil || !strings.Contains(err.Error(), "not both") {
|
||||
@@ -97,7 +97,7 @@ func TestRunOverContextsContextAndAllConflict(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunOverContextsNoTarget(t *testing.T) {
|
||||
setupVctl(t, vctlConfig)
|
||||
setupVcm(t, vcmConfig)
|
||||
fn := func(rc shared.ResolvedContext) (*shared.Token, error) { return nil, nil }
|
||||
if err := runOverContexts(nil, false, "", "", "login", fn); err == nil {
|
||||
t.Error("expected error when neither context nor --all given")
|
||||
@@ -105,7 +105,7 @@ func TestRunOverContextsNoTarget(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunOverContextsAllEmptyConfig(t *testing.T) {
|
||||
setupVctl(t, "contexts: {}\n")
|
||||
setupVcm(t, "contexts: {}\n")
|
||||
fn := func(rc shared.ResolvedContext) (*shared.Token, error) { return nil, nil }
|
||||
err := runOverContexts(nil, true, "", "", "login", fn)
|
||||
if err == nil || !strings.Contains(err.Error(), "no contexts configured") {
|
||||
@@ -114,7 +114,7 @@ func TestRunOverContextsAllEmptyConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunOverContextsAggregatesFailures(t *testing.T) {
|
||||
setupVctl(t, vctlConfig)
|
||||
setupVcm(t, vcmConfig)
|
||||
fn := func(rc shared.ResolvedContext) (*shared.Token, error) {
|
||||
if rc.Name == "sydney" {
|
||||
return nil, io.ErrUnexpectedEOF // simulate a login failure
|
||||
@@ -137,7 +137,7 @@ func TestRunOverContextsAggregatesFailures(t *testing.T) {
|
||||
|
||||
func TestRunOverContextsResolveErrorCounts(t *testing.T) {
|
||||
// A context missing an address fails resolution and is counted as a failure.
|
||||
setupVctl(t, `
|
||||
setupVcm(t, `
|
||||
contexts:
|
||||
good:
|
||||
address: https://vault.example.net
|
||||
@@ -153,7 +153,7 @@ contexts:
|
||||
}
|
||||
|
||||
func TestRunOverContextsAppliesOverrides(t *testing.T) {
|
||||
setupVctl(t, vctlConfig)
|
||||
setupVcm(t, vcmConfig)
|
||||
var got shared.ResolvedContext
|
||||
fn := func(rc shared.ResolvedContext) (*shared.Token, error) {
|
||||
got = rc
|
||||
@@ -237,7 +237,7 @@ func TestDoLoginTokenMethod(t *testing.T) {
|
||||
// --- doRenew ---------------------------------------------------------------
|
||||
|
||||
func TestDoRenewNoCachedToken(t *testing.T) {
|
||||
setupVctl(t, vctlConfig)
|
||||
setupVcm(t, vcmConfig)
|
||||
rc := shared.ResolvedContext{Name: "sydney", Address: "https://vault.example.net"}
|
||||
if _, err := doRenew(rc); err == nil {
|
||||
t.Error("expected error renewing a context with no cached token")
|
||||
@@ -245,7 +245,7 @@ func TestDoRenewNoCachedToken(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDoRenewUsesCachedToken(t *testing.T) {
|
||||
setupVctl(t, vctlConfig)
|
||||
setupVcm(t, vcmConfig)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/auth/token/renew-self" {
|
||||
t.Errorf("path = %q", r.URL.Path)
|
||||
@@ -276,7 +276,7 @@ func TestDoRenewUsesCachedToken(t *testing.T) {
|
||||
// --- runList ---------------------------------------------------------------
|
||||
|
||||
func TestRunListShowsTokenStatus(t *testing.T) {
|
||||
setupVctl(t, vctlConfig)
|
||||
setupVcm(t, vcmConfig)
|
||||
// sydney has a valid token; staging/sydney has none.
|
||||
if err := shared.SaveToken(&shared.Token{
|
||||
Context: "sydney",
|
||||
@@ -300,7 +300,7 @@ func TestRunListShowsTokenStatus(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunListEmptyConfig(t *testing.T) {
|
||||
setupVctl(t, "contexts: {}\n")
|
||||
setupVcm(t, "contexts: {}\n")
|
||||
var buf bytes.Buffer
|
||||
if err := runList(&buf); err != nil {
|
||||
t.Fatal(err)
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
// Command vctx is a thin, context-aware wrapper around the real `vault` CLI.
|
||||
//
|
||||
// It resolves a context (via the same config + token cache as vctl), sets
|
||||
// It resolves a context (via the same config + token cache as vcm), sets
|
||||
// VAULT_ADDR / VAULT_TOKEN / VAULT_NAMESPACE for that single invocation, and
|
||||
// execs `vault` with the remaining arguments:
|
||||
//
|
||||
@@ -37,8 +37,8 @@ func newRootCmd() *cobra.Command {
|
||||
root := &cobra.Command{
|
||||
Use: "vctx --context <context> <vault args...>",
|
||||
Short: "Run the vault CLI against a named context.",
|
||||
Long: "vctx resolves a context from ~/.config/vault (shared with vctl), loads its\n" +
|
||||
"cached token from ~/.cache/vault/<context>, sets VAULT_ADDR, VAULT_TOKEN and\n" +
|
||||
Long: "vctx resolves a context from ~/.config/vcm (shared with vcm), loads its\n" +
|
||||
"cached token from ~/.cache/vcm/<context>, sets VAULT_ADDR, VAULT_TOKEN and\n" +
|
||||
"VAULT_NAMESPACE for this invocation only, and execs `vault` with the\n" +
|
||||
"remaining arguments.\n\n" +
|
||||
"Example: vctx --context sydney kv put kv/foo/bar secret=baz",
|
||||
|
||||
+2
-2
@@ -20,11 +20,11 @@ func setupVctx(t *testing.T, cfg string, tokens map[string]shared.Token) string
|
||||
t.Setenv("XDG_CONFIG_HOME", cfgHome)
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
|
||||
cfgDir := filepath.Join(cfgHome, "vault")
|
||||
cfgDir := filepath.Join(cfgHome, "vcm")
|
||||
if err := os.MkdirAll(cfgDir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(cfgDir, "vctl.yaml"), []byte(cfg), 0o600); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(cfgDir, "config.yaml"), []byte(cfg), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, tok := range tokens {
|
||||
|
||||
Reference in New Issue
Block a user