diff --git a/aws/logs_monitoring_go/internal/client/kms.go b/aws/logs_monitoring_go/internal/client/kms.go new file mode 100644 index 000000000..2abbb7147 --- /dev/null +++ b/aws/logs_monitoring_go/internal/client/kms.go @@ -0,0 +1,72 @@ +// 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 client + +//go:generate go tool mockgen -source=kms.go -package=client -destination=kms_mockgen.go + +import ( + "context" + "encoding/base64" + "fmt" + "log/slog" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" +) + +type KMS interface { + Decrypt(ctx context.Context, params *kms.DecryptInput, optFns ...func(*kms.Options)) (*kms.DecryptOutput, error) +} + +func NewKMS(ctx context.Context, useFIPS bool) (KMS, error) { + cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(timeout))) + if err != nil { + return nil, err + } + + resolver := kms.NewDefaultEndpointResolverV2() + params := kms.EndpointParameters{ + Region: aws.String(cfg.Region), + UseFIPS: aws.Bool(useFIPS), + } + + endpoint, err := resolver.ResolveEndpoint(ctx, params) + if err != nil && useFIPS { + slog.Warn("FIPS endpoint not available, falling back to standard endpoint", slog.String("service", "kms"), slog.String("region", cfg.Region)) + params.UseFIPS = aws.Bool(false) + endpoint, err = resolver.ResolveEndpoint(ctx, params) + } + if err != nil { + return nil, fmt.Errorf("resolve endpoint: %w", err) + } + + return kms.NewFromConfig(cfg, func(o *kms.Options) { + o.BaseEndpoint = aws.String(endpoint.URI.String()) + }), nil +} + +func DecryptKMSCiphertext(ctx context.Context, kmsClient KMS, ciphertext string) (string, error) { + decoded, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", fmt.Errorf("base64-decoding ciphertext: %w", err) + } + + result, err := kmsClient.Decrypt(ctx, &kms.DecryptInput{ + CiphertextBlob: decoded, + }) + if err != nil { + return "", fmt.Errorf("decrypting KMS ciphertext: %w", err) + } + + if result.Plaintext == nil { + return "", fmt.Errorf("KMS decryption returned no plaintext") + } + + return strings.TrimSpace(string(result.Plaintext)), nil +} diff --git a/aws/logs_monitoring_go/internal/client/kms_mockgen.go b/aws/logs_monitoring_go/internal/client/kms_mockgen.go new file mode 100644 index 000000000..07e6fa6f7 --- /dev/null +++ b/aws/logs_monitoring_go/internal/client/kms_mockgen.go @@ -0,0 +1,62 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: kms.go +// +// Generated by this command: +// +// mockgen -source=kms.go -package=client -destination=kms_mockgen.go +// + +// Package client is a generated GoMock package. +package client + +import ( + context "context" + reflect "reflect" + + kms "github.com/aws/aws-sdk-go-v2/service/kms" + gomock "go.uber.org/mock/gomock" +) + +// MockKMS is a mock of KMS interface. +type MockKMS struct { + ctrl *gomock.Controller + recorder *MockKMSMockRecorder + isgomock struct{} +} + +// MockKMSMockRecorder is the mock recorder for MockKMS. +type MockKMSMockRecorder struct { + mock *MockKMS +} + +// NewMockKMS creates a new mock instance. +func NewMockKMS(ctrl *gomock.Controller) *MockKMS { + mock := &MockKMS{ctrl: ctrl} + mock.recorder = &MockKMSMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockKMS) EXPECT() *MockKMSMockRecorder { + return m.recorder +} + +// Decrypt mocks base method. +func (m *MockKMS) Decrypt(ctx context.Context, params *kms.DecryptInput, optFns ...func(*kms.Options)) (*kms.DecryptOutput, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range optFns { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Decrypt", varargs...) + ret0, _ := ret[0].(*kms.DecryptOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Decrypt indicates an expected call of Decrypt. +func (mr *MockKMSMockRecorder) Decrypt(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, "Decrypt", reflect.TypeOf((*MockKMS)(nil).Decrypt), varargs...) +} diff --git a/aws/logs_monitoring_go/internal/config/kms_test.go b/aws/logs_monitoring_go/internal/client/kms_test.go similarity index 82% rename from aws/logs_monitoring_go/internal/config/kms_test.go rename to aws/logs_monitoring_go/internal/client/kms_test.go index f67a0820c..b138e0e8a 100644 --- a/aws/logs_monitoring_go/internal/config/kms_test.go +++ b/aws/logs_monitoring_go/internal/client/kms_test.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2026-Present Datadog, Inc. -package config +package client import ( "encoding/base64" @@ -16,17 +16,17 @@ import ( "go.uber.org/mock/gomock" ) -func TestResolveFromKMS(t *testing.T) { +func TestDecryptKMSCiphertext(t *testing.T) { validCiphertext := base64.StdEncoding.EncodeToString([]byte("encrypted-blob")) tests := map[string]struct { - mockSetup func(m *MockKMSAPIClient) + mockSetup func(m *MockKMS) ciphertext string wantKey string wantErr bool }{ "success": { - mockSetup: func(m *MockKMSAPIClient) { + mockSetup: func(m *MockKMS) { m.EXPECT(). Decrypt(gomock.Any(), gomock.Any()). Return(&kms.DecryptOutput{ @@ -37,7 +37,7 @@ func TestResolveFromKMS(t *testing.T) { wantKey: "abcdef1234567890abcdef1234567890", }, "whitespace_trimmed": { - mockSetup: func(m *MockKMSAPIClient) { + mockSetup: func(m *MockKMS) { m.EXPECT(). Decrypt(gomock.Any(), gomock.Any()). Return(&kms.DecryptOutput{ @@ -48,12 +48,12 @@ func TestResolveFromKMS(t *testing.T) { wantKey: "abcdef1234567890abcdef1234567890", }, "invalid_base64": { - mockSetup: func(m *MockKMSAPIClient) {}, + mockSetup: func(m *MockKMS) {}, ciphertext: "not-valid-base64!@#$", wantErr: true, }, "aws_error": { - mockSetup: func(m *MockKMSAPIClient) { + mockSetup: func(m *MockKMS) { m.EXPECT(). Decrypt(gomock.Any(), gomock.Any()). Return(nil, errors.New("InvalidCiphertextException: invalid ciphertext")) @@ -62,7 +62,7 @@ func TestResolveFromKMS(t *testing.T) { wantErr: true, }, "nil_plaintext": { - mockSetup: func(m *MockKMSAPIClient) { + mockSetup: func(m *MockKMS) { m.EXPECT(). Decrypt(gomock.Any(), gomock.Any()). Return(&kms.DecryptOutput{ @@ -77,10 +77,10 @@ func TestResolveFromKMS(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { ctrl := gomock.NewController(t) - mock := NewMockKMSAPIClient(ctrl) + mock := NewMockKMS(ctrl) tc.mockSetup(mock) - got, err := decryptKMSCiphertext(t.Context(), mock, tc.ciphertext) + got, err := DecryptKMSCiphertext(t.Context(), mock, tc.ciphertext) if tc.wantErr { require.Error(t, err) return diff --git a/aws/logs_monitoring_go/internal/handling/s3_client.go b/aws/logs_monitoring_go/internal/client/s3.go similarity index 70% rename from aws/logs_monitoring_go/internal/handling/s3_client.go rename to aws/logs_monitoring_go/internal/client/s3.go index 29738b379..69424ead7 100644 --- a/aws/logs_monitoring_go/internal/handling/s3_client.go +++ b/aws/logs_monitoring_go/internal/client/s3.go @@ -3,9 +3,9 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2026-Present Datadog, Inc. -package handling +package client -//go:generate go tool mockgen -source=s3_client.go -package=handling -destination=s3_client_mockgen.go +//go:generate go tool mockgen -source=s3.go -package=client -destination=s3_mockgen.go import ( "context" @@ -25,22 +25,25 @@ const timeout = 10 * time.Second var ( s3ClientOnce sync.Once - s3Client S3APIClient + s3Client S3 s3ClientErr error ) -type S3APIClient interface { +type S3 interface { GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) + PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) + DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) + ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) } -func getS3APIClient(ctx context.Context, useFIPS bool) (S3APIClient, error) { +func GetS3(ctx context.Context, useFIPS bool) (S3, error) { s3ClientOnce.Do(func() { - s3Client, s3ClientErr = createS3APIClient(ctx, useFIPS) + s3Client, s3ClientErr = newS3(ctx, useFIPS) }) return s3Client, s3ClientErr } -func createS3APIClient(ctx context.Context, useFIPS bool) (S3APIClient, error) { +func newS3(ctx context.Context, useFIPS bool) (S3, error) { cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(timeout))) if err != nil { return nil, err @@ -67,7 +70,7 @@ func createS3APIClient(ctx context.Context, useFIPS bool) (S3APIClient, error) { }), nil } -func getS3Object(ctx context.Context, client S3APIClient, bucket, key string) (io.ReadCloser, error) { +func GetS3Object(ctx context.Context, client S3, bucket, key string) (io.ReadCloser, error) { result, err := client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key), diff --git a/aws/logs_monitoring_go/internal/client/s3_mockgen.go b/aws/logs_monitoring_go/internal/client/s3_mockgen.go new file mode 100644 index 000000000..faa8df816 --- /dev/null +++ b/aws/logs_monitoring_go/internal/client/s3_mockgen.go @@ -0,0 +1,122 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: s3.go +// +// Generated by this command: +// +// mockgen -source=s3.go -package=client -destination=s3_mockgen.go +// + +// Package client is a generated GoMock package. +package client + +import ( + context "context" + reflect "reflect" + + s3 "github.com/aws/aws-sdk-go-v2/service/s3" + gomock "go.uber.org/mock/gomock" +) + +// MockS3 is a mock of S3 interface. +type MockS3 struct { + ctrl *gomock.Controller + recorder *MockS3MockRecorder + isgomock struct{} +} + +// MockS3MockRecorder is the mock recorder for MockS3. +type MockS3MockRecorder struct { + mock *MockS3 +} + +// NewMockS3 creates a new mock instance. +func NewMockS3(ctrl *gomock.Controller) *MockS3 { + mock := &MockS3{ctrl: ctrl} + mock.recorder = &MockS3MockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockS3) EXPECT() *MockS3MockRecorder { + return m.recorder +} + +// DeleteObject mocks base method. +func (m *MockS3) DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range optFns { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteObject", varargs...) + ret0, _ := ret[0].(*s3.DeleteObjectOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteObject indicates an expected call of DeleteObject. +func (mr *MockS3MockRecorder) DeleteObject(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, "DeleteObject", reflect.TypeOf((*MockS3)(nil).DeleteObject), varargs...) +} + +// GetObject mocks base method. +func (m *MockS3) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range optFns { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetObject", varargs...) + ret0, _ := ret[0].(*s3.GetObjectOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetObject indicates an expected call of GetObject. +func (mr *MockS3MockRecorder) GetObject(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, "GetObject", reflect.TypeOf((*MockS3)(nil).GetObject), varargs...) +} + +// ListObjectsV2 mocks base method. +func (m *MockS3) ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range optFns { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListObjectsV2", varargs...) + ret0, _ := ret[0].(*s3.ListObjectsV2Output) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListObjectsV2 indicates an expected call of ListObjectsV2. +func (mr *MockS3MockRecorder) ListObjectsV2(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, "ListObjectsV2", reflect.TypeOf((*MockS3)(nil).ListObjectsV2), varargs...) +} + +// PutObject mocks base method. +func (m *MockS3) PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range optFns { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PutObject", varargs...) + ret0, _ := ret[0].(*s3.PutObjectOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PutObject indicates an expected call of PutObject. +func (mr *MockS3MockRecorder) PutObject(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, "PutObject", reflect.TypeOf((*MockS3)(nil).PutObject), varargs...) +} diff --git a/aws/logs_monitoring_go/internal/handling/s3_client_test.go b/aws/logs_monitoring_go/internal/client/s3_test.go similarity index 84% rename from aws/logs_monitoring_go/internal/handling/s3_client_test.go rename to aws/logs_monitoring_go/internal/client/s3_test.go index 33c53a26a..2aef240ba 100644 --- a/aws/logs_monitoring_go/internal/handling/s3_client_test.go +++ b/aws/logs_monitoring_go/internal/client/s3_test.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2026-Present Datadog, Inc. -package handling +package client import ( "errors" @@ -20,13 +20,13 @@ func TestGetS3Object(t *testing.T) { t.Parallel() tests := map[string]struct { - mockSetup func(m *MockS3APIClient) + mockSetup func(m *MockS3) bucket string key string wantErr bool }{ "returns body on success": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *MockS3) { m.EXPECT(). GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ @@ -37,7 +37,7 @@ func TestGetS3Object(t *testing.T) { key: "my-key", }, "returns error on S3 failure": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *MockS3) { m.EXPECT(). GetObject(gomock.Any(), gomock.Any()). Return(nil, errors.New("")) @@ -52,10 +52,10 @@ func TestGetS3Object(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mock := NewMockS3APIClient(ctrl) + mock := NewMockS3(ctrl) tc.mockSetup(mock) - body, err := getS3Object(t.Context(), mock, tc.bucket, tc.key) + body, err := GetS3Object(t.Context(), mock, tc.bucket, tc.key) if tc.wantErr { require.Error(t, err) diff --git a/aws/logs_monitoring_go/internal/client/secrets_manager.go b/aws/logs_monitoring_go/internal/client/secrets_manager.go new file mode 100644 index 000000000..ff37c645d --- /dev/null +++ b/aws/logs_monitoring_go/internal/client/secrets_manager.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 client + +//go:generate go tool mockgen -source=secrets_manager.go -package=client -destination=secrets_manager_mockgen.go + +import ( + "context" + "fmt" + "log/slog" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" +) + +type SecretsManager interface { + GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) +} + +func NewSecretsManager(ctx context.Context, useFIPS bool) (SecretsManager, error) { + cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(timeout))) + if err != nil { + return nil, err + } + + resolver := secretsmanager.NewDefaultEndpointResolverV2() + params := secretsmanager.EndpointParameters{ + Region: aws.String(cfg.Region), + UseFIPS: aws.Bool(useFIPS), + } + + endpoint, err := resolver.ResolveEndpoint(ctx, params) + if err != nil && useFIPS { + slog.Warn("FIPS endpoint not available, falling back to standard endpoint", slog.String("service", "secretsmanager"), slog.String("region", cfg.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(cfg, func(o *secretsmanager.Options) { + o.BaseEndpoint = aws.String(endpoint.URI.String()) + }), nil +} + +func FetchSecret(ctx context.Context, smClient SecretsManager, arn string) (string, error) { + result, err := smClient.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/client/secrets_manager_mockgen.go b/aws/logs_monitoring_go/internal/client/secrets_manager_mockgen.go new file mode 100644 index 000000000..bad856a67 --- /dev/null +++ b/aws/logs_monitoring_go/internal/client/secrets_manager_mockgen.go @@ -0,0 +1,62 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: secrets_manager.go +// +// Generated by this command: +// +// mockgen -source=secrets_manager.go -package=client -destination=secrets_manager_mockgen.go +// + +// Package client is a generated GoMock package. +package client + +import ( + context "context" + reflect "reflect" + + secretsmanager "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + gomock "go.uber.org/mock/gomock" +) + +// MockSecretsManager is a mock of SecretsManager interface. +type MockSecretsManager struct { + ctrl *gomock.Controller + recorder *MockSecretsManagerMockRecorder + isgomock struct{} +} + +// MockSecretsManagerMockRecorder is the mock recorder for MockSecretsManager. +type MockSecretsManagerMockRecorder struct { + mock *MockSecretsManager +} + +// NewMockSecretsManager creates a new mock instance. +func NewMockSecretsManager(ctrl *gomock.Controller) *MockSecretsManager { + mock := &MockSecretsManager{ctrl: ctrl} + mock.recorder = &MockSecretsManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSecretsManager) EXPECT() *MockSecretsManagerMockRecorder { + return m.recorder +} + +// GetSecretValue mocks base method. +func (m *MockSecretsManager) 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 *MockSecretsManagerMockRecorder) 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((*MockSecretsManager)(nil).GetSecretValue), varargs...) +} diff --git a/aws/logs_monitoring_go/internal/config/secretsmanager_test.go b/aws/logs_monitoring_go/internal/client/secrets_manager_test.go similarity index 82% rename from aws/logs_monitoring_go/internal/config/secretsmanager_test.go rename to aws/logs_monitoring_go/internal/client/secrets_manager_test.go index 483d4fdb0..7f64d4676 100644 --- a/aws/logs_monitoring_go/internal/config/secretsmanager_test.go +++ b/aws/logs_monitoring_go/internal/client/secrets_manager_test.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2026-Present Datadog, Inc. -package config +package client import ( "errors" @@ -16,15 +16,15 @@ import ( "go.uber.org/mock/gomock" ) -func TestResolveFromSecretsManager(t *testing.T) { +func TestFetchSecret(t *testing.T) { tests := map[string]struct { - mockSetup func(m *MockSecretsManagerAPIClient) + mockSetup func(m *MockSecretsManager) arn string wantKey string wantErr bool }{ "success": { - mockSetup: func(m *MockSecretsManagerAPIClient) { + mockSetup: func(m *MockSecretsManager) { m.EXPECT(). GetSecretValue(gomock.Any(), gomock.Any()). Return(&secretsmanager.GetSecretValueOutput{ @@ -35,7 +35,7 @@ func TestResolveFromSecretsManager(t *testing.T) { wantKey: "abcdef1234567890abcdef1234567890", }, "whitespace_trimmed": { - mockSetup: func(m *MockSecretsManagerAPIClient) { + mockSetup: func(m *MockSecretsManager) { m.EXPECT(). GetSecretValue(gomock.Any(), gomock.Any()). Return(&secretsmanager.GetSecretValueOutput{ @@ -46,7 +46,7 @@ func TestResolveFromSecretsManager(t *testing.T) { wantKey: "abcdef1234567890abcdef1234567890", }, "aws_error": { - mockSetup: func(m *MockSecretsManagerAPIClient) { + mockSetup: func(m *MockSecretsManager) { m.EXPECT(). GetSecretValue(gomock.Any(), gomock.Any()). Return(nil, errors.New("AccessDeniedException: access denied")) @@ -55,7 +55,7 @@ func TestResolveFromSecretsManager(t *testing.T) { wantErr: true, }, "nil_secret_string": { - mockSetup: func(m *MockSecretsManagerAPIClient) { + mockSetup: func(m *MockSecretsManager) { m.EXPECT(). GetSecretValue(gomock.Any(), gomock.Any()). Return(&secretsmanager.GetSecretValueOutput{ @@ -70,10 +70,10 @@ func TestResolveFromSecretsManager(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { ctrl := gomock.NewController(t) - mock := NewMockSecretsManagerAPIClient(ctrl) + mock := NewMockSecretsManager(ctrl) tc.mockSetup(mock) - got, err := fetchSecret(t.Context(), mock, tc.arn) + got, err := FetchSecret(t.Context(), mock, tc.arn) if tc.wantErr { require.Error(t, err) return diff --git a/aws/logs_monitoring_go/internal/client/ssm.go b/aws/logs_monitoring_go/internal/client/ssm.go new file mode 100644 index 000000000..8d33dd0cc --- /dev/null +++ b/aws/logs_monitoring_go/internal/client/ssm.go @@ -0,0 +1,71 @@ +// 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 client + +//go:generate go tool mockgen -source=ssm.go -package=client -destination=ssm_mockgen.go + +import ( + "context" + "fmt" + "log/slog" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ssm" +) + +type SSM interface { + GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) +} + +func NewSSM(ctx context.Context, useFIPS bool) (SSM, error) { + cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(timeout))) + if err != nil { + return nil, err + } + + resolver := ssm.NewDefaultEndpointResolverV2() + params := ssm.EndpointParameters{ + Region: aws.String(cfg.Region), + UseFIPS: aws.Bool(useFIPS), + } + + endpoint, err := resolver.ResolveEndpoint(ctx, params) + if err != nil && useFIPS { + slog.Warn("FIPS endpoint not available, falling back to standard endpoint", slog.String("service", "ssm"), slog.String("region", cfg.Region)) + params.UseFIPS = aws.Bool(false) + endpoint, err = resolver.ResolveEndpoint(ctx, params) + } + if err != nil { + return nil, fmt.Errorf("resolve endpoint: %w", err) + } + + return ssm.NewFromConfig(cfg, func(o *ssm.Options) { + o.BaseEndpoint = aws.String(endpoint.URI.String()) + }), nil +} + +func FetchSSMParameter(ctx context.Context, ssmClient SSM, name string) (string, error) { + result, err := ssmClient.GetParameter(ctx, &ssm.GetParameterInput{ + Name: aws.String(name), + WithDecryption: aws.Bool(true), + }) + if err != nil { + return "", fmt.Errorf("fetching parameter `%s`: %w", name, err) + } + + if result.Parameter == nil { + return "", fmt.Errorf("parameter `%s` has no value", name) + } + + if result.Parameter.Value == nil { + return "", fmt.Errorf("parameter `%s` has no string value", name) + } + + return strings.TrimSpace(*result.Parameter.Value), nil +} diff --git a/aws/logs_monitoring_go/internal/client/ssm_mockgen.go b/aws/logs_monitoring_go/internal/client/ssm_mockgen.go new file mode 100644 index 000000000..61972fe07 --- /dev/null +++ b/aws/logs_monitoring_go/internal/client/ssm_mockgen.go @@ -0,0 +1,62 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ssm.go +// +// Generated by this command: +// +// mockgen -source=ssm.go -package=client -destination=ssm_mockgen.go +// + +// Package client is a generated GoMock package. +package client + +import ( + context "context" + reflect "reflect" + + ssm "github.com/aws/aws-sdk-go-v2/service/ssm" + gomock "go.uber.org/mock/gomock" +) + +// MockSSM is a mock of SSM interface. +type MockSSM struct { + ctrl *gomock.Controller + recorder *MockSSMMockRecorder + isgomock struct{} +} + +// MockSSMMockRecorder is the mock recorder for MockSSM. +type MockSSMMockRecorder struct { + mock *MockSSM +} + +// NewMockSSM creates a new mock instance. +func NewMockSSM(ctrl *gomock.Controller) *MockSSM { + mock := &MockSSM{ctrl: ctrl} + mock.recorder = &MockSSMMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSSM) EXPECT() *MockSSMMockRecorder { + return m.recorder +} + +// GetParameter mocks base method. +func (m *MockSSM) GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range optFns { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetParameter", varargs...) + ret0, _ := ret[0].(*ssm.GetParameterOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetParameter indicates an expected call of GetParameter. +func (mr *MockSSMMockRecorder) GetParameter(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, "GetParameter", reflect.TypeOf((*MockSSM)(nil).GetParameter), varargs...) +} diff --git a/aws/logs_monitoring_go/internal/config/ssm_test.go b/aws/logs_monitoring_go/internal/client/ssm_test.go similarity index 84% rename from aws/logs_monitoring_go/internal/config/ssm_test.go rename to aws/logs_monitoring_go/internal/client/ssm_test.go index 97991b701..a8d436b8c 100644 --- a/aws/logs_monitoring_go/internal/config/ssm_test.go +++ b/aws/logs_monitoring_go/internal/client/ssm_test.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2026-Present Datadog, Inc. -package config +package client import ( "errors" @@ -17,15 +17,15 @@ import ( "go.uber.org/mock/gomock" ) -func TestResolveFromSSM(t *testing.T) { +func TestFetchSSMParameter(t *testing.T) { tests := map[string]struct { - mockSetup func(m *MockSSMAPIClient) + mockSetup func(m *MockSSM) name string wantKey string wantErr bool }{ "success": { - mockSetup: func(m *MockSSMAPIClient) { + mockSetup: func(m *MockSSM) { m.EXPECT(). GetParameter(gomock.Any(), gomock.Any()). Return(&ssm.GetParameterOutput{ @@ -38,7 +38,7 @@ func TestResolveFromSSM(t *testing.T) { wantKey: "abcdef1234567890abcdef1234567890", }, "whitespace_trimmed": { - mockSetup: func(m *MockSSMAPIClient) { + mockSetup: func(m *MockSSM) { m.EXPECT(). GetParameter(gomock.Any(), gomock.Any()). Return(&ssm.GetParameterOutput{ @@ -51,7 +51,7 @@ func TestResolveFromSSM(t *testing.T) { wantKey: "abcdef1234567890abcdef1234567890", }, "aws_error": { - mockSetup: func(m *MockSSMAPIClient) { + mockSetup: func(m *MockSSM) { m.EXPECT(). GetParameter(gomock.Any(), gomock.Any()). Return(nil, errors.New("ParameterNotFound: parameter not found")) @@ -60,7 +60,7 @@ func TestResolveFromSSM(t *testing.T) { wantErr: true, }, "nil_parameter": { - mockSetup: func(m *MockSSMAPIClient) { + mockSetup: func(m *MockSSM) { m.EXPECT(). GetParameter(gomock.Any(), gomock.Any()). Return(&ssm.GetParameterOutput{ @@ -71,7 +71,7 @@ func TestResolveFromSSM(t *testing.T) { wantErr: true, }, "nil_value": { - mockSetup: func(m *MockSSMAPIClient) { + mockSetup: func(m *MockSSM) { m.EXPECT(). GetParameter(gomock.Any(), gomock.Any()). Return(&ssm.GetParameterOutput{ @@ -88,10 +88,10 @@ func TestResolveFromSSM(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { ctrl := gomock.NewController(t) - mock := NewMockSSMAPIClient(ctrl) + mock := NewMockSSM(ctrl) tc.mockSetup(mock) - got, err := fetchSSMParameter(t.Context(), mock, tc.name) + got, err := FetchSSMParameter(t.Context(), mock, tc.name) if tc.wantErr { require.Error(t, err) return diff --git a/aws/logs_monitoring_go/internal/config/kms.go b/aws/logs_monitoring_go/internal/config/kms.go index 896822dab..f019dd535 100644 --- a/aws/logs_monitoring_go/internal/config/kms.go +++ b/aws/logs_monitoring_go/internal/config/kms.go @@ -5,76 +5,16 @@ package config -//go:generate go tool mockgen -source=kms.go -package=config -destination=kms_mockgen.go - import ( "context" - "encoding/base64" - "fmt" - "log/slog" - "strings" - "github.com/aws/aws-sdk-go-v2/aws" - awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" - awsconfig "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/client" ) -type KMSAPIClient interface { - Decrypt(ctx context.Context, params *kms.DecryptInput, optFns ...func(*kms.Options)) (*kms.DecryptOutput, error) -} - func (c *Config) resolveAPIKeyFromKMS(ctx context.Context, ciphertext string) (string, error) { - client, err := c.createKMSAPIClient(ctx) + kmsClient, err := client.NewKMS(ctx, c.UseFIPS) if err != nil { return "", err } - return decryptKMSCiphertext(ctx, client, ciphertext) -} - -func (c *Config) createKMSAPIClient(ctx context.Context) (KMSAPIClient, error) { - cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(httpClientTimeout))) - if err != nil { - return nil, err - } - - resolver := kms.NewDefaultEndpointResolverV2() - params := kms.EndpointParameters{ - Region: aws.String(cfg.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", "kms"), slog.String("region", cfg.Region)) - params.UseFIPS = aws.Bool(false) - endpoint, err = resolver.ResolveEndpoint(ctx, params) - } - if err != nil { - return nil, fmt.Errorf("resolve endpoint: %w", err) - } - - return kms.NewFromConfig(cfg, func(o *kms.Options) { - o.BaseEndpoint = aws.String(endpoint.URI.String()) - }), nil -} - -func decryptKMSCiphertext(ctx context.Context, client KMSAPIClient, ciphertext string) (string, error) { - decoded, err := base64.StdEncoding.DecodeString(ciphertext) - if err != nil { - return "", fmt.Errorf("base64-decoding ciphertext: %w", err) - } - - result, err := client.Decrypt(ctx, &kms.DecryptInput{ - CiphertextBlob: decoded, - }) - if err != nil { - return "", fmt.Errorf("decrypting KMS ciphertext: %w", err) - } - - if result.Plaintext == nil { - return "", fmt.Errorf("KMS decryption returned no plaintext") - } - - return strings.TrimSpace(string(result.Plaintext)), nil + return client.DecryptKMSCiphertext(ctx, kmsClient, ciphertext) } diff --git a/aws/logs_monitoring_go/internal/config/kms_mockgen.go b/aws/logs_monitoring_go/internal/config/kms_mockgen.go deleted file mode 100644 index d0ecb76b4..000000000 --- a/aws/logs_monitoring_go/internal/config/kms_mockgen.go +++ /dev/null @@ -1,62 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: kms.go -// -// Generated by this command: -// -// mockgen -source=kms.go -package=config -destination=kms_mockgen.go -// - -// Package config is a generated GoMock package. -package config - -import ( - context "context" - reflect "reflect" - - kms "github.com/aws/aws-sdk-go-v2/service/kms" - gomock "go.uber.org/mock/gomock" -) - -// MockKMSAPIClient is a mock of KMSAPIClient interface. -type MockKMSAPIClient struct { - ctrl *gomock.Controller - recorder *MockKMSAPIClientMockRecorder - isgomock struct{} -} - -// MockKMSAPIClientMockRecorder is the mock recorder for MockKMSAPIClient. -type MockKMSAPIClientMockRecorder struct { - mock *MockKMSAPIClient -} - -// NewMockKMSAPIClient creates a new mock instance. -func NewMockKMSAPIClient(ctrl *gomock.Controller) *MockKMSAPIClient { - mock := &MockKMSAPIClient{ctrl: ctrl} - mock.recorder = &MockKMSAPIClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockKMSAPIClient) EXPECT() *MockKMSAPIClientMockRecorder { - return m.recorder -} - -// Decrypt mocks base method. -func (m *MockKMSAPIClient) Decrypt(ctx context.Context, params *kms.DecryptInput, optFns ...func(*kms.Options)) (*kms.DecryptOutput, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, params} - for _, a := range optFns { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Decrypt", varargs...) - ret0, _ := ret[0].(*kms.DecryptOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Decrypt indicates an expected call of Decrypt. -func (mr *MockKMSAPIClientMockRecorder) Decrypt(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, "Decrypt", reflect.TypeOf((*MockKMSAPIClient)(nil).Decrypt), varargs...) -} diff --git a/aws/logs_monitoring_go/internal/config/secretsmanager.go b/aws/logs_monitoring_go/internal/config/secretsmanager.go index db4112bbe..c9d96ee67 100644 --- a/aws/logs_monitoring_go/internal/config/secretsmanager.go +++ b/aws/logs_monitoring_go/internal/config/secretsmanager.go @@ -5,70 +5,16 @@ 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" - awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" - awsconfig "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/client" ) -type SecretsManagerAPIClient interface { - GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) -} - func (c *Config) resolveAPIKeyFromSecretsManager(ctx context.Context, arn string) (string, error) { - client, err := c.createSecretsManagerAPIClient(ctx) + smClient, err := client.NewSecretsManager(ctx, c.UseFIPS) if err != nil { return "", err } - return fetchSecret(ctx, client, arn) -} - -func (c *Config) createSecretsManagerAPIClient(ctx context.Context) (SecretsManagerAPIClient, error) { - cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(httpClientTimeout))) - if err != nil { - return nil, err - } - - resolver := secretsmanager.NewDefaultEndpointResolverV2() - params := secretsmanager.EndpointParameters{ - Region: aws.String(cfg.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", cfg.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(cfg, func(o *secretsmanager.Options) { - o.BaseEndpoint = aws.String(endpoint.URI.String()) - }), nil -} - -func fetchSecret(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 + return client.FetchSecret(ctx, smClient, arn) } diff --git a/aws/logs_monitoring_go/internal/config/secretsmanager_mockgen.go b/aws/logs_monitoring_go/internal/config/secretsmanager_mockgen.go deleted file mode 100644 index e5f05e3ee..000000000 --- a/aws/logs_monitoring_go/internal/config/secretsmanager_mockgen.go +++ /dev/null @@ -1,62 +0,0 @@ -// 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/ssm.go b/aws/logs_monitoring_go/internal/config/ssm.go index ed273ebe5..914e8bb1f 100644 --- a/aws/logs_monitoring_go/internal/config/ssm.go +++ b/aws/logs_monitoring_go/internal/config/ssm.go @@ -5,75 +5,16 @@ package config -//go:generate go tool mockgen -source=ssm.go -package=config -destination=ssm_mockgen.go - import ( "context" - "fmt" - "log/slog" - "strings" - "github.com/aws/aws-sdk-go-v2/aws" - awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" - awsconfig "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/client" ) -type SSMAPIClient interface { - GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) -} - func (c *Config) resolveAPIKeyFromSSM(ctx context.Context, name string) (string, error) { - client, err := c.createSSMAPIClient(ctx) + ssmClient, err := client.NewSSM(ctx, c.UseFIPS) if err != nil { return "", err } - return fetchSSMParameter(ctx, client, name) -} - -func (c *Config) createSSMAPIClient(ctx context.Context) (SSMAPIClient, error) { - cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(httpClientTimeout))) - if err != nil { - return nil, err - } - - resolver := ssm.NewDefaultEndpointResolverV2() - params := ssm.EndpointParameters{ - Region: aws.String(cfg.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", "ssm"), slog.String("region", cfg.Region)) - params.UseFIPS = aws.Bool(false) - endpoint, err = resolver.ResolveEndpoint(ctx, params) - } - if err != nil { - return nil, fmt.Errorf("resolve endpoint: %w", err) - } - - return ssm.NewFromConfig(cfg, func(o *ssm.Options) { - o.BaseEndpoint = aws.String(endpoint.URI.String()) - }), nil -} - -func fetchSSMParameter(ctx context.Context, client SSMAPIClient, name string) (string, error) { - result, err := client.GetParameter(ctx, &ssm.GetParameterInput{ - Name: aws.String(name), - WithDecryption: aws.Bool(true), - }) - if err != nil { - return "", fmt.Errorf("fetching parameter `%s`: %w", name, err) - } - - if result.Parameter == nil { - return "", fmt.Errorf("parameter `%s` has no value", name) - } - - if result.Parameter.Value == nil { - return "", fmt.Errorf("parameter `%s` has no string value", name) - } - - return strings.TrimSpace(*result.Parameter.Value), nil + return client.FetchSSMParameter(ctx, ssmClient, name) } diff --git a/aws/logs_monitoring_go/internal/config/ssm_mockgen.go b/aws/logs_monitoring_go/internal/config/ssm_mockgen.go deleted file mode 100644 index 52183f0bc..000000000 --- a/aws/logs_monitoring_go/internal/config/ssm_mockgen.go +++ /dev/null @@ -1,62 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ssm.go -// -// Generated by this command: -// -// mockgen -source=ssm.go -package=config -destination=ssm_mockgen.go -// - -// Package config is a generated GoMock package. -package config - -import ( - context "context" - reflect "reflect" - - ssm "github.com/aws/aws-sdk-go-v2/service/ssm" - gomock "go.uber.org/mock/gomock" -) - -// MockSSMAPIClient is a mock of SSMAPIClient interface. -type MockSSMAPIClient struct { - ctrl *gomock.Controller - recorder *MockSSMAPIClientMockRecorder - isgomock struct{} -} - -// MockSSMAPIClientMockRecorder is the mock recorder for MockSSMAPIClient. -type MockSSMAPIClientMockRecorder struct { - mock *MockSSMAPIClient -} - -// NewMockSSMAPIClient creates a new mock instance. -func NewMockSSMAPIClient(ctrl *gomock.Controller) *MockSSMAPIClient { - mock := &MockSSMAPIClient{ctrl: ctrl} - mock.recorder = &MockSSMAPIClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockSSMAPIClient) EXPECT() *MockSSMAPIClientMockRecorder { - return m.recorder -} - -// GetParameter mocks base method. -func (m *MockSSMAPIClient) GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, params} - for _, a := range optFns { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetParameter", varargs...) - ret0, _ := ret[0].(*ssm.GetParameterOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetParameter indicates an expected call of GetParameter. -func (mr *MockSSMAPIClientMockRecorder) GetParameter(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, "GetParameter", reflect.TypeOf((*MockSSMAPIClient)(nil).GetParameter), varargs...) -} diff --git a/aws/logs_monitoring_go/internal/handling/s3.go b/aws/logs_monitoring_go/internal/handling/s3.go index 21b8468bf..3708c2563 100644 --- a/aws/logs_monitoring_go/internal/handling/s3.go +++ b/aws/logs_monitoring_go/internal/handling/s3.go @@ -15,6 +15,7 @@ import ( "slices" "strings" + "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/client" "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/concurrent" "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/config" "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/model" @@ -44,7 +45,7 @@ func (h *S3Handler) Handle(ctx context.Context, event json.RawMessage, out chan< return fmt.Errorf("unmarshal: %w", err) } - client, err := getS3APIClient(ctx, h.cfg.UseFIPS) + s3Client, err := client.GetS3(ctx, h.cfg.UseFIPS) if err != nil { return fmt.Errorf("get S3 client: %w", err) } @@ -55,15 +56,15 @@ func (h *S3Handler) Handle(ctx context.Context, event json.RawMessage, out chan< } for _, eventRecord := range s3Event.Records { - if err := h.processRecord(ctx, client, out, eventRecord, lambdaOrigin); err != nil { + if err := h.processRecord(ctx, s3Client, out, eventRecord, lambdaOrigin); err != nil { return err } } return nil } -func (h S3Handler) processRecord(ctx context.Context, client S3APIClient, out chan<- model.LogEntry, eventRecord events.S3EventRecord, lambdaOrigin model.LambdaOrigin) error { - body, err := getS3Object(ctx, client, eventRecord.S3.Bucket.Name, eventRecord.S3.Object.URLDecodedKey) +func (h S3Handler) processRecord(ctx context.Context, s3Client client.S3, out chan<- model.LogEntry, eventRecord events.S3EventRecord, lambdaOrigin model.LambdaOrigin) error { + body, err := client.GetS3Object(ctx, s3Client, eventRecord.S3.Bucket.Name, eventRecord.S3.Object.URLDecodedKey) if err != nil { return err } diff --git a/aws/logs_monitoring_go/internal/handling/s3_client_mockgen.go b/aws/logs_monitoring_go/internal/handling/s3_client_mockgen.go deleted file mode 100644 index e6c046565..000000000 --- a/aws/logs_monitoring_go/internal/handling/s3_client_mockgen.go +++ /dev/null @@ -1,62 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: s3_client.go -// -// Generated by this command: -// -// mockgen -source=s3_client.go -package=handling -destination=s3_client_mockgen.go -// - -// Package handling is a generated GoMock package. -package handling - -import ( - context "context" - reflect "reflect" - - s3 "github.com/aws/aws-sdk-go-v2/service/s3" - gomock "go.uber.org/mock/gomock" -) - -// MockS3APIClient is a mock of S3APIClient interface. -type MockS3APIClient struct { - ctrl *gomock.Controller - recorder *MockS3APIClientMockRecorder - isgomock struct{} -} - -// MockS3APIClientMockRecorder is the mock recorder for MockS3APIClient. -type MockS3APIClientMockRecorder struct { - mock *MockS3APIClient -} - -// NewMockS3APIClient creates a new mock instance. -func NewMockS3APIClient(ctrl *gomock.Controller) *MockS3APIClient { - mock := &MockS3APIClient{ctrl: ctrl} - mock.recorder = &MockS3APIClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockS3APIClient) EXPECT() *MockS3APIClientMockRecorder { - return m.recorder -} - -// GetObject mocks base method. -func (m *MockS3APIClient) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, params} - for _, a := range optFns { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetObject", varargs...) - ret0, _ := ret[0].(*s3.GetObjectOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetObject indicates an expected call of GetObject. -func (mr *MockS3APIClientMockRecorder) GetObject(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, "GetObject", reflect.TypeOf((*MockS3APIClient)(nil).GetObject), varargs...) -} diff --git a/aws/logs_monitoring_go/internal/handling/s3_test.go b/aws/logs_monitoring_go/internal/handling/s3_test.go index 76ccb3072..d3e4bbe56 100644 --- a/aws/logs_monitoring_go/internal/handling/s3_test.go +++ b/aws/logs_monitoring_go/internal/handling/s3_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/client" "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/config" "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/model" "github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/testutil" @@ -59,14 +60,14 @@ func TestProcessS3Record(t *testing.T) { t.Parallel() tests := map[string]struct { - mockSetup func(m *MockS3APIClient) + mockSetup func(m *client.MockS3) cfg *config.Config eventRecord events.S3EventRecord want []model.LogEntry wantErr bool }{ "single line": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ Body: io.NopCloser(strings.NewReader("line1")), @@ -77,7 +78,7 @@ func TestProcessS3Record(t *testing.T) { want: []model.LogEntry{wantS3Entry("line1", "s3", "s3", nil)}, }, "multiple lines": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ Body: io.NopCloser(strings.NewReader("line1\nline2\nline3")), @@ -92,7 +93,7 @@ func TestProcessS3Record(t *testing.T) { }, }, "empty file": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ Body: io.NopCloser(strings.NewReader("")), @@ -103,7 +104,7 @@ func TestProcessS3Record(t *testing.T) { want: nil, }, "s3 error": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(nil, errors.New("access denied")) }, @@ -112,7 +113,7 @@ func TestProcessS3Record(t *testing.T) { wantErr: true, }, "ddtags extraction": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ Body: io.NopCloser(strings.NewReader(`{"ddtags":"env:prod,service:myapp","msg":"hello"}`)), @@ -123,7 +124,7 @@ func TestProcessS3Record(t *testing.T) { want: []model.LogEntry{wantS3Entry(`{"msg":"hello"}`, "s3", "myapp", model.Tags{"env:prod"})}, }, "invalid utf8 stripped": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ Body: io.NopCloser(strings.NewReader("hello\x80world")), @@ -134,7 +135,7 @@ func TestProcessS3Record(t *testing.T) { want: []model.LogEntry{wantS3Entry("helloworld", "s3", "s3", nil)}, }, "multiline groups continuation lines": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ Body: io.NopCloser(strings.NewReader("2024-01-15 ERROR NullPointer\n at com.foo.Bar\n2024-01-15 INFO started")), @@ -148,7 +149,7 @@ func TestProcessS3Record(t *testing.T) { }, }, "multiline flushes at eof": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ Body: io.NopCloser(strings.NewReader("2024-01-15 ERROR\n stacktrace")), @@ -159,7 +160,7 @@ func TestProcessS3Record(t *testing.T) { want: []model.LogEntry{wantS3Entry("2024-01-15 ERROR\n stacktrace", "s3", "s3", nil)}, }, "custom tags passed through": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ Body: io.NopCloser(strings.NewReader("line1")), @@ -170,7 +171,7 @@ func TestProcessS3Record(t *testing.T) { want: []model.LogEntry{wantS3Entry("line1", "s3", "s3", model.Tags{"env:prod", "team:aws"})}, }, "cloudtrail with ec2 host": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { data := testutil.MustGzipJSON(t, map[string]any{ "Records": []any{ map[string]any{ @@ -196,7 +197,7 @@ func TestProcessS3Record(t *testing.T) { }, }, "cloudtrail without ec2 host": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { data := testutil.MustGzipJSON(t, map[string]any{ "Records": []any{ map[string]any{ @@ -222,7 +223,7 @@ func TestProcessS3Record(t *testing.T) { }, }, "vpc flow logs skips header line": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ Body: io.NopCloser(strings.NewReader("version account-id interface-id srcaddr dstaddr srcport dstport protocol\n2 123456789012 eni-abc123 10.0.0.1 10.0.0.2 443 49152 6\n2 123456789012 eni-abc123 10.0.0.2 10.0.0.1 49152 443 6")), @@ -236,7 +237,7 @@ func TestProcessS3Record(t *testing.T) { }, }, "waf single line": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { data := testutil.MustGzip(t, []byte(`{"httpRequest":{"headers":[{"name":"Host","value":"example.com"}]}}`)) m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ @@ -248,7 +249,7 @@ func TestProcessS3Record(t *testing.T) { want: []model.LogEntry{wantWAFEntry(`{"httpRequest":{"headers":{"Host":"example.com"}}}`)}, }, "waf multiple lines": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { lines := `{"httpRequest":{"headers":[{"name":"h1","value":"v1"}]}}` + "\n" + `{"httpRequest":{"headers":[{"name":"h2","value":"v2"}]}}` data := testutil.MustGzip(t, []byte(lines)) @@ -265,7 +266,7 @@ func TestProcessS3Record(t *testing.T) { }, }, "waf does not apply multiline regex": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { lines := `{"action":"ALLOW"}` + "\n" + `{"action":"BLOCK"}` data := testutil.MustGzip(t, []byte(lines)) m.EXPECT().GetObject(gomock.Any(), gomock.Any()). @@ -281,7 +282,7 @@ func TestProcessS3Record(t *testing.T) { }, }, "waf non-gzipped": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { m.EXPECT().GetObject(gomock.Any(), gomock.Any()). Return(&s3.GetObjectOutput{ Body: io.NopCloser(strings.NewReader(`{"httpRequest":{"headers":[{"name":"Host","value":"example.com"}]}}`)), @@ -292,7 +293,7 @@ func TestProcessS3Record(t *testing.T) { want: []model.LogEntry{wantWAFEntry(`{"httpRequest":{"headers":{"Host":"example.com"}}}`)}, }, "waf exclude at match": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { lines := `{"action":"ALLOW","httpRequest":{}}` + "\n" + `{"action":"BLOCK","httpRequest":{}}` data := testutil.MustGzip(t, []byte(lines)) m.EXPECT().GetObject(gomock.Any(), gomock.Any()). @@ -305,7 +306,7 @@ func TestProcessS3Record(t *testing.T) { want: []model.LogEntry{wantWAFEntry(`{"action":"ALLOW","httpRequest":{}}`)}, }, "waf include at match": { - mockSetup: func(m *MockS3APIClient) { + mockSetup: func(m *client.MockS3) { lines := `{"action":"ALLOW","httpRequest":{}}` + "\n" + `{"action":"BLOCK","httpRequest":{}}` data := testutil.MustGzip(t, []byte(lines)) m.EXPECT().GetObject(gomock.Any(), gomock.Any()). @@ -324,7 +325,7 @@ func TestProcessS3Record(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mock := NewMockS3APIClient(ctrl) + mock := client.NewMockS3(ctrl) tc.mockSetup(mock) out := make(chan model.LogEntry, len(tc.want))