mirror of
https://github.com/ahmetb/kubectx.git
synced 2026-08-09 09:12:41 +00:00
fix: pipe context/namespace lists to fzf via stdin
Interactive mode was launching fzf and handing it the parent TTY on fd 0, then relying on fzf to re-exec kubectx via FZF_DEFAULT_COMMAND to obtain the candidate list. fzf never received the list on its stdin, so on systems where fzf did not honor FZF_DEFAULT_COMMAND the picker showed no items and kubectx exited with 'you did not choose any of the options'. Build the candidate list in-process and pipe it directly to fzf's stdin. This removes the self-re-exec, the $SHELL -c dependency, and the TTY-plumbing hack on fd 0. Fixes #500
This commit is contained in:
@@ -35,7 +35,7 @@ func (op UnsupportedOp) Run(_, _ io.Writer) error {
|
||||
func parseArgs(argv []string) Op {
|
||||
if len(argv) == 0 {
|
||||
if cmdutil.IsInteractiveMode(os.Stdout) {
|
||||
return InteractiveSwitchOp{SelfCmd: os.Args[0]}
|
||||
return InteractiveSwitchOp{}
|
||||
}
|
||||
return ListOp{}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ func parseArgs(argv []string) Op {
|
||||
if argv[0] == "--readonly" || argv[0] == "-r" {
|
||||
if len(argv) == 1 {
|
||||
if cmdutil.IsInteractiveMode(os.Stdout) {
|
||||
return InteractiveReadonlyShellOp{SelfCmd: os.Args[0]}
|
||||
return InteractiveReadonlyShellOp{}
|
||||
}
|
||||
return UnsupportedOp{Err: fmt.Errorf("'%s' requires a context name argument (or fzf for interactive mode)", argv[0])}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func parseArgs(argv []string) Op {
|
||||
if argv[0] == "--shell" || argv[0] == "-s" {
|
||||
if len(argv) == 1 {
|
||||
if cmdutil.IsInteractiveMode(os.Stdout) {
|
||||
return InteractiveShellOp{SelfCmd: os.Args[0]}
|
||||
return InteractiveShellOp{}
|
||||
}
|
||||
return UnsupportedOp{Err: fmt.Errorf("'%s' requires a context name argument (or fzf for interactive mode)", argv[0])}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func parseArgs(argv []string) Op {
|
||||
if argv[0] == "-d" {
|
||||
if len(argv) == 1 {
|
||||
if cmdutil.IsInteractiveMode(os.Stdout) {
|
||||
return InteractiveDeleteOp{SelfCmd: os.Args[0]}
|
||||
return InteractiveDeleteOp{}
|
||||
} else {
|
||||
return UnsupportedOp{Err: fmt.Errorf("'-d' needs arguments")}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,9 @@ import (
|
||||
"github.com/ahmetb/kubectx/internal/printer"
|
||||
)
|
||||
|
||||
type InteractiveSwitchOp struct {
|
||||
SelfCmd string
|
||||
}
|
||||
type InteractiveSwitchOp struct{}
|
||||
|
||||
type InteractiveDeleteOp struct {
|
||||
SelfCmd string
|
||||
}
|
||||
type InteractiveDeleteOp struct{}
|
||||
|
||||
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
|
||||
if err := checkIsolatedMode(); err != nil {
|
||||
@@ -60,14 +56,18 @@ func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
|
||||
return errors.New("no contexts found in the kubeconfig file")
|
||||
}
|
||||
|
||||
var in bytes.Buffer
|
||||
if err := formatContextList(kc, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("fzf", "--ansi", "--no-preview")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdin = &in
|
||||
cmd.Stderr = stderr
|
||||
cmd.Stdout = &out
|
||||
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
|
||||
fmt.Sprintf("%s=1", env.EnvForceColor))
|
||||
if err := cmd.Run(); err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
@@ -110,14 +110,18 @@ func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error {
|
||||
return errors.New("no contexts found in config")
|
||||
}
|
||||
|
||||
var in bytes.Buffer
|
||||
if err := formatContextList(kc, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("fzf", "--ansi", "--no-preview")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdin = &in
|
||||
cmd.Stderr = stderr
|
||||
cmd.Stdout = &out
|
||||
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
|
||||
fmt.Sprintf("%s=1", env.EnvForceColor))
|
||||
if err := cmd.Run(); err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
|
||||
183
cmd/kubectx/fzf_test.go
Normal file
183
cmd/kubectx/fzf_test.go
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright 2021 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ahmetb/kubectx/internal/kubeconfig"
|
||||
)
|
||||
|
||||
// writeTestKubeconfig writes a kubeconfig with contexts ctx-a (current)
|
||||
// and ctx-b into a temp dir and returns its path.
|
||||
func writeTestKubeconfig(t *testing.T) string {
|
||||
t.Helper()
|
||||
const cfg = `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: ctx-a
|
||||
contexts:
|
||||
- name: ctx-a
|
||||
context: { cluster: cluster-a, user: user-a }
|
||||
- name: ctx-b
|
||||
context: { cluster: cluster-b, user: user-b }
|
||||
clusters:
|
||||
- name: cluster-a
|
||||
cluster: { server: https://example.invalid }
|
||||
- name: cluster-b
|
||||
cluster: { server: https://example.invalid }
|
||||
users:
|
||||
- name: user-a
|
||||
user: { token: fake }
|
||||
- name: user-b
|
||||
user: { token: fake }
|
||||
`
|
||||
p := filepath.Join(t.TempDir(), "kubeconfig")
|
||||
if err := os.WriteFile(p, []byte(cfg), 0644); err != nil {
|
||||
t.Fatalf("write kubeconfig: %v", err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// installFakeFzf installs a fake `fzf` in a temp bin dir that copies its
|
||||
// stdin to the file at $KUBECTX_TEST_FZF_STDIN and echoes $KUBECTX_TEST_FZF_OUT
|
||||
// to stdout. It prepends the dir to PATH. The test reads the recorded file to
|
||||
// verify the candidate list was piped to fzf stdin.
|
||||
func installFakeFzf(t *testing.T) {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fake fzf shell script unsupported on windows")
|
||||
}
|
||||
binDir := t.TempDir()
|
||||
const script = `#!/bin/sh
|
||||
cat > "$KUBECTX_TEST_FZF_STDIN"
|
||||
printf '%s\n' "$KUBECTX_TEST_FZF_OUT"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(binDir, "fzf"), []byte(script), 0755); err != nil {
|
||||
t.Fatalf("write fake fzf: %v", err)
|
||||
}
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
}
|
||||
|
||||
func Test_formatContextList(t *testing.T) {
|
||||
t.Setenv("KUBECONFIG", writeTestKubeconfig(t))
|
||||
|
||||
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
|
||||
defer kc.Close()
|
||||
if err := kc.Parse(); err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := formatContextList(kc, &out); err != nil {
|
||||
t.Fatalf("formatContextList: %v", err)
|
||||
}
|
||||
got := out.String()
|
||||
lines := strings.Split(strings.TrimRight(got, "\n"), "\n")
|
||||
if len(lines) != 2 {
|
||||
t.Fatalf("expected 2 lines, got %d: %q", len(lines), got)
|
||||
}
|
||||
for _, l := range lines {
|
||||
// strip any ANSI color escapes from the active-context line
|
||||
name := strings.Trim(l, "\x1b[0;m123456789")
|
||||
if name != "ctx-a" && name != "ctx-b" {
|
||||
t.Errorf("unexpected line %q", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInteractiveSwitchOp_pipesListToFzfStdin(t *testing.T) {
|
||||
cfg := writeTestKubeconfig(t)
|
||||
t.Setenv("KUBECONFIG", cfg)
|
||||
installFakeFzf(t)
|
||||
|
||||
stdinRec := filepath.Join(t.TempDir(), "fzf-stdin.txt")
|
||||
t.Setenv("KUBECTX_TEST_FZF_STDIN", stdinRec)
|
||||
t.Setenv("KUBECTX_TEST_FZF_OUT", "ctx-b")
|
||||
|
||||
var stderr bytes.Buffer
|
||||
if err := (InteractiveSwitchOp{}).Run(io.Discard, &stderr); err != nil {
|
||||
t.Fatalf("InteractiveSwitchOp.Run: %v", err)
|
||||
}
|
||||
|
||||
rec, err := os.ReadFile(stdinRec)
|
||||
if err != nil {
|
||||
t.Fatalf("read fzf stdin capture: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(rec), "ctx-a") || !strings.Contains(string(rec), "ctx-b") {
|
||||
t.Fatalf("fzf stdin did not contain context list; got %q", string(rec))
|
||||
}
|
||||
|
||||
// switchContext should have switched to ctx-b
|
||||
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
|
||||
defer kc.Close()
|
||||
if err := kc.Parse(); err != nil {
|
||||
t.Fatalf("re-parse: %v", err)
|
||||
}
|
||||
cur, err := kc.GetCurrentContext()
|
||||
if err != nil {
|
||||
t.Fatalf("get current: %v", err)
|
||||
}
|
||||
if cur != "ctx-b" {
|
||||
t.Errorf("current-context = %q, want %q", cur, "ctx-b")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInteractiveDeleteOp_pipesListToFzfStdin(t *testing.T) {
|
||||
cfg := writeTestKubeconfig(t)
|
||||
t.Setenv("KUBECONFIG", cfg)
|
||||
installFakeFzf(t)
|
||||
|
||||
stdinRec := filepath.Join(t.TempDir(), "fzf-stdin.txt")
|
||||
t.Setenv("KUBECTX_TEST_FZF_STDIN", stdinRec)
|
||||
t.Setenv("KUBECTX_TEST_FZF_OUT", "ctx-b")
|
||||
|
||||
var stderr bytes.Buffer
|
||||
if err := (InteractiveDeleteOp{}).Run(io.Discard, &stderr); err != nil {
|
||||
t.Fatalf("InteractiveDeleteOp.Run: %v", err)
|
||||
}
|
||||
|
||||
rec, err := os.ReadFile(stdinRec)
|
||||
if err != nil {
|
||||
t.Fatalf("read fzf stdin capture: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(rec), "ctx-a") || !strings.Contains(string(rec), "ctx-b") {
|
||||
t.Fatalf("fzf stdin did not contain context list; got %q", string(rec))
|
||||
}
|
||||
|
||||
// deleteContext should have removed ctx-b
|
||||
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
|
||||
defer kc.Close()
|
||||
if err := kc.Parse(); err != nil {
|
||||
t.Fatalf("re-parse: %v", err)
|
||||
}
|
||||
names, err := kc.ContextNames()
|
||||
if err != nil {
|
||||
t.Fatalf("context names: %v", err)
|
||||
}
|
||||
for _, n := range names {
|
||||
if n == "ctx-b" {
|
||||
t.Errorf("ctx-b should have been deleted; remaining contexts: %v", names)
|
||||
}
|
||||
}
|
||||
if len(names) != 1 || names[0] != "ctx-a" {
|
||||
t.Errorf("expected only ctx-a to remain; got %v", names)
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,14 @@ func (_ ListOp) Run(stdout, stderr io.Writer) error {
|
||||
}
|
||||
return fmt.Errorf("kubeconfig error: %w", err)
|
||||
}
|
||||
return formatContextList(kc, stdout)
|
||||
}
|
||||
|
||||
// formatContextList writes the sorted, current-context-highlighted list of
|
||||
// contexts from kc to w (one per line). The format mirrors what ListOp.Run
|
||||
// prints so that the interactive fzf picker shows the same list the user would
|
||||
// see from `kubectx | fzf`.
|
||||
func formatContextList(kc *kubeconfig.Kubeconfig, w io.Writer) error {
|
||||
ctxs, err := kc.ContextNames()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get context names: %w", err)
|
||||
@@ -57,7 +64,7 @@ func (_ ListOp) Run(stdout, stderr io.Writer) error {
|
||||
if c == cur {
|
||||
s = printer.ActiveItemColor.Sprint(c)
|
||||
}
|
||||
fmt.Fprintf(stdout, "%s\n", s)
|
||||
fmt.Fprintf(w, "%s\n", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ import (
|
||||
)
|
||||
|
||||
// InteractiveReadonlyShellOp launches fzf to pick a context, then starts a readonly shell.
|
||||
type InteractiveReadonlyShellOp struct {
|
||||
SelfCmd string
|
||||
}
|
||||
type InteractiveReadonlyShellOp struct{}
|
||||
|
||||
// ReadonlyShellOp starts a read-only sub-shell for a context.
|
||||
type ReadonlyShellOp struct {
|
||||
@@ -25,7 +23,7 @@ type ReadonlyShellOp struct {
|
||||
}
|
||||
|
||||
func (op InteractiveReadonlyShellOp) Run(_, stderr io.Writer) error {
|
||||
choice, err := fzfPickContext(op.SelfCmd, stderr)
|
||||
choice, err := fzfPickContext(stderr)
|
||||
if err != nil || choice == "" {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ import (
|
||||
)
|
||||
|
||||
// InteractiveShellOp launches fzf to pick a context, then starts an isolated shell.
|
||||
type InteractiveShellOp struct {
|
||||
SelfCmd string
|
||||
}
|
||||
type InteractiveShellOp struct{}
|
||||
|
||||
// ShellOp indicates intention to start a scoped sub-shell for a context.
|
||||
type ShellOp struct {
|
||||
@@ -23,7 +21,7 @@ type ShellOp struct {
|
||||
}
|
||||
|
||||
func (op InteractiveShellOp) Run(_, stderr io.Writer) error {
|
||||
choice, err := fzfPickContext(op.SelfCmd, stderr)
|
||||
choice, err := fzfPickContext(stderr)
|
||||
if err != nil || choice == "" {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func (s *shellSession) run(stderr io.Writer) error {
|
||||
}
|
||||
|
||||
// fzfPickContext launches fzf for interactive context selection.
|
||||
func fzfPickContext(selfCmd string, stderr io.Writer) (string, error) {
|
||||
func fzfPickContext(stderr io.Writer) (string, error) {
|
||||
if err := checkIsolatedMode(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -138,14 +138,18 @@ func fzfPickContext(selfCmd string, stderr io.Writer) (string, error) {
|
||||
return "", errors.New("no contexts found in the kubeconfig file")
|
||||
}
|
||||
|
||||
var in bytes.Buffer
|
||||
if err := formatContextList(kc, &in); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cmd := exec.Command("fzf", "--ansi", "--no-preview")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdin = &in
|
||||
cmd.Stderr = stderr
|
||||
cmd.Stdout = &out
|
||||
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", selfCmd),
|
||||
fmt.Sprintf("%s=1", env.EnvForceColor))
|
||||
if err := cmd.Run(); err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
|
||||
@@ -38,7 +38,7 @@ func parseArgs(argv []string) Op {
|
||||
|
||||
if n == 0 {
|
||||
if cmdutil.IsInteractiveMode(os.Stdout) {
|
||||
return InteractiveSwitchOp{SelfCmd: os.Args[0]}
|
||||
return InteractiveSwitchOp{}
|
||||
}
|
||||
return ListOp{}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,7 @@ import (
|
||||
"github.com/ahmetb/kubectx/internal/printer"
|
||||
)
|
||||
|
||||
type InteractiveSwitchOp struct {
|
||||
SelfCmd string
|
||||
}
|
||||
type InteractiveSwitchOp struct{}
|
||||
|
||||
// TODO(ahmetb) This method is heavily repetitive vs kubectx/fzf.go.
|
||||
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
|
||||
@@ -46,22 +44,21 @@ func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
|
||||
return fmt.Errorf("kubeconfig error: %w", err)
|
||||
}
|
||||
|
||||
ctxNames, err := kc.ContextNames()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get context names: %w", err)
|
||||
var in bytes.Buffer
|
||||
if err := formatNamespaceList(kc, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ctxNames) == 0 {
|
||||
return errors.New("no contexts found in the kubeconfig file")
|
||||
if in.Len() == 0 {
|
||||
return errors.New("no namespaces found")
|
||||
}
|
||||
|
||||
cmd := exec.Command("fzf", "--ansi", "--no-preview")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdin = &in
|
||||
cmd.Stderr = stderr
|
||||
cmd.Stdout = &out
|
||||
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
|
||||
fmt.Sprintf("%s=1", env.EnvForceColor))
|
||||
if err := cmd.Run(); err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
|
||||
125
cmd/kubens/fzf_test.go
Normal file
125
cmd/kubens/fzf_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2021 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ahmetb/kubectx/internal/kubeconfig"
|
||||
)
|
||||
|
||||
func writeTestKubeconfigWithNamespace(t *testing.T) string {
|
||||
t.Helper()
|
||||
const cfg = `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: ctx-a
|
||||
contexts:
|
||||
- name: ctx-a
|
||||
context: { cluster: cluster-a, user: user-a, namespace: ns1 }
|
||||
clusters:
|
||||
- name: cluster-a
|
||||
cluster: { server: https://example.invalid }
|
||||
users:
|
||||
- name: user-a
|
||||
user: { token: fake }
|
||||
`
|
||||
p := filepath.Join(t.TempDir(), "kubeconfig")
|
||||
if err := os.WriteFile(p, []byte(cfg), 0644); err != nil {
|
||||
t.Fatalf("write kubeconfig: %v", err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func installKubensFakeFzf(t *testing.T) {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fake fzf shell script unsupported on windows")
|
||||
}
|
||||
binDir := t.TempDir()
|
||||
const script = `#!/bin/sh
|
||||
cat > "$KUBECTX_TEST_FZF_STDIN"
|
||||
printf '%s\n' "$KUBECTX_TEST_FZF_OUT"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(binDir, "fzf"), []byte(script), 0755); err != nil {
|
||||
t.Fatalf("write fake fzf: %v", err)
|
||||
}
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
}
|
||||
|
||||
func Test_formatNamespaceList(t *testing.T) {
|
||||
t.Setenv("KUBECONFIG", writeTestKubeconfigWithNamespace(t))
|
||||
t.Setenv("_MOCK_NAMESPACES", "1")
|
||||
|
||||
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
|
||||
defer kc.Close()
|
||||
if err := kc.Parse(); err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := formatNamespaceList(kc, &out); err != nil {
|
||||
t.Fatalf("formatNamespaceList: %v", err)
|
||||
}
|
||||
got := out.String()
|
||||
for _, want := range []string{"ns1", "ns2"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("expected list to contain %q; got %q", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInteractiveSwitchOp_pipesNamespaceListToFzfStdin(t *testing.T) {
|
||||
t.Setenv("KUBECONFIG", writeTestKubeconfigWithNamespace(t))
|
||||
t.Setenv("_MOCK_NAMESPACES", "1")
|
||||
installKubensFakeFzf(t)
|
||||
|
||||
stdinRec := filepath.Join(t.TempDir(), "fzf-stdin.txt")
|
||||
t.Setenv("KUBECTX_TEST_FZF_STDIN", stdinRec)
|
||||
// pick ns2 (not the current ns1) so we exercise the switch path
|
||||
t.Setenv("KUBECTX_TEST_FZF_OUT", "ns2")
|
||||
|
||||
var stderr bytes.Buffer
|
||||
if err := (InteractiveSwitchOp{}).Run(io.Discard, &stderr); err != nil {
|
||||
t.Fatalf("InteractiveSwitchOp.Run: %v", err)
|
||||
}
|
||||
|
||||
rec, err := os.ReadFile(stdinRec)
|
||||
if err != nil {
|
||||
t.Fatalf("read fzf stdin capture: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(rec), "ns1") || !strings.Contains(string(rec), "ns2") {
|
||||
t.Fatalf("fzf stdin did not contain namespace list; got %q", string(rec))
|
||||
}
|
||||
|
||||
// switchNamespace should have set ns2 as the active namespace
|
||||
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
|
||||
defer kc.Close()
|
||||
if err := kc.Parse(); err != nil {
|
||||
t.Fatalf("re-parse: %v", err)
|
||||
}
|
||||
ns, err := kc.NamespaceOfContext("ctx-a")
|
||||
if err != nil {
|
||||
t.Fatalf("namespace of context: %v", err)
|
||||
}
|
||||
if ns != "ns2" {
|
||||
t.Errorf("active namespace = %q, want %q", ns, "ns2")
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,14 @@ func (op ListOp) Run(stdout, stderr io.Writer) error {
|
||||
if err := kc.Parse(); err != nil {
|
||||
return fmt.Errorf("kubeconfig error: %w", err)
|
||||
}
|
||||
return formatNamespaceList(kc, stdout)
|
||||
}
|
||||
|
||||
// formatNamespaceList writes the sorted, current-namespace-highlighted list
|
||||
// of namespaces available in the current context to w (one per line). It
|
||||
// mirrors what ListOp.Run prints so the interactive fzf picker shows the same
|
||||
// list the user would see from `kubens | fzf`.
|
||||
func formatNamespaceList(kc *kubeconfig.Kubeconfig, w io.Writer) error {
|
||||
ctx, err := kc.GetCurrentContext()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current context: %w", err)
|
||||
@@ -63,7 +70,7 @@ func (op ListOp) Run(stdout, stderr io.Writer) error {
|
||||
if c == curNs {
|
||||
s = printer.ActiveItemColor.Sprint(c)
|
||||
}
|
||||
fmt.Fprintf(stdout, "%s\n", s)
|
||||
fmt.Fprintf(w, "%s\n", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user