From 81c207ae3c4fda56253b050e8b0f42a1c9b33c3a Mon Sep 17 00:00:00 2001 From: sabban Date: Fri, 21 Nov 2025 10:55:43 +0100 Subject: [PATCH 01/23] initial commit --- pkg/acquisition/k8s_podlogs.go | 19 + .../modules/kubernetespodlogs/.keep | 0 .../modules/kubernetespodlogs/k8s_podlogs.go | 723 ++++++++++++++++++ .../kubernetespodlogs/k8s_podlogs_schema.yaml | 109 +++ pkg/metrics/acquisition_k8s_podlogs.go | 20 + 5 files changed, 871 insertions(+) create mode 100644 pkg/acquisition/k8s_podlogs.go create mode 100644 pkg/acquisition/modules/kubernetespodlogs/.keep create mode 100644 pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go create mode 100644 pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml create mode 100644 pkg/metrics/acquisition_k8s_podlogs.go diff --git a/pkg/acquisition/k8s_podlogs.go b/pkg/acquisition/k8s_podlogs.go new file mode 100644 index 00000000000..28aa40a5ed8 --- /dev/null +++ b/pkg/acquisition/k8s_podlogs.go @@ -0,0 +1,19 @@ +//go:build !no_datasource_k8s_podlogs + +package acquisition + +import ( + kubernetespodlogs "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/kubernetespodlogs" +) + +var ( + // verify interface compliance + _ DataSource = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) + _ Tailer = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) + _ MetricsProvider = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) +) + +//nolint:gochecknoinits +func init() { + registerDataSource("k8s-podlogs", func() DataSource { return &kubernetespodlogs.KubernetesPodLogsSource{} }) +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/.keep b/pkg/acquisition/modules/kubernetespodlogs/.keep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go new file mode 100644 index 00000000000..ad0b8f0f44d --- /dev/null +++ b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go @@ -0,0 +1,723 @@ +//go:build !no_datasource_k8s_podlogs + +package kubernetespodlogs + +import ( + "bufio" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "sync" + "time" + + yaml "github.com/goccy/go-yaml" + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" + "gopkg.in/tomb.v2" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + + "github.com/crowdsecurity/go-cs-lib/trace" + + "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" + "github.com/crowdsecurity/crowdsec/pkg/metrics" + "github.com/crowdsecurity/crowdsec/pkg/types" +) + +const ( + defaultAPIServer = "https://kubernetes.default.svc" + defaultTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + defaultCACertPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + defaultResyncPeriod = 30 * time.Second + defaultRequestTimeout = 10 * time.Second + defaultMaxLineBytes = 1 << 20 + maxErrorBodyBytes = 4096 + userAgent = "crowdsec-k8s-podlogs" + nodeNameEnvKey = "NODE_NAME" +) + +type KubernetesPodLogsConfig struct { + configuration.DataSourceCommonCfg `yaml:",inline"` + + APIServer string `yaml:"api_server"` + TokenPath string `yaml:"token_file"` + BearerToken string `yaml:"bearer_token"` + CACertPath string `yaml:"ca_cert"` + InsecureSkipTLS bool `yaml:"insecure_skip_verify"` + NodeName string `yaml:"node_name"` + Namespaces []string `yaml:"namespaces"` + LabelSelector string `yaml:"label_selector"` + Containers []string `yaml:"containers"` + SinceSeconds *int64 `yaml:"since_seconds"` + TailLines *int64 `yaml:"tail_lines"` + LimitBytes int64 `yaml:"limit_bytes"` + IncludeTimestamps bool `yaml:"timestamps"` + Follow bool `yaml:"follow"` + ResyncPeriod time.Duration `yaml:"resync_period"` + RequestTimeout time.Duration `yaml:"request_timeout"` + MaxLineBytes int `yaml:"max_line_bytes"` +} + +type podLogTarget struct { + Namespace string + Pod string + Container string +} + +func (t podLogTarget) String() string { + return fmt.Sprintf("%s/%s/%s", t.Namespace, t.Pod, t.Container) +} + +type podLogKey struct { + Namespace string + Pod string + Container string +} + +type KubernetesPodLogsSource struct { + config KubernetesPodLogsConfig + logger *log.Entry + metricsLevel metrics.AcquisitionMetricsLevel + apiClient *http.Client + streamClient *http.Client + authToken string + apiServer string + namespaceFilter map[string]struct{} + containerFilter map[string]struct{} + out chan types.Event + activeStreams map[podLogKey]context.CancelFunc + activeMu sync.Mutex +} + +func defaultKubePodLogsConfig() KubernetesPodLogsConfig { + return KubernetesPodLogsConfig{ + APIServer: defaultAPIServer, + TokenPath: defaultTokenPath, + CACertPath: defaultCACertPath, + Follow: true, + ResyncPeriod: defaultResyncPeriod, + RequestTimeout: defaultRequestTimeout, + MaxLineBytes: defaultMaxLineBytes, + } +} + +func (k *KubernetesPodLogsSource) normalizeConfig(cfg *KubernetesPodLogsConfig) { + cfg.APIServer = strings.TrimSpace(cfg.APIServer) + cfg.TokenPath = strings.TrimSpace(cfg.TokenPath) + cfg.CACertPath = strings.TrimSpace(cfg.CACertPath) + cfg.BearerToken = strings.TrimSpace(cfg.BearerToken) + cfg.LabelSelector = strings.TrimSpace(cfg.LabelSelector) + cfg.NodeName = strings.TrimSpace(cfg.NodeName) + + if cfg.NodeName == "" { + cfg.NodeName = strings.TrimSpace(os.Getenv(nodeNameEnvKey)) + } + + if cfg.APIServer == "" { + cfg.APIServer = defaultAPIServer + } + + if cfg.Mode == "" { + cfg.Mode = configuration.TAIL_MODE + } + + if cfg.ResyncPeriod == 0 { + cfg.ResyncPeriod = defaultResyncPeriod + } + + if cfg.RequestTimeout == 0 { + cfg.RequestTimeout = defaultRequestTimeout + } + + if cfg.MaxLineBytes == 0 { + cfg.MaxLineBytes = defaultMaxLineBytes + } +} + +func (k *KubernetesPodLogsSource) validateConfig(cfg KubernetesPodLogsConfig) error { + if cfg.NodeName == "" { + return errors.New("node_name must be set or NODE_NAME environment variable must be provided") + } + + if cfg.Mode != configuration.TAIL_MODE { + return fmt.Errorf("unsupported mode %s for k8s-podlogs datasource", cfg.Mode) + } + + if cfg.LimitBytes < 0 { + return errors.New("limit_bytes cannot be negative") + } + + if cfg.SinceSeconds != nil && *cfg.SinceSeconds < 0 { + return errors.New("since_seconds cannot be negative") + } + + if cfg.TailLines != nil && *cfg.TailLines < 0 { + return errors.New("tail_lines cannot be negative") + } + + if cfg.ResyncPeriod < 0 { + return errors.New("resync_period cannot be negative") + } + + if cfg.RequestTimeout < 0 { + return errors.New("request_timeout cannot be negative") + } + + if cfg.MaxLineBytes < 0 { + return errors.New("max_line_bytes cannot be negative") + } + + if cfg.TokenPath == "" && cfg.BearerToken == "" { + return errors.New("either token_file or bearer_token must be set") + } + + if cfg.APIServer == "" { + return errors.New("api_server cannot be empty") + } + + return nil +} + +func (k *KubernetesPodLogsSource) GetUuid() string { + return k.config.UniqueId +} + +func (k *KubernetesPodLogsSource) GetMetrics() []prometheus.Collector { + return []prometheus.Collector{metrics.K8SPodLogsLines} +} + +func (k *KubernetesPodLogsSource) GetAggregMetrics() []prometheus.Collector { + return []prometheus.Collector{metrics.K8SPodLogsLines} +} + +func (k *KubernetesPodLogsSource) GetMode() string { + return k.config.Mode +} + +func (k *KubernetesPodLogsSource) GetName() string { + return "k8s-podlogs" +} + +func (k *KubernetesPodLogsSource) Dump() any { + return k +} + +func (*KubernetesPodLogsSource) CanRun() error { + return nil +} + +func (k *KubernetesPodLogsSource) UnmarshalConfig(yamlConfig []byte) error { + cfg := defaultKubePodLogsConfig() + + if err := yaml.UnmarshalWithOptions(yamlConfig, &cfg, yaml.Strict()); err != nil { + return fmt.Errorf("cannot parse k8s-podlogs configuration: %s", yaml.FormatError(err, false, false)) + } + + k.normalizeConfig(&cfg) + + if err := k.validateConfig(cfg); err != nil { + return err + } + + k.namespaceFilter = buildSet(cfg.Namespaces) + k.containerFilter = buildSet(cfg.Containers) + + k.config = cfg + + return nil +} + +func (k *KubernetesPodLogsSource) Configure(config []byte, logger *log.Entry, metricsLevel metrics.AcquisitionMetricsLevel) error { + k.logger = logger + k.metricsLevel = metricsLevel + + if err := k.UnmarshalConfig(config); err != nil { + return err + } + + k.apiServer = strings.TrimRight(k.config.APIServer, "/") + if k.apiServer == "" { + k.apiServer = defaultAPIServer + } + + if err := k.loadToken(); err != nil { + return err + } + + if err := k.buildHTTPClients(); err != nil { + return err + } + + k.activeStreams = make(map[podLogKey]context.CancelFunc) + + if k.logger != nil && k.logger.Logger.IsLevelEnabled(log.TraceLevel) { + safeCfg := k.config + if safeCfg.BearerToken != "" { + safeCfg.BearerToken = "***" + } + k.logger.Tracef("k8s-podlogs configuration: %+v", safeCfg) + } + + return nil +} + +func (k *KubernetesPodLogsSource) StreamingAcquisition(ctx context.Context, out chan types.Event, t *tomb.Tomb) error { + k.out = out + + runCtx, cancel := context.WithCancel(ctx) + + t.Go(func() error { + <-t.Dying() + cancel() + return nil + }) + + t.Go(func() error { + defer trace.CatchPanic("crowdsec/acquis/k8s-podlogs/live") + return k.run(runCtx, t) + }) + + return nil +} + +func (k *KubernetesPodLogsSource) run(ctx context.Context, t *tomb.Tomb) error { + if err := k.syncStreams(ctx, t); err != nil { + return err + } + + ticker := time.NewTicker(k.config.ResyncPeriod) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + k.stopAll() + return nil + case <-ticker.C: + if err := k.syncStreams(ctx, t); err != nil { + k.logger.Errorf("k8s-podlogs sync error: %v", err) + } + } + } +} + +func (k *KubernetesPodLogsSource) syncStreams(ctx context.Context, t *tomb.Tomb) error { + targets, err := k.listTargets(ctx) + if err != nil { + return err + } + + desired := make(map[podLogKey]struct{}, len(targets)) + + for _, target := range targets { + key := target.key() + desired[key] = struct{}{} + k.startStreamIfNeeded(ctx, t, target) + } + + k.stopMissing(desired) + + return nil +} + +func (k *KubernetesPodLogsSource) listTargets(ctx context.Context) ([]podLogTarget, error) { + pods, err := k.fetchPods(ctx) + if err != nil { + return nil, err + } + + targets := make([]podLogTarget, 0) + + for _, pod := range pods { + if pod.Status.Phase != corev1.PodRunning { + continue + } + + if !k.allowedNamespace(pod.Namespace) { + continue + } + + for _, container := range pod.Spec.Containers { + if !k.allowedContainer(container.Name) { + continue + } + + if !isContainerRunning(pod.Status.ContainerStatuses, container.Name) { + continue + } + + targets = append(targets, podLogTarget{Namespace: pod.Namespace, Pod: pod.Name, Container: container.Name}) + } + } + + return targets, nil +} + +func (k *KubernetesPodLogsSource) fetchPods(ctx context.Context) ([]corev1.Pod, error) { + endpoint := fmt.Sprintf("%s/api/v1/pods", k.apiServer) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("building pods request: %w", err) + } + + query := req.URL.Query() + query.Set("fieldSelector", fields.OneTermEqualSelector("spec.nodeName", k.config.NodeName).String()) + if k.config.LabelSelector != "" { + query.Set("labelSelector", k.config.LabelSelector) + } + req.URL.RawQuery = query.Encode() + k.decorateRequest(req) + req.Header.Set("Accept", "application/json") + + resp, err := k.apiClient.Do(req) + if err != nil { + return nil, fmt.Errorf("listing pods: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("listing pods failed: %s: %s", resp.Status, readBodySnippet(resp.Body)) + } + + var list corev1.PodList + if err := json.NewDecoder(resp.Body).Decode(&list); err != nil { + return nil, fmt.Errorf("decoding pods response: %w", err) + } + + return list.Items, nil +} + +func (k *KubernetesPodLogsSource) startStreamIfNeeded(ctx context.Context, t *tomb.Tomb, target podLogTarget) { + key := target.key() + + k.activeMu.Lock() + if _, exists := k.activeStreams[key]; exists { + k.activeMu.Unlock() + return + } + + streamCtx, cancel := context.WithCancel(ctx) + k.activeStreams[key] = cancel + k.activeMu.Unlock() + + t.Go(func() error { + defer func() { + cancel() + k.removeStream(key) + }() + + if err := k.consumeLogs(streamCtx, target); err != nil && !errors.Is(err, context.Canceled) { + k.logger.Errorf("log stream %s failed: %v", key.String(), err) + } + + return nil + }) +} + +func (k *KubernetesPodLogsSource) consumeLogs(ctx context.Context, target podLogTarget) error { + req, err := k.newLogRequest(ctx, target) + if err != nil { + return err + } + + resp, err := k.streamClient.Do(req) + if err != nil { + return fmt.Errorf("opening log stream for %s: %w", target.String(), err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("log stream for %s failed: %s: %s", target.String(), resp.Status, readBodySnippet(resp.Body)) + } + + return k.scanStream(ctx, resp.Body, target) +} + +func (k *KubernetesPodLogsSource) scanStream(ctx context.Context, body io.ReadCloser, target podLogTarget) error { + done := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + _ = body.Close() + case <-done: + } + }() + + scanner := bufio.NewScanner(body) + scanner.Buffer(make([]byte, 0, 64*1024), k.config.MaxLineBytes) + + for scanner.Scan() { + k.emitLine(target, scanner.Text()) + } + + close(done) + + if err := scanner.Err(); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) { + return fmt.Errorf("reading log stream for %s: %w", target.String(), err) + } + + return nil +} + +func (k *KubernetesPodLogsSource) emitLine(target podLogTarget, raw string) { + line := strings.TrimRight(raw, "\r\n") + if line == "" { + return + } + + labels := k.buildLabels(target) + + evt := types.MakeEvent(k.config.UseTimeMachine, types.LOG, true) + evt.Line = types.Line{ + Raw: line, + Labels: labels, + Time: time.Now().UTC(), + Src: target.String(), + Process: true, + Module: k.GetName(), + } + + if k.metricsLevel != metrics.AcquisitionMetricsLevelNone { + metrics.K8SPodLogsLines.WithLabelValues(k.metricSource(), k.GetName(), labels["type"]).Inc() + } + + k.out <- evt +} + +func (k *KubernetesPodLogsSource) buildLabels(target podLogTarget) map[string]string { + labels := make(map[string]string, len(k.config.Labels)+4) + for key, value := range k.config.Labels { + labels[key] = value + } + + labels["k8s_namespace"] = target.Namespace + labels["k8s_pod"] = target.Pod + labels["k8s_container"] = target.Container + labels["k8s_node"] = k.config.NodeName + + return labels +} + +func (k *KubernetesPodLogsSource) metricSource() string { + if k.config.Name != "" { + return k.config.Name + } + + return k.config.NodeName +} + +func (k *KubernetesPodLogsSource) newLogRequest(ctx context.Context, target podLogTarget) (*http.Request, error) { + endpoint := fmt.Sprintf("%s/api/v1/namespaces/%s/pods/%s/log", k.apiServer, target.Namespace, target.Pod) + + u, err := url.Parse(endpoint) + if err != nil { + return nil, fmt.Errorf("invalid log endpoint: %w", err) + } + + query := u.Query() + query.Set("container", target.Container) + query.Set("follow", fmt.Sprintf("%t", k.config.Follow)) + if k.config.IncludeTimestamps { + query.Set("timestamps", "true") + } + if k.config.SinceSeconds != nil { + query.Set("sinceSeconds", fmt.Sprintf("%d", *k.config.SinceSeconds)) + } + if k.config.TailLines != nil { + query.Set("tailLines", fmt.Sprintf("%d", *k.config.TailLines)) + } + if k.config.LimitBytes > 0 { + query.Set("limitBytes", fmt.Sprintf("%d", k.config.LimitBytes)) + } + u.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, fmt.Errorf("building log request: %w", err) + } + + k.decorateRequest(req) + req.Header.Set("Accept", "text/plain") + return req, nil +} + +func (k *KubernetesPodLogsSource) decorateRequest(req *http.Request) { + req.Header.Set("User-Agent", userAgent) + if k.authToken != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", k.authToken)) + } +} + +func (k *KubernetesPodLogsSource) startTLSConfig() (*tls.Config, error) { + tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} + + if k.config.InsecureSkipTLS { + tlsConfig.InsecureSkipVerify = true + return tlsConfig, nil + } + + pool, err := x509.SystemCertPool() + if err != nil { + pool = x509.NewCertPool() + } + + if k.config.CACertPath != "" { + pemBytes, err := os.ReadFile(k.config.CACertPath) + if err != nil { + return nil, fmt.Errorf("reading CA certificate: %w", err) + } + if ok := pool.AppendCertsFromPEM(pemBytes); !ok { + return nil, errors.New("unable to append CA certificate") + } + } + + tlsConfig.RootCAs = pool + + return tlsConfig, nil +} + +func (k *KubernetesPodLogsSource) buildHTTPClients() error { + tlsConfig, err := k.startTLSConfig() + if err != nil { + return err + } + + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + + k.apiClient = &http.Client{ + Timeout: k.config.RequestTimeout, + Transport: transport, + } + + k.streamClient = &http.Client{ + Transport: transport, + } + + return nil +} + +func (k *KubernetesPodLogsSource) loadToken() error { + if k.config.BearerToken != "" { + k.authToken = k.config.BearerToken + return nil + } + + bytes, err := os.ReadFile(k.config.TokenPath) + if err != nil { + return fmt.Errorf("reading token file %s: %w", k.config.TokenPath, err) + } + + token := strings.TrimSpace(string(bytes)) + if token == "" { + return errors.New("token file is empty") + } + + k.authToken = token + + return nil +} + +func (k *KubernetesPodLogsSource) stopMissing(desired map[podLogKey]struct{}) { + k.activeMu.Lock() + defer k.activeMu.Unlock() + + for key, cancel := range k.activeStreams { + if _, ok := desired[key]; ok { + continue + } + cancel() + delete(k.activeStreams, key) + } +} + +func (k *KubernetesPodLogsSource) stopAll() { + k.activeMu.Lock() + defer k.activeMu.Unlock() + + for key, cancel := range k.activeStreams { + k.logger.Debugf("stopping log stream %s", key.String()) + cancel() + } + + k.activeStreams = make(map[podLogKey]context.CancelFunc) +} + +func (k *KubernetesPodLogsSource) removeStream(key podLogKey) { + k.activeMu.Lock() + delete(k.activeStreams, key) + k.activeMu.Unlock() +} + +func (k *KubernetesPodLogsSource) allowedNamespace(ns string) bool { + if len(k.namespaceFilter) == 0 { + return true + } + + _, ok := k.namespaceFilter[ns] + return ok +} + +func (k *KubernetesPodLogsSource) allowedContainer(container string) bool { + if len(k.containerFilter) == 0 { + return true + } + + _, ok := k.containerFilter[container] + return ok +} + +func buildSet(items []string) map[string]struct{} { + set := make(map[string]struct{}) + for _, item := range items { + trimmed := strings.TrimSpace(item) + if trimmed == "" { + continue + } + set[trimmed] = struct{}{} + } + return set +} + +func isContainerRunning(statuses []corev1.ContainerStatus, name string) bool { + for _, status := range statuses { + if status.Name == name && status.State.Running != nil { + return true + } + } + + return false +} + +func (t podLogTarget) key() podLogKey { + return podLogKey{ + Namespace: t.Namespace, + Pod: t.Pod, + Container: t.Container, + } +} + +func (k podLogKey) String() string { + return fmt.Sprintf("%s/%s/%s", k.Namespace, k.Pod, k.Container) +} + +func readBodySnippet(body io.ReadCloser) string { + defer body.Close() + data, err := io.ReadAll(io.LimitReader(body, maxErrorBodyBytes)) + if err != nil { + return "" + } + + return strings.TrimSpace(string(data)) +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml new file mode 100644 index 00000000000..d0fc09da543 --- /dev/null +++ b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml @@ -0,0 +1,109 @@ +$schema: "http://json-schema.org/draft-07/schema#" +title: CrowdSec Kubernetes pod logs datasource configuration +type: object +required: + - type + - node_name +properties: + type: + const: k8s-podlogs + api_server: + type: string + description: Base URL of the Kubernetes API server. + default: https://kubernetes.default.svc + token_file: + type: string + description: Path to the service account token used to authenticate against the API server; at least one of token_file or bearer_token must be provided. + default: /var/run/secrets/kubernetes.io/serviceaccount/token + bearer_token: + type: string + description: Raw bearer token to use instead of reading token_file; at least one of bearer_token or token_file must be provided. + ca_cert: + type: string + description: Path to the CA bundle validating the API server certificate. + default: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + insecure_skip_verify: + type: boolean + description: Disable TLS verification when connecting to the API server. + default: false + node_name: + type: string + description: Name of the node whose pods should be tailed; typically set via the NODE_NAME environment variable. + namespaces: + type: array + description: Optional allow-list of namespaces to watch (defaults to all namespaces). + items: + type: string + label_selector: + type: string + description: Kubernetes label selector applied when listing pods. + containers: + type: array + description: Optional list of container names to include; defaults to all containers. + items: + type: string + since_seconds: + type: integer + minimum: 0 + description: Start log streaming from events newer than the provided age. + tail_lines: + type: integer + minimum: 0 + description: Number of most-recent log lines to read before following. + limit_bytes: + type: integer + minimum: 0 + description: Maximum number of bytes returned by the Kubernetes log endpoint for each request. + timestamps: + type: boolean + description: Request Kubernetes to prefix every log line with a timestamp. + default: false + follow: + type: boolean + description: Keep the log stream open to follow new entries. + default: true + resync_period: + type: string + pattern: '^([0-9]+(ns|us|ms|s|m|h))+$' + description: Interval between reconciliation cycles where new pod streams are created and stale ones are stopped. + default: 30s + request_timeout: + type: string + pattern: '^([0-9]+(ns|us|ms|s|m|h))+$' + description: Timeout applied to Kubernetes API list calls. + default: 10s + max_line_bytes: + type: integer + minimum: 1024 + description: Maximum accepted log line size before failing the stream. + default: 1048576 + mode: + type: string + enum: [tail] + description: Acquisition mode; kubernetes pod logs only support tail. + default: tail + labels: + type: object + additionalProperties: + type: string + description: Optional labels attached to every emitted event. + log_level: + type: string + enum: [panic, fatal, error, warn, info, debug, trace] + description: Overrides datasource logger level. + source: + type: string + description: Custom source string stamped on events. + name: + type: string + description: Datasource name used in metrics and logs. + use_time_machine: + type: boolean + description: Enable acquisition time-machine mode. + unique_id: + type: string + description: Stable identifier used for deduplication. + transform: + type: string + description: ExprLang transform applied to events. +additionalProperties: false diff --git a/pkg/metrics/acquisition_k8s_podlogs.go b/pkg/metrics/acquisition_k8s_podlogs.go new file mode 100644 index 00000000000..ec1963c4b1c --- /dev/null +++ b/pkg/metrics/acquisition_k8s_podlogs.go @@ -0,0 +1,20 @@ +//go:build !no_datasource_k8s_podlogs + +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +const K8SPodLogsLinesMetricName = "cs_k8spodlogs_lines_total" + +var K8SPodLogsLines = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: K8SPodLogsLinesMetricName, + Help: "Total lines collected by the Kubernetes pod logs datasource.", + }, + []string{"source", "datasource_type", "acquis_type"}, +) + +//nolint:gochecknoinits +func init() { + RegisterAcquisitionMetric(K8SPodLogsLinesMetricName) +} From eaab20095305adeab3c74b9769928662976f5c39 Mon Sep 17 00:00:00 2001 From: sabban Date: Fri, 21 Nov 2025 10:55:43 +0100 Subject: [PATCH 02/23] initial commit --- pkg/acquisition/k8s_podlogs.go | 19 + .../modules/kubernetespodlogs/.keep | 0 .../modules/kubernetespodlogs/k8s_podlogs.go | 723 ++++++++++++++++++ .../kubernetespodlogs/k8s_podlogs_schema.yaml | 109 +++ pkg/metrics/acquisition_k8s_podlogs.go | 20 + 5 files changed, 871 insertions(+) create mode 100644 pkg/acquisition/k8s_podlogs.go create mode 100644 pkg/acquisition/modules/kubernetespodlogs/.keep create mode 100644 pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go create mode 100644 pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml create mode 100644 pkg/metrics/acquisition_k8s_podlogs.go diff --git a/pkg/acquisition/k8s_podlogs.go b/pkg/acquisition/k8s_podlogs.go new file mode 100644 index 00000000000..28aa40a5ed8 --- /dev/null +++ b/pkg/acquisition/k8s_podlogs.go @@ -0,0 +1,19 @@ +//go:build !no_datasource_k8s_podlogs + +package acquisition + +import ( + kubernetespodlogs "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/kubernetespodlogs" +) + +var ( + // verify interface compliance + _ DataSource = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) + _ Tailer = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) + _ MetricsProvider = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) +) + +//nolint:gochecknoinits +func init() { + registerDataSource("k8s-podlogs", func() DataSource { return &kubernetespodlogs.KubernetesPodLogsSource{} }) +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/.keep b/pkg/acquisition/modules/kubernetespodlogs/.keep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go new file mode 100644 index 00000000000..ad0b8f0f44d --- /dev/null +++ b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go @@ -0,0 +1,723 @@ +//go:build !no_datasource_k8s_podlogs + +package kubernetespodlogs + +import ( + "bufio" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "sync" + "time" + + yaml "github.com/goccy/go-yaml" + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" + "gopkg.in/tomb.v2" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + + "github.com/crowdsecurity/go-cs-lib/trace" + + "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" + "github.com/crowdsecurity/crowdsec/pkg/metrics" + "github.com/crowdsecurity/crowdsec/pkg/types" +) + +const ( + defaultAPIServer = "https://kubernetes.default.svc" + defaultTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + defaultCACertPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + defaultResyncPeriod = 30 * time.Second + defaultRequestTimeout = 10 * time.Second + defaultMaxLineBytes = 1 << 20 + maxErrorBodyBytes = 4096 + userAgent = "crowdsec-k8s-podlogs" + nodeNameEnvKey = "NODE_NAME" +) + +type KubernetesPodLogsConfig struct { + configuration.DataSourceCommonCfg `yaml:",inline"` + + APIServer string `yaml:"api_server"` + TokenPath string `yaml:"token_file"` + BearerToken string `yaml:"bearer_token"` + CACertPath string `yaml:"ca_cert"` + InsecureSkipTLS bool `yaml:"insecure_skip_verify"` + NodeName string `yaml:"node_name"` + Namespaces []string `yaml:"namespaces"` + LabelSelector string `yaml:"label_selector"` + Containers []string `yaml:"containers"` + SinceSeconds *int64 `yaml:"since_seconds"` + TailLines *int64 `yaml:"tail_lines"` + LimitBytes int64 `yaml:"limit_bytes"` + IncludeTimestamps bool `yaml:"timestamps"` + Follow bool `yaml:"follow"` + ResyncPeriod time.Duration `yaml:"resync_period"` + RequestTimeout time.Duration `yaml:"request_timeout"` + MaxLineBytes int `yaml:"max_line_bytes"` +} + +type podLogTarget struct { + Namespace string + Pod string + Container string +} + +func (t podLogTarget) String() string { + return fmt.Sprintf("%s/%s/%s", t.Namespace, t.Pod, t.Container) +} + +type podLogKey struct { + Namespace string + Pod string + Container string +} + +type KubernetesPodLogsSource struct { + config KubernetesPodLogsConfig + logger *log.Entry + metricsLevel metrics.AcquisitionMetricsLevel + apiClient *http.Client + streamClient *http.Client + authToken string + apiServer string + namespaceFilter map[string]struct{} + containerFilter map[string]struct{} + out chan types.Event + activeStreams map[podLogKey]context.CancelFunc + activeMu sync.Mutex +} + +func defaultKubePodLogsConfig() KubernetesPodLogsConfig { + return KubernetesPodLogsConfig{ + APIServer: defaultAPIServer, + TokenPath: defaultTokenPath, + CACertPath: defaultCACertPath, + Follow: true, + ResyncPeriod: defaultResyncPeriod, + RequestTimeout: defaultRequestTimeout, + MaxLineBytes: defaultMaxLineBytes, + } +} + +func (k *KubernetesPodLogsSource) normalizeConfig(cfg *KubernetesPodLogsConfig) { + cfg.APIServer = strings.TrimSpace(cfg.APIServer) + cfg.TokenPath = strings.TrimSpace(cfg.TokenPath) + cfg.CACertPath = strings.TrimSpace(cfg.CACertPath) + cfg.BearerToken = strings.TrimSpace(cfg.BearerToken) + cfg.LabelSelector = strings.TrimSpace(cfg.LabelSelector) + cfg.NodeName = strings.TrimSpace(cfg.NodeName) + + if cfg.NodeName == "" { + cfg.NodeName = strings.TrimSpace(os.Getenv(nodeNameEnvKey)) + } + + if cfg.APIServer == "" { + cfg.APIServer = defaultAPIServer + } + + if cfg.Mode == "" { + cfg.Mode = configuration.TAIL_MODE + } + + if cfg.ResyncPeriod == 0 { + cfg.ResyncPeriod = defaultResyncPeriod + } + + if cfg.RequestTimeout == 0 { + cfg.RequestTimeout = defaultRequestTimeout + } + + if cfg.MaxLineBytes == 0 { + cfg.MaxLineBytes = defaultMaxLineBytes + } +} + +func (k *KubernetesPodLogsSource) validateConfig(cfg KubernetesPodLogsConfig) error { + if cfg.NodeName == "" { + return errors.New("node_name must be set or NODE_NAME environment variable must be provided") + } + + if cfg.Mode != configuration.TAIL_MODE { + return fmt.Errorf("unsupported mode %s for k8s-podlogs datasource", cfg.Mode) + } + + if cfg.LimitBytes < 0 { + return errors.New("limit_bytes cannot be negative") + } + + if cfg.SinceSeconds != nil && *cfg.SinceSeconds < 0 { + return errors.New("since_seconds cannot be negative") + } + + if cfg.TailLines != nil && *cfg.TailLines < 0 { + return errors.New("tail_lines cannot be negative") + } + + if cfg.ResyncPeriod < 0 { + return errors.New("resync_period cannot be negative") + } + + if cfg.RequestTimeout < 0 { + return errors.New("request_timeout cannot be negative") + } + + if cfg.MaxLineBytes < 0 { + return errors.New("max_line_bytes cannot be negative") + } + + if cfg.TokenPath == "" && cfg.BearerToken == "" { + return errors.New("either token_file or bearer_token must be set") + } + + if cfg.APIServer == "" { + return errors.New("api_server cannot be empty") + } + + return nil +} + +func (k *KubernetesPodLogsSource) GetUuid() string { + return k.config.UniqueId +} + +func (k *KubernetesPodLogsSource) GetMetrics() []prometheus.Collector { + return []prometheus.Collector{metrics.K8SPodLogsLines} +} + +func (k *KubernetesPodLogsSource) GetAggregMetrics() []prometheus.Collector { + return []prometheus.Collector{metrics.K8SPodLogsLines} +} + +func (k *KubernetesPodLogsSource) GetMode() string { + return k.config.Mode +} + +func (k *KubernetesPodLogsSource) GetName() string { + return "k8s-podlogs" +} + +func (k *KubernetesPodLogsSource) Dump() any { + return k +} + +func (*KubernetesPodLogsSource) CanRun() error { + return nil +} + +func (k *KubernetesPodLogsSource) UnmarshalConfig(yamlConfig []byte) error { + cfg := defaultKubePodLogsConfig() + + if err := yaml.UnmarshalWithOptions(yamlConfig, &cfg, yaml.Strict()); err != nil { + return fmt.Errorf("cannot parse k8s-podlogs configuration: %s", yaml.FormatError(err, false, false)) + } + + k.normalizeConfig(&cfg) + + if err := k.validateConfig(cfg); err != nil { + return err + } + + k.namespaceFilter = buildSet(cfg.Namespaces) + k.containerFilter = buildSet(cfg.Containers) + + k.config = cfg + + return nil +} + +func (k *KubernetesPodLogsSource) Configure(config []byte, logger *log.Entry, metricsLevel metrics.AcquisitionMetricsLevel) error { + k.logger = logger + k.metricsLevel = metricsLevel + + if err := k.UnmarshalConfig(config); err != nil { + return err + } + + k.apiServer = strings.TrimRight(k.config.APIServer, "/") + if k.apiServer == "" { + k.apiServer = defaultAPIServer + } + + if err := k.loadToken(); err != nil { + return err + } + + if err := k.buildHTTPClients(); err != nil { + return err + } + + k.activeStreams = make(map[podLogKey]context.CancelFunc) + + if k.logger != nil && k.logger.Logger.IsLevelEnabled(log.TraceLevel) { + safeCfg := k.config + if safeCfg.BearerToken != "" { + safeCfg.BearerToken = "***" + } + k.logger.Tracef("k8s-podlogs configuration: %+v", safeCfg) + } + + return nil +} + +func (k *KubernetesPodLogsSource) StreamingAcquisition(ctx context.Context, out chan types.Event, t *tomb.Tomb) error { + k.out = out + + runCtx, cancel := context.WithCancel(ctx) + + t.Go(func() error { + <-t.Dying() + cancel() + return nil + }) + + t.Go(func() error { + defer trace.CatchPanic("crowdsec/acquis/k8s-podlogs/live") + return k.run(runCtx, t) + }) + + return nil +} + +func (k *KubernetesPodLogsSource) run(ctx context.Context, t *tomb.Tomb) error { + if err := k.syncStreams(ctx, t); err != nil { + return err + } + + ticker := time.NewTicker(k.config.ResyncPeriod) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + k.stopAll() + return nil + case <-ticker.C: + if err := k.syncStreams(ctx, t); err != nil { + k.logger.Errorf("k8s-podlogs sync error: %v", err) + } + } + } +} + +func (k *KubernetesPodLogsSource) syncStreams(ctx context.Context, t *tomb.Tomb) error { + targets, err := k.listTargets(ctx) + if err != nil { + return err + } + + desired := make(map[podLogKey]struct{}, len(targets)) + + for _, target := range targets { + key := target.key() + desired[key] = struct{}{} + k.startStreamIfNeeded(ctx, t, target) + } + + k.stopMissing(desired) + + return nil +} + +func (k *KubernetesPodLogsSource) listTargets(ctx context.Context) ([]podLogTarget, error) { + pods, err := k.fetchPods(ctx) + if err != nil { + return nil, err + } + + targets := make([]podLogTarget, 0) + + for _, pod := range pods { + if pod.Status.Phase != corev1.PodRunning { + continue + } + + if !k.allowedNamespace(pod.Namespace) { + continue + } + + for _, container := range pod.Spec.Containers { + if !k.allowedContainer(container.Name) { + continue + } + + if !isContainerRunning(pod.Status.ContainerStatuses, container.Name) { + continue + } + + targets = append(targets, podLogTarget{Namespace: pod.Namespace, Pod: pod.Name, Container: container.Name}) + } + } + + return targets, nil +} + +func (k *KubernetesPodLogsSource) fetchPods(ctx context.Context) ([]corev1.Pod, error) { + endpoint := fmt.Sprintf("%s/api/v1/pods", k.apiServer) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("building pods request: %w", err) + } + + query := req.URL.Query() + query.Set("fieldSelector", fields.OneTermEqualSelector("spec.nodeName", k.config.NodeName).String()) + if k.config.LabelSelector != "" { + query.Set("labelSelector", k.config.LabelSelector) + } + req.URL.RawQuery = query.Encode() + k.decorateRequest(req) + req.Header.Set("Accept", "application/json") + + resp, err := k.apiClient.Do(req) + if err != nil { + return nil, fmt.Errorf("listing pods: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("listing pods failed: %s: %s", resp.Status, readBodySnippet(resp.Body)) + } + + var list corev1.PodList + if err := json.NewDecoder(resp.Body).Decode(&list); err != nil { + return nil, fmt.Errorf("decoding pods response: %w", err) + } + + return list.Items, nil +} + +func (k *KubernetesPodLogsSource) startStreamIfNeeded(ctx context.Context, t *tomb.Tomb, target podLogTarget) { + key := target.key() + + k.activeMu.Lock() + if _, exists := k.activeStreams[key]; exists { + k.activeMu.Unlock() + return + } + + streamCtx, cancel := context.WithCancel(ctx) + k.activeStreams[key] = cancel + k.activeMu.Unlock() + + t.Go(func() error { + defer func() { + cancel() + k.removeStream(key) + }() + + if err := k.consumeLogs(streamCtx, target); err != nil && !errors.Is(err, context.Canceled) { + k.logger.Errorf("log stream %s failed: %v", key.String(), err) + } + + return nil + }) +} + +func (k *KubernetesPodLogsSource) consumeLogs(ctx context.Context, target podLogTarget) error { + req, err := k.newLogRequest(ctx, target) + if err != nil { + return err + } + + resp, err := k.streamClient.Do(req) + if err != nil { + return fmt.Errorf("opening log stream for %s: %w", target.String(), err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("log stream for %s failed: %s: %s", target.String(), resp.Status, readBodySnippet(resp.Body)) + } + + return k.scanStream(ctx, resp.Body, target) +} + +func (k *KubernetesPodLogsSource) scanStream(ctx context.Context, body io.ReadCloser, target podLogTarget) error { + done := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + _ = body.Close() + case <-done: + } + }() + + scanner := bufio.NewScanner(body) + scanner.Buffer(make([]byte, 0, 64*1024), k.config.MaxLineBytes) + + for scanner.Scan() { + k.emitLine(target, scanner.Text()) + } + + close(done) + + if err := scanner.Err(); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) { + return fmt.Errorf("reading log stream for %s: %w", target.String(), err) + } + + return nil +} + +func (k *KubernetesPodLogsSource) emitLine(target podLogTarget, raw string) { + line := strings.TrimRight(raw, "\r\n") + if line == "" { + return + } + + labels := k.buildLabels(target) + + evt := types.MakeEvent(k.config.UseTimeMachine, types.LOG, true) + evt.Line = types.Line{ + Raw: line, + Labels: labels, + Time: time.Now().UTC(), + Src: target.String(), + Process: true, + Module: k.GetName(), + } + + if k.metricsLevel != metrics.AcquisitionMetricsLevelNone { + metrics.K8SPodLogsLines.WithLabelValues(k.metricSource(), k.GetName(), labels["type"]).Inc() + } + + k.out <- evt +} + +func (k *KubernetesPodLogsSource) buildLabels(target podLogTarget) map[string]string { + labels := make(map[string]string, len(k.config.Labels)+4) + for key, value := range k.config.Labels { + labels[key] = value + } + + labels["k8s_namespace"] = target.Namespace + labels["k8s_pod"] = target.Pod + labels["k8s_container"] = target.Container + labels["k8s_node"] = k.config.NodeName + + return labels +} + +func (k *KubernetesPodLogsSource) metricSource() string { + if k.config.Name != "" { + return k.config.Name + } + + return k.config.NodeName +} + +func (k *KubernetesPodLogsSource) newLogRequest(ctx context.Context, target podLogTarget) (*http.Request, error) { + endpoint := fmt.Sprintf("%s/api/v1/namespaces/%s/pods/%s/log", k.apiServer, target.Namespace, target.Pod) + + u, err := url.Parse(endpoint) + if err != nil { + return nil, fmt.Errorf("invalid log endpoint: %w", err) + } + + query := u.Query() + query.Set("container", target.Container) + query.Set("follow", fmt.Sprintf("%t", k.config.Follow)) + if k.config.IncludeTimestamps { + query.Set("timestamps", "true") + } + if k.config.SinceSeconds != nil { + query.Set("sinceSeconds", fmt.Sprintf("%d", *k.config.SinceSeconds)) + } + if k.config.TailLines != nil { + query.Set("tailLines", fmt.Sprintf("%d", *k.config.TailLines)) + } + if k.config.LimitBytes > 0 { + query.Set("limitBytes", fmt.Sprintf("%d", k.config.LimitBytes)) + } + u.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, fmt.Errorf("building log request: %w", err) + } + + k.decorateRequest(req) + req.Header.Set("Accept", "text/plain") + return req, nil +} + +func (k *KubernetesPodLogsSource) decorateRequest(req *http.Request) { + req.Header.Set("User-Agent", userAgent) + if k.authToken != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", k.authToken)) + } +} + +func (k *KubernetesPodLogsSource) startTLSConfig() (*tls.Config, error) { + tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} + + if k.config.InsecureSkipTLS { + tlsConfig.InsecureSkipVerify = true + return tlsConfig, nil + } + + pool, err := x509.SystemCertPool() + if err != nil { + pool = x509.NewCertPool() + } + + if k.config.CACertPath != "" { + pemBytes, err := os.ReadFile(k.config.CACertPath) + if err != nil { + return nil, fmt.Errorf("reading CA certificate: %w", err) + } + if ok := pool.AppendCertsFromPEM(pemBytes); !ok { + return nil, errors.New("unable to append CA certificate") + } + } + + tlsConfig.RootCAs = pool + + return tlsConfig, nil +} + +func (k *KubernetesPodLogsSource) buildHTTPClients() error { + tlsConfig, err := k.startTLSConfig() + if err != nil { + return err + } + + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + + k.apiClient = &http.Client{ + Timeout: k.config.RequestTimeout, + Transport: transport, + } + + k.streamClient = &http.Client{ + Transport: transport, + } + + return nil +} + +func (k *KubernetesPodLogsSource) loadToken() error { + if k.config.BearerToken != "" { + k.authToken = k.config.BearerToken + return nil + } + + bytes, err := os.ReadFile(k.config.TokenPath) + if err != nil { + return fmt.Errorf("reading token file %s: %w", k.config.TokenPath, err) + } + + token := strings.TrimSpace(string(bytes)) + if token == "" { + return errors.New("token file is empty") + } + + k.authToken = token + + return nil +} + +func (k *KubernetesPodLogsSource) stopMissing(desired map[podLogKey]struct{}) { + k.activeMu.Lock() + defer k.activeMu.Unlock() + + for key, cancel := range k.activeStreams { + if _, ok := desired[key]; ok { + continue + } + cancel() + delete(k.activeStreams, key) + } +} + +func (k *KubernetesPodLogsSource) stopAll() { + k.activeMu.Lock() + defer k.activeMu.Unlock() + + for key, cancel := range k.activeStreams { + k.logger.Debugf("stopping log stream %s", key.String()) + cancel() + } + + k.activeStreams = make(map[podLogKey]context.CancelFunc) +} + +func (k *KubernetesPodLogsSource) removeStream(key podLogKey) { + k.activeMu.Lock() + delete(k.activeStreams, key) + k.activeMu.Unlock() +} + +func (k *KubernetesPodLogsSource) allowedNamespace(ns string) bool { + if len(k.namespaceFilter) == 0 { + return true + } + + _, ok := k.namespaceFilter[ns] + return ok +} + +func (k *KubernetesPodLogsSource) allowedContainer(container string) bool { + if len(k.containerFilter) == 0 { + return true + } + + _, ok := k.containerFilter[container] + return ok +} + +func buildSet(items []string) map[string]struct{} { + set := make(map[string]struct{}) + for _, item := range items { + trimmed := strings.TrimSpace(item) + if trimmed == "" { + continue + } + set[trimmed] = struct{}{} + } + return set +} + +func isContainerRunning(statuses []corev1.ContainerStatus, name string) bool { + for _, status := range statuses { + if status.Name == name && status.State.Running != nil { + return true + } + } + + return false +} + +func (t podLogTarget) key() podLogKey { + return podLogKey{ + Namespace: t.Namespace, + Pod: t.Pod, + Container: t.Container, + } +} + +func (k podLogKey) String() string { + return fmt.Sprintf("%s/%s/%s", k.Namespace, k.Pod, k.Container) +} + +func readBodySnippet(body io.ReadCloser) string { + defer body.Close() + data, err := io.ReadAll(io.LimitReader(body, maxErrorBodyBytes)) + if err != nil { + return "" + } + + return strings.TrimSpace(string(data)) +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml new file mode 100644 index 00000000000..d0fc09da543 --- /dev/null +++ b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml @@ -0,0 +1,109 @@ +$schema: "http://json-schema.org/draft-07/schema#" +title: CrowdSec Kubernetes pod logs datasource configuration +type: object +required: + - type + - node_name +properties: + type: + const: k8s-podlogs + api_server: + type: string + description: Base URL of the Kubernetes API server. + default: https://kubernetes.default.svc + token_file: + type: string + description: Path to the service account token used to authenticate against the API server; at least one of token_file or bearer_token must be provided. + default: /var/run/secrets/kubernetes.io/serviceaccount/token + bearer_token: + type: string + description: Raw bearer token to use instead of reading token_file; at least one of bearer_token or token_file must be provided. + ca_cert: + type: string + description: Path to the CA bundle validating the API server certificate. + default: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + insecure_skip_verify: + type: boolean + description: Disable TLS verification when connecting to the API server. + default: false + node_name: + type: string + description: Name of the node whose pods should be tailed; typically set via the NODE_NAME environment variable. + namespaces: + type: array + description: Optional allow-list of namespaces to watch (defaults to all namespaces). + items: + type: string + label_selector: + type: string + description: Kubernetes label selector applied when listing pods. + containers: + type: array + description: Optional list of container names to include; defaults to all containers. + items: + type: string + since_seconds: + type: integer + minimum: 0 + description: Start log streaming from events newer than the provided age. + tail_lines: + type: integer + minimum: 0 + description: Number of most-recent log lines to read before following. + limit_bytes: + type: integer + minimum: 0 + description: Maximum number of bytes returned by the Kubernetes log endpoint for each request. + timestamps: + type: boolean + description: Request Kubernetes to prefix every log line with a timestamp. + default: false + follow: + type: boolean + description: Keep the log stream open to follow new entries. + default: true + resync_period: + type: string + pattern: '^([0-9]+(ns|us|ms|s|m|h))+$' + description: Interval between reconciliation cycles where new pod streams are created and stale ones are stopped. + default: 30s + request_timeout: + type: string + pattern: '^([0-9]+(ns|us|ms|s|m|h))+$' + description: Timeout applied to Kubernetes API list calls. + default: 10s + max_line_bytes: + type: integer + minimum: 1024 + description: Maximum accepted log line size before failing the stream. + default: 1048576 + mode: + type: string + enum: [tail] + description: Acquisition mode; kubernetes pod logs only support tail. + default: tail + labels: + type: object + additionalProperties: + type: string + description: Optional labels attached to every emitted event. + log_level: + type: string + enum: [panic, fatal, error, warn, info, debug, trace] + description: Overrides datasource logger level. + source: + type: string + description: Custom source string stamped on events. + name: + type: string + description: Datasource name used in metrics and logs. + use_time_machine: + type: boolean + description: Enable acquisition time-machine mode. + unique_id: + type: string + description: Stable identifier used for deduplication. + transform: + type: string + description: ExprLang transform applied to events. +additionalProperties: false diff --git a/pkg/metrics/acquisition_k8s_podlogs.go b/pkg/metrics/acquisition_k8s_podlogs.go new file mode 100644 index 00000000000..ec1963c4b1c --- /dev/null +++ b/pkg/metrics/acquisition_k8s_podlogs.go @@ -0,0 +1,20 @@ +//go:build !no_datasource_k8s_podlogs + +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +const K8SPodLogsLinesMetricName = "cs_k8spodlogs_lines_total" + +var K8SPodLogsLines = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: K8SPodLogsLinesMetricName, + Help: "Total lines collected by the Kubernetes pod logs datasource.", + }, + []string{"source", "datasource_type", "acquis_type"}, +) + +//nolint:gochecknoinits +func init() { + RegisterAcquisitionMetric(K8SPodLogsLinesMetricName) +} From d69e426b3be4101d8af92109293dde46741473ea Mon Sep 17 00:00:00 2001 From: sabban Date: Thu, 22 Jan 2026 14:18:09 +0100 Subject: [PATCH 03/23] first commit for kubernetes acquisition --- go.mod | 10 +- go.sum | 10 + pkg/acquisition/k8s_podlogs.go | 19 - .../modules/kuberneteslogs/config.go | 23 + .../modules/kuberneteslogs/metrics.go | 19 + .../modules/kubernetespodlogs/config.go | 48 ++ .../modules/kubernetespodlogs/init.go | 21 + .../modules/kubernetespodlogs/k8s_podlogs.go | 723 ------------------ .../kubernetespodlogs/k8s_podlogs_schema.yaml | 109 --- .../modules/kubernetespodlogs/metrics.go | 19 + .../modules/kubernetespodlogs/run.go | 190 +++++ .../modules/kubernetespodlogs/source.go | 33 + .../modules/kubernetespodlogs/utils.go | 21 + 13 files changed, 392 insertions(+), 853 deletions(-) delete mode 100644 pkg/acquisition/k8s_podlogs.go create mode 100644 pkg/acquisition/modules/kuberneteslogs/config.go create mode 100644 pkg/acquisition/modules/kuberneteslogs/metrics.go create mode 100644 pkg/acquisition/modules/kubernetespodlogs/config.go create mode 100644 pkg/acquisition/modules/kubernetespodlogs/init.go delete mode 100644 pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go delete mode 100644 pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml create mode 100644 pkg/acquisition/modules/kubernetespodlogs/metrics.go create mode 100644 pkg/acquisition/modules/kubernetespodlogs/run.go create mode 100644 pkg/acquisition/modules/kubernetespodlogs/source.go create mode 100644 pkg/acquisition/modules/kubernetespodlogs/utils.go diff --git a/go.mod b/go.mod index c8524364a5a..d5d45cfbcf4 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,10 @@ require ( gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.28.4 + k8s.io/apimachinery v0.28.4 k8s.io/apiserver v0.28.4 + k8s.io/client-go v0.28.4 modernc.org/sqlite v1.42.2 ) @@ -134,6 +137,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.8.4 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.0.0 // indirect @@ -153,6 +157,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.5 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect @@ -236,15 +241,15 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/arch v0.15.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/term v0.35.0 // indirect golang.org/x/tools v0.37.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - k8s.io/api v0.28.4 // indirect - k8s.io/apimachinery v0.28.4 // indirect k8s.io/klog/v2 v2.100.1 // indirect + k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect @@ -252,6 +257,7 @@ require ( rsc.io/binaryregexp v0.2.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) replace golang.org/x/time => github.com/crowdsecurity/time v0.13.0-crowdsec.20250912 diff --git a/go.sum b/go.sum index 57ac167a41c..e8db5a93719 100644 --- a/go.sum +++ b/go.sum @@ -158,6 +158,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8= github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -243,6 +245,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -688,6 +692,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -811,8 +817,12 @@ k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= k8s.io/apiserver v0.28.4 h1:BJXlaQbAU/RXYX2lRz+E1oPe3G3TKlozMMCZWu5GMgg= k8s.io/apiserver v0.28.4/go.mod h1:Idq71oXugKZoVGUUL2wgBCTHbUR+FYTWa4rq9j4n23w= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= diff --git a/pkg/acquisition/k8s_podlogs.go b/pkg/acquisition/k8s_podlogs.go deleted file mode 100644 index 28aa40a5ed8..00000000000 --- a/pkg/acquisition/k8s_podlogs.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !no_datasource_k8s_podlogs - -package acquisition - -import ( - kubernetespodlogs "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/kubernetespodlogs" -) - -var ( - // verify interface compliance - _ DataSource = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) - _ Tailer = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) - _ MetricsProvider = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) -) - -//nolint:gochecknoinits -func init() { - registerDataSource("k8s-podlogs", func() DataSource { return &kubernetespodlogs.KubernetesPodLogsSource{} }) -} diff --git a/pkg/acquisition/modules/kuberneteslogs/config.go b/pkg/acquisition/modules/kuberneteslogs/config.go new file mode 100644 index 00000000000..f108048c4ab --- /dev/null +++ b/pkg/acquisition/modules/kuberneteslogs/config.go @@ -0,0 +1,23 @@ +package kubernetesacquisition + +import ( + "context" + "errors" + "fmt" + "net/url" + "regexp" + "strconv" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" + "github.com/crowdsecurity/crowdsec/pkg/metrics" +) + +type Configuration struct { + configuration.DataSourceCommonCfg `yaml:",inline"` + + Selector string `yaml:"selector"` + Namespace string `yaml:"namespace"` +} diff --git a/pkg/acquisition/modules/kuberneteslogs/metrics.go b/pkg/acquisition/modules/kuberneteslogs/metrics.go new file mode 100644 index 00000000000..8d33d16c339 --- /dev/null +++ b/pkg/acquisition/modules/kuberneteslogs/metrics.go @@ -0,0 +1,19 @@ +package kubernetesacquisition + +import ( + "github.com/prometheus/client_golang/prometheus" + + "github.com/crowdsecurity/crowdsec/pkg/metrics" +) + +func (*Source) GetMetrics() []prometheus.Collector { + return []prometheus.Collector{ + metrics.KubernetesDatasourceLinesRead, + } +} + +func (*Source) GetAggregMetrics() []prometheus.Collector { + return []prometheus.Collector{ + metrics.KubernetesDatasourceLinesRead, + } +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/config.go b/pkg/acquisition/modules/kubernetespodlogs/config.go new file mode 100644 index 00000000000..0bf6a1092a4 --- /dev/null +++ b/pkg/acquisition/modules/kubernetespodlogs/config.go @@ -0,0 +1,48 @@ +package kubernetespodlogs + +import ( + "context" + "fmt" + + yaml "github.com/goccy/go-yaml" + log "github.com/sirupsen/logrus" + + "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" + "github.com/crowdsecurity/crowdsec/pkg/metrics" +) + +type Configuration struct { + configuration.DataSourceCommonCfg `yaml:",inline"` + + Label string `yaml:"label"` + Namespace string `yaml:"namespace"` +} + +func (d *Source) UnmarshalConfig(yamlConfig []byte) error { + d.Config = Configuration{ + Label: "", + Namespace: "default", + } + + if err := yaml.UnmarshalWithOptions(yamlConfig, &d.Config, yaml.Strict()); err != nil { + return fmt.Errorf("while parsing KubernetesAcquisition configuration: %s", yaml.FormatError(err, false, false)) + } + + if d.logger != nil { + d.logger.Tracef("DockerAcquisition configuration: %+v", d.Config) + } + + return nil +} + +func (d *Source) Configure(ctx context.Context, yamlConfig []byte, logger *log.Entry, metricsLevel metrics.AcquisitionMetricsLevel) error { + d.logger = logger + d.metricsLevel = metricsLevel + + err := d.UnmarshalConfig(yamlConfig) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/init.go b/pkg/acquisition/modules/kubernetespodlogs/init.go new file mode 100644 index 00000000000..b314a7e00ee --- /dev/null +++ b/pkg/acquisition/modules/kubernetespodlogs/init.go @@ -0,0 +1,21 @@ +package kubernetespodlogs + +import ( + "github.com/crowdsecurity/crowdsec/pkg/acquisition/registry" + "github.com/crowdsecurity/crowdsec/pkg/acquisition/types" +) + +var ( + // verify interface compliance + _ types.DataSource = (*Source)(nil) + _ types.Fetcher = (*Source)(nil) + _ types.Tailer = (*Source)(nil) + _ types.MetricsProvider = (*Source)(nil) +) + +const ModuleName = "kubernetespodlogs" + +//nolint:gochecknoinits +func init() { + registry.RegisterFactory(ModuleName, func() types.DataSource { return &Source{} }) +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go deleted file mode 100644 index ad0b8f0f44d..00000000000 --- a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go +++ /dev/null @@ -1,723 +0,0 @@ -//go:build !no_datasource_k8s_podlogs - -package kubernetespodlogs - -import ( - "bufio" - "context" - "crypto/tls" - "crypto/x509" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" - "strings" - "sync" - "time" - - yaml "github.com/goccy/go-yaml" - "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" - "gopkg.in/tomb.v2" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" - - "github.com/crowdsecurity/go-cs-lib/trace" - - "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" - "github.com/crowdsecurity/crowdsec/pkg/metrics" - "github.com/crowdsecurity/crowdsec/pkg/types" -) - -const ( - defaultAPIServer = "https://kubernetes.default.svc" - defaultTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" - defaultCACertPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - defaultResyncPeriod = 30 * time.Second - defaultRequestTimeout = 10 * time.Second - defaultMaxLineBytes = 1 << 20 - maxErrorBodyBytes = 4096 - userAgent = "crowdsec-k8s-podlogs" - nodeNameEnvKey = "NODE_NAME" -) - -type KubernetesPodLogsConfig struct { - configuration.DataSourceCommonCfg `yaml:",inline"` - - APIServer string `yaml:"api_server"` - TokenPath string `yaml:"token_file"` - BearerToken string `yaml:"bearer_token"` - CACertPath string `yaml:"ca_cert"` - InsecureSkipTLS bool `yaml:"insecure_skip_verify"` - NodeName string `yaml:"node_name"` - Namespaces []string `yaml:"namespaces"` - LabelSelector string `yaml:"label_selector"` - Containers []string `yaml:"containers"` - SinceSeconds *int64 `yaml:"since_seconds"` - TailLines *int64 `yaml:"tail_lines"` - LimitBytes int64 `yaml:"limit_bytes"` - IncludeTimestamps bool `yaml:"timestamps"` - Follow bool `yaml:"follow"` - ResyncPeriod time.Duration `yaml:"resync_period"` - RequestTimeout time.Duration `yaml:"request_timeout"` - MaxLineBytes int `yaml:"max_line_bytes"` -} - -type podLogTarget struct { - Namespace string - Pod string - Container string -} - -func (t podLogTarget) String() string { - return fmt.Sprintf("%s/%s/%s", t.Namespace, t.Pod, t.Container) -} - -type podLogKey struct { - Namespace string - Pod string - Container string -} - -type KubernetesPodLogsSource struct { - config KubernetesPodLogsConfig - logger *log.Entry - metricsLevel metrics.AcquisitionMetricsLevel - apiClient *http.Client - streamClient *http.Client - authToken string - apiServer string - namespaceFilter map[string]struct{} - containerFilter map[string]struct{} - out chan types.Event - activeStreams map[podLogKey]context.CancelFunc - activeMu sync.Mutex -} - -func defaultKubePodLogsConfig() KubernetesPodLogsConfig { - return KubernetesPodLogsConfig{ - APIServer: defaultAPIServer, - TokenPath: defaultTokenPath, - CACertPath: defaultCACertPath, - Follow: true, - ResyncPeriod: defaultResyncPeriod, - RequestTimeout: defaultRequestTimeout, - MaxLineBytes: defaultMaxLineBytes, - } -} - -func (k *KubernetesPodLogsSource) normalizeConfig(cfg *KubernetesPodLogsConfig) { - cfg.APIServer = strings.TrimSpace(cfg.APIServer) - cfg.TokenPath = strings.TrimSpace(cfg.TokenPath) - cfg.CACertPath = strings.TrimSpace(cfg.CACertPath) - cfg.BearerToken = strings.TrimSpace(cfg.BearerToken) - cfg.LabelSelector = strings.TrimSpace(cfg.LabelSelector) - cfg.NodeName = strings.TrimSpace(cfg.NodeName) - - if cfg.NodeName == "" { - cfg.NodeName = strings.TrimSpace(os.Getenv(nodeNameEnvKey)) - } - - if cfg.APIServer == "" { - cfg.APIServer = defaultAPIServer - } - - if cfg.Mode == "" { - cfg.Mode = configuration.TAIL_MODE - } - - if cfg.ResyncPeriod == 0 { - cfg.ResyncPeriod = defaultResyncPeriod - } - - if cfg.RequestTimeout == 0 { - cfg.RequestTimeout = defaultRequestTimeout - } - - if cfg.MaxLineBytes == 0 { - cfg.MaxLineBytes = defaultMaxLineBytes - } -} - -func (k *KubernetesPodLogsSource) validateConfig(cfg KubernetesPodLogsConfig) error { - if cfg.NodeName == "" { - return errors.New("node_name must be set or NODE_NAME environment variable must be provided") - } - - if cfg.Mode != configuration.TAIL_MODE { - return fmt.Errorf("unsupported mode %s for k8s-podlogs datasource", cfg.Mode) - } - - if cfg.LimitBytes < 0 { - return errors.New("limit_bytes cannot be negative") - } - - if cfg.SinceSeconds != nil && *cfg.SinceSeconds < 0 { - return errors.New("since_seconds cannot be negative") - } - - if cfg.TailLines != nil && *cfg.TailLines < 0 { - return errors.New("tail_lines cannot be negative") - } - - if cfg.ResyncPeriod < 0 { - return errors.New("resync_period cannot be negative") - } - - if cfg.RequestTimeout < 0 { - return errors.New("request_timeout cannot be negative") - } - - if cfg.MaxLineBytes < 0 { - return errors.New("max_line_bytes cannot be negative") - } - - if cfg.TokenPath == "" && cfg.BearerToken == "" { - return errors.New("either token_file or bearer_token must be set") - } - - if cfg.APIServer == "" { - return errors.New("api_server cannot be empty") - } - - return nil -} - -func (k *KubernetesPodLogsSource) GetUuid() string { - return k.config.UniqueId -} - -func (k *KubernetesPodLogsSource) GetMetrics() []prometheus.Collector { - return []prometheus.Collector{metrics.K8SPodLogsLines} -} - -func (k *KubernetesPodLogsSource) GetAggregMetrics() []prometheus.Collector { - return []prometheus.Collector{metrics.K8SPodLogsLines} -} - -func (k *KubernetesPodLogsSource) GetMode() string { - return k.config.Mode -} - -func (k *KubernetesPodLogsSource) GetName() string { - return "k8s-podlogs" -} - -func (k *KubernetesPodLogsSource) Dump() any { - return k -} - -func (*KubernetesPodLogsSource) CanRun() error { - return nil -} - -func (k *KubernetesPodLogsSource) UnmarshalConfig(yamlConfig []byte) error { - cfg := defaultKubePodLogsConfig() - - if err := yaml.UnmarshalWithOptions(yamlConfig, &cfg, yaml.Strict()); err != nil { - return fmt.Errorf("cannot parse k8s-podlogs configuration: %s", yaml.FormatError(err, false, false)) - } - - k.normalizeConfig(&cfg) - - if err := k.validateConfig(cfg); err != nil { - return err - } - - k.namespaceFilter = buildSet(cfg.Namespaces) - k.containerFilter = buildSet(cfg.Containers) - - k.config = cfg - - return nil -} - -func (k *KubernetesPodLogsSource) Configure(config []byte, logger *log.Entry, metricsLevel metrics.AcquisitionMetricsLevel) error { - k.logger = logger - k.metricsLevel = metricsLevel - - if err := k.UnmarshalConfig(config); err != nil { - return err - } - - k.apiServer = strings.TrimRight(k.config.APIServer, "/") - if k.apiServer == "" { - k.apiServer = defaultAPIServer - } - - if err := k.loadToken(); err != nil { - return err - } - - if err := k.buildHTTPClients(); err != nil { - return err - } - - k.activeStreams = make(map[podLogKey]context.CancelFunc) - - if k.logger != nil && k.logger.Logger.IsLevelEnabled(log.TraceLevel) { - safeCfg := k.config - if safeCfg.BearerToken != "" { - safeCfg.BearerToken = "***" - } - k.logger.Tracef("k8s-podlogs configuration: %+v", safeCfg) - } - - return nil -} - -func (k *KubernetesPodLogsSource) StreamingAcquisition(ctx context.Context, out chan types.Event, t *tomb.Tomb) error { - k.out = out - - runCtx, cancel := context.WithCancel(ctx) - - t.Go(func() error { - <-t.Dying() - cancel() - return nil - }) - - t.Go(func() error { - defer trace.CatchPanic("crowdsec/acquis/k8s-podlogs/live") - return k.run(runCtx, t) - }) - - return nil -} - -func (k *KubernetesPodLogsSource) run(ctx context.Context, t *tomb.Tomb) error { - if err := k.syncStreams(ctx, t); err != nil { - return err - } - - ticker := time.NewTicker(k.config.ResyncPeriod) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - k.stopAll() - return nil - case <-ticker.C: - if err := k.syncStreams(ctx, t); err != nil { - k.logger.Errorf("k8s-podlogs sync error: %v", err) - } - } - } -} - -func (k *KubernetesPodLogsSource) syncStreams(ctx context.Context, t *tomb.Tomb) error { - targets, err := k.listTargets(ctx) - if err != nil { - return err - } - - desired := make(map[podLogKey]struct{}, len(targets)) - - for _, target := range targets { - key := target.key() - desired[key] = struct{}{} - k.startStreamIfNeeded(ctx, t, target) - } - - k.stopMissing(desired) - - return nil -} - -func (k *KubernetesPodLogsSource) listTargets(ctx context.Context) ([]podLogTarget, error) { - pods, err := k.fetchPods(ctx) - if err != nil { - return nil, err - } - - targets := make([]podLogTarget, 0) - - for _, pod := range pods { - if pod.Status.Phase != corev1.PodRunning { - continue - } - - if !k.allowedNamespace(pod.Namespace) { - continue - } - - for _, container := range pod.Spec.Containers { - if !k.allowedContainer(container.Name) { - continue - } - - if !isContainerRunning(pod.Status.ContainerStatuses, container.Name) { - continue - } - - targets = append(targets, podLogTarget{Namespace: pod.Namespace, Pod: pod.Name, Container: container.Name}) - } - } - - return targets, nil -} - -func (k *KubernetesPodLogsSource) fetchPods(ctx context.Context) ([]corev1.Pod, error) { - endpoint := fmt.Sprintf("%s/api/v1/pods", k.apiServer) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, fmt.Errorf("building pods request: %w", err) - } - - query := req.URL.Query() - query.Set("fieldSelector", fields.OneTermEqualSelector("spec.nodeName", k.config.NodeName).String()) - if k.config.LabelSelector != "" { - query.Set("labelSelector", k.config.LabelSelector) - } - req.URL.RawQuery = query.Encode() - k.decorateRequest(req) - req.Header.Set("Accept", "application/json") - - resp, err := k.apiClient.Do(req) - if err != nil { - return nil, fmt.Errorf("listing pods: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("listing pods failed: %s: %s", resp.Status, readBodySnippet(resp.Body)) - } - - var list corev1.PodList - if err := json.NewDecoder(resp.Body).Decode(&list); err != nil { - return nil, fmt.Errorf("decoding pods response: %w", err) - } - - return list.Items, nil -} - -func (k *KubernetesPodLogsSource) startStreamIfNeeded(ctx context.Context, t *tomb.Tomb, target podLogTarget) { - key := target.key() - - k.activeMu.Lock() - if _, exists := k.activeStreams[key]; exists { - k.activeMu.Unlock() - return - } - - streamCtx, cancel := context.WithCancel(ctx) - k.activeStreams[key] = cancel - k.activeMu.Unlock() - - t.Go(func() error { - defer func() { - cancel() - k.removeStream(key) - }() - - if err := k.consumeLogs(streamCtx, target); err != nil && !errors.Is(err, context.Canceled) { - k.logger.Errorf("log stream %s failed: %v", key.String(), err) - } - - return nil - }) -} - -func (k *KubernetesPodLogsSource) consumeLogs(ctx context.Context, target podLogTarget) error { - req, err := k.newLogRequest(ctx, target) - if err != nil { - return err - } - - resp, err := k.streamClient.Do(req) - if err != nil { - return fmt.Errorf("opening log stream for %s: %w", target.String(), err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("log stream for %s failed: %s: %s", target.String(), resp.Status, readBodySnippet(resp.Body)) - } - - return k.scanStream(ctx, resp.Body, target) -} - -func (k *KubernetesPodLogsSource) scanStream(ctx context.Context, body io.ReadCloser, target podLogTarget) error { - done := make(chan struct{}) - go func() { - select { - case <-ctx.Done(): - _ = body.Close() - case <-done: - } - }() - - scanner := bufio.NewScanner(body) - scanner.Buffer(make([]byte, 0, 64*1024), k.config.MaxLineBytes) - - for scanner.Scan() { - k.emitLine(target, scanner.Text()) - } - - close(done) - - if err := scanner.Err(); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) { - return fmt.Errorf("reading log stream for %s: %w", target.String(), err) - } - - return nil -} - -func (k *KubernetesPodLogsSource) emitLine(target podLogTarget, raw string) { - line := strings.TrimRight(raw, "\r\n") - if line == "" { - return - } - - labels := k.buildLabels(target) - - evt := types.MakeEvent(k.config.UseTimeMachine, types.LOG, true) - evt.Line = types.Line{ - Raw: line, - Labels: labels, - Time: time.Now().UTC(), - Src: target.String(), - Process: true, - Module: k.GetName(), - } - - if k.metricsLevel != metrics.AcquisitionMetricsLevelNone { - metrics.K8SPodLogsLines.WithLabelValues(k.metricSource(), k.GetName(), labels["type"]).Inc() - } - - k.out <- evt -} - -func (k *KubernetesPodLogsSource) buildLabels(target podLogTarget) map[string]string { - labels := make(map[string]string, len(k.config.Labels)+4) - for key, value := range k.config.Labels { - labels[key] = value - } - - labels["k8s_namespace"] = target.Namespace - labels["k8s_pod"] = target.Pod - labels["k8s_container"] = target.Container - labels["k8s_node"] = k.config.NodeName - - return labels -} - -func (k *KubernetesPodLogsSource) metricSource() string { - if k.config.Name != "" { - return k.config.Name - } - - return k.config.NodeName -} - -func (k *KubernetesPodLogsSource) newLogRequest(ctx context.Context, target podLogTarget) (*http.Request, error) { - endpoint := fmt.Sprintf("%s/api/v1/namespaces/%s/pods/%s/log", k.apiServer, target.Namespace, target.Pod) - - u, err := url.Parse(endpoint) - if err != nil { - return nil, fmt.Errorf("invalid log endpoint: %w", err) - } - - query := u.Query() - query.Set("container", target.Container) - query.Set("follow", fmt.Sprintf("%t", k.config.Follow)) - if k.config.IncludeTimestamps { - query.Set("timestamps", "true") - } - if k.config.SinceSeconds != nil { - query.Set("sinceSeconds", fmt.Sprintf("%d", *k.config.SinceSeconds)) - } - if k.config.TailLines != nil { - query.Set("tailLines", fmt.Sprintf("%d", *k.config.TailLines)) - } - if k.config.LimitBytes > 0 { - query.Set("limitBytes", fmt.Sprintf("%d", k.config.LimitBytes)) - } - u.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) - if err != nil { - return nil, fmt.Errorf("building log request: %w", err) - } - - k.decorateRequest(req) - req.Header.Set("Accept", "text/plain") - return req, nil -} - -func (k *KubernetesPodLogsSource) decorateRequest(req *http.Request) { - req.Header.Set("User-Agent", userAgent) - if k.authToken != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", k.authToken)) - } -} - -func (k *KubernetesPodLogsSource) startTLSConfig() (*tls.Config, error) { - tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} - - if k.config.InsecureSkipTLS { - tlsConfig.InsecureSkipVerify = true - return tlsConfig, nil - } - - pool, err := x509.SystemCertPool() - if err != nil { - pool = x509.NewCertPool() - } - - if k.config.CACertPath != "" { - pemBytes, err := os.ReadFile(k.config.CACertPath) - if err != nil { - return nil, fmt.Errorf("reading CA certificate: %w", err) - } - if ok := pool.AppendCertsFromPEM(pemBytes); !ok { - return nil, errors.New("unable to append CA certificate") - } - } - - tlsConfig.RootCAs = pool - - return tlsConfig, nil -} - -func (k *KubernetesPodLogsSource) buildHTTPClients() error { - tlsConfig, err := k.startTLSConfig() - if err != nil { - return err - } - - transport := &http.Transport{ - TLSClientConfig: tlsConfig, - } - - k.apiClient = &http.Client{ - Timeout: k.config.RequestTimeout, - Transport: transport, - } - - k.streamClient = &http.Client{ - Transport: transport, - } - - return nil -} - -func (k *KubernetesPodLogsSource) loadToken() error { - if k.config.BearerToken != "" { - k.authToken = k.config.BearerToken - return nil - } - - bytes, err := os.ReadFile(k.config.TokenPath) - if err != nil { - return fmt.Errorf("reading token file %s: %w", k.config.TokenPath, err) - } - - token := strings.TrimSpace(string(bytes)) - if token == "" { - return errors.New("token file is empty") - } - - k.authToken = token - - return nil -} - -func (k *KubernetesPodLogsSource) stopMissing(desired map[podLogKey]struct{}) { - k.activeMu.Lock() - defer k.activeMu.Unlock() - - for key, cancel := range k.activeStreams { - if _, ok := desired[key]; ok { - continue - } - cancel() - delete(k.activeStreams, key) - } -} - -func (k *KubernetesPodLogsSource) stopAll() { - k.activeMu.Lock() - defer k.activeMu.Unlock() - - for key, cancel := range k.activeStreams { - k.logger.Debugf("stopping log stream %s", key.String()) - cancel() - } - - k.activeStreams = make(map[podLogKey]context.CancelFunc) -} - -func (k *KubernetesPodLogsSource) removeStream(key podLogKey) { - k.activeMu.Lock() - delete(k.activeStreams, key) - k.activeMu.Unlock() -} - -func (k *KubernetesPodLogsSource) allowedNamespace(ns string) bool { - if len(k.namespaceFilter) == 0 { - return true - } - - _, ok := k.namespaceFilter[ns] - return ok -} - -func (k *KubernetesPodLogsSource) allowedContainer(container string) bool { - if len(k.containerFilter) == 0 { - return true - } - - _, ok := k.containerFilter[container] - return ok -} - -func buildSet(items []string) map[string]struct{} { - set := make(map[string]struct{}) - for _, item := range items { - trimmed := strings.TrimSpace(item) - if trimmed == "" { - continue - } - set[trimmed] = struct{}{} - } - return set -} - -func isContainerRunning(statuses []corev1.ContainerStatus, name string) bool { - for _, status := range statuses { - if status.Name == name && status.State.Running != nil { - return true - } - } - - return false -} - -func (t podLogTarget) key() podLogKey { - return podLogKey{ - Namespace: t.Namespace, - Pod: t.Pod, - Container: t.Container, - } -} - -func (k podLogKey) String() string { - return fmt.Sprintf("%s/%s/%s", k.Namespace, k.Pod, k.Container) -} - -func readBodySnippet(body io.ReadCloser) string { - defer body.Close() - data, err := io.ReadAll(io.LimitReader(body, maxErrorBodyBytes)) - if err != nil { - return "" - } - - return strings.TrimSpace(string(data)) -} diff --git a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml deleted file mode 100644 index d0fc09da543..00000000000 --- a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml +++ /dev/null @@ -1,109 +0,0 @@ -$schema: "http://json-schema.org/draft-07/schema#" -title: CrowdSec Kubernetes pod logs datasource configuration -type: object -required: - - type - - node_name -properties: - type: - const: k8s-podlogs - api_server: - type: string - description: Base URL of the Kubernetes API server. - default: https://kubernetes.default.svc - token_file: - type: string - description: Path to the service account token used to authenticate against the API server; at least one of token_file or bearer_token must be provided. - default: /var/run/secrets/kubernetes.io/serviceaccount/token - bearer_token: - type: string - description: Raw bearer token to use instead of reading token_file; at least one of bearer_token or token_file must be provided. - ca_cert: - type: string - description: Path to the CA bundle validating the API server certificate. - default: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - insecure_skip_verify: - type: boolean - description: Disable TLS verification when connecting to the API server. - default: false - node_name: - type: string - description: Name of the node whose pods should be tailed; typically set via the NODE_NAME environment variable. - namespaces: - type: array - description: Optional allow-list of namespaces to watch (defaults to all namespaces). - items: - type: string - label_selector: - type: string - description: Kubernetes label selector applied when listing pods. - containers: - type: array - description: Optional list of container names to include; defaults to all containers. - items: - type: string - since_seconds: - type: integer - minimum: 0 - description: Start log streaming from events newer than the provided age. - tail_lines: - type: integer - minimum: 0 - description: Number of most-recent log lines to read before following. - limit_bytes: - type: integer - minimum: 0 - description: Maximum number of bytes returned by the Kubernetes log endpoint for each request. - timestamps: - type: boolean - description: Request Kubernetes to prefix every log line with a timestamp. - default: false - follow: - type: boolean - description: Keep the log stream open to follow new entries. - default: true - resync_period: - type: string - pattern: '^([0-9]+(ns|us|ms|s|m|h))+$' - description: Interval between reconciliation cycles where new pod streams are created and stale ones are stopped. - default: 30s - request_timeout: - type: string - pattern: '^([0-9]+(ns|us|ms|s|m|h))+$' - description: Timeout applied to Kubernetes API list calls. - default: 10s - max_line_bytes: - type: integer - minimum: 1024 - description: Maximum accepted log line size before failing the stream. - default: 1048576 - mode: - type: string - enum: [tail] - description: Acquisition mode; kubernetes pod logs only support tail. - default: tail - labels: - type: object - additionalProperties: - type: string - description: Optional labels attached to every emitted event. - log_level: - type: string - enum: [panic, fatal, error, warn, info, debug, trace] - description: Overrides datasource logger level. - source: - type: string - description: Custom source string stamped on events. - name: - type: string - description: Datasource name used in metrics and logs. - use_time_machine: - type: boolean - description: Enable acquisition time-machine mode. - unique_id: - type: string - description: Stable identifier used for deduplication. - transform: - type: string - description: ExprLang transform applied to events. -additionalProperties: false diff --git a/pkg/acquisition/modules/kubernetespodlogs/metrics.go b/pkg/acquisition/modules/kubernetespodlogs/metrics.go new file mode 100644 index 00000000000..f262e538b1f --- /dev/null +++ b/pkg/acquisition/modules/kubernetespodlogs/metrics.go @@ -0,0 +1,19 @@ +package kubernetespodlogs + +import ( + "github.com/prometheus/client_golang/prometheus" + + "github.com/crowdsecurity/crowdsec/pkg/metrics" +) + +func (*Source) GetMetrics() []prometheus.Collector { + return []prometheus.Collector{ + metrics.DockerDatasourceLinesRead, + } +} + +func (*Source) GetAggregMetrics() []prometheus.Collector { + return []prometheus.Collector{ + metrics.DockerDatasourceLinesRead, + } +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/run.go b/pkg/acquisition/modules/kubernetespodlogs/run.go new file mode 100644 index 00000000000..127afbac626 --- /dev/null +++ b/pkg/acquisition/modules/kubernetespodlogs/run.go @@ -0,0 +1,190 @@ +package kubernetespodlogs + +import ( + "bufio" + "context" + "fmt" + "log" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "gopkg.in/tomb.v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + + "github.com/crowdsecurity/crowdsec/pkg/metrics" + "github.com/crowdsecurity/crowdsec/pkg/pipeline" +) + +func (d *Source) OneShotAcquisition(ctx context.Context, out chan pipeline.Event, t *tomb.Tomb) error { + d.logger.Debug("In oneshot") + return nil +} + +func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Event, t *tomb.Tomb) error { + var wg sync.WaitGroup + var mu sync.Mutex + + d.logger.Info("Starting Kubernetes Pod Logs acquisition") + + cfg, err := buildConfig() + if err != nil { + log.Fatal(err) + } + cs, err := kubernetes.NewForConfig(cfg) + if err != nil { + log.Fatal(err) + } + + ns := "ingress-nginx" + labels := "app.kubernetes.io/component=controller" + + cancels := map[string]context.CancelFunc{} + + f := informers.NewSharedInformerFactoryWithOptions(cs, 0, + informers.WithNamespace(ns), + informers.WithTweakListOptions(func(o *metav1.ListOptions) { o.LabelSelector = labels }), + ) + inf := f.Core().V1().Pods().Informer() + + inf.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { d.startPod(ctx, cs, obj.(*corev1.Pod), out, &wg, &mu, cancels) }, + UpdateFunc: func(_, newObj interface{}) { + d.startPod(ctx, cs, newObj.(*corev1.Pod), out, &wg, &mu, cancels) + }, + DeleteFunc: func(obj interface{}) { + pod, ok := obj.(*corev1.Pod) + if !ok { + t, _ := obj.(cache.DeletedFinalStateUnknown) + pod, _ = t.Obj.(*corev1.Pod) + } + if pod != nil { + d.stopPod(pod, &mu, cancels) + } + }, + }) + + f.Start(ctx.Done()) + if !cache.WaitForCacheSync(ctx.Done(), inf.HasSynced) { + log.Fatal("cache sync failed") + } + + <-ctx.Done() + mu.Lock() + for _, c := range cancels { + c() + } + mu.Unlock() + wg.Wait() + + return nil +} + +func (d *Source) Dump() any { + return d +} + +func (d *Source) followPodLogs(ctx context.Context, cs *kubernetes.Clientset, ns, pod, container string, onLine func(string) error) error { + req := cs.CoreV1().Pods(ns).GetLogs(pod, &corev1.PodLogOptions{Container: container, Follow: true, Timestamps: true}) + stream, err := req.Stream(ctx) + if err != nil { + return err + } + defer stream.Close() + + sc := bufio.NewScanner(stream) + for sc.Scan() { + if err := ctx.Err(); err != nil { + return err + } + if err := onLine(sc.Text()); err != nil { + return err + } + } + return sc.Err() + +} + +func (d *Source) podWorker(meta context.Context, cs *kubernetes.Clientset, pod *corev1.Pod, out chan pipeline.Event, wg *sync.WaitGroup) context.CancelFunc { + podCtx, cancel := context.WithCancel(meta) + wg.Add(1) + go func() { + defer wg.Done() + var cw sync.WaitGroup + for _, c := range pod.Spec.Containers { + c := c.Name + cw.Add(1) + go func() { + defer cw.Done() + _ = d.followPodLogs(podCtx, cs, pod.Namespace, pod.Name, c, func(line string) error { + source := fmt.Sprintf("%s/%s", pod.Namespace, pod.Name) + l := pipeline.Line{} + l.Raw = line + l.Labels = d.Config.Labels + l.Time = time.Now().UTC() + l.Src = source + l.Process = true + l.Module = d.GetName() + if d.metricsLevel != metrics.AcquisitionMetricsLevelNone { + metrics.DockerDatasourceLinesRead.With(prometheus.Labels{"source": source, "acquis_type": l.Labels["type"], "datasource_type": ModuleName}).Inc() + } + evt := pipeline.MakeEvent(true, pipeline.LOG, true) + evt.Line = l + evt.Process = true + evt.Type = pipeline.LOG + out <- evt + return nil + }) + }() + } + cw.Wait() + }() + return cancel +} + +func shouldTail(p *corev1.Pod) bool { return p.Status.Phase == corev1.PodRunning } + +func (d *Source) startPod(meta context.Context, cs *kubernetes.Clientset, p *corev1.Pod, out chan pipeline.Event, wg *sync.WaitGroup, mu *sync.Mutex, cancels map[string]context.CancelFunc) { + if !shouldTail(p) { + return + } + key := string(p.UID) + mu.Lock() + if _, ok := cancels[key]; ok { + mu.Unlock() + return + } + cancels[key] = d.podWorker(meta, cs, p, out, wg) + mu.Unlock() +} + +func (*Source) stopPod(p *corev1.Pod, mu *sync.Mutex, cancels map[string]context.CancelFunc) { + key := string(p.UID) + mu.Lock() + cancel, ok := cancels[key] + if ok { + delete(cancels, key) + } + mu.Unlock() + if ok { + cancel() + } +} + +func printer(ctx context.Context, in <-chan string) { + for { + select { + case <-ctx.Done(): + return + case line, ok := <-in: + if !ok { + return + } + fmt.Println(line) + } + } +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/source.go b/pkg/acquisition/modules/kubernetespodlogs/source.go new file mode 100644 index 00000000000..b6d4a632ec8 --- /dev/null +++ b/pkg/acquisition/modules/kubernetespodlogs/source.go @@ -0,0 +1,33 @@ +package kubernetespodlogs + +import ( + log "github.com/sirupsen/logrus" + + "gopkg.in/tomb.v2" + + "github.com/crowdsecurity/crowdsec/pkg/metrics" +) + +type Source struct { + metricsLevel metrics.AcquisitionMetricsLevel + Config Configuration + + logger *log.Entry + t tomb.Tomb +} + +func (*Source) GetName() string { + return ModuleName +} + +func (d *Source) GetMode() string { + return d.Config.Mode +} + +func (*Source) CanRun() error { + return nil +} + +func (d *Source) GetUuid() string { + return d.Config.UniqueId +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/utils.go b/pkg/acquisition/modules/kubernetespodlogs/utils.go new file mode 100644 index 00000000000..19ec45445dc --- /dev/null +++ b/pkg/acquisition/modules/kubernetespodlogs/utils.go @@ -0,0 +1,21 @@ +package kubernetespodlogs + +import ( + "flag" + "os" + "path/filepath" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +func buildConfig() (*rest.Config, error) { + cfg, err := rest.InClusterConfig() + if err == nil { + return cfg, nil + } + home, _ := os.UserHomeDir() + kubeconfig := flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "kubeconfig path") + flag.Parse() + return clientcmd.BuildConfigFromFlags("", *kubeconfig) +} From d1bdf5332ccdc0a62d433c4e3efc9dd4240238b5 Mon Sep 17 00:00:00 2001 From: sabban Date: Thu, 22 Jan 2026 14:26:44 +0100 Subject: [PATCH 04/23] clean up --- .../modules/kuberneteslogs/config.go | 23 ------------------- .../modules/kuberneteslogs/metrics.go | 19 --------------- 2 files changed, 42 deletions(-) delete mode 100644 pkg/acquisition/modules/kuberneteslogs/config.go delete mode 100644 pkg/acquisition/modules/kuberneteslogs/metrics.go diff --git a/pkg/acquisition/modules/kuberneteslogs/config.go b/pkg/acquisition/modules/kuberneteslogs/config.go deleted file mode 100644 index f108048c4ab..00000000000 --- a/pkg/acquisition/modules/kuberneteslogs/config.go +++ /dev/null @@ -1,23 +0,0 @@ -package kubernetesacquisition - -import ( - "context" - "errors" - "fmt" - "net/url" - "regexp" - "strconv" - "time" - - log "github.com/sirupsen/logrus" - - "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" - "github.com/crowdsecurity/crowdsec/pkg/metrics" -) - -type Configuration struct { - configuration.DataSourceCommonCfg `yaml:",inline"` - - Selector string `yaml:"selector"` - Namespace string `yaml:"namespace"` -} diff --git a/pkg/acquisition/modules/kuberneteslogs/metrics.go b/pkg/acquisition/modules/kuberneteslogs/metrics.go deleted file mode 100644 index 8d33d16c339..00000000000 --- a/pkg/acquisition/modules/kuberneteslogs/metrics.go +++ /dev/null @@ -1,19 +0,0 @@ -package kubernetesacquisition - -import ( - "github.com/prometheus/client_golang/prometheus" - - "github.com/crowdsecurity/crowdsec/pkg/metrics" -) - -func (*Source) GetMetrics() []prometheus.Collector { - return []prometheus.Collector{ - metrics.KubernetesDatasourceLinesRead, - } -} - -func (*Source) GetAggregMetrics() []prometheus.Collector { - return []prometheus.Collector{ - metrics.KubernetesDatasourceLinesRead, - } -} From e8294983cfd173ba16064e882418ed2f3964353f Mon Sep 17 00:00:00 2001 From: sabban Date: Fri, 30 Jan 2026 11:33:19 +0100 Subject: [PATCH 05/23] rename the prod branch to main --- .github/workflows/version.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index b0a6a17d296..033927d67a2 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -53,4 +53,4 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy ./dist --project-name version + command: pages deploy ./dist --project-name version --main From b615f703edcc4581f8a880f851fa7c069012db80 Mon Sep 17 00:00:00 2001 From: sabban Date: Sun, 1 Feb 2026 21:22:31 +0100 Subject: [PATCH 06/23] remove unused file --- .../modules/kubernetespodlogs/k8s_podlogs.go | 723 ------------------ 1 file changed, 723 deletions(-) delete mode 100644 pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go diff --git a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go b/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go deleted file mode 100644 index ad0b8f0f44d..00000000000 --- a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs.go +++ /dev/null @@ -1,723 +0,0 @@ -//go:build !no_datasource_k8s_podlogs - -package kubernetespodlogs - -import ( - "bufio" - "context" - "crypto/tls" - "crypto/x509" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" - "strings" - "sync" - "time" - - yaml "github.com/goccy/go-yaml" - "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" - "gopkg.in/tomb.v2" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" - - "github.com/crowdsecurity/go-cs-lib/trace" - - "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" - "github.com/crowdsecurity/crowdsec/pkg/metrics" - "github.com/crowdsecurity/crowdsec/pkg/types" -) - -const ( - defaultAPIServer = "https://kubernetes.default.svc" - defaultTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" - defaultCACertPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - defaultResyncPeriod = 30 * time.Second - defaultRequestTimeout = 10 * time.Second - defaultMaxLineBytes = 1 << 20 - maxErrorBodyBytes = 4096 - userAgent = "crowdsec-k8s-podlogs" - nodeNameEnvKey = "NODE_NAME" -) - -type KubernetesPodLogsConfig struct { - configuration.DataSourceCommonCfg `yaml:",inline"` - - APIServer string `yaml:"api_server"` - TokenPath string `yaml:"token_file"` - BearerToken string `yaml:"bearer_token"` - CACertPath string `yaml:"ca_cert"` - InsecureSkipTLS bool `yaml:"insecure_skip_verify"` - NodeName string `yaml:"node_name"` - Namespaces []string `yaml:"namespaces"` - LabelSelector string `yaml:"label_selector"` - Containers []string `yaml:"containers"` - SinceSeconds *int64 `yaml:"since_seconds"` - TailLines *int64 `yaml:"tail_lines"` - LimitBytes int64 `yaml:"limit_bytes"` - IncludeTimestamps bool `yaml:"timestamps"` - Follow bool `yaml:"follow"` - ResyncPeriod time.Duration `yaml:"resync_period"` - RequestTimeout time.Duration `yaml:"request_timeout"` - MaxLineBytes int `yaml:"max_line_bytes"` -} - -type podLogTarget struct { - Namespace string - Pod string - Container string -} - -func (t podLogTarget) String() string { - return fmt.Sprintf("%s/%s/%s", t.Namespace, t.Pod, t.Container) -} - -type podLogKey struct { - Namespace string - Pod string - Container string -} - -type KubernetesPodLogsSource struct { - config KubernetesPodLogsConfig - logger *log.Entry - metricsLevel metrics.AcquisitionMetricsLevel - apiClient *http.Client - streamClient *http.Client - authToken string - apiServer string - namespaceFilter map[string]struct{} - containerFilter map[string]struct{} - out chan types.Event - activeStreams map[podLogKey]context.CancelFunc - activeMu sync.Mutex -} - -func defaultKubePodLogsConfig() KubernetesPodLogsConfig { - return KubernetesPodLogsConfig{ - APIServer: defaultAPIServer, - TokenPath: defaultTokenPath, - CACertPath: defaultCACertPath, - Follow: true, - ResyncPeriod: defaultResyncPeriod, - RequestTimeout: defaultRequestTimeout, - MaxLineBytes: defaultMaxLineBytes, - } -} - -func (k *KubernetesPodLogsSource) normalizeConfig(cfg *KubernetesPodLogsConfig) { - cfg.APIServer = strings.TrimSpace(cfg.APIServer) - cfg.TokenPath = strings.TrimSpace(cfg.TokenPath) - cfg.CACertPath = strings.TrimSpace(cfg.CACertPath) - cfg.BearerToken = strings.TrimSpace(cfg.BearerToken) - cfg.LabelSelector = strings.TrimSpace(cfg.LabelSelector) - cfg.NodeName = strings.TrimSpace(cfg.NodeName) - - if cfg.NodeName == "" { - cfg.NodeName = strings.TrimSpace(os.Getenv(nodeNameEnvKey)) - } - - if cfg.APIServer == "" { - cfg.APIServer = defaultAPIServer - } - - if cfg.Mode == "" { - cfg.Mode = configuration.TAIL_MODE - } - - if cfg.ResyncPeriod == 0 { - cfg.ResyncPeriod = defaultResyncPeriod - } - - if cfg.RequestTimeout == 0 { - cfg.RequestTimeout = defaultRequestTimeout - } - - if cfg.MaxLineBytes == 0 { - cfg.MaxLineBytes = defaultMaxLineBytes - } -} - -func (k *KubernetesPodLogsSource) validateConfig(cfg KubernetesPodLogsConfig) error { - if cfg.NodeName == "" { - return errors.New("node_name must be set or NODE_NAME environment variable must be provided") - } - - if cfg.Mode != configuration.TAIL_MODE { - return fmt.Errorf("unsupported mode %s for k8s-podlogs datasource", cfg.Mode) - } - - if cfg.LimitBytes < 0 { - return errors.New("limit_bytes cannot be negative") - } - - if cfg.SinceSeconds != nil && *cfg.SinceSeconds < 0 { - return errors.New("since_seconds cannot be negative") - } - - if cfg.TailLines != nil && *cfg.TailLines < 0 { - return errors.New("tail_lines cannot be negative") - } - - if cfg.ResyncPeriod < 0 { - return errors.New("resync_period cannot be negative") - } - - if cfg.RequestTimeout < 0 { - return errors.New("request_timeout cannot be negative") - } - - if cfg.MaxLineBytes < 0 { - return errors.New("max_line_bytes cannot be negative") - } - - if cfg.TokenPath == "" && cfg.BearerToken == "" { - return errors.New("either token_file or bearer_token must be set") - } - - if cfg.APIServer == "" { - return errors.New("api_server cannot be empty") - } - - return nil -} - -func (k *KubernetesPodLogsSource) GetUuid() string { - return k.config.UniqueId -} - -func (k *KubernetesPodLogsSource) GetMetrics() []prometheus.Collector { - return []prometheus.Collector{metrics.K8SPodLogsLines} -} - -func (k *KubernetesPodLogsSource) GetAggregMetrics() []prometheus.Collector { - return []prometheus.Collector{metrics.K8SPodLogsLines} -} - -func (k *KubernetesPodLogsSource) GetMode() string { - return k.config.Mode -} - -func (k *KubernetesPodLogsSource) GetName() string { - return "k8s-podlogs" -} - -func (k *KubernetesPodLogsSource) Dump() any { - return k -} - -func (*KubernetesPodLogsSource) CanRun() error { - return nil -} - -func (k *KubernetesPodLogsSource) UnmarshalConfig(yamlConfig []byte) error { - cfg := defaultKubePodLogsConfig() - - if err := yaml.UnmarshalWithOptions(yamlConfig, &cfg, yaml.Strict()); err != nil { - return fmt.Errorf("cannot parse k8s-podlogs configuration: %s", yaml.FormatError(err, false, false)) - } - - k.normalizeConfig(&cfg) - - if err := k.validateConfig(cfg); err != nil { - return err - } - - k.namespaceFilter = buildSet(cfg.Namespaces) - k.containerFilter = buildSet(cfg.Containers) - - k.config = cfg - - return nil -} - -func (k *KubernetesPodLogsSource) Configure(config []byte, logger *log.Entry, metricsLevel metrics.AcquisitionMetricsLevel) error { - k.logger = logger - k.metricsLevel = metricsLevel - - if err := k.UnmarshalConfig(config); err != nil { - return err - } - - k.apiServer = strings.TrimRight(k.config.APIServer, "/") - if k.apiServer == "" { - k.apiServer = defaultAPIServer - } - - if err := k.loadToken(); err != nil { - return err - } - - if err := k.buildHTTPClients(); err != nil { - return err - } - - k.activeStreams = make(map[podLogKey]context.CancelFunc) - - if k.logger != nil && k.logger.Logger.IsLevelEnabled(log.TraceLevel) { - safeCfg := k.config - if safeCfg.BearerToken != "" { - safeCfg.BearerToken = "***" - } - k.logger.Tracef("k8s-podlogs configuration: %+v", safeCfg) - } - - return nil -} - -func (k *KubernetesPodLogsSource) StreamingAcquisition(ctx context.Context, out chan types.Event, t *tomb.Tomb) error { - k.out = out - - runCtx, cancel := context.WithCancel(ctx) - - t.Go(func() error { - <-t.Dying() - cancel() - return nil - }) - - t.Go(func() error { - defer trace.CatchPanic("crowdsec/acquis/k8s-podlogs/live") - return k.run(runCtx, t) - }) - - return nil -} - -func (k *KubernetesPodLogsSource) run(ctx context.Context, t *tomb.Tomb) error { - if err := k.syncStreams(ctx, t); err != nil { - return err - } - - ticker := time.NewTicker(k.config.ResyncPeriod) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - k.stopAll() - return nil - case <-ticker.C: - if err := k.syncStreams(ctx, t); err != nil { - k.logger.Errorf("k8s-podlogs sync error: %v", err) - } - } - } -} - -func (k *KubernetesPodLogsSource) syncStreams(ctx context.Context, t *tomb.Tomb) error { - targets, err := k.listTargets(ctx) - if err != nil { - return err - } - - desired := make(map[podLogKey]struct{}, len(targets)) - - for _, target := range targets { - key := target.key() - desired[key] = struct{}{} - k.startStreamIfNeeded(ctx, t, target) - } - - k.stopMissing(desired) - - return nil -} - -func (k *KubernetesPodLogsSource) listTargets(ctx context.Context) ([]podLogTarget, error) { - pods, err := k.fetchPods(ctx) - if err != nil { - return nil, err - } - - targets := make([]podLogTarget, 0) - - for _, pod := range pods { - if pod.Status.Phase != corev1.PodRunning { - continue - } - - if !k.allowedNamespace(pod.Namespace) { - continue - } - - for _, container := range pod.Spec.Containers { - if !k.allowedContainer(container.Name) { - continue - } - - if !isContainerRunning(pod.Status.ContainerStatuses, container.Name) { - continue - } - - targets = append(targets, podLogTarget{Namespace: pod.Namespace, Pod: pod.Name, Container: container.Name}) - } - } - - return targets, nil -} - -func (k *KubernetesPodLogsSource) fetchPods(ctx context.Context) ([]corev1.Pod, error) { - endpoint := fmt.Sprintf("%s/api/v1/pods", k.apiServer) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, fmt.Errorf("building pods request: %w", err) - } - - query := req.URL.Query() - query.Set("fieldSelector", fields.OneTermEqualSelector("spec.nodeName", k.config.NodeName).String()) - if k.config.LabelSelector != "" { - query.Set("labelSelector", k.config.LabelSelector) - } - req.URL.RawQuery = query.Encode() - k.decorateRequest(req) - req.Header.Set("Accept", "application/json") - - resp, err := k.apiClient.Do(req) - if err != nil { - return nil, fmt.Errorf("listing pods: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("listing pods failed: %s: %s", resp.Status, readBodySnippet(resp.Body)) - } - - var list corev1.PodList - if err := json.NewDecoder(resp.Body).Decode(&list); err != nil { - return nil, fmt.Errorf("decoding pods response: %w", err) - } - - return list.Items, nil -} - -func (k *KubernetesPodLogsSource) startStreamIfNeeded(ctx context.Context, t *tomb.Tomb, target podLogTarget) { - key := target.key() - - k.activeMu.Lock() - if _, exists := k.activeStreams[key]; exists { - k.activeMu.Unlock() - return - } - - streamCtx, cancel := context.WithCancel(ctx) - k.activeStreams[key] = cancel - k.activeMu.Unlock() - - t.Go(func() error { - defer func() { - cancel() - k.removeStream(key) - }() - - if err := k.consumeLogs(streamCtx, target); err != nil && !errors.Is(err, context.Canceled) { - k.logger.Errorf("log stream %s failed: %v", key.String(), err) - } - - return nil - }) -} - -func (k *KubernetesPodLogsSource) consumeLogs(ctx context.Context, target podLogTarget) error { - req, err := k.newLogRequest(ctx, target) - if err != nil { - return err - } - - resp, err := k.streamClient.Do(req) - if err != nil { - return fmt.Errorf("opening log stream for %s: %w", target.String(), err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("log stream for %s failed: %s: %s", target.String(), resp.Status, readBodySnippet(resp.Body)) - } - - return k.scanStream(ctx, resp.Body, target) -} - -func (k *KubernetesPodLogsSource) scanStream(ctx context.Context, body io.ReadCloser, target podLogTarget) error { - done := make(chan struct{}) - go func() { - select { - case <-ctx.Done(): - _ = body.Close() - case <-done: - } - }() - - scanner := bufio.NewScanner(body) - scanner.Buffer(make([]byte, 0, 64*1024), k.config.MaxLineBytes) - - for scanner.Scan() { - k.emitLine(target, scanner.Text()) - } - - close(done) - - if err := scanner.Err(); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) { - return fmt.Errorf("reading log stream for %s: %w", target.String(), err) - } - - return nil -} - -func (k *KubernetesPodLogsSource) emitLine(target podLogTarget, raw string) { - line := strings.TrimRight(raw, "\r\n") - if line == "" { - return - } - - labels := k.buildLabels(target) - - evt := types.MakeEvent(k.config.UseTimeMachine, types.LOG, true) - evt.Line = types.Line{ - Raw: line, - Labels: labels, - Time: time.Now().UTC(), - Src: target.String(), - Process: true, - Module: k.GetName(), - } - - if k.metricsLevel != metrics.AcquisitionMetricsLevelNone { - metrics.K8SPodLogsLines.WithLabelValues(k.metricSource(), k.GetName(), labels["type"]).Inc() - } - - k.out <- evt -} - -func (k *KubernetesPodLogsSource) buildLabels(target podLogTarget) map[string]string { - labels := make(map[string]string, len(k.config.Labels)+4) - for key, value := range k.config.Labels { - labels[key] = value - } - - labels["k8s_namespace"] = target.Namespace - labels["k8s_pod"] = target.Pod - labels["k8s_container"] = target.Container - labels["k8s_node"] = k.config.NodeName - - return labels -} - -func (k *KubernetesPodLogsSource) metricSource() string { - if k.config.Name != "" { - return k.config.Name - } - - return k.config.NodeName -} - -func (k *KubernetesPodLogsSource) newLogRequest(ctx context.Context, target podLogTarget) (*http.Request, error) { - endpoint := fmt.Sprintf("%s/api/v1/namespaces/%s/pods/%s/log", k.apiServer, target.Namespace, target.Pod) - - u, err := url.Parse(endpoint) - if err != nil { - return nil, fmt.Errorf("invalid log endpoint: %w", err) - } - - query := u.Query() - query.Set("container", target.Container) - query.Set("follow", fmt.Sprintf("%t", k.config.Follow)) - if k.config.IncludeTimestamps { - query.Set("timestamps", "true") - } - if k.config.SinceSeconds != nil { - query.Set("sinceSeconds", fmt.Sprintf("%d", *k.config.SinceSeconds)) - } - if k.config.TailLines != nil { - query.Set("tailLines", fmt.Sprintf("%d", *k.config.TailLines)) - } - if k.config.LimitBytes > 0 { - query.Set("limitBytes", fmt.Sprintf("%d", k.config.LimitBytes)) - } - u.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) - if err != nil { - return nil, fmt.Errorf("building log request: %w", err) - } - - k.decorateRequest(req) - req.Header.Set("Accept", "text/plain") - return req, nil -} - -func (k *KubernetesPodLogsSource) decorateRequest(req *http.Request) { - req.Header.Set("User-Agent", userAgent) - if k.authToken != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", k.authToken)) - } -} - -func (k *KubernetesPodLogsSource) startTLSConfig() (*tls.Config, error) { - tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} - - if k.config.InsecureSkipTLS { - tlsConfig.InsecureSkipVerify = true - return tlsConfig, nil - } - - pool, err := x509.SystemCertPool() - if err != nil { - pool = x509.NewCertPool() - } - - if k.config.CACertPath != "" { - pemBytes, err := os.ReadFile(k.config.CACertPath) - if err != nil { - return nil, fmt.Errorf("reading CA certificate: %w", err) - } - if ok := pool.AppendCertsFromPEM(pemBytes); !ok { - return nil, errors.New("unable to append CA certificate") - } - } - - tlsConfig.RootCAs = pool - - return tlsConfig, nil -} - -func (k *KubernetesPodLogsSource) buildHTTPClients() error { - tlsConfig, err := k.startTLSConfig() - if err != nil { - return err - } - - transport := &http.Transport{ - TLSClientConfig: tlsConfig, - } - - k.apiClient = &http.Client{ - Timeout: k.config.RequestTimeout, - Transport: transport, - } - - k.streamClient = &http.Client{ - Transport: transport, - } - - return nil -} - -func (k *KubernetesPodLogsSource) loadToken() error { - if k.config.BearerToken != "" { - k.authToken = k.config.BearerToken - return nil - } - - bytes, err := os.ReadFile(k.config.TokenPath) - if err != nil { - return fmt.Errorf("reading token file %s: %w", k.config.TokenPath, err) - } - - token := strings.TrimSpace(string(bytes)) - if token == "" { - return errors.New("token file is empty") - } - - k.authToken = token - - return nil -} - -func (k *KubernetesPodLogsSource) stopMissing(desired map[podLogKey]struct{}) { - k.activeMu.Lock() - defer k.activeMu.Unlock() - - for key, cancel := range k.activeStreams { - if _, ok := desired[key]; ok { - continue - } - cancel() - delete(k.activeStreams, key) - } -} - -func (k *KubernetesPodLogsSource) stopAll() { - k.activeMu.Lock() - defer k.activeMu.Unlock() - - for key, cancel := range k.activeStreams { - k.logger.Debugf("stopping log stream %s", key.String()) - cancel() - } - - k.activeStreams = make(map[podLogKey]context.CancelFunc) -} - -func (k *KubernetesPodLogsSource) removeStream(key podLogKey) { - k.activeMu.Lock() - delete(k.activeStreams, key) - k.activeMu.Unlock() -} - -func (k *KubernetesPodLogsSource) allowedNamespace(ns string) bool { - if len(k.namespaceFilter) == 0 { - return true - } - - _, ok := k.namespaceFilter[ns] - return ok -} - -func (k *KubernetesPodLogsSource) allowedContainer(container string) bool { - if len(k.containerFilter) == 0 { - return true - } - - _, ok := k.containerFilter[container] - return ok -} - -func buildSet(items []string) map[string]struct{} { - set := make(map[string]struct{}) - for _, item := range items { - trimmed := strings.TrimSpace(item) - if trimmed == "" { - continue - } - set[trimmed] = struct{}{} - } - return set -} - -func isContainerRunning(statuses []corev1.ContainerStatus, name string) bool { - for _, status := range statuses { - if status.Name == name && status.State.Running != nil { - return true - } - } - - return false -} - -func (t podLogTarget) key() podLogKey { - return podLogKey{ - Namespace: t.Namespace, - Pod: t.Pod, - Container: t.Container, - } -} - -func (k podLogKey) String() string { - return fmt.Sprintf("%s/%s/%s", k.Namespace, k.Pod, k.Container) -} - -func readBodySnippet(body io.ReadCloser) string { - defer body.Close() - data, err := io.ReadAll(io.LimitReader(body, maxErrorBodyBytes)) - if err != nil { - return "" - } - - return strings.TrimSpace(string(data)) -} From 02d5dd03d5e28aa6c078cf094ed468c96428e256 Mon Sep 17 00:00:00 2001 From: sabban Date: Sun, 1 Feb 2026 21:56:15 +0100 Subject: [PATCH 07/23] update --- pkg/acquisition/k8s_podlogs.go | 19 ------- .../modules/kubernetespodlogs/config.go | 57 +++++++++++++++---- .../modules/kubernetespodlogs/metrics.go | 4 +- .../modules/kubernetespodlogs/run.go | 2 +- .../modules/kubernetespodlogs/utils.go | 27 ++++++--- pkg/cwversion/component/component.go | 37 ++++++------ pkg/metrics/acquisition_k8s_podlogs.go | 20 ------- pkg/metrics/acquisition_kubernetespodlogs.go | 21 +++++++ 8 files changed, 109 insertions(+), 78 deletions(-) delete mode 100644 pkg/acquisition/k8s_podlogs.go delete mode 100644 pkg/metrics/acquisition_k8s_podlogs.go create mode 100644 pkg/metrics/acquisition_kubernetespodlogs.go diff --git a/pkg/acquisition/k8s_podlogs.go b/pkg/acquisition/k8s_podlogs.go deleted file mode 100644 index 28aa40a5ed8..00000000000 --- a/pkg/acquisition/k8s_podlogs.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !no_datasource_k8s_podlogs - -package acquisition - -import ( - kubernetespodlogs "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/kubernetespodlogs" -) - -var ( - // verify interface compliance - _ DataSource = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) - _ Tailer = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) - _ MetricsProvider = (*kubernetespodlogs.KubernetesPodLogsSource)(nil) -) - -//nolint:gochecknoinits -func init() { - registerDataSource("k8s-podlogs", func() DataSource { return &kubernetespodlogs.KubernetesPodLogsSource{} }) -} diff --git a/pkg/acquisition/modules/kubernetespodlogs/config.go b/pkg/acquisition/modules/kubernetespodlogs/config.go index 0bf6a1092a4..0c95927d63c 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/config.go +++ b/pkg/acquisition/modules/kubernetespodlogs/config.go @@ -3,9 +3,12 @@ package kubernetespodlogs import ( "context" "fmt" + "os" + "path/filepath" yaml "github.com/goccy/go-yaml" log "github.com/sirupsen/logrus" + "k8s.io/client-go/tools/clientcmd/api" "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" "github.com/crowdsecurity/crowdsec/pkg/metrics" @@ -14,35 +17,67 @@ import ( type Configuration struct { configuration.DataSourceCommonCfg `yaml:",inline"` - Label string `yaml:"label"` - Namespace string `yaml:"namespace"` + Label string `yaml:"label"` + Namespace string `yaml:"namespace"` + Auth *Auth `yaml:"auth,omitempty"` + KubeConfigFile string `yaml:"kube_config,omitempty"` } -func (d *Source) UnmarshalConfig(yamlConfig []byte) error { - d.Config = Configuration{ +type Auth struct { + Cluster api.Cluster `yaml:"cluster,omitempty"` + User api.AuthInfo `yaml:"user,omitempty"` +} + +func (s *Source) UnmarshalConfig(yamlConfig []byte) error { + s.Config = Configuration{ Label: "", Namespace: "default", } - if err := yaml.UnmarshalWithOptions(yamlConfig, &d.Config, yaml.Strict()); err != nil { + if err := yaml.UnmarshalWithOptions(yamlConfig, &s.Config, yaml.Strict()); err != nil { return fmt.Errorf("while parsing KubernetesAcquisition configuration: %s", yaml.FormatError(err, false, false)) } - if d.logger != nil { - d.logger.Tracef("DockerAcquisition configuration: %+v", d.Config) + if s.logger != nil { + s.logger.Tracef("DockerAcquisition configuration: %+v", s.Config) } return nil } -func (d *Source) Configure(ctx context.Context, yamlConfig []byte, logger *log.Entry, metricsLevel metrics.AcquisitionMetricsLevel) error { - d.logger = logger - d.metricsLevel = metricsLevel +func (s *Source) Configure(ctx context.Context, yamlConfig []byte, logger *log.Entry, metricsLevel metrics.AcquisitionMetricsLevel) error { + s.logger = logger + s.metricsLevel = metricsLevel - err := d.UnmarshalConfig(yamlConfig) + err := s.UnmarshalConfig(yamlConfig) if err != nil { return err } return nil } + +func (c *Configuration) SetDefaults() { + if c.Namespace == "" { + c.Namespace = "default" + } + + if c.Mode == "" { + c.Mode = configuration.TAIL_MODE + } + if c.Auth == nil && c.KubeConfigFile == "" { + home, _ := os.UserHomeDir() + c.KubeConfigFile = filepath.Join(home, ".kube", "config") + } +} + +func (s *Source) Validate() error { + if s.Config.Label == "" { + return fmt.Errorf("label must be set in kubernetespodlogs acquisition") + } + if s.Config.Auth != nil && s.Config.KubeConfigFile != "" { + return fmt.Errorf("cannot use both auth and kube_config options") + + } + return nil +} diff --git a/pkg/acquisition/modules/kubernetespodlogs/metrics.go b/pkg/acquisition/modules/kubernetespodlogs/metrics.go index f262e538b1f..22788161876 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/metrics.go +++ b/pkg/acquisition/modules/kubernetespodlogs/metrics.go @@ -8,12 +8,12 @@ import ( func (*Source) GetMetrics() []prometheus.Collector { return []prometheus.Collector{ - metrics.DockerDatasourceLinesRead, + metrics.KubernetesPodLogsDataSourceLinesRead, } } func (*Source) GetAggregMetrics() []prometheus.Collector { return []prometheus.Collector{ - metrics.DockerDatasourceLinesRead, + metrics.KubernetesPodLogsDataSourceLinesRead, } } diff --git a/pkg/acquisition/modules/kubernetespodlogs/run.go b/pkg/acquisition/modules/kubernetespodlogs/run.go index 127afbac626..fea322257b9 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/run.go +++ b/pkg/acquisition/modules/kubernetespodlogs/run.go @@ -31,7 +31,7 @@ func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve d.logger.Info("Starting Kubernetes Pod Logs acquisition") - cfg, err := buildConfig() + cfg, err := d.buildConfig() if err != nil { log.Fatal(err) } diff --git a/pkg/acquisition/modules/kubernetespodlogs/utils.go b/pkg/acquisition/modules/kubernetespodlogs/utils.go index 19ec45445dc..ef8440188d9 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/utils.go +++ b/pkg/acquisition/modules/kubernetespodlogs/utils.go @@ -2,20 +2,33 @@ package kubernetespodlogs import ( "flag" - "os" - "path/filepath" + "fmt" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) -func buildConfig() (*rest.Config, error) { +func (d *Source) buildConfig() (*rest.Config, error) { cfg, err := rest.InClusterConfig() if err == nil { return cfg, nil } - home, _ := os.UserHomeDir() - kubeconfig := flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "kubeconfig path") - flag.Parse() - return clientcmd.BuildConfigFromFlags("", *kubeconfig) + if d.Config.KubeConfigFile != "" { + kubeconfig := flag.String("kubeconfig", d.Config.KubeConfigFile, "kubeconfig path") + flag.Parse() + return clientcmd.BuildConfigFromFlags("", *kubeconfig) + } + + if d.Config.Auth != nil { + loadingRules := &clientcmd.ClientConfigLoadingRules{} + configOverrides := &clientcmd.ConfigOverrides{ + ClusterInfo: d.Config.Auth.Cluster, + AuthInfo: d.Config.Auth.User, + CurrentContext: "", + } + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + return kubeConfig.ClientConfig() + } + // This should never happen, but just in case... + return nil, fmt.Errorf("could not create kubernetes client configuration") } diff --git a/pkg/cwversion/component/component.go b/pkg/cwversion/component/component.go index 3423f2423f2..cf2994ac339 100644 --- a/pkg/cwversion/component/component.go +++ b/pkg/cwversion/component/component.go @@ -8,24 +8,25 @@ package component // Built is a map of all the known components, and whether they are built-in or not. // This is populated as soon as possible by the respective init() functions var Built = map[string]bool{ - "datasource_appsec": false, - "datasource_cloudwatch": false, - "datasource_docker": false, - "datasource_file": false, - "datasource_journalctl": false, - "datasource_k8s-audit": false, - "datasource_kafka": false, - "datasource_kinesis": false, - "datasource_loki": false, - "datasource_s3": false, - "datasource_syslog": false, - "datasource_wineventlog": false, - "datasource_victorialogs": false, - "datasource_http": false, - "cscli_setup": false, - "db_mysql": false, - "db_postgres": false, - "db_sqlite": false, + "datasource_appsec": false, + "datasource_cloudwatch": false, + "datasource_docker": false, + "datasource_file": false, + "datasource_journalctl": false, + "datasource_k8s-audit": false, + "datasource_kafka": false, + "datasource_kinesis": false, + "datasource_kubernetespodlogs": false, + "datasource_loki": false, + "datasource_s3": false, + "datasource_syslog": false, + "datasource_wineventlog": false, + "datasource_victorialogs": false, + "datasource_http": false, + "cscli_setup": false, + "db_mysql": false, + "db_postgres": false, + "db_sqlite": false, } func Register(name string) { diff --git a/pkg/metrics/acquisition_k8s_podlogs.go b/pkg/metrics/acquisition_k8s_podlogs.go deleted file mode 100644 index ec1963c4b1c..00000000000 --- a/pkg/metrics/acquisition_k8s_podlogs.go +++ /dev/null @@ -1,20 +0,0 @@ -//go:build !no_datasource_k8s_podlogs - -package metrics - -import "github.com/prometheus/client_golang/prometheus" - -const K8SPodLogsLinesMetricName = "cs_k8spodlogs_lines_total" - -var K8SPodLogsLines = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: K8SPodLogsLinesMetricName, - Help: "Total lines collected by the Kubernetes pod logs datasource.", - }, - []string{"source", "datasource_type", "acquis_type"}, -) - -//nolint:gochecknoinits -func init() { - RegisterAcquisitionMetric(K8SPodLogsLinesMetricName) -} diff --git a/pkg/metrics/acquisition_kubernetespodlogs.go b/pkg/metrics/acquisition_kubernetespodlogs.go new file mode 100644 index 00000000000..08e18d34c3e --- /dev/null +++ b/pkg/metrics/acquisition_kubernetespodlogs.go @@ -0,0 +1,21 @@ +//go:build !no_datasource_kubernetespodlogs + +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +const KubernetesPodLogsDataSourceLinesReadMetricName = "cs_kubernetespodlogssource_hits_total" + +var KubernetesPodLogsDataSourceLinesRead = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: KubernetesPodLogsDataSourceLinesReadMetricName, + Help: "Total lines that were read.", + }, + []string{"source", "datasource_type", "acquis_type"}) + +//nolint:gochecknoinits +func init() { + RegisterAcquisitionMetric(KubernetesPodLogsDataSourceLinesReadMetricName) +} From 00596289b24c95214ed85c8d4b4febc0851ef534 Mon Sep 17 00:00:00 2001 From: sabban Date: Mon, 2 Feb 2026 17:46:44 +0100 Subject: [PATCH 08/23] don't flush local machines --- pkg/database/flush.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/database/flush.go b/pkg/database/flush.go index e508c53c0c7..9b37b05c09e 100644 --- a/pkg/database/flush.go +++ b/pkg/database/flush.go @@ -198,6 +198,7 @@ func (c *Client) flushAgents(ctx context.Context, authType string, duration *tim machine.LastHeartbeatLTE(time.Now().UTC().Add(-*duration)), machine.Not(machine.HasAlerts()), machine.AuthTypeEQ(authType), + machine.IpAddressNotIn("127.0.0.1", "::1"), ).Exec(ctx) if err != nil { c.Log.Errorf("while auto-deleting expired machines (%s): %s", authType, err) From d33e2ac4b59b80b85f59271cc4ce6f80632dc0c1 Mon Sep 17 00:00:00 2001 From: sabban Date: Fri, 6 Feb 2026 16:00:31 +0100 Subject: [PATCH 09/23] naming update --- pkg/acquisition/modules/kubernetespodlogs/config.go | 6 +++--- pkg/acquisition/modules/kubernetespodlogs/run.go | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pkg/acquisition/modules/kubernetespodlogs/config.go b/pkg/acquisition/modules/kubernetespodlogs/config.go index 0c95927d63c..ae2f14b40fe 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/config.go +++ b/pkg/acquisition/modules/kubernetespodlogs/config.go @@ -17,7 +17,7 @@ import ( type Configuration struct { configuration.DataSourceCommonCfg `yaml:",inline"` - Label string `yaml:"label"` + Selector string `yaml:"selector"` Namespace string `yaml:"namespace"` Auth *Auth `yaml:"auth,omitempty"` KubeConfigFile string `yaml:"kube_config,omitempty"` @@ -30,7 +30,7 @@ type Auth struct { func (s *Source) UnmarshalConfig(yamlConfig []byte) error { s.Config = Configuration{ - Label: "", + Selector: "", Namespace: "default", } @@ -72,7 +72,7 @@ func (c *Configuration) SetDefaults() { } func (s *Source) Validate() error { - if s.Config.Label == "" { + if s.Config.Selector == "" { return fmt.Errorf("label must be set in kubernetespodlogs acquisition") } if s.Config.Auth != nil && s.Config.KubeConfigFile != "" { diff --git a/pkg/acquisition/modules/kubernetespodlogs/run.go b/pkg/acquisition/modules/kubernetespodlogs/run.go index fea322257b9..33fa975abb4 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/run.go +++ b/pkg/acquisition/modules/kubernetespodlogs/run.go @@ -40,14 +40,11 @@ func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve log.Fatal(err) } - ns := "ingress-nginx" - labels := "app.kubernetes.io/component=controller" - cancels := map[string]context.CancelFunc{} f := informers.NewSharedInformerFactoryWithOptions(cs, 0, - informers.WithNamespace(ns), - informers.WithTweakListOptions(func(o *metav1.ListOptions) { o.LabelSelector = labels }), + informers.WithNamespace(d.Config.Namespace), + informers.WithTweakListOptions(func(o *metav1.ListOptions) { o.LabelSelector = d.Config.Selector }), ) inf := f.Core().V1().Pods().Informer() From b20513526753c1072bdd6f07b25f744f307955f8 Mon Sep 17 00:00:00 2001 From: sabban Date: Fri, 6 Feb 2026 16:51:22 +0100 Subject: [PATCH 10/23] clean up --- pkg/acquisition/modules/journalctl/config.go | 4 +--- .../modules/kubernetespodlogs/run.go | 19 ++++--------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/pkg/acquisition/modules/journalctl/config.go b/pkg/acquisition/modules/journalctl/config.go index 74d202062ef..ce748b7d595 100644 --- a/pkg/acquisition/modules/journalctl/config.go +++ b/pkg/acquisition/modules/journalctl/config.go @@ -18,7 +18,7 @@ type Configuration struct { configuration.DataSourceCommonCfg `yaml:",inline"` Filters []string `yaml:"journalctl_filter"` - since string // set only by DSN + since string // set only by DSN } func ConfigurationFromYAML(y []byte) (Configuration, error) { @@ -70,9 +70,7 @@ func (s *Source) Configure(_ context.Context, yamlConfig []byte, logger *log.Ent } s.setLogger(logger, 0, s.src) - s.metricsLevel = metricsLevel - return nil } diff --git a/pkg/acquisition/modules/kubernetespodlogs/run.go b/pkg/acquisition/modules/kubernetespodlogs/run.go index 33fa975abb4..056b64f5b3e 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/run.go +++ b/pkg/acquisition/modules/kubernetespodlogs/run.go @@ -48,7 +48,7 @@ func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve ) inf := f.Core().V1().Pods().Informer() - inf.AddEventHandler(cache.ResourceEventHandlerFuncs{ + err = inf.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { d.startPod(ctx, cs, obj.(*corev1.Pod), out, &wg, &mu, cancels) }, UpdateFunc: func(_, newObj interface{}) { d.startPod(ctx, cs, newObj.(*corev1.Pod), out, &wg, &mu, cancels) @@ -65,6 +65,9 @@ func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve }, }) + if err != nil { + return fmt.Errorf("while adding event handler: %w", err) + } f.Start(ctx.Done()) if !cache.WaitForCacheSync(ctx.Done(), inf.HasSynced) { log.Fatal("cache sync failed") @@ -171,17 +174,3 @@ func (*Source) stopPod(p *corev1.Pod, mu *sync.Mutex, cancels map[string]context cancel() } } - -func printer(ctx context.Context, in <-chan string) { - for { - select { - case <-ctx.Done(): - return - case line, ok := <-in: - if !ok { - return - } - fmt.Println(line) - } - } -} From ae4d9839d731ba6090107334fbd34a6427e0da5b Mon Sep 17 00:00:00 2001 From: sabban Date: Mon, 9 Feb 2026 11:52:43 +0100 Subject: [PATCH 11/23] clean up --- pkg/acquisition/modules/kubernetespodlogs/run.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/acquisition/modules/kubernetespodlogs/run.go b/pkg/acquisition/modules/kubernetespodlogs/run.go index 056b64f5b3e..44ee344908a 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/run.go +++ b/pkg/acquisition/modules/kubernetespodlogs/run.go @@ -48,7 +48,10 @@ func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve ) inf := f.Core().V1().Pods().Informer() - err = inf.AddEventHandler(cache.ResourceEventHandlerFuncs{ + // We ignore the ResourceEventHandlerRegistration returned by + // AddEventHandler since we don't need to remove the handlers until shutdown, + // and we will stop the entire informer at that time. + _, err = inf.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { d.startPod(ctx, cs, obj.(*corev1.Pod), out, &wg, &mu, cancels) }, UpdateFunc: func(_, newObj interface{}) { d.startPod(ctx, cs, newObj.(*corev1.Pod), out, &wg, &mu, cancels) @@ -70,7 +73,7 @@ func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve } f.Start(ctx.Done()) if !cache.WaitForCacheSync(ctx.Done(), inf.HasSynced) { - log.Fatal("cache sync failed") + return fmt.Errorf("cache sync failed") } <-ctx.Done() From bd8f759eeeac87b8435352c2c2c181eaf0189853 Mon Sep 17 00:00:00 2001 From: sabban Date: Mon, 9 Feb 2026 14:56:13 +0100 Subject: [PATCH 12/23] cleanup --- .../modules/kubernetespodlogs/run.go | 40 +++++++++---------- .../modules/kubernetespodlogs/source.go | 7 +--- .../modules/kubernetespodlogs/utils.go | 15 +++---- 3 files changed, 28 insertions(+), 34 deletions(-) diff --git a/pkg/acquisition/modules/kubernetespodlogs/run.go b/pkg/acquisition/modules/kubernetespodlogs/run.go index 44ee344908a..18afca55d41 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/run.go +++ b/pkg/acquisition/modules/kubernetespodlogs/run.go @@ -20,18 +20,18 @@ import ( "github.com/crowdsecurity/crowdsec/pkg/pipeline" ) -func (d *Source) OneShotAcquisition(ctx context.Context, out chan pipeline.Event, t *tomb.Tomb) error { - d.logger.Debug("In oneshot") +func (s *Source) OneShotAcquisition(ctx context.Context, out chan pipeline.Event, t *tomb.Tomb) error { + s.logger.Debug("In oneshot") return nil } -func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Event, t *tomb.Tomb) error { +func (s *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Event, t *tomb.Tomb) error { var wg sync.WaitGroup var mu sync.Mutex - d.logger.Info("Starting Kubernetes Pod Logs acquisition") + s.logger.Info("Starting Kubernetes Pod Logs acquisition") - cfg, err := d.buildConfig() + cfg, err := s.buildConfig() if err != nil { log.Fatal(err) } @@ -43,8 +43,8 @@ func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve cancels := map[string]context.CancelFunc{} f := informers.NewSharedInformerFactoryWithOptions(cs, 0, - informers.WithNamespace(d.Config.Namespace), - informers.WithTweakListOptions(func(o *metav1.ListOptions) { o.LabelSelector = d.Config.Selector }), + informers.WithNamespace(s.Config.Namespace), + informers.WithTweakListOptions(func(o *metav1.ListOptions) { o.LabelSelector = s.Config.Selector }), ) inf := f.Core().V1().Pods().Informer() @@ -52,9 +52,9 @@ func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve // AddEventHandler since we don't need to remove the handlers until shutdown, // and we will stop the entire informer at that time. _, err = inf.AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { d.startPod(ctx, cs, obj.(*corev1.Pod), out, &wg, &mu, cancels) }, + AddFunc: func(obj interface{}) { s.startPod(ctx, cs, obj.(*corev1.Pod), out, &wg, &mu, cancels) }, UpdateFunc: func(_, newObj interface{}) { - d.startPod(ctx, cs, newObj.(*corev1.Pod), out, &wg, &mu, cancels) + s.startPod(ctx, cs, newObj.(*corev1.Pod), out, &wg, &mu, cancels) }, DeleteFunc: func(obj interface{}) { pod, ok := obj.(*corev1.Pod) @@ -63,7 +63,7 @@ func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve pod, _ = t.Obj.(*corev1.Pod) } if pod != nil { - d.stopPod(pod, &mu, cancels) + s.stopPod(pod, &mu, cancels) } }, }) @@ -87,11 +87,11 @@ func (d *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve return nil } -func (d *Source) Dump() any { - return d +func (s *Source) Dump() any { + return s } -func (d *Source) followPodLogs(ctx context.Context, cs *kubernetes.Clientset, ns, pod, container string, onLine func(string) error) error { +func (*Source) followPodLogs(ctx context.Context, cs *kubernetes.Clientset, ns, pod, container string, onLine func(string) error) error { req := cs.CoreV1().Pods(ns).GetLogs(pod, &corev1.PodLogOptions{Container: container, Follow: true, Timestamps: true}) stream, err := req.Stream(ctx) if err != nil { @@ -112,7 +112,7 @@ func (d *Source) followPodLogs(ctx context.Context, cs *kubernetes.Clientset, ns } -func (d *Source) podWorker(meta context.Context, cs *kubernetes.Clientset, pod *corev1.Pod, out chan pipeline.Event, wg *sync.WaitGroup) context.CancelFunc { +func (s *Source) podWorker(meta context.Context, cs *kubernetes.Clientset, pod *corev1.Pod, out chan pipeline.Event, wg *sync.WaitGroup) context.CancelFunc { podCtx, cancel := context.WithCancel(meta) wg.Add(1) go func() { @@ -123,16 +123,16 @@ func (d *Source) podWorker(meta context.Context, cs *kubernetes.Clientset, pod * cw.Add(1) go func() { defer cw.Done() - _ = d.followPodLogs(podCtx, cs, pod.Namespace, pod.Name, c, func(line string) error { + _ = s.followPodLogs(podCtx, cs, pod.Namespace, pod.Name, c, func(line string) error { source := fmt.Sprintf("%s/%s", pod.Namespace, pod.Name) l := pipeline.Line{} l.Raw = line - l.Labels = d.Config.Labels + l.Labels = s.Config.Labels l.Time = time.Now().UTC() l.Src = source l.Process = true - l.Module = d.GetName() - if d.metricsLevel != metrics.AcquisitionMetricsLevelNone { + l.Module = s.GetName() + if s.metricsLevel != metrics.AcquisitionMetricsLevelNone { metrics.DockerDatasourceLinesRead.With(prometheus.Labels{"source": source, "acquis_type": l.Labels["type"], "datasource_type": ModuleName}).Inc() } evt := pipeline.MakeEvent(true, pipeline.LOG, true) @@ -151,7 +151,7 @@ func (d *Source) podWorker(meta context.Context, cs *kubernetes.Clientset, pod * func shouldTail(p *corev1.Pod) bool { return p.Status.Phase == corev1.PodRunning } -func (d *Source) startPod(meta context.Context, cs *kubernetes.Clientset, p *corev1.Pod, out chan pipeline.Event, wg *sync.WaitGroup, mu *sync.Mutex, cancels map[string]context.CancelFunc) { +func (s *Source) startPod(meta context.Context, cs *kubernetes.Clientset, p *corev1.Pod, out chan pipeline.Event, wg *sync.WaitGroup, mu *sync.Mutex, cancels map[string]context.CancelFunc) { if !shouldTail(p) { return } @@ -161,7 +161,7 @@ func (d *Source) startPod(meta context.Context, cs *kubernetes.Clientset, p *cor mu.Unlock() return } - cancels[key] = d.podWorker(meta, cs, p, out, wg) + cancels[key] = s.podWorker(meta, cs, p, out, wg) mu.Unlock() } diff --git a/pkg/acquisition/modules/kubernetespodlogs/source.go b/pkg/acquisition/modules/kubernetespodlogs/source.go index b6d4a632ec8..901e38e4fec 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/source.go +++ b/pkg/acquisition/modules/kubernetespodlogs/source.go @@ -3,8 +3,6 @@ package kubernetespodlogs import ( log "github.com/sirupsen/logrus" - "gopkg.in/tomb.v2" - "github.com/crowdsecurity/crowdsec/pkg/metrics" ) @@ -13,14 +11,13 @@ type Source struct { Config Configuration logger *log.Entry - t tomb.Tomb } func (*Source) GetName() string { return ModuleName } -func (d *Source) GetMode() string { +func (s *Source) GetMode() string { return d.Config.Mode } @@ -28,6 +25,6 @@ func (*Source) CanRun() error { return nil } -func (d *Source) GetUuid() string { +func (s *Source) GetUuid() string { return d.Config.UniqueId } diff --git a/pkg/acquisition/modules/kubernetespodlogs/utils.go b/pkg/acquisition/modules/kubernetespodlogs/utils.go index ef8440188d9..3472b883f65 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/utils.go +++ b/pkg/acquisition/modules/kubernetespodlogs/utils.go @@ -1,29 +1,26 @@ package kubernetespodlogs import ( - "flag" "fmt" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) -func (d *Source) buildConfig() (*rest.Config, error) { +func (s *Source) buildConfig() (*rest.Config, error) { cfg, err := rest.InClusterConfig() if err == nil { return cfg, nil } - if d.Config.KubeConfigFile != "" { - kubeconfig := flag.String("kubeconfig", d.Config.KubeConfigFile, "kubeconfig path") - flag.Parse() - return clientcmd.BuildConfigFromFlags("", *kubeconfig) + if s.Config.KubeConfigFile != "" { + return clientcmd.BuildConfigFromFlags("", s.Config.KubeConfigFile) } - if d.Config.Auth != nil { + if s.Config.Auth != nil { loadingRules := &clientcmd.ClientConfigLoadingRules{} configOverrides := &clientcmd.ConfigOverrides{ - ClusterInfo: d.Config.Auth.Cluster, - AuthInfo: d.Config.Auth.User, + ClusterInfo: s.Config.Auth.Cluster, + AuthInfo: s.Config.Auth.User, CurrentContext: "", } kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) From 14674454b30d3490abb90ad14b8c6ee779d97c0b Mon Sep 17 00:00:00 2001 From: sabban Date: Mon, 9 Feb 2026 16:48:06 +0100 Subject: [PATCH 13/23] typo --- pkg/acquisition/modules/kubernetespodlogs/source.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/acquisition/modules/kubernetespodlogs/source.go b/pkg/acquisition/modules/kubernetespodlogs/source.go index 901e38e4fec..6511c95adb6 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/source.go +++ b/pkg/acquisition/modules/kubernetespodlogs/source.go @@ -18,7 +18,7 @@ func (*Source) GetName() string { } func (s *Source) GetMode() string { - return d.Config.Mode + return s.Config.Mode } func (*Source) CanRun() error { @@ -26,5 +26,5 @@ func (*Source) CanRun() error { } func (s *Source) GetUuid() string { - return d.Config.UniqueId + return s.Config.UniqueId } From 7edf1e3bc2e0c8a3729d79fb592c8e8e7207f491 Mon Sep 17 00:00:00 2001 From: sabban Date: Mon, 9 Feb 2026 17:05:25 +0100 Subject: [PATCH 14/23] clean up --- pkg/acquisition/modules/kubernetespodlogs/config.go | 5 +++-- pkg/acquisition/modules/kubernetespodlogs/run.go | 3 ++- pkg/acquisition/modules/kubernetespodlogs/utils.go | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/acquisition/modules/kubernetespodlogs/config.go b/pkg/acquisition/modules/kubernetespodlogs/config.go index ae2f14b40fe..349d7ed3c79 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/config.go +++ b/pkg/acquisition/modules/kubernetespodlogs/config.go @@ -2,6 +2,7 @@ package kubernetespodlogs import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -73,10 +74,10 @@ func (c *Configuration) SetDefaults() { func (s *Source) Validate() error { if s.Config.Selector == "" { - return fmt.Errorf("label must be set in kubernetespodlogs acquisition") + return errors.New("label must be set in kubernetespodlogs acquisition") } if s.Config.Auth != nil && s.Config.KubeConfigFile != "" { - return fmt.Errorf("cannot use both auth and kube_config options") + return errors.New("cannot use both auth and kube_config options") } return nil diff --git a/pkg/acquisition/modules/kubernetespodlogs/run.go b/pkg/acquisition/modules/kubernetespodlogs/run.go index 18afca55d41..b37bff1c4d1 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/run.go +++ b/pkg/acquisition/modules/kubernetespodlogs/run.go @@ -3,6 +3,7 @@ package kubernetespodlogs import ( "bufio" "context" + "errors" "fmt" "log" "sync" @@ -73,7 +74,7 @@ func (s *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve } f.Start(ctx.Done()) if !cache.WaitForCacheSync(ctx.Done(), inf.HasSynced) { - return fmt.Errorf("cache sync failed") + return errors.New("cache sync failed") } <-ctx.Done() diff --git a/pkg/acquisition/modules/kubernetespodlogs/utils.go b/pkg/acquisition/modules/kubernetespodlogs/utils.go index 3472b883f65..4193915c1cf 100644 --- a/pkg/acquisition/modules/kubernetespodlogs/utils.go +++ b/pkg/acquisition/modules/kubernetespodlogs/utils.go @@ -1,7 +1,7 @@ package kubernetespodlogs import ( - "fmt" + "errors" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -27,5 +27,5 @@ func (s *Source) buildConfig() (*rest.Config, error) { return kubeConfig.ClientConfig() } // This should never happen, but just in case... - return nil, fmt.Errorf("could not create kubernetes client configuration") + return nil, errors.New("could not create kubernetes client configuration") } From c1285f965d44c076b41fa5dba74ccffb5c08771c Mon Sep 17 00:00:00 2001 From: sabban Date: Mon, 9 Feb 2026 18:07:12 +0100 Subject: [PATCH 15/23] remove unused .keep file --- pkg/acquisition/modules/kubernetespodlogs/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pkg/acquisition/modules/kubernetespodlogs/.keep diff --git a/pkg/acquisition/modules/kubernetespodlogs/.keep b/pkg/acquisition/modules/kubernetespodlogs/.keep deleted file mode 100644 index e69de29bb2d..00000000000 From 28b59dc952dfc90579d027c142ff6177f93f2b6b Mon Sep 17 00:00:00 2001 From: sabban Date: Mon, 9 Feb 2026 18:20:24 +0100 Subject: [PATCH 16/23] add import of the new datasource --- .../modules/{kubernetespodlogs => kubernetes}/config.go | 0 pkg/acquisition/modules/{kubernetespodlogs => kubernetes}/init.go | 0 .../{kubernetespodlogs => kubernetes}/k8s_podlogs_schema.yaml | 0 .../modules/{kubernetespodlogs => kubernetes}/metrics.go | 0 pkg/acquisition/modules/{kubernetespodlogs => kubernetes}/run.go | 0 .../modules/{kubernetespodlogs => kubernetes}/source.go | 0 .../modules/{kubernetespodlogs => kubernetes}/utils.go | 0 ...acquisition_kubernetespodlogs.go => acquisition_kubernetes.go} | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename pkg/acquisition/modules/{kubernetespodlogs => kubernetes}/config.go (100%) rename pkg/acquisition/modules/{kubernetespodlogs => kubernetes}/init.go (100%) rename pkg/acquisition/modules/{kubernetespodlogs => kubernetes}/k8s_podlogs_schema.yaml (100%) rename pkg/acquisition/modules/{kubernetespodlogs => kubernetes}/metrics.go (100%) rename pkg/acquisition/modules/{kubernetespodlogs => kubernetes}/run.go (100%) rename pkg/acquisition/modules/{kubernetespodlogs => kubernetes}/source.go (100%) rename pkg/acquisition/modules/{kubernetespodlogs => kubernetes}/utils.go (100%) rename pkg/metrics/{acquisition_kubernetespodlogs.go => acquisition_kubernetes.go} (100%) diff --git a/pkg/acquisition/modules/kubernetespodlogs/config.go b/pkg/acquisition/modules/kubernetes/config.go similarity index 100% rename from pkg/acquisition/modules/kubernetespodlogs/config.go rename to pkg/acquisition/modules/kubernetes/config.go diff --git a/pkg/acquisition/modules/kubernetespodlogs/init.go b/pkg/acquisition/modules/kubernetes/init.go similarity index 100% rename from pkg/acquisition/modules/kubernetespodlogs/init.go rename to pkg/acquisition/modules/kubernetes/init.go diff --git a/pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml b/pkg/acquisition/modules/kubernetes/k8s_podlogs_schema.yaml similarity index 100% rename from pkg/acquisition/modules/kubernetespodlogs/k8s_podlogs_schema.yaml rename to pkg/acquisition/modules/kubernetes/k8s_podlogs_schema.yaml diff --git a/pkg/acquisition/modules/kubernetespodlogs/metrics.go b/pkg/acquisition/modules/kubernetes/metrics.go similarity index 100% rename from pkg/acquisition/modules/kubernetespodlogs/metrics.go rename to pkg/acquisition/modules/kubernetes/metrics.go diff --git a/pkg/acquisition/modules/kubernetespodlogs/run.go b/pkg/acquisition/modules/kubernetes/run.go similarity index 100% rename from pkg/acquisition/modules/kubernetespodlogs/run.go rename to pkg/acquisition/modules/kubernetes/run.go diff --git a/pkg/acquisition/modules/kubernetespodlogs/source.go b/pkg/acquisition/modules/kubernetes/source.go similarity index 100% rename from pkg/acquisition/modules/kubernetespodlogs/source.go rename to pkg/acquisition/modules/kubernetes/source.go diff --git a/pkg/acquisition/modules/kubernetespodlogs/utils.go b/pkg/acquisition/modules/kubernetes/utils.go similarity index 100% rename from pkg/acquisition/modules/kubernetespodlogs/utils.go rename to pkg/acquisition/modules/kubernetes/utils.go diff --git a/pkg/metrics/acquisition_kubernetespodlogs.go b/pkg/metrics/acquisition_kubernetes.go similarity index 100% rename from pkg/metrics/acquisition_kubernetespodlogs.go rename to pkg/metrics/acquisition_kubernetes.go From a1a5a8e73503039bca13a34fcb58ea0281e203db Mon Sep 17 00:00:00 2001 From: sabban Date: Mon, 9 Feb 2026 18:21:35 +0100 Subject: [PATCH 17/23] change module name --- pkg/acquisition/modules/kubernetes/config.go | 4 ++-- pkg/acquisition/modules/kubernetes/init.go | 2 +- pkg/acquisition/modules/kubernetes/metrics.go | 2 +- pkg/acquisition/modules/kubernetes/run.go | 4 ++-- pkg/acquisition/modules/kubernetes/source.go | 2 +- pkg/metrics/acquisition_kubernetes.go | 10 +++++----- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/acquisition/modules/kubernetes/config.go b/pkg/acquisition/modules/kubernetes/config.go index 349d7ed3c79..a52b49e931b 100644 --- a/pkg/acquisition/modules/kubernetes/config.go +++ b/pkg/acquisition/modules/kubernetes/config.go @@ -1,4 +1,4 @@ -package kubernetespodlogs +package kubernetes import ( "context" @@ -40,7 +40,7 @@ func (s *Source) UnmarshalConfig(yamlConfig []byte) error { } if s.logger != nil { - s.logger.Tracef("DockerAcquisition configuration: %+v", s.Config) + s.logger.Tracef("Kubernetes configuration: %+v", s.Config) } return nil diff --git a/pkg/acquisition/modules/kubernetes/init.go b/pkg/acquisition/modules/kubernetes/init.go index b314a7e00ee..722bf304f32 100644 --- a/pkg/acquisition/modules/kubernetes/init.go +++ b/pkg/acquisition/modules/kubernetes/init.go @@ -1,4 +1,4 @@ -package kubernetespodlogs +package kubernetes import ( "github.com/crowdsecurity/crowdsec/pkg/acquisition/registry" diff --git a/pkg/acquisition/modules/kubernetes/metrics.go b/pkg/acquisition/modules/kubernetes/metrics.go index 22788161876..51c0788c327 100644 --- a/pkg/acquisition/modules/kubernetes/metrics.go +++ b/pkg/acquisition/modules/kubernetes/metrics.go @@ -1,4 +1,4 @@ -package kubernetespodlogs +package kubernetes import ( "github.com/prometheus/client_golang/prometheus" diff --git a/pkg/acquisition/modules/kubernetes/run.go b/pkg/acquisition/modules/kubernetes/run.go index b37bff1c4d1..40be606a314 100644 --- a/pkg/acquisition/modules/kubernetes/run.go +++ b/pkg/acquisition/modules/kubernetes/run.go @@ -1,4 +1,4 @@ -package kubernetespodlogs +package kubernetes import ( "bufio" @@ -134,7 +134,7 @@ func (s *Source) podWorker(meta context.Context, cs *kubernetes.Clientset, pod * l.Process = true l.Module = s.GetName() if s.metricsLevel != metrics.AcquisitionMetricsLevelNone { - metrics.DockerDatasourceLinesRead.With(prometheus.Labels{"source": source, "acquis_type": l.Labels["type"], "datasource_type": ModuleName}).Inc() + metrics.KubernetesDatasourceLinesRead.With(prometheus.Labels{"source": source, "acquis_type": l.Labels["type"], "datasource_type": ModuleName}).Inc() } evt := pipeline.MakeEvent(true, pipeline.LOG, true) evt.Line = l diff --git a/pkg/acquisition/modules/kubernetes/source.go b/pkg/acquisition/modules/kubernetes/source.go index 6511c95adb6..1ef65cf464b 100644 --- a/pkg/acquisition/modules/kubernetes/source.go +++ b/pkg/acquisition/modules/kubernetes/source.go @@ -1,4 +1,4 @@ -package kubernetespodlogs +package kubernetes import ( log "github.com/sirupsen/logrus" diff --git a/pkg/metrics/acquisition_kubernetes.go b/pkg/metrics/acquisition_kubernetes.go index 08e18d34c3e..4c2799b9cc9 100644 --- a/pkg/metrics/acquisition_kubernetes.go +++ b/pkg/metrics/acquisition_kubernetes.go @@ -1,4 +1,4 @@ -//go:build !no_datasource_kubernetespodlogs +//go:build !no_datasource_kubernetes package metrics @@ -6,16 +6,16 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -const KubernetesPodLogsDataSourceLinesReadMetricName = "cs_kubernetespodlogssource_hits_total" +const KubernetesDataSourceLinesReadMetricName = "cs_kubernetessource_hits_total" -var KubernetesPodLogsDataSourceLinesRead = prometheus.NewCounterVec( +var KubernetesDataSourceLinesRead = prometheus.NewCounterVec( prometheus.CounterOpts{ - Name: KubernetesPodLogsDataSourceLinesReadMetricName, + Name: KubernetesDataSourceLinesReadMetricName, Help: "Total lines that were read.", }, []string{"source", "datasource_type", "acquis_type"}) //nolint:gochecknoinits func init() { - RegisterAcquisitionMetric(KubernetesPodLogsDataSourceLinesReadMetricName) + RegisterAcquisitionMetric(KubernetesDataSourceLinesReadMetricName) } From 19eca662f455b1344cf70fe1b9a4c94581cd3f21 Mon Sep 17 00:00:00 2001 From: sabban Date: Tue, 10 Feb 2026 09:50:23 +0100 Subject: [PATCH 18/23] clean up, naming --- pkg/acquisition/modules/kubernetes.go | 5 +++++ pkg/acquisition/modules/kubernetes/config.go | 2 +- pkg/acquisition/modules/kubernetes/init.go | 2 +- pkg/acquisition/modules/kubernetes/metrics.go | 4 ++-- pkg/acquisition/modules/kubernetes/utils.go | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 pkg/acquisition/modules/kubernetes.go diff --git a/pkg/acquisition/modules/kubernetes.go b/pkg/acquisition/modules/kubernetes.go new file mode 100644 index 00000000000..164a020870a --- /dev/null +++ b/pkg/acquisition/modules/kubernetes.go @@ -0,0 +1,5 @@ +//go:build !no_datasource_kubernetespodlogs + +package modules + +import _ "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/kubernetes" // register the datasource diff --git a/pkg/acquisition/modules/kubernetes/config.go b/pkg/acquisition/modules/kubernetes/config.go index a52b49e931b..9be67250a9b 100644 --- a/pkg/acquisition/modules/kubernetes/config.go +++ b/pkg/acquisition/modules/kubernetes/config.go @@ -74,7 +74,7 @@ func (c *Configuration) SetDefaults() { func (s *Source) Validate() error { if s.Config.Selector == "" { - return errors.New("label must be set in kubernetespodlogs acquisition") + return errors.New("label must be set in kubernetes acquisition") } if s.Config.Auth != nil && s.Config.KubeConfigFile != "" { return errors.New("cannot use both auth and kube_config options") diff --git a/pkg/acquisition/modules/kubernetes/init.go b/pkg/acquisition/modules/kubernetes/init.go index 722bf304f32..bfa969c8972 100644 --- a/pkg/acquisition/modules/kubernetes/init.go +++ b/pkg/acquisition/modules/kubernetes/init.go @@ -13,7 +13,7 @@ var ( _ types.MetricsProvider = (*Source)(nil) ) -const ModuleName = "kubernetespodlogs" +const ModuleName = "kubernetes" //nolint:gochecknoinits func init() { diff --git a/pkg/acquisition/modules/kubernetes/metrics.go b/pkg/acquisition/modules/kubernetes/metrics.go index 51c0788c327..a6bdc1504e0 100644 --- a/pkg/acquisition/modules/kubernetes/metrics.go +++ b/pkg/acquisition/modules/kubernetes/metrics.go @@ -8,12 +8,12 @@ import ( func (*Source) GetMetrics() []prometheus.Collector { return []prometheus.Collector{ - metrics.KubernetesPodLogsDataSourceLinesRead, + metrics.KubernetesDataSourceLinesRead, } } func (*Source) GetAggregMetrics() []prometheus.Collector { return []prometheus.Collector{ - metrics.KubernetesPodLogsDataSourceLinesRead, + metrics.KubernetesDataSourceLinesRead, } } diff --git a/pkg/acquisition/modules/kubernetes/utils.go b/pkg/acquisition/modules/kubernetes/utils.go index 4193915c1cf..1accadfa45c 100644 --- a/pkg/acquisition/modules/kubernetes/utils.go +++ b/pkg/acquisition/modules/kubernetes/utils.go @@ -1,4 +1,4 @@ -package kubernetespodlogs +package kubernetes import ( "errors" From 5317e70d39da419674265ac4834d3fc3fec1294e Mon Sep 17 00:00:00 2001 From: sabban Date: Tue, 10 Feb 2026 16:43:48 +0100 Subject: [PATCH 19/23] cleanup --- pkg/acquisition/modules/kubernetes.go | 2 +- pkg/acquisition/modules/kubernetes/run.go | 4 +-- pkg/cwversion/component/component.go | 38 +++++++++++------------ 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/acquisition/modules/kubernetes.go b/pkg/acquisition/modules/kubernetes.go index 164a020870a..ffb1c4f5683 100644 --- a/pkg/acquisition/modules/kubernetes.go +++ b/pkg/acquisition/modules/kubernetes.go @@ -1,4 +1,4 @@ -//go:build !no_datasource_kubernetespodlogs +//go:build !no_datasource_kubernetes package modules diff --git a/pkg/acquisition/modules/kubernetes/run.go b/pkg/acquisition/modules/kubernetes/run.go index 40be606a314..6f50af09c0a 100644 --- a/pkg/acquisition/modules/kubernetes/run.go +++ b/pkg/acquisition/modules/kubernetes/run.go @@ -30,7 +30,7 @@ func (s *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve var wg sync.WaitGroup var mu sync.Mutex - s.logger.Info("Starting Kubernetes Pod Logs acquisition") + s.logger.Info("Starting Kubernetes acquisition") cfg, err := s.buildConfig() if err != nil { @@ -134,7 +134,7 @@ func (s *Source) podWorker(meta context.Context, cs *kubernetes.Clientset, pod * l.Process = true l.Module = s.GetName() if s.metricsLevel != metrics.AcquisitionMetricsLevelNone { - metrics.KubernetesDatasourceLinesRead.With(prometheus.Labels{"source": source, "acquis_type": l.Labels["type"], "datasource_type": ModuleName}).Inc() + metrics.KubernetesDataSourceLinesRead.With(prometheus.Labels{"source": source, "acquis_type": l.Labels["type"], "datasource_type": ModuleName}).Inc() } evt := pipeline.MakeEvent(true, pipeline.LOG, true) evt.Line = l diff --git a/pkg/cwversion/component/component.go b/pkg/cwversion/component/component.go index cf2994ac339..86dc9ba87e1 100644 --- a/pkg/cwversion/component/component.go +++ b/pkg/cwversion/component/component.go @@ -8,25 +8,25 @@ package component // Built is a map of all the known components, and whether they are built-in or not. // This is populated as soon as possible by the respective init() functions var Built = map[string]bool{ - "datasource_appsec": false, - "datasource_cloudwatch": false, - "datasource_docker": false, - "datasource_file": false, - "datasource_journalctl": false, - "datasource_k8s-audit": false, - "datasource_kafka": false, - "datasource_kinesis": false, - "datasource_kubernetespodlogs": false, - "datasource_loki": false, - "datasource_s3": false, - "datasource_syslog": false, - "datasource_wineventlog": false, - "datasource_victorialogs": false, - "datasource_http": false, - "cscli_setup": false, - "db_mysql": false, - "db_postgres": false, - "db_sqlite": false, + "datasource_appsec": false, + "datasource_cloudwatch": false, + "datasource_docker": false, + "datasource_file": false, + "datasource_journalctl": false, + "datasource_k8s-audit": false, + "datasource_kafka": false, + "datasource_kinesis": false, + "datasource_kubernetes": false, + "datasource_loki": false, + "datasource_s3": false, + "datasource_syslog": false, + "datasource_wineventlog": false, + "datasource_victorialogs": false, + "datasource_http": false, + "cscli_setup": false, + "db_mysql": false, + "db_postgres": false, + "db_sqlite": false, } func Register(name string) { From 0c940f5d613c8b4284200c3a36e595ccebf41e8c Mon Sep 17 00:00:00 2001 From: sabban Date: Tue, 10 Feb 2026 17:04:27 +0100 Subject: [PATCH 20/23] more lint cleanup --- pkg/acquisition/modules/kubernetes/run.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/acquisition/modules/kubernetes/run.go b/pkg/acquisition/modules/kubernetes/run.go index 6f50af09c0a..c7c61ced374 100644 --- a/pkg/acquisition/modules/kubernetes/run.go +++ b/pkg/acquisition/modules/kubernetes/run.go @@ -34,11 +34,11 @@ func (s *Source) StreamingAcquisition(ctx context.Context, out chan pipeline.Eve cfg, err := s.buildConfig() if err != nil { - log.Fatal(err) + return err } cs, err := kubernetes.NewForConfig(cfg) if err != nil { - log.Fatal(err) + return fmt.Errorf("can't create a kubernetes client: %s", err) } cancels := map[string]context.CancelFunc{} From 133f6b71fafff4a4b48104bb17765e4685ffb754 Mon Sep 17 00:00:00 2001 From: sabban Date: Tue, 10 Feb 2026 17:27:53 +0100 Subject: [PATCH 21/23] hop --- pkg/acquisition/modules/kubernetes/run.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/acquisition/modules/kubernetes/run.go b/pkg/acquisition/modules/kubernetes/run.go index c7c61ced374..5cd8a27c7a7 100644 --- a/pkg/acquisition/modules/kubernetes/run.go +++ b/pkg/acquisition/modules/kubernetes/run.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "log" "sync" "time" From 2874a91792992d026c717de7411058eefd3cd14a Mon Sep 17 00:00:00 2001 From: sabban Date: Tue, 10 Feb 2026 17:43:43 +0100 Subject: [PATCH 22/23] no one shot --- pkg/acquisition/modules/kubernetes/init.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/acquisition/modules/kubernetes/init.go b/pkg/acquisition/modules/kubernetes/init.go index bfa969c8972..14e70b9f609 100644 --- a/pkg/acquisition/modules/kubernetes/init.go +++ b/pkg/acquisition/modules/kubernetes/init.go @@ -8,7 +8,6 @@ import ( var ( // verify interface compliance _ types.DataSource = (*Source)(nil) - _ types.Fetcher = (*Source)(nil) _ types.Tailer = (*Source)(nil) _ types.MetricsProvider = (*Source)(nil) ) From e5bd9b4f9a656d53173f3ac6e66930bc60a5b6be Mon Sep 17 00:00:00 2001 From: sabban Date: Tue, 10 Feb 2026 17:51:48 +0100 Subject: [PATCH 23/23] update schema path and content --- .../kubernetes/k8s_podlogs_schema.yaml | 109 ------------------ pkg/acquisition/schemas/datasource.yaml | 1 + pkg/acquisition/schemas/kubernetes.yaml | 96 +++++++++++++++ 3 files changed, 97 insertions(+), 109 deletions(-) delete mode 100644 pkg/acquisition/modules/kubernetes/k8s_podlogs_schema.yaml create mode 100644 pkg/acquisition/schemas/kubernetes.yaml diff --git a/pkg/acquisition/modules/kubernetes/k8s_podlogs_schema.yaml b/pkg/acquisition/modules/kubernetes/k8s_podlogs_schema.yaml deleted file mode 100644 index d0fc09da543..00000000000 --- a/pkg/acquisition/modules/kubernetes/k8s_podlogs_schema.yaml +++ /dev/null @@ -1,109 +0,0 @@ -$schema: "http://json-schema.org/draft-07/schema#" -title: CrowdSec Kubernetes pod logs datasource configuration -type: object -required: - - type - - node_name -properties: - type: - const: k8s-podlogs - api_server: - type: string - description: Base URL of the Kubernetes API server. - default: https://kubernetes.default.svc - token_file: - type: string - description: Path to the service account token used to authenticate against the API server; at least one of token_file or bearer_token must be provided. - default: /var/run/secrets/kubernetes.io/serviceaccount/token - bearer_token: - type: string - description: Raw bearer token to use instead of reading token_file; at least one of bearer_token or token_file must be provided. - ca_cert: - type: string - description: Path to the CA bundle validating the API server certificate. - default: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - insecure_skip_verify: - type: boolean - description: Disable TLS verification when connecting to the API server. - default: false - node_name: - type: string - description: Name of the node whose pods should be tailed; typically set via the NODE_NAME environment variable. - namespaces: - type: array - description: Optional allow-list of namespaces to watch (defaults to all namespaces). - items: - type: string - label_selector: - type: string - description: Kubernetes label selector applied when listing pods. - containers: - type: array - description: Optional list of container names to include; defaults to all containers. - items: - type: string - since_seconds: - type: integer - minimum: 0 - description: Start log streaming from events newer than the provided age. - tail_lines: - type: integer - minimum: 0 - description: Number of most-recent log lines to read before following. - limit_bytes: - type: integer - minimum: 0 - description: Maximum number of bytes returned by the Kubernetes log endpoint for each request. - timestamps: - type: boolean - description: Request Kubernetes to prefix every log line with a timestamp. - default: false - follow: - type: boolean - description: Keep the log stream open to follow new entries. - default: true - resync_period: - type: string - pattern: '^([0-9]+(ns|us|ms|s|m|h))+$' - description: Interval between reconciliation cycles where new pod streams are created and stale ones are stopped. - default: 30s - request_timeout: - type: string - pattern: '^([0-9]+(ns|us|ms|s|m|h))+$' - description: Timeout applied to Kubernetes API list calls. - default: 10s - max_line_bytes: - type: integer - minimum: 1024 - description: Maximum accepted log line size before failing the stream. - default: 1048576 - mode: - type: string - enum: [tail] - description: Acquisition mode; kubernetes pod logs only support tail. - default: tail - labels: - type: object - additionalProperties: - type: string - description: Optional labels attached to every emitted event. - log_level: - type: string - enum: [panic, fatal, error, warn, info, debug, trace] - description: Overrides datasource logger level. - source: - type: string - description: Custom source string stamped on events. - name: - type: string - description: Datasource name used in metrics and logs. - use_time_machine: - type: boolean - description: Enable acquisition time-machine mode. - unique_id: - type: string - description: Stable identifier used for deduplication. - transform: - type: string - description: ExprLang transform applied to events. -additionalProperties: false diff --git a/pkg/acquisition/schemas/datasource.yaml b/pkg/acquisition/schemas/datasource.yaml index 4fef69cf509..a1ca051f51a 100644 --- a/pkg/acquisition/schemas/datasource.yaml +++ b/pkg/acquisition/schemas/datasource.yaml @@ -6,3 +6,4 @@ description: > configuration DataSourceCommonCfg. anyOf: - $ref: docker.yaml + - $ref: kubernetes.yaml diff --git a/pkg/acquisition/schemas/kubernetes.yaml b/pkg/acquisition/schemas/kubernetes.yaml new file mode 100644 index 00000000000..31189c4f95a --- /dev/null +++ b/pkg/acquisition/schemas/kubernetes.yaml @@ -0,0 +1,96 @@ +$schema: https://json-schema.org/draft/2020-12/schema +title: CrowdSec Kubernetes datasource +description: > + Schema for Kubernetes acquisition entries consumed by CrowdSec. Every field + mirrors pkg/acquisition/modules/kubernetes.Configuration and the embedded + configuration.DataSourceCommonCfg. +type: object +additionalProperties: false +properties: + source: + type: string + const: kubernetes + description: > + Must be kubernetes to bind this acquisition entry to the Kubernetes + datasource. + mode: + type: string + enum: [tail] + default: tail + description: > + Acquisition mode (only tail streaming is supported). + labels: + type: object + minProperties: 1 + description: > + Labels attached to emitted events (for example type: kubernetes). + additionalProperties: + type: string + properties: + type: + type: string + description: Parser/collection selector; strongly recommended. + log_level: + type: string + enum: [panic, fatal, error, warn, warning, info, debug, trace] + description: > + Overrides the module logger level for this datasource. + name: + type: string + description: Friendly identifier for the datasource entry. + use_time_machine: + type: boolean + default: false + description: > + Replays past events when supported by the acquisition module. + unique_id: + type: string + description: > + Stable identifier injected by cscli/crowdsec auto-run (usually not user set). + transform: + type: string + description: > + expr program applied to events before they enter the pipeline. + selector: + type: string + minLength: 1 + description: > + Kubernetes label selector applied to pods; at least one selector is required. + namespace: + type: string + description: > + Namespace whose pods should be tailed; defaults to default when omitted. + kube_config: + type: string + description: > + Path to a kubeconfig file to use when running outside the cluster. + auth: + type: object + minProperties: 1 + additionalProperties: false + description: > + Inline Kubernetes client authentication overriding kubeconfig defaults. + Mirrors clientcmd/api.Cluster and AuthInfo fields; keys match kubeconfig + file entries (for example server, certificate-authority, client-certificate, + token, exec, auth-provider, ...). + properties: + cluster: + type: object + additionalProperties: true + description: > + Optional cluster stanza (clientcmd/api.Cluster) with fields such as + server, certificate-authority, tls-server-name, or + insecure-skip-tls-verify. + user: + type: object + additionalProperties: true + description: > + Optional user stanza (clientcmd/api.AuthInfo) with fields such as + client-certificate, client-key, token, token-file, exec or auth-provider. +required: + - source + - selector +allOf: + - description: auth and kube_config are mutually exclusive. + not: + required: [auth, kube_config]