feat: delete-volumes (#18)

This commit is contained in:
Julian Sommer
2026-02-03 10:47:00 +11:00
committed by GitHub
parent b772b2e0bf
commit fdb5cca77e
33 changed files with 598 additions and 64 deletions
+4 -4
View File
@@ -51,11 +51,11 @@
"Bash(cd ../viperblock && *)",
"Bash(cd ../predastore && *)",
"Bash(pnpm fix *)",
"Bash(pnpm check *)",
"Bash(pnpm build *)",
"Bash(pnpm fix:*)",
"Bash(pnpm check:*)",
"Bash(pnpm build:*)",
"Bash(pnpm tsc *)",
"Bash(pnpx @tanstack/router-cli generate *)",
"Bash(pnpx @tanstack/router-cli generate:*)",
"Bash(git diff*)",
"Bash(git diff --staged*)",
+1 -13
View File
@@ -82,18 +82,6 @@ aws ec2 detach-volume --volume-id vol-1234567890abcdef0
- Use QMP to detach specified volume
- Terminate running `nbdkit` process for specified volume, ensure WAL state synced to predastore.
## EC2 delete-volume
Allow a volume to be deleted once detached from any running instances.
```sh
aws ec2 delete-volume --volume-id vol-049df61146c4d7901
```
- Confirm volume detached
- Terminate running `nbdkit` process for specified volume, ensure WAL state synced to predastore.
- Delete volume in predastore
## EC2 modify-volume
Example increasing a volume to 64GiB
@@ -226,7 +214,7 @@ Instance `dmesg`:
| `describe-volumes` | `--volume-ids` (fast-path lookup) | `--filters`, `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeVolumes` → daemon queries viperblock for volume metadata → returns volume list with state, size, attachments, type | 1. List all volumes<br>2. Filter by volume ID<br>3. Filter by attachment state<br>4. Non-existent volume returns empty | **DONE** |
| `modify-volume` | `--volume-id`, `--size`, `--volume-type`, `--iops` | `--throughput`, `--dry-run`, `--multi-attach-enabled` | Volume must exist | NATS `ec2.ModifyVolume` → daemon sends resize request to viperblock → NBD does not support live resize, instance must be stopped → returns modification state | 1. Increase volume size<br>2. Modify volume type<br>3. Decrease size (error - not supported)<br>4. Modify attached volume (requires stop/start) | **DONE** |
| `create-volume` | `--size`, `--availability-zone`, `--volume-type` (gp3 only) | `--iops`, `--encrypted`, `--snapshot-id`, `--tag-specifications` | Valid AZ configured via `hive init` | Gateway validates input → NATS `ec2.CreateVolume` → daemon generates vol-ID via viperblock → creates empty volume of specified size → persists config.json to Predastore S3 → returns vol-ID with state=available | 1. Create 80GB gp3 volume<br>2. Boundary sizes (1 GiB min, 16384 GiB max)<br>3. Invalid AZ (error)<br>4. Verify volume in describe-volumes<br>5. Unsupported volume type (error - only gp3)<br>6. Size out of range (error) | **DONE** |
| `delete-volume` | — | `--volume-id` | Volume must exist and be detached | Confirm volume detached → terminate nbdkit process → sync WAL to Predastore → delete volume bucket in Predastore → return success | 1. Delete detached volume<br>2. Delete attached volume (error: VolumeInUse)<br>3. Delete non-existent volume (error)<br>4. Verify volume gone from describe-volumes | **NOT STARTED** |
| `delete-volume` | `--volume-id` | `--dry-run` | Volume must exist and be detached (state=available) | Gateway validates vol- prefix → NATS `ec2.DeleteVolume` → daemon confirms state=available and no AttachedInstance → NATS `ebs.delete` to viperblockd (stops nbdkit/WAL) → deletes S3 objects under vol-id/, vol-id-efi/, vol-id-cloudinit/ → returns success | 1. Delete detached volume<br>2. Delete attached volume (error: VolumeInUse)<br>3. Delete non-existent volume (error: InvalidVolume.NotFound)<br>4. Verify volume gone from describe-volumes<br>5. Malformed volume ID (error: InvalidVolumeID.Malformed)<br>6. Double delete (idempotent NotFound) | **DONE** |
| `attach-volume` | — | `--volume-id`, `--instance-id`, `--device` (e.g. /dev/sdf) | Volume must exist (available), instance must exist (running) | NATS to node running instance → start nbdkit for volume → QMP `blockdev-add` (NBD socket) → QMP `device_add` (virtio-blk-pci) → update instance metadata with attachment | 1. Attach volume to running instance<br>2. Verify device visible in guest OS<br>3. Attach already-attached volume (error)<br>4. Attach to non-existent instance (error)<br>5. Volume persists across stop/start | **NOT STARTED** |
| `detach-volume` | — | `--volume-id`, `--instance-id`, `--device`, `--force` | Volume must be attached | NATS to node running instance → QMP `device_del` → QMP `blockdev-del` → terminate nbdkit for volume → sync WAL to Predastore → update instance metadata | 1. Detach from running instance<br>2. Force detach<br>3. Detach already-detached volume (error)<br>4. Verify device removed from guest OS | **NOT STARTED** |
| `describe-volume-status` | — | `--volume-ids`, `--filters`, `--max-results` | None | Query viperblock for volume health status → return io-enabled status, availability zone, events | 1. Check healthy volume status<br>2. Check volume during modification<br>3. Filter by volume ID | **NOT STARTED** |
+2 -1
View File
@@ -430,6 +430,7 @@ func (d *Daemon) subscribeAll() error {
{"ec2.CreateVolume", d.handleEC2CreateVolume, "hive-workers"},
{"ec2.DescribeVolumes", d.handleEC2DescribeVolumes, "hive-workers"},
{"ec2.ModifyVolume", d.handleEC2ModifyVolume, "hive-workers"},
{"ec2.DeleteVolume", d.handleEC2DeleteVolume, "hive-workers"},
{"ec2.DescribeInstances", d.handleEC2DescribeInstances, ""},
{"ec2.DescribeInstanceTypes", d.handleEC2DescribeInstanceTypes, ""},
{"ec2.startinstances", d.handleEC2StartInstances, "hive-workers"},
@@ -474,7 +475,7 @@ func (d *Daemon) Start() error {
d.instanceService = handlers_ec2_instance.NewInstanceServiceImpl(d.config, d.resourceMgr.instanceTypes, d.natsConn, &d.Instances)
d.keyService = handlers_ec2_key.NewKeyServiceImpl(d.config)
d.imageService = handlers_ec2_image.NewImageServiceImpl(d.config)
d.volumeService = handlers_ec2_volume.NewVolumeServiceImpl(d.config)
d.volumeService = handlers_ec2_volume.NewVolumeServiceImpl(d.config, d.natsConn)
d.restoreInstances()
+35
View File
@@ -772,6 +772,41 @@ func (d *Daemon) handleEC2ModifyVolume(msg *nats.Msg) {
slog.Info("handleEC2ModifyVolume completed", "volumeId", modifyVolumeInput.VolumeId)
}
// handleEC2DeleteVolume processes incoming EC2 DeleteVolume requests
func (d *Daemon) handleEC2DeleteVolume(msg *nats.Msg) {
slog.Debug("Received message", "subject", msg.Subject, "data", string(msg.Data))
deleteVolumeInput := &ec2.DeleteVolumeInput{}
errResp := utils.UnmarshalJsonPayload(deleteVolumeInput, msg.Data)
if errResp != nil {
msg.Respond(errResp)
slog.Error("Request does not match DeleteVolumeInput")
return
}
slog.Info("Processing DeleteVolume request", "volumeId", aws.StringValue(deleteVolumeInput.VolumeId))
output, err := d.volumeService.DeleteVolume(deleteVolumeInput)
if err != nil {
slog.Error("handleEC2DeleteVolume service.DeleteVolume failed", "err", err)
errResp = utils.GenerateErrorPayload(err.Error())
msg.Respond(errResp)
return
}
jsonResponse, err := json.Marshal(output)
if err != nil {
slog.Error("handleEC2DeleteVolume failed to marshal output", "err", err)
errResp = utils.GenerateErrorPayload(awserrors.ErrorServerInternal)
msg.Respond(errResp)
return
}
msg.Respond(jsonResponse)
slog.Info("handleEC2DeleteVolume completed", "volumeId", aws.StringValue(deleteVolumeInput.VolumeId))
}
// handleEC2DescribeInstanceTypes processes incoming EC2 DescribeInstanceTypes requests
// This handler responds with instance types that can currently be provisioned on this node
// based on available resources (CPU and memory not already allocated to running instances)
+22
View File
@@ -371,6 +371,28 @@ func (gw *GatewayConfig) EC2_Request(ctx *fiber.Ctx) error {
return errors.New("failed to marshal response to XML")
}
case "DeleteVolume":
var input = &ec2.DeleteVolumeInput{}
err = awsec2query.QueryParamsToStruct(queryArgs, input)
if err != nil {
return err
}
output, err := gateway_ec2_volume.DeleteVolume(input, gw.NATSConn)
if err != nil {
return err
}
// Convert to XML
payload := utils.GenerateXMLPayload("DeleteVolumeResponse", output)
xmlOutput, err = utils.MarshalToXML(payload)
if err != nil {
return errors.New("failed to marshal response to XML")
}
default:
err = errors.New("InvalidAction")
}
+44
View File
@@ -0,0 +1,44 @@
package gateway_ec2_volume
import (
"errors"
"strings"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/hive/hive/awserrors"
handlers_ec2_volume "github.com/mulgadc/hive/hive/handlers/ec2/volume"
"github.com/nats-io/nats.go"
)
// ValidateDeleteVolumeInput validates the input parameters
func ValidateDeleteVolumeInput(input *ec2.DeleteVolumeInput) error {
if input == nil {
return errors.New(awserrors.ErrorInvalidParameterValue)
}
if input.VolumeId == nil || len(*input.VolumeId) <= len("vol-") || !strings.HasPrefix(*input.VolumeId, "vol-") {
return errors.New(awserrors.ErrorInvalidVolumeIDMalformed)
}
return nil
}
// DeleteVolume handles the DeleteVolume API call
func DeleteVolume(input *ec2.DeleteVolumeInput, natsConn *nats.Conn) (ec2.DeleteVolumeOutput, error) {
var output ec2.DeleteVolumeOutput
err := ValidateDeleteVolumeInput(input)
if err != nil {
return output, err
}
volumeService := handlers_ec2_volume.NewNATSVolumeService(natsConn)
result, err := volumeService.DeleteVolume(input)
if err != nil {
return output, err
}
output = *result
return output, nil
}
@@ -0,0 +1,99 @@
package gateway_ec2_volume
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/hive/hive/awserrors"
"github.com/stretchr/testify/assert"
)
func TestValidateDeleteVolumeInput(t *testing.T) {
tests := []struct {
name string
input *ec2.DeleteVolumeInput
wantErr bool
errMsg string
}{
{
name: "NilInput",
input: nil,
wantErr: true,
errMsg: awserrors.ErrorInvalidParameterValue,
},
{
name: "EmptyInput_NoVolumeId",
input: &ec2.DeleteVolumeInput{},
wantErr: true,
errMsg: awserrors.ErrorInvalidVolumeIDMalformed,
},
{
name: "ValidVolumeId",
input: &ec2.DeleteVolumeInput{
VolumeId: aws.String("vol-0123456789abcdef0"),
},
wantErr: false,
},
{
name: "ValidVolumeId_Short",
input: &ec2.DeleteVolumeInput{
VolumeId: aws.String("vol-abc123"),
},
wantErr: false,
},
{
name: "InvalidVolumeId_NoPrefix",
input: &ec2.DeleteVolumeInput{
VolumeId: aws.String("invalid-id"),
},
wantErr: true,
errMsg: awserrors.ErrorInvalidVolumeIDMalformed,
},
{
name: "InvalidVolumeId_WrongPrefix",
input: &ec2.DeleteVolumeInput{
VolumeId: aws.String("ami-0123456789abcdef0"),
},
wantErr: true,
errMsg: awserrors.ErrorInvalidVolumeIDMalformed,
},
{
name: "InvalidVolumeId_BarePrefix",
input: &ec2.DeleteVolumeInput{
VolumeId: aws.String("vol-"),
},
wantErr: true,
errMsg: awserrors.ErrorInvalidVolumeIDMalformed,
},
{
name: "InvalidVolumeId_EmptyString",
input: &ec2.DeleteVolumeInput{
VolumeId: aws.String(""),
},
wantErr: true,
errMsg: awserrors.ErrorInvalidVolumeIDMalformed,
},
{
name: "InvalidVolumeId_Nil",
input: &ec2.DeleteVolumeInput{
VolumeId: nil,
},
wantErr: true,
errMsg: awserrors.ErrorInvalidVolumeIDMalformed,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateDeleteVolumeInput(tt.input)
if tt.wantErr {
assert.Error(t, err)
assert.Equal(t, tt.errMsg, err.Error())
} else {
assert.NoError(t, err)
}
})
}
}
+1
View File
@@ -7,4 +7,5 @@ type VolumeService interface {
CreateVolume(input *ec2.CreateVolumeInput) (*ec2.Volume, error)
DescribeVolumes(input *ec2.DescribeVolumesInput) (*ec2.DescribeVolumesOutput, error)
ModifyVolume(input *ec2.ModifyVolumeInput) (*ec2.ModifyVolumeOutput, error)
DeleteVolume(input *ec2.DeleteVolumeInput) (*ec2.DeleteVolumeOutput, error)
}
+107 -1
View File
@@ -26,6 +26,7 @@ import (
"github.com/mulgadc/hive/hive/utils"
"github.com/mulgadc/viperblock/viperblock"
s3backend "github.com/mulgadc/viperblock/viperblock/backends/s3"
"github.com/nats-io/nats.go"
"golang.org/x/net/http2"
)
@@ -35,10 +36,11 @@ const defaultGP3IOPS = 3000
type VolumeServiceImpl struct {
config *config.Config
s3Client *s3.S3
natsConn *nats.Conn
}
// NewVolumeServiceImpl creates a new daemon-side volume service
func NewVolumeServiceImpl(cfg *config.Config) *VolumeServiceImpl {
func NewVolumeServiceImpl(cfg *config.Config, natsConn *nats.Conn) *VolumeServiceImpl {
// Create HTTP client with TLS verification disabled and HTTP/2 support
tr := &http.Transport{
TLSClientConfig: &tls.Config{
@@ -69,6 +71,7 @@ func NewVolumeServiceImpl(cfg *config.Config) *VolumeServiceImpl {
return &VolumeServiceImpl{
config: cfg,
s3Client: s3Client,
natsConn: natsConn,
}
}
@@ -555,3 +558,106 @@ func (s *VolumeServiceImpl) ModifyVolume(input *ec2.ModifyVolumeInput) (*ec2.Mod
VolumeModification: modification,
}, nil
}
// DeleteVolume deletes an EBS volume: validates state, notifies viperblockd, and removes S3 data
func (s *VolumeServiceImpl) DeleteVolume(input *ec2.DeleteVolumeInput) (*ec2.DeleteVolumeOutput, error) {
if input == nil || input.VolumeId == nil || *input.VolumeId == "" {
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
}
volumeID := *input.VolumeId
slog.Info("DeleteVolume request", "volumeId", volumeID)
// Fetch volume config to validate state
cfg, err := s.getVolumeConfig(volumeID)
if err != nil {
slog.Error("DeleteVolume failed to get volume config", "volumeId", volumeID, "err", err)
return nil, err
}
// Validate: volume must be available and not attached
if cfg.VolumeMetadata.State != "available" || cfg.VolumeMetadata.AttachedInstance != "" {
slog.Error("DeleteVolume: volume is in use", "volumeId", volumeID, "state", cfg.VolumeMetadata.State, "attachedInstance", cfg.VolumeMetadata.AttachedInstance)
return nil, errors.New(awserrors.ErrorVolumeInUse)
}
// Notify viperblockd to stop nbdkit/WAL syncer (best-effort)
if s.natsConn != nil {
deleteReq := config.EBSDeleteRequest{Volume: volumeID}
deleteData, err := json.Marshal(deleteReq)
if err != nil {
slog.Error("DeleteVolume failed to marshal ebs.delete request", "volumeId", volumeID, "err", err)
} else {
msg, err := s.natsConn.Request("ebs.delete", deleteData, 5*time.Second)
if err != nil {
slog.Warn("ebs.delete notification failed (volume may not be mounted)", "volumeId", volumeID, "err", err)
} else {
var deleteResp config.EBSDeleteResponse
if json.Unmarshal(msg.Data, &deleteResp) == nil && deleteResp.Error != "" {
slog.Error("ebs.delete returned error", "volumeId", volumeID, "err", deleteResp.Error)
return nil, errors.New(awserrors.ErrorServerInternal)
}
}
}
} else {
slog.Warn("DeleteVolume: natsConn is nil, skipping viperblockd notification", "volumeId", volumeID)
}
// Delete all S3 objects for this volume and its sub-volumes.
// Auxiliary prefixes are deleted first so the main config.json remains
// available for retry if an auxiliary deletion fails.
prefixes := []string{
volumeID + "-efi/",
volumeID + "-cloudinit/",
volumeID + "/",
}
for _, prefix := range prefixes {
if err := s.deleteS3Prefix(prefix); err != nil {
slog.Error("DeleteVolume failed to delete S3 prefix", "prefix", prefix, "err", err)
return nil, errors.New(awserrors.ErrorServerInternal)
}
}
slog.Info("DeleteVolume completed", "volumeId", volumeID)
return &ec2.DeleteVolumeOutput{}, nil
}
// deleteS3Prefix deletes all S3 objects under the given prefix
func (s *VolumeServiceImpl) deleteS3Prefix(prefix string) error {
bucket := s.config.Predastore.Bucket
var continuationToken *string
for {
listOutput, err := s.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Prefix: aws.String(prefix),
ContinuationToken: continuationToken,
})
if err != nil {
return fmt.Errorf("failed to list objects with prefix %s: %w", prefix, err)
}
if len(listOutput.Contents) == 0 {
break
}
for _, obj := range listOutput.Contents {
_, err := s.s3Client.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: obj.Key,
})
if err != nil {
return fmt.Errorf("failed to delete object %s: %w", *obj.Key, err)
}
}
if !aws.BoolValue(listOutput.IsTruncated) {
break
}
continuationToken = listOutput.NextContinuationToken
}
return nil
}
@@ -8,6 +8,7 @@ import (
"github.com/mulgadc/hive/hive/awserrors"
"github.com/mulgadc/hive/hive/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestVolumeService(az string) *VolumeServiceImpl {
@@ -185,3 +186,46 @@ func TestCreateVolume_PassesValidation(t *testing.T) {
})
}
}
func TestDeleteVolume_Validation(t *testing.T) {
tests := []struct {
name string
input *ec2.DeleteVolumeInput
wantErr string
}{
{
name: "NilInput",
input: nil,
wantErr: awserrors.ErrorInvalidParameterValue,
},
{
name: "EmptyInput",
input: &ec2.DeleteVolumeInput{},
wantErr: awserrors.ErrorInvalidParameterValue,
},
{
name: "NilVolumeId",
input: &ec2.DeleteVolumeInput{
VolumeId: nil,
},
wantErr: awserrors.ErrorInvalidParameterValue,
},
{
name: "EmptyVolumeId",
input: &ec2.DeleteVolumeInput{
VolumeId: aws.String(""),
},
wantErr: awserrors.ErrorInvalidParameterValue,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := newTestVolumeService("ap-southeast-2a")
_, err := svc.DeleteVolume(tt.input)
require.Error(t, err)
assert.Equal(t, tt.wantErr, err.Error())
})
}
}
+26
View File
@@ -98,3 +98,29 @@ func (s *NATSVolumeService) ModifyVolume(input *ec2.ModifyVolumeInput) (*ec2.Mod
return &output, nil
}
// DeleteVolume sends a DeleteVolume request via NATS and waits for response
func (s *NATSVolumeService) DeleteVolume(input *ec2.DeleteVolumeInput) (*ec2.DeleteVolumeOutput, error) {
jsonData, err := json.Marshal(input)
if err != nil {
return nil, fmt.Errorf("failed to marshal input: %w", err)
}
msg, err := s.natsConn.Request("ec2.DeleteVolume", jsonData, 30*time.Second)
if err != nil {
return nil, fmt.Errorf("NATS request failed: %w", err)
}
responseError, err := utils.ValidateErrorPayload(msg.Data)
if err != nil {
return nil, errors.New(*responseError.Code)
}
var output ec2.DeleteVolumeOutput
err = json.Unmarshal(msg.Data, &output)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &output, nil
}
+1 -1
View File
@@ -1 +1 @@
import{c as k,aH as v,aI as B,aC as $,j as s,aB as C,aJ as P,aK as y,aE as K}from"./index.js";import{u as F}from"./useSuspenseQuery.js";import{B as L}from"./back-link.js";import{F as S,O as E,U as I}from"./upload-button.js";import"./alert-dialog.js";import"./download.js";import"./trash-2.js";import"./useMutation.js";function W(){const e=k.c(20),{bucket:n,_splat:b}=v.useParams(),a=B(b||""),{data:o}=F($(n,a));let c;e[0]!==o.Contents?(c=o.Contents||[],e[0]=o.Contents,e[1]=c):c=e[1];const r=c;let h;e[2]!==o.CommonPrefixes?(h=o.CommonPrefixes||[],e[2]=o.CommonPrefixes,e[3]=h):h=e[3];const i=h,x=a.split("/").filter(Boolean),N=[{name:n,path:`/s3/ls/${n}`},...x.map((t,l)=>{const m=`${x.slice(0,l+1).join("/")}/`;return{name:t,path:`/s3/ls/${n}/${m}`}})],g=x.length>1?`/s3/ls/${n}/${x.slice(0,-1).join("/")}/`:`/s3/ls/${n}`;let d;e[4]!==g?(d=s.jsx(L,{to:g,children:"Back"}),e[4]=g,e[5]=d):d=e[5];let f;e[6]!==n||e[7]!==a?(f=s.jsx(I,{bucket:n,prefix:a}),e[6]=n,e[7]=a,e[8]=f):f=e[8];let p;e[9]!==n||e[10]!==i||e[11]!==a?(p=i.length>0&&s.jsxs("div",{className:"mb-6",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Folders"}),s.jsx("div",{className:"space-y-2",children:i.map(t=>{if(!t.Prefix)return null;const l=C(P(t.Prefix,a)),m=y(t.Prefix,a);return s.jsx(S,{bucketName:n,folderName:l,fullPrefix:m},t.Prefix)})})]}),e[9]=n,e[10]=i,e[11]=a,e[12]=p):p=e[12];let u;e[13]!==n||e[14]!==r||e[15]!==a?(u=r.length>0&&s.jsxs("div",{className:"space-y-2",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Objects"}),r.map(t=>{if(!t.Key)return null;const l=P(t.Key,a),m=y(t.Key,a);return s.jsx(E,{bucketName:n,displayName:l,fullKey:m,object:t},t.Key)})]}),e[13]=n,e[14]=r,e[15]=a,e[16]=u):u=e[16];let j;return e[17]!==i.length||e[18]!==r.length?(j=r.length===0&&i.length===0&&s.jsx("p",{className:"text-muted-foreground",children:"No objects found in this folder."}),e[17]=i.length,e[18]=r.length,e[19]=j):j=e[19],s.jsxs(s.Fragment,{children:[d,s.jsx("div",{className:"mb-6",children:s.jsxs("div",{className:"mb-2 flex items-center justify-between",children:[s.jsx("div",{className:"flex items-center gap-2 text-muted-foreground text-sm",children:N.map((t,l)=>s.jsxs("div",{className:"flex items-center gap-2",children:[l>0&&s.jsx("span",{children:"/"}),l===N.length-1?s.jsx("span",{className:"text-foreground",children:t.name}):s.jsx(K,{className:"hover:text-foreground",to:t.path,children:t.name})]},t.path))}),f]})}),p,u,j]})}export{W as component};
import{c as k,aG as v,aH as B,aC as $,j as s,aB as C,aI as P,aJ as y,aE as K}from"./index.js";import{u as F}from"./useSuspenseQuery.js";import{B as L}from"./back-link.js";import{F as S,O as E,U as I}from"./upload-button.js";import"./alert-dialog.js";import"./download.js";import"./trash-2.js";import"./useMutation.js";function J(){const e=k.c(20),{bucket:n,_splat:b}=v.useParams(),a=B(b||""),{data:o}=F($(n,a));let c;e[0]!==o.Contents?(c=o.Contents||[],e[0]=o.Contents,e[1]=c):c=e[1];const r=c;let h;e[2]!==o.CommonPrefixes?(h=o.CommonPrefixes||[],e[2]=o.CommonPrefixes,e[3]=h):h=e[3];const i=h,x=a.split("/").filter(Boolean),N=[{name:n,path:`/s3/ls/${n}`},...x.map((t,l)=>{const m=`${x.slice(0,l+1).join("/")}/`;return{name:t,path:`/s3/ls/${n}/${m}`}})],g=x.length>1?`/s3/ls/${n}/${x.slice(0,-1).join("/")}/`:`/s3/ls/${n}`;let d;e[4]!==g?(d=s.jsx(L,{to:g,children:"Back"}),e[4]=g,e[5]=d):d=e[5];let f;e[6]!==n||e[7]!==a?(f=s.jsx(I,{bucket:n,prefix:a}),e[6]=n,e[7]=a,e[8]=f):f=e[8];let p;e[9]!==n||e[10]!==i||e[11]!==a?(p=i.length>0&&s.jsxs("div",{className:"mb-6",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Folders"}),s.jsx("div",{className:"space-y-2",children:i.map(t=>{if(!t.Prefix)return null;const l=C(P(t.Prefix,a)),m=y(t.Prefix,a);return s.jsx(S,{bucketName:n,folderName:l,fullPrefix:m},t.Prefix)})})]}),e[9]=n,e[10]=i,e[11]=a,e[12]=p):p=e[12];let u;e[13]!==n||e[14]!==r||e[15]!==a?(u=r.length>0&&s.jsxs("div",{className:"space-y-2",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Objects"}),r.map(t=>{if(!t.Key)return null;const l=P(t.Key,a),m=y(t.Key,a);return s.jsx(E,{bucketName:n,displayName:l,fullKey:m,object:t},t.Key)})]}),e[13]=n,e[14]=r,e[15]=a,e[16]=u):u=e[16];let j;return e[17]!==i.length||e[18]!==r.length?(j=r.length===0&&i.length===0&&s.jsx("p",{className:"text-muted-foreground",children:"No objects found in this folder."}),e[17]=i.length,e[18]=r.length,e[19]=j):j=e[19],s.jsxs(s.Fragment,{children:[d,s.jsx("div",{className:"mb-6",children:s.jsxs("div",{className:"mb-2 flex items-center justify-between",children:[s.jsx("div",{className:"flex items-center gap-2 text-muted-foreground text-sm",children:N.map((t,l)=>s.jsxs("div",{className:"flex items-center gap-2",children:[l>0&&s.jsx("span",{children:"/"}),l===N.length-1?s.jsx("span",{className:"text-foreground",children:t.name}):s.jsx(K,{className:"hover:text-foreground",to:t.path,children:t.name})]},t.path))}),f]})}),p,u,j]})}export{J as component};
+1 -1
View File
@@ -1 +1 @@
import{c as O,bo as T,u as D,bp as J,j as s,bq as K,B as Q}from"./index.js";import{a as U,u as W,I as X,F as Y,b as Z,c as ee}from"./field.js";import{u as se}from"./useSuspenseQuery.js";import{B as q}from"./back-link.js";import{E as te}from"./error-banner.js";import{P as ie}from"./page-heading.js";import{d as le}from"./ec2.js";import"./useMutation.js";function fe(){const e=O.c(58),{id:R}=T.useParams(),o=D();let S;e[0]!==R?(S=K(R),e[0]=R,e[1]=S):S=e[1];const{data:H}=se(S),A=H.Volumes?.[0],t=A?.VolumeId,l=le(),i=A?.Size??1;let a;e[2]!==i?(a=U(J.refine(r=>r.size>i,{message:`New size must be greater than current size (${i} GiB)`,path:["size"]})),e[2]=i,e[3]=a):a=e[3];let n;e[4]!==i?(n={size:i},e[4]=i,e[5]=n):n=e[5];let B;e[6]!==a||e[7]!==n?(B={resolver:a,defaultValues:n},e[6]=a,e[7]=n,e[8]=B):B=e[8];const{handleSubmit:k,register:E,formState:L}=W(B),{errors:V,isSubmitting:$}=L;if(!t){let r;return e[9]===Symbol.for("react.memo_cache_sentinel")?(r=s.jsxs(s.Fragment,{children:[s.jsx(q,{to:"/ec2/describe-volumes",children:"Back to volumes"}),s.jsx("p",{className:"text-muted-foreground",children:"Volume not found."})]}),e[9]=r):r=e[9],r}let F;e[10]!==l||e[11]!==o||e[12]!==t?(F=async r=>{await l.mutateAsync({...r,volumeId:t}),o({to:"/ec2/describe-volumes/$id",params:{id:t}})},e[10]=l,e[11]=o,e[12]=t,e[13]=F):F=e[13];const w=F;let m,c;e[14]!==t?(m=s.jsx(q,{params:{id:t},to:"/ec2/describe-volumes/$id",children:"Back to volume"}),c=s.jsx(ie,{subtitle:t,title:"Resize Volume"}),e[14]=t,e[15]=m,e[16]=c):(m=e[15],c=e[16]);let u;e[17]!==l.error?(u=l.error&&s.jsx(te,{error:l.error,msg:"Failed to resize volume"}),e[17]=l.error,e[18]=u):u=e[18];let d;e[19]!==k||e[20]!==w?(d=k(w),e[19]=k,e[20]=w,e[21]=d):d=e[21];let f;e[22]!==i?(f=s.jsx("div",{className:"space-y-2",children:s.jsxs("p",{className:"text-muted-foreground text-sm",children:["Current size: ",i," GiB"]})}),e[22]=i,e[23]=f):f=e[23];let N;e[24]===Symbol.for("react.memo_cache_sentinel")?(N=s.jsx(Z,{children:s.jsx("label",{htmlFor:"size",children:"New size (GiB)"})}),e[24]=N):N=e[24];const I=!!V.size,_=i+1;let p;e[25]!==E?(p=E("size",{valueAsNumber:!0}),e[25]=E,e[26]=p):p=e[26];let x;e[27]!==I||e[28]!==_||e[29]!==p?(x=s.jsx(X,{"aria-invalid":I,id:"size",min:_,type:"number",...p}),e[27]=I,e[28]=_,e[29]=p,e[30]=x):x=e[30];let b;e[31]!==V.size?(b=s.jsx(Y,{errors:[V.size]}),e[31]=V.size,e[32]=b):b=e[32];let h;e[33]!==x||e[34]!==b?(h=s.jsxs(ee,{children:[N,x,b]}),e[33]=x,e[34]=b,e[35]=h):h=e[35];const C=$||l.isPending;let j;e[36]!==o||e[37]!==t?(j=()=>o({to:"/ec2/describe-volumes/$id",params:{id:t}}),e[36]=o,e[37]=t,e[38]=j):j=e[38];let z;e[39]!==C||e[40]!==j?(z=s.jsx(Q,{disabled:C,onClick:j,type:"button",variant:"outline",children:"Cancel"}),e[39]=C,e[40]=j,e[41]=z):z=e[41];const G=$||l.isPending,M=$||l.isPending?"Resizing…":"Resize Volume";let g;e[42]!==G||e[43]!==M?(g=s.jsx(Q,{disabled:G,type:"submit",children:M}),e[42]=G,e[43]=M,e[44]=g):g=e[44];let v;e[45]!==z||e[46]!==g?(v=s.jsxs("div",{className:"flex gap-2",children:[z,g]}),e[45]=z,e[46]=g,e[47]=v):v=e[47];let y;e[48]!==f||e[49]!==h||e[50]!==v||e[51]!==d?(y=s.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:d,children:[f,h,v]}),e[48]=f,e[49]=h,e[50]=v,e[51]=d,e[52]=y):y=e[52];let P;return e[53]!==y||e[54]!==m||e[55]!==c||e[56]!==u?(P=s.jsxs(s.Fragment,{children:[m,c,u,y]}),e[53]=y,e[54]=m,e[55]=c,e[56]=u,e[57]=P):P=e[57],P}export{fe as component};
import{c as T,bn as q,u as D,bo as J,j as s,bp as K,B as Q}from"./index.js";import{a as U,u as W,I as X,F as Y,b as Z,c as ee}from"./field.js";import{u as se}from"./useSuspenseQuery.js";import{B as H}from"./back-link.js";import{E as te}from"./error-banner.js";import{P as ie}from"./page-heading.js";import{d as le}from"./ec2.js";import"./useMutation.js";function fe(){const e=T.c(58),{id:R}=q.useParams(),o=D();let S;e[0]!==R?(S=K(R),e[0]=R,e[1]=S):S=e[1];const{data:L}=se(S),A=L.Volumes?.[0],t=A?.VolumeId,l=le(),i=A?.Size??1;let n;e[2]!==i?(n=U(J.refine(r=>r.size>i,{message:`New size must be greater than current size (${i} GiB)`,path:["size"]})),e[2]=i,e[3]=n):n=e[3];let a;e[4]!==i?(a={size:i},e[4]=i,e[5]=a):a=e[5];let B;e[6]!==n||e[7]!==a?(B={resolver:n,defaultValues:a},e[6]=n,e[7]=a,e[8]=B):B=e[8];const{handleSubmit:k,register:E,formState:O}=W(B),{errors:V,isSubmitting:$}=O;if(!t){let r;return e[9]===Symbol.for("react.memo_cache_sentinel")?(r=s.jsxs(s.Fragment,{children:[s.jsx(H,{to:"/ec2/describe-volumes",children:"Back to volumes"}),s.jsx("p",{className:"text-muted-foreground",children:"Volume not found."})]}),e[9]=r):r=e[9],r}let F;e[10]!==l||e[11]!==o||e[12]!==t?(F=async r=>{await l.mutateAsync({...r,volumeId:t}),o({to:"/ec2/describe-volumes/$id",params:{id:t}})},e[10]=l,e[11]=o,e[12]=t,e[13]=F):F=e[13];const w=F;let m,c;e[14]!==t?(m=s.jsx(H,{params:{id:t},to:"/ec2/describe-volumes/$id",children:"Back to volume"}),c=s.jsx(ie,{subtitle:t,title:"Resize Volume"}),e[14]=t,e[15]=m,e[16]=c):(m=e[15],c=e[16]);let u;e[17]!==l.error?(u=l.error&&s.jsx(te,{error:l.error,msg:"Failed to resize volume"}),e[17]=l.error,e[18]=u):u=e[18];let d;e[19]!==k||e[20]!==w?(d=k(w),e[19]=k,e[20]=w,e[21]=d):d=e[21];let f;e[22]!==i?(f=s.jsx("div",{className:"space-y-2",children:s.jsxs("p",{className:"text-muted-foreground text-sm",children:["Current size: ",i," GiB"]})}),e[22]=i,e[23]=f):f=e[23];let N;e[24]===Symbol.for("react.memo_cache_sentinel")?(N=s.jsx(Z,{children:s.jsx("label",{htmlFor:"size",children:"New size (GiB)"})}),e[24]=N):N=e[24];const I=!!V.size,_=i+1;let p;e[25]!==E?(p=E("size",{valueAsNumber:!0}),e[25]=E,e[26]=p):p=e[26];let x;e[27]!==I||e[28]!==_||e[29]!==p?(x=s.jsx(X,{"aria-invalid":I,id:"size",min:_,type:"number",...p}),e[27]=I,e[28]=_,e[29]=p,e[30]=x):x=e[30];let b;e[31]!==V.size?(b=s.jsx(Y,{errors:[V.size]}),e[31]=V.size,e[32]=b):b=e[32];let h;e[33]!==x||e[34]!==b?(h=s.jsxs(ee,{children:[N,x,b]}),e[33]=x,e[34]=b,e[35]=h):h=e[35];const C=$||l.isPending;let j;e[36]!==o||e[37]!==t?(j=()=>o({to:"/ec2/describe-volumes/$id",params:{id:t}}),e[36]=o,e[37]=t,e[38]=j):j=e[38];let z;e[39]!==C||e[40]!==j?(z=s.jsx(Q,{disabled:C,onClick:j,type:"button",variant:"outline",children:"Cancel"}),e[39]=C,e[40]=j,e[41]=z):z=e[41];const G=$||l.isPending,M=$||l.isPending?"Resizing…":"Resize Volume";let g;e[42]!==G||e[43]!==M?(g=s.jsx(Q,{disabled:G,type:"submit",children:M}),e[42]=G,e[43]=M,e[44]=g):g=e[44];let v;e[45]!==z||e[46]!==g?(v=s.jsxs("div",{className:"flex gap-2",children:[z,g]}),e[45]=z,e[46]=g,e[47]=v):v=e[47];let y;e[48]!==f||e[49]!==h||e[50]!==v||e[51]!==d?(y=s.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:d,children:[f,h,v]}),e[48]=f,e[49]=h,e[50]=v,e[51]=d,e[52]=y):y=e[52];let P;return e[53]!==y||e[54]!==m||e[55]!==c||e[56]!==u?(P=s.jsxs(s.Fragment,{children:[m,c,u,y]}),e[53]=y,e[54]=m,e[55]=c,e[56]=u,e[57]=P):P=e[57],P}export{fe as component};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
import{c as O,bB as Q,u as L,r as M,bA as V,j as t,bC as $,B as q}from"./index.js";import{u as G}from"./useSuspenseQuery.js";import{B as R}from"./back-link.js";import{D as x,a as j}from"./detail-row.js";import{P as J}from"./page-heading.js";import{b as U,c as W,d as X,h as Y,a as Z,e as ee,f as te,g as se}from"./alert-dialog.js";import{e as ie}from"./ec2.js";import{T as ae}from"./trash-2.js";import"./useMutation.js";function he(){const e=O.c(54),{id:a}=Q.useParams(),S=L();let K;e[0]!==a?(K=$(a),e[0]=a,e[1]=K):K=e[1];const{data:z}=G(K),s=z.KeyPairs?.[0],i=ie(),[w,H]=M.useState(!1);let D;e[2]!==i||e[3]!==a||e[4]!==S?(D=async()=>{await i.mutateAsync(a),S({to:"/ec2/describe-key-pairs"})},e[2]=i,e[3]=a,e[4]=S,e[5]=D):D=e[5];const B=D;if(!s?.KeyPairId){let v;return e[6]===Symbol.for("react.memo_cache_sentinel")?(v=t.jsxs(t.Fragment,{children:[t.jsx(R,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),t.jsx("p",{className:"text-muted-foreground",children:"Key pair not found."})]}),e[6]=v):v=e[6],v}let b;e[7]!==s.CreateTime?(b=V(s.CreateTime),e[7]=s.CreateTime,e[8]=b):b=e[8];const F=b;let P;e[9]===Symbol.for("react.memo_cache_sentinel")?(P=t.jsx(R,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),e[9]=P):P=e[9];let T;e[10]===Symbol.for("react.memo_cache_sentinel")?(T=()=>H(!0),e[10]=T):T=e[10];let C;e[11]===Symbol.for("react.memo_cache_sentinel")?(C=t.jsxs(q,{onClick:T,size:"sm",variant:"destructive",children:[t.jsx(ae,{className:"size-4"}),"Delete"]}),e[11]=C):C=e[11];const I=s.KeyName??"";let l;e[12]!==I?(l=t.jsx(J,{actions:C,subtitle:"EC2 Key Pair Details",title:I}),e[12]=I,e[13]=l):l=e[13];let _;e[14]===Symbol.for("react.memo_cache_sentinel")?(_=t.jsx(x.Header,{children:"Key Pair Information"}),e[14]=_):_=e[14];let r;e[15]!==s.KeyName?(r=t.jsx(j,{label:"Key Name",value:s.KeyName}),e[15]=s.KeyName,e[16]=r):r=e[16];let n;e[17]!==s.KeyPairId?(n=t.jsx(j,{label:"Key Pair ID",value:s.KeyPairId}),e[17]=s.KeyPairId,e[18]=n):n=e[18];let o;e[19]!==s.KeyType?(o=t.jsx(j,{label:"Key Type",value:s.KeyType}),e[19]=s.KeyType,e[20]=o):o=e[20];let c;e[21]!==s.KeyFingerprint?(c=t.jsx(j,{label:"Key Fingerprint",value:s.KeyFingerprint}),e[21]=s.KeyFingerprint,e[22]=c):c=e[22];let m;e[23]!==F?(m=t.jsx(j,{label:"Creation Date",value:F}),e[23]=F,e[24]=m):m=e[24];let y;e[25]!==n||e[26]!==o||e[27]!==c||e[28]!==m||e[29]!==r?(y=t.jsxs(x,{children:[_,t.jsxs(x.Content,{children:[r,n,o,c,m]})]}),e[25]=n,e[26]=o,e[27]=c,e[28]=m,e[29]=r,e[30]=y):y=e[30];let d;e[31]!==s.Tags?(d=s.Tags&&s.Tags.length>0&&t.jsxs(x,{children:[t.jsx(x.Header,{children:"Tags"}),t.jsx(x.Content,{children:s.Tags.map(le)})]}),e[31]=s.Tags,e[32]=d):d=e[32];let f;e[33]!==y||e[34]!==d||e[35]!==l?(f=t.jsxs("div",{className:"space-y-6",children:[l,y,d]}),e[33]=y,e[34]=d,e[35]=l,e[36]=f):f=e[36];let k;e[37]===Symbol.for("react.memo_cache_sentinel")?(k=t.jsx(Z,{children:"Delete Key Pair"}),e[37]=k):k=e[37];let p;e[38]!==s.KeyName?(p=t.jsxs(U,{children:[k,t.jsxs(W,{children:['Are you sure you want to delete the key pair "',s.KeyName,'"? This action cannot be undone. Any instances using this key pair will no longer be able to be accessed with it.']})]}),e[38]=s.KeyName,e[39]=p):p=e[39];let A;e[40]===Symbol.for("react.memo_cache_sentinel")?(A=t.jsx(ee,{children:"Cancel"}),e[40]=A):A=e[40];const E=i.isPending?"Deleting…":"Delete";let h;e[41]!==i.isPending||e[42]!==B||e[43]!==E?(h=t.jsxs(X,{children:[A,t.jsx(Y,{disabled:i.isPending,onClick:B,children:E})]}),e[41]=i.isPending,e[42]=B,e[43]=E,e[44]=h):h=e[44];let u;e[45]!==p||e[46]!==h?(u=t.jsxs(te,{children:[p,h]}),e[45]=p,e[46]=h,e[47]=u):u=e[47];let g;e[48]!==w||e[49]!==u?(g=t.jsx(se,{onOpenChange:H,open:w,children:u}),e[48]=w,e[49]=u,e[50]=g):g=e[50];let N;return e[51]!==f||e[52]!==g?(N=t.jsxs(t.Fragment,{children:[P,f,g]}),e[51]=f,e[52]=g,e[53]=N):N=e[53],N}function le(e){return t.jsx(j,{label:e.Key||"",value:e.Value},e.Key)}export{he as component};
import{c as O,bB as Q,u as L,r as M,bz as V,j as t,bC as $,B as q}from"./index.js";import{u as G}from"./useSuspenseQuery.js";import{B as R}from"./back-link.js";import{D as x,a as j}from"./detail-row.js";import{P as J}from"./page-heading.js";import{b as U,c as W,d as X,h as Y,a as Z,e as ee,f as te,g as se}from"./alert-dialog.js";import{f as ie}from"./ec2.js";import{T as ae}from"./trash-2.js";import"./useMutation.js";function he(){const e=O.c(54),{id:a}=Q.useParams(),S=L();let K;e[0]!==a?(K=$(a),e[0]=a,e[1]=K):K=e[1];const{data:z}=G(K),s=z.KeyPairs?.[0],i=ie(),[w,H]=M.useState(!1);let D;e[2]!==i||e[3]!==a||e[4]!==S?(D=async()=>{await i.mutateAsync(a),S({to:"/ec2/describe-key-pairs"})},e[2]=i,e[3]=a,e[4]=S,e[5]=D):D=e[5];const B=D;if(!s?.KeyPairId){let v;return e[6]===Symbol.for("react.memo_cache_sentinel")?(v=t.jsxs(t.Fragment,{children:[t.jsx(R,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),t.jsx("p",{className:"text-muted-foreground",children:"Key pair not found."})]}),e[6]=v):v=e[6],v}let b;e[7]!==s.CreateTime?(b=V(s.CreateTime),e[7]=s.CreateTime,e[8]=b):b=e[8];const F=b;let P;e[9]===Symbol.for("react.memo_cache_sentinel")?(P=t.jsx(R,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),e[9]=P):P=e[9];let T;e[10]===Symbol.for("react.memo_cache_sentinel")?(T=()=>H(!0),e[10]=T):T=e[10];let C;e[11]===Symbol.for("react.memo_cache_sentinel")?(C=t.jsxs(q,{onClick:T,size:"sm",variant:"destructive",children:[t.jsx(ae,{className:"size-4"}),"Delete"]}),e[11]=C):C=e[11];const I=s.KeyName??"";let l;e[12]!==I?(l=t.jsx(J,{actions:C,subtitle:"EC2 Key Pair Details",title:I}),e[12]=I,e[13]=l):l=e[13];let _;e[14]===Symbol.for("react.memo_cache_sentinel")?(_=t.jsx(x.Header,{children:"Key Pair Information"}),e[14]=_):_=e[14];let r;e[15]!==s.KeyName?(r=t.jsx(j,{label:"Key Name",value:s.KeyName}),e[15]=s.KeyName,e[16]=r):r=e[16];let n;e[17]!==s.KeyPairId?(n=t.jsx(j,{label:"Key Pair ID",value:s.KeyPairId}),e[17]=s.KeyPairId,e[18]=n):n=e[18];let o;e[19]!==s.KeyType?(o=t.jsx(j,{label:"Key Type",value:s.KeyType}),e[19]=s.KeyType,e[20]=o):o=e[20];let c;e[21]!==s.KeyFingerprint?(c=t.jsx(j,{label:"Key Fingerprint",value:s.KeyFingerprint}),e[21]=s.KeyFingerprint,e[22]=c):c=e[22];let m;e[23]!==F?(m=t.jsx(j,{label:"Creation Date",value:F}),e[23]=F,e[24]=m):m=e[24];let y;e[25]!==n||e[26]!==o||e[27]!==c||e[28]!==m||e[29]!==r?(y=t.jsxs(x,{children:[_,t.jsxs(x.Content,{children:[r,n,o,c,m]})]}),e[25]=n,e[26]=o,e[27]=c,e[28]=m,e[29]=r,e[30]=y):y=e[30];let d;e[31]!==s.Tags?(d=s.Tags&&s.Tags.length>0&&t.jsxs(x,{children:[t.jsx(x.Header,{children:"Tags"}),t.jsx(x.Content,{children:s.Tags.map(le)})]}),e[31]=s.Tags,e[32]=d):d=e[32];let f;e[33]!==y||e[34]!==d||e[35]!==l?(f=t.jsxs("div",{className:"space-y-6",children:[l,y,d]}),e[33]=y,e[34]=d,e[35]=l,e[36]=f):f=e[36];let k;e[37]===Symbol.for("react.memo_cache_sentinel")?(k=t.jsx(Z,{children:"Delete Key Pair"}),e[37]=k):k=e[37];let p;e[38]!==s.KeyName?(p=t.jsxs(U,{children:[k,t.jsxs(W,{children:['Are you sure you want to delete the key pair "',s.KeyName,'"? This action cannot be undone. Any instances using this key pair will no longer be able to be accessed with it.']})]}),e[38]=s.KeyName,e[39]=p):p=e[39];let A;e[40]===Symbol.for("react.memo_cache_sentinel")?(A=t.jsx(ee,{children:"Cancel"}),e[40]=A):A=e[40];const E=i.isPending?"Deleting…":"Delete";let h;e[41]!==i.isPending||e[42]!==B||e[43]!==E?(h=t.jsxs(X,{children:[A,t.jsx(Y,{disabled:i.isPending,onClick:B,children:E})]}),e[41]=i.isPending,e[42]=B,e[43]=E,e[44]=h):h=e[44];let u;e[45]!==p||e[46]!==h?(u=t.jsxs(te,{children:[p,h]}),e[45]=p,e[46]=h,e[47]=u):u=e[47];let g;e[48]!==w||e[49]!==u?(g=t.jsx(se,{onOpenChange:H,open:w,children:u}),e[48]=w,e[49]=u,e[50]=g):g=e[50];let N;return e[51]!==f||e[52]!==g?(N=t.jsxs(t.Fragment,{children:[P,f,g]}),e[51]=f,e[52]=g,e[53]=N):N=e[53],N}function le(e){return t.jsx(j,{label:e.Key||"",value:e.Value},e.Key)}export{he as component};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
import{c as g,c7 as u,j as s,bP as p}from"./index.js";import{u as x}from"./useSuspenseQuery.js";import{B as d}from"./back-link.js";import{P as I}from"./page-heading.js";import{B as j}from"./badge.js";import{A as h}from"./ami-details.js";import"./detail-row.js";function E(){const e=g.c(14),{id:c}=u.useParams();let o;e[0]!==c?(o=p(c),e[0]=c,e[1]=o):o=e[1];const{data:f}=x(o),t=f?.Images?.[0];if(!t?.ImageId){let n;return e[2]===Symbol.for("react.memo_cache_sentinel")?(n=s.jsxs(s.Fragment,{children:[s.jsx(d,{to:"/ec2/describe-images",children:"Back to images"}),s.jsx("p",{className:"text-muted-foreground",children:"Image not found."})]}),e[2]=n):n=e[2],n}let r;e[3]===Symbol.for("react.memo_cache_sentinel")?(r=s.jsx(d,{to:"/ec2/describe-images",children:"Back to images"}),e[3]=r):r=e[3];let a;e[4]!==t.State?(a=t.State?s.jsx(j,{variant:t.State==="available"?"default":"outline",children:t.State}):void 0,e[4]=t.State,e[5]=a):a=e[5];let i;e[6]!==t.ImageId||e[7]!==a?(i=s.jsx(I,{actions:a,subtitle:"EC2 Image Details",title:t.ImageId}),e[6]=t.ImageId,e[7]=a,e[8]=i):i=e[8];let m;e[9]!==t?(m=s.jsx(h,{image:t,showExtendedDetails:!0}),e[9]=t,e[10]=m):m=e[10];let l;return e[11]!==i||e[12]!==m?(l=s.jsxs(s.Fragment,{children:[r,s.jsxs("div",{className:"space-y-6",children:[i,m]})]}),e[11]=i,e[12]=m,e[13]=l):l=e[13],l}export{E as component};
import{c as g,c8 as u,j as s,bP as p}from"./index.js";import{u as x}from"./useSuspenseQuery.js";import{B as d}from"./back-link.js";import{P as I}from"./page-heading.js";import{B as j}from"./badge.js";import{A as h}from"./ami-details.js";import"./detail-row.js";function E(){const e=g.c(14),{id:c}=u.useParams();let o;e[0]!==c?(o=p(c),e[0]=c,e[1]=o):o=e[1];const{data:f}=x(o),t=f?.Images?.[0];if(!t?.ImageId){let n;return e[2]===Symbol.for("react.memo_cache_sentinel")?(n=s.jsxs(s.Fragment,{children:[s.jsx(d,{to:"/ec2/describe-images",children:"Back to images"}),s.jsx("p",{className:"text-muted-foreground",children:"Image not found."})]}),e[2]=n):n=e[2],n}let r;e[3]===Symbol.for("react.memo_cache_sentinel")?(r=s.jsx(d,{to:"/ec2/describe-images",children:"Back to images"}),e[3]=r):r=e[3];let a;e[4]!==t.State?(a=t.State?s.jsx(j,{variant:t.State==="available"?"default":"outline",children:t.State}):void 0,e[4]=t.State,e[5]=a):a=e[5];let i;e[6]!==t.ImageId||e[7]!==a?(i=s.jsx(I,{actions:a,subtitle:"EC2 Image Details",title:t.ImageId}),e[6]=t.ImageId,e[7]=a,e[8]=i):i=e[8];let m;e[9]!==t?(m=s.jsx(h,{image:t,showExtendedDetails:!0}),e[9]=t,e[10]=m):m=e[10];let l;return e[11]!==i||e[12]!==m?(l=s.jsxs(s.Fragment,{children:[r,s.jsxs("div",{className:"space-y-6",children:[i,m]})]}),e[11]=i,e[12]=m,e[13]=l):l=e[13],l}export{E as component};
+1 -1
View File
@@ -1 +1 @@
import{c as S,bA as M,j as t}from"./index.js";import{D as T,a}from"./detail-row.js";function C(i){const e=S.c(55),{image:l,showExtendedDetails:P}=i,s=P===void 0?!1:P;let y;e[0]!==l.CreationDate?(y=M(l.CreationDate),e[0]=l.CreationDate,e[1]=y):y=e[1];const V=y,A=s?"AMI Information":"AMI Details";let o;e[2]!==A?(o=t.jsx(T.Header,{children:A}),e[2]=A,e[3]=o):o=e[3];let r;e[4]!==l.ImageId?(r=t.jsx(a,{label:"AMI ID",value:l.ImageId}),e[4]=l.ImageId,e[5]=r):r=e[5];let c;e[6]!==l.Name?(c=t.jsx(a,{label:"AMI Name",value:l.Name}),e[6]=l.Name,e[7]=c):c=e[7];let n;e[8]!==l.Description?(n=t.jsx(a,{label:"Description",value:l.Description}),e[8]=l.Description,e[9]=n):n=e[9];let u;e[10]!==l.Architecture?(u=t.jsx(a,{label:"Architecture",value:l.Architecture}),e[10]=l.Architecture,e[11]=u):u=e[11];let D;e[12]!==l.VirtualizationType?(D=t.jsx(a,{label:"Virtualization Type",value:l.VirtualizationType}),e[12]=l.VirtualizationType,e[13]=D):D=e[13];const R=l.Platform||"Linux";let v;e[14]!==R?(v=t.jsx(a,{label:"Platform",value:R}),e[14]=R,e[15]=v):v=e[15];let m;e[16]!==l.PlatformDetails?(m=t.jsx(a,{label:"Platform Details",value:l.PlatformDetails}),e[16]=l.PlatformDetails,e[17]=m):m=e[17];let x;e[18]!==l.ImageType?(x=t.jsx(a,{label:"Image Type",value:l.ImageType}),e[18]=l.ImageType,e[19]=x):x=e[19];let b;e[20]!==l.RootDeviceType?(b=t.jsx(a,{label:"Root Device Type",value:l.RootDeviceType}),e[20]=l.RootDeviceType,e[21]=b):b=e[21];let f;e[22]!==l.RootDeviceName?(f=t.jsx(a,{label:"Root Device Name",value:l.RootDeviceName}),e[22]=l.RootDeviceName,e[23]=f):f=e[23];let j;e[24]!==l.OwnerId?(j=t.jsx(a,{label:"Owner",value:l.OwnerId}),e[24]=l.OwnerId,e[25]=j):j=e[25];let d;e[26]!==l.State?(d=t.jsx(a,{label:"State",value:l.State}),e[26]=l.State,e[27]=d):d=e[27];let I;e[28]!==V||e[29]!==l.Public||e[30]!==s?(I=s&&t.jsxs(t.Fragment,{children:[t.jsx(a,{label:"Creation Date",value:V}),t.jsx(a,{label:"Public",value:l.Public?"Yes":"No"})]}),e[28]=V,e[29]=l.Public,e[30]=s,e[31]=I):I=e[31];let h;e[32]!==v||e[33]!==m||e[34]!==x||e[35]!==b||e[36]!==f||e[37]!==j||e[38]!==d||e[39]!==I||e[40]!==r||e[41]!==c||e[42]!==n||e[43]!==u||e[44]!==D?(h=t.jsxs(T.Content,{children:[r,c,n,u,D,v,m,x,b,f,j,d,I]}),e[32]=v,e[33]=m,e[34]=x,e[35]=b,e[36]=f,e[37]=j,e[38]=d,e[39]=I,e[40]=r,e[41]=c,e[42]=n,e[43]=u,e[44]=D,e[45]=h):h=e[45];let N;e[46]!==h||e[47]!==o?(N=t.jsxs(T,{children:[o,h]}),e[46]=h,e[47]=o,e[48]=N):N=e[48];let p;e[49]!==l.BlockDeviceMappings||e[50]!==s?(p=s&&l.BlockDeviceMappings&&l.BlockDeviceMappings.length>0&&l.BlockDeviceMappings.map(w),e[49]=l.BlockDeviceMappings,e[50]=s,e[51]=p):p=e[51];let E;return e[52]!==N||e[53]!==p?(E=t.jsxs(t.Fragment,{children:[N,p]}),e[52]=N,e[53]=p,e[54]=E):E=e[54],E}function w(i){return t.jsxs(T,{children:[t.jsxs(T.Header,{children:["Block Device: ",i.DeviceName]}),t.jsxs(T.Content,{children:[t.jsx(a,{label:"Device Name",value:i.DeviceName}),i.Ebs&&t.jsxs(t.Fragment,{children:[t.jsx(a,{label:"Volume Size",value:i.Ebs.VolumeSize?`${i.Ebs.VolumeSize} GB`:void 0}),t.jsx(a,{label:"Volume Type",value:i.Ebs.VolumeType}),t.jsx(a,{label:"Delete On Termination",value:i.Ebs.DeleteOnTermination?"Yes":"No"}),t.jsx(a,{label:"Encrypted",value:i.Ebs.Encrypted?"Yes":"No"}),i.Ebs.SnapshotId&&t.jsx(a,{label:"Snapshot ID",value:i.Ebs.SnapshotId})]}),i.VirtualName&&t.jsx(a,{label:"Virtual Name",value:i.VirtualName})]})]},i.DeviceName)}export{C as A};
import{c as S,bz as M,j as t}from"./index.js";import{D as T,a}from"./detail-row.js";function C(i){const e=S.c(55),{image:l,showExtendedDetails:P}=i,s=P===void 0?!1:P;let y;e[0]!==l.CreationDate?(y=M(l.CreationDate),e[0]=l.CreationDate,e[1]=y):y=e[1];const V=y,R=s?"AMI Information":"AMI Details";let o;e[2]!==R?(o=t.jsx(T.Header,{children:R}),e[2]=R,e[3]=o):o=e[3];let r;e[4]!==l.ImageId?(r=t.jsx(a,{label:"AMI ID",value:l.ImageId}),e[4]=l.ImageId,e[5]=r):r=e[5];let c;e[6]!==l.Name?(c=t.jsx(a,{label:"AMI Name",value:l.Name}),e[6]=l.Name,e[7]=c):c=e[7];let n;e[8]!==l.Description?(n=t.jsx(a,{label:"Description",value:l.Description}),e[8]=l.Description,e[9]=n):n=e[9];let u;e[10]!==l.Architecture?(u=t.jsx(a,{label:"Architecture",value:l.Architecture}),e[10]=l.Architecture,e[11]=u):u=e[11];let D;e[12]!==l.VirtualizationType?(D=t.jsx(a,{label:"Virtualization Type",value:l.VirtualizationType}),e[12]=l.VirtualizationType,e[13]=D):D=e[13];const A=l.Platform||"Linux";let v;e[14]!==A?(v=t.jsx(a,{label:"Platform",value:A}),e[14]=A,e[15]=v):v=e[15];let m;e[16]!==l.PlatformDetails?(m=t.jsx(a,{label:"Platform Details",value:l.PlatformDetails}),e[16]=l.PlatformDetails,e[17]=m):m=e[17];let x;e[18]!==l.ImageType?(x=t.jsx(a,{label:"Image Type",value:l.ImageType}),e[18]=l.ImageType,e[19]=x):x=e[19];let b;e[20]!==l.RootDeviceType?(b=t.jsx(a,{label:"Root Device Type",value:l.RootDeviceType}),e[20]=l.RootDeviceType,e[21]=b):b=e[21];let f;e[22]!==l.RootDeviceName?(f=t.jsx(a,{label:"Root Device Name",value:l.RootDeviceName}),e[22]=l.RootDeviceName,e[23]=f):f=e[23];let j;e[24]!==l.OwnerId?(j=t.jsx(a,{label:"Owner",value:l.OwnerId}),e[24]=l.OwnerId,e[25]=j):j=e[25];let d;e[26]!==l.State?(d=t.jsx(a,{label:"State",value:l.State}),e[26]=l.State,e[27]=d):d=e[27];let I;e[28]!==V||e[29]!==l.Public||e[30]!==s?(I=s&&t.jsxs(t.Fragment,{children:[t.jsx(a,{label:"Creation Date",value:V}),t.jsx(a,{label:"Public",value:l.Public?"Yes":"No"})]}),e[28]=V,e[29]=l.Public,e[30]=s,e[31]=I):I=e[31];let h;e[32]!==v||e[33]!==m||e[34]!==x||e[35]!==b||e[36]!==f||e[37]!==j||e[38]!==d||e[39]!==I||e[40]!==r||e[41]!==c||e[42]!==n||e[43]!==u||e[44]!==D?(h=t.jsxs(T.Content,{children:[r,c,n,u,D,v,m,x,b,f,j,d,I]}),e[32]=v,e[33]=m,e[34]=x,e[35]=b,e[36]=f,e[37]=j,e[38]=d,e[39]=I,e[40]=r,e[41]=c,e[42]=n,e[43]=u,e[44]=D,e[45]=h):h=e[45];let N;e[46]!==h||e[47]!==o?(N=t.jsxs(T,{children:[o,h]}),e[46]=h,e[47]=o,e[48]=N):N=e[48];let p;e[49]!==l.BlockDeviceMappings||e[50]!==s?(p=s&&l.BlockDeviceMappings&&l.BlockDeviceMappings.length>0&&l.BlockDeviceMappings.map(z),e[49]=l.BlockDeviceMappings,e[50]=s,e[51]=p):p=e[51];let E;return e[52]!==N||e[53]!==p?(E=t.jsxs(t.Fragment,{children:[N,p]}),e[52]=N,e[53]=p,e[54]=E):E=e[54],E}function z(i){return t.jsxs(T,{children:[t.jsxs(T.Header,{children:["Block Device: ",i.DeviceName]}),t.jsxs(T.Content,{children:[t.jsx(a,{label:"Device Name",value:i.DeviceName}),i.Ebs&&t.jsxs(t.Fragment,{children:[t.jsx(a,{label:"Volume Size",value:i.Ebs.VolumeSize?`${i.Ebs.VolumeSize} GB`:void 0}),t.jsx(a,{label:"Volume Type",value:i.Ebs.VolumeType}),t.jsx(a,{label:"Delete On Termination",value:i.Ebs.DeleteOnTermination?"Yes":"No"}),t.jsx(a,{label:"Encrypted",value:i.Ebs.Encrypted?"Yes":"No"}),i.Ebs.SnapshotId&&t.jsx(a,{label:"Snapshot ID",value:i.Ebs.SnapshotId})]}),i.VirtualName&&t.jsx(a,{label:"Virtual Name",value:i.VirtualName})]})]},i.DeviceName)}export{C as A};
+1 -1
View File
@@ -1 +1 @@
import{c as g,a5 as f,a as b,by as p,c8 as m}from"./index.js";const h=p("group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 font-medium text-[0.625rem] transition-all transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-2.5!",{variants:{variant:{default:"bg-primary text-primary-foreground [a]:hover:bg-primary/80",secondary:"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",destructive:"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",outline:"border-border bg-input/20 text-foreground dark:bg-input/30 [a]:hover:bg-muted [a]:hover:text-muted-foreground",ghost:"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",link:"text-primary underline-offset-4 hover:underline"}},defaultVariants:{variant:"default"}});function y(d){const e=g.c(13);let t,i,a,s,r;if(e[0]!==d){const{className:l,variant:u,render:v,...c}=d;t=v,r=u===void 0?"default":u,s=m,i="span",a=f({className:b(h({className:l,variant:r}))},c),e[0]=d,e[1]=t,e[2]=i,e[3]=a,e[4]=s,e[5]=r}else t=e[1],i=e[2],a=e[3],s=e[4],r=e[5];let n;e[6]!==r?(n={slot:"badge",variant:r},e[6]=r,e[7]=n):n=e[7];let o;return e[8]!==t||e[9]!==i||e[10]!==a||e[11]!==n?(o={defaultTagName:i,props:a,render:t,state:n},e[8]=t,e[9]=i,e[10]=a,e[11]=n,e[12]=o):o=e[12],s(o)}export{y as B};
import{c as g,a5 as f,a as b,bx as p,c9 as m}from"./index.js";const h=p("group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 font-medium text-[0.625rem] transition-all transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-2.5!",{variants:{variant:{default:"bg-primary text-primary-foreground [a]:hover:bg-primary/80",secondary:"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",destructive:"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",outline:"border-border bg-input/20 text-foreground dark:bg-input/30 [a]:hover:bg-muted [a]:hover:text-muted-foreground",ghost:"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",link:"text-primary underline-offset-4 hover:underline"}},defaultVariants:{variant:"default"}});function y(d){const e=g.c(13);let t,i,a,s,r;if(e[0]!==d){const{className:l,variant:u,render:v,...c}=d;t=v,r=u===void 0?"default":u,s=m,i="span",a=f({className:b(h({className:l,variant:r}))},c),e[0]=d,e[1]=t,e[2]=i,e[3]=a,e[4]=s,e[5]=r}else t=e[1],i=e[2],a=e[3],s=e[4],r=e[5];let n;e[6]!==r?(n={slot:"badge",variant:r},e[6]=r,e[7]=n):n=e[7];let o;return e[8]!==t||e[9]!==i||e[10]!==a||e[11]!==n?(o={defaultTagName:i,props:a,render:t,state:n},e[8]=t,e[9]=i,e[10]=a,e[11]=n,e[12]=o):o=e[12],s(o)}export{y as B};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
import{cj as z,ck as x}from"./index.js";class T{marshaller;serializer;deserializer;serdeContext;defaultContentType;constructor({marshaller:a,serializer:n,deserializer:o,serdeContext:m,defaultContentType:y}){this.marshaller=a,this.serializer=n,this.deserializer=o,this.serdeContext=m,this.defaultContentType=y}async serializeEventStream({eventStream:a,requestSchema:n,initialRequest:o}){const m=this.marshaller,y=n.getEventStreamMember(),p=n.getMemberSchema(y),d=this.serializer,f=this.defaultContentType,h=Symbol("initialRequestMarker"),S={async*[Symbol.asyncIterator](){if(o){const r={":event-type":{type:"string",value:"initial-request"},":message-type":{type:"string",value:"event"},":content-type":{type:"string",value:f}};d.write(n,o);const t=d.flush();yield{[h]:!0,headers:r,body:t}}for await(const r of a)yield r}};return m.serialize(S,r=>{if(r[h])return{headers:r.headers,body:r.body};const t=Object.keys(r).find(s=>s!=="__type")??"",{additionalHeaders:e,body:i,eventType:l,explicitPayloadContentType:c}=this.writeEventBody(t,p,r);return{headers:{":event-type":{type:"string",value:l},":message-type":{type:"string",value:"event"},":content-type":{type:"string",value:c??f},...e},body:i}})}async deserializeEventStream({response:a,responseSchema:n,initialResponseContainer:o}){const m=this.marshaller,y=n.getEventStreamMember(),d=n.getMemberSchema(y).getMemberSchemas(),f=Symbol("initialResponseMarker"),h=m.deserialize(a.body,async t=>{const e=Object.keys(t).find(l=>l!=="__type")??"",i=t[e].body;if(e==="initial-response"){const l=await this.deserializer.read(n,i);return delete l[y],{[f]:!0,...l}}else if(e in d){const l=d[e];if(l.isStructSchema()){const c={};let u=!1;for(const[s,g]of l.structIterator()){const{eventHeader:v,eventPayload:w}=g.getMergedTraits();if(u=u||!!(v||w),w)g.isBlobSchema()?c[s]=i:g.isStringSchema()?c[s]=(this.serdeContext?.utf8Encoder??z)(i):g.isStructSchema()&&(c[s]=await this.deserializer.read(g,i));else if(v){const b=t[e].headers[s]?.value;b!=null&&(g.isNumericSchema()?b&&typeof b=="object"&&"bytes"in b?c[s]=BigInt(b.toString()):c[s]=Number(b):c[s]=b)}}if(u)return{[e]:c};if(i.byteLength===0)return{[e]:{}}}return{[e]:await this.deserializer.read(l,i)}}else return{$unknown:t}}),S=h[Symbol.asyncIterator](),r=await S.next();if(r.done)return h;if(r.value?.[f]){if(!n)throw new Error("@smithy::core/protocols - initial-response event encountered in event stream but no response schema given.");for(const[t,e]of Object.entries(r.value))o[t]=e}return{async*[Symbol.asyncIterator](){for(r?.value?.[f]||(yield r.value);;){const{done:t,value:e}=await S.next();if(t)break;yield e}}}}writeEventBody(a,n,o){const m=this.serializer;let y=a,p=null,d;const f=n.getSchema()[4].includes(a),h={};if(f){const t=n.getMemberSchema(a);if(t.isStructSchema()){for(const[e,i]of t.structIterator()){const{eventHeader:l,eventPayload:c}=i.getMergedTraits();if(c)p=e;else if(l){const u=o[a][e];let s="binary";i.isNumericSchema()?(-2)**31<=u&&u<=2**31-1?s="integer":s="long":i.isTimestampSchema()?s="timestamp":i.isStringSchema()?s="string":i.isBooleanSchema()&&(s="boolean"),u!=null&&(h[e]={type:s,value:u},delete o[a][e])}}if(p!==null){const e=t.getMemberSchema(p);e.isBlobSchema()?d="application/octet-stream":e.isStringSchema()&&(d="text/plain"),m.write(e,o[a][p])}else m.write(t,o[a])}else throw new Error("@smithy/core/event-streams - non-struct member not supported in event stream union.")}else{const[t,e]=o[a];y=t,m.write(15,e)}const S=m.flush();return{body:typeof S=="string"?(this.serdeContext?.utf8Decoder??x)(S):S,eventType:y,explicitPayloadContentType:d,additionalHeaders:h}}}export{T as EventStreamSerde};
import{ck as z,cl as x}from"./index.js";class T{marshaller;serializer;deserializer;serdeContext;defaultContentType;constructor({marshaller:a,serializer:n,deserializer:o,serdeContext:m,defaultContentType:y}){this.marshaller=a,this.serializer=n,this.deserializer=o,this.serdeContext=m,this.defaultContentType=y}async serializeEventStream({eventStream:a,requestSchema:n,initialRequest:o}){const m=this.marshaller,y=n.getEventStreamMember(),p=n.getMemberSchema(y),d=this.serializer,f=this.defaultContentType,h=Symbol("initialRequestMarker"),S={async*[Symbol.asyncIterator](){if(o){const r={":event-type":{type:"string",value:"initial-request"},":message-type":{type:"string",value:"event"},":content-type":{type:"string",value:f}};d.write(n,o);const t=d.flush();yield{[h]:!0,headers:r,body:t}}for await(const r of a)yield r}};return m.serialize(S,r=>{if(r[h])return{headers:r.headers,body:r.body};const t=Object.keys(r).find(s=>s!=="__type")??"",{additionalHeaders:e,body:i,eventType:l,explicitPayloadContentType:c}=this.writeEventBody(t,p,r);return{headers:{":event-type":{type:"string",value:l},":message-type":{type:"string",value:"event"},":content-type":{type:"string",value:c??f},...e},body:i}})}async deserializeEventStream({response:a,responseSchema:n,initialResponseContainer:o}){const m=this.marshaller,y=n.getEventStreamMember(),d=n.getMemberSchema(y).getMemberSchemas(),f=Symbol("initialResponseMarker"),h=m.deserialize(a.body,async t=>{const e=Object.keys(t).find(l=>l!=="__type")??"",i=t[e].body;if(e==="initial-response"){const l=await this.deserializer.read(n,i);return delete l[y],{[f]:!0,...l}}else if(e in d){const l=d[e];if(l.isStructSchema()){const c={};let u=!1;for(const[s,g]of l.structIterator()){const{eventHeader:v,eventPayload:w}=g.getMergedTraits();if(u=u||!!(v||w),w)g.isBlobSchema()?c[s]=i:g.isStringSchema()?c[s]=(this.serdeContext?.utf8Encoder??z)(i):g.isStructSchema()&&(c[s]=await this.deserializer.read(g,i));else if(v){const b=t[e].headers[s]?.value;b!=null&&(g.isNumericSchema()?b&&typeof b=="object"&&"bytes"in b?c[s]=BigInt(b.toString()):c[s]=Number(b):c[s]=b)}}if(u)return{[e]:c};if(i.byteLength===0)return{[e]:{}}}return{[e]:await this.deserializer.read(l,i)}}else return{$unknown:t}}),S=h[Symbol.asyncIterator](),r=await S.next();if(r.done)return h;if(r.value?.[f]){if(!n)throw new Error("@smithy::core/protocols - initial-response event encountered in event stream but no response schema given.");for(const[t,e]of Object.entries(r.value))o[t]=e}return{async*[Symbol.asyncIterator](){for(r?.value?.[f]||(yield r.value);;){const{done:t,value:e}=await S.next();if(t)break;yield e}}}}writeEventBody(a,n,o){const m=this.serializer;let y=a,p=null,d;const f=n.getSchema()[4].includes(a),h={};if(f){const t=n.getMemberSchema(a);if(t.isStructSchema()){for(const[e,i]of t.structIterator()){const{eventHeader:l,eventPayload:c}=i.getMergedTraits();if(c)p=e;else if(l){const u=o[a][e];let s="binary";i.isNumericSchema()?(-2)**31<=u&&u<=2**31-1?s="integer":s="long":i.isTimestampSchema()?s="timestamp":i.isStringSchema()?s="string":i.isBooleanSchema()&&(s="boolean"),u!=null&&(h[e]={type:s,value:u},delete o[a][e])}}if(p!==null){const e=t.getMemberSchema(p);e.isBlobSchema()?d="application/octet-stream":e.isStringSchema()&&(d="text/plain"),m.write(e,o[a][p])}else m.write(t,o[a])}else throw new Error("@smithy/core/event-streams - non-struct member not supported in event stream union.")}else{const[t,e]=o[a];y=t,m.write(15,e)}const S=m.flush();return{body:typeof S=="string"?(this.serdeContext?.utf8Decoder??x)(S):S,eventType:y,explicitPayloadContentType:d,additionalHeaders:h}}}export{T as EventStreamSerde};
+1 -1
View File
@@ -1 +1 @@
import{c as n,aD as l,j as t,aE as m,aF as u}from"./index.js";import{u as c}from"./useSuspenseQuery.js";import{L as d}from"./list-card.js";import{P as p}from"./page-heading.js";import{B as f}from"./badge.js";function S(){const e=n.c(5),{data:r}=c(l);let s;e[0]!==r.Volumes?(s=r.Volumes||[],e[0]=r.Volumes,e[1]=s):s=e[1];const a=s;let o;e[2]===Symbol.for("react.memo_cache_sentinel")?(o=t.jsx(p,{actions:t.jsx(m,{className:u({variant:"outline"}),to:"/ec2/create-volume",children:"Create Volume"}),title:"EC2 Volumes"}),e[2]=o):o=e[2];let i;return e[3]!==a?(i=t.jsxs(t.Fragment,{children:[o,a.length>0?t.jsx("div",{className:"space-y-4",children:a.map(V)}):t.jsx("p",{className:"text-muted-foreground",children:"No volumes found."})]}),e[3]=a,e[4]=i):i=e[4],i}function V(e){return e.VolumeId?t.jsx(d,{badge:e.State?t.jsx(f,{variant:e.State==="available"?"default":"outline",children:e.State}):void 0,params:{id:e.VolumeId},subtitle:`${e.Size} GiB • ${e.VolumeType}`,title:e.VolumeId,to:"/ec2/describe-volumes/$id"},e.VolumeId):null}export{S as component};
import{c as l,aD as n,j as t,aE as m,B as u}from"./index.js";import{u as c}from"./useSuspenseQuery.js";import{L as d}from"./list-card.js";import{P as p}from"./page-heading.js";import{B as f}from"./badge.js";function b(){const e=l.c(5),{data:r}=c(n);let s;e[0]!==r.Volumes?(s=r.Volumes||[],e[0]=r.Volumes,e[1]=s):s=e[1];const o=s;let a;e[2]===Symbol.for("react.memo_cache_sentinel")?(a=t.jsx(p,{actions:t.jsx(m,{to:"/ec2/create-volume",children:t.jsx(u,{children:"Create Volume"})}),title:"EC2 Volumes"}),e[2]=a):a=e[2];let i;return e[3]!==o?(i=t.jsxs(t.Fragment,{children:[a,o.length>0?t.jsx("div",{className:"space-y-4",children:o.map(x)}):t.jsx("p",{className:"text-muted-foreground",children:"No volumes found."})]}),e[3]=o,e[4]=i):i=e[4],i}function x(e){return e.VolumeId?t.jsx(d,{badge:e.State?t.jsx(f,{variant:e.State==="available"?"default":"outline",children:e.State}):void 0,params:{id:e.VolumeId},subtitle:`${e.Size} GiB • ${e.VolumeType}`,title:e.VolumeId,to:"/ec2/describe-volumes/$id"},e.VolumeId):null}export{b as component};
+1 -1
View File
@@ -1 +1 @@
import{c as o,aG as c,j as t,aE as i,B as m}from"./index.js";import{u as d}from"./useSuspenseQuery.js";import{L as u}from"./list-card.js";import{P as l}from"./page-heading.js";import{S as p}from"./state-badge.js";function y(){const e=o.c(3),{data:a}=d(c);let s;if(e[0]!==a.Reservations){const r=a.Reservations?.flatMap(I)||[];let n;e[2]===Symbol.for("react.memo_cache_sentinel")?(n=t.jsx(l,{actions:t.jsx(i,{to:"/ec2/run-instances",children:t.jsx(m,{children:"Run Instances"})}),title:"EC2 Instances"}),e[2]=n):n=e[2],s=t.jsxs(t.Fragment,{children:[n,r.length>0?t.jsx("div",{className:"space-y-4",children:r.map(f)}):t.jsx("p",{className:"text-muted-foreground",children:"No instances found."})]}),e[0]=a.Reservations,e[1]=s}else s=e[1];return s}function f(e){return e.InstanceId?t.jsx(u,{badge:t.jsx(p,{state:e.State?.Name}),params:{id:e.InstanceId},subtitle:`${e.InstanceType} • ${e.ImageId}`,title:e.InstanceId,to:"/ec2/describe-instances/$id"},e.InstanceId):null}function I(e){return e.Instances||[]}export{y as component};
import{c as o,aF as c,j as t,aE as i,B as m}from"./index.js";import{u as d}from"./useSuspenseQuery.js";import{L as u}from"./list-card.js";import{P as l}from"./page-heading.js";import{S as p}from"./state-badge.js";function y(){const e=o.c(3),{data:a}=d(c);let s;if(e[0]!==a.Reservations){const r=a.Reservations?.flatMap(I)||[];let n;e[2]===Symbol.for("react.memo_cache_sentinel")?(n=t.jsx(l,{actions:t.jsx(i,{to:"/ec2/run-instances",children:t.jsx(m,{children:"Run Instances"})}),title:"EC2 Instances"}),e[2]=n):n=e[2],s=t.jsxs(t.Fragment,{children:[n,r.length>0?t.jsx("div",{className:"space-y-4",children:r.map(f)}):t.jsx("p",{className:"text-muted-foreground",children:"No instances found."})]}),e[0]=a.Reservations,e[1]=s}else s=e[1];return s}function f(e){return e.InstanceId?t.jsx(u,{badge:t.jsx(p,{state:e.State?.Name}),params:{id:e.InstanceId},subtitle:`${e.InstanceType} • ${e.ImageId}`,title:e.InstanceId,to:"/ec2/describe-instances/$id"},e.InstanceId):null}function I(e){return e.Instances||[]}export{y as component};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
import{c0 as l,c1 as d,c2 as a,c3 as p,c4 as h,bl as b,r as o,c5 as m,c6 as v}from"./index.js";var y=class extends l{#s;#i=void 0;#t;#e;constructor(t,s){super(),this.#s=t,this.setOptions(s),this.bindMethods(),this.#r()}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(t){const s=this.options;this.options=this.#s.defaultMutationOptions(t),d(this.options,s)||this.#s.getMutationCache().notify({type:"observerOptionsUpdated",mutation:this.#t,observer:this}),s?.mutationKey&&this.options.mutationKey&&a(s.mutationKey)!==a(this.options.mutationKey)?this.reset():this.#t?.state.status==="pending"&&this.#t.setOptions(this.options)}onUnsubscribe(){this.hasListeners()||this.#t?.removeObserver(this)}onMutationUpdate(t){this.#r(),this.#o(t)}getCurrentResult(){return this.#i}reset(){this.#t?.removeObserver(this),this.#t=void 0,this.#r(),this.#o()}mutate(t,s){return this.#e=s,this.#t?.removeObserver(this),this.#t=this.#s.getMutationCache().build(this.#s,this.options),this.#t.addObserver(this),this.#t.execute(t)}#r(){const t=this.#t?.state??p();this.#i={...t,isPending:t.status==="pending",isSuccess:t.status==="success",isError:t.status==="error",isIdle:t.status==="idle",mutate:this.mutate,reset:this.reset}}#o(t){h.batch(()=>{if(this.#e&&this.hasListeners()){const s=this.#i.variables,r=this.#i.context,e={client:this.#s,meta:this.options.meta,mutationKey:this.options.mutationKey};if(t?.type==="success"){try{this.#e.onSuccess?.(t.data,s,r,e)}catch(i){Promise.reject(i)}try{this.#e.onSettled?.(t.data,null,s,r,e)}catch(i){Promise.reject(i)}}else if(t?.type==="error"){try{this.#e.onError?.(t.error,s,r,e)}catch(i){Promise.reject(i)}try{this.#e.onSettled?.(void 0,t.error,s,r,e)}catch(i){Promise.reject(i)}}}this.listeners.forEach(s=>{s(this.#i)})})}};function O(t,s){const r=b(),[e]=o.useState(()=>new y(r,t));o.useEffect(()=>{e.setOptions(t)},[e,t]);const i=o.useSyncExternalStore(o.useCallback(n=>e.subscribe(h.batchCalls(n)),[e]),()=>e.getCurrentResult(),()=>e.getCurrentResult()),u=o.useCallback((n,c)=>{e.mutate(n,c).catch(m)},[e]);if(i.error&&v(e.options.throwOnError,[i.error]))throw i.error;return{...i,mutate:u,mutateAsync:i.mutate}}export{O as u};
import{c1 as l,c2 as d,c3 as a,c4 as p,c5 as h,bk as b,r as o,c6 as m,c7 as v}from"./index.js";var y=class extends l{#s;#i=void 0;#t;#e;constructor(t,s){super(),this.#s=t,this.setOptions(s),this.bindMethods(),this.#r()}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(t){const s=this.options;this.options=this.#s.defaultMutationOptions(t),d(this.options,s)||this.#s.getMutationCache().notify({type:"observerOptionsUpdated",mutation:this.#t,observer:this}),s?.mutationKey&&this.options.mutationKey&&a(s.mutationKey)!==a(this.options.mutationKey)?this.reset():this.#t?.state.status==="pending"&&this.#t.setOptions(this.options)}onUnsubscribe(){this.hasListeners()||this.#t?.removeObserver(this)}onMutationUpdate(t){this.#r(),this.#o(t)}getCurrentResult(){return this.#i}reset(){this.#t?.removeObserver(this),this.#t=void 0,this.#r(),this.#o()}mutate(t,s){return this.#e=s,this.#t?.removeObserver(this),this.#t=this.#s.getMutationCache().build(this.#s,this.options),this.#t.addObserver(this),this.#t.execute(t)}#r(){const t=this.#t?.state??p();this.#i={...t,isPending:t.status==="pending",isSuccess:t.status==="success",isError:t.status==="error",isIdle:t.status==="idle",mutate:this.mutate,reset:this.reset}}#o(t){h.batch(()=>{if(this.#e&&this.hasListeners()){const s=this.#i.variables,r=this.#i.context,e={client:this.#s,meta:this.options.meta,mutationKey:this.options.mutationKey};if(t?.type==="success"){try{this.#e.onSuccess?.(t.data,s,r,e)}catch(i){Promise.reject(i)}try{this.#e.onSettled?.(t.data,null,s,r,e)}catch(i){Promise.reject(i)}}else if(t?.type==="error"){try{this.#e.onError?.(t.error,s,r,e)}catch(i){Promise.reject(i)}try{this.#e.onSettled?.(void 0,t.error,s,r,e)}catch(i){Promise.reject(i)}}}this.listeners.forEach(s=>{s(this.#i)})})}};function O(t,s){const r=b(),[e]=o.useState(()=>new y(r,t));o.useEffect(()=>{e.setOptions(t)},[e,t]);const i=o.useSyncExternalStore(o.useCallback(n=>e.subscribe(h.batchCalls(n)),[e]),()=>e.getCurrentResult(),()=>e.getCurrentResult()),u=o.useCallback((n,c)=>{e.mutate(n,c).catch(m)},[e]);if(i.error&&v(e.options.throwOnError,[i.error]))throw i.error;return{...i,mutate:u,mutateAsync:i.mutate}}export{O as u};
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ import {
CreateKeyPairCommand,
CreateVolumeCommand,
DeleteKeyPairCommand,
DeleteVolumeCommand,
ImportKeyPairCommand,
ModifyVolumeCommand,
RebootInstancesCommand,
@@ -213,3 +214,18 @@ export function useModifyVolume() {
},
})
}
export function useDeleteVolume() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (volumeId: string) => {
const command = new DeleteVolumeCommand({
VolumeId: volumeId,
})
return await getEc2Client().send(command)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ec2", "volumes"] })
},
})
}
@@ -1,13 +1,26 @@
import { useSuspenseQuery } from "@tanstack/react-query"
import { createFileRoute, Link } from "@tanstack/react-router"
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"
import { Trash2 } from "lucide-react"
import { useState } from "react"
import { BackLink } from "@/components/back-link"
import { DetailCard } from "@/components/detail-card"
import { DetailRow } from "@/components/detail-row"
import { PageHeading } from "@/components/page-heading"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Badge } from "@/components/ui/badge"
import { Button, buttonVariants } from "@/components/ui/button"
import { formatDateTime } from "@/lib/utils"
import { useDeleteVolume } from "@/mutations/ec2"
import { ec2VolumeQueryOptions } from "@/queries/ec2"
export const Route = createFileRoute(
@@ -28,8 +41,20 @@ export const Route = createFileRoute(
function VolumeDetail() {
const { id } = Route.useParams()
const navigate = useNavigate()
const { data } = useSuspenseQuery(ec2VolumeQueryOptions(id))
const volume = data.Volumes?.[0]
const deleteMutation = useDeleteVolume()
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const canDelete =
volume?.State === "available" &&
(!volume.Attachments || volume.Attachments.length === 0)
const handleDelete = async () => {
await deleteMutation.mutateAsync(id)
navigate({ to: "/ec2/describe-volumes" })
}
if (!volume?.VolumeId) {
return (
@@ -50,6 +75,15 @@ function VolumeDetail() {
<PageHeading
actions={
<div className="flex items-center gap-2">
<Button
disabled={!canDelete}
onClick={() => setShowDeleteDialog(true)}
size="sm"
variant="destructive"
>
<Trash2 className="size-4" />
Delete
</Button>
{volume.State === "available" ? (
<Link
className={buttonVariants({ variant: "outline" })}
@@ -117,6 +151,27 @@ function VolumeDetail() {
</DetailCard>
)}
</div>
<AlertDialog onOpenChange={setShowDeleteDialog} open={showDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Volume</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete the volume "{volume.VolumeId}"?
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={deleteMutation.isPending}
onClick={handleDelete}
>
{deleteMutation.isPending ? "Deleting…" : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}
+48 -6
View File
@@ -110,21 +110,63 @@ func launchService(cfg *Config) (err error) {
// Subscribe to the viperblock.mount subject
slog.Info("Connected. Waiting for EBS events")
// TODO: Support volume delete and predastore delete bucket
nc.QueueSubscribe("ebs.delete", "hive-workers", func(msg *nats.Msg) {
slog.Info("Received message", "data", string(msg.Data))
slog.Info("Received ebs.delete message", "data", string(msg.Data))
// Parse the message
var ebsRequest config.EBSDeleteRequest
err := json.Unmarshal(msg.Data, &ebsRequest)
if err != nil {
slog.Error("Failed to unmarshal message", "err", err)
slog.Error("Failed to unmarshal ebs.delete message", "err", err)
errResp, _ := json.Marshal(config.EBSDeleteResponse{Error: fmt.Sprintf("bad request: %v", err)})
msg.Respond(errResp)
return
}
// TODO: Improve
nc.Publish("ebs.delete.response", fmt.Appendf(nil, `{"volume":"%s","deleted":true}`, ebsRequest.Volume))
response := config.EBSDeleteResponse{Volume: ebsRequest.Volume, Success: true}
// Find and clean up the mounted volume if it exists
cfg.mu.Lock()
var matched MountedVolume
matchIdx := -1
for i, volume := range cfg.MountedVolumes {
if volume.Name == ebsRequest.Volume {
matched = volume
matchIdx = i
cfg.MountedVolumes = append(cfg.MountedVolumes[:i], cfg.MountedVolumes[i+1:]...)
break
}
}
cfg.mu.Unlock()
if matchIdx >= 0 {
// Stop WAL syncer and kill nbdkit process
if matched.VB != nil {
matched.VB.StopWALSyncer()
}
utils.KillProcess(matched.PID)
// Remove the socket file if using socket transport
if matched.Socket != "" {
slog.Info("Removing socket file", "socket", matched.Socket)
if err := os.Remove(matched.Socket); err != nil && !os.IsNotExist(err) {
slog.Error("Failed to delete nbd socket", "err", err, "socket", matched.Socket)
}
}
slog.Info("ebs.delete: cleaned up mounted volume", "volume", ebsRequest.Volume, "pid", matched.PID)
} else {
// Volume not mounted is expected for "available" volumes
slog.Info("ebs.delete: volume not mounted (expected for available volumes)", "volume", ebsRequest.Volume)
}
respData, err := json.Marshal(response)
if err != nil {
slog.Error("Failed to marshal ebs.delete response", "err", err)
msg.Respond([]byte(`{"Error":"internal marshal failure"}`))
return
}
msg.Respond(respData)
})
nc.QueueSubscribe("ebs.unmount", "hive-workers", func(msg *nats.Msg) {
+30 -4
View File
@@ -232,10 +232,36 @@ if [ "$STANDALONE_VOLUME_SIZE" -ne "$STANDALONE_NEW_SIZE" ]; then
exit 1
fi
# Delete the standalone volume (not implemented yet)
# echo "Deleting standalone volume..."
# $AWS_EC2 delete-volume --volume-id "$STANDALONE_VOLUME_ID"
echo "Standalone volume test passed"
# Delete the standalone volume
echo "Deleting standalone volume $STANDALONE_VOLUME_ID..."
$AWS_EC2 delete-volume --volume-id "$STANDALONE_VOLUME_ID"
# Verify deletion (volume should no longer appear in describe)
echo "Verifying volume deletion..."
COUNT=0
while [ $COUNT -lt 30 ]; do
set +e
VOLUME_CHECK=$($AWS_EC2 describe-volumes --volume-ids "$STANDALONE_VOLUME_ID" \
--query 'Volumes[0].VolumeId' --output text 2>&1)
DESCRIBE_EXIT=$?
set -e
# Volume gone = describe fails, returns "None", or returns empty
if [ $DESCRIBE_EXIT -ne 0 ] || [ "$VOLUME_CHECK" == "None" ] || [ -z "$VOLUME_CHECK" ]; then
echo "Volume deleted successfully"
break
fi
sleep 2
COUNT=$((COUNT + 1))
done
if [ $COUNT -ge 30 ]; then
echo "Volume deletion verification timed out"
exit 1
fi
echo "Standalone volume test passed (create -> resize -> delete)"
# Phase 5: Instance Lifecycle
echo "Phase 5: Instance Lifecycle"
+30 -1
View File
@@ -260,7 +260,36 @@ if [ "$VOLUME_SIZE" -ne "$NEW_SIZE" ]; then
exit 1
fi
echo " Standalone volume test passed"
# Delete the standalone volume
echo " Deleting standalone volume $STANDALONE_VOLUME_ID..."
$AWS_EC2 delete-volume --volume-id "$STANDALONE_VOLUME_ID"
# Verify deletion
echo " Verifying volume deletion..."
COUNT=0
while [ $COUNT -lt 30 ]; do
set +e
VOLUME_CHECK=$($AWS_EC2 describe-volumes --volume-ids "$STANDALONE_VOLUME_ID" \
--query 'Volumes[0].VolumeId' --output text 2>&1)
DESCRIBE_EXIT=$?
set -e
# Volume gone = describe fails, returns "None", or returns empty
if [ $DESCRIBE_EXIT -ne 0 ] || [ "$VOLUME_CHECK" == "None" ] || [ -z "$VOLUME_CHECK" ]; then
echo " Volume deleted successfully"
break
fi
sleep 2
COUNT=$((COUNT + 1))
done
if [ $COUNT -ge 30 ]; then
echo " ERROR: Volume deletion verification timed out"
exit 1
fi
echo " Standalone volume test passed (create -> resize -> delete)"
# Phase 6: Multi-Node Instance Tests
echo ""