diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/app.py b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/app.py index 5e848d302..d37a21cc8 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/app.py +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/app.py @@ -101,7 +101,7 @@ def main(): # ------------------------------ service_props = service_tier.ServiceTierProps( database=storage.database, - file_system=storage.file_system, + mountable_file_system=storage.mountable_file_system, vpc=network.vpc, ubl_certs_secret_arn=config.ubl_certificate_secret_arn, ubl_licenses=config.ubl_licenses, diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py index 3622de6f4..531f91b1e 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py @@ -30,7 +30,7 @@ from aws_rfdk import ( DistinguishedName, - IMountableLinuxFilesystem, + MountableEfs, SessionManagerHelper, X509CertificatePem ) @@ -58,8 +58,8 @@ class ServiceTierProps(StackProps): vpc: IVpc # The database to connect to. database: DatabaseConnection - # The file system to install Deadline Repository to. - file_system: IMountableLinuxFilesystem + # The file-system to install Deadline Repository to. + mountable_file_system: MountableEfs # The ARN of the secret containing the UBL certificates .zip file (in binary form). ubl_certs_secret_arn: typing.Optional[str] # The UBL licenses to configure @@ -109,9 +109,9 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, ] ) - # Granting the bastion access to the file system mount for convenience. - # This can also safely be removed. - props.file_system.mount_to_linux_instance( + # Mounting the root of the EFS file-system to the bastion access for convenience. + # This can safely be removed. + MountableEfs(self, filesystem=props.mountable_file_system.file_system).mount_to_linux_instance( self.bastion.instance, location='/mnt/efs' ) @@ -127,7 +127,7 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, 'Repository', vpc=props.vpc, database=props.database, - file_system=props.file_system, + file_system=props.mountable_file_system, repository_installation_timeout=Duration.minutes(20), version=self.version ) diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/storage_tier.py b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/storage_tier.py index 52858d34c..cc72bf697 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/storage_tier.py +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/storage_tier.py @@ -25,7 +25,10 @@ SubnetType ) from aws_cdk.aws_efs import ( + AccessPoint, + Acl, FileSystem, + PosixUser ) from aws_cdk.aws_route53 import ( IPrivateHostedZone @@ -75,20 +78,60 @@ def __init__(self, scope: Construct, stack_id: str, *, props: StorageTierProps, :param kwargs: Any kwargs that need to be passed on to the parent class. """ super().__init__(scope, stack_id, **kwargs) - # The file system to use (e.g. to install Deadline Repository onto). - self.file_system = MountableEfs( + + # The file-system to use (e.g. to install Deadline Repository onto). + file_system = FileSystem( + self, + 'EfsFileSystem', + vpc=props.vpc, + # TODO - Evaluate this removal policy for your own needs. This is set to DESTROY to + # cleanly remove everything when this stack is destroyed. If you would like to ensure + # that your data is not accidentally deleted, you should modify this value. + removal_policy=RemovalPolicy.DESTROY + ) + + # Create an EFS access point that is used to grant the Repository and RenderQueue with write access to the + # Deadline Repository directory in the EFS file-system. + access_point = AccessPoint( self, - filesystem=FileSystem( - self, - 'EfsFileSystem', - vpc=props.vpc, - # TODO - Evaluate this removal policy for your own needs. This is set to DESTROY to - # cleanly remove everything when this stack is destroyed. If you would like to ensure - # that your data is not accidentally deleted, you should modify this value. - removal_policy=RemovalPolicy.DESTROY + 'AccessPoint', + file_system=file_system, + + # The AccessPoint will create the directory (denoted by the path property below) if it doesn't exist with + # the owning UID/GID set as specified here. These should be set up to grant read and write access to the + # UID/GID configured in the "poxis_user" property below. + create_acl=Acl( + owner_uid='10000', + owner_gid='10000', + permissions='750', + ), + + # When you mount the EFS via the access point, the mount will be rooted at this path in the EFS file-system + path='/DeadlineRepository', + + # TODO - When you mount the EFS via the access point, all file-system operations will be performed using + # these UID/GID values instead of those from the user on the system where the EFS is mounted. + # + # The values below are set as UID/GID 0 for RFDK users that are migrating from previous version of this + # All-in-AWS-infrastructure-Basic example CDK application. This grants root (unrestricted) access to any + # mount of the EFS access point. Users migrating from previous versions of the example are advised to + # instead connect to a bastion instance with the EFS mounted and change the permissions of the Deadline + # Repository directory and files to be owned by a different UID/GID and modify these values accordingly. + # + # Anyone building a new CDK application based on this example are encouraged to change these to match the + # "ownerGid" and "ownerUid" above as a reasonable starting point. + posix_user=PosixUser( + uid='0', + gid='0' ) ) + self.mountable_file_system = MountableEfs( + self, + filesystem=file_system, + access_point=access_point + ) + # The database to connect Deadline to. self.database: Optional[DatabaseConnection] = None diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/app.ts b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/app.ts index 659cbb01b..0f3dce8d9 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/app.ts +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/app.ts @@ -98,7 +98,7 @@ if (config.deployMongoDB) { const service = new ServiceTier(app, 'ServiceTier', { env, database: storage.database, - fileSystem: storage.fileSystem, + mountableFileSystem: storage.mountableFileSystem, vpc: network.vpc, deadlineVersion: config.deadlineVersion, ublCertsSecretArn: config.ublCertificatesSecretArn, diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts index dbb050413..6763ba9ba 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts @@ -17,7 +17,7 @@ import { } from '@aws-cdk/aws-route53'; import * as cdk from '@aws-cdk/core'; import { - IMountableLinuxFilesystem, + MountableEfs, X509CertificatePem, } from 'aws-rfdk'; import { @@ -33,7 +33,6 @@ import { import { Secret, } from '@aws-cdk/aws-secretsmanager'; -import { Duration } from '@aws-cdk/core'; import { SessionManagerHelper } from 'aws-rfdk/lib/core'; /** @@ -51,9 +50,9 @@ export interface ServiceTierProps extends cdk.StackProps { readonly database: DatabaseConnection; /** - * The file system to install Deadline Repository to. + * The file-system to install Deadline Repository to. */ - readonly fileSystem: IMountableLinuxFilesystem; + readonly mountableFileSystem: MountableEfs; /** * Our self-signed root CA certificate for the internal endpoints in the farm. @@ -136,11 +135,15 @@ export class ServiceTier extends cdk.Stack { volume: BlockDeviceVolume.ebs(50, { encrypted: true, })}, - ] + ], }); - // Granting the bastion access to the file system mount for convenience + props.database.allowConnectionsFrom(this.bastion); + + // Granting the bastion access to the entire EFS file-system. // This can also be safely removed - props.fileSystem.mountToLinuxInstance(this.bastion.instance, { + new MountableEfs(this, { + filesystem: props.mountableFileSystem.fileSystem, + }).mountToLinuxInstance(this.bastion.instance, { location: '/mnt/efs', }); @@ -152,8 +155,9 @@ export class ServiceTier extends cdk.Stack { vpc: props.vpc, version: this.version, database: props.database, - fileSystem: props.fileSystem, - repositoryInstallationTimeout: Duration.minutes(20), + fileSystem: props.mountableFileSystem, + repositoryInstallationTimeout: cdk.Duration.minutes(20), + repositoryInstallationPrefix: "/", }); const images = new ThinkboxDockerImages(this, 'Images', { diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/storage-tier.ts b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/storage-tier.ts index 421ba3353..0b234c187 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/storage-tier.ts +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/storage-tier.ts @@ -10,11 +10,13 @@ import { } from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { DatabaseCluster } from '@aws-cdk/aws-docdb'; -import { FileSystem } from '@aws-cdk/aws-efs'; +import { + AccessPoint, + FileSystem, +} from '@aws-cdk/aws-efs'; import { IPrivateHostedZone } from '@aws-cdk/aws-route53'; import { RemovalPolicy, Duration } from '@aws-cdk/core'; import { - IMountableLinuxFilesystem, MongoDbInstance, MongoDbPostInstallSetup, MongoDbSsplLicenseAcceptance, @@ -45,9 +47,9 @@ export interface StorageTierProps extends cdk.StackProps { */ export abstract class StorageTier extends cdk.Stack { /** - * The file system to use (e.g. to install Deadline Repository onto). + * The mountable file-system to use for the Deadline Repository */ - public readonly fileSystem: IMountableLinuxFilesystem; + public readonly mountableFileSystem: MountableEfs; /** * The database to connect Deadline to. @@ -63,15 +65,51 @@ export abstract class StorageTier extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props: StorageTierProps) { super(scope, id, props); - this.fileSystem = new MountableEfs(this, { - filesystem: new FileSystem(this, 'EfsFileSystem', { - vpc: props.vpc, - encrypted: true, - // TODO - Evaluate this removal policy for your own needs. This is set to DESTROY to - // cleanly remove everything when this stack is destroyed. If you would like to ensure - // that your data is not accidentally deleted, you should modify this value. - removalPolicy: RemovalPolicy.DESTROY, - }), + const fileSystem = new FileSystem(this, 'EfsFileSystem', { + vpc: props.vpc, + encrypted: true, + // TODO - Evaluate this removal policy for your own needs. This is set to DESTROY to + // cleanly remove everything when this stack is destroyed. If you would like to ensure + // that your data is not accidentally deleted, you should modify this value. + removalPolicy: RemovalPolicy.DESTROY, + }); + + // Create an EFS access point that is used to grant the Repository and RenderQueue with write access to the Deadline + // Repository directory in the EFS file-system. + const accessPoint = new AccessPoint(this, 'AccessPoint', { + fileSystem, + + // The AccessPoint will create the directory (denoted by the "path" property below) if it doesn't exist with the + // owning UID/GID set as specified here. These should be set up to grant read and write access to the UID/GID + // configured in the "poxisUser" property below. + createAcl: { + ownerGid: '10000', + ownerUid: '10000', + permissions: '750', + }, + + // When you mount the EFS via the access point, the mount will be rooted at this path in the EFS file-system + path: '/DeadlineRepository', + + // TODO - When you mount the EFS via the access point, all file-system operations will be performed using these + // UID/GID values instead of those from the user on the system where the EFS is mounted. + // + // The values below are set as UID/GID 0 for RFDK users that are migrating from previous version of this + // All-in-AWS-infrastructure-Basic example CDK application. This grants root (unrestricted) access to any mount of + // the EFS access point. Users migrating from previous versions of the example are advised to instead connect to a + // bastion instance with the EFS mounted and change the permissions of the Deadline repository directory and files + // to be owned by a different UID/GID and modify these values accordingly. Anyone building a new CDK application + // based on this example are encouraged to change these to match the "ownerGid" and "ownerUid" above as a + // reasonable starting point. + posixUser: { + uid: '0', + gid: '0', + }, + }); + + this.mountableFileSystem = new MountableEfs(this, { + filesystem: fileSystem, + accessPoint, }); } } diff --git a/packages/aws-rfdk/lib/core/lib/mount-permissions-helper.ts b/packages/aws-rfdk/lib/core/lib/mount-permissions-helper.ts index 89e35839d..59f94d342 100644 --- a/packages/aws-rfdk/lib/core/lib/mount-permissions-helper.ts +++ b/packages/aws-rfdk/lib/core/lib/mount-permissions-helper.ts @@ -29,4 +29,21 @@ export class MountPermissionsHelper { } throw new Error(`Unhandled MountPermission: ${permission}`); } + + /** + * Convert the given permission into the appropriate list of IAM actions allowed on the EFS FileSystem required for + * the mount. + * + * @param permission The permission to convert. Defaults to {@link MountPermissions.READWRITE} if not defined. + */ + public static toEfsIAMActions(permission?: MountPermissions): string[] { + permission = permission ?? MountPermissions.READWRITE; + const iamActions = [ + 'elasticfilesystem:ClientMount', + ]; + if (permission === MountPermissions.READWRITE) { + iamActions.push('elasticfilesystem:ClientWrite'); + } + return iamActions; + } } diff --git a/packages/aws-rfdk/lib/core/lib/mountable-efs.ts b/packages/aws-rfdk/lib/core/lib/mountable-efs.ts index 2744877b5..095470a0d 100644 --- a/packages/aws-rfdk/lib/core/lib/mountable-efs.ts +++ b/packages/aws-rfdk/lib/core/lib/mountable-efs.ts @@ -10,6 +10,9 @@ import { Port, } from '@aws-cdk/aws-ec2'; import * as efs from '@aws-cdk/aws-efs'; +import { + PolicyStatement, +} from '@aws-cdk/aws-iam'; import { Asset, } from '@aws-cdk/aws-s3-assets'; @@ -37,6 +40,13 @@ export interface MountableEfsProps { */ readonly filesystem: efs.IFileSystem; + /** + * An optional access point to use for mounting the file-system + * + * @default no access point is used + */ + readonly accessPoint?: efs.IAccessPoint; + /** * Extra NFSv4 mount options that will be added to /etc/fstab for the file system. * See: {@link https://www.man7.org/linux/man-pages//man5/nfs.5.html} @@ -65,7 +75,14 @@ export interface MountableEfsProps { * @todo Add support for specifying an AccessPoint for the EFS filesystem to enforce user and group information for all file system requests. */ export class MountableEfs implements IMountableLinuxFilesystem { - constructor(protected readonly scope: Construct, protected readonly props: MountableEfsProps) {} + /** + * The underlying EFS filesystem that is mounted + */ + public readonly fileSystem: efs.IFileSystem; + + constructor(protected readonly scope: Construct, protected readonly props: MountableEfsProps) { + this.fileSystem = props.filesystem; + } /** * @inheritdoc @@ -75,6 +92,23 @@ export class MountableEfs implements IMountableLinuxFilesystem { throw new Error('Target instance must be Linux.'); } + target.node.addDependency(this.props.filesystem.mountTargetsAvailable); + + if (this.props.accessPoint) { + const grantActions = MountPermissionsHelper.toEfsIAMActions(mount?.permissions); + target.grantPrincipal.addToPrincipalPolicy(new PolicyStatement({ + resources: [ + (this.props.filesystem.node.defaultChild as efs.CfnFileSystem).attrArn, + ], + actions: grantActions, + conditions: { + StringEquals: { + 'elasticfilesystem:AccessPointArn': this.props.accessPoint.accessPointArn, + }, + }, + })); + } + target.connections.allowTo(this.props.filesystem, this.props.filesystem.connections.defaultPort as Port); const mountScriptAsset = this.mountAssetSingleton(); @@ -86,8 +120,14 @@ export class MountableEfs implements IMountableLinuxFilesystem { const mountDir: string = path.posix.normalize(mount.location); const mountOptions: string[] = [ MountPermissionsHelper.toLinuxMountOption(mount.permissions) ]; + if (this.props.accessPoint) { + mountOptions.push( + 'iam', + `accesspoint=${this.props.accessPoint.accessPointId}`, + ); + } if (this.props.extraMountOptions) { - mountOptions.push( ...this.props.extraMountOptions); + mountOptions.push(...this.props.extraMountOptions); } const mountOptionsStr: string = mountOptions.join(','); diff --git a/packages/aws-rfdk/lib/core/lib/mountable-filesystem.ts b/packages/aws-rfdk/lib/core/lib/mountable-filesystem.ts index ba4f45439..e5e6fb70d 100644 --- a/packages/aws-rfdk/lib/core/lib/mountable-filesystem.ts +++ b/packages/aws-rfdk/lib/core/lib/mountable-filesystem.ts @@ -6,6 +6,9 @@ import { IConnectable, } from '@aws-cdk/aws-ec2'; +import { + IConstruct, +} from '@aws-cdk/core'; import { IScriptHost } from './script-assets'; @@ -14,7 +17,7 @@ import { IScriptHost } from './script-assets'; * {@link https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.Instance.html|EC2 Instance} * or an {@link https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-autoscaling.AutoScalingGroup.html|EC2 Auto Scaling Group} */ -export interface IMountingInstance extends IConnectable, IScriptHost { +export interface IMountingInstance extends IConnectable, IConstruct, IScriptHost { } /** diff --git a/packages/aws-rfdk/lib/core/test/mount-permissions-helper.test.ts b/packages/aws-rfdk/lib/core/test/mount-permissions-helper.test.ts index f5be47ff1..0b0dcdf5f 100644 --- a/packages/aws-rfdk/lib/core/test/mount-permissions-helper.test.ts +++ b/packages/aws-rfdk/lib/core/test/mount-permissions-helper.test.ts @@ -17,3 +17,29 @@ test.each([ ])('toLinuxMountOption test: %p', (permission, expected) => { expect(MountPermissionsHelper.toLinuxMountOption(permission)).toBe(expected); }); + +test.each<[MountPermissions | undefined, string[]]>([ + [ + MountPermissions.READONLY, + [ + 'elasticfilesystem:ClientMount', + ], + ], + [ + MountPermissions.READWRITE, + [ + 'elasticfilesystem:ClientMount', + 'elasticfilesystem:ClientWrite', + ], + ], + [ + undefined, + [ + 'elasticfilesystem:ClientMount', + 'elasticfilesystem:ClientWrite', + ], + ], +])('toEfsIAMActions test: %p', (permission, expected) => { + expect(MountPermissionsHelper.toEfsIAMActions(permission)).toEqual(expected); +}); + diff --git a/packages/aws-rfdk/lib/core/test/mountable-ebs.test.ts b/packages/aws-rfdk/lib/core/test/mountable-ebs.test.ts index 22d3764c8..467804e2c 100644 --- a/packages/aws-rfdk/lib/core/test/mountable-ebs.test.ts +++ b/packages/aws-rfdk/lib/core/test/mountable-ebs.test.ts @@ -270,6 +270,7 @@ describe('Test MountableBlockVolume', () => { public readonly osType = instance.osType; public readonly userData = instance.userData; public readonly grantPrincipal = instance.grantPrincipal; + public readonly node = instance.node; } const fakeTarget = new FakeTarget(); diff --git a/packages/aws-rfdk/lib/core/test/mountable-efs.test.ts b/packages/aws-rfdk/lib/core/test/mountable-efs.test.ts index ff02cd679..4bd14c928 100644 --- a/packages/aws-rfdk/lib/core/test/mountable-efs.test.ts +++ b/packages/aws-rfdk/lib/core/test/mountable-efs.test.ts @@ -4,8 +4,10 @@ */ import { + arrayWith, expect as cdkExpect, haveResourceLike, + objectLike, } from '@aws-cdk/assert'; import { AmazonLinuxGeneration, @@ -17,6 +19,8 @@ import { } from '@aws-cdk/aws-ec2'; import * as efs from '@aws-cdk/aws-efs'; import { + App, + CfnResource, Stack, } from '@aws-cdk/core'; @@ -25,18 +29,24 @@ import { MountPermissions, } from '../lib'; +import { + MountPermissionsHelper, +} from '../lib/mount-permissions-helper'; + import { escapeTokenRegex, } from './token-regex-helpers'; describe('Test MountableEFS', () => { + let app: App; let stack: Stack; let vpc: Vpc; let efsFS: efs.FileSystem; let instance: Instance; beforeEach(() => { - stack = new Stack(); + app = new App(); + stack = new Stack(app); vpc = new Vpc(stack, 'Vpc'); efsFS = new efs.FileSystem(stack, 'EFS', { vpc }); instance = new Instance(stack, 'Instance', { @@ -122,6 +132,79 @@ describe('Test MountableEFS', () => { expect(userData).toMatch(new RegExp(escapeTokenRegex('mountEfs.sh ${Token[TOKEN.\\d+]} /mnt/efs/fs1 r'))); }); + describe.each<[MountPermissions | undefined]>([ + [undefined], + [MountPermissions.READONLY], + [MountPermissions.READWRITE], + ])('access point with %s access permissions', (mountPermission) => { + // GIVEN + const expectedActions: string[] = MountPermissionsHelper.toEfsIAMActions(mountPermission); + const mountPath = '/mnt/efs/fs1'; + + let userData: any; + let accessPoint: efs.AccessPoint; + let expectedMountMode: string; + + beforeEach(() => { + // GIVEN + accessPoint = new efs.AccessPoint(stack, 'AccessPoint', { + fileSystem: efsFS, + }); + const mount = new MountableEfs(efsFS, { + filesystem: efsFS, + accessPoint, + }); + expectedMountMode = (mountPermission === MountPermissions.READONLY) ? 'ro' : 'rw'; + + // WHEN + mount.mountToLinuxInstance(instance, { + location: mountPath, + permissions: mountPermission, + }); + userData = stack.resolve(instance.userData.render()); + }); + + test('userdata specifies access point when mounting', () => { + // THEN + expect(userData).toEqual({ + 'Fn::Join': [ + '', + expect.arrayContaining([ + expect.stringMatching(new RegExp('(\\n|^)bash \\./mountEfs.sh $')), + stack.resolve(efsFS.fileSystemId), + ` ${mountPath} ${expectedMountMode},iam,accesspoint=`, + stack.resolve(accessPoint.accessPointId), + expect.stringMatching(/^\n/), + ]), + ], + }); + }); + + test('grants IAM access point permissions', () => { + cdkExpect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: objectLike({ + Statement: arrayWith( + { + Action: expectedActions.length === 1 ? expectedActions[0] : expectedActions, + Condition: { + StringEquals: { + 'elasticfilesystem:AccessPointArn': stack.resolve(accessPoint.accessPointArn), + }, + }, + Effect: 'Allow', + Resource: stack.resolve((efsFS.node.defaultChild as efs.CfnFileSystem).attrArn), + }, + ), + Version: '2012-10-17', + }), + Roles: arrayWith( + // The Policy construct micro-optimizes the reference to a role in the same stack using its logical ID + stack.resolve((instance.role.node.defaultChild as CfnResource).ref), + ), + })); + }); + }); + test('extra mount options', () => { // GIVEN const mount = new MountableEfs(efsFS, { diff --git a/packages/aws-rfdk/lib/deadline/lib/repository.ts b/packages/aws-rfdk/lib/deadline/lib/repository.ts index e3f45a316..9a7f50f31 100644 --- a/packages/aws-rfdk/lib/deadline/lib/repository.ts +++ b/packages/aws-rfdk/lib/deadline/lib/repository.ts @@ -488,11 +488,8 @@ export class Repository extends Construct implements IRepository { this.version = props.version; - // Set up the Filesystem of the repository - if (props.fileSystem !== undefined) { - this.fileSystem = props.fileSystem; - } else { - this.efs = new EfsFileSystem(this, 'FileSystem', { + this.fileSystem = props.fileSystem ?? (() => { + const fs = new EfsFileSystem(this, 'FileSystem', { vpc: props.vpc, vpcSubnets: props.vpcSubnets ?? { subnetType: SubnetType.PRIVATE }, encrypted: true, @@ -500,10 +497,26 @@ export class Repository extends Construct implements IRepository { removalPolicy: props.removalPolicy?.filesystem ?? RemovalPolicy.RETAIN, securityGroup: props.securityGroupsOptions?.fileSystem, }); - this.fileSystem = new MountableEfs(this, { - filesystem: this.efs, + + const accessPoint = fs.addAccessPoint('AccessPoint', { + path: '/DeadlineRepository', + createAcl: { + ownerUid: '10000', + ownerGid: '10000', + permissions: '770', + }, + posixUser: { + uid: '10000', + gid: '10000', + }, }); - } + + return new MountableEfs(this, { + filesystem: fs, + accessPoint, + extraMountOptions: [`accesspoint=${accessPoint.accessPointId}`], + }); + })(); // Set up the Database of the repository if (props.database) { diff --git a/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-recipes.ts b/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-recipes.ts index 8bb220fb4..74df7dc5e 100644 --- a/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-recipes.ts +++ b/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-recipes.ts @@ -96,16 +96,20 @@ export class ThinkboxDockerRecipes extends Construct { */ public readonly ublImages: UsageBasedLicensingImages; + /** + * The staged recipes + */ + private readonly stage: Stage; + /** * The version of Deadline in the stage directory. */ - public readonly version: IVersion; + private versionInstance?: IVersion; constructor(scope: Construct, id: string, props: ThinkboxDockerRecipesProps) { super(scope, id); - this.version = props.stage.getVersion(this, 'Version'); - + this.stage = props.stage; for (const recipe of [ThinkboxManagedDeadlineDockerRecipes.REMOTE_CONNECTION_SERVER, ThinkboxManagedDeadlineDockerRecipes.LICENSE_FORWARDER]) { if (!props.stage.manifest.recipes[recipe]) { throw new Error(`Could not find ${recipe} recipe`); @@ -132,4 +136,11 @@ export class ThinkboxDockerRecipes extends Construct { licenseForwarder: ContainerImage.fromDockerImageAsset(this.licenseForwarder), }; } + + public get version(): IVersion { + if (!this.versionInstance) { + this.versionInstance = this.stage.getVersion(this, 'Version'); + } + return this.versionInstance; + } } diff --git a/packages/aws-rfdk/lib/deadline/test/repository.test.ts b/packages/aws-rfdk/lib/deadline/test/repository.test.ts index cd0192bae..d0c1db186 100644 --- a/packages/aws-rfdk/lib/deadline/test/repository.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/repository.test.ts @@ -31,6 +31,7 @@ import { WindowsVersion, } from '@aws-cdk/aws-ec2'; import { + CfnFileSystem, FileSystem as EfsFileSystem, } from '@aws-cdk/aws-efs'; import { Bucket } from '@aws-cdk/aws-s3'; @@ -971,9 +972,7 @@ test('configureClientInstance uses singleton for repo config script', () => { // Make sure that both instances have access to the same Asset for the configureRepositoryDirectConnect script expectCDK(stack).to(countResourcesLike('AWS::IAM::Policy', 2, { PolicyDocument: { - Statement: [ - {}, // secretsmanager:GetSecretValue for docdb secret - {}, // asset access for EFS mount script + Statement: arrayWith( { Effect: 'Allow', Action: [ @@ -1015,7 +1014,7 @@ test('configureClientInstance uses singleton for repo config script', () => { }, ], }, - ], + ), }, })); }); @@ -1153,3 +1152,29 @@ test('validates VersionQuery is not in a different stack', () => { // THEN expect(synth).toThrow('A VersionQuery can not be supplied from a different stack'); }); + +test('creates an EFS AccessPoint when no filesystem is supplied', () => { + // WHEN + const repo = new Repository(stack, 'Repository', { + version, + vpc, + }); + + // THEN + const efsResource = (repo.node.findChild('FileSystem') as CfnElement).node.defaultChild as CfnFileSystem; + expectCDK(stack).to(haveResourceLike('AWS::EFS::AccessPoint', { + FileSystemId: stack.resolve(efsResource.ref), + PosixUser: { + Gid: '10000', + Uid: '10000', + }, + RootDirectory: { + CreationInfo: { + OwnerGid: '10000', + OwnerUid: '10000', + Permissions: '770', + }, + Path: '/DeadlineRepository', + }, + })); +}); diff --git a/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-recipes.test.ts b/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-recipes.test.ts index fde6f4f76..b3476599a 100644 --- a/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-recipes.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-recipes.test.ts @@ -7,6 +7,7 @@ import * as path from 'path'; import { expect as expectCDK, + haveResource, haveResourceLike, stringLike, } from '@aws-cdk/assert'; @@ -134,16 +135,30 @@ describe('ThinkboxDockerRecipes', () => { expect(actualImage.sourceHash).toEqual(expectedImage.sourceHash); }); - test('provides the Deadline version', () => { - // WHEN - new ThinkboxDockerRecipes(stack, 'Recipes', { - stage, + describe('.version', () => { + test('creates a VersionQuery when referenced', () => { + // GIVEN + const recipes = new ThinkboxDockerRecipes(stack, 'Recipes', { + stage, + }); + + // WHEN + recipes.version; + + expectCDK(stack).to(haveResourceLike('Custom::RFDK_DEADLINE_INSTALLERS', { + forceRun: stringLike('*'), + versionString: RELEASE_VERSION_STRING, + })); }); - expectCDK(stack).to(haveResourceLike('Custom::RFDK_DEADLINE_INSTALLERS', { - forceRun: stringLike('*'), - versionString: RELEASE_VERSION_STRING, - })); + test('does not create a VersionQuery when not referenced', () => { + // GIVEN + new ThinkboxDockerRecipes(stack, 'Recipes', { + stage, + }); + + expectCDK(stack).notTo(haveResource('Custom::RFDK_DEADLINE_INSTALLERS')); + }); }); test.each([