Skip to content

Commit

Permalink
Merge pull request #132 from naemono/121-support-filters
Browse files Browse the repository at this point in the history
Support filtering for Elastic types.
  • Loading branch information
naemono authored Nov 17, 2022
2 parents 8deee72 + 2a6535b commit 0bfdaa2
Show file tree
Hide file tree
Showing 8 changed files with 586 additions and 40 deletions.
59 changes: 48 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ Usage:
eck-diagnostics [flags]
Flags:
--diagnostic-image string Diagnostic image to be used for stack diagnostics, see run-stack-diagnostics (default "docker.elastic.co/eck-dev/support-diagnostics:8.1.4")
--eck-version string ECK version in use, will try to autodetect if not specified
-h, --help help for eck-diagnostics
--kubeconfig string optional path to kube config, defaults to $HOME/.kube/config
-o, --operator-namespaces strings Comma-separated list of namespace(s) in which operator(s) are running (default [elastic-system])
--output-directory string Path where to output diagnostic results
-r, --resources-namespaces strings Comma-separated list of namespace(s) in which resources are managed
--run-agent-diagnostics Run diagnostics on deployed Elastic Agents. Warning: credentials will not be redacted and appear as plain text in the archive
--run-stack-diagnostics Run diagnostics on deployed Elasticsearch clusters and Kibana instances, requires deploying diagnostic Pods into the cluster (default true)
--verbose Verbose mode
--diagnostic-image string Diagnostic image to be used for stack diagnostics, see run-stack-diagnostics (default "docker.elastic.co/eck-dev/support-diagnostics:8.4.0")
--eck-version string ECK version in use, will try to autodetect if not specified
-f, --filters strings Comma-separated list of filters in format "type=name". ex: elasticsearch=my-cluster (Supported types [agent apm beat elasticsearch enterprisesearch kibana maps])
-h, --help help for eck-diagnostics
--kubeconfig string optional path to kube config, defaults to $HOME/.kube/config
-o, --operator-namespaces strings Comma-separated list of namespace(s) in which operator(s) are running (default [elastic-system])
--output-directory string Path where to output diagnostic results
-r, --resources-namespaces strings Comma-separated list of namespace(s) in which resources are managed
--run-agent-diagnostics Run diagnostics on deployed Elastic Agents. Warning: credentials will not be redacted and appear as plain text in the archive
--run-stack-diagnostics Run diagnostics on deployed Elasticsearch clusters and Kibana instances, requires deploying diagnostic Pods into the cluster (default true)
--stack-diagnostics-timeout duration Maximum time to wait for Elaticsearch and Kibana diagnostics to complete (default 5m0s)
--verbose Verbose mode
```

## Information collected by eck-diagnostics
Expand Down Expand Up @@ -82,4 +84,39 @@ The ECK related custom resources are included in those namespaces as well:
* Kibana

### Logs
In the operator namespaces (`-o, --operator-namespaces`) all logs are collected, while in the workload resource namespaces only logs from Pods managed by ECK are collected.
In the operator namespaces (`-o, --operator-namespaces`) all logs are collected, while in the workload resource namespaces only logs from Pods managed by ECK are collected.

## Filtering collected resources

The resources in the specified namespaces that are collected by eck-diagnostics can be filtered with the `-f, --filters` flag.

### Usage Example

The following example will run the diagnostics for Elastic resources in namespace `a`, and will only return resources associated with either an Elasticsearch cluster named `mycluster` or a Kibana instance named `my-kb`.

```shell
eck-diagnostics -r a -f "elasticsearch=mycluster" -f "kibana=my-kb"
```

### Filtered resources

Only a certain number of Kubernetes resources support filtering when filtering by an Elastic custom resource. Along with the named Elastic custom resource type, the following resources will be returned that are associated:

* ConfigMap
* ControllerRevision
* Deployment
* DaemonSet
* Endpoint
* Pod
* PersistentVolumeClaim
* Replicaset
* Service
* StatefulSet

The following resources are returned unfiltered:

* Event
* NetworkPolicy
* PersistentVolume
* ServiceAccount
* Secret (metadata only)
14 changes: 14 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
package main

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

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

var (
filters []string
diagParams = internal.Params{}
)

Expand All @@ -22,6 +25,7 @@ func main() {
Use: "eck-diagnostics",
Short: "ECK support diagnostics tool",
Long: "Dump ECK and Kubernetes data for support and troubleshooting purposes.",
PreRunE: parseFilters,
Version: internal.Version(),
RunE: func(cmd *cobra.Command, args []string) error {
return internal.Run(diagParams)
Expand All @@ -32,6 +36,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, fmt.Sprintf(`Comma-separated list of filters in format "type=name". ex: elasticsearch=my-cluster (Supported types %v)`, internal_filters.ValidTypes))
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 @@ -48,6 +53,15 @@ func main() {
}
}

func parseFilters(_ *cobra.Command, _ []string) error {
filters, err := internal_filters.New(filters)
if err != nil {
return err
}
diagParams.Filters = filters
return nil
}

func exitWithError(err error) {
if err != nil {
log.Printf("Error: %v", err)
Expand Down
8 changes: 7 additions & 1 deletion internal/agentdiag.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import (

"github.com/elastic/eck-diagnostics/internal/archive"
"github.com/elastic/eck-diagnostics/internal/extraction"
internal_filters "github.com/elastic/eck-diagnostics/internal/filters"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/cli-runtime/pkg/resource"
)

func runAgentDiagnostics(k *Kubectl, ns string, zipFile *archive.ZipFile, verbose bool, stopCh chan struct{}) {
func runAgentDiagnostics(k *Kubectl, ns string, zipFile *archive.ZipFile, verbose bool, stopCh chan struct{}, filters internal_filters.Filters) {
outputFile := time.Now().Format("eck-agent-diag-2006-01-02T15-04-05Z.zip")
resources, err := k.getResourcesMatching("pod", ns, "common.k8s.elastic.co/type=agent")
if err != nil {
Expand All @@ -31,6 +32,7 @@ func runAgentDiagnostics(k *Kubectl, ns string, zipFile *archive.ZipFile, verbos
default:
// continue processing agents
}

resourceName := info.Name
labels, err := meta.NewAccessor().Labels(info.Object)
if err != nil {
Expand All @@ -54,6 +56,10 @@ func runAgentDiagnostics(k *Kubectl, ns string, zipFile *archive.ZipFile, verbos
return nil
}

if !filters.Matches(labels) {
return nil
}

nsn := types.NamespacedName{Namespace: ns, Name: resourceName}

needsCleanup := diagnosticForAgentPod(nsn, k, outputFile, zipFile, verbose)
Expand Down
54 changes: 34 additions & 20 deletions internal/diag.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"time"

"github.com/elastic/eck-diagnostics/internal/archive"
"github.com/elastic/eck-diagnostics/internal/filters"
"github.com/elastic/eck-diagnostics/internal/log"
"k8s.io/apimachinery/pkg/util/version"

Expand All @@ -38,6 +39,7 @@ type Params struct {
RunAgentDiagnostics bool
Verbose bool
StackDiagnosticsTimeout time.Duration
Filters filters.Filters
}

// AllNamespaces returns a slice containing all namespaces from which we want to extract diagnostic data.
Expand Down Expand Up @@ -81,18 +83,20 @@ func Run(params Params) error {
return err
}

// Filters is intentionally empty in many of these, as Elastic labels
// are not applied to these resources.
zipFile.Add(map[string]func(io.Writer) error{
"version.json": func(writer io.Writer) error {
return kubectl.Version(writer)
},
"nodes.json": func(writer io.Writer) error {
return kubectl.Get("nodes", "", writer)
return kubectl.GetByLabel("nodes", "", filters.Filters{}, writer)
},
"podsecuritypolicies.json": func(writer io.Writer) error {
return kubectl.Get("podsecuritypolicies", "", writer)
return kubectl.GetByLabel("podsecuritypolicies", "", filters.Filters{}, writer)
},
"storageclasses.json": func(writer io.Writer) error {
return kubectl.Get("storageclasses", "", writer)
return kubectl.GetByLabel("storageclasses", "", filters.Filters{}, writer)
},
"clusterroles.txt": func(writer io.Writer) error {
return kubectl.Describe("clusterroles", "elastic", "", writer)
Expand All @@ -109,7 +113,7 @@ func Run(params Params) error {

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

zipFile.Add(getResources(kubectl, ns, []string{
zipFile.Add(getResources(kubectl.GetByLabel, ns, filters.Filters{}, []string{
"statefulsets",
"pods",
"services",
Expand All @@ -126,7 +130,9 @@ func Run(params Params) error {
},
})

if err := kubectl.Logs(ns, "", zipFile.Create); err != nil {
// Filters is intentionally empty here, as label filtering doesn't apply to
// pods in the operator namespace.
if err := kubectl.Logs(ns, "", filters.Filters{}, zipFile.Create); err != nil {
zipFile.AddError(err)
}
}
Expand All @@ -142,41 +148,49 @@ LOOP:
default:
}
logger.Printf("Extracting Kubernetes diagnostics from %s\n", ns)
zipFile.Add(getResources(kubectl, ns, []string{
zipFile.Add(getResources(kubectl.GetByLabel, ns, params.Filters, []string{
"statefulsets",
"replicasets",
"deployments",
"daemonsets",
"pods",
"persistentvolumes",
"persistentvolumeclaims",
"services",
"endpoints",
"configmaps",
"events",
"networkpolicies",
"controllerrevisions",
}))

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

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

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

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

if maxOperatorVersion.AtLeast(version.MustParseSemantic("1.6.0")) {
zipFile.Add(getResources(kubectl, ns, []string{
zipFile.Add(getResources(kubectl.GetByName, ns, params.Filters, []string{
"elasticmapsserver",
}))
}
Expand All @@ -187,23 +201,23 @@ LOOP:
},
})

getLogs(kubectl, zipFile, ns,
getLogs(kubectl, zipFile, ns, params.Filters,
"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
"common.k8s.elastic.co/type=maps", // 1.6.0
)

if params.RunStackDiagnostics {
runStackDiagnostics(kubectl, ns, zipFile, params.Verbose, params.DiagnosticImage, params.StackDiagnosticsTimeout, stopCh)
runStackDiagnostics(kubectl, ns, zipFile, params.Verbose, params.DiagnosticImage, params.StackDiagnosticsTimeout, stopCh, params.Filters)
}

if params.RunAgentDiagnostics {
runAgentDiagnostics(kubectl, ns, zipFile, params.Verbose, stopCh)
runAgentDiagnostics(kubectl, ns, zipFile, params.Verbose, stopCh, params.Filters)
}
}

Expand Down Expand Up @@ -232,22 +246,22 @@ func addDiagnosticLogToArchive(zipFile *archive.ZipFile, logContents *bytes.Buff
}

// getLogs extracts logs from all Pods that match the given selectors in the namespace ns and adds them to zipFile.
func getLogs(k *Kubectl, zipFile *archive.ZipFile, ns string, selector ...string) {
func getLogs(k *Kubectl, zipFile *archive.ZipFile, ns string, filters filters.Filters, selector ...string) {
for _, s := range selector {
if err := k.Logs(ns, s, zipFile.Create); err != nil {
if err := k.Logs(ns, s, filters, zipFile.Create); err != nil {
zipFile.AddError(err)
}
}
}

// 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, filters.Filters, io.Writer) error, ns string, filters filters.Filters, 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, filters, w)
}
}
return m
Expand Down
Loading

0 comments on commit 0bfdaa2

Please sign in to comment.