@hyperdrive.bot/serverless-arn-prefixer
Version:
A serverless plugin that injects custom ARN properties for building AWS resource ARNs
162 lines (137 loc) • 6.72 kB
JavaScript
;
const AWS = require('aws-sdk');
class ArnPrefixerPlugin {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
// Initialize arn properties early
this.initializeArnProperties();
// Set up variable resolvers
this.configurationVariablesSources = {
arn: {
resolve: async ({ address, params, resolveConfigurationProperty, options }) => {
return this.resolveArnVariable(address);
}
}
};
this.hooks = {
'before:package:initialize': this.logInjectedProperties.bind(this)
};
}
initializeArnProperties() {
// Get basic properties from serverless configuration
const service = this.serverless.service.service;
const provider = this.serverless.service.provider || {};
const stage = provider.stage || 'dev';
const region = provider.region || 'us-east-1';
// Get account ID synchronously first from env var
let accountId = process.env.AWS_ACCOUNT_ID;
// If no account ID from env, we'll mark it for async resolution
if (!accountId) {
accountId = 'ASYNC_RESOLVE';
}
// Build prefix values
const prefix = `${service}-${stage}`;
const regionalPrefix = `${region}-${service}-${stage}`;
const globalPrefix = accountId === 'ASYNC_RESOLVE'
? 'ASYNC_RESOLVE'
: `${accountId}-${region}-${service}-${stage}`;
// Build ARN base values for AWS services
const arnBase = 'arn:aws';
const accountRegionBase = accountId === 'ASYNC_RESOLVE'
? 'ASYNC_RESOLVE'
: `${accountId}:${region}`;
// Store the properties for resolution
this.arnProperties = {
accountId,
stage,
service,
region,
prefix,
regionalPrefix,
globalPrefix,
// ARN builders
arnBase,
accountRegion: accountRegionBase,
// Service-specific ARN bases
dynamodb: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:dynamodb:${accountRegionBase}`,
s3: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:s3:${accountRegionBase}`,
lambda: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:lambda:${accountRegionBase}`,
iam: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:iam:${accountRegionBase}`,
sns: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:sns:${accountRegionBase}`,
sqs: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:sqs:${accountRegionBase}`,
apigateway: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:apigateway:${accountRegionBase}`,
events: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:events:${accountRegionBase}`,
logs: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:logs:${accountRegionBase}`,
kinesis: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:kinesis:${accountRegionBase}`,
firehose: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:firehose:${accountRegionBase}`,
stepfunctions: accountId === 'ASYNC_RESOLVE' ? 'ASYNC_RESOLVE' : `${arnBase}:states:${accountRegionBase}`
};
// Also inject directly into custom for backward compatibility
if (!this.serverless.service.custom) {
this.serverless.service.custom = {};
}
this.serverless.service.custom.arn = this.arnProperties;
}
async resolveArnVariable(address) {
// First check if the property exists
if (!this.arnProperties.hasOwnProperty(address)) {
throw new Error(`Unknown arn property: ${address}`);
}
// If accountId needs async resolution, do it now
if (this.arnProperties.accountId === 'ASYNC_RESOLVE') {
try {
const provider = this.serverless.service.provider || {};
const stage = provider.stage || 'dev';
const region = provider.region || 'us-east-1';
const sts = new AWS.STS({
region: region,
credentials: provider.credentials
});
const identity = await sts.getCallerIdentity().promise();
const accountId = identity.Account;
// Update all properties that depend on accountId
this.arnProperties.accountId = accountId;
this.arnProperties.globalPrefix = `${accountId}-${region}-${this.arnProperties.service}-${stage}`;
// Update ARN base properties
const accountRegionBase = `${accountId}:${region}`;
this.arnProperties.accountRegion = accountRegionBase;
this.arnProperties.dynamodb = `arn:aws:dynamodb:${accountRegionBase}`;
this.arnProperties.s3 = `arn:aws:s3:${accountRegionBase}`;
this.arnProperties.lambda = `arn:aws:lambda:${accountRegionBase}`;
this.arnProperties.iam = `arn:aws:iam:${accountRegionBase}`;
this.arnProperties.sns = `arn:aws:sns:${accountRegionBase}`;
this.arnProperties.sqs = `arn:aws:sqs:${accountRegionBase}`;
this.arnProperties.apigateway = `arn:aws:apigateway:${accountRegionBase}`;
this.arnProperties.events = `arn:aws:events:${accountRegionBase}`;
this.arnProperties.logs = `arn:aws:logs:${accountRegionBase}`;
this.arnProperties.kinesis = `arn:aws:kinesis:${accountRegionBase}`;
this.arnProperties.firehose = `arn:aws:firehose:${accountRegionBase}`;
this.arnProperties.stepfunctions = `arn:aws:states:${accountRegionBase}`;
// Update the custom object too
this.serverless.service.custom.arn = this.arnProperties;
} catch (error) {
const errorMessage = `ARN Prefixer Plugin Error: Cannot determine AWS Account ID.
Please either:
1. Set the AWS_ACCOUNT_ID environment variable, or
2. Ensure you have valid AWS credentials configured
AWS STS Error: ${error.message}`;
this.serverless.cli.log(errorMessage);
throw new Error(errorMessage);
}
}
// Return the requested property
return { value: this.arnProperties[address] };
}
logInjectedProperties() {
this.serverless.cli.log('ARN Prefixer: Injected custom.arn properties');
this.serverless.cli.log(` - accountId: ${this.arnProperties.accountId}`);
this.serverless.cli.log(` - stage: ${this.arnProperties.stage}`);
this.serverless.cli.log(` - service: ${this.arnProperties.service}`);
this.serverless.cli.log(` - region: ${this.arnProperties.region}`);
this.serverless.cli.log(` - prefix: ${this.arnProperties.prefix}`);
this.serverless.cli.log(` - regionalPrefix: ${this.arnProperties.regionalPrefix}`);
this.serverless.cli.log(` - globalPrefix: ${this.arnProperties.globalPrefix}`);
}
}
module.exports = ArnPrefixerPlugin;