From a31dd61be4316f440106c973868ed7ead6b399bd Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Thu, 12 Aug 2021 12:12:42 -0400 Subject: [PATCH] Support for cluster context name in kube_config function (#122) This patch introduces support for cluster context name when declaring kubeernetes configuration using the kube_config starlark script function. Now, command functions such as kube_get and kube_capture can create connection to the API server using the local kubectl CLI cluster context name. Signed-off-by: Vladimir Vivien --- docs/README.md | 4 ++- k8s/client.go | 39 +++++++++++++++++++++---- k8s/client_test.go | 50 +++++++++++++++++++++++++++++++++ starlark/capa_provider.go | 2 +- starlark/capv_provider.go | 2 +- starlark/kube_capture.go | 6 ++-- starlark/kube_capture_test.go | 13 +++++---- starlark/kube_config.go | 27 ++++++++++++++---- starlark/kube_config_test.go | 3 -- starlark/kube_get.go | 7 +++-- starlark/kube_get_test.go | 13 +++++---- starlark/kube_nodes_provider.go | 2 +- testing/setup.go | 4 +++ 13 files changed, 137 insertions(+), 35 deletions(-) diff --git a/docs/README.md b/docs/README.md index 965bef59..9c8197c9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -145,6 +145,7 @@ This configuration function declares and stores configuration needed to connect | Param | Description | Required | | -------- | -------- | ------- | | `path` | Path to the local Kubernetes config file. Default: `$HOME/.kube/config`| No | +| `cluster_context` | The name of a context to use when accessing the cluster. Default: (empty) | No | | `capi_provider` | A Cluster-API provider (see providers below) to obtain Kubernetes configurations | No | #### Output @@ -153,11 +154,12 @@ This configuration function declares and stores configuration needed to connect | Field | Description | | --------| --------- | | `path` | The path to the local Kubernetes config that was set | +| `cluster_context` | The name of a context that was set for the cluster | | `capi_provider`|A provider that was set for Cluster-API usage| #### Example ```python -kube_config(path=args.kube_conf) +kube_config(path=args.kube_conf, cluster_context="my-cluster") ``` ### `ssh_config()` This function creates configuration that can be used to connect via SSH to remote machines. diff --git a/k8s/client.go b/k8s/client.go index 397e950b..6d5569c0 100644 --- a/k8s/client.go +++ b/k8s/client.go @@ -36,11 +36,17 @@ type Client struct { JsonPrinter printers.JSONPrinter } -// New returns a *Client -func New(kubeconfig string) (*Client, error) { +// New returns a *Client built with the kubecontext file path +// and an optional (at most one) K8s CLI context name. +func New(kubeconfig string, clusterContextOptions ...string) (*Client, error) { + var clusterCtxName string + if len(clusterContextOptions) > 0 { + clusterCtxName = clusterContextOptions[0] + } + // creating cfg for each client type because each - // setup its own cfg default which may not be compatible - dynCfg, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + // setup needs its own cfg default which may not be compatible + dynCfg, err := makeRESTConfig(kubeconfig, clusterCtxName) if err != nil { return nil, err } @@ -49,7 +55,7 @@ func New(kubeconfig string) (*Client, error) { return nil, err } - discoCfg, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + discoCfg, err := makeRESTConfig(kubeconfig, clusterCtxName) if err != nil { return nil, err } @@ -64,7 +70,7 @@ func New(kubeconfig string) (*Client, error) { } mapper := restmapper.NewDiscoveryRESTMapper(resources) - restCfg, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + restCfg, err := makeRESTConfig(kubeconfig, clusterCtxName) if err != nil { return nil, err } @@ -77,6 +83,27 @@ func New(kubeconfig string) (*Client, error) { return &Client{Client: client, Disco: disco, CoreRest: restc, Mapper: mapper}, nil } +// makeRESTConfig creates a new *rest.Config with a k8s context name if one is provided. +func makeRESTConfig(fileName, contextName string) (*rest.Config, error) { + if fileName == "" { + return nil, fmt.Errorf("kubeconfig file path required") + } + + if contextName != "" { + // create the config object from k8s config path and context + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: fileName}, + &clientcmd.ConfigOverrides{ + CurrentContext: contextName, + }).ClientConfig() + } + + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: fileName}, + &clientcmd.ConfigOverrides{}, + ).ClientConfig() +} + func (k8sc *Client) Search(ctx context.Context, params SearchParams) ([]SearchResult, error) { return k8sc._search(ctx, strings.Join(params.Groups, " "), strings.Join(params.Categories, " "), diff --git a/k8s/client_test.go b/k8s/client_test.go index fd0c8cdc..3c8ea269 100644 --- a/k8s/client_test.go +++ b/k8s/client_test.go @@ -8,6 +8,56 @@ import ( "testing" ) +func TestClientNew(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + { + name: "client with no cluster context", + test: func(t *testing.T) { + client, err := New(support.KindKubeConfigFile()) + if err != nil { + t.Fatal(err) + } + results, err := client.Search(context.TODO(), SearchParams{Kinds: []string{"pods"}}) + if err != nil { + t.Fatal(err) + } + count := 0 + for _, result := range results { + count = len(result.List.Items) + count + } + t.Logf("found %d objects", count) + }, + }, + { + name: "client with cluster context", + test: func(t *testing.T) { + client, err := New(support.KindKubeConfigFile(), support.KindClusterContextName()) + if err != nil { + t.Fatal(err) + } + results, err := client.Search(context.TODO(), SearchParams{Kinds: []string{"pods"}}) + if err != nil { + t.Fatal(err) + } + count := 0 + for _, result := range results { + count = len(result.List.Items) + count + } + t.Logf("found %d objects", count) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.test(t) + }) + } +} + func TestClient_Search(t *testing.T) { tests := []struct { name string diff --git a/starlark/capa_provider.go b/starlark/capa_provider.go index 9c12d9ff..cb0f45dc 100644 --- a/starlark/capa_provider.go +++ b/starlark/capa_provider.go @@ -47,7 +47,7 @@ func CapaProviderFn(thread *starlark.Thread, _ *starlark.Builtin, args starlark. if mgmtKubeConfig == nil { mgmtKubeConfig = thread.Local(identifiers.kubeCfg).(*starlarkstruct.Struct) } - mgmtKubeConfigPath, err := getKubeConfigFromStruct(mgmtKubeConfig) + mgmtKubeConfigPath, err := getKubeConfigPathFromStruct(mgmtKubeConfig) if err != nil { return starlark.None, errors.Wrap(err, "failed to extract management kubeconfig") } diff --git a/starlark/capv_provider.go b/starlark/capv_provider.go index 861ceaed..22c5af30 100644 --- a/starlark/capv_provider.go +++ b/starlark/capv_provider.go @@ -47,7 +47,7 @@ func CapvProviderFn(thread *starlark.Thread, _ *starlark.Builtin, args starlark. if mgmtKubeConfig == nil { mgmtKubeConfig = thread.Local(identifiers.kubeCfg).(*starlarkstruct.Struct) } - mgmtKubeConfigPath, err := getKubeConfigFromStruct(mgmtKubeConfig) + mgmtKubeConfigPath, err := getKubeConfigPathFromStruct(mgmtKubeConfig) if err != nil { return starlark.None, errors.Wrap(err, "failed to extract management kubeconfig") } diff --git a/starlark/kube_capture.go b/starlark/kube_capture.go index bbfa3732..e50494e7 100644 --- a/starlark/kube_capture.go +++ b/starlark/kube_capture.go @@ -47,11 +47,13 @@ func KubeCaptureFn(thread *starlark.Thread, _ *starlark.Builtin, args starlark.T if kubeConfig == nil { kubeConfig = thread.Local(identifiers.kubeCfg).(*starlarkstruct.Struct) } - path, err := getKubeConfigFromStruct(kubeConfig) + path, err := getKubeConfigPathFromStruct(kubeConfig) if err != nil { return starlark.None, errors.Wrap(err, "failed to kubeconfig") } - client, err := k8s.New(path) + clusterCtxName := getKubeConfigContextNameFromStruct(kubeConfig) + + client, err := k8s.New(path, clusterCtxName) if err != nil { return starlark.None, errors.Wrap(err, "could not initialize search client") } diff --git a/starlark/kube_capture_test.go b/starlark/kube_capture_test.go index 310172fd..16655075 100644 --- a/starlark/kube_capture_test.go +++ b/starlark/kube_capture_test.go @@ -346,6 +346,7 @@ func TestKubeCapture(t *testing.T) { func TestKubeCaptureScript(t *testing.T) { workdir := testSupport.TmpDirRoot() k8sconfig := testSupport.KindKubeConfigFile() + clusterCtxName := testSupport.KindClusterContextName() execute := func(t *testing.T, script string) *starlarkstruct.Struct { executor := New() @@ -369,11 +370,11 @@ func TestKubeCaptureScript(t *testing.T) { eval func(t *testing.T, script string) }{ { - name: "simple search with namespaced objects", + name: "simple search with namespaced objects with cluster context", script: fmt.Sprintf(` crashd_config(workdir="%s") -set_defaults(kube_config(path="%s")) -kube_data = kube_capture(what="objects", groups=["core"], kinds=["services"], namespaces=["default", "kube-system"])`, workdir, k8sconfig), +set_defaults(kube_config(path="%s", cluster_context="%s")) +kube_data = kube_capture(what="objects", groups=["core"], kinds=["services"], namespaces=["default", "kube-system"])`, workdir, k8sconfig, clusterCtxName), eval: func(t *testing.T, script string) { data := execute(t, script) @@ -511,11 +512,11 @@ kube_data = kube_capture(what="objects", groups=["core"], categories=["all"], na }, }, { - name: "search for all logs in a namespace", + name: "search for all logs in a namespace with cluster context", script: fmt.Sprintf(` crashd_config(workdir="%s") -set_defaults(kube_config(path="%s")) -kube_data = kube_capture(what="logs", namespaces=["kube-system"])`, workdir, k8sconfig), +set_defaults(kube_config(path="%s", cluster_context="%s")) +kube_data = kube_capture(what="logs", namespaces=["kube-system"])`, workdir, k8sconfig, clusterCtxName), eval: func(t *testing.T, script string) { data := execute(t, script) diff --git a/starlark/kube_config.go b/starlark/kube_config.go index 6f32d717..8b424b10 100644 --- a/starlark/kube_config.go +++ b/starlark/kube_config.go @@ -14,13 +14,14 @@ import ( // KubeConfigFn is built-in starlark function that wraps the kwargs into a dictionary value. // The result is also added to the thread for other built-in to access. -// Starlark: kube_config(path=kubecf/path) +// Starlark: kube_config(path=kubecf/path, [cluster_context=context_name]) func KubeConfigFn(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { - var path string + var path, clusterCtxName string var provider *starlarkstruct.Struct if err := starlark.UnpackArgs( identifiers.kubeCfg, args, kwargs, + "cluster_context?", &clusterCtxName, "path?", &path, "capi_provider?", &provider, ); err != nil { @@ -58,7 +59,8 @@ func KubeConfigFn(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, } structVal := starlarkstruct.FromStringDict(starlark.String(identifiers.kubeCfg), starlark.StringDict{ - "path": starlark.String(path), + "cluster_context": starlark.String(clusterCtxName), + "path": starlark.String(path), }) return structVal, nil @@ -79,14 +81,29 @@ func addDefaultKubeConf(thread *starlark.Thread) error { return nil } -func getKubeConfigFromStruct(kubeConfigStructVal *starlarkstruct.Struct) (string, error) { +func getKubeConfigPathFromStruct(kubeConfigStructVal *starlarkstruct.Struct) (string, error) { kvPathVal, err := kubeConfigStructVal.Attr("path") if err != nil { return "", errors.Wrap(err, "failed to extract kubeconfig path") } kvPathStrVal, ok := kvPathVal.(starlark.String) if !ok { - return "", errors.New("failed to extract management kubeconfig") + return "", errors.New("failed to extract kubeconfig") } return kvPathStrVal.GoString(), nil } + +// getKubeConfigContextNameFromStruct returns the cluster name from the KubeConfig struct +// provided. If filed cluster_context not provided or unable to convert, it is returned +// as an empty context. +func getKubeConfigContextNameFromStruct(kubeConfigStructVal *starlarkstruct.Struct) string { + ctxVal, err := kubeConfigStructVal.Attr("cluster_context") + if err != nil { + return "" + } + ctxName, ok := ctxVal.(starlark.String) + if !ok { + return "" + } + return ctxName.GoString() +} diff --git a/starlark/kube_config_test.go b/starlark/kube_config_test.go index 0a8fd047..838fee30 100644 --- a/starlark/kube_config_test.go +++ b/starlark/kube_config_test.go @@ -44,7 +44,6 @@ var _ = Describe("kube_config", func() { Expect(kubeConfigData).To(BeAssignableToTypeOf(&starlarkstruct.Struct{})) cfg, _ := kubeConfigData.(*starlarkstruct.Struct) - Expect(cfg.AttrNames()).To(HaveLen(1)) val, err := cfg.Attr("path") Expect(err).To(BeNil()) @@ -66,7 +65,6 @@ var _ = Describe("kube_config", func() { Expect(kubeConfigData).NotTo(BeNil()) cfg, _ := kubeConfigData.(*starlarkstruct.Struct) - Expect(cfg.AttrNames()).To(HaveLen(1)) val, err := cfg.Attr("path") Expect(err).To(BeNil()) @@ -111,7 +109,6 @@ var _ = Describe("KubeConfigFn", func() { Expect(err).NotTo(HaveOccurred()) cfg, _ := val.(*starlarkstruct.Struct) - Expect(cfg.AttrNames()).To(HaveLen(1)) path, err := cfg.Attr("path") Expect(err).To(BeNil()) diff --git a/starlark/kube_get.go b/starlark/kube_get.go index cc318d81..adc53cb0 100644 --- a/starlark/kube_get.go +++ b/starlark/kube_get.go @@ -42,12 +42,13 @@ func KubeGetFn(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple if kubeConfig == nil { kubeConfig = thread.Local(identifiers.kubeCfg).(*starlarkstruct.Struct) } - path, err := getKubeConfigFromStruct(kubeConfig) + path, err := getKubeConfigPathFromStruct(kubeConfig) if err != nil { - return starlark.None, errors.Wrap(err, "failed to kubeconfig") + return starlark.None, errors.Wrap(err, "failed to get kubeconfig") } + clusterCtxName := getKubeConfigContextNameFromStruct(kubeConfig) - client, err := k8s.New(path) + client, err := k8s.New(path, clusterCtxName) if err != nil { return starlark.None, errors.Wrap(err, "could not initialize search client") } diff --git a/starlark/kube_get_test.go b/starlark/kube_get_test.go index e941c075..7119a34d 100644 --- a/starlark/kube_get_test.go +++ b/starlark/kube_get_test.go @@ -149,6 +149,7 @@ func TestKubeGet(t *testing.T) { func TestKubeGetScript(t *testing.T) { k8sconfig := testSupport.KindKubeConfigFile() + clusterName := testSupport.KindClusterContextName() execute := func(t *testing.T, script string) *starlarkstruct.Struct { executor := New() @@ -172,11 +173,11 @@ func TestKubeGetScript(t *testing.T) { eval func(t *testing.T, script string) }{ { - name: "namespaced objects as starlark objects", + name: "namespaced objects as starlark objects with context", script: fmt.Sprintf(` -set_defaults(kube_config(path="%s")) +set_defaults(kube_config(path="%s", cluster_context="%s")) kube_data = kube_get(groups=["core"], kinds=["services"], namespaces=["default", "kube-system"]) -`, k8sconfig), +`, k8sconfig, clusterName), eval: func(t *testing.T, script string) { data := execute(t, script) @@ -234,11 +235,11 @@ kube_data = kube_get(groups=["core"], kinds=["nodes"]) }, }, { - name: "different categories of objects as starlark objects", + name: "different categories of objects as starlark objects with context", script: fmt.Sprintf(` -set_defaults(kube_config(path="%s")) +set_defaults(kube_config(path="%s", cluster_context="%s")) kube_data = kube_get(categories=["all"]) -`, k8sconfig), +`, k8sconfig, clusterName), eval: func(t *testing.T, script string) { data := execute(t, script) diff --git a/starlark/kube_nodes_provider.go b/starlark/kube_nodes_provider.go index bdfec458..1eb596ca 100644 --- a/starlark/kube_nodes_provider.go +++ b/starlark/kube_nodes_provider.go @@ -39,7 +39,7 @@ func KubeNodesProviderFn(thread *starlark.Thread, _ *starlark.Builtin, args star if kubeConfig == nil { kubeConfig = thread.Local(identifiers.kubeCfg).(*starlarkstruct.Struct) } - path, err := getKubeConfigFromStruct(kubeConfig) + path, err := getKubeConfigPathFromStruct(kubeConfig) if err != nil { return starlark.None, errors.Wrap(err, "failed to kubeconfig") } diff --git a/testing/setup.go b/testing/setup.go index 7c8b81a3..88febf71 100644 --- a/testing/setup.go +++ b/testing/setup.go @@ -200,6 +200,10 @@ func (t *TestSupport) KindKubeConfigFile() string { return t.kindKubeCfg } +func (t *TestSupport) KindClusterContextName() string { + return t.kindCluster.GetKubeCtlContext() +} + func (t *TestSupport) TearDown() error { var errs []error