Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: e2e

on:
push:
branches: [main]
paths:
- 'src/**'
- 'e2e/**'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/e2e.yml'
pull_request:
paths:
- 'src/**'
- 'e2e/**'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/e2e.yml'
workflow_dispatch:

# One in-flight e2e run per ref; cancel older runs so ephemeral resources don't pile up.
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
id-token: write

jobs:
check-lambda-changes:
name: Check lambda changes
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.filter.outputs.lambda }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
id: filter
with:
filters: |
lambda:
- 'src/**'
- 'e2e/**'
- '.github/workflows/e2e.yml'

e2e-lambda:
name: Lambda e2e (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
needs: [check-lambda-changes]
strategy:
matrix:
node-version: [20]
permissions:
contents: read
id-token: write
env:
FORCE_COLOR: 1
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Set up Node ${{ matrix.node-version }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ matrix.node-version }}

- name: Set up Yarn
run: corepack enable && corepack prepare yarn@4.10.3 --activate

- name: Install plugin dependencies
run: yarn install --immutable

# OIDC federation: GitHub -> AWS. The role is assumable only from this repo's
# workflows and scoped to the e2e sandbox account.
- name: AWS auth (OIDC)
if: needs.check-lambda-changes.outputs.should_run == 'true'
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN_E2E }}
aws-region: ${{ vars.AWS_REGION_E2E || 'ap-northeast-3' }}

- name: Install e2e dependencies
run: npm install
working-directory: e2e

# Short-lived Datadog API + App keys via OIDC federation (dd-sts), governed by
# the serverless-plugin-datadog-e2e policy. No static Datadog keys in this repo.
- name: Get Datadog credentials (dd-sts)
id: dd-sts
if: needs.check-lambda-changes.outputs.should_run == 'true'
uses: DataDog/dd-sts-action@2e8187910199bd93129520183c093e19aa585c75 # v1.0.0
with:
policy: serverless-plugin-datadog-e2e

- name: Run e2e suite
run: npm test
working-directory: e2e
env:
DATADOG_API_KEY: ${{ steps.dd-sts.outputs.api_key }}
DATADOG_APP_KEY: ${{ steps.dd-sts.outputs.app_key }}
DATADOG_SITE: ${{ vars.DD_SITE_E2E || 'datadoghq.com' }}
AWS_REGION: ${{ vars.AWS_REGION_E2E || 'ap-northeast-3' }}
# Skipped only when nothing relevant changed. When relevant files change the
# suite runs for real and the dd-sts / AWS OIDC steps must succeed -- an
# auth/federation failure fails the job loudly rather than skipping green.
SKIP_LAMBDA_TESTS: ${{ needs.check-lambda-changes.outputs.should_run != 'true' }}
17 changes: 17 additions & 0 deletions e2e/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copy to e2e/.env.local (gitignored) for local runs. Real shell env vars take
# precedence, so this file is purely a convenience.

# Datadog API + APP keys. The API key is wired into the Lambda extension AND used
# to authenticate the API client; the APP key is needed to poll spans/logs.
DATADOG_API_KEY=
DATADOG_APP_KEY=

# Datadog site the org lives in (datadoghq.com, datadoghq.eu, us3.datadoghq.com, ...).
DATADOG_SITE=datadoghq.com

# AWS region to deploy the ephemeral Lambda into. Must have pinned layers in
# ../src/layers.json (ap-northeast-3 is the default).
AWS_REGION=ap-northeast-3

# Set to skip the suite entirely.
# SKIP_LAMBDA_TESTS=true
7 changes: 7 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
.env.local
fixtures/**/node_modules
fixtures/**/.serverless
fixtures/**/package-lock.json
serverless-plugin-datadog-*.tgz
*.log
101 changes: 101 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# serverless-plugin-datadog e2e suite

End-to-end coverage for the AWS Lambda instrumentation this plugin performs. It
deploys a real, ephemeral Lambda with the plugin enabled, verifies the deployed
config and the telemetry it produces in Datadog, proves re-deploy is idempotent,
then tears the stack down and verifies a clean end-state.

Conforms to the shared contract in `serverless-ci/e2e/spec.md`; mirrors the
`datadog-ci` reference suite (`e2e/cloud-run.test.ts` + `e2e/helpers/*`).

## What it does

```
sls deploy (APPLY: provision + instrument) -> verify CONFIG
-> aws lambda invoke (trigger) -> verify TELEMETRY (traces + logs)
-> sls deploy again -> assert IDEMPOTENT (no diff/dup)
-> sls remove (REMOVE) -> verify CLEAN (function gone)
-> teardown (always, even on failure)
```

For this tool the plugin runs as part of `sls deploy`, so provisioning the
uninstrumented workload and APPLY are the same step. REMOVE deletes the whole
CloudFormation stack, so the clean end-state is the function (and all its DD
config) being absent -- asserted explicitly.

**Config verified** (`helpers/lambda-verifier.ts`): the pinned Datadog Node layer
+ extension layer (versions read from `../src/layers.json`, so drift blames the
plugin), the redirected handler with the original preserved in `DD_LAMBDA_HANDLER`,
the required `DD_*` env vars, and the `service` / `env` / `version` / `dd_sls_plugin`
tags. Identity (run-id service name, env, version) is asserted -- not mere presence.

**Telemetry verified** (`helpers/lambda-telemetry-checker.ts`): spans and logs are
polled (15s × 20) filtered by the unique service name, and the matched records must
carry the full identity (service + env + version), not just exist.

## Resource hygiene

Every run uses a unique name `one-e2e-slsplugin-lambda-<runid>` and stamps a
`one_e2e_created:<unix-ts>` tag at creation (`helpers/naming.ts`). The shared
cross-repo sweeper ages out anything older than the grace window. In-test teardown
runs in `afterAll` regardless of outcome.

## Prerequisites

- **Node 20** and **npm** (the suite is a standalone npm project, isolated from the
plugin's Yarn Berry setup).
- The plugin is built and the fixture is installed automatically by `pretest`
(`npm test` runs `yarn build` at the repo root, then `npm install` in the fixture).
- **AWS auth** with permission to deploy Lambda / CloudFormation in the target
account. Locally, wrap the run with `aws-vault`:
```
aws-vault exec sso-serverless-sandbox-account-admin -- npm test
```
In CI, credentials come from GitHub→AWS OIDC (no static keys).
- **Datadog keys**: `DATADOG_API_KEY` (wired into the extension and used for the API
client) and `DATADOG_APP_KEY` (used to poll spans/logs).

## Run locally

```
cd e2e
npm install

# aws-vault provides AWS creds; dd-auth mints short-lived keys for the org -- no pasted keys.
aws-vault exec sso-serverless-sandbox-account-admin -- \
dd-auth --domain app.datadoghq.com -- bash -c '
export DATADOG_API_KEY="$DD_API_KEY" DATADOG_APP_KEY="$DD_APP_KEY"
npm test
'
```

`dd-auth` injects `$DD_API_KEY` / `$DD_APP_KEY` into the wrapped subprocess; the
suite reads them as `DATADOG_API_KEY` / `DATADOG_APP_KEY`. AWS credentials still come
from `aws-vault`. Set `SKIP_LAMBDA_TESTS=true` to skip the suite.

## Configuration

| Env var | Required | Default | Purpose |
| ------------------ | -------- | ---------------- | ---------------------------------------------- |
| `DATADOG_API_KEY` | yes | -- | Wired into the extension + API-client auth |
| `DATADOG_APP_KEY` | yes | -- | API-client auth for span/log polling |
| `DATADOG_SITE` | no | `datadoghq.com` | Datadog site |
| `AWS_REGION` | no | `ap-northeast-3` | Deploy region (must be pinned in `layers.json`)|
| `SKIP_LAMBDA_TESTS`| no | -- | `true` skips the suite |

(AWS credentials come from the ambient AWS env / `aws-vault` / OIDC.)

## CI

`.github/workflows/e2e.yml` runs the suite behind a `dorny/paths-filter` gate
(`src/**`, `e2e/**`, the workflow file) and the `SKIP_LAMBDA_TESTS` flag, with
GitHub→AWS OIDC (`aws-actions/configure-aws-credentials`). Required repo settings:

- Datadog auth (dd-sts): short-lived API + App keys minted at runtime via
[`DataDog/dd-sts-action`](https://github.com/DataDog/dd-sts-action) under the
`serverless-plugin-datadog-e2e` policy -- no static Datadog keys in this repo
- Variables: `AWS_ROLE_ARN_E2E` (the OIDC deploy role), `AWS_REGION_E2E` (default
`ap-northeast-3`), optionally `DD_SITE_E2E`

The OIDC deploy role and the policy backing it are cataloged in
`serverless-ci/e2e/iam-infra.md`.
86 changes: 86 additions & 0 deletions e2e/helpers/e2e.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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 Datadog, Inc.
*/

import assert from 'node:assert/strict';
import fs from 'node:fs';
import {fileURLToPath} from 'node:url';

import {type E2ENaming} from './naming';
import {type ExpectedLayers, type LambdaVerifierConfig} from './lambda-verifier';

// Repo-local config feeding the shared e2e helpers. This file is NOT synced -- it holds
// everything specific to serverless-plugin-datadog that the shared, parameterized helpers
// read through their config arguments.

export const NAMING: E2ENaming = {tool: 'slsplugin', platform: 'lambda'};

export const ENV_NAME = 'e2e';
export const ENV_VERSION = '1.0.0';
const RUNTIME = 'nodejs20.x';

// Transient cloud-provider errors safe to retry, passed as ExecOptions.retryPatterns.
export const RETRY_PATTERNS = [
// Generic / cross-cloud
'GatewayTimeout',
'Operation was canceled',
'ETIMEDOUT',
'ECONNRESET',
'temporarily unavailable',
// AWS Lambda / CloudFormation / STS
'ThrottlingException',
'TooManyRequestsException',
'Rate exceeded',
'RequestLimitExceeded',
'ResourceConflictException',
'ServiceException',
'InternalFailure',
'ServiceUnavailable',
'is in progress', // CloudFormation stack op already running
'ProvisionedConcurrencyConfig', // eventual-consistency churn on update
];

// `sls deploy` names functions `<service>-<stage>-<fn>`; stage is pinned to `e2e` and the
// only function is `hello`.
export const functionName = (serviceName: string): string => `${serviceName}-e2e-hello`;

// Pinned artifact versions come from the plugin's own src/layers.json, so a version
// mismatch blames the plugin/registry, not upstream drift.
const expectedLayerArns = (region: string): ExpectedLayers => {
const layersPath = fileURLToPath(new URL('../../src/layers.json', import.meta.url));
const layers = JSON.parse(fs.readFileSync(layersPath, 'utf-8')) as {
regions: Record<string, Record<string, string>>;
};
const regionLayers = layers.regions[region];
assert.ok(regionLayers, `region ${region} not present in src/layers.json`);
const node = regionLayers[RUNTIME];
const extension = regionLayers.extension;
assert.ok(node, `no ${RUNTIME} layer pinned for ${region} in src/layers.json`);
assert.ok(extension, `no extension layer pinned for ${region} in src/layers.json`);

return {node, extension};
};

export const VERIFIER: LambdaVerifierConfig = {
functionName,
expectedLayerArns,
redirectHandler: '/opt/nodejs/node_modules/datadog-lambda-js/handler.handler',
originalHandler: 'handler.handler',
// With the extension enabled the plugin tags the function with its own version marker.
toolTag: {key: 'dd_sls_plugin', pattern: /^v\d+\.\d+\.\d+/},
env: {
apiKeyVars: ['DD_API_KEY', 'DD_API_KEY_SECRET_ARN', 'DD_KMS_API_KEY', 'DD_API_KEY_SSM_ARN'],
present: ['DD_SITE'],
values: (serviceName) => ({
DD_TRACE_ENABLED: 'true',
DD_SERVERLESS_LOGS_ENABLED: 'true',
DD_SERVICE: serviceName,
DD_ENV: ENV_NAME,
DD_VERSION: ENV_VERSION,
}),
},
};
Loading
Loading