Compare commits

...

4 Commits

Author SHA1 Message Date
Zakaria 09b7eba9c4 feat(frontend): show how to mount an EFS file system on an EC2 instance
CI / checks (pull_request) Successful in 28m13s
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.
2026-10-25 21:13:38 -04:00
Zakaria 62faf162de Merge pull request 'feat(api): add AppSync GraphQL APIs to the cloud explorer' (#30) from feat/appsync into main
CI / checks (push) Successful in 27m59s
Reviewed-on: #30
2026-10-25 23:46:33 +00:00
Zakaria 1c0ef3091e Merge pull request 'feat(api): add the Glue Data Catalog to the cloud explorer' (#29) from feat/athena into main
CI / checks (push) Has been cancelled
Reviewed-on: #29
2026-10-25 23:46:09 +00:00
Zakaria a2a9c19ff6 Merge pull request 'feat(api): add EFS file systems to the cloud explorer' (#27) from feat/efs into main
CI / checks (push) Has been cancelled
Reviewed-on: #27
2026-10-25 23:44:41 +00:00
3 changed files with 60 additions and 0 deletions
@@ -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&apos;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&apos;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&apos;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&apos;s files are in its access point&apos;s root directory, such as <code>efs/my-function</code>.</p>
</div>}
</>
}
+5
View File
@@ -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; }