-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(location): support GeofenceCollection (#30711)
### Issue # (if applicable) Closes #30710. ### Reason for this change To support L2 level geofence collection. ### Description of changes Add `Geofence Collection` class. ### Description of how you validated changes Add unit tests and integ tests. ### 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*
- Loading branch information
Showing
16 changed files
with
821 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
163 changes: 163 additions & 0 deletions
163
packages/@aws-cdk/aws-location-alpha/lib/geofence-collection.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import * as iam from 'aws-cdk-lib/aws-iam'; | ||
import * as kms from 'aws-cdk-lib/aws-kms'; | ||
import { ArnFormat, IResource, Lazy, Resource, Stack, Token } from 'aws-cdk-lib/core'; | ||
import { Construct } from 'constructs'; | ||
import { CfnGeofenceCollection } from 'aws-cdk-lib/aws-location'; | ||
import { generateUniqueId } from './util'; | ||
|
||
/** | ||
* A Geofence Collection | ||
*/ | ||
export interface IGeofenceCollection extends IResource { | ||
/** | ||
* The name of the geofence collection | ||
* | ||
* @attribute | ||
*/ | ||
readonly geofenceCollectionName: string; | ||
|
||
/** | ||
* The Amazon Resource Name (ARN) of the geofence collection resource | ||
* | ||
* @attribute Arn, CollectionArn | ||
*/ | ||
readonly geofenceCollectionArn: string; | ||
} | ||
|
||
/** | ||
* Properties for a geofence collection | ||
*/ | ||
export interface GeofenceCollectionProps { | ||
/** | ||
* A name for the geofence collection | ||
* | ||
* Must be between 1 and 100 characters and contain only alphanumeric characters, | ||
* hyphens, periods and underscores. | ||
* | ||
* @default - A name is automatically generated | ||
*/ | ||
readonly geofenceCollectionName?: string; | ||
|
||
/** | ||
* A description for the geofence collection | ||
* | ||
* @default - no description | ||
*/ | ||
readonly description?: string; | ||
|
||
/** | ||
* The customer managed to encrypt your data. | ||
* | ||
* @default - Use an AWS managed key | ||
* @see https://docs.aws.amazon.com/location/latest/developerguide/encryption-at-rest.html | ||
*/ | ||
readonly kmsKey?: kms.IKey; | ||
} | ||
|
||
/** | ||
* A Geofence Collection | ||
* | ||
* @see https://docs.aws.amazon.com/location/latest/developerguide/geofence-tracker-concepts.html#geofence-overview | ||
*/ | ||
export class GeofenceCollection extends Resource implements IGeofenceCollection { | ||
/** | ||
* Use an existing geofence collection by name | ||
*/ | ||
public static fromGeofenceCollectionName(scope: Construct, id: string, geofenceCollectionName: string): IGeofenceCollection { | ||
const geofenceCollectionArn = Stack.of(scope).formatArn({ | ||
service: 'geo', | ||
resource: 'geofence-collection', | ||
resourceName: geofenceCollectionName, | ||
}); | ||
|
||
return GeofenceCollection.fromGeofenceCollectionArn(scope, id, geofenceCollectionArn); | ||
} | ||
|
||
/** | ||
* Use an existing geofence collection by ARN | ||
*/ | ||
public static fromGeofenceCollectionArn(scope: Construct, id: string, geofenceCollectionArn: string): IGeofenceCollection { | ||
const parsedArn = Stack.of(scope).splitArn(geofenceCollectionArn, ArnFormat.SLASH_RESOURCE_NAME); | ||
|
||
if (!parsedArn.resourceName) { | ||
throw new Error(`Geofence Collection Arn ${geofenceCollectionArn} does not have a resource name.`); | ||
} | ||
|
||
class Import extends Resource implements IGeofenceCollection { | ||
public readonly geofenceCollectionName = parsedArn.resourceName!; | ||
public readonly geofenceCollectionArn = geofenceCollectionArn; | ||
} | ||
|
||
return new Import(scope, id, { | ||
account: parsedArn.account, | ||
region: parsedArn.region, | ||
}); | ||
} | ||
|
||
public readonly geofenceCollectionName: string; | ||
|
||
public readonly geofenceCollectionArn: string; | ||
|
||
/** | ||
* The timestamp for when the geofence collection resource was created in ISO 8601 format | ||
* | ||
* @attribute | ||
*/ | ||
public readonly geofenceCollectionCreateTime: string; | ||
|
||
/** | ||
* The timestamp for when the geofence collection resource was last updated in ISO 8601 format | ||
* | ||
* @attribute | ||
*/ | ||
public readonly geofenceCollectionUpdateTime: string; | ||
|
||
constructor(scope: Construct, id: string, props: GeofenceCollectionProps = {}) { | ||
|
||
if (props.description && !Token.isUnresolved(props.description) && props.description.length > 1000) { | ||
throw new Error(`\`description\` must be between 0 and 1000 characters. Received: ${props.description.length} characters`); | ||
} | ||
|
||
if (props.geofenceCollectionName && !Token.isUnresolved(props.geofenceCollectionName) && !/^[-.\w]{1,100}$/.test(props.geofenceCollectionName)) { | ||
throw new Error(`Invalid geofence collection name. The geofence collection name must be between 1 and 100 characters and contain only alphanumeric characters, hyphens, periods and underscores. Received: ${props.geofenceCollectionName}`); | ||
} | ||
|
||
super(scope, id, { | ||
physicalName: props.geofenceCollectionName ?? Lazy.string({ produce: () => generateUniqueId(this) }), | ||
}); | ||
|
||
const geofenceCollection = new CfnGeofenceCollection(this, 'Resource', { | ||
collectionName: this.physicalName, | ||
description: props.description, | ||
kmsKeyId: props.kmsKey?.keyArn, | ||
}); | ||
|
||
this.geofenceCollectionName = geofenceCollection.ref; | ||
this.geofenceCollectionArn = geofenceCollection.attrArn; | ||
this.geofenceCollectionCreateTime = geofenceCollection.attrCreateTime; | ||
this.geofenceCollectionUpdateTime = geofenceCollection.attrUpdateTime; | ||
} | ||
|
||
/** | ||
* Grant the given principal identity permissions to perform the actions on this geofence collection. | ||
*/ | ||
public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { | ||
return iam.Grant.addToPrincipal({ | ||
grantee: grantee, | ||
actions: actions, | ||
resourceArns: [this.geofenceCollectionArn], | ||
}); | ||
} | ||
|
||
/** | ||
* Grant the given identity permissions to read this geofence collection | ||
* | ||
* @see https://docs.aws.amazon.com/location/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_id-based-policy-examples-read-only-geofences | ||
*/ | ||
public grantRead(grantee: iam.IGrantable): iam.Grant { | ||
return this.grant(grantee, | ||
'geo:ListGeofences', | ||
'geo:GetGeofence', | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './geofence-collection'; | ||
export * from './place-index'; | ||
|
||
// AWS::Location CloudFormation Resources: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { Names } from 'aws-cdk-lib/core'; | ||
import { IConstruct } from 'constructs'; | ||
|
||
export function generateUniqueId(context: IConstruct): string { | ||
const name = Names.uniqueId(context); | ||
if (name.length > 100) { | ||
return name.substring(0, 50) + name.substring(name.length - 50); | ||
} | ||
return name; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
packages/@aws-cdk/aws-location-alpha/test/geofence-collection.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { Match, Template } from 'aws-cdk-lib/assertions'; | ||
import * as iam from 'aws-cdk-lib/aws-iam'; | ||
import * as kms from 'aws-cdk-lib/aws-kms'; | ||
import { Stack } from 'aws-cdk-lib'; | ||
import { GeofenceCollection } from '../lib/geofence-collection'; | ||
|
||
let stack: Stack; | ||
beforeEach(() => { | ||
stack = new Stack(); | ||
}); | ||
|
||
test('create a geofence collection', () => { | ||
new GeofenceCollection(stack, 'GeofenceCollection', { description: 'test' }); | ||
|
||
Template.fromStack(stack).hasResourceProperties('AWS::Location::GeofenceCollection', { | ||
CollectionName: 'GeofenceCollection', | ||
Description: 'test', | ||
}); | ||
}); | ||
|
||
test('creates geofence collection with empty description', () => { | ||
new GeofenceCollection(stack, 'GeofenceCollection', { description: '' }); | ||
|
||
Template.fromStack(stack).hasResourceProperties('AWS::Location::GeofenceCollection', { | ||
Description: '', | ||
}); | ||
}); | ||
|
||
test('throws with invalid description', () => { | ||
expect(() => new GeofenceCollection(stack, 'GeofenceCollection', { | ||
description: 'a'.repeat(1001), | ||
})).toThrow('`description` must be between 0 and 1000 characters. Received: 1001 characters'); | ||
}); | ||
|
||
test('throws with invalid name', () => { | ||
expect(() => new GeofenceCollection(stack, 'GeofenceCollection', { | ||
geofenceCollectionName: 'inv@lid', | ||
})).toThrow('Invalid geofence collection name. The geofence collection name must be between 1 and 100 characters and contain only alphanumeric characters, hyphens, periods and underscores. Received: inv@lid'); | ||
}); | ||
|
||
test('grant read actions', () => { | ||
const geofenceCollection = new GeofenceCollection(stack, 'GeofenceCollection', { | ||
}); | ||
|
||
const role = new iam.Role(stack, 'Role', { | ||
assumedBy: new iam.ServicePrincipal('foo'), | ||
}); | ||
|
||
geofenceCollection.grantRead(role); | ||
|
||
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.objectLike({ | ||
PolicyDocument: Match.objectLike({ | ||
Statement: [ | ||
{ | ||
Action: [ | ||
'geo:ListGeofences', | ||
'geo:GetGeofence', | ||
], | ||
Effect: 'Allow', | ||
Resource: { | ||
'Fn::GetAtt': [ | ||
'GeofenceCollection6FAC681F', | ||
'Arn', | ||
], | ||
}, | ||
}, | ||
], | ||
}), | ||
})); | ||
}); | ||
|
||
test('import from arn', () => { | ||
const geofenceCollectionArn = stack.formatArn({ | ||
service: 'geo', | ||
resource: 'geofence-collection', | ||
resourceName: 'MyGeofenceCollection', | ||
}); | ||
const geofenceCollection = GeofenceCollection.fromGeofenceCollectionArn(stack, 'GeofenceCollection', geofenceCollectionArn); | ||
|
||
// THEN | ||
expect(geofenceCollection.geofenceCollectionName).toEqual('MyGeofenceCollection'); | ||
expect(geofenceCollection.geofenceCollectionArn).toEqual(geofenceCollectionArn); | ||
}); | ||
|
||
test('import from name', () => { | ||
// WHEN | ||
const geofenceCollectionName = 'MyGeofenceCollection'; | ||
const geofenceCollection = GeofenceCollection.fromGeofenceCollectionName(stack, 'GeofenceCollection', geofenceCollectionName); | ||
|
||
// THEN | ||
expect(geofenceCollection.geofenceCollectionName).toEqual(geofenceCollectionName); | ||
expect(geofenceCollection.geofenceCollectionArn).toEqual(stack.formatArn({ | ||
service: 'geo', | ||
resource: 'geofence-collection', | ||
resourceName: 'MyGeofenceCollection', | ||
})); | ||
}); | ||
|
||
test('create a geofence collection with a customer managed key)', () => { | ||
// GIVEN | ||
const kmsKey = new kms.Key(stack, 'Key'); | ||
|
||
// WHEN | ||
new GeofenceCollection(stack, 'GeofenceCollection', | ||
{ kmsKey }, | ||
); | ||
|
||
// THEN | ||
Template.fromStack(stack).hasResourceProperties('AWS::Location::GeofenceCollection', { | ||
KmsKeyId: stack.resolve(kmsKey.keyArn), | ||
}); | ||
}); |
19 changes: 19 additions & 0 deletions
19
...-collection.js.snapshot/GeofenceCollectionTestDefaultTestDeployAssert44609017.assets.json
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.