@webda/aws
Version:
Webda AWS Services implementation
530 lines • 22.4 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { APIGateway, CreateDeploymentCommand, PutRestApiCommand } from "@aws-sdk/client-api-gateway";
import { CloudFormation, CreateChangeSetCommand, CreateStackCommand, DeleteChangeSetCommand, DeleteStackCommand, DescribeChangeSetCommand, DescribeStackEventsCommand, DescribeStacksCommand, ExecuteChangeSetCommand, ListStackResourcesCommand } from "@aws-sdk/client-cloudformation";
import { GetCallerIdentityCommand, STS } from "@aws-sdk/client-sts";
import { suite, test } from "@testdeck/mocha";
import { JSONUtils } from "@webda/core";
import { DeployerTest } from "@webda/shell/lib/deployers/deployertest.js";
import * as assert from "assert";
import { mockClient } from "aws-sdk-client-mock";
import * as sinon from "sinon";
import { defaultCreds } from "../index.spec.js";
import { CloudFormationDeployer, LAMBDA_LATEST_VERSION } from "./cloudformation.js";
import { MockAWSDeployerMethods } from "./index.spec.js";
let CloudFormationDeployerTest = class CloudFormationDeployerTest extends DeployerTest {
constructor() {
super(...arguments);
this.mocks = {};
}
async getDeployer(manager) {
let deployer = await manager.getDeployer("WebdaSampleApplication");
MockAWSDeployerMethods(deployer, this);
return deployer;
}
async defaultResources() {
let resources = this.deployer.resources;
resources.Lambda = {};
resources.APIGateway = {};
resources.APIGatewayDomain = { DomainName: "webda.io" };
resources.Policy = {};
resources.APIGatewayBasePathMapping = {};
resources = JSONUtils.duplicate(resources);
await this.deployer.defaultResources();
assert.deepStrictEqual(this.deployer.resources.APIGatewayBasePathMapping, {
...resources.APIGatewayDomain,
BasePath: ""
});
assert.deepStrictEqual(this.deployer.resources.APIGatewayDomain, {
...resources.APIGatewayDomain,
SecurityPolicy: "TLS_1_2"
});
assert.deepStrictEqual(this.deployer.resources.Lambda, {
...resources.Lambda,
FunctionName: "WebdaSampleApplication",
Handler: "node_modules/@webda/aws/lib/deployers/lambda-entrypoint.handler",
MemorySize: 2048,
Role: { "Fn::GetAtt": ["Role", "Arn"] },
Runtime: LAMBDA_LATEST_VERSION,
Timeout: 30
});
assert.deepStrictEqual(this.deployer.resources.Policy, {
...resources.Policy,
PolicyDocument: { Statement: [] },
PolicyName: "WebdaSampleApplicationPolicy",
Roles: [{ Ref: "Role" }]
});
this.deployer.resources.ResourcesToImport = [];
this.deployer.resources.ChangeSetType = "CREATE";
await assert.rejects(() => this.deployer.defaultResources(), /ChangeSetType cannot be anything else than IMPORT if you have ResourcesToImport set/);
this.deployer.resources.ResourcesToImport = [];
this.deployer.resources.ChangeSetType = undefined;
this.deployer.resources.APIGatewayStage = {};
// @ts-ignore
this.deployer.resources.APIGatewayV2Domain = {};
// @ts-ignore
this.deployer.resources.Docker = { tag: "plop" };
this.deployer.getAWSIdentity = async () => ({
Account: "mine"
});
this.deployer.resources.APIGatewayBasePathMapping.DomainName = "webda.io.";
this.deployer.resources.APIGatewayV2ApiMapping = {
DomainName: "webda.io."
};
// @ts-ignore
this.deployer.resources.Statics = [{ Source: "/plop" }];
await this.deployer.defaultResources();
this.deployer.resources.Role.Policies = [];
await this.deployer.defaultResources();
this.deployer.resources.AssetsBucket = undefined;
await assert.rejects(() => this.deployer.defaultResources(), /AssetsBucket must be defined/);
}
async testSendCloudFormationTemplate() {
this.deployer.resources.AssetsBucket = "webda";
this.deployer.resources.AssetsPrefix = "plop/";
this.deployer.resources.FileName = "123";
this.deployer.template = {
fake: true
};
await this.deployer.sendCloudFormationTemplate();
assert.deepStrictEqual(this.deployer.result.CloudFormation, {
Bucket: "webda",
Key: "plop/123.json"
});
this.deployer.resources.AssetsPrefix = "/plop/";
this.deployer.resources.FileName = "123.json";
this.deployer.result = {};
await this.deployer.sendCloudFormationTemplate();
assert.deepStrictEqual(this.deployer.result.CloudFormation, {
Bucket: "webda",
Key: "plop/123.json"
});
this.deployer.resources.FileName = "123";
this.deployer.resources.Format = "YAML";
this.deployer.result = {};
await this.deployer.sendCloudFormationTemplate();
assert.deepStrictEqual(this.deployer.result.CloudFormation, {
Bucket: "webda",
Key: "plop/123.yml"
});
this.deployer.resources.FileName = "123.yml";
this.deployer.result = {};
await this.deployer.sendCloudFormationTemplate();
assert.deepStrictEqual(this.deployer.result.CloudFormation, {
Bucket: "webda",
Key: "plop/123.yml"
});
this.deployer.resources.FileName = "123.taml";
this.deployer.result = {};
await this.deployer.sendCloudFormationTemplate();
assert.deepStrictEqual(this.deployer.result.CloudFormation, {
Bucket: "webda",
Key: "plop/123.taml.yml"
});
this.deployer.resources.FileName = "123.yaml";
this.deployer.result = {};
this.deployer.resources.AssetsPrefix = "";
await this.deployer.sendCloudFormationTemplate();
assert.deepStrictEqual(this.deployer.result.CloudFormation, {
Bucket: "webda",
Key: "123.yaml"
});
}
async testDeploy() {
let resources = this.deployer.resources;
resources.Lambda = {};
resources.Resources = {};
resources.APIGateway = {};
resources.APIGatewayDomain = {
DomainName: "webda.io"
};
resources.Policy = {};
resources.Role = {};
resources.Fargate = {};
resources.Tags = [{ Key: "test", Value: "test" }];
resources.CustomResources = {
MyResource: { Type: "Test" }
};
let uploadStatics = sinon.stub(this.deployer, "uploadStatics");
let createCloudFormation = sinon.stub(this.deployer, "createCloudFormation");
let sendCloudFormation = sinon.stub(this.deployer, "sendCloudFormationTemplate");
let generateLambdaPackage = sinon.stub(this.deployer, "generateLambdaPackage");
sendCloudFormation.callsFake(async () => {
this.deployer.result.CloudFormation = {
Bucket: "plop",
Key: "mycf.json"
};
});
generateLambdaPackage.callsFake(async () => {
return {
S3Bucket: "fake",
S3Key: "lambda.zip"
};
});
await this.deployer.defaultResources();
const mock = mockClient(APIGateway);
try {
mock.on(PutRestApiCommand).resolves({});
this.deployer.resources.Docker = {
tag: "plop",
includeRepository: false
};
this.deployer.resources.APIGatewayImportOpenApi = "yop";
sinon.stub(this.deployer.manager, "run").callsFake(async () => { });
await this.deployer.deploy();
assert.strictEqual(sendCloudFormation.calledOnce, true);
assert.strictEqual(generateLambdaPackage.calledOnce, true);
await this.deployer.sleep(0.001);
}
finally {
mock.restore();
}
}
async testDeleteCloudFormation() {
this.mocks["deleteStack"] = sinon.stub().resolves({});
this.mocks["describeStacks"] = sinon.stub().callsFake(async () => {
if (this.mocks["describeStacks"].callCount > 1) {
throw new Error();
}
return {};
});
this.mocks["waitFor"] = sinon.stub(this.deployer, "waitFor").callsFake(async (c) => {
await c(() => { });
await c(() => { });
});
const mock = mockClient(CloudFormation)
.on(DeleteStackCommand)
.callsFake(this.mocks["deleteStack"])
.on(DescribeStacksCommand)
.callsFake(this.mocks["describeStacks"]);
try {
await this.deployer.deleteCloudFormation();
assert.strictEqual(this.mocks["deleteStack"].calledOnce, true);
assert.strictEqual(this.mocks["waitFor"].calledOnce, true);
assert.strictEqual(this.mocks["describeStacks"].calledTwice, true);
}
finally {
mock.restore();
}
}
async testInit() {
let logs = [];
let console = {
app: {
getAppPath: () => {
return "./noexisting";
}
},
log: (...args) => {
logs.push(args);
}
};
let caller = sinon.stub().callsFake(async () => {
if (caller.callCount === 1) {
throw new Error("Bad");
}
return { Account: "myAccount" };
});
let saver = sinon.stub(JSONUtils, "saveFile").callsFake(() => { });
const mocks = [];
try {
mocks.push(mockClient(STS).on(GetCallerIdentityCommand).callsFake(caller));
let stub = sinon.stub().resolves({});
mocks.push(mockClient(CloudFormation).on(CreateStackCommand).callsFake(stub));
assert.ok((await CloudFormationDeployer.init(console)) === -1);
assert.deepStrictEqual(logs[0], ["ERROR", "package.json not found"]);
console.app.getAppPath = () => "./test/package.json";
assert.ok((await CloudFormationDeployer.init(console)) === -1);
assert.deepStrictEqual(logs[1], [
"ERROR",
"Cannot retrieve your AWS credentials, make sure to have a correct AWS setup"
]);
logs = [];
assert.ok((await CloudFormationDeployer.init(console)) === 0);
assert.strictEqual(saver.getCall(0).args[0].webda.aws.AssetsBucket, "webda-webda-testor-assets");
assert.strictEqual(saver.getCall(0).args[0].webda.aws.Repository, "webda-webda-testor");
console.app.getAppPath = () => "./test/package-initiated.json";
logs = [];
assert.ok((await CloudFormationDeployer.init(console)) === -1);
assert.deepStrictEqual(logs[0], ["WARN", "Default information are already in your package.json"]);
}
finally {
mocks.forEach(m => m.restore());
}
}
async createCloudFormation() {
let describeChangeSetResult = { Status: "FAILED" };
let describeStackEventsResult = {
StackEvents: [
{
EventId: "123",
LogicalResourceId: this.deployer.resources.StackName,
ResourceStatus: "IN_PROGRESS"
}
]
};
let logs = sinon.stub(this.deployer.logger, "log");
sinon.stub(this.deployer, "waitFor").callsFake(callback => {
return new Promise((resolve, reject) => {
callback(resolve, reject);
});
});
const mocks = [];
sinon.stub(this.deployer, "createCloudFormationChangeSet").callsFake(async () => "mychange");
const mock = mockClient(CloudFormation)
.on(DescribeChangeSetCommand)
.resolves(describeChangeSetResult)
.on(ExecuteChangeSetCommand)
.resolves({})
.on(DescribeStackEventsCommand)
.callsFake(async () => {
let res = {
...describeStackEventsResult,
StackEvents: [...describeStackEventsResult.StackEvents]
};
describeStackEventsResult.StackEvents.unshift({
EventId: "123",
ResourceType: "any",
LogicalResourceId: this.deployer.resources.StackName,
ResourceStatus: "UPDATE_ROLLBACK_COMPLETE"
});
return res;
})
.on(ListStackResourcesCommand)
.resolves({
StackResourceSummaries: [
{
ResourceType: "AWS::ApiGateway::Stage",
PhysicalResourceId: "stage",
LogicalResourceId: "",
LastUpdatedTimestamp: new Date(),
ResourceStatus: "CREATE_COMPLETE"
},
{
ResourceType: "AWS::ApiGateway::RestApi",
PhysicalResourceId: "restApi",
LogicalResourceId: "",
LastUpdatedTimestamp: new Date(),
ResourceStatus: "CREATE_COMPLETE"
}
]
});
mocks.push(mock);
try {
let createDeployment = sinon.stub().resolves({});
mocks.push(mockClient(APIGateway).on(CreateDeploymentCommand).callsFake(createDeployment));
// 'FAILED' scenario with unknown error
logs.resetHistory();
describeChangeSetResult.Status = "FAILED";
describeChangeSetResult.StatusReason = "plop";
await this.deployer.createCloudFormation();
assert.deepStrictEqual(logs.getCall(1).args, ["ERROR", "Cannot execute ChangeSet:", "plop"]);
// 'FAILED' scenario because no changes to be made
describeChangeSetResult.StatusReason =
"The submitted information didn't contain changes. Submit different information to create a change set.";
logs.resetHistory();
await this.deployer.createCloudFormation();
assert.deepStrictEqual(logs.getCall(1).args, ["INFO", "No changes to be made"]);
// 'CREATE_COMPLETE' scenario
logs.resetHistory();
describeChangeSetResult.Status = "CREATE_COMPLETE";
describeChangeSetResult.Changes = [
{
Type: "Resource",
ResourceChange: {
Action: "Create",
ResourceType: "Fake",
LogicalResourceId: "FakeId"
}
},
{ Type: "Plop" }
];
await this.deployer.createCloudFormation();
assert.deepStrictEqual(createDeployment.getCall(0).args[0], {
restApiId: "restApi",
stageName: "stage"
});
// 'TIMEOUT'
this.deployer.sleep = async () => { };
describeStackEventsResult.StackEvents = [];
mock.on(DescribeStackEventsCommand).resolves(describeStackEventsResult);
await this.deployer.createCloudFormation();
assert.ok(createDeployment.callCount === 1);
}
finally {
mocks.forEach(m => m.restore());
}
}
async uploadStatics() {
sinon.stub(this.deployer, "putFolderOnBucket").callsFake(async () => { });
this.deployer.resources.Statics = [
// @ts-ignore
{ Source: "//", AssetsPath: "//" }
];
await this.deployer.uploadStatics();
}
async generateLambdaPackage() {
let run = sinon.stub(this.deployer.manager, "run").callsFake(async () => { });
try {
this.deployer.resources.KeepPackage = true;
await this.deployer.generateLambdaPackage();
}
finally {
run.restore();
}
}
async createCloudFormationChangeSet() {
let describeStacksResult = {
Stacks: [{ StackStatus: "ZZ", StackName: "", CreationTime: new Date() }]
};
this.deployer.result.CloudFormation = {
Bucket: "Bucket",
Key: "mykey"
};
const mock = mockClient(CloudFormation)
.on(CreateChangeSetCommand)
.resolves({})
.on(DescribeStacksCommand)
.callsFake(async () => describeStacksResult)
.on(DeleteChangeSetCommand)
.resolves({});
sinon.stub(this.deployer, "deleteCloudFormation").callsFake(async () => { });
try {
let cloudformation = new CloudFormation({ credentials: defaultCreds });
// Nominal case
await this.deployer.createCloudFormationChangeSet(cloudformation);
// With error
let testError;
let errorFirst = sinon.stub().callsFake(async () => {
if (errorFirst.callCount === 1) {
throw testError;
}
else {
return {};
}
});
mock.on(CreateChangeSetCommand).callsFake(errorFirst);
testError = new Error("plop is in ROLLBACK_COMPLETE state and can not be updated.");
// Should still go through with a bugous stack
await this.deployer.createCloudFormationChangeSet(cloudformation);
// Non-existing stack
testError = new Error(`Stack [${this.deployer.resources.StackName}] does not exist`);
errorFirst.resetHistory();
await this.deployer.createCloudFormationChangeSet(cloudformation);
// Change set exist
testError = new Error();
testError.code = "AlreadyExistsException";
errorFirst.resetHistory();
await this.deployer.createCloudFormationChangeSet(cloudformation);
// True error
testError = new Error("Unknown");
errorFirst.resetHistory();
await assert.rejects(() => this.deployer.createCloudFormationChangeSet(cloudformation), /Unknown/);
// Test with stack status
testError = new Error("bouzouf state and can not be updated.");
sinon.stub(this.deployer, "waitFor").callsFake(callback => {
return new Promise(async (resolve, reject) => {
if (!(await callback(resolve, reject))) {
reject("BAD");
}
});
});
errorFirst.resetHistory();
await assert.rejects(() => this.deployer.createCloudFormationChangeSet(cloudformation), /BAD/);
describeStacksResult = {
Stacks: [
{
StackStatus: "ANY_COMPLETE",
StackName: "",
CreationTime: new Date()
}
]
};
errorFirst.resetHistory();
await this.deployer.createCloudFormationChangeSet(cloudformation);
describeStacksResult = { Stacks: [] };
errorFirst.resetHistory();
await this.deployer.createCloudFormationChangeSet(cloudformation);
}
finally {
mock.restore();
}
}
async createStatic() {
this.deployer.template = { Resources: {} };
await this.deployer.createStatic({
Bucket: { Tags: {} },
DomainName: "webda.io"
});
assert.deepStrictEqual(this.deployer.template.Resources[`StaticwebdaioBucket`], {
Type: "AWS::S3::Bucket",
Properties: {
BucketName: "webda.io",
Tags: [
{
Key: "test",
Value: "webda3"
}
]
}
});
}
async APIGatewayDomain() {
this.deployer.template = { Resources: {} };
this.deployer.resources.APIGatewayDomain.EndpointConfiguration = {
Types: ["EDGE"]
};
sinon.stub(this.deployer, "createCloudFormationDNSEntry");
await this.deployer.APIGatewayDomain();
}
async createCloudFormationDNSEntry() {
this.mocks.getZoneForDomainName.callsFake(async () => {
return undefined;
});
await this.deployer.createCloudFormationDNSEntry(undefined, "plop.com", undefined);
}
};
__decorate([
test
], CloudFormationDeployerTest.prototype, "defaultResources", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "testSendCloudFormationTemplate", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "testDeploy", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "testDeleteCloudFormation", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "testInit", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "createCloudFormation", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "uploadStatics", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "generateLambdaPackage", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "createCloudFormationChangeSet", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "createStatic", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "APIGatewayDomain", null);
__decorate([
test
], CloudFormationDeployerTest.prototype, "createCloudFormationDNSEntry", null);
CloudFormationDeployerTest = __decorate([
suite
], CloudFormationDeployerTest);
//# sourceMappingURL=cloudformation.spec.js.map