diff --git a/aws/logs_monitoring_go/Makefile b/aws/logs_monitoring_go/Makefile index a3878f199..af8ab7429 100644 --- a/aws/logs_monitoring_go/Makefile +++ b/aws/logs_monitoring_go/Makefile @@ -1,33 +1,54 @@ -.PHONY: build package test lint clean sam-build sam-invoke sam-deploy build-ForwarderFunction - BINARY_NAME := bootstrap ZIP_NAME := forwarder.zip -build: - GOOS=linux GOARCH=arm64 go build -o $(BINARY_NAME) ./cmd/forwarder/ +# Go -# Used by `sam build` -build-ForwarderFunction: - GOOS=linux GOARCH=arm64 go build -o $(ARTIFACTS_DIR)/bootstrap ./cmd/forwarder/ +.PHONY: build +build: + GOOS=linux GOARCH=arm64 go build -ldflags="-s" -installsuffix nocgo -o $(BINARY_NAME) ./cmd/forwarder/ +.PHONY: package package: build zip $(ZIP_NAME) $(BINARY_NAME) +.PHONY: test test: go test -race ./... +.PHONY: lint lint: golangci-lint run ./... +.PHONY: audit +audit: + go vet ./... + go tool govulncheck + +.PHONY: generate +generate: + go generate ./... + +.PHONY: clean clean: rm -f $(BINARY_NAME) $(ZIP_NAME) +# SAM + +# Used only by SAM +.PHONY: build-ForwarderFunction +build-ForwarderFunction: + GOOS=linux GOARCH=arm64 go build -ldflags="-s" -installsuffix nocgo -o $(ARTIFACTS_DIR)/bootstrap ./cmd/forwarder/ + +.PHONY: sam-build sam-build: sam build +.PHONY: sam-deploy +sam-deploy: sam-build + sam deploy + EVENT ?= events/cloudwatch_logs.json + +.PHONY: sam-invoke sam-invoke: sam-build sam local invoke ForwarderFunction -e $(EVENT) - -sam-deploy: sam-build - sam deploy diff --git a/aws/logs_monitoring_go/cmd/forwarder/main.go b/aws/logs_monitoring_go/cmd/forwarder/main.go index 347a2a716..5b8f8649b 100644 --- a/aws/logs_monitoring_go/cmd/forwarder/main.go +++ b/aws/logs_monitoring_go/cmd/forwarder/main.go @@ -8,16 +8,28 @@ package main import ( "context" "encoding/json" - "log" + "log/slog" + + "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/config" "github.com/aws/aws-lambda-go/lambda" ) -func handleRequest(ctx context.Context, event json.RawMessage) error { - log.Printf("Received event: %s", string(event)) - return nil +func main() { + ctx := context.Background() + cfg, err := config.Load(ctx) + if err != nil { + slog.Error("config load failed", slog.Any("error", err)) + return + } + + lambda.Start(handleRequest(cfg)) } -func main() { - lambda.Start(handleRequest) +// cfg not used for now, will be when forwarding logic added +func handleRequest(cfg *config.Config) func(context.Context, json.RawMessage) error { + return func(ctx context.Context, event json.RawMessage) error { + slog.Info("received event", slog.String("event", string(event))) + return nil + } } diff --git a/aws/logs_monitoring_go/go.mod b/aws/logs_monitoring_go/go.mod index 99422bf68..9b5d63d3e 100644 --- a/aws/logs_monitoring_go/go.mod +++ b/aws/logs_monitoring_go/go.mod @@ -2,4 +2,39 @@ module github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go go 1.26 -require github.com/aws/aws-lambda-go v1.53.0 +require ( + github.com/aws/aws-lambda-go v1.53.0 + github.com/aws/aws-sdk-go-v2 v1.41.4 + github.com/aws/aws-sdk-go-v2/config v1.32.12 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4 + github.com/google/go-cmp v0.7.0 +) + +require ( + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect + github.com/aws/smithy-go v1.24.2 // indirect + github.com/stretchr/testify v1.11.1 // indirect + go.uber.org/mock v0.6.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect + golang.org/x/tools v0.43.0 // indirect + golang.org/x/vuln v1.1.4 // indirect +) + +tool ( + go.uber.org/mock/mockgen + golang.org/x/tools/cmd/goimports + golang.org/x/vuln/cmd/govulncheck +) diff --git a/aws/logs_monitoring_go/go.sum b/aws/logs_monitoring_go/go.sum index 0d766f423..994d196a5 100644 --- a/aws/logs_monitoring_go/go.sum +++ b/aws/logs_monitoring_go/go.sum @@ -1,10 +1,62 @@ github.com/aws/aws-lambda-go v1.53.0 h1:uAMv6W/vCP/L494BAUSxe+8KVBIPK+SGPyapFt3FuMk= github.com/aws/aws-lambda-go v1.53.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4 h1:9aZbO86sraeCIHHCpZhxwN9tnVy9POkSKzi4/TpT54A= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4/go.mod h1:cxiXDhEzIq7Xx1BtmC4lGBK3SwAZ79+EUWiKawYHo14= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= +golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/aws/logs_monitoring_go/internal/config/apikey.go b/aws/logs_monitoring_go/internal/config/apikey.go new file mode 100644 index 000000000..f146f9f36 --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/apikey.go @@ -0,0 +1,103 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-Present Datadog, Inc. + +package config + +import ( + "context" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "os" + "time" + + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + awsconfig "github.com/aws/aws-sdk-go-v2/config" +) + +var ( + ErrMissingAPIKey = errors.New("missing Datadog API key") + ErrInvalidAPIKey = errors.New("invalid Datadog API key format") +) + +const ( + httpClientTimeout = 5 * time.Second +) + +func (c *Config) resolveAPIKey(ctx context.Context) error { + awsCfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(httpClientTimeout))) + if err != nil { + return fmt.Errorf("loading AWS config: %w", err) + } + + if v, ok := os.LookupEnv("DD_API_KEY_SECRET_ARN"); ok { + client, err := c.createSecretsManagerAPIClient(ctx, awsCfg) + if err != nil { + return fmt.Errorf("creating Secrets Manager client: %w", err) + } + + apiKey, err := resolveFromSecretsManager(ctx, client, v) + if err != nil { + return fmt.Errorf("resolving from secrets manager: %w", err) + } + + c.APIKey = apiKey + return nil + } + + if v, ok := os.LookupEnv("DD_API_KEY_SSM_NAME"); ok { + return c.resolveFromSSM(ctx, awsCfg, v) + } + + if v, ok := os.LookupEnv("DD_KMS_API_KEY"); ok { + return c.resolveFromKMS(ctx, awsCfg, v) + } + + return errors.New("no API key configured: set DD_API_KEY_SECRET_ARN, DD_API_KEY_SSM_NAME or DD_KMS_API_KEY. See: https://docs.datadoghq.com/serverless/forwarder/") +} + +func (c *Config) validateAPIKey(ctx context.Context) error { + if c.APIKey == "" { + return fmt.Errorf("set DD_API_KEY_SECRET_ARN, DD_API_KEY_SSM_NAME or DD_KMS_API_KEY. See: https://docs.datadoghq.com/serverless/forwarder/: %w", ErrMissingAPIKey) + } + + if len(c.APIKey) != 32 { + return fmt.Errorf("expected 32 characters, got %d. Verify your API key at https://app.%s/organization-settings/api-keys: %w", len(c.APIKey), c.Site, ErrInvalidAPIKey) + } + + slog.Debug("validating Datadog API key") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.APIURL+"/api/v1/validate", nil) + if err != nil { + slog.Warn("failed to build API key validation request", slog.Any("error", err)) + return nil + } + req.Header.Set("DD-API-KEY", c.APIKey) + + client := &http.Client{Timeout: httpClientTimeout} + resp, err := client.Do(req) + if err != nil { + slog.Warn("failed to validate API key", slog.Any("error", err)) + return nil + } + defer func() { + if _, err := io.Copy(io.Discard, resp.Body); err != nil { + slog.Warn("failed to drain response body", slog.Any("error", err)) + } + if err := resp.Body.Close(); err != nil { + slog.Warn("failed to close response body", slog.Any("error", err)) + } + }() + + if resp.StatusCode == http.StatusForbidden { + slog.Warn("invalid Datadog API key", slog.String("url", "https://app."+c.Site+"/organization-settings/api-keys")) + } else if resp.StatusCode != http.StatusOK { + slog.Warn("unexpected response from validation endpoint", slog.String("status", resp.Status)) + } + + return nil +} diff --git a/aws/logs_monitoring_go/internal/config/config.go b/aws/logs_monitoring_go/internal/config/config.go new file mode 100644 index 000000000..41a29be43 --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/config.go @@ -0,0 +1,50 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-Present Datadog, Inc. + +package config + +import ( + "context" + "fmt" + "log/slog" +) + +type Config struct { + APIKey string + Site string + IntakeURL string + APIURL string + LogLevel string + UseFIPS bool +} + +func Load(ctx context.Context) (*Config, error) { + initLogger(envOrDefault("DD_LOG_LEVEL", "INFO")) + logDroppedEnvVars() + + cfg := loadConfig() + slog.Debug("config loaded", slog.String("site", cfg.Site), slog.String("intakeURL", cfg.IntakeURL), slog.String("apiURL", cfg.APIURL), slog.String("logLevel", cfg.LogLevel), slog.Bool("useFIPS", cfg.UseFIPS)) + + if err := cfg.resolveAPIKey(ctx); err != nil { + return nil, fmt.Errorf("resolving API key: %w", err) + } + + if err := cfg.validateAPIKey(ctx); err != nil { + return nil, fmt.Errorf("validating API key: %w", err) + } + + return cfg, nil +} + +func loadConfig() *Config { + site := envOrDefault("DD_SITE", "datadoghq.com") + return &Config{ + Site: site, + IntakeURL: envOrDefault("DD_URL", "https://http-intake.logs."+site), + APIURL: envOrDefault("DD_API_URL", "https://api."+site), + LogLevel: envOrDefault("DD_LOG_LEVEL", "INFO"), + UseFIPS: envOrDefaultBool("DD_USE_FIPS", false), + } +} diff --git a/aws/logs_monitoring_go/internal/config/config_test.go b/aws/logs_monitoring_go/internal/config/config_test.go new file mode 100644 index 000000000..efddcac9f --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/config_test.go @@ -0,0 +1,63 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-Present Datadog, Inc. + +package config + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestLoadConfig(t *testing.T) { + tests := map[string]struct { + env map[string]string + want Config + }{ + "default": { + want: Config{ + Site: "datadoghq.com", + IntakeURL: "https://http-intake.logs.datadoghq.com", + APIURL: "https://api.datadoghq.com", + LogLevel: "INFO", + UseFIPS: false, + }, + }, + "eu_site": { + env: map[string]string{"DD_SITE": "datadoghq.eu"}, + want: Config{ + Site: "datadoghq.eu", + IntakeURL: "https://http-intake.logs.datadoghq.eu", + APIURL: "https://api.datadoghq.eu", + LogLevel: "INFO", + }, + }, + "custom_url": { + env: map[string]string{ + "DD_SITE": "datadoghq.com", + "DD_URL": "https://custom-intake.example.com", + }, + want: Config{ + Site: "datadoghq.com", + IntakeURL: "https://custom-intake.example.com", + APIURL: "https://api.datadoghq.com", + LogLevel: "INFO", + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + for k, v := range tc.env { + t.Setenv(k, v) + } + got := loadConfig() + if diff := cmp.Diff(tc.want, *got, cmpopts.IgnoreFields(Config{}, "APIKey")); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/aws/logs_monitoring_go/internal/config/environment.go b/aws/logs_monitoring_go/internal/config/environment.go new file mode 100644 index 000000000..d7d183699 --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/environment.go @@ -0,0 +1,60 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-Present Datadog, Inc. + +package config + +import ( + "log/slog" + "os" + "strconv" +) + +var deprecatedEnvironmentVariables = []string{ + "DD_ADDITIONAL_TARGET_LAMBDAS", + "DD_API_KEY", + "DD_COMPRESSION_LEVEL", + "DD_ENRICH_CLOUDWATCH_TAGS", + "DD_ENRICH_S3_TAGS", + "DD_FETCH_LAMBDA_TAGS", + "DD_FETCH_LOG_GROUP_TAGS", + "DD_FETCH_S3_TAGS", + "DD_FETCH_STEP_FUNCTIONS_TAGS", + "DD_FORWARD_LOG", + "DD_NO_SSL", + "DD_PORT", + "DD_SKIP_SSL_VALIDATION", + "DD_TAGS_CACHE_TTL_SECONDS", + "DD_TRACE_INTAKE_URL", + "DD_USE_COMPRESSION", + "DD_USE_VPC", +} + +func envOrDefault(key, fallback string) string { + if v, ok := os.LookupEnv(key); ok { + return v + } + return fallback +} + +func envOrDefaultBool(key string, fallback bool) bool { + v, ok := os.LookupEnv(key) + if !ok { + return fallback + } + b, err := strconv.ParseBool(v) + if err != nil { + slog.Warn("invalid boolean env var, using default", slog.String("key", key), slog.String("value", v), slog.Bool("default", fallback)) + return fallback + } + return b +} + +func logDroppedEnvVars() { + for _, name := range deprecatedEnvironmentVariables { + if _, ok := os.LookupEnv(name); ok { + slog.Warn("deprecated env var set, will be ignored", slog.String("name", name)) + } + } +} diff --git a/aws/logs_monitoring_go/internal/config/environment_test.go b/aws/logs_monitoring_go/internal/config/environment_test.go new file mode 100644 index 000000000..d2cf37896 --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/environment_test.go @@ -0,0 +1,66 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-Present Datadog, Inc. + +package config + +import "testing" + +func TestEnvOrDefault(t *testing.T) { + tests := map[string]struct { + key string + value string + set bool + fallback string + want string + }{ + "env_set": {key: "DD_TEST_VAR", value: "from_env", set: true, fallback: "default", want: "from_env"}, + "env_not_set": {key: "DD_TEST_VAR", value: "", set: false, fallback: "default", want: "default"}, + "env_set_empty": {key: "DD_TEST_VAR", value: "", set: true, fallback: "default", want: ""}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + if tc.set { + t.Setenv(tc.key, tc.value) + } + got := envOrDefault(tc.key, tc.fallback) + if got != tc.want { + t.Errorf("got %q, want %q", got, tc.want) + } + }) + } +} + +func TestEnvOrDefaultBool(t *testing.T) { + tests := map[string]struct { + value string + set bool + fallback bool + want bool + }{ + "env_not_set": {value: "", set: false, fallback: false, want: false}, + "env_not_set_fallback": {value: "", set: false, fallback: true, want: true}, + "true_lowercase": {value: "true", set: true, fallback: false, want: true}, + "true_uppercase": {value: "TRUE", set: true, fallback: false, want: true}, + "true_mixed_case": {value: "True", set: true, fallback: false, want: true}, + "false_value": {value: "false", set: true, fallback: true, want: false}, + "one_is_true": {value: "1", set: true, fallback: false, want: true}, + "zero_is_false": {value: "0", set: true, fallback: true, want: false}, + "invalid_uses_fallback": {value: "yes", set: true, fallback: true, want: true}, + "empty_uses_fallback": {value: "", set: true, fallback: true, want: true}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + if tc.set { + t.Setenv("DD_TEST_BOOL", tc.value) + } + got := envOrDefaultBool("DD_TEST_BOOL", tc.fallback) + if got != tc.want { + t.Errorf("got %v, want %v", got, tc.want) + } + }) + } +} diff --git a/aws/logs_monitoring_go/internal/config/kms.go b/aws/logs_monitoring_go/internal/config/kms.go new file mode 100644 index 000000000..028298132 --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/kms.go @@ -0,0 +1,16 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-Present Datadog, Inc. + +package config + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +func (c *Config) resolveFromKMS(ctx context.Context, awsCfg aws.Config, ciphertext string) error { + return nil +} diff --git a/aws/logs_monitoring_go/internal/config/logger.go b/aws/logs_monitoring_go/internal/config/logger.go new file mode 100644 index 000000000..91cbcff3b --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/logger.go @@ -0,0 +1,19 @@ +package config + +import ( + "log/slog" + "os" +) + +func initLogger(level string) { + var slogLevel slog.Level + + if err := slogLevel.UnmarshalText([]byte(level)); err != nil { + slog.Error(err.Error(), slog.String("level", level)) + slogLevel = slog.LevelInfo + } + + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + Level: slogLevel, + }))) +} diff --git a/aws/logs_monitoring_go/internal/config/secretsmanager.go b/aws/logs_monitoring_go/internal/config/secretsmanager.go new file mode 100644 index 000000000..c96af9415 --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/secretsmanager.go @@ -0,0 +1,59 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-Present Datadog, Inc. + +package config + +//go:generate go tool mockgen -source=secretsmanager.go -package=config -destination=secretsmanager_mockgen.go + +import ( + "context" + "fmt" + "log/slog" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" +) + +type SecretsManagerAPIClient interface { + GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) +} + +func (c *Config) createSecretsManagerAPIClient(ctx context.Context, awsCfg aws.Config) (SecretsManagerAPIClient, error) { + resolver := secretsmanager.NewDefaultEndpointResolverV2() + params := secretsmanager.EndpointParameters{ + Region: aws.String(awsCfg.Region), + UseFIPS: aws.Bool(c.UseFIPS), + } + + endpoint, err := resolver.ResolveEndpoint(ctx, params) + if err != nil && c.UseFIPS { + slog.Warn("FIPS endpoint not available, falling back to standard endpoint", slog.String("service", "secretsmanager"), slog.String("region", awsCfg.Region)) + params.UseFIPS = aws.Bool(false) + endpoint, err = resolver.ResolveEndpoint(ctx, params) + } + if err != nil { + return nil, fmt.Errorf("resolve endpoint: %w", err) + } + + return secretsmanager.NewFromConfig(awsCfg, func(o *secretsmanager.Options) { + o.BaseEndpoint = aws.String(endpoint.URI.String()) + }), nil +} + +func resolveFromSecretsManager(ctx context.Context, client SecretsManagerAPIClient, arn string) (string, error) { + result, err := client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ + SecretId: aws.String(arn), + }) + if err != nil { + return "", fmt.Errorf("fetching secret `%s`: %w", arn, err) + } + + if result.SecretString == nil { + return "", fmt.Errorf("secret `%s` has no string value", arn) + } + + return strings.TrimSpace(*result.SecretString), nil +} diff --git a/aws/logs_monitoring_go/internal/config/secretsmanager_mockgen.go b/aws/logs_monitoring_go/internal/config/secretsmanager_mockgen.go new file mode 100644 index 000000000..e5f05e3ee --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/secretsmanager_mockgen.go @@ -0,0 +1,62 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: secretsmanager.go +// +// Generated by this command: +// +// mockgen -source=secretsmanager.go -package=config -destination=secretsmanager_mockgen.go +// + +// Package config is a generated GoMock package. +package config + +import ( + context "context" + reflect "reflect" + + secretsmanager "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + gomock "go.uber.org/mock/gomock" +) + +// MockSecretsManagerAPIClient is a mock of SecretsManagerAPIClient interface. +type MockSecretsManagerAPIClient struct { + ctrl *gomock.Controller + recorder *MockSecretsManagerAPIClientMockRecorder + isgomock struct{} +} + +// MockSecretsManagerAPIClientMockRecorder is the mock recorder for MockSecretsManagerAPIClient. +type MockSecretsManagerAPIClientMockRecorder struct { + mock *MockSecretsManagerAPIClient +} + +// NewMockSecretsManagerAPIClient creates a new mock instance. +func NewMockSecretsManagerAPIClient(ctrl *gomock.Controller) *MockSecretsManagerAPIClient { + mock := &MockSecretsManagerAPIClient{ctrl: ctrl} + mock.recorder = &MockSecretsManagerAPIClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSecretsManagerAPIClient) EXPECT() *MockSecretsManagerAPIClientMockRecorder { + return m.recorder +} + +// GetSecretValue mocks base method. +func (m *MockSecretsManagerAPIClient) GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range optFns { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetSecretValue", varargs...) + ret0, _ := ret[0].(*secretsmanager.GetSecretValueOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSecretValue indicates an expected call of GetSecretValue. +func (mr *MockSecretsManagerAPIClientMockRecorder) GetSecretValue(ctx, params any, optFns ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, optFns...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecretValue", reflect.TypeOf((*MockSecretsManagerAPIClient)(nil).GetSecretValue), varargs...) +} diff --git a/aws/logs_monitoring_go/internal/config/secretsmanager_test.go b/aws/logs_monitoring_go/internal/config/secretsmanager_test.go new file mode 100644 index 000000000..22a9430e5 --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/secretsmanager_test.go @@ -0,0 +1,90 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-Present Datadog, Inc. + +package config + +import ( + "context" + "errors" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "go.uber.org/mock/gomock" +) + +func TestResolveFromSecretsManager(t *testing.T) { + tests := map[string]struct { + mockSetup func(m *MockSecretsManagerAPIClient) + arn string + wantKey string + wantErr bool + }{ + "success": { + mockSetup: func(m *MockSecretsManagerAPIClient) { + m.EXPECT(). + GetSecretValue(gomock.Any(), gomock.Any()). + Return(&secretsmanager.GetSecretValueOutput{ + SecretString: aws.String("abcdef1234567890abcdef1234567890"), + }, nil) + }, + arn: "arn:aws:secretsmanager:us-east-1:012345678901:secret:my-secret", + wantKey: "abcdef1234567890abcdef1234567890", + }, + "whitespace_trimmed": { + mockSetup: func(m *MockSecretsManagerAPIClient) { + m.EXPECT(). + GetSecretValue(gomock.Any(), gomock.Any()). + Return(&secretsmanager.GetSecretValueOutput{ + SecretString: aws.String(" abcdef1234567890abcdef1234567890 \n"), + }, nil) + }, + arn: "arn:aws:secretsmanager:us-east-1:012345678901:secret:my-secret", + wantKey: "abcdef1234567890abcdef1234567890", + }, + "aws_error": { + mockSetup: func(m *MockSecretsManagerAPIClient) { + m.EXPECT(). + GetSecretValue(gomock.Any(), gomock.Any()). + Return(nil, errors.New("AccessDeniedException: access denied")) + }, + arn: "arn:aws:secretsmanager:us-east-1:012345678901:secret:my-secret", + wantErr: true, + }, + "nil_secret_string": { + mockSetup: func(m *MockSecretsManagerAPIClient) { + m.EXPECT(). + GetSecretValue(gomock.Any(), gomock.Any()). + Return(&secretsmanager.GetSecretValueOutput{ + SecretString: nil, + }, nil) + }, + arn: "arn:aws:secretsmanager:us-east-1:012345678901:secret:my-secret", + wantErr: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + mock := NewMockSecretsManagerAPIClient(ctrl) + tc.mockSetup(mock) + + got, err := resolveFromSecretsManager(context.Background(), mock, tc.arn) + if tc.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.wantKey { + t.Errorf("got %q, want %q", got, tc.wantKey) + } + }) + } +} diff --git a/aws/logs_monitoring_go/internal/config/ssm.go b/aws/logs_monitoring_go/internal/config/ssm.go new file mode 100644 index 000000000..29f071b94 --- /dev/null +++ b/aws/logs_monitoring_go/internal/config/ssm.go @@ -0,0 +1,16 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-Present Datadog, Inc. + +package config + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +func (c *Config) resolveFromSSM(ctx context.Context, awsCfg aws.Config, name string) error { + return nil +}