Compare commits
2 Commits
62faf162de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 49b9ed1dea | |||
| 09b7eba9c4 |
@@ -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(await screen.findByText(/No mount targets yet/)).toBeInTheDocument()
|
||||||
expect(within(screen.getByRole('form', {name: 'Attach to a function'})).getByRole('button', {name: 'Attach'})).toBeDisabled()
|
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()
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {RefreshCw} from 'lucide-react'
|
|||||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
import {useEffect, useState} from 'react'
|
import {useEffect, useState} from 'react'
|
||||||
import {Link} from 'react-router-dom'
|
import {Link} from 'react-router-dom'
|
||||||
|
import {CopyValueButton} from '@/components/CopyValueButton'
|
||||||
import {
|
import {
|
||||||
attachFileSystemToFunction,
|
attachFileSystemToFunction,
|
||||||
createAccessPoint,
|
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}
|
type MountedFunction = {name: string; accessPointId: string | null; mountPath: string | null}
|
||||||
|
|
||||||
const EMPTY_ACCESS_POINT = {name: '', path: '/', uid: '1000', gid: '1000', permissions: '755'}
|
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) {
|
export function FileSystemPanel({resource, runtimeReachable}: Props) {
|
||||||
const accountId = useAccountId()
|
const accountId = useAccountId()
|
||||||
@@ -40,6 +42,11 @@ export function FileSystemPanel({resource, runtimeReachable}: Props) {
|
|||||||
queryKey: detailKey,
|
queryKey: detailKey,
|
||||||
queryFn: ({signal}) => getCloudResource('aws', 'filesystems', resource?.id ?? '', signal),
|
queryFn: ({signal}) => getCloudResource('aws', 'filesystems', resource?.id ?? '', signal),
|
||||||
enabled: Boolean(resource) && runtimeReachable,
|
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({
|
const functionsQuery = useQuery({
|
||||||
queryKey: cloudQueryKeys.resources('aws', 'serverless'),
|
queryKey: cloudQueryKeys.resources('aws', 'serverless'),
|
||||||
@@ -185,7 +192,36 @@ export function FileSystemPanel({resource, runtimeReachable}: Props) {
|
|||||||
</form>}
|
</form>}
|
||||||
{accessPoints.length > 0 && <p className="muted compact-text efs-note">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.</p>}
|
{accessPoints.length > 0 && <p className="muted compact-text efs-note">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.</p>}
|
||||||
|
|
||||||
|
<Ec2MountInstructions fileSystemId={String(metadata.fileSystemId ?? fileSystem.id)} region={fileSystem.region ?? 'us-east-1'} mountTargets={mountTargets}/>
|
||||||
|
|
||||||
{notice && <p className="cognito-outcome cognito-outcome--ok" role="status">{notice}</p>}
|
{notice && <p className="cognito-outcome cognito-outcome--ok" role="status">{notice}</p>}
|
||||||
{error instanceof Error && <p className="error-text compact-text" role="alert">{error.message}</p>}
|
{error instanceof Error && <p className="error-text compact-text" role="alert">{error.message}</p>}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => <div className="efs-command" key={label}>
|
||||||
|
<span className="muted">{label}</span>
|
||||||
|
<code>{value}</code>
|
||||||
|
<CopyValueButton value={value} label={`${label.toLowerCase()} command`}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="input-row resource-table-bar"><div><p className="eyebrow">EC2 instances</p><span className="muted">An instance in this file system's VPC mounts it over NFS through a mount target, as on AWS.</span></div></div>
|
||||||
|
{available.length === 0
|
||||||
|
? <p className="muted compact-text efs-note">{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.'}</p>
|
||||||
|
: <div className="efs-commands" aria-label="Mount on an EC2 instance">
|
||||||
|
<p className="muted compact-text">DNS name <code>{dnsName}</code></p>
|
||||||
|
{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`))}
|
||||||
|
<p className="muted compact-text">The EFS mount helper is the <code>amazon-efs-utils</code> package. TheCloud serves plain NFS, so its <code>tls</code> and <code>accesspoint</code> options are not supported. A function's files are in its access point's root directory, such as <code>efs/my-function</code>.</p>
|
||||||
|
</div>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|||||||
@@ -6691,3 +6691,8 @@ tr[aria-current="true"] td { background: var(--hover-md); }
|
|||||||
.graphql-span { grid-column: span 2; }
|
.graphql-span { grid-column: span 2; }
|
||||||
.graphql-target { overflow-wrap: anywhere; }
|
.graphql-target { overflow-wrap: anywhere; }
|
||||||
@media (max-width: 640px) { .graphql-span { grid-column: auto; } .graphql-endpoints { grid-template-columns: minmax(0, 1fr); } }
|
@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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user