Skip to content

Commit

Permalink
Adds kube_nodes_provider starlark built-in
Browse files Browse the repository at this point in the history
This patch includes the implementation of the kube_nodes_provider
starlark built-in. This provider allows listing of the hosts in a
k8s cluster accessible by the provided/default kube config.
It also includes identification of the provider in the resources
directive, so that the crashd config script can use the
kube_nodes_provider to enumerate the node IP addresses of the hosts to
run crashd commands on.
  • Loading branch information
srm09 committed Jul 6, 2020
1 parent 12e11cc commit 427eece
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 20 deletions.
1 change: 1 addition & 0 deletions k8s/search_params.go
Expand Up @@ -75,6 +75,7 @@ func (sp SearchParams) Containers() string {
return strings.Join(sp.containers, " ")
}

// TODO: Change this to accept a string dictionary instead
func NewSearchParams(p *starlarkstruct.Struct) SearchParams {
var (
kinds []string
Expand Down
4 changes: 2 additions & 2 deletions starlark/kube_get.go
Expand Up @@ -21,11 +21,11 @@ func KubeGetFn(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple

kubeconfig, err := getKubeConfigPath(thread, structVal)
if err != nil {
return nil, errors.Wrap(err, "failed to kubeconfig")
return starlark.None, errors.Wrap(err, "failed to kubeconfig")
}
client, err := k8s.New(kubeconfig)
if err != nil {
return nil, errors.Wrap(err, "could not initialize search client")
return starlark.None, errors.Wrap(err, "could not initialize search client")
}

searchParams := k8s.NewSearchParams(structVal)
Expand Down
121 changes: 121 additions & 0 deletions starlark/kube_nodes_provider.go
@@ -0,0 +1,121 @@
// Copyright (c) 2020 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package starlark

import (
"fmt"

"github.com/pkg/errors"
"github.com/vmware-tanzu/crash-diagnostics/k8s"
coreV1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"

"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
)

// KubeNodesProviderFn is a built-in starlark function that collects compute resources from a k8s cluster
// Starlark format: kube_nodes_provider([kube_config=kube_config(), ssh_config=ssh_config(), names=["foo", "bar], labels=["bar", "baz"]])
func KubeNodesProviderFn(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {

structVal, err := kwargsToStruct(kwargs)
if err != nil {
return starlark.None, err
}

return newKubeNodesProvider(thread, structVal)
}

// newKubeNodesProvider returns a struct with k8s cluster node provider info
func newKubeNodesProvider(thread *starlark.Thread, structVal *starlarkstruct.Struct) (*starlarkstruct.Struct, error) {
kubeconfig, err := getKubeConfigPath(thread, structVal)
if err != nil {
return nil, errors.Wrap(err, "failed to kubeconfig")
}
client, err := k8s.New(kubeconfig)
if err != nil {
return nil, errors.Wrap(err, "could not initialize search client")
}

searchParams := generateSearchParams(structVal)
nodes, err := getNodes(client, searchParams.Names(), searchParams.Labels())
if err != nil {
return nil, errors.Wrapf(err, "could not fetch nodes")
}

// dictionary for node provider struct
kubeNodesProviderDict := starlark.StringDict{
"kind": starlark.String(identifiers.kubeNodesProvider),
"transport": starlark.String("ssh"),
}

// add node info to dictionary
var nodeIps []starlark.Value
for _, node := range nodes {
nodeIps = append(nodeIps, starlark.String(getNodeInternalIP(node)))
}
kubeNodesProviderDict["hosts"] = starlark.NewList(nodeIps)

// add ssh info to dictionary
if _, ok := kubeNodesProviderDict[identifiers.sshCfg]; !ok {
data := thread.Local(identifiers.sshCfg)
sshcfg, ok := data.(*starlarkstruct.Struct)
if !ok {
return nil, fmt.Errorf("%s: default ssh_config not found", identifiers.kubeNodesProvider)
}
kubeNodesProviderDict[identifiers.sshCfg] = sshcfg
}

return starlarkstruct.FromStringDict(starlarkstruct.Default, kubeNodesProviderDict), nil
}

func generateSearchParams(structVal *starlarkstruct.Struct) k8s.SearchParams {
// change nodes key to names
if _, err := structVal.Attr("nodes"); err == nil {
dict := starlark.StringDict{}
structVal.ToStringDict(dict)

dict["names"] = dict["nodes"]
structVal = starlarkstruct.FromStringDict(starlarkstruct.Default, dict)
}
return k8s.NewSearchParams(structVal)
}

func getNodes(k8sc *k8s.Client, names, labels string) ([]*coreV1.Node, error) {
nodeResults, err := k8sc.Search(
"core", // group
"nodes", // kind
"", // namespaces
"", // version
names,
labels,
"", // containers
)
if err != nil {
return nil, err
}

// collate
var nodes []*coreV1.Node
for _, result := range nodeResults {
for _, item := range result.List.Items {
node := new(coreV1.Node)
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, &node); err != nil {
return nil, err
}
nodes = append(nodes, node)
}
}
return nodes, nil
}

func getNodeInternalIP(node *coreV1.Node) (ipAddr string) {
for _, addr := range node.Status.Addresses {
if addr.Type == "InternalIP" {
ipAddr = addr.Address
return
}
}
return
}
68 changes: 68 additions & 0 deletions starlark/kube_nodes_provider_test.go
@@ -0,0 +1,68 @@
// Copyright (c) 2020 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package starlark

import (
"fmt"
"strings"

"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("kube_nodes_provider", func() {
var (
executor *Executor
err error
)

execSetup := func(crashdScript string) error {
executor = New()
return executor.Exec("test.kube.nodes.provider", strings.NewReader(crashdScript))
}

It("returns a struct with the list of k8s nodes", func() {
crashdScript := fmt.Sprintf(`
kube_config(path="%s")
ssh_config(username="uname", private_key_path="path")
provider = kube_nodes_provider()`, k8sconfig)
err = execSetup(crashdScript)
Expect(err).NotTo(HaveOccurred())

data := executor.result["provider"]
Expect(data).NotTo(BeNil())

provider, ok := data.(*starlarkstruct.Struct)
Expect(ok).To(BeTrue())

val, err := provider.Attr("hosts")
Expect(err).NotTo(HaveOccurred())

list := val.(*starlark.List)
Expect(list.Len()).To(Equal(1))
})

It("returns a struct with ssh config", func() {
crashdScript := fmt.Sprintf(`
cfg = kube_config(path="%s")
kube_config(path="/foo/bar")
ssh_config(username="uname", private_key_path="path")
provider = kube_nodes_provider(kube_config=cfg)`, k8sconfig)
err = execSetup(crashdScript)
Expect(err).NotTo(HaveOccurred())

data := executor.result["provider"]
Expect(data).NotTo(BeNil())

provider, ok := data.(*starlarkstruct.Struct)
Expect(ok).To(BeTrue())

sshCfg, err := provider.Attr(identifiers.sshCfg)
Expect(err).NotTo(HaveOccurred())
Expect(sshCfg).NotTo(BeNil())
})
})
2 changes: 1 addition & 1 deletion starlark/resources.go
Expand Up @@ -73,7 +73,7 @@ func enum(provider *starlarkstruct.Struct) (*starlark.List, error) {
kind := trimQuotes(kindVal.String())

switch kind {
case identifiers.hostListProvider:
case identifiers.hostListProvider, identifiers.kubeNodesProvider:
hosts, err := provider.Attr("hosts")
if err != nil {
return nil, fmt.Errorf("hosts not found in %s", identifiers.hostListProvider)
Expand Down
56 changes: 56 additions & 0 deletions starlark/resources_kube_nodes_provider_test.go
@@ -0,0 +1,56 @@
// Copyright (c) 2020 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package starlark

import (
"fmt"
"strings"

"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("resources with kube_nodes_provider()", func() {

It("populates the resources with the cluster nodes as hosts", func() {
crashdScript := fmt.Sprintf(`
cfg = kube_config(path="%s")
ssh_config(username="uname", private_key_path="path")
res = resources(provider=kube_nodes_provider(kube_config=cfg))`, k8sconfig)

executor := New()
err := executor.Exec("test.resources.kube.nodes.provider", strings.NewReader(crashdScript))
Expect(err).NotTo(HaveOccurred())

data := executor.result["res"]
Expect(data).NotTo(BeNil())

resources, ok := data.(*starlark.List)
Expect(ok).To(BeTrue())
Expect(resources.Len()).To(Equal(1))

resStruct, ok := resources.Index(0).(*starlarkstruct.Struct)
Expect(ok).To(BeTrue())

val, err := resStruct.Attr("kind")
Expect(err).NotTo(HaveOccurred())
Expect(trimQuotes(val.String())).To(Equal(identifiers.hostResource))

transport, err := resStruct.Attr("transport")
Expect(err).NotTo(HaveOccurred())
Expect(trimQuotes(transport.String())).To(Equal("ssh"))

sshCfg, err := resStruct.Attr(identifiers.sshCfg)
Expect(err).NotTo(HaveOccurred())
Expect(sshCfg).NotTo(BeNil())

host, err := resStruct.Attr("host")
Expect(err).NotTo(HaveOccurred())
// Regex to match IP address of the host
Expect(trimQuotes(host.String())).To(MatchRegexp("^([1-9]?[0-9]{2}\\.)([0-9]{1,3}\\.){2}[0-9]{1,3}$"))
})
})
27 changes: 14 additions & 13 deletions starlark/starlark_exec.go
Expand Up @@ -69,19 +69,20 @@ func setupLocalDefaults(thread *starlark.Thread) error {
// runing script.
func newPredeclareds() starlark.StringDict {
return starlark.StringDict{
"os": setupOSStruct(),
identifiers.crashdCfg: starlark.NewBuiltin(identifiers.crashdCfg, crashdConfigFn),
identifiers.sshCfg: starlark.NewBuiltin(identifiers.sshCfg, sshConfigFn),
identifiers.hostListProvider: starlark.NewBuiltin(identifiers.hostListProvider, hostListProvider),
identifiers.resources: starlark.NewBuiltin(identifiers.resources, resourcesFunc),
identifiers.run: starlark.NewBuiltin(identifiers.run, runFunc),
identifiers.runLocal: starlark.NewBuiltin(identifiers.runLocal, runLocalFunc),
identifiers.capture: starlark.NewBuiltin(identifiers.capture, captureFunc),
identifiers.captureLocal: starlark.NewBuiltin(identifiers.capture, captureLocalFunc),
identifiers.copyFrom: starlark.NewBuiltin(identifiers.copyFrom, copyFromFunc),
identifiers.kubeCfg: starlark.NewBuiltin(identifiers.kubeCfg, kubeConfigFn),
identifiers.kubeCapture: starlark.NewBuiltin(identifiers.kubeGet, KubeCaptureFn),
identifiers.kubeGet: starlark.NewBuiltin(identifiers.kubeGet, KubeGetFn),
"os": setupOSStruct(),
identifiers.crashdCfg: starlark.NewBuiltin(identifiers.crashdCfg, crashdConfigFn),
identifiers.sshCfg: starlark.NewBuiltin(identifiers.sshCfg, sshConfigFn),
identifiers.hostListProvider: starlark.NewBuiltin(identifiers.hostListProvider, hostListProvider),
identifiers.resources: starlark.NewBuiltin(identifiers.resources, resourcesFunc),
identifiers.run: starlark.NewBuiltin(identifiers.run, runFunc),
identifiers.runLocal: starlark.NewBuiltin(identifiers.runLocal, runLocalFunc),
identifiers.capture: starlark.NewBuiltin(identifiers.capture, captureFunc),
identifiers.captureLocal: starlark.NewBuiltin(identifiers.capture, captureLocalFunc),
identifiers.copyFrom: starlark.NewBuiltin(identifiers.copyFrom, copyFromFunc),
identifiers.kubeCfg: starlark.NewBuiltin(identifiers.kubeCfg, kubeConfigFn),
identifiers.kubeCapture: starlark.NewBuiltin(identifiers.kubeGet, KubeCaptureFn),
identifiers.kubeGet: starlark.NewBuiltin(identifiers.kubeGet, KubeGetFn),
identifiers.kubeNodesProvider: starlark.NewBuiltin(identifiers.kubeNodesProvider, KubeNodesProviderFn),
}
}

Expand Down
10 changes: 6 additions & 4 deletions starlark/support.go
Expand Up @@ -39,8 +39,9 @@ var (
captureLocal string
copyFrom string

kubeCapture string
kubeGet string
kubeCapture string
kubeGet string
kubeNodesProvider string
}{
crashdCfg: "crashd_config",
kubeCfg: "kube_config",
Expand All @@ -62,8 +63,9 @@ var (
captureLocal: "capture_local",
copyFrom: "copy_from",

kubeCapture: "kube_capture",
kubeGet: "kube_get",
kubeCapture: "kube_capture",
kubeGet: "kube_get",
kubeNodesProvider: "kube_nodes_provider",
}

defaults = struct {
Expand Down

0 comments on commit 427eece

Please sign in to comment.