mirror of
https://github.com/serverless/serverless.git
synced 2026-01-18 14:58:43 +00:00
359 lines
13 KiB
JavaScript
359 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const d = require('d');
|
|
const lazy = require('d/lazy');
|
|
const path = require('path');
|
|
const fsp = require('fs').promises;
|
|
const fetch = require('node-fetch');
|
|
const filesize = require('filesize');
|
|
const log = require('@serverless/utils/log').log.get('console');
|
|
const isAuthenticated = require('@serverless/dashboard-plugin/lib/is-authenticated');
|
|
const { getPlatformClientWithAccessKey } = require('@serverless/dashboard-plugin/lib/client-utils');
|
|
const ServerlessError = require('../serverless-error');
|
|
const { setBucketName } = require('../plugins/aws/lib/set-bucket-name');
|
|
const { uploadZipFile } = require('../plugins/aws/lib/upload-zip-file');
|
|
|
|
const supportedCommands = new Set(['deploy', 'deploy function', 'package', 'remove', 'rollback']);
|
|
const devVersionTimeBase = new Date(2022, 1, 17).getTime();
|
|
|
|
class Console {
|
|
constructor(serverless) {
|
|
this.serverless = serverless;
|
|
// Used to confirm that we obtained compatible console state data for deployment
|
|
this.stateSchemaVersion = '1';
|
|
}
|
|
|
|
async initialize() {
|
|
this.isEnabled = (() => {
|
|
const {
|
|
configurationInput: configuration,
|
|
processedInput: { commands, options },
|
|
} = this.serverless;
|
|
if (!_.get(configuration, 'console')) return false;
|
|
this.org = options.org || configuration.org;
|
|
if (!this.org) return false;
|
|
const command = commands.join(' ');
|
|
if (!supportedCommands.has(command)) return false;
|
|
|
|
const providerName = configuration.provider.name || configuration.provider;
|
|
if (providerName !== 'aws') {
|
|
log.error(`Provider "${providerName}" is currently not supported by the console`);
|
|
return false;
|
|
}
|
|
|
|
if (command !== 'rollback' && (command !== 'deploy' || !options.package)) {
|
|
if (
|
|
!Object.values(this.serverless.service.functions).some((functionConfig) =>
|
|
this.isFunctionSupported(functionConfig)
|
|
)
|
|
) {
|
|
log.warning(
|
|
"Cannot enable console: Service doesn't configure any function with the supported runtime"
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
})();
|
|
if (!this.isEnabled) return;
|
|
if (!isAuthenticated()) {
|
|
const errorMessage = process.env.CI
|
|
? 'You are not currently logged in. Follow instructions in http://slss.io/run-in-cicd to setup env vars for authentication.'
|
|
: 'You are not currently logged in. To log in, run "serverless login"';
|
|
throw new ServerlessError(errorMessage, 'CONSOLE_NOT_AUTHENTICATED');
|
|
}
|
|
this.packagePath =
|
|
this.serverless.processedInput.options.package ||
|
|
this.serverless.service.package.path ||
|
|
path.join(this.serverless.serviceDir, '.serverless');
|
|
this.provider = this.serverless.getProvider('aws');
|
|
this.sdk = await getPlatformClientWithAccessKey(this.org);
|
|
this.orgId = (await this.sdk.getOrgByName(this.org)).orgUid;
|
|
this.service = this.serverless.service.service;
|
|
this.stage = this.provider.getStage();
|
|
this.otelIngestionUrl = (() => {
|
|
if (process.env.SLS_CONSOLE_OTEL_INGESTION_URL) {
|
|
return process.env.SLS_CONSOLE_OTEL_INGESTION_URL;
|
|
}
|
|
return process.env.SERVERLESS_PLATFORM_STAGE === 'dev'
|
|
? 'https://core.serverless-dev.com/ingestion/kinesis'
|
|
: 'https://core.serverless.com/ingestion/kinesis';
|
|
})();
|
|
}
|
|
|
|
isFunctionSupported({ handler, runtime }) {
|
|
if (!handler) return false; // Docker container image (not supported yet)
|
|
if (!runtime) return true; // Default is supported nodejs runtime
|
|
return runtime.startsWith('nodejs');
|
|
}
|
|
|
|
async createOtelIngestionToken() {
|
|
const url = `${this.otelIngestionUrl}/org/${this.orgId}/service/${this.service}/stage/${this.stage}`;
|
|
const data = {
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.sdk.accessKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
};
|
|
log.debug('get token: %s %o', url, data);
|
|
const response = await fetch(url, data);
|
|
if (!response.ok) {
|
|
throw new ServerlessError(
|
|
`Cannot deploy to the Console: Cannot retrieve token (${
|
|
response.status
|
|
}: ${await response.text()})`,
|
|
'CONSOLE_TOKEN_GENERATION_FAILURE'
|
|
);
|
|
}
|
|
const responseBody = await response.json();
|
|
log.debug('get token response: %o', responseBody);
|
|
if (!_.get(responseBody, 'token.accessToken')) {
|
|
throw new Error(
|
|
`Cannot deploy to the Console: Unexpected server response (${JSON.stringify(responseBody)})`
|
|
);
|
|
}
|
|
if (responseBody.status === 'new_token') {
|
|
this.isFreshOtelIngestionToken = true;
|
|
if (this.serverless.processedInput.commands.join(' ') === 'deploy') {
|
|
log.info(
|
|
'Generated a new access token for the Console (deployment may take longer than' +
|
|
' anticipated as the configuration of all functions will be updated)'
|
|
);
|
|
}
|
|
}
|
|
return responseBody.token.accessToken;
|
|
}
|
|
|
|
async activateOtelIngestionToken() {
|
|
const url = `${this.otelIngestionUrl}/token`;
|
|
const data = {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.sdk.accessKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
orgId: this.orgId,
|
|
serviceId: this.service,
|
|
stage: this.stage,
|
|
token: await this.deferredOtelIngestionToken,
|
|
}),
|
|
};
|
|
log.debug('activate token %s %o', url, data);
|
|
const response = await fetch(url, data);
|
|
if (!response.ok) {
|
|
throw new ServerlessError(
|
|
`Cannot deploy to the Console: Cannot activate token (${
|
|
response.status
|
|
}: ${await response.text()})`,
|
|
'CONSOLE_TOKEN_CREATION_FAILURE'
|
|
);
|
|
}
|
|
await response.text();
|
|
}
|
|
|
|
async deactivateOtherOtelIngestionTokens() {
|
|
const searchParams = new URLSearchParams();
|
|
searchParams.set('orgId', this.orgId);
|
|
searchParams.set('serviceId', this.service);
|
|
searchParams.set('stage', this.stage);
|
|
searchParams.set('token', await this.deferredOtelIngestionToken);
|
|
const url = `${this.otelIngestionUrl}/tokens?${searchParams}`;
|
|
const data = {
|
|
method: 'DELETE',
|
|
headers: {
|
|
Authorization: `Bearer ${this.sdk.accessKey}`,
|
|
},
|
|
};
|
|
log.debug('deactivate other tokens %s %o', url, data);
|
|
const response = await fetch(url, data);
|
|
if (!response.ok) {
|
|
log.error(
|
|
'Console communication problem ' +
|
|
'when deactivating no longer used otel ingestion tokens: %d %s',
|
|
response.status,
|
|
await response.text()
|
|
);
|
|
return;
|
|
}
|
|
await response.text();
|
|
}
|
|
|
|
async deactivateAllOtelIngestionTokens() {
|
|
const searchParams = new URLSearchParams();
|
|
searchParams.set('orgId', this.orgId);
|
|
searchParams.set('serviceId', this.service);
|
|
searchParams.set('stage', this.stage);
|
|
const url = `${this.otelIngestionUrl}/tokens?${searchParams}`;
|
|
const data = {
|
|
method: 'DELETE',
|
|
headers: {
|
|
Authorization: `Bearer ${this.sdk.accessKey}`,
|
|
},
|
|
};
|
|
log.debug('deactivate all tokens %s %o', url, data);
|
|
const response = await fetch(url, data);
|
|
if (!response.ok) {
|
|
log.error(
|
|
'Console communication problem when deactivating otel ingestion tokens: %d %s',
|
|
response.status,
|
|
await response.text()
|
|
);
|
|
return;
|
|
}
|
|
await response.text();
|
|
}
|
|
|
|
async deactivateOtelIngestionToken() {
|
|
const url = `${this.otelIngestionUrl}/token?token=${await this.deferredOtelIngestionToken}`;
|
|
const data = {
|
|
method: 'DELETE',
|
|
headers: {
|
|
Authorization: `Bearer ${this.sdk.accessKey}`,
|
|
},
|
|
};
|
|
log.debug('deactivate token %s %o', url, data);
|
|
const response = await fetch(url, data);
|
|
if (!response.ok) {
|
|
log.error(
|
|
'Console communication problem when deactivating otel ingestion token: %d %s',
|
|
response.status,
|
|
await response.text()
|
|
);
|
|
return;
|
|
}
|
|
await response.text();
|
|
}
|
|
|
|
overrideSettings({ otelIngestionToken, extensionLayerVersionPostfix, service, stage }) {
|
|
Object.defineProperties(this, {
|
|
deferredOtelIngestionToken: d(Promise.resolve(otelIngestionToken)),
|
|
extensionLayerVersionPostfix: d(extensionLayerVersionPostfix),
|
|
service: d('cew', service),
|
|
stage: d('cew', stage),
|
|
});
|
|
}
|
|
|
|
compileOtelExtensionLayer() {
|
|
log.debug('compile extension resource (%s)', this.extensionLayerName);
|
|
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
|
|
this.provider.naming.getConsoleExtensionLayerLogicalId()
|
|
] = {
|
|
Type: 'AWS::Lambda::LayerVersion',
|
|
Properties: {
|
|
Content: {
|
|
S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
|
|
S3Key: `${this.serverless.service.package.artifactsS3KeyDirname}/${this.extensionLayerFilename}`,
|
|
},
|
|
LayerName: this.extensionLayerName,
|
|
},
|
|
};
|
|
}
|
|
|
|
async packageOtelExtensionLayer() {
|
|
log.debug('copy extension file (%s) to package directory', this.extensionLayerFilename);
|
|
await fsp.copyFile(
|
|
require.resolve('@serverless/aws-lambda-otel-extension-dist/extension.zip'),
|
|
path.join(this.serverless.serviceDir, '.serverless', this.extensionLayerFilename)
|
|
);
|
|
}
|
|
|
|
async ensureLayerVersion() {
|
|
let layerVersionMeta = (
|
|
await this.provider.request('Lambda', 'listLayerVersions', {
|
|
LayerName: this.extensionLayerName,
|
|
})
|
|
).LayerVersions[0];
|
|
if (!layerVersionMeta) {
|
|
log.debug('publish layer version (%s)', this.extensionLayerName);
|
|
await this.uploadOtelExtensionLayer({ readFromTheSource: true });
|
|
await setBucketName.call(this);
|
|
await this.provider.request('Lambda', 'publishLayerVersion', {
|
|
LayerName: this.extensionLayerName,
|
|
Content: {
|
|
S3Bucket: this.bucketName,
|
|
S3Key: `${this.serverless.service.package.artifactsS3KeyDirname}/${this.extensionLayerFilename}`,
|
|
},
|
|
});
|
|
layerVersionMeta = (
|
|
await this.provider.request('Lambda', 'listLayerVersions', {
|
|
LayerName: this.extensionLayerName,
|
|
})
|
|
).LayerVersions[0];
|
|
} else {
|
|
log.debug('layer version already published (%s)', this.extensionLayerName);
|
|
}
|
|
log.debug('retrieved layer version arn (%s)', layerVersionMeta.LayerVersionArn);
|
|
return layerVersionMeta.LayerVersionArn;
|
|
}
|
|
|
|
async uploadOtelExtensionLayer(options = {}) {
|
|
log.debug(
|
|
'check if extension file (%s) is already uploaded to S3',
|
|
this.extensionLayerFilename
|
|
);
|
|
await setBucketName.call(this);
|
|
try {
|
|
await this.provider.request('S3', 'headObject', {
|
|
Bucket: this.bucketName,
|
|
Key: `${this.serverless.service.package.artifactsS3KeyDirname}/${this.extensionLayerFilename}`,
|
|
});
|
|
// Extension layer is already available at S3, skip
|
|
log.debug('extension file is already uploaded to S3');
|
|
return;
|
|
} catch (error) {
|
|
if (error.code !== 'AWS_S3_HEAD_OBJECT_NOT_FOUND') throw error;
|
|
const filename = options.readFromTheSource
|
|
? require.resolve('@serverless/aws-lambda-otel-extension-dist/extension.zip')
|
|
: path.join(this.packagePath, this.extensionLayerFilename);
|
|
const stats = await fsp.stat(filename);
|
|
log.info(`Uploading console otel extension file to S3 (${filesize(stats.size)})`);
|
|
await uploadZipFile.call(this, {
|
|
filename,
|
|
s3KeyDirname: this.serverless.service.package.artifactsS3KeyDirname,
|
|
basename: this.extensionLayerFilename,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Object.defineProperties(
|
|
Console.prototype,
|
|
lazy({
|
|
deferredFunctionEnvironmentVariables: d(function () {
|
|
return this.deferredOtelIngestionToken.then((otelIngestionToken) => {
|
|
const result = {
|
|
SLS_OTEL_REPORT_REQUEST_HEADERS: `serverless_token=${otelIngestionToken}`,
|
|
SLS_OTEL_REPORT_METRICS_URL: `${this.otelIngestionUrl}/v1/metrics`,
|
|
SLS_OTEL_REPORT_TRACES_URL: `${this.otelIngestionUrl}/v1/traces`,
|
|
OTEL_RESOURCE_ATTRIBUTES: `sls_service_name=${this.service},sls_stage=${this.stage},sls_org_id=${this.orgId}`,
|
|
AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-extension/otel-handler',
|
|
};
|
|
if (process.env.SLS_OTEL_LAYER_DEV_BUILD) result.DEBUG_SLS_OTEL_LAYER = '1';
|
|
return result;
|
|
});
|
|
}),
|
|
deferredOtelIngestionToken: d(function () {
|
|
return this.createOtelIngestionToken();
|
|
}),
|
|
extensionLayerName: d(function () {
|
|
return `sls-console-otel-extension-${this.extensionLayerVersionPostfix.replace(/\./g, '-')}`;
|
|
}),
|
|
extensionLayerFilename: d(function () {
|
|
return `sls-otel.${this.extensionLayerVersionPostfix}.zip`;
|
|
}),
|
|
extensionLayerVersionPostfix: d(() => {
|
|
if (process.env.SLS_OTEL_LAYER_VERSION) return process.env.SLS_OTEL_LAYER_VERSION;
|
|
const installedVersion =
|
|
require('@serverless/aws-lambda-otel-extension-dist/package').version;
|
|
if (installedVersion) return installedVersion;
|
|
// If we link to package in repository, then there's no version exposed
|
|
return (Date.now() - devVersionTimeBase).toString(32);
|
|
}),
|
|
})
|
|
);
|
|
|
|
module.exports = Console;
|