Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update AWS creds initialization #82

Merged
merged 2 commits into from
Jan 19, 2024
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [5.0.1] - 2024-01-19

### Changed

- Updated credentials initialization

## [5.0.0] - 2023-12-15

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ To run integration tests, set an environment variable TEST\_VPC\_NAME to the VPC
```
export AWS_PROFILE=your_profile
export TEST_VPC_NAME=vpc_name
npm build
npm run build
npm run integration-test
```

Expand Down
1,381 changes: 1,170 additions & 211 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "serverless-vpc-discovery",
"version": "5.0.0",
"version": "5.0.1",
"engines": {
"node": ">=14"
},
Expand Down Expand Up @@ -77,6 +77,9 @@
},
"dependencies": {
"@aws-sdk/client-ec2": "^3.467.0",
"@aws-sdk/credential-providers": "^3.495.0",
"@smithy/config-resolver": "^2.1.0",
"@smithy/node-config-provider": "^2.2.0",
"@smithy/smithy-client": "^2.1.18",
"@smithy/util-retry": "^2.0.8",
"ts-md5": "^1.3.1"
Expand Down
15 changes: 15 additions & 0 deletions src/aws/ec2-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,25 @@ export class EC2Wrapper {
constructor (credentials: any) {
this.ec2 = new EC2Client([{
credentials,
region: Globals.getRegion(),
retryStrategy: Globals.getRetryStrategy()
}]);
}

/**
* Returns the promise that contains the vpc list
* @returns {Promise.<Vpc[]>}
*/
public async getVpcs (): Promise<Vpc[]> {
return await getAWSPagedResults(
this.ec2,
"Vpcs",
"NextToken",
"NextToken",
new DescribeVpcsCommand({})
);
}

/**
* Returns the promise that contains the vpc-id
* @param {string} vpcName
Expand Down
15 changes: 15 additions & 0 deletions src/globals.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ServerlessInstance, ServerlessOptions, ServerlessUtils } from "./types";
import { ConfiguredRetryStrategy } from "@smithy/util-retry";
import { fromIni } from "@aws-sdk/credential-providers";

export default class Globals {
public static pluginName = "Serverless VPC Discovery";
Expand All @@ -8,6 +9,20 @@ export default class Globals {
public static options: ServerlessOptions;
public static v3Utils?: ServerlessUtils;

public static currentRegion: string;
public static credentials: any;

public static defaultRegion = "us-east-1";

public static getRegion () {
const slsRegion = Globals.options.region || Globals.serverless.service.provider.region;
return slsRegion || Globals.currentRegion || Globals.defaultRegion;
}

public static async getProfileCreds (profile: string) {
return await fromIni({ profile })();
}

public static getRetryStrategy (attempts: number = 3, delay: number = 3000, backoff: number = 500) {
return new ConfiguredRetryStrategy(
attempts, // max attempts.
Expand Down
46 changes: 40 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import Globals from "./globals";
import { validateVPCDiscoveryConfig } from "./validation";
import { customProperties, functionProperties } from "./schema";
import Logging from "./logging";
import { loadConfig } from "@smithy/node-config-provider";
import { NODE_REGION_CONFIG_FILE_OPTIONS, NODE_REGION_CONFIG_OPTIONS } from "@smithy/config-resolver";

class VPCPlugin {
private serverless: ServerlessInstance;
public hooks: object;
public awsCredentials: any;
public lambdaFunction: LambdaFunction;

constructor (serverless, options, v3Utils?: ServerlessUtils) {
this.serverless = serverless;

Globals.serverless = serverless;
Globals.options = options;

if (v3Utils?.log) {
Globals.v3Utils = v3Utils;
Expand All @@ -36,7 +39,7 @@ class VPCPlugin {
*/
public async hookWrapper (lifecycleFunc: any) {
this.validateCustomVPCDiscoveryConfig();
this.initResources();
await this.initResources();
return await lifecycleFunc.call(this);
}

Expand Down Expand Up @@ -89,12 +92,43 @@ class VPCPlugin {
/**
* Setup AWS resources
*/
public initResources (): void {
this.awsCredentials = this.serverless.providers.aws.getCredentials();
this.awsCredentials.region = this.serverless.providers.aws.getRegion();
public async initResources (): Promise<void> {
// setup AWS resources
await this.initSLSCredentials();
await this.initAWSRegion();

const baseVPCDiscovery = this.serverless.service.custom ? this.serverless.service.custom.vpcDiscovery : null;
this.lambdaFunction = new LambdaFunction(this.awsCredentials, baseVPCDiscovery);
this.lambdaFunction = new LambdaFunction(Globals.credentials, baseVPCDiscovery);

// start of the legacy AWS SDK V2 creds support
// TODO: remove it in case serverless will add V3 support
try {
await this.lambdaFunction.ec2Wrapper.getVpcs();
} catch (error) {
if (error.message.includes("Could not load credentials from any providers")) {
Globals.credentials = this.serverless.providers.aws.getCredentials();
this.lambdaFunction = new LambdaFunction(Globals.credentials, baseVPCDiscovery);
}
}
}

/**
* Init AWS credentials based on sls `provider.profile`
*/
public async initSLSCredentials (): Promise<void> {
const slsProfile = Globals.options["aws-profile"] || Globals.serverless.service.provider.profile;
Globals.credentials = slsProfile ? await Globals.getProfileCreds(slsProfile) : null;
}

/**
* Init AWS current region based on Node options
*/
public async initAWSRegion (): Promise<void> {
try {
Globals.currentRegion = await loadConfig(NODE_REGION_CONFIG_OPTIONS, NODE_REGION_CONFIG_FILE_OPTIONS)();
} catch (err) {
Logging.logInfo("Node region was not found.");
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface VPC {
export interface ServerlessService {
service: string
provider: {
profile?: string,
stage: string,
region?: string
vpc: {},
Expand Down
57 changes: 29 additions & 28 deletions test/unit-tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const vpcId = "vpc-test";
Globals.options = {
stage: "test"
};
Globals.currentRegion = Globals.defaultRegion;

// This will create a mock plugin to be used for testing
const testFuncName = "funcTest";
Expand All @@ -66,7 +67,7 @@ const constructPlugin = (vpcConfig) => {
providers: {
aws: {
getRegion: () => "us-moon-1",
getCredentials: () => new Object()
getCredentials: () => {}
}
},
configSchemaHandler: {
Expand All @@ -76,7 +77,7 @@ const constructPlugin = (vpcConfig) => {
}
}
};
return new VPCPlugin(serverless, null);
return new VPCPlugin(serverless, { });
};

const initMessage = "[Info] Updating VPC config...";
Expand All @@ -103,7 +104,7 @@ describe("Given a vpc,", () => {
securityGroups: securityGroups
});
plugin.validateCustomVPCDiscoveryConfig();
plugin.initResources();
await plugin.initResources();

const expectedResult = {
funcTest: {
Expand All @@ -123,7 +124,7 @@ describe("Given a vpc,", () => {
EC2ClientMock.on(DescribeVpcsCommand).resolves(emptyData);

const plugin = constructPlugin({});
plugin.initResources();
await plugin.initResources();

const funcVPCDiscovery: VPCDiscovery = {
vpcName: "test",
Expand All @@ -132,8 +133,8 @@ describe("Given a vpc,", () => {

return await plugin.lambdaFunction.getFuncVPC("test", funcVPCDiscovery).then(() => {
const expectedMessage = `VPC with tag key 'Name' and tag value '${funcVPCDiscovery.vpcName}' does not exist`;
expect(consoleOutput[0]).to.equal(initFuncMessage.replace(testFuncName, "test"));
expect(consoleOutput[1]).to.contain(expectedMessage);
expect(consoleOutput[1]).to.equal(initFuncMessage.replace(testFuncName, "test"));
expect(consoleOutput[2]).to.contain(expectedMessage);
});
});

Expand All @@ -144,14 +145,14 @@ describe("Given a vpc,", () => {

describe("Given valid inputs for Subnets and Security Groups ", () => {
let plugin;
beforeEach(() => {
beforeEach(async () => {
const EC2ClientMock = mockClient(EC2Client);
EC2ClientMock.on(DescribeVpcsCommand).resolves(testData);
EC2ClientMock.on(DescribeSubnetsCommand).resolves(testData);
EC2ClientMock.on(DescribeSecurityGroupsCommand).resolves(testData);

plugin = constructPlugin({});
plugin.initResources();
await plugin.initResources();
});

it("without wildcards", async () => {
Expand Down Expand Up @@ -199,13 +200,13 @@ describe("Given invalid input for ", () => {
EC2ClientMock.on(DescribeSecurityGroupsCommand).resolves(testData);

const plugin = constructPlugin({});
plugin.initResources();
await plugin.initResources();

await plugin.lambdaFunction.getFuncVPC("test", funcVPCDiscovery).then(() => {
const expectedMessage = `Subnets with vpc id '${vpcId}', tag key 'Name' and tag values '${funcVPCDiscovery.subnets[0].tagValues}' do not exist`;
expect(consoleOutput[0]).to.equal(initFuncMessage.replace(testFuncName, "test"));
expect(consoleOutput[1]).to.equal(foundFuncMessage);
expect(consoleOutput[2]).to.contain(expectedMessage);
expect(consoleOutput[1]).to.equal(initFuncMessage.replace(testFuncName, "test"));
expect(consoleOutput[2]).to.equal(foundFuncMessage);
expect(consoleOutput[3]).to.contain(expectedMessage);
});
});

Expand All @@ -216,13 +217,13 @@ describe("Given invalid input for ", () => {
EC2ClientMock.on(DescribeSecurityGroupsCommand).resolves(emptyData);

const plugin = constructPlugin({});
plugin.initResources();
await plugin.initResources();

await plugin.lambdaFunction.getFuncVPC("test", funcVPCDiscovery).then(() => {
const expectedMessage = `Security groups with vpc id '${vpcId}', names '${securityGroups[0].names[0]}' do not exist`;
expect(consoleOutput[0]).to.equal(initFuncMessage.replace(testFuncName, "test"));
expect(consoleOutput[1]).to.equal(foundFuncMessage);
expect(consoleOutput[2]).to.contain(expectedMessage);
expect(consoleOutput[1]).to.equal(initFuncMessage.replace(testFuncName, "test"));
expect(consoleOutput[2]).to.equal(foundFuncMessage);
expect(consoleOutput[3]).to.contain(expectedMessage);
});
});

Expand All @@ -246,13 +247,13 @@ describe("Given input missing in AWS for ", () => {
securityGroups: [{ names: ["test_group_*"] }]
};
const plugin = constructPlugin({});
plugin.initResources();
await plugin.initResources();

await plugin.lambdaFunction.getFuncVPC("test", funcVPCDiscovery).then(() => {
const expectedMessage = `Subnets with vpc id '${vpcId}', tag key 'Name' and tag values 'missing_subnet' do not exist.`;
expect(consoleOutput[0]).to.equal(initFuncMessage.replace(testFuncName, "test"));
expect(consoleOutput[1]).to.equal(foundFuncMessage);
expect(consoleOutput[2]).to.contain(expectedMessage);
expect(consoleOutput[1]).to.equal(initFuncMessage.replace(testFuncName, "test"));
expect(consoleOutput[2]).to.equal(foundFuncMessage);
expect(consoleOutput[3]).to.contain(expectedMessage);
});
});

Expand All @@ -264,13 +265,13 @@ describe("Given input missing in AWS for ", () => {
};

const plugin = constructPlugin({});
plugin.initResources();
await plugin.initResources();

await plugin.lambdaFunction.getFuncVPC("test", funcVPCDiscovery).then(() => {
const expectedMessage = "Security groups do not exist for the names";
expect(consoleOutput[0]).to.equal(initFuncMessage.replace(testFuncName, "test"));
expect(consoleOutput[1]).to.equal(foundFuncMessage);
expect(consoleOutput[2]).to.contain(expectedMessage);
expect(consoleOutput[1]).to.equal(initFuncMessage.replace(testFuncName, "test"));
expect(consoleOutput[2]).to.equal(foundFuncMessage);
expect(consoleOutput[3]).to.contain(expectedMessage);
});
});

Expand All @@ -290,13 +291,13 @@ describe("Catching errors in updateVpcConfig ", () => {
securityGroups: securityGroups
});
plugin.validateCustomVPCDiscoveryConfig();
plugin.initResources();
await plugin.initResources();

await plugin.updateFunctionsVpcConfig().then(() => {
const expectedErrorMessage = `VPC with tag key 'Name' and tag value '${vpc}' does not exist.`;
expect(consoleOutput[0]).to.equal(initMessage);
expect(consoleOutput[1]).to.equal(initFuncMessage);
expect(consoleOutput[2]).to.contain(expectedErrorMessage);
expect(consoleOutput[1]).to.equal(initMessage);
expect(consoleOutput[2]).to.equal(initFuncMessage);
expect(consoleOutput[3]).to.contain(expectedErrorMessage);
});
});

Expand Down