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; }