diff --git a/go.mod b/go.mod index ee6e02b0..13f3d11d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/imdario/mergo v0.3.7 // indirect github.com/onsi/ginkgo v1.10.1 github.com/onsi/gomega v1.7.0 + github.com/pkg/errors v0.8.0 github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.5 github.com/vladimirvivien/echo v0.0.1-alpha.4 diff --git a/go.sum b/go.sum index 17f75ef8..30a2ab56 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,7 @@ github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/k8s/client.go b/k8s/client.go index 58f85098..5b9f2aa4 100644 --- a/k8s/client.go +++ b/k8s/client.go @@ -31,16 +31,6 @@ type Client struct { JsonPrinter printers.JSONPrinter } -type SearchResult struct { - ListKind string - ResourceName string - ResourceKind string - GroupVersionResource schema.GroupVersionResource - List *unstructured.UnstructuredList - Namespaced bool - Namespace string -} - // New returns a *Client func New(kubeconfig string) (*Client, error) { // creating cfg for each client type because each diff --git a/k8s/container.go b/k8s/container.go new file mode 100644 index 00000000..8427da16 --- /dev/null +++ b/k8s/container.go @@ -0,0 +1,49 @@ +package k8s + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +func GetContainers(podItem unstructured.Unstructured) ([]Container, error) { + var containers []Container + coreContainers, err := _getPodContainers(podItem) + if err != nil { + return containers, err + } + + for _, c := range coreContainers { + containers = append(containers, NewContainerLogger(podItem.GetNamespace(), podItem.GetName(), c)) + } + return containers, nil +} + +func _getPodContainers(podItem unstructured.Unstructured) ([]corev1.Container, error) { + var containers []corev1.Container + + pod := new(corev1.Pod) + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(podItem.Object, &pod); err != nil { + return nil, fmt.Errorf("error converting container objects: %s", err) + } + + for _, c := range pod.Spec.InitContainers { + containers = append(containers, c) + } + + for _, c := range pod.Spec.Containers { + containers = append(containers, c) + } + containers = append(containers, _getPodEphemeralContainers(pod)...) + return containers, nil +} + +func _getPodEphemeralContainers(pod *corev1.Pod) []corev1.Container { + var containers []corev1.Container + for _, ec := range pod.Spec.EphemeralContainers { + containers = append(containers, corev1.Container(ec.EphemeralContainerCommon)) + } + return containers +} diff --git a/k8s/container_logger.go b/k8s/container_logger.go new file mode 100644 index 00000000..33c62ef1 --- /dev/null +++ b/k8s/container_logger.go @@ -0,0 +1,64 @@ +package k8s + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" +) + +type ContainerLogsImpl struct { + namespace string + podName string + container corev1.Container +} + +func NewContainerLogger(namespace, podName string, container corev1.Container) ContainerLogsImpl { + return ContainerLogsImpl{ + namespace: namespace, + podName: podName, + container: container, + } +} + +func (c ContainerLogsImpl) Fetch(restApi rest.Interface) (io.ReadCloser, error) { + opts := &corev1.PodLogOptions{Container: c.container.Name} + req := restApi.Get().Namespace(c.namespace).Name(c.podName).Resource("pods").SubResource("log").VersionedParams(opts, scheme.ParameterCodec) + stream, err := req.Stream() + if err != nil { + err = errors.Wrap(err, "failed to create container log stream") + } + return stream, err +} + +func (c ContainerLogsImpl) Write(reader io.ReadCloser, rootDir string) error { + containerLogDir := filepath.Join(rootDir, c.container.Name) + if err := os.MkdirAll(containerLogDir, 0744); err != nil && !os.IsExist(err) { + return fmt.Errorf("error creating container log dir: %s", err) + } + + path := filepath.Join(containerLogDir, fmt.Sprintf("%s.log", c.container.Name)) + logrus.Debugf("Writing pod container log %s", path) + + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + defer reader.Close() + if _, err := io.Copy(file, reader); err != nil { + cpErr := fmt.Errorf("failed to copy container log:\n%s", err) + if wErr := writeError(cpErr, file); wErr != nil { + return fmt.Errorf("failed to write previous err [%s] to file: %s", err, wErr) + } + return err + } + return nil +} diff --git a/k8s/k8s.go b/k8s/k8s.go new file mode 100644 index 00000000..73bdd7ba --- /dev/null +++ b/k8s/k8s.go @@ -0,0 +1,20 @@ +package k8s + +import ( + "fmt" + "io" + + "k8s.io/client-go/rest" +) + +const BaseDirname = "kubecapture" + +type Container interface { + Fetch(rest.Interface) (io.ReadCloser, error) + Write(io.ReadCloser, string) error +} + +func writeError(errStr error, w io.Writer) error { + _, err := fmt.Fprintln(w, errStr.Error()) + return err +} diff --git a/k8s/k8s_suite_test.go b/k8s/k8s_suite_test.go new file mode 100644 index 00000000..3b769f8c --- /dev/null +++ b/k8s/k8s_suite_test.go @@ -0,0 +1,13 @@ +package k8s + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestK8s(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "K8s Suite") +} diff --git a/k8s/object_writer.go b/k8s/object_writer.go new file mode 100644 index 00000000..83d65c3c --- /dev/null +++ b/k8s/object_writer.go @@ -0,0 +1,42 @@ +package k8s + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" + "k8s.io/cli-runtime/pkg/printers" +) + +type ObjectWriter struct { + writeDir string +} + +func (w ObjectWriter) Write(result SearchResult) (string, error) { + resultDir := w.writeDir + if result.Namespaced { + resultDir = filepath.Join(w.writeDir, result.Namespace) + } + if err := os.MkdirAll(resultDir, 0744); err != nil && !os.IsExist(err) { + return "", fmt.Errorf("failed to create search result dir: %s", err) + } + + path := filepath.Join(resultDir, fmt.Sprintf("%s.json", result.ResourceName)) + file, err := os.Create(path) + if err != nil { + return "", err + } + defer file.Close() + + logrus.Debugf("kube_capture(): saving %s search results to: %s", result.ResourceName, path) + + printer := new(printers.JSONPrinter) + if err := printer.PrintObj(result.List, file); err != nil { + if wErr := writeError(err, file); wErr != nil { + return "", fmt.Errorf("failed to write previous err [%s] to file: %s", err, wErr) + } + return "", err + } + return resultDir, nil +} diff --git a/k8s/result_writer.go b/k8s/result_writer.go new file mode 100644 index 00000000..767b7cd1 --- /dev/null +++ b/k8s/result_writer.go @@ -0,0 +1,81 @@ +package k8s + +import ( + "fmt" + "os" + "path/filepath" + + "k8s.io/client-go/rest" +) + +type ResultWriter struct { + workdir string + writeLogs bool + restApi rest.Interface +} + +func NewResultWriter(workdir, what string, restApi rest.Interface) (*ResultWriter, error) { + var err error + workdir = filepath.Join(workdir, BaseDirname) + if err := os.MkdirAll(workdir, 0744); err != nil && !os.IsExist(err) { + return nil, err + } + + writeLogs := what == "logs" || what == "all" + return &ResultWriter{ + workdir: workdir, + writeLogs: writeLogs, + restApi: restApi, + }, err +} + +func (w *ResultWriter) GetResultDir() string { + return w.workdir +} + +func (w *ResultWriter) Write(searchResults []SearchResult) error { + if searchResults == nil || len(searchResults) == 0 { + return fmt.Errorf("cannot write empty (or nil) search result") + } + + // each result represents a list of searched item + // write each list in a namespaced location in working dir + for _, result := range searchResults { + objWriter := ObjectWriter{ + writeDir: w.workdir, + } + writeDir, err := objWriter.Write(result) + if err != nil { + return err + } + + if w.writeLogs && result.ListKind == "PodList" { + if len(result.List.Items) == 0 { + continue + } + for _, podItem := range result.List.Items { + logDir := filepath.Join(writeDir, podItem.GetName()) + if err := os.MkdirAll(logDir, 0744); err != nil && !os.IsExist(err) { + return fmt.Errorf("failed to create pod log dir: %s", err) + } + + containers, err := GetContainers(podItem) + if err != nil { + return err + } + for _, containerLogger := range containers { + reader, err := containerLogger.Fetch(w.restApi) + if err != nil { + return err + } + err = containerLogger.Write(reader, logDir) + if err != nil { + return err + } + } + } + } + } + + return nil +} diff --git a/k8s/search_params.go b/k8s/search_params.go new file mode 100644 index 00000000..a2502c6a --- /dev/null +++ b/k8s/search_params.go @@ -0,0 +1,154 @@ +package k8s + +import ( + "strings" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +type SearchParams struct { + 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) SetKinds(input []string) { + sp.kinds = input +} + +func (sp SearchParams) SetNames(input []string) { + sp.names = input +} + +func (sp SearchParams) SetNamespaces(input []string) { + sp.namespaces = input +} + +func (sp SearchParams) SetVersions(input []string) { + sp.versions = input +} + +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, " ") +} + +func NewSearchParams(p *starlarkstruct.Struct) SearchParams { + var ( + kinds []string + groups []string + names []string + namespaces []string + versions []string + labels []string + containers []string + ) + + groups = parseStructAttr(p, "groups") + kinds = parseStructAttr(p, "kinds") + names = parseStructAttr(p, "names") + namespaces = parseStructAttr(p, "namespaces") + if len(namespaces) == 0 { + namespaces = append(namespaces, "default") + } + versions = parseStructAttr(p, "versions") + labels = parseStructAttr(p, "labels") + containers = parseStructAttr(p, "containers") + + return SearchParams{ + kinds: kinds, + groups: groups, + names: names, + namespaces: namespaces, + versions: versions, + labels: labels, + containers: containers, + } +} + +func parseStructAttr(p *starlarkstruct.Struct, attrName string) []string { + values := make([]string, 0) + + attrVal, err := p.Attr(attrName) + if err == nil { + values, err = parse(attrVal) + if err != nil { + logrus.Errorf("error while parsing attr %s: %v", attrName, err) + } + } + return values +} + +func parse(inputValue starlark.Value) ([]string, error) { + var values []string + var err error + + switch inputValue.Type() { + case "string": + val, ok := inputValue.(starlark.String) + if !ok { + err = errors.Errorf("cannot process starlark value %s", inputValue.String()) + break + } + values = append(values, val.GoString()) + case "list": + val, ok := inputValue.(*starlark.List) + if !ok { + err = errors.Errorf("cannot process starlark value %s", inputValue.String()) + break + } + iter := val.Iterate() + defer iter.Done() + var x starlark.Value + for iter.Next(&x) { + str, _ := x.(starlark.String) + values = append(values, str.GoString()) + } + default: + err = errors.New("unknown input type for parse()") + } + + return values, err +} diff --git a/k8s/search_params_test.go b/k8s/search_params_test.go new file mode 100644 index 00000000..b6be1b26 --- /dev/null +++ b/k8s/search_params_test.go @@ -0,0 +1,106 @@ +package k8s + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +var _ = Describe("SearchParams", func() { + + var searchParams SearchParams + + Context("Building a new instance from a Starlark struct", func() { + + var ( + input *starlarkstruct.Struct + args starlark.StringDict + ) + + It("returns a new instance of the SearchParams type", func() { + input = starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{}) + searchParams = NewSearchParams(input) + Expect(searchParams).To(BeAssignableToTypeOf(SearchParams{})) + }) + + Context("With kinds", func() { + + Context("In the input struct", func() { + + It("returns a new instance with kinds struct member populated", func() { + args = starlark.StringDict{ + "kinds": starlark.String("deployments"), + } + 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")) + }) + + It("returns a new instance with kinds struct member populated", func() { + args = starlark.StringDict{ + "kinds": starlark.NewList([]starlark.Value{starlark.String("deployments"), starlark.String("replicasets")}), + } + 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")) + }) + }) + + Context("not in the input struct", func() { + + It("returns a new instance with default value of kinds struct member populated", 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("")) + }) + }) + }) + + Context("With namespaces", func() { + + Context("In the input struct", func() { + + It("returns a new instance with namespaces struct member populated", func() { + args = starlark.StringDict{ + "namespaces": starlark.String("foo"), + } + 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")) + }) + + It("returns a new instance with namespaces struct member populated", func() { + args = starlark.StringDict{ + "namespaces": starlark.NewList([]starlark.Value{starlark.String("foo"), starlark.String("bar")}), + } + 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")) + }) + }) + + Context("not in the input struct", func() { + + It("returns a new instance with default value of namespaces struct member populated", 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")) + }) + }) + }) + }) +}) diff --git a/k8s/search_result.go b/k8s/search_result.go new file mode 100644 index 00000000..d3afd803 --- /dev/null +++ b/k8s/search_result.go @@ -0,0 +1,22 @@ +package k8s + +import ( + "go.starlark.net/starlark" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type SearchResult struct { + ListKind string + ResourceName string + ResourceKind string + GroupVersionResource schema.GroupVersionResource + List *unstructured.UnstructuredList + Namespaced bool + Namespace string +} + +func (sr SearchResult) ToStarlarkValue() starlark.Value { + var val starlark.Value + return val +} diff --git a/k8s/search_result_test.go b/k8s/search_result_test.go new file mode 100644 index 00000000..77c2ba43 --- /dev/null +++ b/k8s/search_result_test.go @@ -0,0 +1,35 @@ +package k8s + +import ( + . "github.com/onsi/ginkgo" +) + +var _ = Describe("SearchResult", func() { + + Context("ToStarlarkValue", func() { + + Context("ListKind", func() { + sr := SearchResult{ListKind: "PodList"} + + It("creates value object with ListKind value", func() { + _ = sr.ToStarlarkValue() + }) + }) + + Context("For ResourceName", func() { + + }) + + Context("For ResourceKind", func() { + + }) + + Context("For Namespaced", func() { + + }) + + Context("For Namespace", func() { + + }) + }) +}) diff --git a/starlark/crashd_config.go b/starlark/crashd_config.go index 6c7ead53..e49f6c00 100644 --- a/starlark/crashd_config.go +++ b/starlark/crashd_config.go @@ -12,10 +12,10 @@ import ( // crashd_config configuration data func addDefaultCrashdConf(thread *starlark.Thread) error { args := []starlark.Tuple{ - starlark.Tuple{starlark.String("gid"), starlark.String(getGid())}, - starlark.Tuple{starlark.String("uid"), starlark.String(getUid())}, - starlark.Tuple{starlark.String("workdir"), starlark.String(defaults.workdir)}, - starlark.Tuple{starlark.String("output_path"), starlark.String(defaults.outPath)}, + {starlark.String("gid"), starlark.String(getGid())}, + {starlark.String("uid"), starlark.String(getUid())}, + {starlark.String("workdir"), starlark.String(defaults.workdir)}, + {starlark.String("output_path"), starlark.String(defaults.outPath)}, } _, err := crashdConfigFn(thread, nil, nil, args) diff --git a/starlark/kube_capture.go b/starlark/kube_capture.go new file mode 100644 index 00000000..7a2c0a7a --- /dev/null +++ b/starlark/kube_capture.go @@ -0,0 +1,118 @@ +// Copyright (c) 2020 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package starlark + +import ( + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/vmware-tanzu/crash-diagnostics/k8s" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +// KubeCaptureFn is the Starlark built-in for the fetching kubernetes objects +// and returns the result as a Starlark value containing the file path and error message, if any +// Starlark format: kube_capture(what="logs" [, groups="core", namespaces=["default"], kube_config=kube_config()]) +func KubeCaptureFn(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var argDict starlark.StringDict + + if kwargs != nil { + dict, err := kwargsToStringDict(kwargs) + if err != nil { + return starlark.None, err + } + argDict = dict + } + structVal := starlarkstruct.FromStringDict(starlarkstruct.Default, argDict) + + kubeconfig, err := kubeconfigPath(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") + } + + data := thread.Local(identifiers.crashdCfg) + cfg, _ := data.(*starlarkstruct.Struct) + workDirVal, _ := cfg.Attr("workdir") + resultDir, err := write(trimQuotes(workDirVal.String()), client, structVal) + + dict := starlark.StringDict{ + "error": starlark.String(""), + } + if err != nil { + dict["error"] = starlark.String(err.Error()) + } else { + dict["file"] = starlark.String(resultDir) + } + return starlarkstruct.FromStringDict(starlarkstruct.Default, dict), nil +} + +func write(workdir string, client *k8s.Client, structVal *starlarkstruct.Struct) (string, error) { + var searchResults []k8s.SearchResult + whatVal, err := structVal.Attr("what") + // TODO: check if we need default value + if err != nil { + return "", errors.Wrap(err, "what input parameter not specified") + } + whatStrVal, _ := whatVal.(starlark.String) + what := whatStrVal.GoString() + + searchParams := k8s.NewSearchParams(structVal) + + logrus.Debugf("kube_capture(what=%s)", what) + switch what { + case "logs": + searchParams.SetGroups([]string{"core"}) + searchParams.SetKinds([]string{"pods"}) + searchParams.SetVersions([]string{}) + case "objects", "all", "*": + default: + return "", errors.Errorf("don't know how to get: %s", what) + } + + searchResults, err = client.Search(searchParams.Groups(), searchParams.Kinds(), searchParams.Namespaces(), searchParams.Versions(), searchParams.Names(), searchParams.Labels(), searchParams.Containers()) + if err != nil { + return "", err + } + + resultWriter, err := k8s.NewResultWriter(workdir, what, client.CoreRest) + if err != nil { + return "", errors.Wrap(err, "failed to initialize writer") + } + err = resultWriter.Write(searchResults) + if err != nil { + return "", errors.Wrap(err, "failed to write search results") + } + return resultWriter.GetResultDir(), nil +} + +// kubeconfigPath is responsible to obtain the path to the kubeconfig +// It checks for the `path` key in the input args for the directive otherwise +// falls back to the default kube_config from the thread context +func kubeconfigPath(thread *starlark.Thread, structVal *starlarkstruct.Struct) (string, error) { + var kubeConfigPath string + + if v, err := structVal.Attr("path"); err == nil { + kubeConfigPath = v.String() + } else { + kubeConfigData := thread.Local(identifiers.kubeCfg) + if kubeConfigData == nil { + return kubeConfigPath, errors.New("unable to find kubeconfig data") + } + cfg, ok := kubeConfigData.(*starlarkstruct.Struct) + if !ok { + return kubeConfigPath, errors.New("unable to process kubeconfig data") + } + path, err := cfg.Attr("path") + if err != nil { + return kubeConfigPath, errors.New("unable to find path to kubeconfig") + } + kubeConfigPath = path.String() + } + + return trimQuotes(kubeConfigPath), nil +} diff --git a/starlark/kube_capture_test.go b/starlark/kube_capture_test.go new file mode 100644 index 00000000..36910d12 --- /dev/null +++ b/starlark/kube_capture_test.go @@ -0,0 +1,187 @@ +package starlark + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "go.starlark.net/starlarkstruct" + + "github.com/sirupsen/logrus" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + testcrashd "github.com/vmware-tanzu/crash-diagnostics/testing" +) + +var _ = Describe("kube_capture", func() { + + var ( + k8sconfig string + kind *testcrashd.KindCluster + waitTime = time.Second * 11 + workdir string + + executor *Executor + err error + ) + + BeforeSuite(func() { + clusterName := "crashd-test-kubecapture" + tmpFile, err := ioutil.TempFile(os.TempDir(), clusterName) + Expect(err).NotTo(HaveOccurred()) + k8sconfig = tmpFile.Name() + + // create kind cluster + kind = testcrashd.NewKindCluster("../testing/kind-cluster-docker.yaml", clusterName) + err = kind.Create() + Expect(err).NotTo(HaveOccurred()) + + err = kind.MakeKubeConfigFile(k8sconfig) + Expect(err).NotTo(HaveOccurred()) + + logrus.Infof("Sleeping %v ... waiting for pods", waitTime) + time.Sleep(waitTime) + }) + + AfterSuite(func() { + kind.Destroy() + os.RemoveAll(k8sconfig) + }) + + execSetup := func(crashdScript string) { + executor = New() + err = executor.Exec("test.kube.capture", strings.NewReader(crashdScript)) + Expect(err).To(BeNil()) + } + + BeforeEach(func() { + workdir, err = ioutil.TempDir(os.TempDir(), "test") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + os.RemoveAll(workdir) + }) + + It("creates a directory and files for namespaced objects", func() { + crashdScript := fmt.Sprintf(` +crashd_config(workdir="%s") +kube_config(path="%s") +kube_data = kube_capture(what="objects", groups="core", kinds="services", namespaces=["default", "kube-system"]) + `, workdir, k8sconfig) + execSetup(crashdScript) + Expect(executor.result.Has("kube_data")).NotTo(BeNil()) + + data := executor.result["kube_data"] + Expect(data).NotTo(BeNil()) + + captureData, _ := data.(*starlarkstruct.Struct) + Expect(captureData.AttrNames()).To(HaveLen(2)) + + errVal, err := captureData.Attr("error") + Expect(err).NotTo(HaveOccurred()) + Expect(trimQuotes(errVal.String())).To(BeEmpty()) + + fileVal, err := captureData.Attr("file") + Expect(err).NotTo(HaveOccurred()) + Expect(trimQuotes(fileVal.String())).To(BeADirectory()) + + kubeCaptureDir := trimQuotes(fileVal.String()) + Expect(filepath.Join(kubeCaptureDir, "default", "services.json")).To(BeARegularFile()) + Expect(filepath.Join(kubeCaptureDir, "kube-system", "services.json")).To(BeARegularFile()) + }) + + It("creates a directory and files for non-namespaced objects", func() { + crashdScript := fmt.Sprintf(` +crashd_config(workdir="%s") +kube_config(path="%s") +kube_data = kube_capture(what="objects", groups="core", kinds="nodes") + `, workdir, k8sconfig) + execSetup(crashdScript) + Expect(executor.result.Has("kube_data")).NotTo(BeNil()) + + data := executor.result["kube_data"] + Expect(data).NotTo(BeNil()) + + captureData, _ := data.(*starlarkstruct.Struct) + Expect(captureData.AttrNames()).To(HaveLen(2)) + + errVal, err := captureData.Attr("error") + Expect(err).NotTo(HaveOccurred()) + Expect(trimQuotes(errVal.String())).To(BeEmpty()) + + fileVal, err := captureData.Attr("file") + Expect(err).NotTo(HaveOccurred()) + Expect(trimQuotes(fileVal.String())).To(BeADirectory()) + + kubeCaptureDir := trimQuotes(fileVal.String()) + Expect(filepath.Join(kubeCaptureDir, "nodes.json")).To(BeARegularFile()) + }) + + It("creates a directory and log files for all objects in a namespace", func() { + crashdScript := fmt.Sprintf(` +crashd_config(workdir="%s") +kube_config(path="%s") +kube_data = kube_capture(what="logs", namespaces="kube-system") + `, workdir, k8sconfig) + execSetup(crashdScript) + Expect(executor.result.Has("kube_data")).NotTo(BeNil()) + + data := executor.result["kube_data"] + Expect(data).NotTo(BeNil()) + + captureData, _ := data.(*starlarkstruct.Struct) + Expect(captureData.AttrNames()).To(HaveLen(2)) + + errVal, err := captureData.Attr("error") + Expect(err).NotTo(HaveOccurred()) + Expect(trimQuotes(errVal.String())).To(BeEmpty()) + + fileVal, err := captureData.Attr("file") + Expect(err).NotTo(HaveOccurred()) + Expect(trimQuotes(fileVal.String())).To(BeADirectory()) + + kubeCaptureDir := trimQuotes(fileVal.String()) + Expect(filepath.Join(kubeCaptureDir, "kube-system")).To(BeADirectory()) + + files, err := ioutil.ReadDir(filepath.Join(kubeCaptureDir, "kube-system")) + Expect(err).NotTo(HaveOccurred()) + Expect(len(files)).NotTo(BeNumerically("<", 3)) + }) + + It("creates a log file for specific container in a namespace", func() { + crashdScript := fmt.Sprintf(` +crashd_config(workdir="%s") +kube_config(path="%s") +kube_data = kube_capture(what="logs", namespaces="kube-system", containers=["etcd"]) + `, workdir, k8sconfig) + execSetup(crashdScript) + Expect(executor.result.Has("kube_data")).NotTo(BeNil()) + + data := executor.result["kube_data"] + Expect(data).NotTo(BeNil()) + + captureData, _ := data.(*starlarkstruct.Struct) + Expect(captureData.AttrNames()).To(HaveLen(2)) + + errVal, err := captureData.Attr("error") + Expect(err).NotTo(HaveOccurred()) + Expect(trimQuotes(errVal.String())).To(BeEmpty()) + + fileVal, err := captureData.Attr("file") + Expect(err).NotTo(HaveOccurred()) + Expect(trimQuotes(fileVal.String())).To(BeADirectory()) + + kubeCaptureDir := trimQuotes(fileVal.String()) + Expect(filepath.Join(kubeCaptureDir, "kube-system")).To(BeADirectory()) + + files, err := ioutil.ReadDir(filepath.Join(kubeCaptureDir, "kube-system")) + Expect(err).NotTo(HaveOccurred()) + Expect(files).NotTo(HaveLen(0)) + }) + +}) diff --git a/starlark/starlark_exec.go b/starlark/starlark_exec.go index 0b1e619f..cbff35ee 100644 --- a/starlark/starlark_exec.go +++ b/starlark/starlark_exec.go @@ -52,13 +52,14 @@ func newThreadLocal() *starlark.Thread { // 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.kubeCfg: starlark.NewBuiltin(identifiers.kubeCfg, kubeConfigFn), + "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.kubeCfg: starlark.NewBuiltin(identifiers.kubeCfg, kubeConfigFn), + identifiers.kubeCaptureDirective: starlark.NewBuiltin(identifiers.kubeGetDirective, KubeCaptureFn), } } diff --git a/starlark/support.go b/starlark/support.go index bfac79ed..6254a103 100644 --- a/starlark/support.go +++ b/starlark/support.go @@ -25,6 +25,10 @@ var ( hostResource string resources string run string + + // Directives + kubeCaptureDirective string + kubeGetDirective string }{ crashdCfg: "crashd_config", kubeCfg: "kube_config", @@ -41,6 +45,9 @@ var ( hostResource: "host_resource", resources: "resources", run: "run", + + kubeGetDirective: "kube_get", + kubeCaptureDirective: "kube_capture", } defaults = struct {