packages/aws-cdk-lib/aws-sns/lib/topic.ts (174 lines of code) (raw):
import { Construct } from 'constructs';
import { CfnTopic } from './sns.generated';
import { ITopic, TopicBase } from './topic-base';
import { IRole } from '../../aws-iam';
import { IKey, Key } from '../../aws-kms';
import { ArnFormat, Lazy, Names, Stack, Token } from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
/**
* Properties for a new SNS topic
*/
export interface TopicProps {
/**
* A developer-defined string that can be used to identify this SNS topic.
*
* The display name must be maximum 100 characters long, including hyphens (-),
* underscores (_), spaces, and tabs.
*
* @default None
*/
readonly displayName?: string;
/**
* A name for the topic.
*
* If you don't specify a name, AWS CloudFormation generates a unique
* physical ID and uses that ID for the topic name. For more information,
* see Name Type.
*
* @default Generated name
*/
readonly topicName?: string;
/**
* A KMS Key, either managed by this CDK app, or imported.
*
* @default None
*/
readonly masterKey?: IKey;
/**
* Enables content-based deduplication for FIFO topics.
*
* @default None
*/
readonly contentBasedDeduplication?: boolean;
/**
* Set to true to create a FIFO topic.
*
* @default None
*/
readonly fifo?: boolean;
/**
* The list of delivery status logging configurations for the topic.
*
* @see https://docs.aws.amazon.com/sns/latest/dg/sns-topic-attributes.html.
*
* @default None
*/
readonly loggingConfigs?: LoggingConfig[];
/**
* The number of days Amazon SNS retains messages.
*
* It can only be set for FIFO topics.
*
* @see https://docs.aws.amazon.com/sns/latest/dg/fifo-message-archiving-replay.html
*
* @default - do not archive messages
*/
readonly messageRetentionPeriodInDays?: number;
/**
* Adds a statement to enforce encryption of data in transit when publishing to the topic.
*
* @see https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit.
*
* @default false
*/
readonly enforceSSL?: boolean;
/**
* The signature version corresponds to the hashing algorithm used while creating the signature of the notifications,
* subscription confirmations, or unsubscribe confirmation messages sent by Amazon SNS.
*
* @see https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html.
*
* @default 1
*/
readonly signatureVersion?: string;
/**
* Tracing mode of an Amazon SNS topic.
*
* @see https://docs.aws.amazon.com/sns/latest/dg/sns-active-tracing.html
*
* @default TracingConfig.PASS_THROUGH
*/
readonly tracingConfig?: TracingConfig;
/**
* Specifies the throughput quota and deduplication behavior to apply for the FIFO topic.
*
* You can only set this property when `fifo` is `true`.
*
* @default undefined - SNS default setting is FifoThroughputScope.TOPIC
*/
readonly fifoThroughputScope?: FifoThroughputScope;
}
/**
* The throughput quota and deduplication behavior to apply for the FIFO topic.
*/
export enum FifoThroughputScope {
/**
* Topic scope
* - Throughput: 3000 messages per second and a bandwidth of 20MB per second.
* - Deduplication: Message deduplication is verified on the entire FIFO topic.
*/
TOPIC = 'Topic',
/**
* Message group scope
* - Throughput: Maximum regional limits.
* - Deduplication: Message deduplication is only verified within a message group.
*/
MESSAGE_GROUP = 'MessageGroup',
}
/**
* A logging configuration for delivery status of messages sent from SNS topic to subscribed endpoints.
*
* @see https://docs.aws.amazon.com/sns/latest/dg/sns-topic-attributes.html.
*/
export interface LoggingConfig {
/**
* Indicates one of the supported protocols for the SNS topic.
*/
readonly protocol: LoggingProtocol;
/**
* The IAM role to be used when logging failed message deliveries in Amazon CloudWatch.
*
* @default None
*/
readonly failureFeedbackRole?: IRole;
/**
* The IAM role to be used when logging successful message deliveries in Amazon CloudWatch.
*
* @default None
*/
readonly successFeedbackRole?: IRole;
/**
* The percentage of successful message deliveries to be logged in Amazon CloudWatch.
*
* Valid values are integer between 0-100
*
* @default None
*/
readonly successFeedbackSampleRate?: number;
}
/**
* The type of supported protocol for delivery status logging.
*/
export enum LoggingProtocol {
/**
* HTTP
*/
HTTP = 'http/s',
/**
* Amazon Simple Queue Service
*/
SQS = 'sqs',
/**
* AWS Lambda
*/
LAMBDA = 'lambda',
/**
* Amazon Data Firehose
*/
FIREHOSE = 'firehose',
/**
* Platform application endpoint
*/
APPLICATION = 'application',
}
/**
* The tracing mode of an Amazon SNS topic
*/
export enum TracingConfig {
/**
* The mode that topic passes trace headers received from the Amazon SNS publisher to its subscription.
*/
PASS_THROUGH = 'PassThrough',
/**
* The mode that Amazon SNS vend X-Ray segment data to topic owner account if the sampled flag in the tracing header is true.
*/
ACTIVE = 'Active',
}
/**
* Represents an SNS topic defined outside of this stack.
*/
export interface TopicAttributes {
/**
* The ARN of the SNS topic.
*/
readonly topicArn: string;
/**
* KMS encryption key, if this topic is server-side encrypted by a KMS key.
*
* @default - None
*/
readonly keyArn?: string;
/**
* Whether content-based deduplication is enabled.
* Only applicable for FIFO topics.
*
* @default false
*/
readonly contentBasedDeduplication?: boolean;
}
/**
* A new SNS topic
*/
export class Topic extends TopicBase {
/**
* Import an existing SNS topic provided an ARN
*
* @param scope The parent creating construct
* @param id The construct's name
* @param topicArn topic ARN (i.e. arn:aws:sns:us-east-2:444455556666:MyTopic)
*/
public static fromTopicArn(scope: Construct, id: string, topicArn: string): ITopic {
return Topic.fromTopicAttributes(scope, id, { topicArn });
}
/**
* Import an existing SNS topic provided a topic attributes
*
* @param scope The parent creating construct
* @param id The construct's name
* @param attrs the attributes of the topic to import
*/
public static fromTopicAttributes(scope: Construct, id: string, attrs: TopicAttributes): ITopic {
const topicName = Stack.of(scope).splitArn(attrs.topicArn, ArnFormat.NO_RESOURCE_NAME).resource;
const fifo = topicName.endsWith('.fifo');
if (attrs.contentBasedDeduplication && !fifo) {
throw new ValidationError('Cannot import topic; contentBasedDeduplication is only available for FIFO SNS topics.', scope);
}
class Import extends TopicBase {
public readonly topicArn = attrs.topicArn;
public readonly topicName = topicName;
public readonly masterKey = attrs.keyArn
? Key.fromKeyArn(this, 'Key', attrs.keyArn)
: undefined;
public readonly fifo = fifo;
public readonly contentBasedDeduplication = attrs.contentBasedDeduplication || false;
protected autoCreatePolicy: boolean = false;
}
return new Import(scope, id, {
environmentFromArn: attrs.topicArn,
});
}
public readonly topicArn: string;
public readonly topicName: string;
public readonly masterKey?: IKey;
public readonly contentBasedDeduplication: boolean;
public readonly fifo: boolean;
protected readonly autoCreatePolicy: boolean = true;
private readonly loggingConfigs: LoggingConfig[] = [];
constructor(scope: Construct, id: string, props: TopicProps = {}) {
super(scope, id, {
physicalName: props.topicName,
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
this.enforceSSL = props.enforceSSL;
if (props.contentBasedDeduplication && !props.fifo) {
throw new ValidationError('Content based deduplication can only be enabled for FIFO SNS topics.', this);
}
if (props.messageRetentionPeriodInDays && !props.fifo) {
throw new ValidationError('`messageRetentionPeriodInDays` is only valid for FIFO SNS topics.', this);
}
if (props.fifoThroughputScope && !props.fifo) {
throw new ValidationError('`fifoThroughputScope` can only be set for FIFO SNS topics.', this);
}
if (
props.messageRetentionPeriodInDays !== undefined
&& !Token.isUnresolved(props.messageRetentionPeriodInDays)
&& (!Number.isInteger(props.messageRetentionPeriodInDays) || props.messageRetentionPeriodInDays > 365 || props.messageRetentionPeriodInDays < 1)
) {
throw new ValidationError('`messageRetentionPeriodInDays` must be an integer between 1 and 365', this);
}
if (props.loggingConfigs) {
props.loggingConfigs.forEach(c => this.addLoggingConfig(c));
}
let cfnTopicName: string;
if (props.fifo && props.topicName && !props.topicName.endsWith('.fifo')) {
cfnTopicName = this.physicalName + '.fifo';
} else if (props.fifo && !props.topicName) {
// Max length allowed by CloudFormation is 256, we subtract 5 to allow for ".fifo" suffix
const prefixName = Names.uniqueResourceName(this, {
maxLength: 256 - 5,
separator: '-',
});
cfnTopicName = `${prefixName}.fifo`;
} else {
cfnTopicName = this.physicalName;
}
if (
props.signatureVersion &&
!Token.isUnresolved(props.signatureVersion) &&
props.signatureVersion !== '1' &&
props.signatureVersion !== '2'
) {
throw new ValidationError(`signatureVersion must be "1" or "2", received: "${props.signatureVersion}"`, this);
}
if (props.displayName && !Token.isUnresolved(props.displayName) && props.displayName.length > 100) {
throw new ValidationError(`displayName must be less than or equal to 100 characters, got ${props.displayName.length}`, this);
}
const resource = new CfnTopic(this, 'Resource', {
archivePolicy: props.messageRetentionPeriodInDays ? {
MessageRetentionPeriod: props.messageRetentionPeriodInDays,
} : undefined,
displayName: props.displayName,
topicName: cfnTopicName,
kmsMasterKeyId: props.masterKey && props.masterKey.keyArn,
contentBasedDeduplication: props.contentBasedDeduplication,
fifoTopic: props.fifo,
signatureVersion: props.signatureVersion,
deliveryStatusLogging: Lazy.any({ produce: () => this.renderLoggingConfigs() }, { omitEmptyArray: true }),
tracingConfig: props.tracingConfig,
fifoThroughputScope: props.fifoThroughputScope,
});
this.topicArn = this.getResourceArnAttribute(resource.ref, {
service: 'sns',
resource: this.physicalName,
});
this.topicName = this.getResourceNameAttribute(resource.attrTopicName);
this.masterKey = props.masterKey;
this.fifo = props.fifo || false;
this.contentBasedDeduplication = props.contentBasedDeduplication || false;
if (this.enforceSSL) {
this.addSSLPolicy();
}
}
private renderLoggingConfigs(): CfnTopic.LoggingConfigProperty[] {
const renderLoggingConfig = (spec: LoggingConfig): CfnTopic.LoggingConfigProperty => {
if (spec.successFeedbackSampleRate !== undefined) {
const rate = spec.successFeedbackSampleRate;
if (!Number.isInteger(rate) || rate < 0 || rate > 100) {
throw new ValidationError('Success feedback sample rate must be an integer between 0 and 100', this);
}
}
return {
protocol: spec.protocol,
failureFeedbackRoleArn: spec.failureFeedbackRole?.roleArn,
successFeedbackRoleArn: spec.successFeedbackRole?.roleArn,
successFeedbackSampleRate: spec.successFeedbackSampleRate?.toString(),
};
};
return this.loggingConfigs.map(renderLoggingConfig);
}
/**
* Adds a delivery status logging configuration to the topic.
*/
@MethodMetadata()
public addLoggingConfig(config: LoggingConfig) {
this.loggingConfigs.push(config);
}
}