Skip to content

Commit 04d73ac

Browse files
authored
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*
1 parent c7b236c commit 04d73ac

16 files changed

+821
-10
lines changed

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

+28
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,31 @@ declare const role: iam.Role;
4949
const placeIndex = new location.PlaceIndex(this, 'PlaceIndex');
5050
placeIndex.grantSearch(role);
5151
```
52+
53+
## Geofence Collection
54+
55+
Geofence collection resources allow you to store and manage geofences—virtual boundaries on a map.
56+
You can evaluate locations against a geofence collection resource and get notifications when the location
57+
update crosses the boundary of any of the geofences in the geofence collection.
58+
59+
```ts
60+
declare const key: kms.Key;
61+
62+
new location.GeofenceCollection(this, 'GeofenceCollection', {
63+
geofenceCollectionName: 'MyGeofenceCollection', // optional, defaults to a generated name
64+
kmsKey: key, // optional, defaults to use an AWS managed key
65+
});
66+
```
67+
68+
Use the `grant()` or `grantRead()` method to grant the given identity permissions to perform actions
69+
on the geofence collection:
70+
71+
```ts
72+
declare const role: iam.Role;
73+
74+
const geofenceCollection = new location.GeofenceCollection(this, 'GeofenceCollection', {
75+
geofenceCollectionName: 'MyGeofenceCollection',
76+
});
77+
78+
geofenceCollection.grantRead(role);
79+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import * as iam from 'aws-cdk-lib/aws-iam';
2+
import * as kms from 'aws-cdk-lib/aws-kms';
3+
import { ArnFormat, IResource, Lazy, Resource, Stack, Token } from 'aws-cdk-lib/core';
4+
import { Construct } from 'constructs';
5+
import { CfnGeofenceCollection } from 'aws-cdk-lib/aws-location';
6+
import { generateUniqueId } from './util';
7+
8+
/**
9+
* A Geofence Collection
10+
*/
11+
export interface IGeofenceCollection extends IResource {
12+
/**
13+
* The name of the geofence collection
14+
*
15+
* @attribute
16+
*/
17+
readonly geofenceCollectionName: string;
18+
19+
/**
20+
* The Amazon Resource Name (ARN) of the geofence collection resource
21+
*
22+
* @attribute Arn, CollectionArn
23+
*/
24+
readonly geofenceCollectionArn: string;
25+
}
26+
27+
/**
28+
* Properties for a geofence collection
29+
*/
30+
export interface GeofenceCollectionProps {
31+
/**
32+
* A name for the geofence collection
33+
*
34+
* Must be between 1 and 100 characters and contain only alphanumeric characters,
35+
* hyphens, periods and underscores.
36+
*
37+
* @default - A name is automatically generated
38+
*/
39+
readonly geofenceCollectionName?: string;
40+
41+
/**
42+
* A description for the geofence collection
43+
*
44+
* @default - no description
45+
*/
46+
readonly description?: string;
47+
48+
/**
49+
* The customer managed to encrypt your data.
50+
*
51+
* @default - Use an AWS managed key
52+
* @see https://docs.aws.amazon.com/location/latest/developerguide/encryption-at-rest.html
53+
*/
54+
readonly kmsKey?: kms.IKey;
55+
}
56+
57+
/**
58+
* A Geofence Collection
59+
*
60+
* @see https://docs.aws.amazon.com/location/latest/developerguide/geofence-tracker-concepts.html#geofence-overview
61+
*/
62+
export class GeofenceCollection extends Resource implements IGeofenceCollection {
63+
/**
64+
* Use an existing geofence collection by name
65+
*/
66+
public static fromGeofenceCollectionName(scope: Construct, id: string, geofenceCollectionName: string): IGeofenceCollection {
67+
const geofenceCollectionArn = Stack.of(scope).formatArn({
68+
service: 'geo',
69+
resource: 'geofence-collection',
70+
resourceName: geofenceCollectionName,
71+
});
72+
73+
return GeofenceCollection.fromGeofenceCollectionArn(scope, id, geofenceCollectionArn);
74+
}
75+
76+
/**
77+
* Use an existing geofence collection by ARN
78+
*/
79+
public static fromGeofenceCollectionArn(scope: Construct, id: string, geofenceCollectionArn: string): IGeofenceCollection {
80+
const parsedArn = Stack.of(scope).splitArn(geofenceCollectionArn, ArnFormat.SLASH_RESOURCE_NAME);
81+
82+
if (!parsedArn.resourceName) {
83+
throw new Error(`Geofence Collection Arn ${geofenceCollectionArn} does not have a resource name.`);
84+
}
85+
86+
class Import extends Resource implements IGeofenceCollection {
87+
public readonly geofenceCollectionName = parsedArn.resourceName!;
88+
public readonly geofenceCollectionArn = geofenceCollectionArn;
89+
}
90+
91+
return new Import(scope, id, {
92+
account: parsedArn.account,
93+
region: parsedArn.region,
94+
});
95+
}
96+
97+
public readonly geofenceCollectionName: string;
98+
99+
public readonly geofenceCollectionArn: string;
100+
101+
/**
102+
* The timestamp for when the geofence collection resource was created in ISO 8601 format
103+
*
104+
* @attribute
105+
*/
106+
public readonly geofenceCollectionCreateTime: string;
107+
108+
/**
109+
* The timestamp for when the geofence collection resource was last updated in ISO 8601 format
110+
*
111+
* @attribute
112+
*/
113+
public readonly geofenceCollectionUpdateTime: string;
114+
115+
constructor(scope: Construct, id: string, props: GeofenceCollectionProps = {}) {
116+
117+
if (props.description && !Token.isUnresolved(props.description) && props.description.length > 1000) {
118+
throw new Error(`\`description\` must be between 0 and 1000 characters. Received: ${props.description.length} characters`);
119+
}
120+
121+
if (props.geofenceCollectionName && !Token.isUnresolved(props.geofenceCollectionName) && !/^[-.\w]{1,100}$/.test(props.geofenceCollectionName)) {
122+
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}`);
123+
}
124+
125+
super(scope, id, {
126+
physicalName: props.geofenceCollectionName ?? Lazy.string({ produce: () => generateUniqueId(this) }),
127+
});
128+
129+
const geofenceCollection = new CfnGeofenceCollection(this, 'Resource', {
130+
collectionName: this.physicalName,
131+
description: props.description,
132+
kmsKeyId: props.kmsKey?.keyArn,
133+
});
134+
135+
this.geofenceCollectionName = geofenceCollection.ref;
136+
this.geofenceCollectionArn = geofenceCollection.attrArn;
137+
this.geofenceCollectionCreateTime = geofenceCollection.attrCreateTime;
138+
this.geofenceCollectionUpdateTime = geofenceCollection.attrUpdateTime;
139+
}
140+
141+
/**
142+
* Grant the given principal identity permissions to perform the actions on this geofence collection.
143+
*/
144+
public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant {
145+
return iam.Grant.addToPrincipal({
146+
grantee: grantee,
147+
actions: actions,
148+
resourceArns: [this.geofenceCollectionArn],
149+
});
150+
}
151+
152+
/**
153+
* Grant the given identity permissions to read this geofence collection
154+
*
155+
* @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
156+
*/
157+
public grantRead(grantee: iam.IGrantable): iam.Grant {
158+
return this.grant(grantee,
159+
'geo:ListGeofences',
160+
'geo:GetGeofence',
161+
);
162+
}
163+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './geofence-collection';
12
export * from './place-index';
23

34
// AWS::Location CloudFormation Resources:

packages/@aws-cdk/aws-location-alpha/lib/place-index.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as iam from 'aws-cdk-lib/aws-iam';
2-
import { ArnFormat, IResource, Lazy, Names, Resource, Stack, Token } from 'aws-cdk-lib/core';
2+
import { ArnFormat, IResource, Lazy, Resource, Stack, Token } from 'aws-cdk-lib/core';
33
import { Construct } from 'constructs';
44
import { CfnPlaceIndex } from 'aws-cdk-lib/aws-location';
5+
import { generateUniqueId } from './util';
56

67
/**
78
* A Place Index
@@ -164,7 +165,7 @@ export class PlaceIndex extends PlaceIndexBase {
164165
public readonly placeIndexArn: string;
165166

166167
/**
167-
* The timestamp for when the place index resource was created in ISO 8601 forma
168+
* The timestamp for when the place index resource was created in ISO 8601 format
168169
*
169170
* @attribute
170171
*/
@@ -187,7 +188,7 @@ export class PlaceIndex extends PlaceIndexBase {
187188
}
188189

189190
super(scope, id, {
190-
physicalName: props.placeIndexName ?? Lazy.string({ produce: () => this.generateUniqueId() }),
191+
physicalName: props.placeIndexName ?? Lazy.string({ produce: () => generateUniqueId(this) }),
191192
});
192193

193194
const placeIndex = new CfnPlaceIndex(this, 'Resource', {
@@ -205,11 +206,4 @@ export class PlaceIndex extends PlaceIndexBase {
205206
this.placeIndexUpdateTime = placeIndex.attrUpdateTime;
206207
}
207208

208-
private generateUniqueId(): string {
209-
const name = Names.uniqueId(this);
210-
if (name.length > 100) {
211-
return name.substring(0, 50) + name.substring(name.length - 50);
212-
}
213-
return name;
214-
}
215209
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Names } from 'aws-cdk-lib/core';
2+
import { IConstruct } from 'constructs';
3+
4+
export function generateUniqueId(context: IConstruct): string {
5+
const name = Names.uniqueId(context);
6+
if (name.length > 100) {
7+
return name.substring(0, 50) + name.substring(name.length - 50);
8+
}
9+
return name;
10+
}

packages/@aws-cdk/aws-location-alpha/rosetta/default.ts-fixture

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Stack } from 'aws-cdk-lib';
33
import { Construct } from 'constructs';
44
import * as location from '@aws-cdk/aws-location-alpha';
55
import * as iam from 'aws-cdk-lib/aws-iam';
6+
import * as kms from 'aws-cdk-lib/aws-kms';
67

78
class Fixture extends Stack {
89
constructor(scope: Construct, id: string) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Match, Template } from 'aws-cdk-lib/assertions';
2+
import * as iam from 'aws-cdk-lib/aws-iam';
3+
import * as kms from 'aws-cdk-lib/aws-kms';
4+
import { Stack } from 'aws-cdk-lib';
5+
import { GeofenceCollection } from '../lib/geofence-collection';
6+
7+
let stack: Stack;
8+
beforeEach(() => {
9+
stack = new Stack();
10+
});
11+
12+
test('create a geofence collection', () => {
13+
new GeofenceCollection(stack, 'GeofenceCollection', { description: 'test' });
14+
15+
Template.fromStack(stack).hasResourceProperties('AWS::Location::GeofenceCollection', {
16+
CollectionName: 'GeofenceCollection',
17+
Description: 'test',
18+
});
19+
});
20+
21+
test('creates geofence collection with empty description', () => {
22+
new GeofenceCollection(stack, 'GeofenceCollection', { description: '' });
23+
24+
Template.fromStack(stack).hasResourceProperties('AWS::Location::GeofenceCollection', {
25+
Description: '',
26+
});
27+
});
28+
29+
test('throws with invalid description', () => {
30+
expect(() => new GeofenceCollection(stack, 'GeofenceCollection', {
31+
description: 'a'.repeat(1001),
32+
})).toThrow('`description` must be between 0 and 1000 characters. Received: 1001 characters');
33+
});
34+
35+
test('throws with invalid name', () => {
36+
expect(() => new GeofenceCollection(stack, 'GeofenceCollection', {
37+
geofenceCollectionName: 'inv@lid',
38+
})).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');
39+
});
40+
41+
test('grant read actions', () => {
42+
const geofenceCollection = new GeofenceCollection(stack, 'GeofenceCollection', {
43+
});
44+
45+
const role = new iam.Role(stack, 'Role', {
46+
assumedBy: new iam.ServicePrincipal('foo'),
47+
});
48+
49+
geofenceCollection.grantRead(role);
50+
51+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.objectLike({
52+
PolicyDocument: Match.objectLike({
53+
Statement: [
54+
{
55+
Action: [
56+
'geo:ListGeofences',
57+
'geo:GetGeofence',
58+
],
59+
Effect: 'Allow',
60+
Resource: {
61+
'Fn::GetAtt': [
62+
'GeofenceCollection6FAC681F',
63+
'Arn',
64+
],
65+
},
66+
},
67+
],
68+
}),
69+
}));
70+
});
71+
72+
test('import from arn', () => {
73+
const geofenceCollectionArn = stack.formatArn({
74+
service: 'geo',
75+
resource: 'geofence-collection',
76+
resourceName: 'MyGeofenceCollection',
77+
});
78+
const geofenceCollection = GeofenceCollection.fromGeofenceCollectionArn(stack, 'GeofenceCollection', geofenceCollectionArn);
79+
80+
// THEN
81+
expect(geofenceCollection.geofenceCollectionName).toEqual('MyGeofenceCollection');
82+
expect(geofenceCollection.geofenceCollectionArn).toEqual(geofenceCollectionArn);
83+
});
84+
85+
test('import from name', () => {
86+
// WHEN
87+
const geofenceCollectionName = 'MyGeofenceCollection';
88+
const geofenceCollection = GeofenceCollection.fromGeofenceCollectionName(stack, 'GeofenceCollection', geofenceCollectionName);
89+
90+
// THEN
91+
expect(geofenceCollection.geofenceCollectionName).toEqual(geofenceCollectionName);
92+
expect(geofenceCollection.geofenceCollectionArn).toEqual(stack.formatArn({
93+
service: 'geo',
94+
resource: 'geofence-collection',
95+
resourceName: 'MyGeofenceCollection',
96+
}));
97+
});
98+
99+
test('create a geofence collection with a customer managed key)', () => {
100+
// GIVEN
101+
const kmsKey = new kms.Key(stack, 'Key');
102+
103+
// WHEN
104+
new GeofenceCollection(stack, 'GeofenceCollection',
105+
{ kmsKey },
106+
);
107+
108+
// THEN
109+
Template.fromStack(stack).hasResourceProperties('AWS::Location::GeofenceCollection', {
110+
KmsKeyId: stack.resolve(kmsKey.keyArn),
111+
});
112+
});

packages/@aws-cdk/aws-location-alpha/test/integ.geofence-collection.js.snapshot/GeofenceCollectionTestDefaultTestDeployAssert44609017.assets.json

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)