diff --git a/packages/@aws-cdk/aws-gamelift/.gitignore b/packages/@aws-cdk/aws-gamelift/.gitignore index ff82c8959aad2..da70d0d9db790 100644 --- a/packages/@aws-cdk/aws-gamelift/.gitignore +++ b/packages/@aws-cdk/aws-gamelift/.gitignore @@ -23,4 +23,5 @@ junit.xml !**/*.integ.snapshot/**/asset.*/** #include game build js file -!test/my-game-build/*.js +!test/my-game-build/*.js +!test/my-game-script/*.js diff --git a/packages/@aws-cdk/aws-gamelift/README.md b/packages/@aws-cdk/aws-gamelift/README.md index 3d84f4b418ebb..48a572920d223 100644 --- a/packages/@aws-cdk/aws-gamelift/README.md +++ b/packages/@aws-cdk/aws-gamelift/README.md @@ -66,7 +66,9 @@ your game server files. This section provides guidance on preparing and uploadin files or Realtime Servers server script files. When you upload files, you create a GameLift build or script resource, which you then deploy on fleets of hosting resources. -### Upload a custom server build to GameLift +To troubleshoot fleet activation problems related to the server script, see [Debug GameLift fleet issues](https://docs.aws.amazon.com/gamelift/latest/developerguide/fleets-creating-debug.html). + +#### Upload a custom server build to GameLift Before uploading your configured game server to GameLift for hosting, package the game build files into a build directory. This directory must include all components required to run your game servers and host game sessions, including the following: @@ -89,3 +91,21 @@ new gamelift.Build(this, 'Build', { content: gamelift.Content.fromBucket(bucket, "sample-asset-key") }); ``` + +#### Upload a realtime server Script + +Your server script can include one or more files combined into a single .zip file for uploading. The .zip file must contain +all files that your script needs to run. + +You can store your zipped script files in either a local file directory or in an Amazon Simple Storage Service (Amazon S3) +bucket or defines a directory asset which is archived as a .zip file and uploaded to S3 during deployment. + +After you create the script resource, GameLift deploys the script with a new Realtime Servers fleet. GameLift installs your +server script onto each instance in the fleet, placing the script files in `/local/game`. + +```ts +declare const bucket: s3.Bucket; +new gamelift.Script(this, 'Script', { + content: gamelift.Content.fromBucket(bucket, "sample-asset-key") +}); +``` diff --git a/packages/@aws-cdk/aws-gamelift/lib/build.ts b/packages/@aws-cdk/aws-gamelift/lib/build.ts index 12c066880473c..a2d4422e47453 100644 --- a/packages/@aws-cdk/aws-gamelift/lib/build.ts +++ b/packages/@aws-cdk/aws-gamelift/lib/build.ts @@ -7,7 +7,13 @@ import { Content } from './content'; import { CfnBuild } from './gamelift.generated'; /** - * Represents a GameLift server build. + * Your custom-built game server software that runs on GameLift and hosts game sessions for your players. + * A game build represents the set of files that run your game server on a particular operating system. + * You can have many different builds, such as for different flavors of your game. + * The game build must be integrated with the GameLift service. + * You upload game build files to the GameLift service in the Regions where you plan to set up fleets. + * + * @see https://docs.aws.amazon.com/gamelift/latest/developerguide/gamelift-build-cli-uploading.html */ export interface IBuild extends cdk.IResource, iam.IGrantable { diff --git a/packages/@aws-cdk/aws-gamelift/lib/index.ts b/packages/@aws-cdk/aws-gamelift/lib/index.ts index cf989967b922d..13cc0c8eada9c 100644 --- a/packages/@aws-cdk/aws-gamelift/lib/index.ts +++ b/packages/@aws-cdk/aws-gamelift/lib/index.ts @@ -1,5 +1,6 @@ export * from './content'; export * from './build'; +export * from './script'; // AWS::GameLift CloudFormation Resources: export * from './gamelift.generated'; diff --git a/packages/@aws-cdk/aws-gamelift/lib/script.ts b/packages/@aws-cdk/aws-gamelift/lib/script.ts new file mode 100644 index 0000000000000..f387388732486 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/lib/script.ts @@ -0,0 +1,243 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { Content } from './content'; +import { CfnScript } from './gamelift.generated'; + +/** + * Your configuration and custom game logic for use with Realtime Servers. + * Realtime Servers are provided by GameLift to use instead of a custom-built game server. + * You configure Realtime Servers for your game clients by creating a script using JavaScript, + * and add custom game logic as appropriate to host game sessions for your players. + * You upload the Realtime script to the GameLift service in the Regions where you plan to set up fleets. + * + * @see https://docs.aws.amazon.com/gamelift/latest/developerguide/realtime-script-uploading.html + */ +export interface IScript extends cdk.IResource, iam.IGrantable { + + /** + * The Identifier of the realtime server script. + * + * @attribute + */ + readonly scriptId: string; + + /** + * The ARN of the realtime server script. + * + * @attribute + */ + readonly scriptArn: string; +} + +/** + * Base class for new and imported GameLift realtime server script. + */ +export abstract class ScriptBase extends cdk.Resource implements IScript { + /** + * The Identifier of the realtime server script. + */ + public abstract readonly scriptId: string; + public abstract readonly scriptArn: string; + + public abstract readonly grantPrincipal: iam.IPrincipal; +} + +/** + * Represents a Script content defined outside of this stack. + */ +export interface ScriptAttributes { + /** + * The ARN of the realtime server script + */ + readonly scriptArn: string; + /** + * The IAM role assumed by GameLift to access server script in S3. + * @default - undefined + */ + readonly role?: iam.IRole; +} + +/** + * Properties for a new realtime server script + */ +export interface ScriptProps { + /** + * Name of this realtime server script + * + * @default No name + */ + readonly scriptName?: string; + + /** + * Version of this realtime server script + * + * @default No version + */ + readonly scriptVersion?: string; + + /** + * The game content + */ + readonly content: Content; + + /** + * The IAM role assumed by GameLift to access server script in S3. + * If providing a custom role, it needs to trust the GameLift service principal (gamelift.amazonaws.com) and be granted sufficient permissions + * to have Read access to a specific key content into a specific S3 bucket. + * Below an example of required permission: + * { + * "Version": "2012-10-17", + * "Statement": [{ + * "Effect": "Allow", + * "Action": [ + * "s3:GetObject", + * "s3:GetObjectVersion" + * ], + * "Resource": "arn:aws:s3:::bucket-name/object-name" + * }] + *} + * + * @see https://docs.aws.amazon.com/gamelift/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_id-based-policy-examples-access-storage-loc + * + * @default - a role will be created with default permissions. + */ + readonly role?: iam.IRole; +} + +/** + * A GameLift script, that is installed and runs on instances in an Amazon GameLift fleet. It consists of + * a zip file with all of the components of the realtime game server script. + * + * @see https://docs.aws.amazon.com/gamelift/latest/developerguide/realtime-script-uploading.html + * + * @resource AWS::GameLift::Script + */ +export class Script extends ScriptBase { + + /** + * Create a new realtime server script from s3 content + */ + static fromBucket(scope: Construct, id: string, bucket: s3.IBucket, key: string, objectVersion?: string) { + return new Script(scope, id, { + content: Content.fromBucket(bucket, key, objectVersion), + }); + } + + /** + * Create a new realtime server script from asset content + */ + static fromAsset(scope: Construct, id: string, path: string, options?: s3_assets.AssetOptions) { + return new Script(scope, id, { + content: Content.fromAsset(path, options), + }); + } + + /** + * Import a script into CDK using its ARN + */ + static fromScriptArn(scope: Construct, id: string, scriptArn: string): IScript { + return this.fromScriptAttributes(scope, id, { scriptArn }); + } + + /** + * Import an existing realtime server script from its attributes. + */ + static fromScriptAttributes(scope: Construct, id: string, attrs: ScriptAttributes): IScript { + const scriptArn = attrs.scriptArn; + const scriptId = extractIdFromArn(attrs.scriptArn); + const role = attrs.role; + + class Import extends ScriptBase { + public readonly scriptArn = scriptArn; + public readonly scriptId = scriptId; + public readonly grantPrincipal:iam.IPrincipal; + public readonly role = role + + constructor(s: Construct, i: string) { + super(s, i, { + environmentFromArn: scriptArn, + }); + + this.grantPrincipal = this.role || new iam.UnknownPrincipal({ resource: this }); + } + } + + return new Import(scope, id); + } + + /** + * The Identifier of the realtime server script. + */ + public readonly scriptId: string; + + /** + * The ARN of the realtime server script. + */ + public readonly scriptArn: string; + + /** + * The IAM role GameLift assumes to acccess server script content. + */ + public readonly role: iam.IRole; + + /** + * The principal this GameLift script is using. + */ + public readonly grantPrincipal: iam.IPrincipal; + + constructor(scope: Construct, id: string, props: ScriptProps) { + super(scope, id, { + physicalName: props.scriptName, + }); + + if (props.scriptName && !cdk.Token.isUnresolved(props.scriptName)) { + if (props.scriptName.length > 1024) { + throw new Error(`Script name can not be longer than 1024 characters but has ${props.scriptName.length} characters.`); + } + } + this.role = props.role ?? new iam.Role(this, 'ServiceRole', { + assumedBy: new iam.ServicePrincipal('gamelift.amazonaws.com'), + }); + this.grantPrincipal = this.role; + const content = props.content.bind(this, this.role); + + const resource = new CfnScript(this, 'Resource', { + name: props.scriptName, + version: props.scriptVersion, + storageLocation: { + bucket: content.s3Location && content.s3Location.bucketName, + key: content.s3Location && content.s3Location.objectKey, + objectVersion: content.s3Location && content.s3Location.objectVersion, + roleArn: this.role.roleArn, + }, + }); + + this.scriptId = this.getResourceNameAttribute(resource.ref); + this.scriptArn = this.getResourceArnAttribute(resource.attrArn, { + service: 'gamelift', + resource: `script/${this.physicalName}`, + arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME, + }); + } +} + +/** + * Given an opaque (token) ARN, returns a CloudFormation expression that extracts the script + * identifier from the ARN. + * + * Script ARNs look like this: + * + * arn:aws:gamelift:region:account-id:script/script-identifier + * + * ..which means that in order to extract the `script-identifier` component from the ARN, we can + * split the ARN using ":" and select the component in index 5 then split using "/" and select the component in index 1. + * + * @returns the script identifier from his ARN + */ +function extractIdFromArn(arn: string) { + const splitValue = cdk.Fn.select(5, cdk.Fn.split(':', arn)); + return cdk.Fn.select(1, cdk.Fn.split('/', splitValue)); +} diff --git a/packages/@aws-cdk/aws-gamelift/test/build.test.ts b/packages/@aws-cdk/aws-gamelift/test/build.test.ts index 7bf763ea2ebfc..9e5b6bd6855db 100644 --- a/packages/@aws-cdk/aws-gamelift/test/build.test.ts +++ b/packages/@aws-cdk/aws-gamelift/test/build.test.ts @@ -5,7 +5,6 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import * as gamelift from '../lib'; -import { OperatingSystem } from '../lib'; describe('build', () => { const buildId = 'test-identifier'; @@ -207,13 +206,13 @@ describe('build', () => { build = new gamelift.Build(stack, 'BuildWithName', { ...defaultProps, buildName: buildName, - operatingSystem: OperatingSystem.AMAZON_LINUX_2, + operatingSystem: gamelift.OperatingSystem.AMAZON_LINUX_2, buildVersion: '1.0', }); Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { Name: buildName, - OperatingSystem: OperatingSystem.AMAZON_LINUX_2, + OperatingSystem: gamelift.OperatingSystem.AMAZON_LINUX_2, Version: '1.0', }); }); diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.script.ts b/packages/@aws-cdk/aws-gamelift/test/integ.script.ts new file mode 100644 index 0000000000000..fb85f5b00ab37 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.script.ts @@ -0,0 +1,13 @@ +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import * as gamelift from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-gamelift-script'); + +new gamelift.Script(stack, 'Script', { + content: gamelift.Content.fromAsset(path.join(__dirname, 'my-game-script')), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-gamelift/test/my-game-script/index.js b/packages/@aws-cdk/aws-gamelift/test/my-game-script/index.js new file mode 100644 index 0000000000000..73c02658c48d9 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/my-game-script/index.js @@ -0,0 +1 @@ +console.log('Hello World'); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7/index.js b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7/index.js new file mode 100644 index 0000000000000..73c02658c48d9 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7/index.js @@ -0,0 +1 @@ +console.log('Hello World'); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.assets.json b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.assets.json new file mode 100644 index 0000000000000..b5db81d532125 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.assets.json @@ -0,0 +1,32 @@ +{ + "version": "21.0.0", + "files": { + "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7": { + "source": { + "path": "asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222": { + "source": { + "path": "aws-gamelift-script.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222.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-gamelift/test/script.integ.snapshot/aws-gamelift-script.template.json b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.template.json new file mode 100644 index 0000000000000..fe0c724ffaad9 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.template.json @@ -0,0 +1,129 @@ +{ + "Resources": { + "ScriptServiceRole23DD8079": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "gamelift.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ScriptServiceRoleDefaultPolicyEE85DAE7": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ScriptServiceRoleDefaultPolicyEE85DAE7", + "Roles": [ + { + "Ref": "ScriptServiceRole23DD8079" + } + ] + } + }, + "Script09016516": { + "Type": "AWS::GameLift::Script", + "Properties": { + "StorageLocation": { + "Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "Key": "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7.zip", + "RoleArn": { + "Fn::GetAtt": [ + "ScriptServiceRole23DD8079", + "Arn" + ] + } + } + } + } + }, + "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-gamelift/test/script.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/integ.json b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/integ.json new file mode 100644 index 0000000000000..87f04519b0e06 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/integ.json @@ -0,0 +1,14 @@ +{ + "version": "21.0.0", + "testCases": { + "integ.script": { + "stacks": [ + "aws-gamelift-script" + ], + "diffAssets": false, + "stackUpdateWorkflow": true + } + }, + "synthContext": {}, + "enableLookups": false +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..5bc5612710eb2 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/manifest.json @@ -0,0 +1,64 @@ +{ + "version": "21.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-gamelift-script.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-gamelift-script.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-gamelift-script": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-gamelift-script.template.json", + "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}/9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-gamelift-script.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": [ + "aws-gamelift-script.assets" + ], + "metadata": { + "/aws-gamelift-script/Script/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ScriptServiceRole23DD8079" + } + ], + "/aws-gamelift-script/Script/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ScriptServiceRoleDefaultPolicyEE85DAE7" + } + ], + "/aws-gamelift-script/Script/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Script09016516" + } + ] + }, + "displayName": "aws-gamelift-script" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/tree.json b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/tree.json new file mode 100644 index 0000000000000..8a6195c91e0cc --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/tree.json @@ -0,0 +1,202 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.33" + } + }, + "script-test-assets": { + "id": "script-test-assets", + "path": "script-test-assets", + "children": { + "Script": { + "id": "Script", + "path": "script-test-assets/Script", + "children": { + "Service Role": { + "id": "Service Role", + "path": "script-test-assets/Script/Service Role", + "children": { + "Resource": { + "id": "Resource", + "path": "script-test-assets/Script/Service Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "gamelift.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "script-test-assets/Script/Service Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "script-test-assets/Script/Service Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "ScriptServiceRoleDefaultPolicy90803718", + "roles": [ + { + "Ref": "ScriptServiceRole4643E19E" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Content": { + "id": "Content", + "path": "script-test-assets/Script/Content", + "children": { + "Stage": { + "id": "Stage", + "path": "script-test-assets/Script/Content/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "script-test-assets/Script/Content/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "script-test-assets/Script/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::GameLift::Script", + "aws:cdk:cloudformation:props": { + "storageLocation": { + "bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "key": "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7.zip", + "roleArn": { + "Fn::GetAtt": [ + "ScriptServiceRole4643E19E", + "Arn" + ] + } + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-gamelift.CfnScript", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-gamelift.Script", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.test.ts b/packages/@aws-cdk/aws-gamelift/test/script.test.ts new file mode 100644 index 0000000000000..b7c5efc2f075d --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.test.ts @@ -0,0 +1,245 @@ +import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as gamelift from '../lib'; + +describe('script', () => { + const scriptId = 'script-test-identifier'; + const scriptArn = `arn:aws:gamelift:script-region:123456789012:script/${scriptId}`; + const scriptName = 'test-script'; + let stack: cdk.Stack; + + beforeEach(() => { + const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + stack = new cdk.Stack(app, 'Base', { + env: { account: '111111111111', region: 'stack-region' }, + }); + }); + + describe('.fromScriptArn()', () => { + test('with required fields', () => { + const script = gamelift.Script.fromScriptArn(stack, 'ImportedScript', scriptArn); + + expect(script.scriptArn).toEqual(scriptArn); + expect(script.grantPrincipal).toEqual(new iam.UnknownPrincipal({ resource: script })); + }); + }); + + describe('.fromScriptAttributes()', () => { + test('with required attrs only', () => { + const script = gamelift.Script.fromScriptAttributes(stack, 'ImportedScript', { scriptArn }); + + expect(script.scriptId).toEqual(scriptId); + expect(script.scriptArn).toEqual(scriptArn); + expect(script.env.account).toEqual('123456789012'); + expect(script.env.region).toEqual('script-region'); + expect(script.grantPrincipal).toEqual(new iam.UnknownPrincipal({ resource: script })); + }); + + test('with all attrs', () => { + const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/TestRole'); + const script = gamelift.Script.fromScriptAttributes(stack, 'ImportedScript', { scriptArn, role }); + + expect(scriptId).toEqual(scriptId); + expect(script.grantPrincipal).toEqual(role); + }); + }); + + describe('new', () => { + const localAsset = path.join(__dirname, 'my-game-script'); + const contentBucketName = 'bucketname'; + const contentBucketAccessStatement = { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + `:s3:::${contentBucketName}`, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + `:s3:::${contentBucketName}/content`, + ], + ], + }, + ], + }; + let contentBucket: s3.IBucket; + let content: gamelift.Content; + let script: gamelift.Script; + let defaultProps: gamelift.ScriptProps; + + beforeEach(() => { + contentBucket = s3.Bucket.fromBucketName(stack, 'ContentBucket', contentBucketName); + content = gamelift.Content.fromBucket(contentBucket, 'content'); + defaultProps = { + content, + }; + }); + + describe('.fromAsset()', () => { + test('should create a new script from asset', () => { + script = gamelift.Script.fromAsset(stack, 'ImportedScript', localAsset); + + expect(stack.node.metadata.find(m => m.type === 'aws:cdk:asset')).toBeDefined(); + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + StorageLocation: { + Bucket: { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3Bucket72AA8348', + }, + }, + }); + + }); + }); + + describe('.fromBucket()', () => { + test('should create a new script from bucket', () => { + script = gamelift.Script.fromBucket(stack, 'ImportedScript', contentBucket, 'content'); + + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + StorageLocation: { + Bucket: 'bucketname', + Key: 'content', + }, + }); + + }); + }); + + describe('with necessary props only', () => { + beforeEach(() => { + script = new gamelift.Script(stack, 'Script', defaultProps); + }); + + test('should create a role and use it with the script', () => { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'gamelift.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + + // Role policy should grant reading from the assets bucket + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + contentBucketAccessStatement, + ], + }, + Roles: [ + { + Ref: 'ScriptServiceRole23DD8079', + }, + ], + }); + + // check the script using the role + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + StorageLocation: { + Bucket: 'bucketname', + Key: 'content', + RoleArn: { + 'Fn::GetAtt': [ + 'ScriptServiceRole23DD8079', + 'Arn', + ], + }, + }, + }); + }); + + test('should return correct script attributes from CloudFormation', () => { + expect(stack.resolve(script.scriptId)).toEqual({ Ref: 'Script09016516' }); + expect(stack.resolve(script.scriptArn)).toEqual({ + 'Fn::GetAtt': [ + 'Script09016516', + 'Arn', + ], + }); + }); + + test('with a custom role should use it and set it in CloudFormation', () => { + const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/TestRole'); + script = new gamelift.Script(stack, 'ScriptWithRole', { + ...defaultProps, + role, + }); + + expect(script.grantPrincipal).toEqual(role); + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + StorageLocation: { + RoleArn: role.roleArn, + }, + }); + }); + + test('with a custom scriptName should set it in CloudFormation', () => { + script = new gamelift.Script(stack, 'ScriptWithName', { + ...defaultProps, + scriptName: scriptName, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + Name: scriptName, + }); + }); + + test('with all optional attributes should set it in CloudFormation', () => { + script = new gamelift.Script(stack, 'ScriptWithName', { + ...defaultProps, + scriptName: scriptName, + scriptVersion: '1.0', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + Name: scriptName, + Version: '1.0', + }); + }); + + test('with an incorrect scriptName (>1024)', () => { + let incorrectScriptName = ''; + for (let i = 0; i < 1025; i++) { + incorrectScriptName += 'A'; + } + + expect(() => new gamelift.Script(stack, 'ScriptWithWrongName', { + content, + scriptName: incorrectScriptName, + })).toThrow(/Script name can not be longer than 1024 characters but has 1025 characters./); + }); + }); + }); +}); + +