Skip to content

Commit

Permalink
New provider for CAPV resource enumeration
Browse files Browse the repository at this point in the history
This patch adds a new capv_provider() which is used to enumerate the
compute resources for the management/or workload clusters managed by
CAPV.
It also extends the kube_config() directive to accept a CAPI provider
implementation as an input, thus enabling workload cluster kube configs
to be discoverable by the directive and used in the script subsequently.

Also, this patch updates the generation of sshArgs by including the
private key path.

In addition, it also updates the SearchParams struct from the k8s
package to accept the search parameters directly as golang slices.

Adds private key path to ssh args
  • Loading branch information
srm09 committed Jul 17, 2020
1 parent 2bece74 commit 7bfa1a0
Show file tree
Hide file tree
Showing 19 changed files with 477 additions and 199 deletions.
2 changes: 1 addition & 1 deletion cmd/run.go
Expand Up @@ -32,7 +32,7 @@ func newRunCommand() *cobra.Command {
return run(flags)
},
}
cmd.Flags().StringToStringVar(&flags.args, "args", flags.args, "space-separated key=value arguments to passed to diagnostics file")
cmd.Flags().StringToStringVar(&flags.args, "args", flags.args, "comma-separated key=value arguments to pass to the diagnostics file")
cmd.Flags().StringVar(&flags.file, "file", flags.file, "the path to the diagnostics script file to run")
return cmd
}
Expand Down
32 changes: 32 additions & 0 deletions examples/capv_provider.file
@@ -0,0 +1,32 @@
conf = crashd_config(workdir=args.workdir)
ssh_conf = ssh_config(username="capv", private_key_path=args.private_key)
kube_conf = kube_config(path=args.mc_config)

#list out management and workload cluster nodes
wc_provider=capv_provider(
workload_cluster=args.cluster_name,
ssh_config=ssh_conf,
kube_config=kube_conf
)
nodes = resources(provider=wc_provider)

capture(cmd="sudo df -i", resources=nodes)
capture(cmd="sudo crictl info", resources=nodes)
capture(cmd="df -h /var/lib/containerd", resources=nodes)
capture(cmd="sudo systemctl status kubelet", resources=nodes)
capture(cmd="sudo systemctl status containerd", resources=nodes)
capture(cmd="sudo journalctl -xeu kubelet", resources=nodes)

capture(cmd="sudo cat /var/log/cloud-init-output.log", resources=nodes)
capture(cmd="sudo cat /var/log/cloud-init.log", resources=nodes)

#add code to collect pod info from cluster
wc_kube_conf = kube_config(capi_provider = wc_provider)

pod_ns=["default", "kube-system"]

kube_capture(what="logs", namespaces=pod_ns, kube_config=wc_kube_conf)
kube_capture(what="objects", kinds=["pods", "services"], namespaces=pod_ns, kube_config=wc_kube_conf)
kube_capture(what="objects", kinds=["deployments", "replicasets"], groups=["apps"], namespaces=pod_ns, kube_config=wc_kube_conf)

archive(output_file="diagnostics.tar.gz", source_paths=[conf.workdir])
12 changes: 11 additions & 1 deletion k8s/client.go
Expand Up @@ -66,13 +66,23 @@ func New(kubeconfig string) (*Client, error) {
return &Client{Client: client, Disco: disco, CoreRest: restc}, nil
}

func (k8sc *Client) Search(params SearchParams) ([]SearchResult, error) {
return k8sc._search(strings.Join(params.Groups, " "),
strings.Join(params.Kinds, " "),
strings.Join(params.Namespaces, " "),
strings.Join(params.Versions, " "),
strings.Join(params.Names, " "),
strings.Join(params.Labels, " "),
strings.Join(params.Containers, " "))
}

// Search does a drill-down search from group, version, resourceList, to resources. The following rules are applied
// 1) Legacy core group (api/v1) can be specified as "core"
// 2) All specified search params will use AND operator for match (i.e. groups=core AND kinds=pods AND versions=v1 AND ... etc)
// 3) kinds will match resource.Kind or resource.Name
// 4) All search params are passed as comma- or space-separated sets that are matched using OR (i.e. kinds=pods services
// will match resouces of type pods or services)
func (k8sc *Client) Search(groups, kinds, namespaces, versions, names, labels, containers string) ([]SearchResult, error) {
func (k8sc *Client) _search(groups, kinds, namespaces, versions, names, labels, containers string) ([]SearchResult, error) {
// normalize params
groups = strings.ToLower(groups)
kinds = strings.ToLower(kinds)
Expand Down
38 changes: 38 additions & 0 deletions k8s/kube_config.go
@@ -0,0 +1,38 @@
// Copyright (c) 2020 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package k8s

import (
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"os"

"github.com/pkg/errors"
"github.com/vladimirvivien/echo"
)

// FetchWorkloadConfig...
func FetchWorkloadConfig(name, mgmtKubeConfigPath string) (string, error) {
var filePath string
cmdStr := fmt.Sprintf(`kubectl get secrets/%s-kubeconfig --template '{{.data.value}}' --kubeconfig %s`, name, mgmtKubeConfigPath)
p := echo.New().RunProc(cmdStr)
if p.Err() != nil {
return filePath, fmt.Errorf("kubectl get secrets failed: %s: %s", p.Err(), p.Result())
}

f, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("%s-workload-config", name))
if err != nil {
return filePath, errors.Wrap(err, "Cannot create temporary file")
}
filePath = f.Name()
defer f.Close()

base64Dec := base64.NewDecoder(base64.StdEncoding, p.Out())
if _, err := io.Copy(f, base64Dec); err != nil {
return filePath, errors.Wrap(err, "error decoding workload kubeconfig")
}
return filePath, nil
}
63 changes: 63 additions & 0 deletions k8s/nodes.go
@@ -0,0 +1,63 @@
// Copyright (c) 2020 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package k8s

import (
"github.com/pkg/errors"
coreV1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)

func GetNodeAddresses(kubeconfigPath string, labels, names []string) ([]string, error) {
client, err := New(kubeconfigPath)
if err != nil {
return nil, errors.Wrap(err, "could not initialize search client")
}

nodes, err := getNodes(client, names, labels)
if err != nil {
return nil, errors.Wrapf(err, "could not fetch nodes")
}

var nodeIps []string
for _, node := range nodes {
nodeIps = append(nodeIps, getNodeInternalIP(node))
}
return nodeIps, nil
}

func getNodes(k8sc *Client, names, labels []string) ([]*coreV1.Node, error) {
nodeResults, err := k8sc.Search(SearchParams{
Groups: []string{"core"},
Kinds: []string{"nodes"},
Names: names,
Labels: labels,
})
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
}
93 changes: 35 additions & 58 deletions k8s/search_params.go
Expand Up @@ -10,69 +10,46 @@ import (
)

type SearchParams struct {
groups []string
kinds []string
namespaces []string
versions []string
names []string
labels []string
containers []string
Groups []string
Kinds []string
Namespaces []string
Versions []string
Names []string
Labels []string
Containers []string
}

func (sp SearchParams) SetGroups(input []string) {
sp.groups = input
func (sp SearchParams) ContainsGroup(group string) bool {
return contains(sp.Groups, group)
}

func (sp SearchParams) SetKinds(input []string) {
sp.kinds = input
func (sp SearchParams) ContainsVersion(version string) bool {
return contains(sp.Versions, version)
}

func (sp SearchParams) SetNames(input []string) {
sp.names = input
func (sp SearchParams) ContainsKind(kind string) bool {
return contains(sp.Kinds, kind)
}

func (sp SearchParams) SetNamespaces(input []string) {
sp.namespaces = input
func (sp SearchParams) ContainsContainer(container string) bool {
return contains(sp.Containers, container)
}

func (sp SearchParams) SetVersions(input []string) {
sp.versions = input
func (sp SearchParams) ContainsName(name string) bool {
return contains(sp.Names, name)
}

func (sp SearchParams) SetLabels(input []string) {
sp.labels = input
}

func (sp SearchParams) SetContainers(input []string) {
sp.containers = input
}

func (sp SearchParams) Groups() string {
return strings.Join(sp.groups, " ")
}

func (sp SearchParams) Kinds() string {
return strings.Join(sp.kinds, " ")
}

func (sp SearchParams) Names() string {
return strings.Join(sp.names, " ")
}

func (sp SearchParams) Namespaces() string {
return strings.Join(sp.namespaces, " ")
}

func (sp SearchParams) Versions() string {
return strings.Join(sp.versions, " ")
}

func (sp SearchParams) Labels() string {
return strings.Join(sp.labels, " ")
}

func (sp SearchParams) Containers() string {
return strings.Join(sp.containers, " ")
// contains performs a case-insensitive search for the item in the input array
func contains(arr []string, item string) bool {
if len(arr) == 0 {
return false
}
for _, str := range arr {
if strings.ToLower(str) == strings.ToLower(item) {
return true
}
}
return false
}

// TODO: Change this to accept a string dictionary instead
Expand All @@ -99,13 +76,13 @@ func NewSearchParams(p *starlarkstruct.Struct) SearchParams {
containers = parseStructAttr(p, "containers")

return SearchParams{
kinds: kinds,
groups: groups,
names: names,
namespaces: namespaces,
versions: versions,
labels: labels,
containers: containers,
Kinds: kinds,
Groups: groups,
Names: names,
Namespaces: namespaces,
Versions: versions,
Labels: labels,
Containers: containers,
}
}

Expand Down
23 changes: 11 additions & 12 deletions k8s/search_params_test.go
Expand Up @@ -36,8 +36,8 @@ var _ = Describe("SearchParams", func() {
input = starlarkstruct.FromStringDict(starlarkstruct.Default, args)
searchParams = NewSearchParams(input)
Expect(searchParams).To(BeAssignableToTypeOf(SearchParams{}))
Expect(searchParams.kinds).To(HaveLen(1))
Expect(searchParams.Kinds()).To(Equal("deployments"))
Expect(searchParams.Kinds).To(HaveLen(1))
Expect(searchParams.Kinds).To(ConsistOf("deployments"))
})

It("returns a new instance with kinds struct member populated", func() {
Expand All @@ -47,8 +47,8 @@ var _ = Describe("SearchParams", func() {
input = starlarkstruct.FromStringDict(starlarkstruct.Default, args)
searchParams = NewSearchParams(input)
Expect(searchParams).To(BeAssignableToTypeOf(SearchParams{}))
Expect(searchParams.kinds).To(HaveLen(2))
Expect(searchParams.Kinds()).To(Equal("deployments replicasets"))
Expect(searchParams.Kinds).To(HaveLen(2))
Expect(searchParams.Kinds).To(ConsistOf("deployments", "replicasets"))
})
})

Expand All @@ -58,8 +58,7 @@ var _ = Describe("SearchParams", func() {
input = starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{})
searchParams = NewSearchParams(input)
Expect(searchParams).To(BeAssignableToTypeOf(SearchParams{}))
Expect(searchParams.kinds).To(HaveLen(0))
Expect(searchParams.Kinds()).To(Equal(""))
Expect(searchParams.Kinds).To(HaveLen(0))
})
})
})
Expand All @@ -75,8 +74,8 @@ var _ = Describe("SearchParams", func() {
input = starlarkstruct.FromStringDict(starlarkstruct.Default, args)
searchParams = NewSearchParams(input)
Expect(searchParams).To(BeAssignableToTypeOf(SearchParams{}))
Expect(searchParams.namespaces).To(HaveLen(1))
Expect(searchParams.Namespaces()).To(Equal("foo"))
Expect(searchParams.Namespaces).To(HaveLen(1))
Expect(searchParams.Namespaces).To(ConsistOf("foo"))
})

It("returns a new instance with namespaces struct member populated", func() {
Expand All @@ -86,8 +85,8 @@ var _ = Describe("SearchParams", func() {
input = starlarkstruct.FromStringDict(starlarkstruct.Default, args)
searchParams = NewSearchParams(input)
Expect(searchParams).To(BeAssignableToTypeOf(SearchParams{}))
Expect(searchParams.namespaces).To(HaveLen(2))
Expect(searchParams.Namespaces()).To(Equal("foo bar"))
Expect(searchParams.Namespaces).To(HaveLen(2))
Expect(searchParams.Namespaces).To(ConsistOf("foo", "bar"))
})
})

Expand All @@ -97,8 +96,8 @@ var _ = Describe("SearchParams", func() {
input = starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{})
searchParams = NewSearchParams(input)
Expect(searchParams).To(BeAssignableToTypeOf(SearchParams{}))
Expect(searchParams.namespaces).To(HaveLen(1))
Expect(searchParams.Namespaces()).To(Equal("default"))
Expect(searchParams.Namespaces).To(HaveLen(1))
Expect(searchParams.Namespaces).To(ConsistOf("default"))
})
})
})
Expand Down
26 changes: 26 additions & 0 deletions provider/kube_config.go
@@ -0,0 +1,26 @@
// Copyright (c) 2020 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package provider

import (
"fmt"

"github.com/pkg/errors"
"github.com/vmware-tanzu/crash-diagnostics/k8s"
)

// KubeConfig returns the kubeconfig that needs to be used by the provider.
// The path of the management kubeconfig file gets returned if the workload cluster name is empty
func KubeConfig(mgmtKubeConfigPath, workloadClusterName string) (string, error) {
var err error

kubeConfigPath := mgmtKubeConfigPath
if len(workloadClusterName) != 0 {
kubeConfigPath, err = k8s.FetchWorkloadConfig(workloadClusterName, mgmtKubeConfigPath)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("could not fetch kubeconfig for workload cluster %s", workloadClusterName))
}
}
return kubeConfigPath, err
}
2 changes: 1 addition & 1 deletion starlark/archive.go
Expand Up @@ -13,7 +13,7 @@ import (

// archiveFunc is a built-in starlark function that bundles specified directories into
// an arhive format (i.e. tar.gz)
// Starlark format: archive(file_name=<file name> ,source_paths=list)
// Starlark format: archive(output_file=<file name> ,source_paths=list)
func archiveFunc(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var outputFile string
var paths *starlark.List
Expand Down

0 comments on commit 7bfa1a0

Please sign in to comment.