Skip to content

Commit 576c9ec

Browse files
authored
feat(s3-tables): add L2 construct support for Table and Namespace resources (#35023)
### Issue Related to #33054 ### Reason for this change This adds L2 construct support for S3 Tables Namespace and Table resources ### Description of changes - `Namespace`: defines an underlying [CfnNamespace L1 Resource](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3tables.CfnNamespace.html) - `Table`: defines an underlying [CfnTable L1 Resource](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3tables.CfnTable.html) These L2 constructs improve the user experience with strong type safety for input properties, more fail-fast validations, and the ability to import existing resources into CDK. #### Example Usage ```ts // Build a namespace const sampleNamespace = new Namespace(scope, 'ExampleNamespace', { namespaceName: 'example-namespace-1', tableBucket: tableBucket, }); // Build a table const sampleTable = new Table(scope, 'ExampleTable', { tableName: 'example_table', namespace: namespace, openTableFormat: OpenTableFormat.ICEBERG, withoutMetadata: true, }); // Build a table with an Iceberg Schema const sampleTableWithSchema = new Table(scope, 'ExampleSchemaTable', { tableName: 'example_table_with_schema', namespace: namespace, openTableFormat: OpenTableFormat.ICEBERG, icebergMetadata: { icebergSchema: { schemaFieldList: [ { name: 'id', type: 'int', required: true, }, { name: 'name', type: 'string', }, ], }, }, compaction: { status: Status.ENABLED, targetFileSizeMb: 128, }, snapshotManagement: { status: Status.ENABLED, maxSnapshotAgeHours: 48, minSnapshotsToKeep: 5, }, }); ``` ### Describe any new or updated permissions being added No permissions are being added with these changes. ### Description of how you validated changes - Added unit test coverage for new constructs - Added integration tests with default and explicit props ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 8f0f26d commit 576c9ec

33 files changed

+65721
-3
lines changed

packages/@aws-cdk/aws-s3tables-alpha/README.md

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,59 @@ const sampleTableBucket = new TableBucket(scope, 'ExampleTableBucket', {
3939
});
4040
```
4141

42+
### Define an S3 Tables Namespace
43+
44+
```ts
45+
// Build a namespace
46+
const sampleNamespace = new Namespace(scope, 'ExampleNamespace', {
47+
namespaceName: 'example-namespace-1',
48+
tableBucket: tableBucket,
49+
});
50+
```
51+
52+
### Define an S3 Table
53+
54+
```ts
55+
// Build a table
56+
const sampleTable = new Table(scope, 'ExampleTable', {
57+
tableName: 'example_table',
58+
namespace: namespace,
59+
openTableFormat: OpenTableFormat.ICEBERG,
60+
withoutMetadata: true,
61+
});
62+
63+
// Build a table with an Iceberg Schema
64+
const sampleTableWithSchema = new Table(scope, 'ExampleSchemaTable', {
65+
tableName: 'example_table_with_schema',
66+
namespace: namespace,
67+
openTableFormat: OpenTableFormat.ICEBERG,
68+
icebergMetadata: {
69+
icebergSchema: {
70+
schemaFieldList: [
71+
{
72+
name: 'id',
73+
type: 'int',
74+
required: true,
75+
},
76+
{
77+
name: 'name',
78+
type: 'string',
79+
},
80+
],
81+
},
82+
},
83+
compaction: {
84+
status: Status.ENABLED,
85+
targetFileSizeMb: 128,
86+
},
87+
snapshotManagement: {
88+
status: Status.ENABLED,
89+
maxSnapshotAgeHours: 48,
90+
minSnapshotsToKeep: 5,
91+
},
92+
});
93+
```
94+
4295
Learn more about table buckets maintenance operations and default behavior from the [S3 Tables User Guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-table-buckets-maintenance.html)
4396

4497
### Controlling Table Bucket Permissions
@@ -110,5 +163,5 @@ const encryptedBucketAuto = new TableBucket(scope, 'EncryptedTableBucketAuto', {
110163

111164
L2 Construct support for:
112165

113-
- Namespaces
114-
- Tables
166+
- Table Policy
167+
- KMS encryption support for Tables

packages/@aws-cdk/aws-s3tables-alpha/lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55

66
export * from './table-bucket';
77
export * from './table-bucket-policy';
8+
export * from './namespace';
9+
export * from './table';
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { Construct } from 'constructs';
2+
import { IResource, RemovalPolicy, Resource, Token, UnscopedValidationError } from 'aws-cdk-lib/core';
3+
import { ITableBucket } from './table-bucket';
4+
import { addConstructMetadata } from 'aws-cdk-lib/core/lib/metadata-resource';
5+
import { CfnNamespace } from 'aws-cdk-lib/aws-s3tables';
6+
import { EOL } from 'os';
7+
8+
/**
9+
* Represents an S3 Tables Namespace.
10+
*/
11+
export interface INamespace extends IResource {
12+
/**
13+
* The name of this namespace
14+
* @attribute
15+
*/
16+
readonly namespaceName: string;
17+
18+
/**
19+
* The table bucket which this namespace belongs to
20+
* @attribute
21+
*/
22+
readonly tableBucket: ITableBucket;
23+
}
24+
25+
/**
26+
* Parameters for constructing a Namespace
27+
*/
28+
export interface NamespaceProps {
29+
/**
30+
* A name for the namespace
31+
*/
32+
readonly namespaceName: string;
33+
/**
34+
* The table bucket this namespace belongs to.
35+
*/
36+
readonly tableBucket: ITableBucket;
37+
/**
38+
* Policy to apply when the policy is removed from this stack.
39+
* @default RemovalPolicy.DESTROY
40+
*/
41+
readonly removalPolicy?: RemovalPolicy;
42+
}
43+
44+
/**
45+
* Attributes for importing an existing namespace
46+
*/
47+
export interface NamespaceAttributes {
48+
/**
49+
* The name of the namespace
50+
*/
51+
readonly namespaceName: string;
52+
53+
/**
54+
* The table bucket this namespace belongs to
55+
*/
56+
readonly tableBucket: ITableBucket;
57+
}
58+
59+
/**
60+
* An S3 Tables Namespace with helpers.
61+
*
62+
* A namespace is a logical container for tables within a table bucket.
63+
*/
64+
export class Namespace extends Resource implements INamespace {
65+
/**
66+
* Import an existing namespace from its attributes
67+
*/
68+
public static fromNamespaceAttributes(scope: Construct, id: string, attrs: NamespaceAttributes): INamespace {
69+
class Import extends Resource implements INamespace {
70+
public readonly namespaceName = attrs.namespaceName;
71+
public readonly tableBucket = attrs.tableBucket;
72+
}
73+
74+
return new Import(scope, id);
75+
}
76+
/**
77+
* See https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-buckets-naming.html
78+
* @param namespaceName Name of the namespace
79+
* @throws UnscopedValidationError if any naming errors are detected
80+
*/
81+
public static validateNamespaceName(namespaceName: string) {
82+
if (namespaceName == undefined || Token.isUnresolved(namespaceName)) {
83+
// the name is a late-bound value, not a defined string, so skip validation
84+
return;
85+
}
86+
87+
const errors: string[] = [];
88+
89+
// Length validation
90+
if (namespaceName.length < 1 || namespaceName.length > 255) {
91+
errors.push(
92+
'Namespace name must be at least 1 and no more than 255 characters',
93+
);
94+
}
95+
96+
// Character set validation
97+
const illegalCharsetRegEx = /[^a-z0-9_]/;
98+
const allowedEdgeCharsetRegEx = /[a-z0-9]/;
99+
100+
const illegalCharMatch = namespaceName.match(illegalCharsetRegEx);
101+
if (illegalCharMatch) {
102+
errors.push(
103+
'Namespace name must only contain lowercase characters, numbers, and underscores (_)' +
104+
` (offset: ${illegalCharMatch.index})`,
105+
);
106+
}
107+
108+
// Edge character validation
109+
if (!allowedEdgeCharsetRegEx.test(namespaceName.charAt(0))) {
110+
errors.push(
111+
'Namespace name must start with a lowercase letter or number (offset: 0)',
112+
);
113+
}
114+
if (
115+
!allowedEdgeCharsetRegEx.test(namespaceName.charAt(namespaceName.length - 1))
116+
) {
117+
errors.push(
118+
`Namespace name must end with a lowercase letter or number (offset: ${
119+
namespaceName.length - 1
120+
})`,
121+
);
122+
}
123+
124+
if (namespaceName.startsWith('aws')) {
125+
errors.push('Namespace name must not start with reserved prefix \'aws\'');
126+
}
127+
128+
if (errors.length > 0) {
129+
throw new UnscopedValidationError(
130+
`Invalid S3 Tables namespace name (value: ${namespaceName})${EOL}${errors.join(EOL)}`,
131+
);
132+
}
133+
}
134+
135+
/**
136+
* @internal The underlying namespace resource.
137+
*/
138+
private readonly _resource: CfnNamespace;
139+
140+
/**
141+
* The name of this namespace
142+
*/
143+
public readonly namespaceName: string;
144+
145+
/**
146+
* The table bucket which this namespace belongs to
147+
*/
148+
public readonly tableBucket: ITableBucket;
149+
150+
constructor(scope: Construct, id: string, props: NamespaceProps) {
151+
super(scope, id);
152+
// Enhanced CDK Analytics Telemetry
153+
addConstructMetadata(this, props);
154+
155+
Namespace.validateNamespaceName(props.namespaceName);
156+
157+
this.tableBucket = props.tableBucket;
158+
this.namespaceName = props.namespaceName;
159+
this._resource = new CfnNamespace(this, id, {
160+
namespace: props.namespaceName,
161+
tableBucketArn: this.tableBucket.tableBucketArn,
162+
});
163+
164+
if (props.removalPolicy) {
165+
this._resource.applyRemovalPolicy(props.removalPolicy);
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)