diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db0c81b..1965123 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,16 +18,16 @@ jobs: include: - goos: darwin goarch: amd64 - archive: kubectl-multi_darwin_amd64.tar.gz + archive: kubectl-multi-get_darwin_amd64.tar.gz - goos: darwin goarch: arm64 - archive: kubectl-multi_darwin_arm64.tar.gz + archive: kubectl-multi-get_darwin_arm64.tar.gz - goos: linux goarch: amd64 - archive: kubectl-multi_linux_amd64.tar.gz + archive: kubectl-multi-get_linux_amd64.tar.gz - goos: linux goarch: arm64 - archive: kubectl-multi_linux_arm64.tar.gz + archive: kubectl-multi-get_linux_arm64.tar.gz steps: - uses: actions/checkout@v4 @@ -43,9 +43,9 @@ jobs: CGO_ENABLED: '0' run: | mkdir -p dist - go build -trimpath -ldflags='-s -w' -o dist/kubectl-multi ./cmd/kubectl-multi - tar -czf "dist/${{ matrix.archive }}" -C dist kubectl-multi - rm dist/kubectl-multi + go build -trimpath -ldflags='-s -w' -o dist/kubectl-multi-get ./cmd/kubectl-multi-get + tar -czf "dist/${{ matrix.archive }}" -C dist kubectl-multi-get -C "$GITHUB_WORKSPACE" LICENSE + rm dist/kubectl-multi-get - name: Upload release archive uses: actions/upload-artifact@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a9155e..e06cdcf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,4 +15,4 @@ jobs: check-latest: true - run: go test ./... - run: go vet ./... - - run: go build ./cmd/kubectl-multi + - run: go build ./cmd/kubectl-multi-get diff --git a/.krew.yaml b/.krew.yaml new file mode 100644 index 0000000..dc71e07 --- /dev/null +++ b/.krew.yaml @@ -0,0 +1,57 @@ +apiVersion: krew.googlecontainertools.github.com/v1alpha2 +kind: Plugin +metadata: + name: multi-get +spec: + version: {{ .TagName }} + homepage: https://github.com/squatboy/multi-get + shortDescription: Query resources across namespaces + description: | + Query the same Kubernetes resource across multiple namespaces with one + kubectl command. Select namespaces explicitly, find them by name, or + query every namespace. + platforms: + - selector: + matchLabels: + os: darwin + arch: amd64 + {{addURIAndSha "https://github.com/squatboy/multi-get/releases/download/{{ .TagName }}/kubectl-multi-get_darwin_amd64.tar.gz" .TagName }} + bin: kubectl-multi-get + files: + - from: kubectl-multi-get + to: . + - from: LICENSE + to: . + - selector: + matchLabels: + os: darwin + arch: arm64 + {{addURIAndSha "https://github.com/squatboy/multi-get/releases/download/{{ .TagName }}/kubectl-multi-get_darwin_arm64.tar.gz" .TagName }} + bin: kubectl-multi-get + files: + - from: kubectl-multi-get + to: . + - from: LICENSE + to: . + - selector: + matchLabels: + os: linux + arch: amd64 + {{addURIAndSha "https://github.com/squatboy/multi-get/releases/download/{{ .TagName }}/kubectl-multi-get_linux_amd64.tar.gz" .TagName }} + bin: kubectl-multi-get + files: + - from: kubectl-multi-get + to: . + - from: LICENSE + to: . + - selector: + matchLabels: + os: linux + arch: arm64 + {{addURIAndSha "https://github.com/squatboy/multi-get/releases/download/{{ .TagName }}/kubectl-multi-get_linux_arm64.tar.gz" .TagName }} + bin: kubectl-multi-get + files: + - from: kubectl-multi-get + to: . + - from: LICENSE + to: . diff --git a/Makefile b/Makefile index a085bad..b748fe3 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ GO ?= go -BINARY ?= bin/kubectl-multi +BINARY ?= bin/kubectl-multi-get .PHONY: build test vet e2e-local build: mkdir -p bin - $(GO) build -o $(BINARY) ./cmd/kubectl-multi + $(GO) build -o $(BINARY) ./cmd/kubectl-multi-get test: $(GO) test ./... diff --git a/README.md b/README.md index 5cca0a2..77034cf 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ kubectl [plugin](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins Choose the namespaces explicitly, find them by name, or query every namespace: ```bash -kubectl multi get pods -n dev,stage,prod -kubectl multi get deploy --find-ns dev -kubectl multi get pods -A +kubectl multi-get get pods -n dev,stage,prod +kubectl multi-get get deploy --find-ns dev +kubectl multi-get get pods -A ``` This multi-namespace lookup is the core feature. The plugin sends the resource query to each selected Namespace and combines the results with the Namespace shown in human-readable output. @@ -21,10 +21,10 @@ Regular kubectl already supports multiple resource kinds in one namespace: kubectl get pod,svc,pvc -n dev ``` -It also supports all namespaces for many normal list commands. `kubectl-multi` keeps those familiar resource expressions as a secondary composition feature, so the following applies each resource query to both namespaces and prints separate blocks: +It also supports all namespaces for many normal list commands. `kubectl-multi-get` keeps those familiar resource expressions as a secondary composition feature, so the following applies each resource query to both namespaces and prints separate blocks: ```bash -kubectl multi get pods,services -n dev,stage +kubectl multi-get get pods,services -n dev,stage ``` ## Install @@ -38,7 +38,7 @@ curl -fsSL https://github.com/squatboy/multi-get/releases/latest/download/instal ``` The installer detects the operating system and CPU architecture, verifies the -downloaded archive, and installs `kubectl-multi` into `$HOME/.local/bin`. +downloaded archive, and installs `kubectl-multi-get` into `$HOME/.local/bin`. The installer adds `$HOME/.local/bin` to the detected shell configuration. Open a new terminal, or reload the shell configuration using the command shown @@ -47,7 +47,23 @@ by the installer. Verify the installation: ```bash -kubectl multi --help +kubectl multi-get --help +``` + +### Krew + +Install from the official Krew index: + +```bash +kubectl krew update +kubectl krew install multi-get +``` + +Verify the installation: + +```bash +kubectl multi-get --help +kubectl multi-get get pods -n dev,stage ``` ### Local Build @@ -57,7 +73,7 @@ Build the plugin locally: ```bash make build export PATH="$PWD/bin:$PATH" -kubectl multi get pods -n dev,stage +kubectl multi-get get pods -n dev,stage ``` This repository targets Go 1.25.x and Kubernetes client modules v0.35.x. @@ -70,10 +86,10 @@ Exactly one namespace mode is used: | Command | Behavior | | --- | --- | -| `kubectl multi get pods` | Current context namespace, or `default` | -| `kubectl multi get pods -n dev,stage` | Explicit namespaces, in input order | -| `kubectl multi get pods --find-ns dev` | Namespace names containing `dev`, sorted by name | -| `kubectl multi get pods -A` | All namespaces, sorted by name | +| `kubectl multi-get get pods` | Current context namespace, or `default` | +| `kubectl multi-get get pods -n dev,stage` | Explicit namespaces, in input order | +| `kubectl multi-get get pods --find-ns dev` | Namespace names containing `dev`, sorted by name | +| `kubectl multi-get get pods -A` | All namespaces, sorted by name | Explicit lists trim whitespace and remove duplicate names while preserving the first occurrence. Empty entries and mixed namespace modes are errors. `--find-ns` trims the search text and matches it literally anywhere in each Namespace name. Empty search text and a search with no matches are errors and do not start resource requests. @@ -82,24 +98,24 @@ Explicit lists trim whitespace and remove duplicate names while preserving the f Selectors are sent to every namespaced List request: ```bash -kubectl multi get pods -n dev,stage -l app=backend -o wide -kubectl multi get pods -n dev,stage --field-selector status.phase=Running -kubectl multi get pods -n dev,stage --sort-by=.metadata.creationTimestamp +kubectl multi-get get pods -n dev,stage -l app=backend -o wide +kubectl multi-get get pods -n dev,stage --field-selector status.phase=Running +kubectl multi-get get pods -n dev,stage --sort-by=.metadata.creationTimestamp ``` Supported output formats are `wide`, `json`, `yaml`, `name`, `custom-columns`, `jsonpath`, and `go-template`. Human tables always put `NAMESPACE` first. JSON and YAML for one resource are emitted as one Kubernetes `List` object. ```bash -kubectl multi get pods -n dev,stage -o json -kubectl multi get pods -n dev,stage -o name -kubectl multi get pods -n dev,stage -o 'custom-columns=NAME:.metadata.name,PHASE:.status.phase' +kubectl multi-get get pods -n dev,stage -o json +kubectl multi-get get pods -n dev,stage -o name +kubectl multi-get get pods -n dev,stage -o 'custom-columns=NAME:.metadata.name,PHASE:.status.phase' ``` Named lookup supports multiple object names for one resource type: ```bash -kubectl multi get pod api web -n dev,stage -kubectl multi get pod missing -n dev,stage --ignore-not-found +kubectl multi-get get pod api web -n dev,stage +kubectl multi-get get pod missing -n dev,stage --ignore-not-found ``` Namespace requests and resource requests are sequential. Successful results are printed even when another namespace or resource request fails; any real API error still gives exit code 1. @@ -116,7 +132,9 @@ Namespace requests and resource requests are sequential. Successful results are | Table, wide, JSON, YAML, name, and template output | Multi-resource with object names | | Namespaced built-in resources and namespaced CRDs | Namespace exclude and parallel queries | -The plugin does not register with Krew or provide shell-completion. +The repository includes the Krew manifest template used for the +[official index](https://github.com/kubernetes-sigs/krew-index/blob/master/plugins/multi-get.yaml). +The plugin does not provide shell-completion. ## Verification diff --git a/cmd/kubectl-multi/main.go b/cmd/kubectl-multi-get/main.go similarity index 80% rename from cmd/kubectl-multi/main.go rename to cmd/kubectl-multi-get/main.go index b86ad61..e2ffba2 100644 --- a/cmd/kubectl-multi/main.go +++ b/cmd/kubectl-multi-get/main.go @@ -5,6 +5,7 @@ import ( "os" "github.com/squatboy/multi-get/internal/cli" + _ "k8s.io/client-go/plugin/pkg/client/auth" ) func main() { diff --git a/hack/e2e-local.sh b/hack/e2e-local.sh index 449f1f4..a5f1f3a 100755 --- a/hack/e2e-local.sh +++ b/hack/e2e-local.sh @@ -13,7 +13,7 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" kubectl_bin="$(command -v kubectl)" go_bin="${GO:-go}" -"$go_bin" build -o "$repo_root/bin/kubectl-multi" ./cmd/kubectl-multi +"$go_bin" build -o "$repo_root/bin/kubectl-multi-get" ./cmd/kubectl-multi-get export PATH="$repo_root/bin:$PATH" context_name="$(kubectl config current-context)" @@ -171,35 +171,35 @@ spec: {} EOF done -kubectl multi get pods -n "${namespace_a},${namespace_b}" >/dev/null -kubectl multi get pods -n "${namespace_b},${namespace_a}" >/dev/null -kubectl multi get pods --find-ns "${prefix}-dev" >/dev/null -kubectl multi get pods -A >/dev/null -kubectl multi get pods,services -n "${namespace_a},${namespace_b}" >/dev/null -kubectl multi get pods -n "${namespace_a},${namespace_b}" -l app=multi-get-e2e >/dev/null -kubectl multi get pods -n "${namespace_a},${namespace_b}" --field-selector "metadata.namespace=${namespace_a}" >/dev/null -kubectl multi get pods -n "${namespace_a},${namespace_b}" -o wide >/dev/null +kubectl multi-get get pods -n "${namespace_a},${namespace_b}" >/dev/null +kubectl multi-get get pods -n "${namespace_b},${namespace_a}" >/dev/null +kubectl multi-get get pods --find-ns "${prefix}-dev" >/dev/null +kubectl multi-get get pods -A >/dev/null +kubectl multi-get get pods,services -n "${namespace_a},${namespace_b}" >/dev/null +kubectl multi-get get pods -n "${namespace_a},${namespace_b}" -l app=multi-get-e2e >/dev/null +kubectl multi-get get pods -n "${namespace_a},${namespace_b}" --field-selector "metadata.namespace=${namespace_a}" >/dev/null +kubectl multi-get get pods -n "${namespace_a},${namespace_b}" -o wide >/dev/null -json_output="$(kubectl multi get pods -n "${namespace_a},${namespace_b}" -o json)" +json_output="$(kubectl multi-get get pods -n "${namespace_a},${namespace_b}" -o json)" grep -q '"kind": "List"' <<<"$json_output" grep -q "${namespace_a}" <<<"$json_output" -kubectl multi get pods -n "${namespace_a},${namespace_b}" -o yaml >/dev/null -kubectl multi get pods -n "${namespace_a},${namespace_b}" -o name >/dev/null -kubectl multi get widgets -n "${namespace_a},${namespace_b}" >/dev/null +kubectl multi-get get pods -n "${namespace_a},${namespace_b}" -o yaml >/dev/null +kubectl multi-get get pods -n "${namespace_a},${namespace_b}" -o name >/dev/null +kubectl multi-get get widgets -n "${namespace_a},${namespace_b}" >/dev/null -if kubectl multi get pods --find-ns "${prefix}-missing" >/dev/null 2>&1; then +if kubectl multi-get get pods --find-ns "${prefix}-missing" >/dev/null 2>&1; then echo "expected no-match to fail" >&2 exit 1 fi -if kubectl multi get pods,services "${prefix}-pod" -n "$namespace_a" >/dev/null 2>&1; then +if kubectl multi-get get pods,services "${prefix}-pod" -n "$namespace_a" >/dev/null 2>&1; then echo "expected multi-resource named lookup to fail" >&2 exit 1 fi -if kubectl multi get pods,services -o json -n "$namespace_a" >/dev/null 2>&1; then +if kubectl multi-get get pods,services -o json -n "$namespace_a" >/dev/null 2>&1; then echo "expected multi-resource structured output to fail" >&2 exit 1 fi -if kubectl multi get nodes -A >/dev/null 2>&1; then +if kubectl multi-get get nodes -A >/dev/null 2>&1; then echo "expected cluster-scoped lookup to fail" >&2 exit 1 fi diff --git a/install.sh b/install.sh index 65dc9fb..45e539e 100755 --- a/install.sh +++ b/install.sh @@ -3,7 +3,7 @@ set -eu die() { - echo "kubectl-multi installer: $*" >&2 + echo "kubectl-multi-get installer: $*" >&2 exit 1 } @@ -51,11 +51,11 @@ esac base_url=${MULTI_GET_RELEASE_BASE_URL:-https://github.com/squatboy/multi-get/releases/latest/download} base_url=${base_url%/} -archive_name="kubectl-multi_${release_os}_${release_arch}.tar.gz" +archive_name="kubectl-multi-get_${release_os}_${release_arch}.tar.gz" tmp_dir= staged_target= -if ! tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/kubectl-multi-install.XXXXXX"); then +if ! tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/kubectl-multi-get-install.XXXXXX"); then die "could not create a temporary directory" fi @@ -79,7 +79,7 @@ download() { archive_path="$tmp_dir/$archive_name" checksums_path="$tmp_dir/checksums.txt" -echo "Downloading kubectl-multi for $release_os/$release_arch..." +echo "Downloading kubectl-multi-get for $release_os/$release_arch..." download "$base_url/$archive_name" "$archive_path" download "$base_url/checksums.txt" "$checksums_path" @@ -115,24 +115,24 @@ if ! tar -xzf "$archive_path" -C "$extract_dir"; then die "could not extract $archive_name" fi -binary_path="$extract_dir/kubectl-multi" -[ -f "$binary_path" ] || die "archive does not contain kubectl-multi" +binary_path="$extract_dir/kubectl-multi-get" +[ -f "$binary_path" ] || die "archive does not contain kubectl-multi-get" install_dir="$home/.local/bin" -target_path="$install_dir/kubectl-multi" +target_path="$install_dir/kubectl-multi-get" if ! mkdir -p "$install_dir"; then die "could not create $install_dir" fi -staged_target="$install_dir/.kubectl-multi.$$" +staged_target="$install_dir/.kubectl-multi-get.$$" if ! cp "$binary_path" "$staged_target"; then - die "could not stage kubectl-multi" + die "could not stage kubectl-multi-get" fi if ! chmod 0755 "$staged_target"; then - die "could not make kubectl-multi executable" + die "could not make kubectl-multi-get executable" fi if ! mv -f "$staged_target" "$target_path"; then - die "could not install kubectl-multi into $install_dir" + die "could not install kubectl-multi-get into $install_dir" fi staged_target= @@ -157,17 +157,17 @@ path_status= if [ -n "$shell_config" ]; then if [ -f "$shell_config" ] && grep -F '$HOME/.local/bin' "$shell_config" >/dev/null 2>&1; then path_status="already present" - elif printf '\n# kubectl-multi\n%s\n' "$path_line" >> "$shell_config"; then + elif printf '\n# kubectl-multi-get\n%s\n' "$path_line" >> "$shell_config"; then path_status="added to $shell_config" else path_status="could not update $shell_config" fi fi -echo "Installed kubectl-multi to $target_path." +echo "Installed kubectl-multi-get to $target_path." if [ -n "$shell_config" ]; then echo "PATH entry: $path_status." echo "Reload your shell with: . \"$shell_config\"" else - echo "Add $install_dir to PATH before running kubectl multi." + echo "Add $install_dir to PATH before running kubectl multi-get." fi diff --git a/internal/cli/root.go b/internal/cli/root.go index 5859c26..2fc0178 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -20,7 +20,7 @@ func Run(ctx context.Context, args []string, stdout, stderr io.Writer) int { return 0 } -// NewRootCommand creates the kubectl-multi command tree. +// NewRootCommand creates the kubectl-multi-get command tree. func NewRootCommand(ctx context.Context, stdout, stderr io.Writer) *cobra.Command { configFlags := genericclioptions.NewConfigFlags(false) // -n/--namespace belongs to this plugin and means a comma-separated list. @@ -29,8 +29,11 @@ func NewRootCommand(ctx context.Context, stdout, stderr io.Writer) *cobra.Comman configFlags.Namespace = nil root := &cobra.Command{ - Use: "kubectl-multi", - Short: "Query namespaced Kubernetes resources across namespaces", + Use: "kubectl-multi-get", + Short: "Query namespaced Kubernetes resources across namespaces", + Annotations: map[string]string{ + cobra.CommandDisplayNameAnnotation: "kubectl multi-get", + }, SilenceErrors: true, SilenceUsage: true, }