Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/@aws-cdk/aws-mediapackagev2-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ const channelGroup = ChannelGroup.fromChannelGroupAttributes(stack, 'ImportedCha
});
```

You can also import from an ARN, which automatically extracts the name and region:

```ts
declare const stack: Stack;
const channelGroup = ChannelGroup.fromChannelGroupArn(stack, 'ImportedChannelGroup',
'arn:aws:mediapackagev2:us-west-2:123456789012:channelGroup/MyChannelGroup',
);
```

For cross-region imports, pass the `region` parameter to ensure the correct ARN is constructed:

```ts
declare const stack: Stack;
const channelGroup = ChannelGroup.fromChannelGroupAttributes(stack, 'ImportedChannelGroup', {
channelGroupName: 'MyChannelGroup',
region: 'us-west-2',
});
```

## Channel

A channel is part of a channel group and represents the entry point for a content stream into MediaPackage.
Expand Down Expand Up @@ -140,6 +159,17 @@ const channel = Channel.fromChannelAttributes(stack, 'ImportedChannel', {
});
```

You can also import from an ARN:

```ts
declare const stack: Stack;
const channel = Channel.fromChannelArn(stack, 'ImportedChannel',
'arn:aws:mediapackagev2:us-west-2:123456789012:channelGroup/MyGroup/channel/MyChannel',
);
```

Imported channels expose a `region` property, which is parsed from the ARN or falls back to the importing stack's region.

### Channel Resource Policy

The following code creates a resource policy directly on the channel. This
Expand Down Expand Up @@ -184,6 +214,15 @@ const originEndpoint = OriginEndpoint.fromOriginEndpointAttributes(stack, 'Impor
});
```

You can also import from an ARN:

```ts
declare const stack: Stack;
const originEndpoint = OriginEndpoint.fromOriginEndpointArn(stack, 'ImportedOriginEndpoint',
'arn:aws:mediapackagev2:us-west-2:123456789012:channelGroup/MyGroup/channel/MyChannel/originEndpoint/MyEndpoint',
);
```

The following code creates a resource policy on the origin endpoint. This
will automatically create a policy on the first call:

Expand Down
43 changes: 43 additions & 0 deletions packages/@aws-cdk/aws-mediapackagev2-alpha/lib/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ export interface IChannel extends IResource, IChannelRef {
*/
readonly channelGroup?: IChannelGroup;

/**
* The AWS region where this channel lives.
*/
readonly region: string;

/**
* Grants IAM resource policy to the role used to write to MediaPackage V2 Channel.
*/
Expand Down Expand Up @@ -351,12 +356,45 @@ export interface ChannelAttributes {
* @attribute
*/
readonly channelGroupName: string;

/**
* The AWS region where the channel lives.
*
* Required for cross-region imports to construct the correct ARN.
*
* @default - the importing stack's region
*/
readonly region?: string;
}

/**
* A new or imported Channel.
*/
abstract class ChannelBase extends Resource implements IChannel {
/**
* Creates a Channel construct that represents an external (imported) Channel from its ARN.
*
* The ARN is expected to be in the format:
* `arn:<partition>:mediapackagev2:<region>:<account>:channelGroup/<groupName>/channel/<channelName>`
*/
public static fromChannelArn(scope: Construct, id: string, channelArn: string): IChannel {
const parsedArn = Stack.of(scope).splitArn(channelArn, ArnFormat.SLASH_RESOURCE_NAME);
// resourceName is "<groupName>/channel/<channelName>"
const [channelGroupName, , channelName] = parsedArn.resourceName?.split('/') ?? [];
if (!channelGroupName || !channelName) {
throw new ValidationError(
lit`InvalidChannelArn`,
`Could not parse channel ARN: ${channelArn}. Expected format: arn:<partition>:mediapackagev2:<region>:<account>:channelGroup/<groupName>/channel/<channelName>`,
scope,
);
}
return ChannelBase.fromChannelAttributes(scope, id, {
channelGroupName,
channelName,
region: parsedArn.region,
});
}

/**
* Creates a Channel construct that represents an external (imported) Channel.
*/
Expand All @@ -367,6 +405,7 @@ abstract class ChannelBase extends Resource implements IChannel {
public readonly channelName = attrs.channelName;
public readonly createdAt = undefined;
public readonly modifiedAt = undefined;
public readonly region = attrs.region ?? Stack.of(this).region;
protected autoCreatePolicy = false;

public get ingestEndpointUrls(): string[] {
Expand All @@ -381,6 +420,7 @@ abstract class ChannelBase extends Resource implements IChannel {
resource: `channelGroup/${attrs.channelGroupName}/channel`,
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
resourceName: this.channelName,
region: attrs.region,
});
}

Expand All @@ -393,6 +433,7 @@ abstract class ChannelBase extends Resource implements IChannel {
public abstract readonly createdAt?: string;
public abstract readonly modifiedAt?: string;
public abstract readonly ingestEndpointUrls: string[];
public abstract readonly region: string;

/**
* A reference to this Channel resource
Expand Down Expand Up @@ -568,6 +609,7 @@ export class Channel extends ChannelBase implements IChannel {
public readonly channelName: string;
public readonly channelArn: string;
public readonly channelGroup?: IChannelGroup;
public readonly region: string;

/**
* The date and time the channel was created.
Expand Down Expand Up @@ -634,6 +676,7 @@ export class Channel extends ChannelBase implements IChannel {
this.channelArn = channel.attrArn;
this.createdAt = channel.attrCreatedAt;
this.modifiedAt = channel.attrModifiedAt;
this.region = Stack.of(this).region;
this.ingestEndpointUrls = [Fn.select(0, channel.attrIngestEndpointUrls), Fn.select(1, channel.attrIngestEndpointUrls)];

channel.applyRemovalPolicy(props?.removalPolicy ?? RemovalPolicy.DESTROY);
Expand Down
52 changes: 52 additions & 0 deletions packages/@aws-cdk/aws-mediapackagev2-alpha/lib/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1880,6 +1880,15 @@ export interface OriginEndpointAttributes {
* @attribute
*/
readonly originEndpointName: string;

/**
* The AWS region where the origin endpoint lives.
*
* Required for cross-region imports to construct the correct ARN.
*
* @default - the importing stack's region
*/
readonly region?: string;
}

/**
Expand Down Expand Up @@ -2537,6 +2546,31 @@ export class Segment {
}

abstract class OriginEndpointBase extends Resource implements IOriginEndpoint {
/**
* Creates an OriginEndpoint construct that represents an external (imported) Origin Endpoint from its ARN.
*
* The ARN is expected to be in the format:
* `arn:<partition>:mediapackagev2:<region>:<account>:channelGroup/<groupName>/channel/<channelName>/originEndpoint/<endpointName>`
*/
public static fromOriginEndpointArn(scope: Construct, id: string, originEndpointArn: string): IOriginEndpoint {
const parsedArn = Stack.of(scope).splitArn(originEndpointArn, ArnFormat.SLASH_RESOURCE_NAME);
// resourceName is "<groupName>/channel/<channelName>/originEndpoint/<endpointName>"
const [channelGroupName, , channelName, , originEndpointName] = parsedArn.resourceName?.split('/') ?? [];
if (!channelGroupName || !channelName || !originEndpointName) {
throw new ValidationError(
lit`InvalidOriginEndpointArn`,
`Could not parse origin endpoint ARN: ${originEndpointArn}. Expected format: arn:<partition>:mediapackagev2:<region>:<account>:channelGroup/<groupName>/channel/<channelName>/originEndpoint/<endpointName>`,
scope,
);
}
return OriginEndpointBase.fromOriginEndpointAttributes(scope, id, {
channelGroupName,
channelName,
originEndpointName,
region: parsedArn.region,
});
}

/**
* Creates an OriginEndpoint construct that represents an external (imported) Origin Endpoint.
*/
Expand All @@ -2563,6 +2597,7 @@ abstract class OriginEndpointBase extends Resource implements IOriginEndpoint {
resource: `channelGroup/${attrs.channelGroupName}/channel/${this.channelName}/originEndpoint`,
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
resourceName: this.originEndpointName,
region: attrs.region,
});
}

Expand Down Expand Up @@ -2887,6 +2922,23 @@ export class OriginEndpoint extends OriginEndpointBase implements IOriginEndpoin
});
});

// Validate manifest name uniqueness across all manifest types
const allManifestNames = [
...this.hlsManifests.map(m => m.manifestName),
...this.llHlsManifests.map(m => m.manifestName),
...this.dashManifests.map(m => m.manifestName),
...this.mssManifests.map(m => m.manifestName),
].filter(name => !Token.isUnresolved(name));

const duplicateNames = [...new Set(allManifestNames.filter((name, i) => allManifestNames.indexOf(name) !== i))];
if (duplicateNames.length > 0) {
throw new ValidationError(
lit`DuplicateManifestName`,
`Duplicate manifest names: [${duplicateNames.join(', ')}]. Each manifest in an OriginEndpoint must have a unique manifestName.`,
this,
);
}

// Validate manifest and container type compatibility
if (this.mssManifests.length > 0 && containerType !== ContainerType.ISM) {
throw new ValidationError(lit`MssRequiresIsm`, 'MSS manifests require ISM container type. Use Segment.ism() for MSS manifests.', this);
Expand Down
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-mediapackagev2-alpha/lib/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,40 @@ export interface ChannelGroupAttributes {
* @default - not available on imported channel groups
*/
readonly egressDomain?: string;

/**
* The AWS region where the channel group lives.
*
* Required for cross-region imports to construct the correct ARN.
*
* @default - the importing stack's region
*/
readonly region?: string;
}

/**
* A new or imported Channel Group.
*/
abstract class ChannelGroupBase extends Resource implements IChannelGroup {
/**
* Creates a Channel Group construct that represents an external (imported) Channel Group from its ARN.
*/
public static fromChannelGroupArn(scope: Construct, id: string, channelGroupArn: string): IChannelGroup {
const parsedArn = Stack.of(scope).splitArn(channelGroupArn, ArnFormat.SLASH_RESOURCE_NAME);
const channelGroupName = parsedArn.resourceName;
if (!channelGroupName) {
throw new ValidationError(
lit`InvalidChannelGroupArn`,
`Could not parse channel group name from ARN: ${channelGroupArn}`,
scope,
);
}
return ChannelGroupBase.fromChannelGroupAttributes(scope, id, {
channelGroupName,
region: parsedArn.region,
});
}

/**
* Creates a Channel Group construct that represents an external (imported) Channel Group.
*/
Expand All @@ -189,6 +217,7 @@ abstract class ChannelGroupBase extends Resource implements IChannelGroup {
resource: 'channelGroup',
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
resourceName: attrs.channelGroupName,
region: attrs.region,
});

public get egressDomain(): string {
Expand Down
54 changes: 54 additions & 0 deletions packages/@aws-cdk/aws-mediapackagev2-alpha/test/channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ test('existing Channel can be imported', () => {
expect(importedChannel.channelArn).toMatch(/^arn:.*:mediapackagev2:us-east-1:123456789012:channelGroup\/MyChannelGroup\/channel\/test$/);
});

test('imported Channel with cross-region override', () => {
const importedChannel = mediapackagev2.Channel.fromChannelAttributes(stack, 'ImportedChannel', {
channelName: 'test',
channelGroupName: 'MyChannelGroup',
region: 'eu-west-1',
});

expect(importedChannel.channelArn).toMatch(/^arn:.*:mediapackagev2:eu-west-1:123456789012:channelGroup\/MyChannelGroup\/channel\/test$/);
});

test('Channel can be imported from ARN', () => {
const importedChannel = mediapackagev2.Channel.fromChannelArn(stack, 'ImportedChannel', 'arn:aws:mediapackagev2:eu-west-1:123456789012:channelGroup/MyGroup/channel/MyChannel');

expect(importedChannel.channelGroupName).toBe('MyGroup');
expect(importedChannel.channelName).toBe('MyChannel');
expect(importedChannel.channelArn).toMatch(/mediapackagev2:eu-west-1:123456789012/);
expect(importedChannel.region).toBe('eu-west-1');
});

test('Channel.fromChannelArn throws on invalid ARN', () => {
expect(() => {
mediapackagev2.Channel.fromChannelArn(stack, 'Bad', 'arn:aws:mediapackagev2:us-east-1:123456789012:channelGroup/MyGroup');
}).toThrow(/Could not parse channel ARN/);
});

test('Channel has accessible ingest URLs - Tokens returned in Array', () => {
const group = new mediapackagev2.ChannelGroup(stack, 'MyChannelGroup', {
channelGroupName: 'test',
Expand Down Expand Up @@ -361,3 +386,32 @@ test('imported channel has undefined for createdAt and modifiedAt, throws for in
expect(imported.modifiedAt).toBeUndefined();
expect(() => imported.ingestEndpointUrls).toThrow(/ingestEndpointUrls.*is not available/);
});

test('Channel exposes region from Stack', () => {
const group = new mediapackagev2.ChannelGroup(stack, 'MyChannelGroup');
const channel = new mediapackagev2.Channel(stack, 'myChannel', {
channelGroup: group,
input: mediapackagev2.InputConfiguration.cmaf(),
});

expect(channel.region).toBe('us-east-1');
});

test('imported channel region falls back to importing stack region', () => {
const imported = mediapackagev2.Channel.fromChannelAttributes(stack, 'ImportedChannel3', {
channelName: 'test',
channelGroupName: 'MyChannelGroup',
});

expect(imported.region).toBe('us-east-1');
});

test('imported channel uses explicit region from attributes', () => {
const imported = mediapackagev2.Channel.fromChannelAttributes(stack, 'ImportedChannel4', {
channelName: 'test',
channelGroupName: 'MyChannelGroup',
region: 'eu-west-1',
});

expect(imported.region).toBe('eu-west-1');
});
16 changes: 16 additions & 0 deletions packages/@aws-cdk/aws-mediapackagev2-alpha/test/group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ test('existing Channel Group can be imported', () => {
expect(importedChannelGroup.channelGroupArn).toMatch(/^arn:.*:mediapackagev2:us-east-1:123456789012:channelGroup\/MyChannelGroup$/);
});

test('imported Channel Group with cross-region override', () => {
const importedChannelGroup = mediapackagev2.ChannelGroup.fromChannelGroupAttributes(stack, 'ImportedChannelGroup', {
channelGroupName: 'MyChannelGroup',
region: 'eu-west-1',
});

expect(importedChannelGroup.channelGroupArn).toMatch(/^arn:.*:mediapackagev2:eu-west-1:123456789012:channelGroup\/MyChannelGroup$/);
});

test('Channel Group can be imported from ARN', () => {
const imported = mediapackagev2.ChannelGroup.fromChannelGroupArn(stack, 'ImportedChannelGroup', 'arn:aws:mediapackagev2:eu-west-1:123456789012:channelGroup/MyGroup');

expect(imported.channelGroupName).toBe('MyGroup');
expect(imported.channelGroupArn).toMatch(/mediapackagev2:eu-west-1:123456789012/);
});

test('existing Channel Group can be imported and used by a Channel', () => {
const importedChannelGroup = mediapackagev2.ChannelGroup.fromChannelGroupAttributes(stack, 'ImportedChannelGroup', {
channelGroupName: 'MyChannelGroup',
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading