Skip to content

Commit

Permalink
Support for cluster context name in kube_config function (#122)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
vladimirvivien committed Aug 13, 2021
1 parent c84da5c commit a31dd61
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 35 deletions.
4 changes: 3 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
39 changes: 33 additions & 6 deletions k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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, " "),
Expand Down
50 changes: 50 additions & 0 deletions k8s/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion starlark/capa_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
2 changes: 1 addition & 1 deletion starlark/capv_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
6 changes: 4 additions & 2 deletions starlark/kube_capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
13 changes: 7 additions & 6 deletions starlark/kube_capture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
27 changes: 22 additions & 5 deletions starlark/kube_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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()
}
3 changes: 0 additions & 3 deletions starlark/kube_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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())
Expand Down Expand Up @@ -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())
Expand Down
7 changes: 4 additions & 3 deletions starlark/kube_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
13 changes: 7 additions & 6 deletions starlark/kube_get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion starlark/kube_nodes_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
4 changes: 4 additions & 0 deletions testing/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit a31dd61

Please sign in to comment.