From 09b7eba9c4b482be72dde36331d00da87d38bead Mon Sep 17 00:00:00 2001 From: Zakaria Date: Sun, 25 Oct 2026 21:13:33 -0400 Subject: [PATCH] feat(frontend): show how to mount an EFS file system on an EC2 instance The EFS page gains an EC2 instances section like the AWS console's Attach dialog: the file system's DNS name, the commands to install the NFS client, and mount commands by DNS name, with the EFS mount helper, and by each available mount target's address, each with a copy button. It appears once a mount target is available, and the page refreshes while one is creating. TheCloud serves plain NFS, so the mount helper's tls and accesspoint options are called out as unsupported. --- .../src/components/FileSystemPanel.test.tsx | 19 ++++++++++ .../src/components/FileSystemPanel.tsx | 36 +++++++++++++++++++ packages/frontend/src/index.css | 5 +++ 3 files changed, 60 insertions(+) diff --git a/packages/frontend/src/components/FileSystemPanel.test.tsx b/packages/frontend/src/components/FileSystemPanel.test.tsx index 72e0bb2..8aa082b 100644 --- a/packages/frontend/src/components/FileSystemPanel.test.tsx +++ b/packages/frontend/src/components/FileSystemPanel.test.tsx @@ -102,3 +102,22 @@ test('cannot attach until the file system has a mount target', async () => { expect(await screen.findByText(/No mount targets yet/)).toBeInTheDocument() expect(within(screen.getByRole('form', {name: 'Attach to a function'})).getByRole('button', {name: 'Attach'})).toBeDisabled() }) + +test('shows how to mount the file system on an EC2 instance once a mount target is available', async () => { + renderPanel(fileSystem()) + + const commands = within(await screen.findByLabelText('Mount on an EC2 instance')) + expect(commands.getByText('fs-1.efs.us-east-1.amazonaws.com')).toBeInTheDocument() + expect(commands.getByText('sudo mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport fs-1.efs.us-east-1.amazonaws.com:/ efs')).toBeInTheDocument() + expect(commands.getByText('sudo mount -t efs fs-1:/ efs')).toBeInTheDocument() + expect(commands.getByText('Mount by IP address in us-east-1a')).toBeInTheDocument() + expect(commands.getByText(/10\.0\.0\.10:\/ efs$/)).toBeInTheDocument() + expect(commands.getByRole('button', {name: 'Copy mount with the nfs client command'})).toBeInTheDocument() +}) + +test('holds the mount commands back while the mount target is still creating', async () => { + renderPanel(fileSystem({mountTargets: [{id: 'fsmt-1', subnetId: 'subnet-a', availabilityZone: 'us-east-1a', ipAddress: '10.0.0.10', state: 'creating'}]})) + + expect(await screen.findByText('The mount commands appear once a mount target is available.')).toBeInTheDocument() + expect(screen.queryByLabelText('Mount on an EC2 instance')).not.toBeInTheDocument() +}) diff --git a/packages/frontend/src/components/FileSystemPanel.tsx b/packages/frontend/src/components/FileSystemPanel.tsx index 1c20fd8..741478a 100644 --- a/packages/frontend/src/components/FileSystemPanel.tsx +++ b/packages/frontend/src/components/FileSystemPanel.tsx @@ -2,6 +2,7 @@ import {RefreshCw} from 'lucide-react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {useEffect, useState} from 'react' import {Link} from 'react-router-dom' +import {CopyValueButton} from '@/components/CopyValueButton' import { attachFileSystemToFunction, createAccessPoint, @@ -26,6 +27,7 @@ type AccessPoint = {id: string; arn: string | null; name: string | null; path: s type MountedFunction = {name: string; accessPointId: string | null; mountPath: string | null} const EMPTY_ACCESS_POINT = {name: '', path: '/', uid: '1000', gid: '1000', permissions: '755'} +const NFS_OPTIONS = 'nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport' export function FileSystemPanel({resource, runtimeReachable}: Props) { const accountId = useAccountId() @@ -40,6 +42,11 @@ export function FileSystemPanel({resource, runtimeReachable}: Props) { queryKey: detailKey, queryFn: ({signal}) => getCloudResource('aws', 'filesystems', resource?.id ?? '', signal), enabled: Boolean(resource) && runtimeReachable, + // A new mount target is creating until its NFS server answers, a few seconds. + refetchInterval: (query) => { + const targets = (query.state.data?.metadata.mountTargets ?? []) as MountTarget[] + return targets.some((mountTarget) => mountTarget.state === 'creating') ? 2_000 : false + }, }) const functionsQuery = useQuery({ queryKey: cloudQueryKeys.resources('aws', 'serverless'), @@ -185,7 +192,36 @@ export function FileSystemPanel({resource, runtimeReachable}: Props) { } {accessPoints.length > 0 &&

Lambda needs a VPC to mount a file system. A function not yet in one is put in this file system's mount target subnets with the default security group.

} + + {notice &&

{notice}

} {error instanceof Error &&

{error.message}

} } + +function Ec2MountInstructions({fileSystemId, region, mountTargets}: {fileSystemId: string; region: string; mountTargets: MountTarget[]}) { + const available = mountTargets.filter((mountTarget) => mountTarget.state === 'available' && mountTarget.ipAddress) + const dnsName = `${fileSystemId}.efs.${region}.amazonaws.com` + const command = (label: string, value: string) =>
+ {label} + {value} + +
+ + return <> +

EC2 instances

An instance in this file system's VPC mounts it over NFS through a mount target, as on AWS.
+ {available.length === 0 + ?

{mountTargets.length === 0 ? 'Add a mount target to mount the file system on an instance.' : 'The mount commands appear once a mount target is available.'}

+ :
+

DNS name {dnsName}

+ {command('Install the NFS client on Amazon Linux', 'sudo dnf install -y nfs-utils')} + {command('Install the NFS client on Debian or Ubuntu', 'sudo apt-get install -y nfs-common')} + {command('Make a mount point', 'sudo mkdir -p efs')} + {command('Mount with the NFS client', `sudo mount -t nfs4 -o ${NFS_OPTIONS} ${dnsName}:/ efs`)} + {command('Mount with the EFS mount helper', `sudo mount -t efs ${fileSystemId}:/ efs`)} + {available.map((mountTarget) => command(`Mount by IP address in ${mountTarget.availabilityZone ?? mountTarget.id}`, + `sudo mount -t nfs4 -o ${NFS_OPTIONS} ${mountTarget.ipAddress}:/ efs`))} +

The EFS mount helper is the amazon-efs-utils package. TheCloud serves plain NFS, so its tls and accesspoint options are not supported. A function's files are in its access point's root directory, such as efs/my-function.

+
} + +} diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css index 70d3852..f664b78 100644 --- a/packages/frontend/src/index.css +++ b/packages/frontend/src/index.css @@ -6691,3 +6691,8 @@ tr[aria-current="true"] td { background: var(--hover-md); } .graphql-span { grid-column: span 2; } .graphql-target { overflow-wrap: anywhere; } @media (max-width: 640px) { .graphql-span { grid-column: auto; } .graphql-endpoints { grid-template-columns: minmax(0, 1fr); } } +.efs-commands { display: grid; gap: 8px; padding: 0 16px 12px; } +.efs-commands p { margin: 0; } +.efs-command { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; row-gap: 2px; align-items: center; } +.efs-command > span { grid-column: 1 / -1; font-size: 12px; } +.efs-command > code { overflow-x: auto; white-space: nowrap; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--raised); font-size: 12px; }