Skip to content

Commit

Permalink
feat(efs): import access point - fromAccessPointAttributes() (#10712)
Browse files Browse the repository at this point in the history
Cannot use `efs.AccessPoint.fromAccessPointId()` with `lambda.FileSystem.fromEfsAccessPoint()`. the former returns an `IAccessPoint` when the later expect an `AccessPoint`. I think following the CDK guidelines, `lambda.FileSystem.fromEfsAccessPoint()` should expect an `IAccessPoint`, not an `AccessPoint`.

Argument of type `IAccessPoint` is not assignable to parameter of type `AccessPoint`.

### Solution
----

Add a new import method to the `AccessPoint` class called `fromAccessPointAttributes()` allowing to pass a fileSystem as an attribute.

Closes #10711.


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
DaWyz authored Nov 11, 2020
1 parent f251ce6 commit ec72c85
Show file tree
Hide file tree
Showing 7 changed files with 542 additions and 40 deletions.
19 changes: 19 additions & 0 deletions packages/@aws-cdk/aws-efs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ the access point. You may specify custom path with the `path` property. If `path
created with the settings defined in the `creationInfo`. See
[Creating Access Points](https://docs.aws.amazon.com/efs/latest/ug/create-access-point.html) for more details.

Any access point that has been created outside the stack can be imported into your CDK app.

Use the `fromAccessPointAttributes()` API to import an existing access point.

```ts
efs.AccessPoint.fromAccessPointAttributes(this, 'ap', {
accessPointArn: 'fsap-1293c4d9832fo0912',
fileSystem: efs.FileSystem.fromFileSystemAttributes(this, 'efs', {
fileSystemId: 'fs-099d3e2f',
securityGroup: SecurityGroup.fromSecurityGroupId(this, 'sg', 'sg-51530134'),
}),
});
```

⚠️ Notice: When importing an Access Point using `fromAccessPointAttributes()`, you must make sure the mount targets are deployed and their lifecycle state is `available`. Otherwise, you may encounter the following error when deploying:
> EFS file system <ARN of efs> referenced by access point <ARN of access point of EFS> has
mount targets created in all availability zones the function will execute in, but not all are in the available life cycle
state yet. Please wait for them to become available and try the request again.

### Connecting

To control who can access the EFS, use the `.connections` attribute. EFS has
Expand Down
123 changes: 111 additions & 12 deletions packages/@aws-cdk/aws-efs/lib/access-point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ export interface IAccessPoint extends IResource {
* @attribute
*/
readonly accessPointArn: string;

/**
* The efs filesystem
*/
readonly fileSystem: IFileSystem;
}

/**
* Permissions as POSIX ACL
*/
*/
export interface Acl {
/**
* Specifies the POSIX user ID to apply to the RootDirectory. Accepts values from 0 to 2^32 (4294967295).
Expand Down Expand Up @@ -109,23 +114,71 @@ export interface AccessPointProps extends AccessPointOptions {
readonly fileSystem: IFileSystem;
}

/**
* Attributes that can be specified when importing an AccessPoint
*/
export interface AccessPointAttributes {
/**
* The ID of the AccessPoint
* One of this, of {@link accessPointArn} is required
*
* @default - determined based on accessPointArn
*/
readonly accessPointId?: string;

/**
* The ARN of the AccessPoint
* One of this, of {@link accessPointId} is required
*
* @default - determined based on accessPointId
*/
readonly accessPointArn?: string;

/**
* The EFS filesystem
*
* @default - no EFS filesystem
*/
readonly fileSystem?: IFileSystem;
}

abstract class AccessPointBase extends Resource implements IAccessPoint {
/**
* The ARN of the Access Point
* @attribute
*/
public abstract readonly accessPointArn: string;

/**
* The ID of the Access Point
* @attribute
*/
public abstract readonly accessPointId: string;

/**
* The filesystem of the access point
*/
public abstract readonly fileSystem: IFileSystem;
}

/**
* Represents the AccessPoint
*/
export class AccessPoint extends Resource implements IAccessPoint {
export class AccessPoint extends AccessPointBase {
/**
* Import an existing Access Point
* Import an existing Access Point by attributes
*/
public static fromAccessPointAttributes(scope: Construct, id: string, attrs: AccessPointAttributes): IAccessPoint {
return new ImportedAccessPoint(scope, id, attrs);
}

/**
* Import an existing Access Point by id
*/
public static fromAccessPointId(scope: Construct, id: string, accessPointId: string): IAccessPoint {
class Import extends Resource implements IAccessPoint {
public readonly accessPointId = accessPointId;
public readonly accessPointArn = Stack.of(scope).formatArn({
service: 'elasticfilesystem',
resource: 'access-point',
resourceName: accessPointId,
});
}
return new Import(scope, id);
return new ImportedAccessPoint(scope, id, {
accessPointId: accessPointId,
});
}

/**
Expand Down Expand Up @@ -174,3 +227,49 @@ export class AccessPoint extends Resource implements IAccessPoint {
this.fileSystem = props.fileSystem;
}
}

class ImportedAccessPoint extends AccessPointBase {
public readonly accessPointId: string;
public readonly accessPointArn: string;
private readonly _fileSystem?: IFileSystem;

constructor(scope: Construct, id: string, attrs: AccessPointAttributes) {
super(scope, id);

if (!attrs.accessPointId) {
if (!attrs.accessPointArn) {
throw new Error('One of accessPointId or AccessPointArn is required!');
}

this.accessPointArn = attrs.accessPointArn;
let maybeApId = Stack.of(scope).parseArn(attrs.accessPointArn).resourceName;

if (!maybeApId) {
throw new Error('ARN for AccessPoint must provide the resource name.');
}

this.accessPointId = maybeApId;
} else {
if (attrs.accessPointArn) {
throw new Error('Only one of accessPointId or AccessPointArn can be provided!');
}

this.accessPointId = attrs.accessPointId;
this.accessPointArn = Stack.of(scope).formatArn({
service: 'elasticfilesystem',
resource: 'access-point',
resourceName: attrs.accessPointId,
});
}

this._fileSystem = attrs.fileSystem;
}

public get fileSystem() {
if (!this._fileSystem) {
throw new Error("fileSystem is not available when 'fromAccessPointId()' is used. Use 'fromAccessPointAttributes()' instead");
}

return this._fileSystem;
}
}
53 changes: 38 additions & 15 deletions packages/@aws-cdk/aws-efs/lib/efs-file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,28 +206,18 @@ export interface FileSystemAttributes {
* @resource AWS::EFS::FileSystem
*/
export class FileSystem extends Resource implements IFileSystem {
/**
* The default port File System listens on.
*/
public static readonly DEFAULT_PORT: number = 2049;

/**
* Import an existing File System from the given properties.
*/
public static fromFileSystemAttributes(scope: Construct, id: string, attrs: FileSystemAttributes): IFileSystem {
class Import extends Resource implements IFileSystem {
public readonly fileSystemId = attrs.fileSystemId;
public readonly connections = new ec2.Connections({
securityGroups: [attrs.securityGroup],
defaultPort: ec2.Port.tcp(FileSystem.DEFAULT_PORT),
});
public readonly mountTargetsAvailable = new ConcreteDependable();
}

return new Import(scope, id);
return new ImportedFileSystem(scope, id, attrs);
}

/**
* The default port File System listens on.
*/
private static readonly DEFAULT_PORT: number = 2049;

/**
* The security groups/rules used to allow network connections to the file system.
*/
Expand Down Expand Up @@ -303,3 +293,36 @@ export class FileSystem extends Resource implements IFileSystem {
});
}
}


class ImportedFileSystem extends Resource implements IFileSystem {
/**
* The security groups/rules used to allow network connections to the file system.
*/
public readonly connections: ec2.Connections;

/**
* @attribute
*/
public readonly fileSystemId: string;

/**
* Dependable that can be depended upon to ensure the mount targets of the filesystem are ready
*/
public readonly mountTargetsAvailable: IDependable;

constructor(scope: Construct, id: string, attrs: FileSystemAttributes) {
super(scope, id);

this.fileSystemId = attrs.fileSystemId;

this.connections = new ec2.Connections({
securityGroups: [attrs.securityGroup],
defaultPort: ec2.Port.tcp(FileSystem.DEFAULT_PORT),
});

this.mountTargetsAvailable = new ConcreteDependable();
}


}
85 changes: 83 additions & 2 deletions packages/@aws-cdk/aws-efs/test/access-point.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ test('new AccessPoint correctly', () => {
expectCDK(stack).to(haveResource('AWS::EFS::AccessPoint'));
});

test('import correctly', () => {
test('import an AccessPoint using fromAccessPointId', () => {
// WHEN
const ap = new AccessPoint(stack, 'MyAccessPoint', {
fileSystem,
Expand All @@ -41,6 +41,87 @@ test('import correctly', () => {
expect(imported.accessPointId).toEqual(ap.accessPointId);
});

test('import an AccessPoint using fromAccessPointId', () => {
// WHEN
const ap = new AccessPoint(stack, 'MyAccessPoint', {
fileSystem,
});
const imported = AccessPoint.fromAccessPointId(stack, 'ImportedAccessPoint', ap.accessPointId);
// THEN
expect(() => imported.fileSystem).toThrow(/fileSystem is not available when 'fromAccessPointId\(\)' is used. Use 'fromAccessPointAttributes\(\)' instead/);
});

test('import an AccessPoint using fromAccessPointAttributes and the accessPointId', () => {
// WHEN
const ap = new AccessPoint(stack, 'MyAccessPoint', {
fileSystem,
});
const imported = AccessPoint.fromAccessPointAttributes(stack, 'ImportedAccessPoint', {
accessPointId: ap.accessPointId,
fileSystem: fileSystem,
});
// THEN
expect(imported.accessPointId).toEqual(ap.accessPointId);
expect(imported.accessPointArn).toEqual(ap.accessPointArn);
expect(imported.fileSystem).toEqual(ap.fileSystem);
});

test('import an AccessPoint using fromAccessPointAttributes and the accessPointArn', () => {
// WHEN
const ap = new AccessPoint(stack, 'MyAccessPoint', {
fileSystem,
});
const imported = AccessPoint.fromAccessPointAttributes(stack, 'ImportedAccessPoint', {
accessPointArn: ap.accessPointArn,
fileSystem: fileSystem,
});
// THEN
expect(imported.accessPointId).toEqual(ap.accessPointId);
expect(imported.accessPointArn).toEqual(ap.accessPointArn);
expect(imported.fileSystem).toEqual(ap.fileSystem);
});

test('import using accessPointArn', () => {
// WHEN
const ap = new AccessPoint(stack, 'MyAccessPoint', {
fileSystem,
});
const imported = AccessPoint.fromAccessPointAttributes(stack, 'ImportedAccessPoint', {
accessPointArn: ap.accessPointArn,
fileSystem: fileSystem,
});
// THEN
expect(imported.accessPointId).toEqual(ap.accessPointId);
expect(imported.accessPointArn).toEqual(ap.accessPointArn);
expect(imported.fileSystem).toEqual(ap.fileSystem);
});

test('throw when import using accessPointArn and accessPointId', () => {
// WHEN
const ap = new AccessPoint(stack, 'MyAccessPoint', {
fileSystem,
});

// THEN
expect(() => AccessPoint.fromAccessPointAttributes(stack, 'ImportedAccessPoint', {
accessPointArn: ap.accessPointArn,
accessPointId: ap.accessPointId,
fileSystem: fileSystem,
})).toThrow(/Only one of accessPointId or AccessPointArn can be provided!/);
});

test('throw when import without accessPointArn or accessPointId', () => {
// WHEN
new AccessPoint(stack, 'MyAccessPoint', {
fileSystem,
});

// THEN
expect(() => AccessPoint.fromAccessPointAttributes(stack, 'ImportedAccessPoint', {
fileSystem: fileSystem,
})).toThrow(/One of accessPointId or AccessPointArn is required!/);
});

test('custom access point is created correctly', () => {
// WHEN
new AccessPoint(stack, 'MyAccessPoint', {
Expand Down Expand Up @@ -83,4 +164,4 @@ test('custom access point is created correctly', () => {
Path: '/export/share',
},
}));
});
});
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-lambda/lib/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class FileSystem {
* @param ap the Amazon EFS access point
* @param mountPath the target path in the lambda runtime environment
*/
public static fromEfsAccessPoint(ap: efs.AccessPoint, mountPath: string): FileSystem {
public static fromEfsAccessPoint(ap: efs.IAccessPoint, mountPath: string): FileSystem {
return new FileSystem({
localMountPath: mountPath,
arn: ap.accessPointArn,
Expand Down
Loading

0 comments on commit ec72c85

Please sign in to comment.