From fb4d6072949737c982e79d21b14ef5147c0c4b3c Mon Sep 17 00:00:00 2001 From: maz Date: Fri, 30 Aug 2024 18:14:18 +0900 Subject: [PATCH] add tracker --- .../@aws-cdk/aws-location-alpha/README.md | 42 +++ .../@aws-cdk/aws-location-alpha/lib/index.ts | 2 + .../lib/tracker-consumer.ts | 37 +++ .../aws-location-alpha/lib/tracker.ts | 240 +++++++++++++++++ ...efaultTestDeployAssert0E17BB8B.assets.json | 19 ++ ...aultTestDeployAssert0E17BB8B.template.json | 36 +++ .../cdk-integ-location-tracker.assets.json | 19 ++ .../cdk-integ-location-tracker.template.json | 110 ++++++++ .../test/integ.tracker.js.snapshot/cdk.out | 1 + .../test/integ.tracker.js.snapshot/integ.json | 12 + .../integ.tracker.js.snapshot/manifest.json | 131 ++++++++++ .../test/integ.tracker.js.snapshot/tree.json | 243 ++++++++++++++++++ .../aws-location-alpha/test/integ.tracker.ts | 40 +++ .../test/tracker-consumer.test.ts | 25 ++ .../aws-location-alpha/test/tracker.test.ts | 162 ++++++++++++ 15 files changed, 1119 insertions(+) create mode 100644 packages/@aws-cdk/aws-location-alpha/lib/tracker-consumer.ts create mode 100644 packages/@aws-cdk/aws-location-alpha/lib/tracker.ts create mode 100644 packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/TrackerTestDefaultTestDeployAssert0E17BB8B.assets.json create mode 100644 packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/TrackerTestDefaultTestDeployAssert0E17BB8B.template.json create mode 100644 packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk-integ-location-tracker.assets.json create mode 100644 packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk-integ-location-tracker.template.json create mode 100644 packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/integ.json create mode 100644 packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/tree.json create mode 100644 packages/@aws-cdk/aws-location-alpha/test/integ.tracker.ts create mode 100644 packages/@aws-cdk/aws-location-alpha/test/tracker-consumer.test.ts create mode 100644 packages/@aws-cdk/aws-location-alpha/test/tracker.test.ts diff --git a/packages/@aws-cdk/aws-location-alpha/README.md b/packages/@aws-cdk/aws-location-alpha/README.md index 810c29532a163..f0f252001489a 100644 --- a/packages/@aws-cdk/aws-location-alpha/README.md +++ b/packages/@aws-cdk/aws-location-alpha/README.md @@ -104,3 +104,45 @@ const routeCalculator = new location.RouteCalculator(this, 'RouteCalculator', { }); routeCalculator.grantRead(role); ``` + +## Tracker + +A tracker stores position updates for a collection of devices. The tracker can be used to query the devices' current location or location history. It stores the updates, but reduces storage space and visual noise by filtering the locations before storing them. + +For more information, see [Trackers](https://docs.aws.amazon.com/location/latest/developerguide/geofence-tracker-concepts.html#tracking-overview). + +To create a tracker, define a `Tracker`: + +```ts +declare const key: kms.Key; + +new location.Tracker(this, 'Tracker', { + trackerName: 'MyTracker', // optional, defaults to a generated name + kmsKey: key, // optional, defaults to use an AWS managed key +}); +``` + +Use the `grant()`, `grantUpdateDevicePositions` or `grantRead()` method to grant the given identity permissions to perform actions +on the geofence collection: + +```ts +declare const role: iam.Role; + +const tracker = new location.Tracker(this, 'Tracker', { + trackerName: 'MyTracker', +}); + +tracker.grantRead(role); +``` + +If you want to associate a tracker with a geofence collection, define a `TrackerConsumer`. + +```ts +declare const geofenceCollection: location.GeofenceCollection; +declare const tracker: location.Tracker; + +new location.TrackerConsumer(this, 'TrackerConsumer', { + consumer: geofenceCollection, + tracker: tracker, +}); +``` diff --git a/packages/@aws-cdk/aws-location-alpha/lib/index.ts b/packages/@aws-cdk/aws-location-alpha/lib/index.ts index 7a5cee74e9bdd..8ae724cdafff2 100644 --- a/packages/@aws-cdk/aws-location-alpha/lib/index.ts +++ b/packages/@aws-cdk/aws-location-alpha/lib/index.ts @@ -1,6 +1,8 @@ export * from './geofence-collection'; export * from './place-index'; export * from './route-calculator'; +export * from './tracker'; +export * from './tracker-consumer'; export * from './util'; // AWS::Location CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-location-alpha/lib/tracker-consumer.ts b/packages/@aws-cdk/aws-location-alpha/lib/tracker-consumer.ts new file mode 100644 index 0000000000000..c5116494ae1fb --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/lib/tracker-consumer.ts @@ -0,0 +1,37 @@ +import { Resource } from 'aws-cdk-lib/core'; +import { Construct } from 'constructs'; +import { CfnTrackerConsumer } from 'aws-cdk-lib/aws-location'; +import { Tracker } from './tracker'; +import { GeofenceCollection } from './geofence-collection'; + +/** + * Properties for a tracker consumer + */ +export interface TrackerConsumerProps { + /** + * The geofence collection to be associated to tracker resource. Used when you need to specify a resource across all AWS. + */ + readonly consumer: GeofenceCollection; + + /** + * The tracker associated with the geofence collection. + */ + readonly tracker: Tracker; +} + +/** + * A Tracker Consumer + */ +export class TrackerConsumer extends Resource { + + constructor(scope: Construct, id: string, props: TrackerConsumerProps) { + + super(scope, id, {}); + + new CfnTrackerConsumer(this, 'Resource', { + consumerArn: props.consumer.geofenceCollectionArn, + trackerName: props.tracker.trackerName, + }); + + } +} diff --git a/packages/@aws-cdk/aws-location-alpha/lib/tracker.ts b/packages/@aws-cdk/aws-location-alpha/lib/tracker.ts new file mode 100644 index 0000000000000..971cf8d6cefb2 --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/lib/tracker.ts @@ -0,0 +1,240 @@ +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 { CfnTracker } from 'aws-cdk-lib/aws-location'; +import { generateUniqueId } from './util'; + +/** + * A Tracker + */ +export interface ITracker extends IResource { + /** + * The name of the tracker + * + * @attribute + */ + readonly trackerName: string; + + /** + * The Amazon Resource Name (ARN) of the tracker resource + * + * @attribute Arn, TrackerArn + */ + readonly trackerArn: string; +} + +/** + * Properties for a tracker + */ +export interface TrackerProps { + /** + * A name for the tracker + * + * Must be between 1 and 100 characters and contain only alphanumeric characters, + * hyphens, periods and underscores. + * + * @default - A name is automatically generated + */ + readonly trackerName?: string; + + /** + * A description for the tracker + * + * @default - no description + */ + readonly description?: string; + + /** + * Send filtered device position updates to default EventBridge bus. + * + * @default false + */ + readonly eventBridgeEnabled?: boolean; + + /** + * The customer managed key to encrypt data. + * If you set customer managed key, the Bounding Polygon Queries feature will be disabled by default. + * You can choose to opt-in to the Bounding Polygon Queries feature by setting the kmsKeyEnableGeospatialQueries parameter to true. + * + * @default - Use an AWS managed key + */ + readonly kmsKey?: kms.IKey; + + /** + * Whether to opt-in to the Bounding Polygon Queries feature with customer managed key + * + * @default false + */ + readonly kmsKeyEnableGeospatialQueries?: boolean; + + /** + * The position filtering for the tracker resource + * + * @default PositionFiltering.TIME_BASED + */ + readonly positionFiltering?: PositionFiltering; +} + +/** + * The position filtering for the tracker resource + */ +export enum PositionFiltering { + /** + * Location updates are evaluated against linked geofence collections, but not every location update is stored. + * If your update frequency is more often than 30 seconds, only one update per 30 seconds is stored for each unique device ID. + */ + TIME_BASED = 'TimeBased', + + /** + * If the device has moved less than 30 m (98.4 ft), location updates are ignored. + * Location updates within this area are neither evaluated against linked geofence collections, nor stored. + * This helps control costs by reducing the number of geofence evaluations and historical device positions to paginate through. + * Distance-based filtering can also reduce the effects of GPS noise when displaying device trajectories on a map. + */ + DISTANCE_BASED = 'DistanceBased', + + /** + * If the device has moved less than the measured accuracy, location updates are ignored. + * For example, if two consecutive updates from a device have a horizontal accuracy of 5 m and 10 m, + * the second update is ignored if the device has moved less than 15 m. + * Ignored location updates are neither evaluated against linked geofence collections, nor stored. + * This can reduce the effects of GPS noise when displaying device trajectories on a map, + * and can help control your costs by reducing the number of geofence evaluations. + */ + ACCURACY_BASED = 'AccuracyBased', +} + +/** + * A Tracker + * + * @see https://docs.aws.amazon.com/location/latest/developerguide/geofence-tracker-concepts.html#tracking-overview + */ +export class Tracker extends Resource implements ITracker { + /** + * Use an existing tracker by name + */ + public static fromTrackerName(scope: Construct, id: string, trackerName: string): ITracker { + const trackerArn = Stack.of(scope).formatArn({ + service: 'geo', + resource: 'tracker', + resourceName: trackerName, + }); + + return Tracker.fromTrackerArn(scope, id, trackerArn); + } + + /** + * Use an existing tracker by ARN + */ + public static fromTrackerArn(scope: Construct, id: string, trackerArn: string): ITracker { + const parsedArn = Stack.of(scope).splitArn(trackerArn, ArnFormat.SLASH_RESOURCE_NAME); + + if (!parsedArn.resourceName) { + throw new Error(`Tracker Arn ${trackerArn} does not have a resource name.`); + } + + class Import extends Resource implements ITracker { + public readonly trackerName = parsedArn.resourceName!; + public readonly trackerArn = trackerArn; + } + + return new Import(scope, id, { + account: parsedArn.account, + region: parsedArn.region, + }); + } + + public readonly trackerName: string; + + public readonly trackerArn: string; + + /** + * The timestamp for when the tracker resource was created in ISO 8601 format + * + * @attribute + */ + public readonly trackerCreateTime: string; + + /** + * The timestamp for when the tracker resource was last updated in ISO 8601 format + * + * @attribute + */ + public readonly trackerUpdateTime: string; + + constructor(scope: Construct, id: string, props: TrackerProps) { + + 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.trackerName && !Token.isUnresolved(props.trackerName) && !/^[-.\w]{1,100}$/.test(props.trackerName)) { + throw new Error(`Invalid tracker name. The tracker name must be between 1 and 100 characters and contain only alphanumeric characters, hyphens, periods and underscores. Received: ${props.trackerName}`); + } + + if (!Token.isUnresolved(props.kmsKey) + && !props.kmsKey + && props.kmsKeyEnableGeospatialQueries + ) { + throw new Error('`kmsKeyEnableGeospatialQueries` can only be enabled that are configured to use an AWS KMS customer managed key'); + } + + super(scope, id, { + physicalName: props.trackerName ?? Lazy.string({ produce: () => generateUniqueId(this) }), + }); + + const tracker = new CfnTracker(this, 'Resource', { + trackerName: this.physicalName, + description: props.description, + eventBridgeEnabled: props.eventBridgeEnabled, + kmsKeyEnableGeospatialQueries: props.kmsKeyEnableGeospatialQueries, + kmsKeyId: props.kmsKey?.keyArn, + positionFiltering: props.positionFiltering, + }); + + this.trackerName = tracker.ref; + this.trackerArn = tracker.attrArn; + this.trackerCreateTime = tracker.attrCreateTime; + this.trackerUpdateTime = tracker.attrUpdateTime; + } + + /** + * Grant the given principal identity permissions to perform the actions on this tracker. + */ + public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee: grantee, + actions: actions, + resourceArns: [this.trackerArn], + }); + } + + /** + * Grant the given identity permissions to update device positions for a tracker + * + * @see https://docs.aws.amazon.com/location/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_id-based-policy-examples-read-only-trackers + */ + public grantUpdateDevicePositions(grantee: iam.IGrantable): iam.Grant { + return this.grant(grantee, + 'geo:BatchUpdateDevicePosition', + ); + } + + /** + * Grant the given identity permissions to read device positions from a tracker + * + * @see https://docs.aws.amazon.com/location/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_id-based-policy-examples-read-only-trackers + */ + public grantRead(grantee: iam.IGrantable): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee, + actions: [ + 'geo:BatchGetDevicePosition', + 'geo:GetDevicePosition', + 'geo:GetDevicePositionHistory', + ], + resourceArns: [`${this.trackerArn}/*`], + }); + } +} diff --git a/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/TrackerTestDefaultTestDeployAssert0E17BB8B.assets.json b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/TrackerTestDefaultTestDeployAssert0E17BB8B.assets.json new file mode 100644 index 0000000000000..70295e267b6c2 --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/TrackerTestDefaultTestDeployAssert0E17BB8B.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.5", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "TrackerTestDefaultTestDeployAssert0E17BB8B.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/TrackerTestDefaultTestDeployAssert0E17BB8B.template.json b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/TrackerTestDefaultTestDeployAssert0E17BB8B.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/TrackerTestDefaultTestDeployAssert0E17BB8B.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk-integ-location-tracker.assets.json b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk-integ-location-tracker.assets.json new file mode 100644 index 0000000000000..195c3319afd45 --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk-integ-location-tracker.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.5", + "files": { + "4caf9e99fbcddaf2e30f316b2ab8afcfa158595e3e5bff18dc8c6bae6299528e": { + "source": { + "path": "cdk-integ-location-tracker.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "4caf9e99fbcddaf2e30f316b2ab8afcfa158595e3e5bff18dc8c6bae6299528e.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk-integ-location-tracker.template.json b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk-integ-location-tracker.template.json new file mode 100644 index 0000000000000..db3ad73dc7de9 --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk-integ-location-tracker.template.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "GeofenceCollection6FAC681F": { + "Type": "AWS::Location::GeofenceCollection", + "Properties": { + "CollectionName": "MyGeofenceCollection", + "Description": "test" + } + }, + "Key961B73FD": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "TrackerAF5FC55F": { + "Type": "AWS::Location::Tracker", + "Properties": { + "Description": "test tracker", + "EventBridgeEnabled": true, + "KmsKeyEnableGeospatialQueries": true, + "KmsKeyId": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + }, + "PositionFiltering": "AccuracyBased", + "TrackerName": "MyTracker" + } + }, + "TrackerConsumer3AEB9150": { + "Type": "AWS::Location::TrackerConsumer", + "Properties": { + "ConsumerArn": { + "Fn::GetAtt": [ + "GeofenceCollection6FAC681F", + "Arn" + ] + }, + "TrackerName": { + "Ref": "TrackerAF5FC55F" + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk.out b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk.out new file mode 100644 index 0000000000000..bd5311dc372de --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.5"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/integ.json b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/integ.json new file mode 100644 index 0000000000000..e0d073feb73ca --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.5", + "testCases": { + "TrackerTest/DefaultTest": { + "stacks": [ + "cdk-integ-location-tracker" + ], + "assertionStack": "TrackerTest/DefaultTest/DeployAssert", + "assertionStackName": "TrackerTestDefaultTestDeployAssert0E17BB8B" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/manifest.json b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/manifest.json new file mode 100644 index 0000000000000..518c743936f6a --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/manifest.json @@ -0,0 +1,131 @@ +{ + "version": "36.0.5", + "artifacts": { + "cdk-integ-location-tracker.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "cdk-integ-location-tracker.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "cdk-integ-location-tracker": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "cdk-integ-location-tracker.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/4caf9e99fbcddaf2e30f316b2ab8afcfa158595e3e5bff18dc8c6bae6299528e.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "cdk-integ-location-tracker.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "cdk-integ-location-tracker.assets" + ], + "metadata": { + "/cdk-integ-location-tracker/GeofenceCollection/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "GeofenceCollection6FAC681F" + } + ], + "/cdk-integ-location-tracker/Key/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Key961B73FD" + } + ], + "/cdk-integ-location-tracker/Tracker/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TrackerAF5FC55F" + } + ], + "/cdk-integ-location-tracker/TrackerConsumer/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TrackerConsumer3AEB9150" + } + ], + "/cdk-integ-location-tracker/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/cdk-integ-location-tracker/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "cdk-integ-location-tracker" + }, + "TrackerTestDefaultTestDeployAssert0E17BB8B.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "TrackerTestDefaultTestDeployAssert0E17BB8B.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "TrackerTestDefaultTestDeployAssert0E17BB8B": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "TrackerTestDefaultTestDeployAssert0E17BB8B.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "TrackerTestDefaultTestDeployAssert0E17BB8B.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "TrackerTestDefaultTestDeployAssert0E17BB8B.assets" + ], + "metadata": { + "/TrackerTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/TrackerTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "TrackerTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/tree.json b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/tree.json new file mode 100644 index 0000000000000..bf3a57e9307dd --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.js.snapshot/tree.json @@ -0,0 +1,243 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "cdk-integ-location-tracker": { + "id": "cdk-integ-location-tracker", + "path": "cdk-integ-location-tracker", + "children": { + "GeofenceCollection": { + "id": "GeofenceCollection", + "path": "cdk-integ-location-tracker/GeofenceCollection", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-integ-location-tracker/GeofenceCollection/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Location::GeofenceCollection", + "aws:cdk:cloudformation:props": { + "collectionName": "MyGeofenceCollection", + "description": "test" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Key": { + "id": "Key", + "path": "cdk-integ-location-tracker/Key", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-integ-location-tracker/Key/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::KMS::Key", + "aws:cdk:cloudformation:props": { + "keyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Tracker": { + "id": "Tracker", + "path": "cdk-integ-location-tracker/Tracker", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-integ-location-tracker/Tracker/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Location::Tracker", + "aws:cdk:cloudformation:props": { + "description": "test tracker", + "eventBridgeEnabled": true, + "kmsKeyEnableGeospatialQueries": true, + "kmsKeyId": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + }, + "positionFiltering": "AccuracyBased", + "trackerName": "MyTracker" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "TrackerConsumer": { + "id": "TrackerConsumer", + "path": "cdk-integ-location-tracker/TrackerConsumer", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-integ-location-tracker/TrackerConsumer/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Location::TrackerConsumer", + "aws:cdk:cloudformation:props": { + "consumerArn": { + "Fn::GetAtt": [ + "GeofenceCollection6FAC681F", + "Arn" + ] + }, + "trackerName": { + "Ref": "TrackerAF5FC55F" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "cdk-integ-location-tracker/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "cdk-integ-location-tracker/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "TrackerTest": { + "id": "TrackerTest", + "path": "TrackerTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "TrackerTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "TrackerTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "TrackerTest/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "TrackerTest/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "TrackerTest/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.ts b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.ts new file mode 100644 index 0000000000000..5c524137e7ba5 --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/integ.tracker.ts @@ -0,0 +1,40 @@ +import { App, RemovalPolicy, Stack } from 'aws-cdk-lib'; +import * as kms from 'aws-cdk-lib/aws-kms'; +import * as integ from '@aws-cdk/integ-tests-alpha'; +import { Construct } from 'constructs'; +import { PositionFiltering, Tracker } from '../lib/tracker'; +import { GeofenceCollection } from '../lib'; +import { TrackerConsumer } from '../lib/tracker-consumer'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const geofenceCollection = new GeofenceCollection(this, 'GeofenceCollection', { + geofenceCollectionName: 'MyGeofenceCollection', + description: 'test', + }); + + const key = new kms.Key(this, 'Key', { removalPolicy: RemovalPolicy.DESTROY }); + + const tracker = new Tracker(this, 'Tracker', { + trackerName: 'MyTracker', + description: 'test tracker', + eventBridgeEnabled: true, + kmsKeyEnableGeospatialQueries: true, + kmsKey: key, + positionFiltering: PositionFiltering.ACCURACY_BASED, + }); + + new TrackerConsumer(this, 'TrackerConsumer', { + consumer: geofenceCollection, + tracker, + }); + } +} + +const app = new App(); + +new integ.IntegTest(app, 'TrackerTest', { + testCases: [new TestStack(app, 'cdk-integ-location-tracker')], +}); diff --git a/packages/@aws-cdk/aws-location-alpha/test/tracker-consumer.test.ts b/packages/@aws-cdk/aws-location-alpha/test/tracker-consumer.test.ts new file mode 100644 index 0000000000000..735bf1fe18356 --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/tracker-consumer.test.ts @@ -0,0 +1,25 @@ +import { Template } from 'aws-cdk-lib/assertions'; +import { Stack } from 'aws-cdk-lib'; +import { Tracker } from '../lib/tracker'; +import { GeofenceCollection } from '../lib'; +import { TrackerConsumer } from '../lib/tracker-consumer'; + +let stack: Stack; +beforeEach(() => { + stack = new Stack(); +}); + +test('create a tracker consumer', () => { + const geofenceCollection = new GeofenceCollection(stack, 'GeofenceCollection', {}); + const tracker = new Tracker(stack, 'Tracker', {}); + + new TrackerConsumer(stack, 'TrackerConsumer', { + tracker, + consumer: geofenceCollection, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Location::TrackerConsumer', { + ConsumerArn: stack.resolve(geofenceCollection.geofenceCollectionArn), + TrackerName: stack.resolve(tracker.trackerName), + }); +}); diff --git a/packages/@aws-cdk/aws-location-alpha/test/tracker.test.ts b/packages/@aws-cdk/aws-location-alpha/test/tracker.test.ts new file mode 100644 index 0000000000000..b03b2588b8cec --- /dev/null +++ b/packages/@aws-cdk/aws-location-alpha/test/tracker.test.ts @@ -0,0 +1,162 @@ +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 { PositionFiltering, Tracker } from '../lib/tracker'; + +let stack: Stack; +beforeEach(() => { + stack = new Stack(); +}); + +test('create a tracker', () => { + const key = new kms.Key(stack, 'Key'); + + new Tracker(stack, 'Tracker', { + trackerName: 'MyTracker', + description: 'My Tracker', + eventBridgeEnabled: true, + kmsKeyEnableGeospatialQueries: true, + kmsKey: key, + positionFiltering: PositionFiltering.TIME_BASED, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Location::Tracker', { + TrackerName: 'MyTracker', + Description: 'My Tracker', + EventBridgeEnabled: true, + KmsKeyEnableGeospatialQueries: true, + KmsKeyId: stack.resolve(key.keyArn), + PositionFiltering: PositionFiltering.TIME_BASED, + }); +}); + +test('creates a tracker with empty description', () => { + new Tracker(stack, 'Tracker', { + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Location::Tracker', { + }); +}); + +test('creates a tracker with empty description', () => { + new Tracker(stack, 'Tracker', { + description: '', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Location::Tracker', { + Description: '', + }); +}); + +test('throws with invalid description', () => { + expect(() => new Tracker(stack, 'Tracker', { + description: 'a'.repeat(1001), + })).toThrow('`description` must be between 0 and 1000 characters. Received: 1001 characters'); +}); + +test('throws with invalid name', () => { + expect(() => new Tracker(stack, 'Tracker', { + trackerName: 'inv@lid', + })).toThrow(/Invalid tracker name/); +}); + +test('throws when kmsKeyEnableGeospatialQueries is true without a customer managed key', () => { + expect(() => new Tracker(stack, 'Tracker', { + kmsKeyEnableGeospatialQueries: true, + })).toThrow('`kmsKeyEnableGeospatialQueries` can only be enabled that are configured to use an AWS KMS customer managed key'); +}); + +test('grant update device positions action', () => { + const tracker = new Tracker(stack, 'Tracker', {}); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('foo'), + }); + + tracker.grantUpdateDevicePositions(role); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.objectLike({ + PolicyDocument: Match.objectLike({ + Statement: [ + { + Action: 'geo:BatchUpdateDevicePosition', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'TrackerAF5FC55F', + 'Arn', + ], + }, + }, + ], + }), + })); +}); + +test('grant resd device positions actions', () => { + const tracker = new Tracker(stack, 'Tracker', {}); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('foo'), + }); + + tracker.grantRead(role); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.objectLike({ + PolicyDocument: Match.objectLike({ + Statement: [ + { + Action: [ + 'geo:BatchGetDevicePosition', + 'geo:GetDevicePosition', + 'geo:GetDevicePositionHistory', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': + [ + 'TrackerAF5FC55F', + 'Arn', + ], + }, + '/*', + ], + ], + }, + }, + ], + }), + })); +}); + +test('import from arn', () => { + const trackerArn = stack.formatArn({ + service: 'geo', + resource: 'tracker', + resourceName: 'MyTracker', + }); + const tracker = Tracker.fromTrackerArn(stack, 'Tracker', trackerArn); + + // THEN + expect(tracker.trackerName).toEqual('MyTracker'); + expect(tracker.trackerArn).toEqual(trackerArn); +}); + +test('import from name', () => { + // WHEN + const trackerName = 'MyTracker'; + const tracker = Tracker.fromTrackerName(stack, 'Tracker', trackerName); + + // THEN + expect(tracker.trackerName).toEqual(trackerName); + expect(tracker.trackerArn).toEqual(stack.formatArn({ + service: 'geo', + resource: 'tracker', + resourceName: 'MyTracker', + })); +});