UNPKG

@swarmion/serverless-plugin

Version:

A plugin to safely deploy Serverless microservices.

413 lines (397 loc) 15 kB
"use strict"; 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;