Skip to content

Commit

Permalink
Interactive mod (safe mod) for collecting cluster (#206)
Browse files Browse the repository at this point in the history
* interactive mod for collecting cluster

* nit

* fix
  • Loading branch information
jt-dd committed Jun 19, 2024
1 parent 70d383d commit a8f409f
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 16 deletions.
3 changes: 3 additions & 0 deletions pkg/cmd/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ func InitDumpCmd(cmd *cobra.Command) {
cmd.PersistentFlags().Int32("page-buffer-count", config.DefaultK8sAPIPageBufferSize, "Number of pages to buffer")
viper.BindPFlag(config.CollectorLivePageBufferSize, cmd.PersistentFlags().Lookup("page-buffer-count")) //nolint: errcheck

cmd.PersistentFlags().BoolP("non-interactive", "y", config.DefaultK8sAPINonInteractive, "Non interactive mode (skip cluster confirmation)")
viper.BindPFlag(config.CollectorNonInteractive, cmd.PersistentFlags().Lookup("non-interactive")) //nolint: errcheck

cmd.PersistentFlags().Bool("debug", false, "Enable debug logs")
viper.BindPFlag(config.GlobalDebug, cmd.PersistentFlags().Lookup("debug")) //nolint: errcheck
}
Expand Down
29 changes: 29 additions & 0 deletions pkg/cmd/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cmd

import (
"fmt"

"strings"

"github.com/DataDog/KubeHound/pkg/telemetry/log"
)

func AskForConfirmation() (bool, error) {
var response string

_, err := fmt.Scanln(&response)
if err.Error() != "unexpected newline" {
return false, fmt.Errorf("scanln: %w", err)
}

switch strings.ToLower(response) {
case "y", "yes":
return true, nil
case "n", "no":
return false, nil
default:
log.I.Error("Please type (y)es or (n)o and then press enter:")

return AskForConfirmation()
}
}
41 changes: 32 additions & 9 deletions pkg/collector/k8s_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"sync"
"time"

"github.com/DataDog/KubeHound/pkg/cmd"
"github.com/DataDog/KubeHound/pkg/config"
"github.com/DataDog/KubeHound/pkg/telemetry/log"
"github.com/DataDog/KubeHound/pkg/telemetry/metric"
Expand Down Expand Up @@ -67,9 +68,31 @@ func tunedListOptions() metav1.ListOptions {

// NewK8sAPICollector creates a new instance of the k8s live API collector from the provided application config.
func NewK8sAPICollector(ctx context.Context, cfg *config.KubehoundConfig) (CollectorClient, error) {
l := log.Trace(ctx, log.WithComponent(K8sAPICollectorName))
clusterName, err := config.GetClusterName(ctx)
if err != nil {
return nil, err
}

l := log.Trace(ctx,
log.WithComponent(K8sAPICollectorName),
log.WithCollectedCluster(clusterName),
)

if !cfg.Collector.NonInteractive {
l.Warnf("About to dump k8s cluster: %s - Do you want to continue ? [Yes/No]", clusterName)
proceed, err := cmd.AskForConfirmation()
if err != nil {
return nil, err
}

if !proceed {
return nil, errors.New("user did not confirm")
}
} else {
l.Warnf("Non-interactive mode enabled, proceeding with k8s cluster dump: %s", clusterName)
}

err := checkK8sAPICollectorConfig(cfg.Collector.Type)
err = checkK8sAPICollectorConfig(cfg.Collector.Type)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -115,7 +138,7 @@ func (c *k8sAPICollector) wait(_ context.Context, resourceType string, tags []st
// entity := tag.Entity(resourceType)
err := statsd.Gauge(metric.CollectorWait, float64(c.waitTime[resourceType]), tags, 1)
if err != nil {
log.I.Error(err)
c.log.Error(err)
}
}

Expand All @@ -125,7 +148,7 @@ func (c *k8sAPICollector) waitTimeByResource(resourceType string, span ddtrace.S

waitTime := c.waitTime[resourceType]
span.SetTag(tag.WaitTag, waitTime)
log.I.Debugf("Wait time for %s: %s", resourceType, waitTime)
c.log.Debugf("Wait time for %s: %s", resourceType, waitTime)
}

func (c *k8sAPICollector) Name() string {
Expand Down Expand Up @@ -164,21 +187,21 @@ func (c *k8sAPICollector) computeMetrics(_ context.Context) error {
err := statsd.Gauge(metric.CollectorRunWait, float64(runTotalWaitTime), c.tags.baseTags, 1)
if err != nil {
errMetric = errors.Join(errMetric, err)
log.I.Error(err)
c.log.Error(err)
}
err = statsd.Gauge(metric.CollectorRunDuration, float64(runDuration), c.tags.baseTags, 1)
if err != nil {
errMetric = errors.Join(errMetric, err)
log.I.Error(err)
c.log.Error(err)
}

runThrottlingPercentage := 1 - (float64(runDuration-runTotalWaitTime) / float64(runDuration))
err = statsd.Gauge(metric.CollectorRunThrottling, runThrottlingPercentage, c.tags.baseTags, 1)
if err != nil {
errMetric = errors.Join(errMetric, err)
log.I.Error(err)
c.log.Error(err)
}
log.I.Infof("Stats for the run time duration: %s / wait: %s / throttling: %f%%", runDuration, runTotalWaitTime, 100*runThrottlingPercentage) //nolint:gomnd
c.log.Infof("Stats for the run time duration: %s / wait: %s / throttling: %f%%", runDuration, runTotalWaitTime, 100*runThrottlingPercentage) //nolint:gomnd

return errMetric
}
Expand All @@ -187,7 +210,7 @@ func (c *k8sAPICollector) Close(ctx context.Context) error {
err := c.computeMetrics(ctx)
if err != nil {
// We don't want to return an error here as it is just metrics and won't affect the collection of data
log.I.Errorf("Error computing metrics: %s", err)
c.log.Errorf("Error computing metrics: %s", err)
}

return nil
Expand Down
9 changes: 6 additions & 3 deletions pkg/config/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ const (
DefaultK8sAPIPageSize int64 = 500
DefaultK8sAPIPageBufferSize int32 = 10
DefaultK8sAPIRateLimitPerSecond int = 100
DefaultK8sAPINonInteractive bool = false

CollectorLiveRate = "collector.live.rate_limit_per_second"
CollectorLivePageSize = "collector.live.page_size"
CollectorLivePageBufferSize = "collector.live.page_buffer_size"
CollectorNonInteractive = "collector.non_interactive"
CollectorFileArchiveFormat = "collector.file.archive.format"
CollectorFileDirectory = "collector.file.directory"
CollectorFileClusterName = "collector.file.cluster_name"
Expand All @@ -22,9 +24,10 @@ const (

// CollectorConfig configures collector specific parameters.
type CollectorConfig struct {
Type string `mapstructure:"type"` // Collector type
File *FileCollectorConfig `mapstructure:"file"` // File collector specific configuration
Live *K8SAPICollectorConfig `mapstructure:"live"` // File collector specific configuration
Type string `mapstructure:"type"` // Collector type
File *FileCollectorConfig `mapstructure:"file"` // File collector specific configuration
Live *K8SAPICollectorConfig `mapstructure:"live"` // File collector specific configuration
NonInteractive bool `mapstructure:"non_interactive"` // Skip confirmation
}

// K8SAPICollectorConfig configures the K8sAPI collector.
Expand Down
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func SetDefaultValues(c *viper.Viper) {
c.SetDefault(CollectorLivePageSize, DefaultK8sAPIPageSize)
c.SetDefault(CollectorLivePageBufferSize, DefaultK8sAPIPageBufferSize)
c.SetDefault(CollectorLiveRate, DefaultK8sAPIRateLimitPerSecond)
c.SetDefault(CollectorNonInteractive, DefaultK8sAPINonInteractive)

// Default values for storage provider
c.SetDefault("storage.wipe", true)
Expand Down
7 changes: 4 additions & 3 deletions pkg/globals/tags.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package globals

const (
TagComponent = "component"
TagTeam = "team"
TagService = "service"
TagComponent = "component"
CollectedClusterComponent = "collected_cluster"
TagTeam = "team"
TagService = "service"
)
7 changes: 7 additions & 0 deletions pkg/telemetry/log/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ func WithComponent(name string) LoggerOption {
}
}

// WithCollectedCluster adds a component name tag to the logger.
func WithCollectedCluster(name string) LoggerOption {
return func(l *logrus.Entry) *logrus.Entry {
return l.WithField(globals.CollectedClusterComponent, name)
}
}

// Trace creates a logger from the current context, attaching trace and span IDs for use with APM.
func Trace(ctx context.Context, opts ...LoggerOption) *KubehoundLogger {
baseLogger := Base()
Expand Down
1 change: 1 addition & 0 deletions test/system/kubehound.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ storage:
retry: 6
collector:
type: live-k8s-api-collector
non_interactive: true
janusgraph:
url: "ws://localhost:8183/gremlin"
connection_timeout: 60s
Expand Down
1 change: 1 addition & 0 deletions test/system/kubehound_dump.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ storage:
retry: 6
collector:
type: file-collector
non_interactive: true
janusgraph:
url: "ws://localhost:8183/gremlin"
connection_timeout: 60s
Expand Down
4 changes: 3 additions & 1 deletion test/system/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func Dump(ctx context.Context, compress bool) (*config.KubehoundConfig, string)
log.I.Fatalf(err.Error())
}
viper.Set(config.CollectorFileDirectory, tmpDir)
viper.Set(config.CollectorNonInteractive, true)

// Initialisation of the Kubehound config
err = cmd.InitializeKubehoundConfig(ctx, "", true, false)
Expand Down Expand Up @@ -125,7 +126,8 @@ func RunLocal(ctx context.Context, runArgs *runArgs, compress bool, p *providers
runID := runArgs.runID

if compress {
err := puller.ExtractTarGz(false, runArgs.resultPath, collectorDir, config.DefaultMaxArchiveSize)
dryRun := false
err := puller.ExtractTarGz(dryRun, runArgs.resultPath, collectorDir, config.DefaultMaxArchiveSize)
if err != nil {
log.I.Fatalf(err.Error())
}
Expand Down

0 comments on commit a8f409f

Please sign in to comment.