Skip to content

Commit

Permalink
feat(location): support GeofenceCollection (#30711)
Browse files Browse the repository at this point in the history
### 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
mazyu36 authored Aug 29, 2024
1 parent c7b236c commit 04d73ac
Show file tree
Hide file tree
Showing 16 changed files with 821 additions and 10 deletions.
28 changes: 28 additions & 0 deletions packages/@aws-cdk/aws-location-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,31 @@ declare const role: iam.Role;
const placeIndex = new location.PlaceIndex(this, 'PlaceIndex');
placeIndex.grantSearch(role);
```

## Geofence Collection

Geofence collection resources allow you to store and manage geofences—virtual boundaries on a map.
You can evaluate locations against a geofence collection resource and get notifications when the location
update crosses the boundary of any of the geofences in the geofence collection.

```ts
declare const key: kms.Key;

new location.GeofenceCollection(this, 'GeofenceCollection', {
geofenceCollectionName: 'MyGeofenceCollection', // optional, defaults to a generated name
kmsKey: key, // optional, defaults to use an AWS managed key
});
```

Use the `grant()` or `grantRead()` method to grant the given identity permissions to perform actions
on the geofence collection:

```ts
declare const role: iam.Role;

const geofenceCollection = new location.GeofenceCollection(this, 'GeofenceCollection', {
geofenceCollectionName: 'MyGeofenceCollection',
});

geofenceCollection.grantRead(role);
```
163 changes: 163 additions & 0 deletions packages/@aws-cdk/aws-location-alpha/lib/geofence-collection.ts
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',
);
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-location-alpha/lib/index.ts
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:
14 changes: 4 additions & 10 deletions packages/@aws-cdk/aws-location-alpha/lib/place-index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as iam from 'aws-cdk-lib/aws-iam';
import { ArnFormat, IResource, Lazy, Names, Resource, Stack, Token } from 'aws-cdk-lib/core';
import { ArnFormat, IResource, Lazy, Resource, Stack, Token } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { CfnPlaceIndex } from 'aws-cdk-lib/aws-location';
import { generateUniqueId } from './util';

/**
* A Place Index
Expand Down Expand Up @@ -164,7 +165,7 @@ export class PlaceIndex extends PlaceIndexBase {
public readonly placeIndexArn: string;

/**
* The timestamp for when the place index resource was created in ISO 8601 forma
* The timestamp for when the place index resource was created in ISO 8601 format
*
* @attribute
*/
Expand All @@ -187,7 +188,7 @@ export class PlaceIndex extends PlaceIndexBase {
}

super(scope, id, {
physicalName: props.placeIndexName ?? Lazy.string({ produce: () => this.generateUniqueId() }),
physicalName: props.placeIndexName ?? Lazy.string({ produce: () => generateUniqueId(this) }),
});

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

private generateUniqueId(): string {
const name = Names.uniqueId(this);
if (name.length > 100) {
return name.substring(0, 50) + name.substring(name.length - 50);
}
return name;
}
}
10 changes: 10 additions & 0 deletions packages/@aws-cdk/aws-location-alpha/lib/util.ts
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as location from '@aws-cdk/aws-location-alpha';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';

class Fixture extends Stack {
constructor(scope: Construct, id: string) {
Expand Down
112 changes: 112 additions & 0 deletions packages/@aws-cdk/aws-location-alpha/test/geofence-collection.test.ts
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),
});
});

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

Loading

0 comments on commit 04d73ac

Please sign in to comment.