Move kubeconfig loader utils to cmdutil pkg

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
This commit is contained in:
Ahmet Alp Balkan 2020-04-18 13:32:53 -07:00
parent 1982becb15
commit c4252b5795
No known key found for this signature in database
GPG Key ID: 441833503E604E2C
19 changed files with 191 additions and 65 deletions

View File

@ -6,6 +6,7 @@ import (
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
)
@ -13,7 +14,7 @@ import (
type CurrentOp struct{}
func (_op CurrentOp) Run(stdout, _ io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader)
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")

View File

@ -5,6 +5,7 @@ import (
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
@ -35,7 +36,7 @@ func (op DeleteOp) Run(_, stderr io.Writer) error {
// deleteContext deletes a context entry by NAME or current-context
// indicated by ".".
func deleteContext(name string) (deleteName string, wasActiveContext bool, err error) {
kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader)
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return deleteName, false, errors.Wrap(err, "kubeconfig error")

View File

@ -20,7 +20,7 @@ func (op UnsupportedOp) Run(_, _ io.Writer) error {
// and decides which operation should be taken.
func parseArgs(argv []string) Op {
if len(argv) == 0 {
if env.IsInteractiveMode(os.Stdout) {
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveSwitchOp{SelfCmd: os.Args[0]}
}
return ListOp{}

View File

@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
@ -21,9 +22,9 @@ type InteractiveSwitchOp struct {
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
// parse kubeconfig just to see if it can be loaded
kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader)
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader)
if err := kc.Parse(); err != nil {
if isENOENT(err) {
if cmdutil.IsNotFoundErr(err) {
printer.Warning(stderr, "kubeconfig file not found")
return nil
}

View File

@ -3,12 +3,12 @@ package main
import (
"fmt"
"io"
"os"
"facette.io/natsort"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
@ -17,10 +17,10 @@ import (
type ListOp struct{}
func (_ ListOp) Run(stdout, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader)
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
if isENOENT(err) {
if cmdutil.IsNotFoundErr(err) {
printer.Warning(stderr, "kubeconfig file not found")
return nil
}
@ -50,14 +50,3 @@ func (_ ListOp) Run(stdout, stderr io.Writer) error {
}
return nil
}
// isENOENT determines if the underlying error is os.IsNotExist. Right now
// errors from github.com/pkg/errors doesn't work with os.IsNotExist.
func isENOENT(err error) bool {
for e := err; e != nil; e = errors.Unwrap(e) {
if os.IsNotExist(e) {
return true
}
}
return false
}

View File

@ -6,6 +6,7 @@ import (
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
@ -34,7 +35,7 @@ func parseRenameSyntax(v string) (string, string, bool) {
// to the "new" value. If the old refers to the current-context,
// current-context preference is also updated.
func (op RenameOp) Run(_, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader)
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")

View File

@ -6,10 +6,12 @@ import (
"path/filepath"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
)
func kubectxPrevCtxFile() (string, error) {
home := homeDir()
home := cmdutil.HomeDir()
if home == "" {
return "", errors.New("HOME or USERPROFILE environment variable not set")
}

View File

@ -5,6 +5,8 @@ import (
"os"
"path/filepath"
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
)
func Test_readLastContext_nonExistingFile(t *testing.T) {
@ -18,7 +20,7 @@ func Test_readLastContext_nonExistingFile(t *testing.T) {
}
func Test_readLastContext(t *testing.T) {
path, cleanup := testfile(t, "foo")
path, cleanup := testutil.TempFile(t, "foo")
defer cleanup()
s, err := readLastContext(path)
@ -86,3 +88,4 @@ func Test_kubectxFilePath_error(t *testing.T) {
t.Fatal(err)
}
}

View File

@ -5,6 +5,7 @@ import (
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
@ -36,7 +37,7 @@ func switchContext(name string) (string, error) {
return "", errors.Wrap(err, "failed to determine state file")
}
kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader)
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return "", errors.Wrap(err, "kubeconfig error")

View File

@ -5,6 +5,7 @@ import (
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
@ -13,7 +14,7 @@ import (
type UnsetOp struct{}
func (_ UnsetOp) Run(_, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader)
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")

View File

@ -1,9 +1,32 @@
package main
import "io"
import (
"fmt"
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
)
type CurrentOp struct{}
func (c CurrentOp) Run(stdout, stderr io.Writer) error {
panic("implement me")
func (c CurrentOp) Run(stdout, _ io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
}
ctx := kc.GetCurrentContext()
if ctx == "" {
return errors.New("current-context is not set")
}
ns, err := kc.NamespaceOfContext(ctx)
if err != nil {
return errors.Wrapf(err, "failed to read namespace of %q", ctx)
}
_, err = fmt.Fprintln(stdout, ns)
return errors.Wrap(err, "write error")
}

View File

@ -1,4 +1,4 @@
package env
package cmdutil
import (
"os"

View File

@ -1,4 +1,4 @@
package main
package cmdutil
import (
"os"
@ -10,21 +10,13 @@ import (
)
var (
defaultLoader kubeconfig.Loader = new(StandardKubeconfigLoader)
DefaultLoader kubeconfig.Loader = new(StandardKubeconfigLoader)
)
type StandardKubeconfigLoader struct{}
type kubeconfigFile struct{ *os.File }
func (kf *kubeconfigFile) Reset() error {
if err := kf.Truncate(0); err != nil {
return errors.Wrap(err, "failed to truncate file")
}
_, err := kf.Seek(0, 0)
return errors.Wrap(err, "failed to seek in file")
}
func (*StandardKubeconfigLoader) Load() (kubeconfig.ReadWriteResetCloser, error) {
cfgPath, err := kubeconfigPath()
if err != nil {
@ -40,6 +32,14 @@ func (*StandardKubeconfigLoader) Load() (kubeconfig.ReadWriteResetCloser, error)
return &kubeconfigFile{f}, nil
}
func (kf *kubeconfigFile) Reset() error {
if err := kf.Truncate(0); err != nil {
return errors.Wrap(err, "failed to truncate file")
}
_, err := kf.Seek(0, 0)
return errors.Wrap(err, "failed to seek in file")
}
func kubeconfigPath() (string, error) {
// KUBECONFIG env var
if v := os.Getenv("KUBECONFIG"); v != "" {
@ -52,14 +52,14 @@ func kubeconfigPath() (string, error) {
}
// default path
home := homeDir()
home := HomeDir()
if home == "" {
return "", errors.New("HOME or USERPROFILE environment variable not set")
}
return filepath.Join(home, ".kube", "config"), nil
}
func homeDir() string {
func HomeDir() string {
if v := os.Getenv("XDG_CACHE_HOME"); v != "" {
return v
}
@ -69,3 +69,14 @@ func homeDir() string {
}
return home
}
// IsNotFoundErr determines if the underlying error is os.IsNotExist. Right now
// errors from github.com/pkg/errors doesn't work with os.IsNotExist.
func IsNotFoundErr(err error) bool {
for e := err; e != nil; e = errors.Unwrap(e) {
if os.IsNotExist(e) {
return true
}
}
return false
}

View File

@ -1,7 +1,6 @@
package main
package cmdutil
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -61,7 +60,7 @@ func Test_homeDir(t *testing.T) {
unsets = append(unsets, testutil.WithEnvVar(e.k, e.v))
}
got := homeDir()
got := HomeDir()
if got != c.want {
t.Errorf("expected:%q got:%q", c.want, got)
}
@ -120,30 +119,12 @@ func Test_kubeconfigPath_envOvverideDoesNotSupportPathSeparator(t *testing.T) {
func TestStandardKubeconfigLoader_returnsNotFoundErr(t *testing.T) {
defer testutil.WithEnvVar("KUBECONFIG", "foo")()
kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader)
kc := new(kubeconfig.Kubeconfig).WithLoader(DefaultLoader)
err := kc.Parse()
if err == nil {
t.Fatal("expected err")
}
if !isENOENT(err) {
if !IsNotFoundErr(err) {
t.Fatalf("expected ENOENT error; got=%v", err)
}
}
func testfile(t *testing.T, contents string) (path string, cleanup func()) {
t.Helper()
f, err := ioutil.TempFile(os.TempDir(), "test-file")
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
path = f.Name()
if _, err := f.Write([]byte(contents)); err != nil {
t.Fatalf("failed to write to test file: %v", err)
}
return path, func() {
f.Close()
os.Remove(path)
}
}

View File

@ -1 +0,0 @@
package kubeconfig

View File

@ -0,0 +1,39 @@
package kubeconfig
import (
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
const (
defaultNamespace = "default"
)
func (k *Kubeconfig) contextNode(name string) (*yaml.Node, error) {
contexts := valueOf(k.rootNode, "contexts")
if contexts == nil {
return nil, errors.New("\"contexts\" entry is nil")
} else if contexts.Kind != yaml.SequenceNode {
return nil, errors.New("\"contexts\" is not a sequence node")
}
for _, contextNode := range contexts.Content {
nameNode := valueOf(contextNode, "name")
if nameNode.Kind == yaml.ScalarNode && nameNode.Value == name {
return contextNode, nil
}
}
return nil, errors.Errorf("context with name %q not found", name)
}
func (k *Kubeconfig) NamespaceOfContext(contextName string) (string, error) {
ctx, err := k.contextNode(contextName)
if err != nil {
return "", err
}
ns := valueOf(ctx, "namespace")
if ns == nil || ns.Value == "" {
return defaultNamespace, nil
}
return ns.Value, nil
}

View File

@ -0,0 +1,46 @@
package kubeconfig
import (
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
)
func TestKubeconfig_NamespaceOfContext_ctxNotFound(t *testing.T) {
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
WithCtxs(testutil.Ctx("c1")).ToYAML(t)))
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
_, err := kc.NamespaceOfContext("c2")
if err == nil {
t.Fatal("expected err")
}
}
func TestKubeconfig_NamespaceOfContext(t *testing.T) {
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
WithCtxs(
testutil.Ctx("c1"),
testutil.Ctx("c2").Ns("c2n1")).ToYAML(t)))
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
v1, err := kc.NamespaceOfContext("c1")
if err != nil {
t.Fatal("expected err")
}
if expected := `default`; v1 != expected {
t.Fatalf("c1: expected=%q got=%q", expected, v1)
}
v2, err := kc.NamespaceOfContext("c2")
if err != nil {
t.Fatal("expected err")
}
if expected := `c2n1`; v2 != expected {
t.Fatalf("c2: expected=%q got=%q", expected, v2)
}
}

View File

@ -12,7 +12,8 @@ type Context struct {
Namespace string `yaml:"namespace,omitempty"`
}
func Ctx(name string) *Context { return &Context{Name: name} }
func Ctx(name string) *Context { return &Context{Name: name} }
func (c *Context) Ns(ns string) *Context { c.Namespace = ns; return c }
type Kubeconfig map[string]interface{}

View File

@ -0,0 +1,26 @@
package testutil
import (
"io/ioutil"
"os"
"testing"
)
func TempFile(t *testing.T, contents string) (path string, cleanup func()) {
// TODO consider removing, used only in one place.
t.Helper()
f, err := ioutil.TempFile(os.TempDir(), "test-file")
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
path = f.Name()
if _, err := f.Write([]byte(contents)); err != nil {
t.Fatalf("failed to write to test file: %v", err)
}
return path, func() {
f.Close()
os.Remove(path)
}
}