UNPKG

serverless-scaleway-functions

Version:

Provider plugin for the Serverless Framework v3.x which adds support for Scaleway Functions.

391 lines (331 loc) 12 kB
"use strict"; const BbPromise = require("bluebird"); const fs = require("fs"); const path = require("path"); // COMPILED_RUNTIMES_PREFIXES is an array containing all runtimes // that are considered as "compiled runtimes". // If you fill this array with "go" it will match all runtimes that starts with "go". // For example "golang", "go118" matches this filter. const COMPILED_RUNTIMES_PREFIXES = ["go", "rust"]; // RUNTIMES_EXTENSIONS serves two purposes : // - the struct key is used to list different runtimes families (go, python etc...) // - the content is used to list file extensions of the runtime, file extensions are only // required on non-compiled runtimes. const RUNTIMES_EXTENSIONS = { // tester .ts in node runtime node: ["ts", "js"], python: ["py"], php: ["php"], go: [], rust: [], }; const REGION_LIST = ["fr-par", "nl-ams", "pl-waw"]; const cronScheduleRegex = new RegExp( /^((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})$/ ); const triggerNameRegex = new RegExp(/^([a-zA-Z0-9-]){2,100}$/); const triggerNatsAccountIdRegex = new RegExp(/^([A-Z0-9]){56}$/); const triggerNatsProjectIdRegex = new RegExp( /^([a-z0-9]){8}-([a-z0-9]){4}-([a-z0-9]){4}-([a-z0-9]){4}-([a-z0-9]){12}$/ ); const triggerNatsSubjectRegex = new RegExp( /^(\$?[a-zA-Z0-9_*>][a-zA-Z0-9_*>.]*){1,200}$/ ); const TRIGGERS_VALIDATION = { schedule: (trigger) => { if (!trigger.rate || !cronScheduleRegex.test(trigger.rate)) { throw new Error( `Trigger Schedule is invalid: ${trigger.rate}, schedule should be formatted like a UNIX-Compliant Cronjob, for example: '1 * * * *'` ); } }, nats: (trigger) => { if (!trigger.name || !triggerNameRegex.test(trigger.name)) { throw new Error( `Trigger Schedule is invalid: ${ trigger.name }, name is invalid, should match regex: ${triggerNameRegex.toString()}` ); } if (!trigger.scw_nats_config) { throw new Error( `Trigger Schedule is invalid: ${trigger.name}, scw_nats_config is missing` ); } if ( !trigger.scw_nats_config.subject || !triggerNatsSubjectRegex.test(trigger.scw_nats_config.subject) ) { throw new Error( `Trigger Schedule is invalid: ${ trigger.name }, scw_nats_config.subject is invalid, should match regex: ${triggerNatsSubjectRegex.toString()}` ); } if ( !trigger.scw_nats_config.mnq_nats_account_id || !triggerNatsAccountIdRegex.test( trigger.scw_nats_config.mnq_nats_account_id ) ) { throw new Error( `Trigger Schedule is invalid: ${ trigger.name }, scw_nats_config.mnq_nats_account_id is invalid, should match regex: ${triggerNatsAccountIdRegex.toString()}` ); } if ( !trigger.scw_nats_config.mnq_project_id || !triggerNatsProjectIdRegex.test(trigger.scw_nats_config.mnq_project_id) ) { throw new Error( `Trigger Schedule is invalid: ${ trigger.name }, scw_nats_config.mnq_project_id is invalid, should match regex: ${triggerNatsProjectIdRegex.toString()}` ); } if ( !trigger.scw_nats_config.mnq_region || !REGION_LIST.includes(trigger.scw_nats_config.mnq_region) ) { throw new Error( `Trigger Schedule is invalid: ${trigger.name}, scw_nats_config.region is unknown}` ); } }, }; module.exports = { validate() { return BbPromise.bind(this) .then(this.validateServicePath) .then(this.validateCredentials) .then(this.validateRegion) .then(this.validateNamespace) .then(this.validateApplications) .then(this.checkErrors); }, validateServicePath() { if (!this.serverless.config.servicePath) { throw new Error( "This command can only be run inside a service directory" ); } return BbPromise.resolve(); }, validateCredentials() { if ( this.provider.scwToken.length !== 36 || this.provider.getScwProject().length !== 36 ) { const errorMessage = [ 'Either "scwToken" or "scwProject" is invalid.', " Credentials to deploy on your Scaleway Account are required, please read the documentation.", ].join(""); throw new Error(errorMessage); } }, validateRegion() { if (!REGION_LIST.includes(this.provider.scwRegion)) { throw new Error("unknown region"); } }, checkErrors(errors) { if (!errors || !errors.length) { return BbPromise.resolve(); } // Format error messages for user return BbPromise.reject(errors); }, validateNamespace(errors) { const currentErrors = Array.isArray(errors) ? errors : []; // Check space env vars: const namespaceEnvVars = this.serverless.service.provider.env; const namespaceErrors = this.validateEnv(namespaceEnvVars); return BbPromise.resolve(currentErrors.concat(namespaceErrors)); }, validateApplications(errors) { let functionNames = []; let containerNames = []; const currentErrors = Array.isArray(errors) ? errors : []; let functionErrors = []; let containers = []; let extensions = []; const { functions } = this.serverless.service; if (functions && Object.keys(functions).length !== 0) { functionNames = Object.keys(functions); let defaultRTexists = false; const rtKeys = Object.getOwnPropertyNames(RUNTIMES_EXTENSIONS); for (let i = 0; i < rtKeys.length; i += 1) { if (this.runtime.startsWith(rtKeys[i])) { defaultRTexists = true; extensions = RUNTIMES_EXTENSIONS[rtKeys[i]]; break; } } if (!defaultRTexists) { functionErrors.push( `Runtime ${this.runtime} is not supported, please check documentation for available runtimes` ); } functionNames.forEach((functionName) => { const func = functions[functionName]; // check if runtime is compiled runtime, if so we skip validations for (let i = 0; i < COMPILED_RUNTIMES_PREFIXES.length; i += 1) { if ( (func.runtime !== undefined && func.runtime.startsWith(COMPILED_RUNTIMES_PREFIXES[i])) || (!func.runtime && this.runtime.startsWith(COMPILED_RUNTIMES_PREFIXES[i])) ) { return; // for compiled runtimes there is no need to validate specific files } } // Check that function's runtime is authorized if existing if (func.runtime) { let RTexists = false; for (let i = 0; i < rtKeys.length; i += 1) { if (func.runtime.startsWith(rtKeys[i])) { RTexists = true; extensions = RUNTIMES_EXTENSIONS[rtKeys[i]]; break; } } if (!RTexists) { functionErrors.push( `Runtime ${func.runtime} is not supported, please check documentation for available runtimes` ); } } // Check if function handler exists try { // get handler file => path/to/file.handler => split ['path/to/file', 'handler'] const splitHandlerPath = func.handler.split("."); if (splitHandlerPath.length !== 2) { throw new Error( `Handler is malformatted for ${functionName}: handler should be path/to/file.functionInsideFile` ); } const handlerPath = splitHandlerPath[0]; // For each extensions linked to a language (node: .ts,.js, python: .py ...), // check that a handler file exists with one of the extensions let handlerFileExists = false; for (let i = 0; i < extensions.length; i += 1) { const handler = `${handlerPath}.${extensions[i]}`; if (fs.existsSync(path.resolve("./", handler))) { handlerFileExists = true; break; } } // If Handler file does not exist, throw an error if (!handlerFileExists) { throw new Error("File does not exists"); } } catch (error) { const message = `Handler file defined for function ${functionName} does not exist (${func.handler}, err : ${error} ).`; functionErrors.push(message); } // Check that triggers are valid func.events = func.events || []; functionErrors = [ ...functionErrors, ...this.validateTriggers(func.events), ]; }); } if (this.serverless.service.custom) { containers = this.serverless.service.custom.containers; } if (containers && Object.keys(containers).length !== 0) { containerNames = Object.keys(containers); // Validate triggers/events for containers containerNames.forEach((containerName) => { const container = containers[containerName]; container.events = container.events || []; functionErrors = [ ...functionErrors, ...this.validateTriggers(container.events), ]; }); } if (!functionNames.length && !containerNames.length) { functionErrors.push( "You must define at least one function or container to deploy under the functions or custom key." ); } return BbPromise.resolve(currentErrors.concat(functionErrors)); }, validateTriggers(triggers) { // Check that key schedule exists return triggers.reduce((accumulator, trigger) => { const triggerKeys = Object.keys(trigger); if (triggerKeys.length !== 1) { const errorMessage = "Trigger is invalid, it should contain at least one event type configuration (example: schedule)."; return [...accumulator, errorMessage]; } // e.g schedule, http const triggerName = triggerKeys[0]; const authorizedTriggers = Object.keys(TRIGGERS_VALIDATION); if (!authorizedTriggers.includes(triggerName)) { const errorMessage = `Trigger Type ${triggerName} is not currently supported by Scaleway's Serverless platform, supported types are the following: ${authorizedTriggers.join( ", " )}`; return [...accumulator, errorMessage]; } // Run Trigger validation try { TRIGGERS_VALIDATION[triggerName](trigger[triggerName]); } catch (error) { return [...accumulator, error.message]; } return accumulator; }, []); }, validateEnv(variables) { const errors = []; if (!variables) return errors; if (typeof variables !== "object") { throw new Error( "Environment variables should be a map of strings under the form: key - value" ); } const variableNames = Object.keys(variables); variableNames.forEach((variableName) => { const variable = variables[variableName]; if (typeof variable !== "string") { const error = `Variable ${variableName}: variable is invalid, environment variables may only be strings`; errors.push(error); } }); return errors; }, isDefinedContainer(containerName) { // Check if given name is listed as a container let res = false; if ( this.provider.serverless.service.custom && this.provider.serverless.service.custom.containers ) { let foundKey = Object.keys( this.provider.serverless.service.custom.containers ).find((k) => k == containerName); if (foundKey) { res = true; } } return res; }, isDefinedFunction(functionName) { // Check if given name is listed as a function let res = false; if (this.provider.serverless.service.functions) { let foundKey = Object.keys( this.provider.serverless.service.functions ).find((k) => k == functionName); if (foundKey) { res = true; } } return res; }, };