Skip to content
Open
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
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ module.exports = {
testEnvironment: "<rootDir>/testEnvironment.js",
clearMocks: true,
collectCoverageFrom: ["src/**/*.ts"],
testRegex: "(src\\/).*(\\.spec\\.ts)$",
testPathIgnorePatterns: ["\\.snap$", "<rootDir>/node_modules/", "snapshot_tests/"],
testRegex: String.raw`(src[\/]).*(\.spec\.ts)$`,
testPathIgnorePatterns: [String.raw`\.snap$`, "<rootDir>/node_modules/", "snapshot_tests/"],
};
2 changes: 1 addition & 1 deletion jest.integration.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ module.exports = {
testEnvironment: "<rootDir>/testEnvironment.js",
clearMocks: true,
collectCoverageFrom: ["src/**/*.ts"],
testRegex: ["(snapshot_tests\\/).*(\\.spec\\.ts)$"],
testRegex: [String.raw`(snapshot_tests[\/]).*(\.spec\.ts)$`],
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
]
},
"dependencies": {
"@aws-sdk/client-cloudformation": "^3.137.0",
"@aws-sdk/client-cloudwatch-logs": "^3.137.0",
"@aws-sdk/client-lambda": "^3.137.0",
"@datadog/datadog-ci-base": "^5.16.1",
"simple-git": "^3.36.0"
},
Expand Down
102 changes: 102 additions & 0 deletions src/aws-sdk-request.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Aws from "serverless/plugins/aws/provider/awsProvider";
import {
cloudFormationDescribeStacks,
cloudWatchLogsDescribeSubscriptionFilters,
lambdaGetFunction,
} from "./aws-sdk-request";

const lambdaSend = jest.fn();
const logsSend = jest.fn();
const cfnSend = jest.fn();

const LambdaClient = jest.fn().mockImplementation(() => ({ send: lambdaSend }));
const CloudWatchLogsClient = jest.fn().mockImplementation(() => ({ send: logsSend }));
const CloudFormationClient = jest.fn().mockImplementation(() => ({ send: cfnSend }));

jest.mock("@aws-sdk/client-lambda", () => ({
LambdaClient,
GetFunctionCommand: jest.fn().mockImplementation((input) => ({ input })),
}));
jest.mock("@aws-sdk/client-cloudwatch-logs", () => ({
CloudWatchLogsClient,
DescribeSubscriptionFiltersCommand: jest.fn().mockImplementation((input) => ({ input })),
}));
jest.mock("@aws-sdk/client-cloudformation", () => ({
CloudFormationClient,
DescribeStacksCommand: jest.fn().mockImplementation((input) => ({ input })),
}));

/** Serverless Framework style provider: exposes request(), no getAwsSdkV3Config(). */
function legacyAwsMock(requestImpl: jest.Mock): Aws {
return { request: requestImpl } as unknown as Aws;
}

/** osls v4 style provider: exposes getAwsSdkV3Config(), request() throws. */
function oslsV4AwsMock(config: Record<string, unknown> = { region: "us-east-1" }): Aws {
return {
getAwsSdkV3Config: jest.fn().mockResolvedValue(config),
request: jest.fn().mockRejectedValue(new Error("AWS_SDK_V2_SURFACE_REMOVED")),
} as unknown as Aws;
}

describe("aws-sdk-request dual path", () => {
describe("Serverless Framework (legacy provider.request path)", () => {
it("lambdaGetFunction calls provider.request", async () => {
const request = jest.fn().mockResolvedValue(undefined);
await lambdaGetFunction(legacyAwsMock(request), "my-forwarder");
expect(request).toHaveBeenCalledWith("Lambda", "getFunction", { FunctionName: "my-forwarder" });
expect(LambdaClient).not.toHaveBeenCalled();
});

it("cloudWatchLogsDescribeSubscriptionFilters returns filters from provider.request", async () => {
const filters = [{ filterName: "dd" }];
const request = jest.fn().mockResolvedValue({ subscriptionFilters: filters });
const result = await cloudWatchLogsDescribeSubscriptionFilters(legacyAwsMock(request), "/aws/lambda/foo");
expect(request).toHaveBeenCalledWith("CloudWatchLogs", "describeSubscriptionFilters", {
logGroupName: "/aws/lambda/foo",
});
expect(result).toBe(filters);
});

it("cloudFormationDescribeStacks calls provider.request with region option", async () => {
const output = { Stacks: [{ StackId: "abc" }] };
const request = jest.fn().mockResolvedValue(output);
const result = await cloudFormationDescribeStacks(legacyAwsMock(request), "my-stack", "eu-west-1");
expect(request).toHaveBeenCalledWith(
"CloudFormation",
"describeStacks",
{ StackName: "my-stack" },
{ region: "eu-west-1" },
);
expect(result).toBe(output);
});
});

describe("osls v4 (AWS SDK v3 client path)", () => {
it("lambdaGetFunction constructs a LambdaClient and sends GetFunctionCommand", async () => {
lambdaSend.mockResolvedValue({});
const aws = oslsV4AwsMock();
await lambdaGetFunction(aws, "my-forwarder");
expect((aws as any).getAwsSdkV3Config).toHaveBeenCalled();
expect(LambdaClient).toHaveBeenCalledWith({ region: "us-east-1" });
expect(lambdaSend).toHaveBeenCalledWith({ input: { FunctionName: "my-forwarder" } });
});

it("cloudWatchLogsDescribeSubscriptionFilters returns [] when SDK response omits filters", async () => {
logsSend.mockResolvedValue({});
const result = await cloudWatchLogsDescribeSubscriptionFilters(oslsV4AwsMock(), "/aws/lambda/foo");
expect(CloudWatchLogsClient).toHaveBeenCalled();
expect(logsSend).toHaveBeenCalledWith({ input: { logGroupName: "/aws/lambda/foo" } });
expect(result).toEqual([]);
});

it("cloudFormationDescribeStacks passes the region to getAwsSdkV3Config and sends the command", async () => {
cfnSend.mockResolvedValue({ Stacks: [{ StackId: "abc" }] });
const aws = oslsV4AwsMock({ region: "eu-west-1" });
const result = await cloudFormationDescribeStacks(aws, "my-stack", "eu-west-1");
expect((aws as any).getAwsSdkV3Config).toHaveBeenCalledWith({ region: "eu-west-1" });
expect(cfnSend).toHaveBeenCalledWith({ input: { StackName: "my-stack" } });
expect(result).toEqual({ Stacks: [{ StackId: "abc" }] });
});
});
});
67 changes: 67 additions & 0 deletions src/aws-sdk-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Aws from "serverless/plugins/aws/provider/awsProvider";

/**
* osls v4 removed the AWS SDK v2 surface from the AWS provider: calling
* `provider.request()`, `provider.sdk` or `provider.getCredentials()` now throws
* an error with code `AWS_SDK_V2_SURFACE_REMOVED`. Instead osls v4 exposes
* `provider.getAwsSdkV3Config()`, which returns osls-resolved client
* configuration (region, credentials, retry/proxy/CA settings) so plugins can
* build their own AWS SDK v3 clients.
*
* The Serverless Framework (1.x–4.x) still supports `provider.request()` and
* does not expose `getAwsSdkV3Config()`. To stay compatible with both we detect
* `getAwsSdkV3Config()` at runtime: when present (osls v4) we construct an AWS
* SDK v3 client, otherwise we fall back to the still-supported
* `provider.request()`. The SDK v3 client packages are loaded lazily so they
* are only required when the osls v4 code path actually runs.
*
* See https://github.com/oss-serverless/osls/blob/4.x/docs/guides/upgrading-to-v4.md#aws-sdk-v2-removed-plugin-authors
*/

interface AwsSdkV3ClientConfig {
region?: string;
[key: string]: unknown;
}

// `getAwsSdkV3Config` is not part of @types/serverless yet, so extend the type.
type AwsProviderWithSdkV3 = Aws & {
getAwsSdkV3Config(options?: { region?: string }): Promise<AwsSdkV3ClientConfig>;
};

function supportsAwsSdkV3(aws: Aws): aws is AwsProviderWithSdkV3 {
return typeof (aws as Partial<AwsProviderWithSdkV3>).getAwsSdkV3Config === "function";
}

/** Calls Lambda GetFunction. Rejects if the function does not exist. */
export async function lambdaGetFunction(aws: Aws, functionName: string): Promise<void> {
if (supportsAwsSdkV3(aws)) {
const { LambdaClient, GetFunctionCommand } = await import("@aws-sdk/client-lambda");
const client = new LambdaClient(await aws.getAwsSdkV3Config());
await client.send(new GetFunctionCommand({ FunctionName: functionName }));
return;
}
await aws.request("Lambda", "getFunction", { FunctionName: functionName });
}

/** Calls CloudWatchLogs DescribeSubscriptionFilters for a log group. */
export async function cloudWatchLogsDescribeSubscriptionFilters(aws: Aws, logGroupName: string): Promise<any[]> {
if (supportsAwsSdkV3(aws)) {
const { CloudWatchLogsClient, DescribeSubscriptionFiltersCommand } =
await import("@aws-sdk/client-cloudwatch-logs");
const client = new CloudWatchLogsClient(await aws.getAwsSdkV3Config());
const output = await client.send(new DescribeSubscriptionFiltersCommand({ logGroupName }));
return output.subscriptionFilters ?? [];
}
const result = await aws.request("CloudWatchLogs", "describeSubscriptionFilters", { logGroupName });
return result.subscriptionFilters;
}

/** Calls CloudFormation DescribeStacks for a stack in the given region. */
export async function cloudFormationDescribeStacks(aws: Aws, stackName: string, region: string): Promise<any> {
if (supportsAwsSdkV3(aws)) {
const { CloudFormationClient, DescribeStacksCommand } = await import("@aws-sdk/client-cloudformation");
const client = new CloudFormationClient(await aws.getAwsSdkV3Config({ region }));
return client.send(new DescribeStacksCommand({ StackName: stackName }));
}
return aws.request("CloudFormation", "describeStacks", { StackName: stackName }, { region });
}
16 changes: 3 additions & 13 deletions src/forwarder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Service from "serverless/classes/Service";
import { FunctionInfo } from "./layer";
import { version } from "../package.json";
import Aws = require("serverless/plugins/aws/provider/awsProvider");
import { cloudWatchLogsDescribeSubscriptionFilters, lambdaGetFunction } from "./aws-sdk-request";

const logGroupKey = "AWS::Logs::LogGroup";
const logGroupSubscriptionKey = "AWS::Logs::SubscriptionFilter";
Expand Down Expand Up @@ -38,10 +39,6 @@ interface SubscriptionFilter {
logGroupName: string;
roleArn: string;
}
interface DescribeSubscriptionFiltersResponse {
subscriptionFilters: SubscriptionFilter[];
}

type SubLogsConfig =
| boolean
| {
Expand Down Expand Up @@ -115,7 +112,7 @@ function isLogGroup(value: unknown): value is LogGroupResource {
*/
async function validateForwarderArn(aws: Aws, functionArn: CloudFormationObjectArn | string): Promise<void> {
try {
await aws.request("Lambda", "getFunction", { FunctionName: functionArn });
await lambdaGetFunction(aws, functionArn as string);
} catch (err) {
throw new DatadogForwarderNotFoundError(`Could not perform GetFunction on ${functionArn}.`);
}
Expand Down Expand Up @@ -293,14 +290,7 @@ export async function canSubscribeLogGroup(aws: Aws, logGroupName: string, expec

export async function describeSubscriptionFilters(aws: Aws, logGroupName: string): Promise<SubscriptionFilter[]> {
try {
const result: DescribeSubscriptionFiltersResponse = await aws.request(
"CloudWatchLogs",
"describeSubscriptionFilters",
{
logGroupName,
},
);
return result.subscriptionFilters;
return await cloudWatchLogsDescribeSubscriptionFilters(aws, logGroupName);
} catch (err) {
// An error will occur if the log group doesn't exist, so we swallow this and return an empty list.
return [];
Expand Down
18 changes: 6 additions & 12 deletions src/monitor-api-requests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as Serverless from "serverless";
import { MonitorParams, ServerlessMonitor, replaceCriticalThreshold } from "./monitors";
import { cloudFormationDescribeStacks } from "./aws-sdk-request";

export class InvalidAuthenticationError extends Error {
constructor(message: string) {
Expand Down Expand Up @@ -127,18 +128,11 @@ export async function searchMonitors(
}

export async function getCloudFormationStackId(serverless: Serverless): Promise<string> {
const stackName = serverless.getProvider("aws").naming.getStackName();
const describeStackOutput = await serverless
.getProvider("aws")
.request(
"CloudFormation",
"describeStacks",
{ StackName: stackName },
{ region: serverless.getProvider("aws").getRegion() },
)
.catch(() => {
// Ignore any request exceptions, fail silently and skip output logging
});
const aws = serverless.getProvider("aws");
const stackName = aws.naming.getStackName();
const describeStackOutput = await cloudFormationDescribeStacks(aws, stackName, aws.getRegion()).catch(() => {
// Ignore any request exceptions, fail silently and skip output logging
});
const cloudFormationStackId: string = describeStackOutput ? describeStackOutput.Stacks[0].StackId : "";
return cloudFormationStackId;
}
Expand Down
18 changes: 6 additions & 12 deletions src/output.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as Serverless from "serverless";
import { FunctionInfo } from "./layer";
import { cloudFormationDescribeStacks } from "./aws-sdk-request";

const yellowFont = "\x1b[33m";
const orangeFont = "\x1b[38;2;255;165;0m";
Expand Down Expand Up @@ -41,18 +42,11 @@ export async function printOutputs(
service: string,
env: string,
): Promise<void> {
const stackName = serverless.getProvider("aws").naming.getStackName();
const describeStackOutput = await serverless
.getProvider("aws")
.request(
"CloudFormation",
"describeStacks",
{ StackName: stackName },
{ region: serverless.getProvider("aws").getRegion() },
)
.catch(() => {
// Ignore any request exceptions, fail silently and skip output logging
});
const aws = serverless.getProvider("aws");
const stackName = aws.naming.getStackName();
const describeStackOutput = await cloudFormationDescribeStacks(aws, stackName, aws.getRegion()).catch(() => {
// Ignore any request exceptions, fail silently and skip output logging
});
if (describeStackOutput === undefined) {
return;
}
Expand Down
Loading