@swarmion/serverless-plugin
Version:
A plugin to safely deploy Serverless microservices.
413 lines (397 loc) • 15 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
module.exports = __toCommonJS(src_exports);
// src/plugin/utils/artifactDirectory.ts
var getTimestampFromArtifactDirectoryName = (artifactDirectoryName) => {
const [, , , timestamp] = artifactDirectoryName.split("/");
if (timestamp === void 0) {
throw new Error(
`${artifactDirectoryName} is not of the form 'serverless/{service}/{stage}/{timestamp}'`
);
}
return timestamp;
};
var buildPreviousDeploymentArtifactDirectoryName = (prefix, service, stage, previousTimestamp) => {
return [prefix, service, stage, previousTimestamp].join("/");
};
// src/plugin/utils/constants.ts
var COMPILED_CONTRACTS_FILE_NAME = "serverless-contracts.json";
var LATEST_DEPLOYED_TIMESTAMP_TAG_NAME = "LATEST_DEPLOYED_TIMESTAMP";
var CONTRACTS_VERSION = "1.0.0";
// src/plugin/utils/generateOpenApiDocumentation.ts
var import_serverless_contracts = require("@swarmion/serverless-contracts");
var generateOpenApiDocumentation = (serverless) => {
const { provides } = (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
serverless.service.initialServerlessConfig.contracts
);
const openApiDocumentation = (0, import_serverless_contracts.getOpenApiDocumentation)({
title: serverless.service.getServiceName(),
description: serverless.service.initialServerlessConfig.resources.Description,
contracts: Object.values(provides)
});
console.log(JSON.stringify(openApiDocumentation, null, 2));
};
// src/plugin/utils/listLocalContractSchemas.ts
var import_isUndefined = __toESM(require("lodash/isUndefined.js"), 1);
var import_mapValues = __toESM(require("lodash/mapValues.js"), 1);
var import_omitBy = __toESM(require("lodash/omitBy.js"), 1);
var import_serverless_contracts2 = require("@swarmion/serverless-contracts");
var listLocalContractSchemas = (serverless) => {
const { provides, consumes } = (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
serverless.service.initialServerlessConfig.contracts
);
return {
provides: (0, import_omitBy.default)(
(0, import_mapValues.default)(provides, (contract) => (0, import_serverless_contracts2.getContractFullSchema)(contract)),
import_isUndefined.default
),
consumes: (0, import_omitBy.default)(
(0, import_mapValues.default)(consumes, (contract) => (0, import_serverless_contracts2.getContractFullSchema)(contract)),
import_isUndefined.default
)
};
};
// src/plugin/utils/getLatestDeployedTimestamp.ts
var getLatestDeployedTimestamp = async (provider) => {
if (provider.naming.getStackName === void 0) {
throw new Error("Could not get stack name");
}
const stackName = provider.naming.getStackName();
try {
const { Stacks } = await provider.request(
"CloudFormation",
"describeStacks",
{
StackName: stackName
}
);
return Stacks !== void 0 ? Stacks[0]?.Tags?.find(
({ Key }) => Key === LATEST_DEPLOYED_TIMESTAMP_TAG_NAME
)?.Value : void 0;
} catch {
return void 0;
}
};
// src/plugin/utils/listRemoteContractSchemas.ts
var listRemoteContractSchemas = async (serverless) => {
const provider = serverless.getProvider("aws");
const latestDeployedTimestamp = await getLatestDeployedTimestamp(provider);
if (latestDeployedTimestamp === void 0) {
return;
}
const previousArtifactDirectoryName = buildPreviousDeploymentArtifactDirectoryName(
"serverless",
serverless.service.getServiceName(),
serverless.service.provider.stage,
latestDeployedTimestamp
);
const bucketName = await provider.getServerlessDeploymentBucketName();
const params = {
Bucket: bucketName,
Key: `${previousArtifactDirectoryName}/${COMPILED_CONTRACTS_FILE_NAME}`
};
const { Body: remoteContractsBuffer } = await provider.request(
"S3",
"getObject",
params
);
if (remoteContractsBuffer === void 0) {
return {
provides: {},
consumes: {},
gitCommit: "",
contractsVersion: CONTRACTS_VERSION
};
}
const contractSchemas = JSON.parse(
// eslint-disable-next-line @typescript-eslint/no-base-to-string
remoteContractsBuffer.toString()
);
return contractSchemas;
};
// src/plugin/utils/printContractSchemas.ts
var printContractSchemas = ({ provides, consumes }, contractSchemasLocation) => {
console.log(
`--- Serverless contract schemas for location ${contractSchemasLocation} ---`
);
console.log();
console.log("-- Provides --");
console.log(JSON.stringify(provides, null, 2));
console.log();
console.log("-- Consumes --");
console.log(JSON.stringify(consumes, null, 2));
};
// src/plugin/utils/uploadContractSchemas.ts
var import_crypto = __toESM(require("crypto"), 1);
var import_simple_git = require("simple-git");
var uploadContractSchemas = async (serverless, log) => {
if (serverless.service.provider.shouldNotDeploy) {
log.info("Service files not changed. Skipping contract schemas upload...");
}
const provider = serverless.getProvider("aws");
const bucketName = await provider.getServerlessDeploymentBucketName();
const artifactDirectoryName = serverless.service.package.artifactDirectoryName;
const contractSchemas = listLocalContractSchemas(serverless);
const git = (0, import_simple_git.simpleGit)();
const gitCommit = await git.revparse("HEAD");
const contractSchemasToUpload = {
...contractSchemas,
gitCommit,
contractsVersion: CONTRACTS_VERSION
};
const fileHash = import_crypto.default.createHash("sha256").update(JSON.stringify(contractSchemasToUpload)).digest("base64");
const params = {
Bucket: bucketName,
Key: `${artifactDirectoryName}/${COMPILED_CONTRACTS_FILE_NAME}`,
Body: JSON.stringify(contractSchemasToUpload),
ContentType: "application/json",
Metadata: {
filesha256: fileHash
}
};
log.info("Uploading contract schemas file to S3...");
await provider.request("S3", "upload", params);
};
// src/plugin/utils/validateDeployment.ts
var import_json_schema_diff = __toESM(require("json-schema-diff"), 1);
// src/types/deploymentTypes.ts
var DeploymentStrategies = /* @__PURE__ */ ((DeploymentStrategies2) => {
DeploymentStrategies2["PROVIDER_FIRST"] = "PROVIDER_FIRST";
DeploymentStrategies2["CONSUMER_FIRST"] = "CONSUMER_FIRST";
return DeploymentStrategies2;
})(DeploymentStrategies || {});
// src/plugin/utils/validateDeployment.ts
var validateDeployment = async (localContracts, remoteContracts, deploymentStrategy) => {
if (deploymentStrategy === "PROVIDER_FIRST" /* PROVIDER_FIRST */) {
await validateProviderFirstDeployment(localContracts, remoteContracts);
} else {
await validateConsumerFirstDeployment(localContracts, remoteContracts);
}
};
var validateProviderFirstDeployment = async (localContractSchemas, remoteContractSchemas) => {
const { provides: localProvides } = localContractSchemas;
const { provides: remoteProvides } = remoteContractSchemas;
await Promise.all(
Object.entries(remoteProvides).map(
async ([contractName, remoteContractSchema]) => {
const localContractSchema = localProvides[contractName];
if (localContractSchema === void 0) {
throw new Error(`Expected to find local contract: ${contractName}`);
}
const { removalsFound } = await import_json_schema_diff.default.diffSchemas({
// @ts-ignore this is not well typed
sourceSchema: remoteContractSchema,
// @ts-ignore this is not well typed
destinationSchema: localContractSchema
});
if (removalsFound) {
throw new Error(
`Unexpected removal in provided contract schema: ${contractName}`
);
}
}
)
);
};
var validateConsumerFirstDeployment = async (localContractSchemas, remoteContractSchemas) => {
const { consumes: localConsumes } = localContractSchemas;
const { consumes: remoteConsumes } = remoteContractSchemas;
await Promise.all(
Object.entries(localConsumes).map(
async ([contractName, localContractSchema]) => {
const remoteContractSchema = remoteConsumes[contractName];
if (remoteContractSchema === void 0) {
throw new Error(`Expected to find remote contract: ${contractName}`);
}
const { additionsFound } = await import_json_schema_diff.default.diffSchemas({
// @ts-ignore this is not well typed
sourceSchema: remoteContractSchema,
// @ts-ignore this is not well typed
destinationSchema: localContractSchema
});
if (additionsFound) {
throw new Error(
`Unexpected addition in consumed contract schema: ${contractName}`
);
}
}
)
);
};
// src/types/serviceOptions.ts
var serviceOptionsSchema = {
type: "object",
properties: {
provides: {
type: "object",
patternProperties: {
"^[a-zA-Z0-9]{1,255}$": {
type: "object",
properties: {}
}
}
},
consumes: {
type: "object",
patternProperties: {
"^[a-zA-Z0-9]{1,255}$": {
type: "object",
properties: {}
}
}
}
},
required: ["provides", "consumes"]
};
// src/plugin/serverlessContractsPlugin.ts
var ServerlessContractsPlugin = class {
cliOptions;
serverless;
hooks;
commands;
log;
constructor(serverless, cliOptions, { log }) {
this.cliOptions = cliOptions;
this.log = log;
if (this.cliOptions.strategy !== void 0 && !Object.values(DeploymentStrategies).includes(this.cliOptions.strategy)) {
throw new Error(
`Invalid deployment strategy. Choices are ${JSON.stringify(
Object.values(DeploymentStrategies)
)}`
);
}
this.serverless = serverless;
serverless.configSchemaHandler.defineTopLevelProperty(
"contracts",
serviceOptionsSchema
);
this.commands = {
localContracts: {
usage: "Show local Serverless contracts",
lifecycleEvents: ["run"]
},
remoteContracts: {
usage: "Show currently deployed Serverless contracts",
lifecycleEvents: ["run"]
},
safeDeploy: {
usage: "Deploy you service and specify the deployment strategy",
lifecycleEvents: ["run"],
options: {
// Define the '--strategy' option with the '-s' shortcut
strategy: {
usage: "Specify the deployment strategy",
shortcut: "s",
required: true,
// @ts-ignore mistype in @types/serverless
type: "string"
}
}
},
generateOpenApiDocumentation: {
usage: "Generate OpenAPI with local Serverless contracts provided by the service",
lifecycleEvents: ["run"]
}
};
this.hooks = {
"localContracts:run": this.printLocalServerlessContractSchemas.bind(this),
"remoteContracts:run": this.printRemoteServerlessContractSchemas.bind(this),
"safeDeploy:run": this.deployWithContractSchemasValidation.bind(this),
"before:deploy:deploy": this.validateDeployment.bind(this),
"before:package:finalize": this.tagStackWithTimestamp.bind(this),
"after:aws:deploy:deploy:uploadArtifacts": this.uploadContractSchemas.bind(this),
"generateOpenApiDocumentation:run": this.generateOpenApiDocumentation.bind(this)
};
}
/**
* This command is merely a wrapper around the `deploy` command from the serverless Framework,
* leveraging the use of the `--strategy` option.
* Therefore, while this option has been set in the constructor, all we need to to is
* launch the serverless framework deployment
*/
async deployWithContractSchemasValidation() {
await this.serverless.pluginManager.spawn("deploy");
}
listLocalContractSchemas() {
return listLocalContractSchemas(this.serverless);
}
printLocalServerlessContractSchemas() {
const contractSchemas = this.listLocalContractSchemas();
printContractSchemas(contractSchemas, "LOCAL" /* LOCAL */);
}
async printRemoteServerlessContractSchemas() {
const contractSchemas = await this.listRemoteContractSchemas();
if (contractSchemas === void 0) {
this.log.error("Unable to retrieve remote contract schemas");
return;
}
printContractSchemas(contractSchemas, "REMOTE" /* REMOTE */);
}
async listRemoteContractSchemas() {
return listRemoteContractSchemas(this.serverless);
}
tagStackWithTimestamp() {
const artifactDirectoryName = this.serverless.service.package.artifactDirectoryName;
const timestamp = getTimestampFromArtifactDirectoryName(
artifactDirectoryName
);
this.serverless.service.provider.stackTags = {
...this.serverless.service.provider.stackTags,
[LATEST_DEPLOYED_TIMESTAMP_TAG_NAME]: timestamp
};
}
async uploadContractSchemas() {
await uploadContractSchemas(this.serverless, this.log);
}
async validateDeployment() {
const localContractSchemas = listLocalContractSchemas(this.serverless);
const remoteContractSchemas = await listRemoteContractSchemas(
this.serverless
);
if (remoteContractSchemas === void 0) {
this.log.warning(
"Contracts: Unable to retrieve remote contract schemas, deployment is unsafe"
);
return;
}
if (this.cliOptions.strategy !== void 0) {
this.log.info("Validating contract schemas...");
await validateDeployment(
localContractSchemas,
remoteContractSchemas,
this.cliOptions.strategy
);
}
}
generateOpenApiDocumentation() {
generateOpenApiDocumentation(this.serverless);
}
};
// src/index.ts
module.exports = ServerlessContractsPlugin;