diff --git a/packages/@aws-cdk/aws-ivs-alpha/README.md b/packages/@aws-cdk/aws-ivs-alpha/README.md index 5af6031301a0b..f8ea242c9990d 100644 --- a/packages/@aws-cdk/aws-ivs-alpha/README.md +++ b/packages/@aws-cdk/aws-ivs-alpha/README.md @@ -115,3 +115,90 @@ const myChannel = new ivs.Channel(this, 'Channel', { authorized: true, // default value is false }); ``` + +## Recording Configurations + +An Amazon IVS Recording Configuration stores settings that specify how a channel's live streams should be recorded. +You can configure video quality, thumbnail generation, and where recordings are stored in Amazon S3. + +For more information about IVS recording, see [IVS Auto-Record to Amazon S3 | Low-Latency Streaming](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html). + +You can create a recording configuration: + +```ts +// create an S3 bucket for storing recordings +const recordingBucket = new s3.Bucket(this, 'RecordingBucket'); + +// create a basic recording configuration +const recordingConfiguration = new ivs.RecordingConfiguration(this, 'RecordingConfiguration', { + bucket: recordingBucket, +}); +``` + +### Renditions of a Recording + +When you stream content to an Amazon IVS channel, auto-record-to-s3 uses the source video to generate multiple renditions. + +For more information, see [Discovering the Renditions of a Recording](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html#r2s3-recording-renditions). + +```ts +declare const recordingBucket: s3.Bucket; + +const recordingConfiguration= new ivs.RecordingConfiguration(this, 'RecordingConfiguration', { + bucket: recordingBucket, + + // set rendition configuration + renditionConfiguration: ivs.RenditionConfiguration.custom([ivs.Resolution.HD, ivs.Resolution.SD]), +}); +``` + +### Thumbnail Generation + +You can enable or disable the recording of thumbnails for a live session and modify the interval at which thumbnails are generated for the live session. + +Thumbnail intervals may range from 1 second to 60 seconds; by default, thumbnail recording is enabled, at an interval of 60 seconds. + +For more information, see [Thumbnails](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html#r2s3-thumbnails). + +```ts +declare const recordingBucket: s3.Bucket; + +const recordingConfiguration = new ivs.RecordingConfiguration(this, 'RecordingConfiguration', { + bucket: recordingBucket, + + // set thumbnail settings + thumbnailConfiguration: ivs.ThumbnailConfiguration.interval(ivs.Resolution.HD, [ivs.Storage.LATEST, ivs.Storage.SEQUENTIAL], Duration.seconds(30)), +}); +``` + +### Merge Fragmented Streams + +The `recordingReconnectWindow` property allows you to specify a window of time (in seconds) during which, if your stream is interrupted and a new stream is started, Amazon IVS tries to record to the same S3 prefix as the previous stream. + +In other words, if a broadcast disconnects and then reconnects within the specified interval, the multiple streams are considered a single broadcast and merged together. + +For more information, see [Merge Fragmented Streams](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html#r2s3-merge-fragmented-streams). + +```ts +declare const recordingBucket: s3.Bucket; + +const recordingConfiguration= new ivs.RecordingConfiguration(this, 'RecordingConfiguration', { + bucket: recordingBucket, + + // set recording reconnect window + recordingReconnectWindow: Duration.seconds(60), +}); +``` + +### Attaching Recording Configuration to a Channel + +To enable recording for a channel, specify the recording configuration when creating the channel: + +```ts +declare const recordingConfiguration: ivs.RecordingConfiguration; + +const channel = new ivs.Channel(this, 'Channel', { + // set recording configuration + recordingConfiguration: recordingConfiguration, +}); +``` diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/channel.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/channel.ts index 07eda0279f5ca..8bb48712a7375 100644 --- a/packages/@aws-cdk/aws-ivs-alpha/lib/channel.ts +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/channel.ts @@ -3,6 +3,7 @@ import { Lazy, Names } from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; import { CfnChannel } from 'aws-cdk-lib/aws-ivs'; import { StreamKey } from './stream-key'; +import { IRecordingConfiguration } from './recording-configuration'; /** * Represents an IVS Channel @@ -153,6 +154,13 @@ export interface ChannelProps { * @default - Preset.HIGHER_BANDWIDTH_DELIVERY if channelType is ADVANCED_SD or ADVANCED_HD, none otherwise */ readonly preset?: Preset; + + /** + * A recording configuration for the channel. + * + * @default - recording is disabled + */ + readonly recordingConfiguration?: IRecordingConfiguration; } /** @@ -223,6 +231,7 @@ export class Channel extends ChannelBase { name: this.physicalName, type: props.type, preset, + recordingConfigurationArn: props.recordingConfiguration?.recordingConfigurationArn, }); this.channelArn = resource.attrArn; diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/index.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/index.ts index c873eb9662c7b..6a5a36b273cd6 100644 --- a/packages/@aws-cdk/aws-ivs-alpha/lib/index.ts +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/index.ts @@ -1,5 +1,9 @@ export * from './channel'; export * from './playback-key-pair'; +export * from './recording-configuration'; +export * from './rendition-configuration'; export * from './stream-key'; +export * from './thumbnail-configuration'; +export * from './util'; // AWS::IVS CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/recording-configuration.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/recording-configuration.ts new file mode 100644 index 0000000000000..8864e958233b0 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/recording-configuration.ts @@ -0,0 +1,210 @@ +import { CfnRecordingConfiguration } from 'aws-cdk-lib/aws-ivs'; +import { IBucket } from 'aws-cdk-lib/aws-s3'; +import { Duration, Fn, IResource, Resource, Stack, Token } from 'aws-cdk-lib/core'; +import { Construct } from 'constructs'; +import { RenditionConfiguration } from './rendition-configuration'; +import { ThumbnailConfiguration } from './thumbnail-configuration'; + +/** + * Properties of the IVS Recording configuration + */ +export interface RecordingConfigurationProps { + /** + * S3 bucket where recorded videos will be stored. + */ + readonly bucket: IBucket; + + /** + * The name of the Recording configuration. + * The value does not need to be unique. + * + * @default - auto generate + */ + readonly recordingConfigurationName?: string; + + /** + * If a broadcast disconnects and then reconnects within the specified interval, + * the multiple streams will be considered a single broadcast and merged together. + * + * `recordingReconnectWindow` must be between 0 and 300 seconds + * + * @default - 0 seconds (means disabled) + */ + readonly recordingReconnectWindow?: Duration; + + /** + * A rendition configuration describes which renditions should be recorded for a stream. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-renditionconfiguration.html + * + * @default - no rendition configuration + */ + readonly renditionConfiguration?: RenditionConfiguration; + + /** + * A thumbnail configuration enables/disables the recording of thumbnails for a live session and controls the interval at which thumbnails are generated for the live session. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-thumbnailconfiguration.html + * + * @default - no thumbnail configuration + */ + readonly thumbnailConfiguration?:ThumbnailConfiguration; +} + +/** + * Represents the IVS Recording configuration. + */ +export interface IRecordingConfiguration extends IResource { + /** + * The ID of the Recording configuration. + * @attribute + */ + readonly recordingConfigurationId: string; + + /** + * The ARN of the Recording configuration. + * @attribute + */ + readonly recordingConfigurationArn: string; +} + +/** + * The IVS Recording configuration + * + * @resource AWS::IVS::RecordingConfiguration + */ +export class RecordingConfiguration extends Resource implements IRecordingConfiguration { + /** + * Imports an IVS Recording Configuration from attributes. + */ + public static fromRecordingConfigurationId(scope: Construct, id: string, + recordingConfigurationId: string): IRecordingConfiguration { + + class Import extends Resource implements IRecordingConfiguration { + public readonly recordingConfigurationId = recordingConfigurationId; + public readonly recordingConfigurationArn = Stack.of(this).formatArn({ + resource: 'recording-configuration', + service: 'ivs', + resourceName: recordingConfigurationId, + }); + } + + return new Import(scope, id); + } + + /** + * Imports an IVS Recording Configuration from its ARN + */ + public static fromArn(scope: Construct, id: string, recordingConfigurationArn: string): IRecordingConfiguration { + const resourceParts = Fn.split('/', recordingConfigurationArn); + + if (!resourceParts || resourceParts.length < 2) { + throw new Error(`Unexpected ARN format: ${recordingConfigurationArn}`); + } + + const recordingConfigurationId = Fn.select(1, resourceParts); + + class Import extends Resource implements IRecordingConfiguration { + public readonly recordingConfigurationId = recordingConfigurationId; + public readonly recordingConfigurationArn = recordingConfigurationArn; + } + + return new Import(scope, id); + } + + /** + * The ID of the Recording configuration. + * @attribute + */ + readonly recordingConfigurationId: string; + + /** + * The ARN of the Recording configuration. + * @attribute + */ + readonly recordingConfigurationArn: string; + + private readonly props: RecordingConfigurationProps; + + public constructor(scope: Construct, id: string, props: RecordingConfigurationProps) { + super(scope, id, { + physicalName: props.recordingConfigurationName, + }); + + this.props = props; + + this.validateRecordingConfigurationName(); + this.validateRecordingReconnectWindowSeconds(); + + const resource = new CfnRecordingConfiguration(this, 'Resource', { + destinationConfiguration: { + s3: { + bucketName: this.props.bucket.bucketName, + }, + }, + name: this.props.recordingConfigurationName, + recordingReconnectWindowSeconds: this.props.recordingReconnectWindow?.toSeconds(), + renditionConfiguration: this._renderRenditionConfiguration(), + thumbnailConfiguration: this._renderThumbnailConfiguration(), + }); + + this.recordingConfigurationId = resource.ref; + this.recordingConfigurationArn = resource.attrArn; + } + + private _renderRenditionConfiguration(): CfnRecordingConfiguration.RenditionConfigurationProperty | undefined { + if (!this.props.renditionConfiguration) { + return; + } + + return { + renditions: this.props.renditionConfiguration.renditions, + renditionSelection: this.props.renditionConfiguration.renditionSelection, + }; + }; + + private _renderThumbnailConfiguration(): CfnRecordingConfiguration.ThumbnailConfigurationProperty | undefined { + if (!this.props.thumbnailConfiguration) { + return; + } + + return { + recordingMode: this.props.thumbnailConfiguration.recordingMode, + resolution: this.props.thumbnailConfiguration.resolution, + storage: this.props.thumbnailConfiguration.storage, + targetIntervalSeconds: this.props.thumbnailConfiguration.targetInterval?.toSeconds(), + }; + }; + + private validateRecordingConfigurationName(): undefined { + const recordingConfigurationName = this.props.recordingConfigurationName; + + if (recordingConfigurationName == undefined || Token.isUnresolved(recordingConfigurationName)) { + return; + } + + if (!/^[a-zA-Z0-9-_]*$/.test(recordingConfigurationName)) { + throw new Error(`\`recordingConfigurationName\` must consist only of alphanumeric characters, hyphens or underbars, got: ${recordingConfigurationName}.`); + } + + if (recordingConfigurationName.length > 128) { + throw new Error(`\`recordingConfigurationName\` must be less than or equal to 128 characters, got: ${recordingConfigurationName.length} characters.`); + } + }; + + private validateRecordingReconnectWindowSeconds(): undefined { + const recordingReconnectWindow = this.props.recordingReconnectWindow; + + if (recordingReconnectWindow === undefined || Token.isUnresolved(recordingReconnectWindow)) { + return; + } + + if (0 < recordingReconnectWindow.toMilliseconds() && recordingReconnectWindow.toMilliseconds() < Duration.seconds(1).toMilliseconds()) { + throw new Error(`\`recordingReconnectWindow\` must be between 0 and 300 seconds, got ${recordingReconnectWindow.toMilliseconds()} milliseconds.`); + } + + if (recordingReconnectWindow.toSeconds() > 300) { + throw new Error(`\`recordingReconnectWindow\` must be between 0 and 300 seconds, got ${recordingReconnectWindow.toSeconds()} seconds.`); + } + }; +} diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/rendition-configuration.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/rendition-configuration.ts new file mode 100644 index 0000000000000..691fff7dec0bf --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/rendition-configuration.ts @@ -0,0 +1,55 @@ +import { Resolution } from './util'; + +/** + * Rendition selection mode. + */ +export enum RenditionSelection { + /** + * Record all available renditions. + */ + ALL = 'ALL', + + /** + * Does not record any video. This option is useful if you just want to record thumbnails. + */ + NONE = 'NONE', + + /** + * Select a subset of video renditions to record. + */ + CUSTOM = 'CUSTOM', +} + +/** + * Rendition configuration for IVS Recording configuration + */ +export class RenditionConfiguration { + /** + * Record all available renditions. + */ + public static all(): RenditionConfiguration { + return new RenditionConfiguration(RenditionSelection.ALL); + } + + /** + * Does not record any video. + */ + public static none(): RenditionConfiguration { + return new RenditionConfiguration(RenditionSelection.NONE); + } + + /** + * Record a subset of video renditions. + * + * @param renditions A list of which renditions are recorded for a stream. + */ + public static custom(renditions: Resolution[]): RenditionConfiguration { + return new RenditionConfiguration(RenditionSelection.CUSTOM, renditions); + } + + /** + * @param renditionSelection The set of renditions are recorded for a stream. + * @param renditions A list of which renditions are recorded for a stream. If you do not specify this property, no resolution is selected. + */ + private constructor(public readonly renditionSelection: RenditionSelection, public readonly renditions?: Resolution[]) { } +} diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/thumbnail-configuration.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/thumbnail-configuration.ts new file mode 100644 index 0000000000000..ba8d060bdf832 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/thumbnail-configuration.ts @@ -0,0 +1,82 @@ +import { Duration, Token } from 'aws-cdk-lib'; +import { Resolution } from './util'; + +/** + * Thumbnail recording mode. + */ +export enum RecordingMode { + /** + * Use INTERVAL to enable the generation of thumbnails for recorded video at a time interval controlled by the TargetIntervalSeconds property. + */ + INTERVAL = 'INTERVAL', + + /** + * Use DISABLED to disable the generation of thumbnails for recorded video. + */ + DISABLED = 'DISABLED', +} + +/** + * The format in which thumbnails are recorded for a stream. + */ +export enum Storage { + /** + * SEQUENTIAL records all generated thumbnails in a serial manner, to the media/thumbnails directory. + */ + SEQUENTIAL = 'SEQUENTIAL', + + /** + * LATEST saves the latest thumbnail in media/thumbnails/latest/thumb.jpg and overwrites it at the interval specified by thumbnailTargetInterval. + */ + LATEST = 'LATEST', +} + +/** + * Thumbnail configuration for IVS Recording configuration + */ +export class ThumbnailConfiguration { + /** + * Disable the generation of thumbnails for recorded video + */ + public static disable(): ThumbnailConfiguration { + return new ThumbnailConfiguration(RecordingMode.DISABLED); + } + + /** + * Enable the generation of thumbnails for recorded video at a time interval. + * + * @param resolution The desired resolution of recorded thumbnails for a stream. If you do not specify this property, same resolution as Input stream is used. + * @param storage The format in which thumbnails are recorded for a stream. If you do not specify this property, `ThumbnailStorage.SEQUENTIAL` is set. + * @param targetInterval The targeted thumbnail-generation interval. If you do not specify this property, `Duration.seconds(60)` is set. + */ + public static interval(resolution?: Resolution, storage?: Storage[], targetInterval?: Duration): ThumbnailConfiguration { + return new ThumbnailConfiguration(RecordingMode.INTERVAL, resolution, storage, targetInterval); + } + + /** + * @param recordingMode Thumbnail recording mode. If you do not specify this property, `ThumbnailRecordingMode.INTERVAL` is set. + * @param resolution The desired resolution of recorded thumbnails for a stream. If you do not specify this property, same resolution as Input stream is used. + * @param storage The format in which thumbnails are recorded for a stream. If you do not specify this property, `ThumbnailStorage.SEQUENTIAL` is set. + * @param targetInterval The targeted thumbnail-generation interval. Must be between 1 and 60 seconds. If you do not specify this property, `Duration.seconds(60)` is set. + */ + private constructor( + public readonly recordingMode?: RecordingMode, + public readonly resolution?: Resolution, + public readonly storage?: Storage[], + public readonly targetInterval?: Duration, + ) { + + if (targetInterval === undefined || Token.isUnresolved(targetInterval)) { + return; + } + + if (targetInterval.toMilliseconds() < Duration.seconds(1).toMilliseconds()) { + throw new Error(`\`targetInterval\` must be between 1 and 60 seconds, got ${targetInterval.toMilliseconds()} milliseconds.`); + } + + if (targetInterval.toSeconds() > 60) { + throw new Error(`\`targetInterval\` must be between 1 and 60 seconds, got ${targetInterval.toSeconds()} seconds.`); + } + } +} + diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/util.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/util.ts new file mode 100644 index 0000000000000..dd6f65b5a4bfc --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/util.ts @@ -0,0 +1,24 @@ +/** + * Resolution for rendition + */ +export enum Resolution { + /** + * Full HD (1080p) + */ + FULL_HD = 'FULL_HD', + + /** + * HD (720p) + */ + HD = 'HD', + + /** + * SD (480p) + */ + SD = 'SD', + + /** + * Lowest resolution + */ + LOWEST_RESOLUTION = 'LOWEST_RESOLUTION', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-ivs-alpha/rosetta/default.ts-fixture index 5cde59056e614..b4e8d3356cfdc 100644 --- a/packages/@aws-cdk/aws-ivs-alpha/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-ivs-alpha/rosetta/default.ts-fixture @@ -2,6 +2,7 @@ import { Duration, Stack } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as ivs from '@aws-cdk/aws-ivs-alpha'; +import * as s3 from 'aws-cdk-lib/aws-s3'; class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js new file mode 100644 index 0000000000000..1002ba018e9fb --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js @@ -0,0 +1 @@ +"use strict";var f=Object.create;var i=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var w=Object.getPrototypeOf,P=Object.prototype.hasOwnProperty;var A=(t,e)=>{for(var o in e)i(t,o,{get:e[o],enumerable:!0})},d=(t,e,o,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of C(e))!P.call(t,s)&&s!==o&&i(t,s,{get:()=>e[s],enumerable:!(r=I(e,s))||r.enumerable});return t};var l=(t,e,o)=>(o=t!=null?f(w(t)):{},d(e||!t||!t.__esModule?i(o,"default",{value:t,enumerable:!0}):o,t)),B=t=>d(i({},"__esModule",{value:!0}),t);var q={};A(q,{autoDeleteHandler:()=>S,handler:()=>H});module.exports=B(q);var h=require("@aws-sdk/client-s3");var y=l(require("https")),m=l(require("url")),a={sendHttpRequest:D,log:T,includeStackTraces:!0,userHandlerIndex:"./index"},p="AWSCDK::CustomResourceProviderFramework::CREATE_FAILED",L="AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID";function R(t){return async(e,o)=>{let r={...e,ResponseURL:"..."};if(a.log(JSON.stringify(r,void 0,2)),e.RequestType==="Delete"&&e.PhysicalResourceId===p){a.log("ignoring DELETE event caused by a failed CREATE event"),await u("SUCCESS",e);return}try{let s=await t(r,o),n=k(e,s);await u("SUCCESS",n)}catch(s){let n={...e,Reason:a.includeStackTraces?s.stack:s.message};n.PhysicalResourceId||(e.RequestType==="Create"?(a.log("CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored"),n.PhysicalResourceId=p):a.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(e)}`)),await u("FAILED",n)}}}function k(t,e={}){let o=e.PhysicalResourceId??t.PhysicalResourceId??t.RequestId;if(t.RequestType==="Delete"&&o!==t.PhysicalResourceId)throw new Error(`DELETE: cannot change the physical resource ID from "${t.PhysicalResourceId}" to "${e.PhysicalResourceId}" during deletion`);return{...t,...e,PhysicalResourceId:o}}async function u(t,e){let o={Status:t,Reason:e.Reason??t,StackId:e.StackId,RequestId:e.RequestId,PhysicalResourceId:e.PhysicalResourceId||L,LogicalResourceId:e.LogicalResourceId,NoEcho:e.NoEcho,Data:e.Data},r=m.parse(e.ResponseURL),s=`${r.protocol}//${r.hostname}/${r.pathname}?***`;a.log("submit response to cloudformation",s,o);let n=JSON.stringify(o),E={hostname:r.hostname,path:r.path,method:"PUT",headers:{"content-type":"","content-length":Buffer.byteLength(n,"utf8")}};await O({attempts:5,sleep:1e3},a.sendHttpRequest)(E,n)}async function D(t,e){return new Promise((o,r)=>{try{let s=y.request(t,n=>{n.resume(),!n.statusCode||n.statusCode>=400?r(new Error(`Unsuccessful HTTP response: ${n.statusCode}`)):o()});s.on("error",r),s.write(e),s.end()}catch(s){r(s)}})}function T(t,...e){console.log(t,...e)}function O(t,e){return async(...o)=>{let r=t.attempts,s=t.sleep;for(;;)try{return await e(...o)}catch(n){if(r--<=0)throw n;await b(Math.floor(Math.random()*s)),s*=2}}}async function b(t){return new Promise(e=>setTimeout(e,t))}var g="aws-cdk:auto-delete-objects",x=JSON.stringify({Version:"2012-10-17",Statement:[]}),c=new h.S3({}),H=R(S);async function S(t){switch(t.RequestType){case"Create":return;case"Update":return{PhysicalResourceId:(await F(t)).PhysicalResourceId};case"Delete":return N(t.ResourceProperties?.BucketName)}}async function F(t){let e=t,o=e.OldResourceProperties?.BucketName;return{PhysicalResourceId:e.ResourceProperties?.BucketName??o}}async function _(t){try{let e=(await c.getBucketPolicy({Bucket:t}))?.Policy??x,o=JSON.parse(e);o.Statement.push({Principal:"*",Effect:"Deny",Action:["s3:PutObject"],Resource:[`arn:aws:s3:::${t}/*`]}),await c.putBucketPolicy({Bucket:t,Policy:JSON.stringify(o)})}catch(e){if(e.name==="NoSuchBucket")throw e;console.log(`Could not set new object deny policy on bucket '${t}' prior to deletion.`)}}async function U(t){let e;do{e=await c.listObjectVersions({Bucket:t});let o=[...e.Versions??[],...e.DeleteMarkers??[]];if(o.length===0)return;let r=o.map(s=>({Key:s.Key,VersionId:s.VersionId}));await c.deleteObjects({Bucket:t,Delete:{Objects:r}})}while(e?.IsTruncated)}async function N(t){if(!t)throw new Error("No BucketName was provided.");try{if(!await W(t)){console.log(`Bucket does not have '${g}' tag, skipping cleaning.`);return}await _(t),await U(t)}catch(e){if(e.name==="NoSuchBucket"){console.log(`Bucket '${t}' does not exist.`);return}throw e}}async function W(t){return(await c.getBucketTagging({Bucket:t})).TagSet?.some(o=>o.Key===g&&o.Value==="true")}0&&(module.exports={autoDeleteHandler,handler}); diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.assets.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.assets.json new file mode 100644 index 0000000000000..414a84517ac66 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.assets.json @@ -0,0 +1,32 @@ +{ + "version": "38.0.1", + "files": { + "44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61": { + "source": { + "path": "asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "2651be7888f4c0925451bcaab35c5758d4e6cf8b94f1952c1b31f7d99057da7a": { + "source": { + "path": "aws-cdk-ivs-recording-configuration-test.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "2651be7888f4c0925451bcaab35c5758d4e6cf8b94f1952c1b31f7d99057da7a.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.template.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.template.json new file mode 100644 index 0000000000000..a67c3b3a34e6b --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.template.json @@ -0,0 +1,357 @@ +{ + "Resources": { + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "Properties": { + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "BucketPolicyE9A3008A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Bucket83908E77" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:List*", + "s3:PutBucketPolicy" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "BucketAutoDeleteObjectsCustomResourceBAFD23C2": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "Bucket83908E77" + } + }, + "DependsOn": [ + "BucketPolicyE9A3008A" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": { + "Fn::FindInMap": [ + "LatestNodeRuntimeMap", + { + "Ref": "AWS::Region" + }, + "value" + ] + }, + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "Bucket83908E77" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "RecordingConfigurationA528CBBF": { + "Type": "AWS::IVS::RecordingConfiguration", + "Properties": { + "DestinationConfiguration": { + "S3": { + "BucketName": { + "Ref": "Bucket83908E77" + } + } + }, + "Name": "my-recording-configuration", + "RecordingReconnectWindowSeconds": 10, + "RenditionConfiguration": { + "RenditionSelection": "CUSTOM", + "Renditions": [ + "FULL_HD", + "HD", + "SD", + "LOWEST_RESOLUTION" + ] + }, + "ThumbnailConfiguration": { + "RecordingMode": "INTERVAL", + "Resolution": "FULL_HD", + "Storage": [ + "LATEST", + "SEQUENTIAL" + ], + "TargetIntervalSeconds": 30 + } + } + }, + "Channel4048F119": { + "Type": "AWS::IVS::Channel", + "Properties": { + "Name": "aws-cdk-ivs-recording-configuration-testChannelE0AF024A", + "RecordingConfigurationArn": { + "Fn::GetAtt": [ + "RecordingConfigurationA528CBBF", + "Arn" + ] + }, + "Type": "ADVANCED_SD" + } + } + }, + "Mappings": { + "LatestNodeRuntimeMap": { + "af-south-1": { + "value": "nodejs20.x" + }, + "ap-east-1": { + "value": "nodejs20.x" + }, + "ap-northeast-1": { + "value": "nodejs20.x" + }, + "ap-northeast-2": { + "value": "nodejs20.x" + }, + "ap-northeast-3": { + "value": "nodejs20.x" + }, + "ap-south-1": { + "value": "nodejs20.x" + }, + "ap-south-2": { + "value": "nodejs20.x" + }, + "ap-southeast-1": { + "value": "nodejs20.x" + }, + "ap-southeast-2": { + "value": "nodejs20.x" + }, + "ap-southeast-3": { + "value": "nodejs20.x" + }, + "ap-southeast-4": { + "value": "nodejs20.x" + }, + "ap-southeast-5": { + "value": "nodejs20.x" + }, + "ap-southeast-7": { + "value": "nodejs20.x" + }, + "ca-central-1": { + "value": "nodejs20.x" + }, + "ca-west-1": { + "value": "nodejs20.x" + }, + "cn-north-1": { + "value": "nodejs18.x" + }, + "cn-northwest-1": { + "value": "nodejs18.x" + }, + "eu-central-1": { + "value": "nodejs20.x" + }, + "eu-central-2": { + "value": "nodejs20.x" + }, + "eu-isoe-west-1": { + "value": "nodejs18.x" + }, + "eu-north-1": { + "value": "nodejs20.x" + }, + "eu-south-1": { + "value": "nodejs20.x" + }, + "eu-south-2": { + "value": "nodejs20.x" + }, + "eu-west-1": { + "value": "nodejs20.x" + }, + "eu-west-2": { + "value": "nodejs20.x" + }, + "eu-west-3": { + "value": "nodejs20.x" + }, + "il-central-1": { + "value": "nodejs20.x" + }, + "me-central-1": { + "value": "nodejs20.x" + }, + "me-south-1": { + "value": "nodejs20.x" + }, + "mx-central-1": { + "value": "nodejs20.x" + }, + "sa-east-1": { + "value": "nodejs20.x" + }, + "us-east-1": { + "value": "nodejs20.x" + }, + "us-east-2": { + "value": "nodejs20.x" + }, + "us-gov-east-1": { + "value": "nodejs18.x" + }, + "us-gov-west-1": { + "value": "nodejs18.x" + }, + "us-iso-east-1": { + "value": "nodejs18.x" + }, + "us-iso-west-1": { + "value": "nodejs18.x" + }, + "us-isob-east-1": { + "value": "nodejs18.x" + }, + "us-west-1": { + "value": "nodejs20.x" + }, + "us-west-2": { + "value": "nodejs20.x" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/cdk.out b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/cdk.out new file mode 100644 index 0000000000000..c6e612584e352 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"38.0.1"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/integ.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/integ.json new file mode 100644 index 0000000000000..6c23c9761b918 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "38.0.1", + "testCases": { + "ivs-recording-configuration-test/DefaultTest": { + "stacks": [ + "aws-cdk-ivs-recording-configuration-test" + ], + "assertionStack": "ivs-recording-configuration-test/DefaultTest/DeployAssert", + "assertionStackName": "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets.json new file mode 100644 index 0000000000000..09caab92a2fdf --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets.json @@ -0,0 +1,19 @@ +{ + "version": "38.0.1", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/manifest.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/manifest.json new file mode 100644 index 0000000000000..e206699737663 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/manifest.json @@ -0,0 +1,163 @@ +{ + "version": "38.0.1", + "artifacts": { + "aws-cdk-ivs-recording-configuration-test.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-ivs-recording-configuration-test.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-ivs-recording-configuration-test": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-ivs-recording-configuration-test.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "notificationArns": [], + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/2651be7888f4c0925451bcaab35c5758d4e6cf8b94f1952c1b31f7d99057da7a.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-ivs-recording-configuration-test.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-ivs-recording-configuration-test.assets" + ], + "metadata": { + "/aws-cdk-ivs-recording-configuration-test/Bucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Bucket83908E77" + } + ], + "/aws-cdk-ivs-recording-configuration-test/Bucket/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketPolicyE9A3008A" + } + ], + "/aws-cdk-ivs-recording-configuration-test/Bucket/AutoDeleteObjectsCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketAutoDeleteObjectsCustomResourceBAFD23C2" + } + ], + "/aws-cdk-ivs-recording-configuration-test/LatestNodeRuntimeMap": [ + { + "type": "aws:cdk:logicalId", + "data": "LatestNodeRuntimeMap" + } + ], + "/aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider": [ + { + "type": "aws:cdk:is-custom-resource-handler-customResourceProvider", + "data": true + } + ], + "/aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + } + ], + "/aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F" + } + ], + "/aws-cdk-ivs-recording-configuration-test/RecordingConfiguration/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "RecordingConfigurationA528CBBF" + } + ], + "/aws-cdk-ivs-recording-configuration-test/Channel/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Channel4048F119" + } + ], + "/aws-cdk-ivs-recording-configuration-test/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-ivs-recording-configuration-test/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-ivs-recording-configuration-test" + }, + "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "notificationArns": [], + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets" + ], + "metadata": { + "/ivs-recording-configuration-test/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/ivs-recording-configuration-test/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "ivs-recording-configuration-test/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/tree.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/tree.json new file mode 100644 index 0000000000000..16364b235964b --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/tree.json @@ -0,0 +1,341 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-ivs-recording-configuration-test": { + "id": "aws-cdk-ivs-recording-configuration-test", + "path": "aws-cdk-ivs-recording-configuration-test", + "children": { + "Bucket": { + "id": "Bucket", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": { + "tags": [ + { + "key": "aws-cdk:auto-delete-objects", + "value": "true" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::BucketPolicy", + "aws:cdk:cloudformation:props": { + "bucket": { + "Ref": "Bucket83908E77" + }, + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:List*", + "s3:PutBucketPolicy" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketPolicy", + "version": "0.0.0" + } + }, + "AutoDeleteObjectsCustomResource": { + "id": "AutoDeleteObjectsCustomResource", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket/AutoDeleteObjectsCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket/AutoDeleteObjectsCustomResource/Default", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0" + } + }, + "LatestNodeRuntimeMap": { + "id": "LatestNodeRuntimeMap", + "path": "aws-cdk-ivs-recording-configuration-test/LatestNodeRuntimeMap", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnMapping", + "version": "0.0.0" + } + }, + "Custom::S3AutoDeleteObjectsCustomResourceProvider": { + "id": "Custom::S3AutoDeleteObjectsCustomResourceProvider", + "path": "aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider", + "children": { + "Staging": { + "id": "Staging", + "path": "aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider/Staging", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResourceProviderBase", + "version": "0.0.0" + } + }, + "RecordingConfiguration": { + "id": "RecordingConfiguration", + "path": "aws-cdk-ivs-recording-configuration-test/RecordingConfiguration", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-ivs-recording-configuration-test/RecordingConfiguration/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IVS::RecordingConfiguration", + "aws:cdk:cloudformation:props": { + "destinationConfiguration": { + "s3": { + "bucketName": { + "Ref": "Bucket83908E77" + } + } + }, + "name": "my-recording-configuration", + "recordingReconnectWindowSeconds": 10, + "renditionConfiguration": { + "renditions": [ + "FULL_HD", + "HD", + "SD", + "LOWEST_RESOLUTION" + ], + "renditionSelection": "CUSTOM" + }, + "thumbnailConfiguration": { + "recordingMode": "INTERVAL", + "resolution": "FULL_HD", + "storage": [ + "LATEST", + "SEQUENTIAL" + ], + "targetIntervalSeconds": 30 + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ivs.CfnRecordingConfiguration", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Channel": { + "id": "Channel", + "path": "aws-cdk-ivs-recording-configuration-test/Channel", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-ivs-recording-configuration-test/Channel/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IVS::Channel", + "aws:cdk:cloudformation:props": { + "name": "aws-cdk-ivs-recording-configuration-testChannelE0AF024A", + "recordingConfigurationArn": { + "Fn::GetAtt": [ + "RecordingConfigurationA528CBBF", + "Arn" + ] + }, + "type": "ADVANCED_SD" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ivs.CfnChannel", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-ivs-recording-configuration-test/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-ivs-recording-configuration-test/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "ivs-recording-configuration-test": { + "id": "ivs-recording-configuration-test", + "path": "ivs-recording-configuration-test", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "ivs-recording-configuration-test/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "ivs-recording-configuration-test/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "ivs-recording-configuration-test/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "ivs-recording-configuration-test/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "ivs-recording-configuration-test/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.ts b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.ts new file mode 100644 index 0000000000000..24a402e622e3a --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.ts @@ -0,0 +1,37 @@ +import { App, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { Channel, ChannelType, RecordingConfiguration, Resolution } from '../lib'; +import * as integ from '@aws-cdk/integ-tests-alpha'; +import { RenditionConfiguration } from '../lib/rendition-configuration'; +import { Storage, ThumbnailConfiguration } from '../lib/thumbnail-configuration'; + +const app = new App(); + +const stack = new Stack(app, 'aws-cdk-ivs-recording-configuration-test'); + +const bucket = new Bucket(stack, 'Bucket', { + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, +}); + +const recordingConfiguration = new RecordingConfiguration(stack, 'RecordingConfiguration', { + recordingConfigurationName: 'my-recording-configuration', + bucket, + recordingReconnectWindow: Duration.seconds(10), + renditionConfiguration: RenditionConfiguration.custom([ + Resolution.FULL_HD, + Resolution.HD, + Resolution.SD, + Resolution.LOWEST_RESOLUTION, + ]), + thumbnailConfiguration: ThumbnailConfiguration.interval(Resolution.FULL_HD, [Storage.LATEST, Storage.SEQUENTIAL], Duration.seconds(30)), +}); + +new Channel(stack, 'Channel', { + type: ChannelType.ADVANCED_SD, + recordingConfiguration, +}); + +new integ.IntegTest(app, 'ivs-recording-configuration-test', { + testCases: [stack], +}); diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/recording-configuration.test.ts b/packages/@aws-cdk/aws-ivs-alpha/test/recording-configuration.test.ts new file mode 100644 index 0000000000000..28b92265bff3c --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/recording-configuration.test.ts @@ -0,0 +1,280 @@ +import { App, Duration, Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { IRecordingConfiguration, RecordingConfiguration, Resolution } from '../lib'; +import { Storage, ThumbnailConfiguration } from '../lib/thumbnail-configuration'; +import { RenditionConfiguration } from '../lib/rendition-configuration'; + +describe('IVS Recording Configuration', () => { + let app: App; + let stack: Stack; + let bucket: Bucket; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'TestStack', {}); + bucket = new Bucket(stack, 'Bucket', {}); + }); + + test('creates a recording configuration with minimum properties', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + recordingConfigurationName: 'my-recording-configuration', + bucket, + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + Name: 'my-recording-configuration', + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + }); + }); + + test('set recordingReconnectWindowSeconds', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + recordingConfigurationName: 'my-recording-configuration', + bucket, + recordingReconnectWindow: Duration.seconds(30), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + Name: 'my-recording-configuration', + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + RecordingReconnectWindowSeconds: 30, + }); + }); + + describe('test rendition configuration', () => { + test('set rendition all', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + renditionConfiguration: RenditionConfiguration.all(), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + RenditionConfiguration: { + RenditionSelection: 'ALL', + }, + }); + }); + + test('set rendition none', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + renditionConfiguration: RenditionConfiguration.none(), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + RenditionConfiguration: { + RenditionSelection: 'NONE', + }, + }); + }); + + test('set rendition custom', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + renditionConfiguration: RenditionConfiguration.custom([Resolution.HD, Resolution.SD]), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + RenditionConfiguration: { + RenditionSelection: 'CUSTOM', + Renditions: ['HD', 'SD'], + }, + }); + }); + }); + + describe('test thumbnail configuration', () => { + test('set thumbnail disable', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + recordingConfigurationName: 'my-recording-configuration', + bucket, + thumbnailConfiguration: ThumbnailConfiguration.disable(), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + Name: 'my-recording-configuration', + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + ThumbnailConfiguration: { + RecordingMode: 'DISABLED', + }, + }); + }); + + test('set thumbnail interval', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + recordingConfigurationName: 'my-recording-configuration', + bucket, + thumbnailConfiguration: ThumbnailConfiguration.interval(Resolution.HD, [Storage.LATEST, Storage.SEQUENTIAL], Duration.seconds(30)), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + Name: 'my-recording-configuration', + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + ThumbnailConfiguration: { + RecordingMode: 'INTERVAL', + Resolution: 'HD', + Storage: ['LATEST', 'SEQUENTIAL'], + TargetIntervalSeconds: 30, + }, + }); + }); + }); + + describe('fromRecordingConfigurationId method test', () => { + let importRecordingConfiguration: IRecordingConfiguration; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'TestStack'); + importRecordingConfiguration = RecordingConfiguration.fromRecordingConfigurationId(stack, 'ImportedRecordingConfiguration', 'my-record-configuration'); + }); + + test('should correctly set recordingConfigurationId', () => { + expect(importRecordingConfiguration.recordingConfigurationId).toEqual('my-record-configuration'); + }); + + test('should correctly format recordingConfigurationArn', () => { + expect(importRecordingConfiguration.recordingConfigurationArn).toEqual( + Stack.of(stack).formatArn({ + service: 'ivs', + resource: 'recording-configuration', + resourceName: 'my-record-configuration', + }), + ); + }); + }); + + describe('fromArn method test', () => { + let importRecordingConfiguration: IRecordingConfiguration; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'TestStack'); + importRecordingConfiguration = RecordingConfiguration.fromArn(stack, 'ImportedRecordingConfiguration', 'arn:aws:ivs:us-east-1:012345678912:recording-configuration/my-record-configuration'); + }); + + test('should correctly set recordingConfigurationId', () => { + expect(importRecordingConfiguration.recordingConfigurationId).toEqual('my-record-configuration'); + }); + + test('should correctly format recordingConfigurationArn', () => { + expect(importRecordingConfiguration.recordingConfigurationArn).toEqual('arn:aws:ivs:us-east-1:012345678912:recording-configuration/my-record-configuration'); + }); + }); + + describe('validateRecordingConfigurationName test', () => { + test('throws when recordingConfigurationName include invalid characters.', () => { + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + recordingConfigurationName: 'invalid name', + }); + }).toThrow('\`recordingConfigurationName\` must consist only of alphanumeric characters, hyphens or underbars, got: invalid name.'); + }, + ); + + test('throws when recordingConfigurationName length is invalid.', () => { + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + recordingConfigurationName: 'a'.repeat(129), + }); + }).toThrow('\`recordingConfigurationName\` must be less than or equal to 128 characters, got: 129 characters.'); + }, + ); + }); + + describe('validateRecordingReconnectWindowSeconds test', () => { + test('throws when recordingReconnectWindow is smaller than 1 second.', () => { + + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + recordingReconnectWindow: Duration.millis(1), + }); + }).toThrow('\`recordingReconnectWindow\` must be between 0 and 300 seconds, got 1 milliseconds.'); + }); + + test('throws when recordingReconnectWindow is invalid seconds.', () => { + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + recordingReconnectWindow: Duration.seconds(301), + }); + }).toThrow('\`recordingReconnectWindow\` must be between 0 and 300 seconds, got 301 seconds.'); + }); + }); + + describe('validate thumbnailConfiguraion test', () => { + test('throws when targetInterval is smaller than 1 second.', () => { + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + thumbnailConfiguration: ThumbnailConfiguration.interval(Resolution.HD, [Storage.LATEST], Duration.millis(1)), + }); + }).toThrow('\`targetInterval\` must be between 1 and 60 seconds, got 1 milliseconds.'); + }); + + test('throws when targetInterval is invalid seconds.', () => { + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + thumbnailConfiguration: ThumbnailConfiguration.interval(Resolution.HD, [Storage.LATEST], Duration.seconds(61)), + }); + }).toThrow('\`targetInterval\` must be between 1 and 60 seconds, got 61 seconds.'); + }); + }); +}); \ No newline at end of file