Skip to content

Commit

Permalink
Initial functioning implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
naemono committed Oct 28, 2022
1 parent 9559685 commit 151b870
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 20 deletions.
69 changes: 65 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,30 @@
package main

import (
"fmt"
"log"
"os"
"strings"
"time"

"github.com/elastic/eck-diagnostics/internal"
"github.com/spf13/cobra"
)

var (
diagParams = internal.Params{}
filters []string
diagParams = internal.Params{}
elasticTypeKey = "common.k8s.elastic.co/type"
elasticsearchNameFormat = "%s.k8s.elastic.co/cluster-name"
elasticNameFormat = "%s.k8s.elastic.co/name"
)

func main() {
cmd := &cobra.Command{
Use: "eck-diagnostics",
Short: "ECK support diagnostics tool",
Long: "Dump ECK and Kubernetes data for support and troubleshooting purposes.",
Use: "eck-diagnostics",
Short: "ECK support diagnostics tool",
Long: "Dump ECK and Kubernetes data for support and troubleshooting purposes.",
PreRunE: formatFilters,
RunE: func(cmd *cobra.Command, args []string) error {
return internal.Run(diagParams)
},
Expand All @@ -31,6 +38,7 @@ func main() {
cmd.Flags().BoolVar(&diagParams.RunAgentDiagnostics, "run-agent-diagnostics", false, "Run diagnostics on deployed Elastic Agents. Warning: credentials will not be redacted and appear as plain text in the archive")
cmd.Flags().StringSliceVarP(&diagParams.OperatorNamespaces, "operator-namespaces", "o", []string{"elastic-system"}, "Comma-separated list of namespace(s) in which operator(s) are running")
cmd.Flags().StringSliceVarP(&diagParams.ResourcesNamespaces, "resources-namespaces", "r", nil, "Comma-separated list of namespace(s) in which resources are managed")
cmd.Flags().StringSliceVarP(&filters, "filters", "f", nil, `Comma-separated list of filters in format "type=type, name=name" (Supported types 'elasticsearch')`)
cmd.Flags().StringVar(&diagParams.ECKVersion, "eck-version", "", "ECK version in use, will try to autodetect if not specified")
cmd.Flags().StringVar(&diagParams.OutputDir, "output-directory", "", "Path where to output diagnostic results")
cmd.Flags().StringVar(&diagParams.Kubeconfig, "kubeconfig", "", "optional path to kube config, defaults to $HOME/.kube/config")
Expand All @@ -47,6 +55,59 @@ func main() {
}
}

func formatFilters(_ *cobra.Command, _ []string) error {
var typ, name string
if len(filters) == 0 {
return nil
}
for _, filter := range filters {
filterSlice := strings.Split(filter, "=")
if len(filterSlice) != 2 {
return fmt.Errorf("Invalid filter: %s", filter)
}
k, v := filterSlice[0], filterSlice[1]
switch k {
case "type":
{
if typ != "" {
return fmt.Errorf("Only a single type filter is supported.")
}
typ = v
}
case "name":
{
if name != "" {
return fmt.Errorf("Only a single name filter is supported.")
}
name = v
}
default:
return fmt.Errorf("Invalid filter key: %s. Only 'type', and 'name' are supported.", k)
}
}
if typ == "" {
return fmt.Errorf("Invalid Filter: missing 'type'")
}
if name == "" {
return fmt.Errorf("Invalid Filter: missing 'name'")
}
diagParams.LabelSelector = expandfilter(typ, name)
return nil
}

func expandfilter(typ, name string) string {
var elasticfilter string
elasticfilter += elasticTypeKey + "=" + strings.ToLower(typ) + ","
switch typ {
case "elasticsearch":
elasticfilter += fmt.Sprintf(elasticsearchNameFormat, strings.ToLower(typ)) + "=" + name
default:
elasticfilter += fmt.Sprintf(elasticNameFormat, strings.ToLower(typ)) + "=" + name
}

return elasticfilter
}

func exitWithError(err error) {
if err != nil {
log.Printf("Error: %v", err)
Expand Down
37 changes: 23 additions & 14 deletions internal/diag.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Params struct {
RunAgentDiagnostics bool
Verbose bool
StackDiagnosticsTimeout time.Duration
LabelSelector string
}

// AllNamespaces returns a slice containing all namespaces from which we want to extract diagnostic data.
Expand Down Expand Up @@ -86,13 +87,13 @@ func Run(params Params) error {
return kubectl.Version(writer)
},
"nodes.json": func(writer io.Writer) error {
return kubectl.Get("nodes", "", writer)
return kubectl.Get("nodes", "", "", writer)
},
"podsecuritypolicies.json": func(writer io.Writer) error {
return kubectl.Get("podsecuritypolicies", "", writer)
return kubectl.Get("podsecuritypolicies", "", "", writer)
},
"storageclasses.json": func(writer io.Writer) error {
return kubectl.Get("storageclasses", "", writer)
return kubectl.Get("storageclasses", "", "", writer)
},
"clusterroles.txt": func(writer io.Writer) error {
return kubectl.Describe("clusterroles", "elastic", "", writer)
Expand All @@ -109,7 +110,7 @@ func Run(params Params) error {

operatorVersions = append(operatorVersions, detectECKVersion(clientSet, ns, params.ECKVersion))

zipFile.Add(getResources(kubectl, ns, []string{
zipFile.Add(getResources(kubectl.Get, ns, params.LabelSelector, []string{
"statefulsets",
"pods",
"services",
Expand Down Expand Up @@ -142,41 +143,49 @@ LOOP:
default:
}
logger.Printf("Extracting Kubernetes diagnostics from %s\n", ns)
zipFile.Add(getResources(kubectl, ns, []string{
zipFile.Add(getResources(kubectl.Get, ns, params.LabelSelector, []string{
"statefulsets",
"replicasets",
"deployments",
"daemonsets",
"pods",
"persistentvolumes",
"persistentvolumeclaims",
"services",
"endpoints",
"configmaps",
"events",
"networkpolicies",
"controllerrevisions",
}))

zipFile.Add(getResources(kubectl.GetElastic, ns, params.LabelSelector, []string{
"kibana",
"elasticsearch",
"apmserver",
}))

// labelSelector is intentionally empty, as Elastic labels
// are not applied to these resources.
zipFile.Add(getResources(kubectl.Get, ns, "", []string{
"persistentvolumes",
"events",
"networkpolicies",
"serviceaccount",
}))

if maxOperatorVersion.AtLeast(version.MustParseSemantic("1.2.0")) {
zipFile.Add(getResources(kubectl, ns, []string{
zipFile.Add(getResources(kubectl.GetElastic, ns, params.LabelSelector, []string{
"enterprisesearch",
"beat",
}))
}

if maxOperatorVersion.AtLeast(version.MustParseSemantic("1.4.0")) {
zipFile.Add(getResources(kubectl, ns, []string{
zipFile.Add(getResources(kubectl.GetElastic, ns, params.LabelSelector, []string{
"agent",
}))
}

if maxOperatorVersion.AtLeast(version.MustParseSemantic("1.6.0")) {
zipFile.Add(getResources(kubectl, ns, []string{
zipFile.Add(getResources(kubectl.GetElastic, ns, params.LabelSelector, []string{
"elasticmapsserver",
}))
}
Expand All @@ -191,7 +200,7 @@ LOOP:
"common.k8s.elastic.co/type=elasticsearch",
"common.k8s.elastic.co/type=kibana",
"common.k8s.elastic.co/type=apm-server",
// the below where introduced in later version but label selector will just return no result:
// the below were introduced in later version but label selector will just return no result:
"common.k8s.elastic.co/type=enterprise-search", // 1.2.0
"common.k8s.elastic.co/type=beat", // 1.2.0
"common.k8s.elastic.co/type=agent", // 1.4.0
Expand Down Expand Up @@ -242,12 +251,12 @@ func getLogs(k *Kubectl, zipFile *archive.ZipFile, ns string, selector ...string

// getResources produces a map of filenames to functions that will when invoked retrieve the resources identified by rs
// and add write them to a writer passed to said functions.
func getResources(k *Kubectl, ns string, rs []string) map[string]func(io.Writer) error {
func getResources(f func(string, string, string, io.Writer) error, ns string, labelSelector string, rs []string) map[string]func(io.Writer) error {
m := map[string]func(io.Writer) error{}
for _, r := range rs {
resource := r
m[archive.Path(ns, resource+".json")] = func(w io.Writer) error {
return k.Get(resource, ns, w)
return f(resource, ns, labelSelector, w)
}
}
return m
Expand Down
83 changes: 81 additions & 2 deletions internal/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"fmt"
"io"
"os"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -159,8 +160,8 @@ func (c Kubectl) Exec(nsn types.NamespacedName, cmd ...string) error {
}

// Get retrieves the K8s objects of type resource in namespace and marshals them into the writer w.
func (c Kubectl) Get(resource, namespace string, w io.Writer) error {
r, err := c.getResources(resource, namespace)
func (c Kubectl) Get(resource, namespace, labelSelector string, w io.Writer) error {
r, err := c.getResourcesMatching(resource, namespace, labelSelector)
if err != nil {
return err
}
Expand All @@ -177,6 +178,65 @@ func (c Kubectl) Get(resource, namespace string, w io.Writer) error {
return printer.PrintObj(obj, w)
}

// Get retrieves the K8s objects of type resource in namespace and marshals them into the writer w.
func (c Kubectl) GetElastic(resourceName, namespace, labelSelector string, w io.Writer) error {
var (
err error
r *resource.Result
)

if labelSelector != "" {
typ, name, err := extractTypeName(labelSelector)
if err != nil {
return err
}
if typ != resourceName {
return nil
}
r, err = c.getResourcesWithFieldSelector(resourceName, namespace, fmt.Sprintf("metadata.name=%s", name))
if err != nil {
return err
}
} else {
r, err = c.getResources(resourceName, namespace)
if err != nil {
return err
}
}

printer, err := printers.NewTypeSetter(scheme.Scheme).WrapToPrinter(&printers.JSONPrinter{}, nil)
if err != nil {
return err
}

obj, err := r.Object()
if err != nil {
return err
}

return printer.PrintObj(obj, w)
}

func extractTypeName(selectors string) (string, string, error) {
var typ, name string
r := regexp.MustCompile(`^[a-z]*\.k8s\.elastic\.co\/(cluster\-){0,1}name$`)
for _, selector := range strings.Split(selectors, ",") {
kvs := strings.Split(selector, "=")
for i, v := range kvs {
if v == "common.k8s.elastic.co/type" {
typ = kvs[i+1]
}
if r.Match([]byte(v)) {
name = kvs[i+1]
}
}
}
if typ != "" && name != "" {
return typ, name, nil
}
return "", "", fmt.Errorf("type and/or name selector not found")
}

// getResources retrieves the K8s objects of type resource and returns a resource.Result.
func (c Kubectl) getResources(resource string, namespace string) (*resource.Result, error) {
r := c.factory.NewBuilder().
Expand Down Expand Up @@ -214,6 +274,25 @@ func (c Kubectl) getResourcesMatching(resource string, namespace string, selecto
return r, nil
}

// getResourcesWithFieldSelector retrieves the K8s objects of type resource matching field selector and returns a resource.Result.
func (c Kubectl) getResourcesWithFieldSelector(resource string, namespace string, fieldSelector string) (*resource.Result, error) {
r := c.factory.NewBuilder().
Unstructured().
NamespaceParam(namespace).DefaultNamespace().AllNamespaces(false).
ResourceTypeOrNameArgs(true, resource).
FieldSelectorParam(fieldSelector).
ContinueOnError().
Latest().
Flatten().
Do()

r.IgnoreErrors(apierrors.IsNotFound)
if err := r.Err(); err != nil {
return nil, err
}
return r, nil
}

// GetMeta retrieves the metadata for the K8s objects of type resource and marshals them into writer w.
// It tries to elide sensitive data like secret contents or kubectl last-applied configuration annotations.
func (c Kubectl) GetMeta(resource, namespace string, w io.Writer) error {
Expand Down
40 changes: 40 additions & 0 deletions internal/kubectl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package internal

import "testing"

func Test_extractTypeName(t *testing.T) {
tests := []struct {
name string
selectors string
want string
want1 string
wantErr bool
}{
{
name: "elasticsearch type/name should extract properly",
selectors: "common.k8s.elastic.co/type=elasticsearch,elasticsearch.k8s.elastic.co/cluster-name=myname",
want: "elasticsearch",
want1: "myname",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := extractTypeName(tt.selectors)
if (err != nil) != tt.wantErr {
t.Errorf("extractTypeName() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("extractTypeName() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("extractTypeName() got1 = %v, want %v", got1, tt.want1)
}
})
}
}

0 comments on commit 151b870

Please sign in to comment.