Skip to content

Commit 1820c05

Browse files
go-to-khoegertn
andauthored
feat(aws-cur): add new property enableDefaultUniqueReportName that creates a unique cost report name by default (#48)
* fix(aws-cur): default name for cost report is not unique * update API.md * enableDefaultUniqueReportName API.md * change an integ test chage var name snapshots --------- Signed-off-by: Thorsten Hoeger <thorsten.hoeger@taimos.de> Co-authored-by: Thorsten Hoeger <thorsten.hoeger@taimos.de>
1 parent 649175a commit 1820c05

File tree

9 files changed

+302
-10
lines changed

9 files changed

+302
-10
lines changed

API.md

Lines changed: 37 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/aws-cur/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { CostReport, ReportGranularity, CurFormat } from '@open-constructs/aws-c
1919
```
2020

2121
### Basic Example
22+
2223
Here's how you can create a monthly cost and usage report in Parquet format:
2324

2425
```typescript
@@ -50,6 +51,23 @@ new CostReport(stack, 'MyDetailedCostReport', {
5051
});
5152
```
5253

54+
### Generating Unique Report Name By Default
55+
56+
If you set the `enableDefaultUniqueReportName` property to `true`, the construct will automatically
57+
generate a unique report name by default.
58+
59+
The cost report name must be unique within your AWS account. So this property is useful when you want
60+
to create multiple reports without specifying a report name for each one.
61+
62+
If you specify a report name directly via the `costReportName`, the construct will use that name instead
63+
of generating a unique one.
64+
65+
```typescript
66+
new CostReport(stack, 'MyCostReport', {
67+
enableDefaultUniqueReportName: true,
68+
});
69+
```
70+
5371
### Additional Notes
5472

5573
The construct automatically handles the permissions required for AWS billing services to access the S3 bucket.

src/aws-cur/cost-report.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Resource, Stack, Token, aws_cur, aws_iam, aws_s3 } from 'aws-cdk-lib';
1+
import { Names, Resource, Stack, Token, aws_cur, aws_iam, aws_s3 } from 'aws-cdk-lib';
22
import { Construct } from 'constructs';
33

44
/**
@@ -55,10 +55,28 @@ export class CurFormat {
5555
* Properties for defining a Cost and Usage Report.
5656
*/
5757
export interface CostReportProps {
58+
/**
59+
* Whether to generate a unique report name automatically if the `costReportName` property
60+
* is not specified.
61+
*
62+
* The default value of the `costReportName` is normally ‘default-cur’, but setting this flag
63+
* to true will generate a unique default value.
64+
*
65+
* This flag is ignored if the `costReportName` property is specified.
66+
*
67+
* @default false
68+
*/
69+
readonly enableDefaultUniqueReportName?: boolean;
70+
5871
/**
5972
* The name of the cost report.
6073
*
61-
* @default - 'default-cur'
74+
* The name must be unique, is case sensitive, and can't include spaces.
75+
*
76+
* The length of this name must be between 1 and 256.
77+
*
78+
* @default - a unique name automatically generated if `enableDefaultUniqueReportName` is
79+
* true, otherwise 'default-cur'.
6280
*/
6381
readonly costReportName?: string;
6482

@@ -99,6 +117,8 @@ export interface CostReportProps {
99117
export class CostReport extends Resource {
100118
/** The S3 bucket that stores the cost report */
101119
public readonly reportBucket: aws_s3.IBucket;
120+
/** The name of the cost report */
121+
public readonly costReportName: string;
102122

103123
constructor(scope: Construct, id: string, props: CostReportProps) {
104124
super(scope, id);
@@ -137,11 +157,33 @@ export class CostReport extends Resource {
137157

138158
const format = props.format ?? CurFormat.TEXT_OR_CSV;
139159

160+
if (props.costReportName !== undefined && !Token.isUnresolved(props.costReportName)) {
161+
if (props.costReportName.length < 1 || props.costReportName.length > 256) {
162+
throw new Error(
163+
`'costReportName' must be between 1 and 256 characters long, got: ${props.costReportName.length}`,
164+
);
165+
}
166+
if (!/^[0-9A-Za-z!\-_.*'()]+$/.test(props.costReportName)) {
167+
throw new Error(
168+
`'costReportName' must only contain alphanumeric characters and the following special characters: !-_.*'(), got: '${props.costReportName}'`,
169+
);
170+
}
171+
}
172+
173+
const reportName =
174+
props.costReportName ??
175+
(props.enableDefaultUniqueReportName
176+
? Names.uniqueResourceName(this, {
177+
maxLength: 256,
178+
allowedSpecialCharacters: "!-_.*'()",
179+
})
180+
: 'default-cur');
181+
140182
const reportDefinition = this.createReportDefinition(this, 'Resource', {
141183
compression: format.compression,
142184
format: format.format,
143185
refreshClosedReports: false,
144-
reportName: props.costReportName ?? 'default-cur',
186+
reportName,
145187
reportVersioning: 'CREATE_NEW_REPORT',
146188
s3Bucket: this.reportBucket.bucketName,
147189
s3Prefix: 'reports',
@@ -150,6 +192,8 @@ export class CostReport extends Resource {
150192
additionalSchemaElements: ['RESOURCES'],
151193
});
152194
reportDefinition.node.addDependency(this.reportBucket.policy!);
195+
196+
this.costReportName = reportDefinition.ref;
153197
}
154198

155199
protected createReportBucket(scope: Construct, id: string, props: aws_s3.BucketProps): aws_s3.IBucket {

test/aws-cur/cost-report.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,84 @@ describe('CostReport', () => {
111111
template.resourceCountIs('AWS::CUR::ReportDefinition', 2);
112112
});
113113

114+
test('unique report name is generated if costReportName is not specified and enableDefaultUniqueReportName is true', () => {
115+
new CostReport(stack, 'MyCostReport', {
116+
enableDefaultUniqueReportName: true,
117+
});
118+
119+
const template = Template.fromStack(stack);
120+
121+
template.hasResourceProperties('AWS::CUR::ReportDefinition', {
122+
ReportName: 'TestStackMyCostReportF831D765',
123+
});
124+
});
125+
126+
test('fixed report name is generated if costReportName is not specified and enableDefaultUniqueReportName is false', () => {
127+
new CostReport(stack, 'MyCostReport', {
128+
enableDefaultUniqueReportName: false,
129+
});
130+
131+
const template = Template.fromStack(stack);
132+
133+
template.hasResourceProperties('AWS::CUR::ReportDefinition', {
134+
ReportName: 'default-cur',
135+
});
136+
});
137+
138+
test('report name can be specified even if enableDefaultUniqueReportName is true', () => {
139+
new CostReport(stack, 'MyCostReport', {
140+
enableDefaultUniqueReportName: true,
141+
costReportName: 'custom-cur',
142+
});
143+
144+
const template = Template.fromStack(stack);
145+
146+
template.hasResourceProperties('AWS::CUR::ReportDefinition', {
147+
ReportName: 'custom-cur',
148+
});
149+
});
150+
151+
test('report name can have special characters', () => {
152+
new CostReport(stack, 'Report', {
153+
costReportName: "report1!-_.*'()",
154+
});
155+
156+
const template = Template.fromStack(stack);
157+
158+
template.hasResourceProperties('AWS::CUR::ReportDefinition', {
159+
ReportName: "report1!-_.*'()",
160+
});
161+
});
162+
163+
test('throws if the report name has spaces', () => {
164+
expect(
165+
() =>
166+
new CostReport(stack, 'MyCostReport', {
167+
costReportName: 'report with spaces',
168+
}),
169+
).toThrow(
170+
"'costReportName' must only contain alphanumeric characters and the following special characters: !-_.*'(), got: 'report with spaces'",
171+
);
172+
});
173+
174+
test('throws if the length of the report name is greater than 256 characters', () => {
175+
expect(
176+
() =>
177+
new CostReport(stack, 'MyCostReport', {
178+
costReportName: 'a'.repeat(257),
179+
}),
180+
).toThrow("'costReportName' must be between 1 and 256 characters long, got: 257");
181+
});
182+
183+
test('throws if the length of the report name is less than 1 character', () => {
184+
expect(
185+
() =>
186+
new CostReport(stack, 'MyCostReport', {
187+
costReportName: '',
188+
}),
189+
).toThrow("'costReportName' must be between 1 and 256 characters long, got: 0");
190+
});
191+
114192
test('regions other than us-east-1', () => {
115193
const regionOtherStack = new Stack(app, 'OtherRegionStack', {
116194
env: { region: 'ap-northeast-1' },

test/aws-cur/integ.cost-report.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,20 @@ class CostReportStack extends cdk.Stack {
1313
blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL,
1414
});
1515

16-
new ocf.aws_cur.CostReport(this, 'CostReport', {
16+
const report = new ocf.aws_cur.CostReport(this, 'CostReport', {
1717
bucket,
1818
});
19+
const reportWithUniqueReportName = new ocf.aws_cur.CostReport(this, 'CostReportWithUniqueReportName', {
20+
bucket,
21+
enableDefaultUniqueReportName: true,
22+
});
23+
24+
new cdk.CfnOutput(this, 'CostReportName', {
25+
value: report.costReportName,
26+
});
27+
new cdk.CfnOutput(this, 'UniqueCostReportName', {
28+
value: reportWithUniqueReportName.costReportName,
29+
});
1930
}
2031
}
2132

test/aws-cur/integ.cost-report.ts.snapshot/cur-report.assets.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515
}
1616
}
1717
},
18-
"ea273a55939f3c24361eca95307fd8752c4fee982b66f7fd4716f69bd89a7ed3": {
18+
"a019b27bb46cfc5076913cf39dac56a1e51b11dcdc8521cf57b88a9c8d2b0f32": {
1919
"source": {
2020
"path": "cur-report.template.json",
2121
"packaging": "file"
2222
},
2323
"destinations": {
2424
"current_account-us-east-1": {
2525
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-us-east-1",
26-
"objectKey": "ea273a55939f3c24361eca95307fd8752c4fee982b66f7fd4716f69bd89a7ed3.json",
26+
"objectKey": "a019b27bb46cfc5076913cf39dac56a1e51b11dcdc8521cf57b88a9c8d2b0f32.json",
2727
"region": "us-east-1",
2828
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-us-east-1"
2929
}

test/aws-cur/integ.cost-report.ts.snapshot/cur-report.template.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,40 @@
251251
"DependsOn": [
252252
"ReportBucketPolicyA15D85AD"
253253
]
254+
},
255+
"CostReportWithUniqueReportName449C5FC3": {
256+
"Type": "AWS::CUR::ReportDefinition",
257+
"Properties": {
258+
"AdditionalSchemaElements": [
259+
"RESOURCES"
260+
],
261+
"Compression": "GZIP",
262+
"Format": "textORcsv",
263+
"RefreshClosedReports": false,
264+
"ReportName": "cur-reportCostReportWithUniqueReportName82909F43",
265+
"ReportVersioning": "CREATE_NEW_REPORT",
266+
"S3Bucket": {
267+
"Ref": "ReportBucket577F0FCD"
268+
},
269+
"S3Prefix": "reports",
270+
"S3Region": "us-east-1",
271+
"TimeUnit": "HOURLY"
272+
},
273+
"DependsOn": [
274+
"ReportBucketPolicyA15D85AD"
275+
]
276+
}
277+
},
278+
"Outputs": {
279+
"CostReportName": {
280+
"Value": {
281+
"Ref": "CostReport0C3566A0"
282+
}
283+
},
284+
"UniqueCostReportName": {
285+
"Value": {
286+
"Ref": "CostReportWithUniqueReportName449C5FC3"
287+
}
254288
}
255289
},
256290
"Parameters": {

test/aws-cur/integ.cost-report.ts.snapshot/manifest.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"validateOnSynth": false,
1919
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-us-east-1",
2020
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-us-east-1",
21-
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-us-east-1/ea273a55939f3c24361eca95307fd8752c4fee982b66f7fd4716f69bd89a7ed3.json",
21+
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-us-east-1/a019b27bb46cfc5076913cf39dac56a1e51b11dcdc8521cf57b88a9c8d2b0f32.json",
2222
"requiresBootstrapStackVersion": 6,
2323
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
2424
"additionalDependencies": [
@@ -70,6 +70,24 @@
7070
"data": "CostReport0C3566A0"
7171
}
7272
],
73+
"/cur-report/CostReportWithUniqueReportName/Resource": [
74+
{
75+
"type": "aws:cdk:logicalId",
76+
"data": "CostReportWithUniqueReportName449C5FC3"
77+
}
78+
],
79+
"/cur-report/CostReportName": [
80+
{
81+
"type": "aws:cdk:logicalId",
82+
"data": "CostReportName"
83+
}
84+
],
85+
"/cur-report/UniqueCostReportName": [
86+
{
87+
"type": "aws:cdk:logicalId",
88+
"data": "UniqueCostReportName"
89+
}
90+
],
7391
"/cur-report/BootstrapVersion": [
7492
{
7593
"type": "aws:cdk:logicalId",

0 commit comments

Comments
 (0)