Merge branch 'main' of github.com:mulgadc/hive

This commit is contained in:
benduncan
2026-01-29 20:13:00 +10:00
74 changed files with 1241 additions and 420 deletions
+3 -3
View File
@@ -24,13 +24,13 @@
"editor.defaultFormatter": "golang.go",
"editor.tabSize": 4
},
"biome.configurationPath": "./hive-ui/frontend/biome.jsonc",
"biome.configurationPath": "./hive/services/hiveui/frontend/biome.jsonc",
"biome.requireConfiguration": true,
"biome.lsp.bin": "./hive-ui/frontend/node_modules/.bin/biome",
"biome.lsp.bin": "./hive/services/hiveui/frontend/node_modules/.bin/biome",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"typescript.tsdk": "./hive-ui/frontend/node_modules/typescript/lib"
"typescript.tsdk": "./hive/services/hiveui/frontend/node_modules/typescript/lib"
}
+6 -61
View File
@@ -19,23 +19,6 @@ Each component can be used independently or as part of the integrated Hive stack
- `../viperblock/CLAUDE.md` - Viperblock development guide
- `../predastore/CLAUDE.md` - Predastore development guide
## Cross-Repository Development Setup
For platform development, clone all repositories to the same parent directory:
```bash
mkdir mulga
cd mulga
git clone <hive-repo-url> hive
git clone <viperblock-repo-url> viperblock
git clone <predastore-repo-url> predastore
```
The `hive/go.mod` uses local replace directives for cross-component development:
```go
replace github.com/mulgadc/viperblock => ../viperblock
replace github.com/mulgadc/predastore => ../predastore
```
## Build Commands
### Hive (Platform Orchestrator)
@@ -123,7 +106,7 @@ AWS SDK (custom endpoint) → Hive API Gateway → NATS Topics → Specialized D
- **Volume Lifecycle**: Coordinated through NATS between EC2 and EBS daemons
### Key Design Patterns
- **Message-Driven**: All service communication via NATS topics (ec2.*, ebs.*, s3.*, vpc.*)
- **Message-Driven**: All service communication via NATS topics
- **Queue Groups**: Load balancing and fault tolerance via NATS queue groups
- **Request-Response**: 30-second timeout pattern for AWS API compatibility
- **Service Specialization**: Dedicated daemon types for each AWS service
@@ -131,35 +114,12 @@ AWS SDK (custom endpoint) → Hive API Gateway → NATS Topics → Specialized D
### NATS Topic Structure
The system uses structured NATS topics for service communication:
- **EC2 Daemons**: Listen on `ec2.*` topics, manage QEMU/KVM instances
- **EBS Daemons**: Listen on `ebs.*` topics, coordinate with Viperblock
- **S3 Daemons**: Listen on `s3.*` topics, proxy to Predastore
- **VPC Daemons**: Listen on `vpc.*` topics, manage networking
```bash
# EC2 Service Topics:
ec2.runinstances # Launch new instances
ec2.describeinstances # Query instance status
ec2.startinstances # Start stopped instances
ec2.stopinstances # Stop running instances
ec2.terminateinstances # Terminate instances
# EBS Service Topics:
ebs.createvolume # Create new EBS volume
ebs.attachvolume # Attach volume to instance
ebs.detachvolume # Detach volume
ebs.describevolumes # List volumes
ebs.mount # Mount volume (internal)
ebs.unmount # Unmount volume (internal)
# S3 Service Topics:
s3.createbucket # Create S3 bucket
s3.putobject # Store object
s3.getobject # Retrieve object
s3.listbuckets # List buckets
# VPC Service Topics:
vpc.createvpc # Create VPC
vpc.createsubnet # Create subnet
vpc.describevpcs # List VPCs
# Health and Discovery:
health.ec2.daemon.{id} # Daemon health heartbeats
discovery.services # Service registration
@@ -174,22 +134,7 @@ All Go projects use:
### Development Workflow
#### Quick Setup (Recommended)
```bash
# 1. Clone dependencies (if not already done)
./scripts/clone-deps.sh
# 2. Setup development environment
./scripts/dev-setup.sh
# 3. Start all services
./scripts/start-dev.sh
# 4. Stop all services when done
./scripts/stop-dev.sh
```
#### Manual Development Process
#### Development Process
1. **Cross-repo Setup**: Clone all repositories to same parent directory
2. **Service Dependencies**: Start services in order: NATS → Predastore → Viperblock → NBDkit → Gateway → Daemons
3. **Build Order**: Build components: predastore → viperblock → hive
+103 -101
View File
@@ -4,13 +4,13 @@ Notes for development environment installation.
## Dependencies
The current supported OS that is validated and tested includes:
The currently supported operating systems that are validated and tested include:
* Ubuntu 22.04
* Ubuntu 24.04
* Ubuntu 25.10
For the development preview please use one of the supported versions above.
For the development preview, please use one of the supported versions above.
## Download
@@ -24,7 +24,7 @@ git clone https://github.com/mulgadc/hive.git
### Quick Install
To bootstrap the dependencies of Hive in one simple step (QEMU, Go, AWS CLI)
To bootstrap the dependencies of Hive in one simple step (QEMU, Go, AWS CLI):
```bash
sudo make -C hive quickinstall
@@ -32,16 +32,16 @@ sudo make -C hive quickinstall
### Manual Install
Alternatively, run the following steps to manually setup the required dependencies.
Alternatively, run the following steps to manually set up the required dependencies.
```bash
sudo add-apt-repository universe
sudo apt install nbdkit nbdkit-plugin-dev pkg-config qemu-system qemu-utils qemu-kvm libvirt-daemon-system libvirt-clients libvirt-dev make gcc unzip xz-utils file
```
Ensure the Go toolkit is installed for version 1.25.5 or higher. Recommended to install the latest directly from [https://go.dev/dl/](https://go.dev/dl/).
Ensure the Go toolkit is installed for version 1.25.6 or higher. It is recommended to install the latest directly from [https://go.dev/dl/](https://go.dev/dl/).
Confirm go is correctly installed, and set in your $PATH.
Confirm Go is correctly installed, and set in your $PATH.
```bash
go version
@@ -55,7 +55,7 @@ unzip awscliv2.zip
sudo ./aws/install
```
Confirm awscli version > 2.0 is installed which is required by Hive.
Confirm awscli version > 2.0 is installed.
[https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
@@ -73,7 +73,7 @@ cd hive
./scripts/dev-setup.sh # Setup complete development environment
```
Once complete, confirm `./bin/hive` exists and executable.
Once complete, confirm `./bin/hive` exists and is executable.
# Single Node Installation
@@ -104,8 +104,6 @@ Verify the certificate was added:
ls -la /usr/local/share/ca-certificates/hive-ca.crt
```
This step is required for AWS CLI 2.33+ which requires proper CA trust chains. Without this, you would need to use `--no-verify-ssl` for every AWS CLI command.
Next, set the AWS profile to use `hive` which points to the local environment.
```bash
@@ -148,7 +146,7 @@ aws ec2 create-key-pair \
| jq -r '.KeyMaterial | rtrimstr("\n")' > ~/.ssh/hive-key
```
Update permissions for the key for ssh to accept reading the file.
Update permissions for the key for SSH to accept reading the file.
```bash
chmod 600 ~/.ssh/hive-key
@@ -160,7 +158,7 @@ Next, generate a public key from the specified private key pair.
ssh-keygen -y -f ~/.ssh/hive-key > ~/.ssh/hive-key.pub
```
### Validate New Key
### Verify New Key
```bash
aws ec2 describe-key-pairs
@@ -183,13 +181,13 @@ aws ec2 describe-key-pairs
## Create AMI Template
Use the hive CLI tool to import a selected OS image. Note, the source file can be compressed (e.g image.tar.gz, image.gz, image.tar.xz) and the tool will automatically extract and upload the raw OS image as an AMI after validation the disk image contains a UEFI/BIOS boot capability.
The Hive CLI tool offers 2 ways of importing an image. You can use automatic image importing, and select from our range of images. Alternatively, provide your own image source file (e.g image.tar.gz, image.gz, image.tar.xz) and the tool will automatically extract and upload the raw OS image as an AMI (after validating the image contains UEFI/BIOS boot capability).
Note, when downloading OS images, use supported platforms that support the `cloud-init` feature to automatically bootstrap when using the Hive EC2 functionality to access SSH and networking services.
**Note:** When downloading OS images, use platforms that support the `cloud-init` feature to automatically bootstrap when using the Hive EC2 functionality to access SSH and networking services.
### Automatic Image Import
Discover available images to automatically download and install. This will pull the images from the distro official mirror and simplify the process to bootstrap a Hive installation with AMIs that include common operating systems.
Discover available images to automatically download and install. This will pull the images from the distro official mirror and simplify the process to bootstrap a Hive installation with AMIs for common operating systems.
```bash
./bin/hive admin images list
@@ -207,19 +205,7 @@ ubuntu-24.04-x86_64 | ubuntu | 24.04 | x86_64 | bios
Next, choose the image you would like to import as an AMI. Choose the appropriate `x86_64` or `arm64` architecture depending on your platform.
```bash
./bin/hive admin images import --name debian-12-arm64 --force
```
Make note of the Image-ID `ami-XXX`
```bash
✅ Image import complete. Image-ID (AMI): ami-e29fcc65734aec9ea
```
Next, verify available disk images to confirm the import was successful, replace `ami-XXX` with the value from the command output.
```bash
aws ec2 describe-images --image-ids ami-XXX
./bin/hive admin images import --name debian-12-arm64
```
### Manual Image Import
@@ -238,25 +224,29 @@ Import as an AMI to the backend store:
./bin/hive admin images import --file ~/debian-12-genericcloud-arm64.tar.xz --arch arm64 --distro debian --version 12
```
Next, verify available disk images to confirm the import was successful.
### Verify Image Import
```bash
aws ec2 describe-images
```
## Run Instance
Once Hive is successfully installed and bootstrapped with a system AMI and SSH keys, proceed to run an instance. Replace `ami-XXX` with your imported ImageId above.
Export the AMI.
```bash
export HIVE_AMI="ami-XXX"
```
### Query instance types available
Next, verify available disk images to confirm the import was successful.
Depending on the host platform Hive is installed for the CPU capabilities and vendor (e.g AMD, Intel, ARM), different compute instances will be available.
```bash
aws ec2 describe-images --image-ids $HIVE_AMI
```
Query available instance types and choose an instance type available on your host with 2 vCPUs:
## Run Instance
Once Hive is successfully installed and bootstrapped with a system AMI and SSH keys, proceed to run an instance.
### Query Instance Types
Depending on the host platform Hive is installed, different compute instances will be available.
Query available instance types and choose an instance type available on your host:
```bash
aws ec2 describe-instance-types
@@ -298,7 +288,9 @@ Export the instance-type:
export HIVE_INSTANCE="m8a.small"
```
Next, launch a new instance, note `hive-key` is the SSH key specified in the previous stage. Be sure to specify the instance-type that is available from your host as above.
### Run Instance
Next, launch a new instance, note `hive-key` is the SSH key specified in the previous stage.
```bash
aws ec2 run-instances \
@@ -332,12 +324,14 @@ A sample response is below from the `RunInstance` request, note the `InstanceId`
}
```
Export the instance ID for following the rest of the tutorial.
Export the instance ID.
```bash
export INSTANCE_ID="i-XXX"
```
### Verify Instance
Next, validate the running instance is ready.
```bash
@@ -372,7 +366,7 @@ Confirm the `State.Name` attribute is set as `running`.
## SSH Connection (development)
For a Hive development environment (toggled off for production), a local SSH port forwaring will be active to connect directly to the instance, regardless of the VPC and network settings.
For a Hive development environment (toggled off for production), a local SSH port forwarding will be active to connect directly to the instance, regardless of the VPC and network settings.
Determine the SSH port allocated.
@@ -395,7 +389,7 @@ Linux hive-vm-36765eb0 6.1.0-40-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2
ec2-user@hive-vm-36765eb0:~$
```
Congratulations, your first AMI image is imported, a new EC2 instance launched, and successfully connected via SSH for the configured SSH key, using the OS `cloud-init` procedure.
Congratulations! Your first AMI image is imported, a new EC2 instance launched, and successfully connected via SSH for the configured SSH key, using the OS `cloud-init` procedure.
## Managing Instances
@@ -409,19 +403,20 @@ aws ec2 stop-instances --instance-ids $INSTANCE_ID
```json
{
"StoppingInstances": [
{
"InstanceId": "i-36765eb0c6609e4d2",
"CurrentState": {
"Code": 64,
"Name": "stopping"
},
"PreviousState": {
"Code": 16,
"Name": "running"
}
}
]
"StoppingInstances": [
{
"InstanceId": "i-36765eb0c6609e4d2",
"CurrentState": {
"Code": 64,
"Name": "stopping"
},
"PreviousState": {
"Code": 16,
"Name": "running"
}
}
]
}
```
Next, confirm the instance has stopped as requested.
@@ -464,19 +459,20 @@ aws ec2 start-instances --instance-ids $INSTANCE_ID
```json
{
"StartingInstances": [
{
"InstanceId": "i-36765eb0c6609e4d2",
"CurrentState": {
"Code": 0,
"Name": "pending"
},
"PreviousState": {
"Code": 80,
"Name": "stopped"
}
}
]
"StartingInstances": [
{
"InstanceId": "i-36765eb0c6609e4d2",
"CurrentState": {
"Code": 0,
"Name": "pending"
},
"PreviousState": {
"Code": 80,
"Name": "stopped"
}
}
]
}
```
Next, validate the instance is running as expected.
@@ -511,7 +507,7 @@ aws ec2 describe-instances --instance-ids "i-36765eb0c6609e4d2"
### Terminate Instance
To terminate an instance, which will first stop the instance, and on success, remove the EBS volumes and permanately remove the instance data.
To terminate an instance, which will first stop the instance, and on success, remove the EBS volumes and permanently remove the instance data.
```bash
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
@@ -535,7 +531,7 @@ aws ec2 terminate-instances --instance-ids $INSTANCE_ID
}
```
Next, validate the instance is removed, this may take a few minutes depending the instance volume size.
Next, validate the instance is removed, this may take a few minutes depending on the instance volume size.
```bash
aws ec2 describe-instances --instance-ids $INSTANCE_ID
@@ -543,9 +539,15 @@ aws ec2 describe-instances --instance-ids $INSTANCE_ID
On success no data will be returned, since the instance is no longer available.
## UI Management Panel
The Hive Platform also offers a web interface for managing hive services. Simply have the hive server running and go to [https://localhost:3000](https://localhost:3000) in your browser to continue.
**Note:** If you are not viewing the website on the same machine that is running the hive server, you will need to go to [https://localhost:9999](https://localhost:9999) and [https://localhost:8443](https://localhost:8443) and accept the certificates. If you have added the CA to your local machine this is not needed.
# Multi-node Configuration
To create a simulate multi-node installation on a single server (e.g node1, node2, node3) review the script `./scripts/create-multi-node.sh`.
To create a simulated multi-node installation on a single server (e.g node1, node2, node3) review the script `./scripts/create-multi-node.sh`.
Specifically this will create 3 IPs on a specified ethernet adapter.
@@ -560,14 +562,15 @@ Next, `hive` will be installed and initiated in `$HOME/node1` for configuration
```bash
# Initialize node1
./bin/hive admin init \
--region ap-southeast-2 \
--az ap-southeast-2a \
--node node1 \
--bind 10.11.12.1 \
--cluster-bind 10.11.12.1 \
--cluster-routes 10.11.12.1:4248 \
--port 4432 \
--hive-dir ~/node1/ \
--config-dir ~/node1/config/
--config-dir ~/node1/config/ \
--region ap-southeast-2 \
--az ap-southeast-2a \
```
Two other nodes will be created, joining node1 to exchange configuration files and identify as a new node.
@@ -575,8 +578,6 @@ Two other nodes will be created, joining node1 to exchange configuration files a
```bash
# Join cluster
./bin/hive admin join \
--region ap-southeast-2 \
--az ap-southeast-2a \
--node node2 \
--bind 10.11.12.2 \
--cluster-bind 10.11.12.2 \
@@ -584,6 +585,8 @@ Two other nodes will be created, joining node1 to exchange configuration files a
--host 10.11.12.1:4432 \
--data-dir ~/node2/ \
--config-dir ~/node2/config/ \
--region ap-southeast-2 \
--az ap-southeast-2a \
# Start node2
./scripts/start-dev.sh ~/node2/
@@ -591,8 +594,6 @@ Two other nodes will be created, joining node1 to exchange configuration files a
echo "Node3 Setup:"
./bin/hive admin join \
--region ap-southeast-2 \
--az ap-southeast-2a \
--node node3 \
--bind 10.11.12.3 \
--cluster-bind 10.11.12.3 \
@@ -600,11 +601,13 @@ echo "Node3 Setup:"
--host 10.11.12.1:4432 \
--data-dir ~/node3/ \
--config-dir ~/node3/config/
--region ap-southeast-2 \
--az ap-southeast-2a \
./scripts/start-dev.sh ~/node3/
```
Once configured, each node will have it's own storage for config, data and state in `~/node[1,2,3]` to simulate a unique node and network.
Once configured, each node will have its own storage for config, data and state in `~/node[1,2,3]` to simulate a unique node and network.
Once the multi-node is configured, follow the original installation instructions above to:
@@ -614,33 +617,32 @@ Once the multi-node is configured, follow the original installation instructions
Note - when using the AWS CLI tool you must specify the IP address of the node (10.11.12.1, 10.11.12.2, or 10.11.12.3).
**Important**: For multi-node setups, the server certificates need to include the additional IP addresses. You may need to regenerate certificates with the `--force` flag after configuring multi-node, or ensure the CA certificate is trusted system-wide (see "Trust CA Certificate" section above).
```bash
aws --endpoint-url https://10.11.12.3:9999/ ec2 describe-instances --instance-ids i-9f5f648adc57ea46d
aws --endpoint-url https://10.11.12.3:9999 ec2 describe-instances --instance-ids i-9f5f648adc57ea46d
```
```json
{
"Reservations": [
"Reservations": [
{
"ReservationId": "r-9d26866c3b0d53bf4",
"OwnerId": "123456789012",
"Instances": [
{
"ReservationId": "r-9d26866c3b0d53bf4",
"OwnerId": "123456789012",
"Instances": [
{
"InstanceId": "i-9f5f648adc57ea46d",
"ImageId": "ami-d0de73dd18fa33ac9",
"State": {
"Code": 16,
"Name": "running"
},
"KeyName": "hive-key",
"InstanceType": "t3.micro",
"LaunchTime": "2025-11-21T10:36:42.834000+00:00"
}
]
"InstanceId": "i-9f5f648adc57ea46d",
"ImageId": "ami-d0de73dd18fa33ac9",
"State": {
"Code": 16,
"Name": "running"
},
"KeyName": "hive-key",
"InstanceType": "t3.micro",
"LaunchTime": "2025-11-21T10:36:42.834000+00:00"
}
]
]
}
]
}
```
For debugging, reference the node unique configuration files, e.g
@@ -666,4 +668,4 @@ To stop a simulated multi-node instance:
./scripts/stop-multi-node.sh
```
Note if multiple EC2 instances are running, it make take a few minutes to gracefully terminate the instance, unmount the attached EBS volume (via NBD) and push the write-ahead-log (WAL) to the S3 server (predastore).
Note if multiple EC2 instances are running, it may take a few minutes to gracefully terminate the instance, unmount the attached EBS volume (via NBD) and push the write-ahead-log (WAL) to the S3 server (predastore).
-4
View File
@@ -43,10 +43,6 @@ build-ui:
# GO commands
go_build:
@echo "\n....Building $(GO_PROJECT_NAME)"
@if [ ! -f hive/services/hiveui/frontend/dist/index.html ]; then \
mkdir -p hive/services/hiveui/frontend/dist && \
echo '<!DOCTYPE html><html><head><title>Hive UI</title></head><body><h1>Hive UI</h1><p>Frontend not built. Run <code>make build-ui</code></p></body></html>' > hive/services/hiveui/frontend/dist/index.html; \
fi
GOEXPERIMENT=greenteagc go build $(GO_BUILD_MOD) -ldflags "-s -w" -o ./bin/$(GO_PROJECT_NAME) cmd/hive/main.go
go_run:
+5
View File
@@ -24,6 +24,11 @@
- Confirm cloud-init will resize volume on boot and configured correctly.
- Attach additional volumes
- Attach to VPC / Security group (required Open vSwitch implementation)
- only allow hive to use x% of cpu and memory. dont allow allocating 100% of resources.
- placement group instances, allow spreading instance group between nodes.
- capabilities flag
- more instance types - per cpu gen
- multi part uploads on frontend
## EC2 describe-instances
+3 -2
View File
@@ -63,8 +63,9 @@ var awsgwCmd = &cobra.Command{
}
var hiveUICmd = &cobra.Command{
Use: "hive-ui",
Short: "Manage the hive-ui service",
Use: "hive-ui",
Aliases: []string{"ui", "hiveui"},
Short: "Manage the hive-ui service",
}
var predastoreStartCmd = &cobra.Command{
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/mulgadc/hive
go 1.25.5
go 1.25.6
require (
github.com/NYTimes/gziphandler v1.1.1
+291 -182
View File
@@ -445,9 +445,101 @@ func (d *Daemon) Start() error {
return fmt.Errorf("failed to start cluster manager: %w", err)
}
// Initialize JetStream in background - don't block daemon startup
// This allows the daemon to start accepting requests while waiting for NATS cluster to form
go d.initJetStreamBackground()
// Initialize JetStream for KV state storage (required - no disk fallback)
// Start with 1 replica to allow single-node startup, then upgrade if cluster has more nodes
// Retry with backoff if JetStream is not ready yet
maxRetries := 10
retryDelay := 500 * time.Millisecond
for attempt := 1; attempt <= maxRetries; attempt++ {
d.jsManager, err = NewJetStreamManager(d.natsConn, 1)
if err != nil {
slog.Warn("Failed to init JetStream", "error", err, "attempt", attempt, "maxRetries", maxRetries)
if attempt < maxRetries {
time.Sleep(retryDelay)
retryDelay *= 2 // Exponential backoff
if retryDelay > 5*time.Second {
retryDelay = 5 * time.Second
}
continue
}
return fmt.Errorf("failed to initialize JetStream after %d attempts: %w", maxRetries, err)
}
if err := d.jsManager.InitKVBucket(); err != nil {
slog.Warn("Failed to init KV bucket", "error", err, "attempt", attempt, "maxRetries", maxRetries)
if attempt < maxRetries {
time.Sleep(retryDelay)
retryDelay *= 2
if retryDelay > 5*time.Second {
retryDelay = 5 * time.Second
}
continue
}
return fmt.Errorf("failed to initialize JetStream KV bucket after %d attempts: %w", maxRetries, err)
}
// Success
slog.Info("JetStream KV store initialized successfully", "replicas", 1, "attempts", attempt)
break
}
// Try to upgrade replicas if cluster has more nodes
// This handles the case where this daemon starts after other NATS nodes are already up
clusterSize := len(d.clusterConfig.Nodes)
if clusterSize > 1 {
if err := d.jsManager.UpdateReplicas(clusterSize); err != nil {
slog.Warn("Failed to upgrade JetStream replicas on startup (other NATS nodes may not be ready)", "targetReplicas", clusterSize, "error", err)
}
}
// Load existing state for VMs from JetStream or disk
err = d.LoadState()
if err != nil {
slog.Warn("Failed to load state from disk, continuing with empty state", "error", err)
} else {
slog.Info("Loaded state from disk", "instance count", len(d.Instances.VMS))
//d.mu.Lock()
// Ensure mutex is usable after unmarshalling
d.Instances.Mu = sync.Mutex{}
for i := range d.Instances.VMS {
instance := d.Instances.VMS[i]
instance.EBSRequests.Mu = sync.Mutex{}
instance.QMPClient = &qmp.QMPClient{}
d.Instances.VMS[i] = instance
//d.Instances.VMS[i].EBSRequests.Mu = sync.Mutex{}
// d.Instances.VMS[i].QMPClient.Mu = sync.Mutex{}
if instance.Attributes.StopInstance {
slog.Info("Instance flagged as user initiated stop, skipping", "instance", instance.ID)
} else if instance.Status != "terminated" {
instanceType, ok := d.resourceMgr.instanceTypes[instance.InstanceType]
if ok {
slog.Info("Re-allocating resources for instance", "instanceId", instance.ID, "type", instance.InstanceType)
if err := d.resourceMgr.allocate(instanceType); err != nil {
slog.Error("Failed to re-allocate resources for instance on startup", "instanceId", instance.ID, "err", err)
}
}
slog.Info("Launching instance", "instance", instance.ID)
err = d.LaunchInstance(instance)
if err != nil {
slog.Error("Failed to launch instance:", "err", err)
}
} else {
slog.Info("Instance state is terminated, skipping", "instance", instance.ID)
}
}
//d.mu.Unlock()
// Launch running instances
}
// Create instance service for handling EC2 instance operations
d.instanceService = handlers_ec2_instance.NewInstanceServiceImpl(d.config, d.resourceMgr.instanceTypes, d.natsConn, &d.Instances)
@@ -801,16 +893,10 @@ func (d *Daemon) ClusterManager() error {
// WriteState writes the instance state to JetStream KV store (required)
func (d *Daemon) WriteState() error {
d.mu.Lock()
jsm := d.jsManager
d.mu.Unlock()
if jsm == nil {
// JetStream not ready yet - this is expected during startup
slog.Debug("WriteState skipped - JetStream not initialized yet")
return nil
if d.jsManager == nil {
return fmt.Errorf("JetStream manager not initialized - cannot write state")
}
if err := jsm.WriteState(d.node, &d.Instances); err != nil {
if err := d.jsManager.WriteState(d.node, &d.Instances); err != nil {
slog.Error("JetStream write failed", "error", err)
return fmt.Errorf("failed to write state to JetStream: %w", err)
}
@@ -845,121 +931,21 @@ func (d *Daemon) InitaliseVMs() {
// LoadState loads the instance state from JetStream KV store (required)
func (d *Daemon) LoadState() error {
d.mu.Lock()
jsm := d.jsManager
d.mu.Unlock()
if jsm == nil {
if d.jsManager == nil {
return fmt.Errorf("JetStream manager not initialized - cannot load state")
}
instances, err := jsm.LoadState(d.node)
instances, err := d.jsManager.LoadState(d.node)
if err != nil {
slog.Error("JetStream load failed", "error", err)
return fmt.Errorf("failed to load state from JetStream: %w", err)
}
// Copy only the VMS map, not the mutex
d.Instances.Mu.Lock()
d.Instances.VMS = instances.VMS
d.Instances.Mu.Unlock()
return nil
}
// initJetStreamBackground initializes JetStream in a background goroutine.
// This allows the daemon to start and accept cluster join requests while waiting
// for the NATS cluster to form and JetStream to become available.
func (d *Daemon) initJetStreamBackground() {
retryDelay := 500 * time.Millisecond
const maxDelay = 10 * time.Second
for attempt := 1; ; attempt++ {
// Check for shutdown
select {
case <-d.ctx.Done():
slog.Info("JetStream init cancelled due to shutdown")
return
default:
}
// Try to create JetStream manager and init KV bucket
jsm, err := NewJetStreamManager(d.natsConn, 1)
if err != nil {
slog.Warn("Failed to init JetStream (will retry)", "error", err, "attempt", attempt)
} else if err := jsm.InitKVBucket(); err != nil {
slog.Warn("Failed to init KV bucket (will retry)", "error", err, "attempt", attempt)
} else {
// Success - store the manager
d.mu.Lock()
d.jsManager = jsm
d.mu.Unlock()
slog.Info("JetStream KV store initialized successfully", "replicas", 1, "attempts", attempt)
break
}
// Wait before retry (with shutdown check)
select {
case <-d.ctx.Done():
slog.Info("JetStream init cancelled due to shutdown")
return
case <-time.After(retryDelay):
}
// Exponential backoff
retryDelay *= 2
if retryDelay > maxDelay {
retryDelay = maxDelay
}
}
// Note: Replica upgrades are handled by the /join endpoint on the leader node.
// Non-leader nodes should not try to update replicas to avoid race conditions.
// Load state and restore VMs
if err := d.LoadState(); err != nil {
slog.Warn("Failed to load state from JetStream, continuing with empty state", "error", err)
slog.Info("JetStream background initialization complete")
return
}
slog.Info("Loaded state from JetStream", "instance_count", len(d.Instances.VMS))
// Restore VMs from loaded state
d.Instances.Mu.Lock()
defer d.Instances.Mu.Unlock()
for id, instance := range d.Instances.VMS {
instance.EBSRequests.Mu = sync.Mutex{}
instance.QMPClient = &qmp.QMPClient{}
d.Instances.VMS[id] = instance
if instance.Attributes.StopInstance {
slog.Info("Instance flagged as user initiated stop, skipping", "instance", instance.ID)
continue
}
if instance.Status == "terminated" {
slog.Info("Instance state is terminated, skipping", "instance", instance.ID)
continue
}
// Re-allocate resources
if instanceType, ok := d.resourceMgr.instanceTypes[instance.InstanceType]; ok {
slog.Info("Re-allocating resources for instance", "instanceId", instance.ID, "type", instance.InstanceType)
if err := d.resourceMgr.allocate(instanceType); err != nil {
slog.Error("Failed to re-allocate resources for instance on startup", "instanceId", instance.ID, "err", err)
}
}
slog.Info("Launching instance", "instance", instance.ID)
if err := d.LaunchInstance(instance); err != nil {
slog.Error("Failed to launch instance", "err", err)
}
}
slog.Info("JetStream background initialization complete")
}
// NATS events
func (d *Daemon) handleEC2StartInstances(msg *nats.Msg) {
@@ -1091,7 +1077,6 @@ func (d *Daemon) SendQMPCommand(q *qmp.QMPClient, cmd qmp.QMPCommand, instanceId
// handleEC2Events processes incoming EC2 events (start, stop, terminate)
func (d *Daemon) handleEC2Events(msg *nats.Msg) {
slog.Info("handleEC2Events called", "subject", msg.Subject)
var command qmp.Command
var resp *qmp.QMPResponse
@@ -1108,7 +1093,8 @@ func (d *Daemon) handleEC2Events(msg *nats.Msg) {
return
}
slog.Info("handleEC2Events received command", "subject", msg.Subject, "instanceId", command.ID, "start", command.Attributes.StartInstance, "stop", command.Attributes.StopInstance, "terminate", command.Attributes.TerminateInstance)
log.Printf("Received message on subject: %s", msg.Subject)
log.Printf("Message data: %s", string(msg.Data))
d.Instances.Mu.Lock()
instance, ok := d.Instances.VMS[command.ID]
@@ -1309,85 +1295,166 @@ func (d *Daemon) handleEC2RunInstances(msg *nats.Msg) {
return
}
// Check if we have enough resources
if !d.resourceMgr.canAllocate(instanceType) {
slog.Error("handleEC2RunInstances canAllocate", "err", awserrors.ErrorInsufficientInstanceCapacity, "InstanceType", *runInstancesInput.InstanceType)
// Determine how many instances to launch based on MinCount/MaxCount
minCount := int(*runInstancesInput.MinCount)
maxCount := int(*runInstancesInput.MaxCount)
// Check how many we can actually launch
allocatableCount := d.resourceMgr.canAllocate(instanceType, maxCount)
if allocatableCount < minCount {
// Cannot satisfy MinCount requirement - fail entirely
slog.Error("handleEC2RunInstances insufficient capacity", "requested", minCount, "available", allocatableCount, "InstanceType", *runInstancesInput.InstanceType)
errResp = utils.GenerateErrorPayload(awserrors.ErrorInsufficientInstanceCapacity)
msg.Respond(errResp)
return
}
// Allocate resources
if err := d.resourceMgr.allocate(instanceType); err != nil {
slog.Error("handleEC2RunInstances allocate", "err", awserrors.ErrorInsufficientInstanceCapacity, "InstanceType", *runInstancesInput.InstanceType)
// Launch up to MaxCount, capped by available capacity
// Note: canAllocate() already caps at maxCount, so allocatableCount <= maxCount
launchCount := allocatableCount
slog.Info("Instance count determined", "min", minCount, "max", maxCount, "launching", launchCount)
// Allocate resources for all instances upfront
var allocatedCount int
for i := 0; i < launchCount; i++ {
if err := d.resourceMgr.allocate(instanceType); err != nil {
slog.Error("handleEC2RunInstances allocate failed mid-allocation", "allocated", allocatedCount, "err", err)
break
}
allocatedCount++
}
// Check if we still meet MinCount after allocation
if allocatedCount < minCount {
// Rollback allocations
for i := 0; i < allocatedCount; i++ {
d.resourceMgr.deallocate(instanceType)
}
slog.Error("handleEC2RunInstances insufficient capacity after allocation", "allocated", allocatedCount, "minCount", minCount)
errResp = utils.GenerateErrorPayload(awserrors.ErrorInsufficientInstanceCapacity)
msg.Respond(errResp)
return
}
// Update launchCount to what we actually allocated
launchCount = allocatedCount
// Delegate to service for business logic (volume creation, cloud-init, etc.)
instanceTypeName := ""
if instanceType.InstanceType != nil {
instanceTypeName = *instanceType.InstanceType
}
slog.Info("Launching EC2 instance", "instanceType", instanceTypeName)
slog.Info("Launching EC2 instances", "instanceType", instanceTypeName, "count", launchCount)
instance, reservation, err := d.instanceService.RunInstances(runInstancesInput)
// Create all instances
var instances []*vm.VM
var allEC2Instances []*ec2.Instance
if err != nil {
slog.Error("handleEC2RunInstances service.RunInstances failed", "err", err)
for i := 0; i < launchCount; i++ {
instance, ec2Instance, err := d.instanceService.RunInstance(runInstancesInput)
if err != nil {
slog.Error("handleEC2RunInstances service.RunInstance failed", "index", i, "err", err)
// Deallocate this instance's resources
d.resourceMgr.deallocate(instanceType)
continue
}
instances = append(instances, instance)
allEC2Instances = append(allEC2Instances, ec2Instance)
}
// Check if we still have enough instances after creation errors
if len(instances) < minCount {
// Rollback: deallocate resources for successfully created instances
// (failed instances already deallocated their resources above)
for range instances {
d.resourceMgr.deallocate(instanceType)
}
slog.Error("handleEC2RunInstances failed to create minimum instances", "created", len(instances), "minCount", minCount)
errResp = utils.GenerateErrorPayload(awserrors.ErrorServerInternal)
msg.Respond(errResp)
d.resourceMgr.deallocate(instanceType)
return
}
// Respond to NATS immediately with reservation (instance is provisioning)
// Build reservation with all instances
reservation := ec2.Reservation{}
reservation.SetReservationId(vm.GenerateEC2ReservationID())
reservation.SetOwnerId("123456789012") // TODO: Use actual owner ID from config
reservation.Instances = allEC2Instances
// Store reservation reference in all VMs
for _, instance := range instances {
instance.Reservation = &reservation
}
// Respond to NATS immediately with reservation (instances are provisioning)
jsonResponse, err := json.Marshal(reservation)
if err != nil {
slog.Error("handleEC2RunInstances failed to marshal reservation", "err", err)
errResp = utils.GenerateErrorPayload(awserrors.ErrorServerInternal)
msg.Respond(errResp)
d.resourceMgr.deallocate(instanceType)
// Deallocate all resources
for range instances {
d.resourceMgr.deallocate(instanceType)
}
return
}
msg.Respond(jsonResponse)
// Add instance to state immediately so DescribeInstances can find it
// while volumes are being prepared and VM is launching
// Add all instances to state immediately so DescribeInstances can find them
// while volumes are being prepared and VMs are launching
d.Instances.Mu.Lock()
d.Instances.VMS[instance.ID] = instance
for _, instance := range instances {
d.Instances.VMS[instance.ID] = instance
}
d.Instances.Mu.Unlock()
if err := d.WriteState(); err != nil {
slog.Error("handleEC2RunInstances failed to write initial state", "err", err)
}
slog.Info("Instance added to state with pending status", "instanceId", instance.ID)
slog.Info("Instances added to state with pending status", "count", len(instances))
// Next, prepare the root volume, cloud-init, EFI drives via NBD (AMI clone to new volume)
err = d.instanceService.GenerateVolumes(runInstancesInput, instance)
if err != nil {
slog.Error("handleEC2RunInstances GenerateVolumes failed", "err", err)
d.resourceMgr.deallocate(instanceType)
// Update instance status to failed
d.markInstanceFailed(instance, "volume_preparation_failed")
return
// Launch all instances (volumes and VMs)
var successCount int
for _, instance := range instances {
// Prepare the root volume, cloud-init, EFI drives via NBD (AMI clone to new volume)
volumeInfos, err := d.instanceService.GenerateVolumes(runInstancesInput, instance)
if err != nil {
slog.Error("handleEC2RunInstances GenerateVolumes failed", "instanceId", instance.ID, "err", err)
d.resourceMgr.deallocate(instanceType)
d.markInstanceFailed(instance, "volume_preparation_failed")
continue
}
// Populate BlockDeviceMappings on the ec2.Instance
instance.Instance.BlockDeviceMappings = make([]*ec2.InstanceBlockDeviceMapping, 0, len(volumeInfos))
for _, vi := range volumeInfos {
mapping := &ec2.InstanceBlockDeviceMapping{}
mapping.SetDeviceName(vi.DeviceName)
mapping.Ebs = &ec2.EbsInstanceBlockDevice{}
mapping.Ebs.SetVolumeId(vi.VolumeId)
mapping.Ebs.SetAttachTime(vi.AttachTime)
mapping.Ebs.SetDeleteOnTermination(vi.DeleteOnTermination)
mapping.Ebs.SetStatus("attached")
instance.Instance.BlockDeviceMappings = append(instance.Instance.BlockDeviceMappings, mapping)
}
// Launch the instance infrastructure (QEMU, QMP, NATS subscriptions)
err = d.LaunchInstance(instance)
if err != nil {
slog.Error("handleEC2RunInstances LaunchInstance failed", "instanceId", instance.ID, "err", err)
d.resourceMgr.deallocate(instanceType)
d.markInstanceFailed(instance, "launch_failed")
continue
}
successCount++
slog.Info("handleEC2RunInstances launched instance", "instanceId", instance.ID)
}
// Launch the instance infrastructure (QEMU, QMP, NATS subscriptions), this can take sometime
err = d.LaunchInstance(instance)
if err != nil {
slog.Error("handleEC2RunInstances LaunchInstance failed", "err", err)
d.resourceMgr.deallocate(instanceType)
// Update instance status to failed
d.markInstanceFailed(instance, "launch_failed")
return
}
slog.Info("handleEC2RunInstances launched", "instanceId", reservation.Instances[0].InstanceId)
slog.Info("handleEC2RunInstances completed", "requested", launchCount, "created", len(instances), "launched", successCount)
}
// handleEC2CreateKeyPair processes incoming EC2 CreateKeyPair requests
@@ -1573,12 +1640,10 @@ func (d *Daemon) stopInstance(instances map[string]*vm.VM, deleteVolume bool) er
// For stop operations, keep the subscription so we can receive start commands
if deleteVolume {
for _, instance := range instances {
subKey := fmt.Sprintf("ec2.cmd.%s", instance.ID)
if sub, ok := d.natsSubscriptions[subKey]; ok && sub != nil {
slog.Info("Unsubscribing from NATS subject", "instance", instance.ID, "subject", subKey)
sub.Unsubscribe()
delete(d.natsSubscriptions, subKey)
}
slog.Info("Unsubscribing from NATS subject", "instance", instance.ID)
d.natsSubscriptions[fmt.Sprintf("ec2.cmd.%s", instance.ID)].Unsubscribe()
// TODO: Remove redundant subscription if not used
//d.natsSubscriptions[fmt.Sprintf("ec2.describe.%s", instance.ID)].Unsubscribe()
}
}
return nil
@@ -1737,16 +1802,11 @@ func (d *Daemon) LaunchInstance(instance *vm.VM) (err error) {
d.mu.Lock()
defer d.mu.Unlock()
// Check if subscription already exists (e.g., when restarting a stopped instance)
subKey := fmt.Sprintf("ec2.cmd.%s", instance.ID)
if existingSub, ok := d.natsSubscriptions[subKey]; ok && existingSub != nil && existingSub.IsValid() {
slog.Info("NATS subscription already exists for instance", "instance", instance.ID)
} else {
d.natsSubscriptions[subKey], err = d.natsConn.QueueSubscribe(subKey, "hive-events", d.handleEC2Events)
if err != nil {
slog.Error("failed to subscribe to NATS", "err", err)
return err
}
d.natsSubscriptions[instance.ID], err = d.natsConn.QueueSubscribe(fmt.Sprintf("ec2.cmd.%s", instance.ID), "hive-events", d.handleEC2Events)
if err != nil {
slog.Error("failed to subscribe to NATS", "err", err)
return err
}
// TODO: Replaced with describe-instances with Inbox subscription
@@ -2133,8 +2193,9 @@ func (d *Daemon) MountVolumes(instance *vm.VM) error {
}
// canAllocate checks if there are enough resources available
func (rm *ResourceManager) canAllocate(instanceType *ec2.InstanceTypeInfo) bool {
// canAllocate checks how many instances of the given type can be allocated
// Returns the count that can actually be allocated (0 to count)
func (rm *ResourceManager) canAllocate(instanceType *ec2.InstanceTypeInfo, count int) int {
rm.mu.RLock()
defer rm.mu.RUnlock()
@@ -2147,14 +2208,43 @@ func (rm *ResourceManager) canAllocate(instanceType *ec2.InstanceTypeInfo) bool
memoryGB = float64(*instanceType.MemoryInfo.SizeInMiB) / 1024.0
}
return rm.availableVCPU-rm.allocatedVCPU >= int(vCPUs) &&
rm.availableMem-rm.allocatedMem >= memoryGB
availableVCPU := rm.availableVCPU - rm.allocatedVCPU
availableMem := rm.availableMem - rm.allocatedMem
// Calculate how many instances we can fit based on CPU and memory
countByCPU := count
if vCPUs > 0 {
countByCPU = availableVCPU / int(vCPUs)
}
countByMem := count
if memoryGB > 0 {
countByMem = int(availableMem / memoryGB)
}
// Take the minimum of CPU-limited and memory-limited counts
allocatableCount := countByCPU
if countByMem < allocatableCount {
allocatableCount = countByMem
}
// Cap at requested count
if allocatableCount > count {
allocatableCount = count
}
// Ensure non-negative
if allocatableCount < 0 {
allocatableCount = 0
}
return allocatableCount
}
// allocate reserves resources for an instance
func (rm *ResourceManager) allocate(instanceType *ec2.InstanceTypeInfo) error {
if !rm.canAllocate(instanceType) {
if rm.canAllocate(instanceType, 1) < 1 {
instanceTypeName := ""
if instanceType.InstanceType != nil {
instanceTypeName = *instanceType.InstanceType
@@ -2441,9 +2531,6 @@ func (d *Daemon) handleEC2DescribeInstances(msg *nats.Msg) {
slog.Info("Processing DescribeInstances request from this node")
// Build response with reservations from instances on this node
var reservations []*ec2.Reservation
d.Instances.Mu.Lock()
defer d.Instances.Mu.Unlock()
@@ -2457,6 +2544,9 @@ func (d *Daemon) handleEC2DescribeInstances(msg *nats.Msg) {
}
}
// Group instances by reservation ID (AWS returns instances grouped by reservation)
reservationMap := make(map[string]*ec2.Reservation)
// Iterate through all instances on this node
for _, instance := range d.Instances.VMS {
// Skip if filtering by instance IDs and this instance is not in the filter
@@ -2466,8 +2556,21 @@ func (d *Daemon) handleEC2DescribeInstances(msg *nats.Msg) {
// Use stored reservation metadata if available
if instance.Reservation != nil && instance.Instance != nil {
// Create a copy of the reservation with updated instance state
reservation := *instance.Reservation
resID := ""
if instance.Reservation.ReservationId != nil {
resID = *instance.Reservation.ReservationId
}
// Create reservation entry if it doesn't exist
if _, exists := reservationMap[resID]; !exists {
reservation := &ec2.Reservation{}
reservation.SetReservationId(resID)
if instance.Reservation.OwnerId != nil {
reservation.SetOwnerId(*instance.Reservation.OwnerId)
}
reservation.Instances = []*ec2.Instance{}
reservationMap[resID] = reservation
}
// Update the instance state to current state
instanceCopy := *instance.Instance
@@ -2498,11 +2601,17 @@ func (d *Daemon) handleEC2DescribeInstances(msg *nats.Msg) {
instanceCopy.State.SetName("pending")
}
reservation.Instances = []*ec2.Instance{&instanceCopy}
reservations = append(reservations, &reservation)
// Add instance to its reservation
reservationMap[resID].Instances = append(reservationMap[resID].Instances, &instanceCopy)
}
}
// Convert map to slice
reservations := make([]*ec2.Reservation, 0, len(reservationMap))
for _, reservation := range reservationMap {
reservations = append(reservations, reservation)
}
// Create the response
output := &ec2.DescribeInstancesOutput{
Reservations: reservations,
+526 -7
View File
@@ -406,8 +406,8 @@ func TestResourceManager(t *testing.T) {
require.True(t, exists, "Should have at least one .micro instance type")
// Check if can allocate
canAlloc := rm.canAllocate(instanceType)
assert.True(t, canAlloc)
canAlloc := rm.canAllocate(instanceType, 1)
assert.Equal(t, 1, canAlloc)
// Allocate
err := rm.allocate(instanceType)
@@ -429,6 +429,42 @@ func TestResourceManager(t *testing.T) {
rm.deallocate(instanceType)
assert.Equal(t, 0, rm.allocatedVCPU)
assert.Equal(t, float64(0), rm.allocatedMem)
// Test canAllocate with count parameter
t.Run("canAllocate_with_count", func(t *testing.T) {
// Fresh resource manager for predictable testing
rm := NewResourceManager()
// Find a .micro instance type
var microType *ec2.InstanceTypeInfo
for key, it := range rm.instanceTypes {
if strings.HasSuffix(key, ".micro") {
microType = it
break
}
}
require.NotNil(t, microType, "Should have a .micro instance type")
// Test requesting more than available
maxPossible := rm.canAllocate(microType, 1000)
assert.Greater(t, maxPossible, 0, "Should be able to allocate at least 1")
assert.LessOrEqual(t, maxPossible, 1000, "Should not exceed requested count")
// Test requesting exactly 1
oneAlloc := rm.canAllocate(microType, 1)
assert.Equal(t, 1, oneAlloc, "Should be able to allocate exactly 1")
// Test with 0 request
zeroAlloc := rm.canAllocate(microType, 0)
assert.Equal(t, 0, zeroAlloc, "Requesting 0 should return 0")
// Test after allocating resources
rm.allocate(microType)
afterOneAlloc := rm.canAllocate(microType, 1000)
assert.Equal(t, maxPossible-1, afterOneAlloc, "Should have 1 less slot available")
rm.deallocate(microType)
})
}
// TestGetInstanceTypeInfos tests the GetInstanceTypeInfos method
@@ -438,7 +474,10 @@ func TestGetInstanceTypeInfos(t *testing.T) {
infos := rm.GetInstanceTypeInfos()
require.NotEmpty(t, infos, "Should return at least one instance type")
assert.Len(t, infos, 14, "Should have 14 instance types (7 sizes x 2 families: CPU-specific + burstable)")
// Number of instance types depends on whether CPU family differs from burstable family
// 7 sizes per family, 1-2 families depending on detected CPU
assert.True(t, len(infos) == 7 || len(infos) == 14,
"Should have 7 or 14 instance types (7 sizes x 1-2 families), got %d", len(infos))
// Verify structure of returned instance type info
for _, info := range infos {
@@ -784,6 +823,17 @@ func TestHandleEC2DescribeInstanceTypes(t *testing.T) {
}
require.NotEmpty(t, family, "Should have a detected instance family")
// Count unique families in resourceMgr
familySet := make(map[string]bool)
for name := range daemon.resourceMgr.instanceTypes {
parts := strings.Split(name, ".")
if len(parts) > 0 {
familySet[parts[0]] = true
}
}
numFamilies := len(familySet)
t.Logf("Detected %d unique instance families", numFamilies)
// Force resources to a predictable state for the first part of the test (2 vCPUs)
daemon.resourceMgr.mu.Lock()
oldAvailableVCPU := daemon.resourceMgr.availableVCPU
@@ -818,8 +868,9 @@ func TestHandleEC2DescribeInstanceTypes(t *testing.T) {
require.NoError(t, err)
// With 2 vCPUs, we expect 1 slot for each 2-vCPU type (nano, micro, small, medium, large)
// and 0 slots for 4+ vCPU types. With 2 families, that's 5 x 2 = 10 entries.
assert.Equal(t, 10, len(output.InstanceTypes), "Should have 10 total slots available with 2 vCPUs (5 sizes x 2 families)")
// and 0 slots for 4+ vCPU types. 5 sizes per family.
expectedSlots := 5 * numFamilies
assert.Equal(t, expectedSlots, len(output.InstanceTypes), "Should have %d total slots available with 2 vCPUs (5 sizes x %d families)", expectedSlots, numFamilies)
// Now let's "fake" more capacity to test multiple slots (duplicates) per type
daemon.resourceMgr.mu.Lock()
@@ -841,8 +892,8 @@ func TestHandleEC2DescribeInstanceTypes(t *testing.T) {
// xlarge (4 cores, 16.0GB): 0 slots (15GB < 16GB)
// 2xlarge (8 cores, 32.0GB): 0 slots
// Total per family: 2+2+2+2+1 = 9 slots
// With 2 families: 9 x 2 = 18 slots
assert.Equal(t, 18, len(output.InstanceTypes), "Should have 18 total slots with 4 cores and 15GB memory (9 slots x 2 families)")
expectedSlots = 9 * numFamilies
assert.Equal(t, expectedSlots, len(output.InstanceTypes), "Should have %d total slots with 4 cores and 15GB memory (9 slots x %d families)", expectedSlots, numFamilies)
// Verify duplicates exist
typeCounts := make(map[string]int)
@@ -1048,3 +1099,471 @@ func createValidRunInstancesInput() *ec2.RunInstancesInput {
UserData: aws.String(""), // Empty UserData to bypass cloud-init requirements
}
}
// TestCanAllocate_CountEdgeCases tests edge cases for canAllocate with count parameter
func TestCanAllocate_CountEdgeCases(t *testing.T) {
t.Run("MinCount_equals_MaxCount", func(t *testing.T) {
rm := NewResourceManager()
var microType *ec2.InstanceTypeInfo
for key, it := range rm.instanceTypes {
if strings.HasSuffix(key, ".micro") {
microType = it
break
}
}
require.NotNil(t, microType)
// When min=max, canAllocate should return exactly that or less
result := rm.canAllocate(microType, 3)
assert.GreaterOrEqual(t, result, 0)
assert.LessOrEqual(t, result, 3)
})
t.Run("Request_exceeds_capacity", func(t *testing.T) {
rm := NewResourceManager()
// Find the largest instance type to exhaust resources faster
var largeType *ec2.InstanceTypeInfo
for key, it := range rm.instanceTypes {
if strings.HasSuffix(key, ".xlarge") {
largeType = it
break
}
}
require.NotNil(t, largeType)
// Request way more than possible
maxPossible := rm.canAllocate(largeType, 10000)
t.Logf("Can allocate %d xlarge instances", maxPossible)
// Should be capped by actual resources, not request
assert.Less(t, maxPossible, 10000)
assert.GreaterOrEqual(t, maxPossible, 0)
})
t.Run("Capacity_decreases_after_allocation", func(t *testing.T) {
rm := NewResourceManager()
var microType *ec2.InstanceTypeInfo
for key, it := range rm.instanceTypes {
if strings.HasSuffix(key, ".micro") {
microType = it
break
}
}
require.NotNil(t, microType)
initial := rm.canAllocate(microType, 100)
t.Logf("Initial capacity: %d micro instances", initial)
// Allocate one
err := rm.allocate(microType)
require.NoError(t, err)
afterOne := rm.canAllocate(microType, 100)
assert.Equal(t, initial-1, afterOne, "Capacity should decrease by 1")
// Allocate another
err = rm.allocate(microType)
require.NoError(t, err)
afterTwo := rm.canAllocate(microType, 100)
assert.Equal(t, initial-2, afterTwo, "Capacity should decrease by 2")
// Deallocate both
rm.deallocate(microType)
rm.deallocate(microType)
restored := rm.canAllocate(microType, 100)
assert.Equal(t, initial, restored, "Capacity should be restored")
})
t.Run("Mixed_instance_types", func(t *testing.T) {
rm := NewResourceManager()
var microType, mediumType *ec2.InstanceTypeInfo
for key, it := range rm.instanceTypes {
if strings.HasSuffix(key, ".micro") {
microType = it
}
if strings.HasSuffix(key, ".medium") {
mediumType = it
}
}
require.NotNil(t, microType)
require.NotNil(t, mediumType)
initialMicro := rm.canAllocate(microType, 100)
initialMedium := rm.canAllocate(mediumType, 100)
// Allocate a medium (uses more resources)
err := rm.allocate(mediumType)
require.NoError(t, err)
// Both capacities should decrease
afterMicro := rm.canAllocate(microType, 100)
afterMedium := rm.canAllocate(mediumType, 100)
assert.Less(t, afterMicro, initialMicro, "Micro capacity should decrease")
assert.Less(t, afterMedium, initialMedium, "Medium capacity should decrease")
rm.deallocate(mediumType)
})
t.Run("Zero_and_negative_counts", func(t *testing.T) {
rm := NewResourceManager()
var microType *ec2.InstanceTypeInfo
for key, it := range rm.instanceTypes {
if strings.HasSuffix(key, ".micro") {
microType = it
break
}
}
require.NotNil(t, microType)
// Zero request should return 0
zeroResult := rm.canAllocate(microType, 0)
assert.Equal(t, 0, zeroResult)
// Negative request (edge case - shouldn't happen but should handle gracefully)
negResult := rm.canAllocate(microType, -1)
assert.GreaterOrEqual(t, negResult, -1) // Implementation dependent
})
}
// TestDescribeInstances_ReservationGrouping tests that instances are grouped by reservation ID
func TestDescribeInstances_ReservationGrouping(t *testing.T) {
ns, natsURL := startTestNATSServer(t)
defer ns.Shutdown()
daemon := createTestDaemon(t, natsURL)
// Create instances with shared reservation (simulating --count 3)
reservation1 := &ec2.Reservation{}
reservation1.SetReservationId("r-shared-001")
reservation1.SetOwnerId("123456789012")
// Add 3 instances with same reservation ID
for i := 1; i <= 3; i++ {
instanceID := fmt.Sprintf("i-group1-%03d", i)
ec2Instance := &ec2.Instance{}
ec2Instance.SetInstanceId(instanceID)
ec2Instance.SetInstanceType("t3.micro")
daemon.Instances.VMS[instanceID] = &vm.VM{
ID: instanceID,
Status: "running",
Reservation: reservation1,
Instance: ec2Instance,
}
}
// Create another reservation with 2 instances
reservation2 := &ec2.Reservation{}
reservation2.SetReservationId("r-shared-002")
reservation2.SetOwnerId("123456789012")
for i := 1; i <= 2; i++ {
instanceID := fmt.Sprintf("i-group2-%03d", i)
ec2Instance := &ec2.Instance{}
ec2Instance.SetInstanceId(instanceID)
ec2Instance.SetInstanceType("t3.small")
daemon.Instances.VMS[instanceID] = &vm.VM{
ID: instanceID,
Status: "running",
Reservation: reservation2,
Instance: ec2Instance,
}
}
// Create a single-instance reservation
reservation3 := &ec2.Reservation{}
reservation3.SetReservationId("r-single-003")
reservation3.SetOwnerId("123456789012")
ec2Instance := &ec2.Instance{}
ec2Instance.SetInstanceId("i-single-001")
ec2Instance.SetInstanceType("t3.large")
daemon.Instances.VMS["i-single-001"] = &vm.VM{
ID: "i-single-001",
Status: "stopped",
Reservation: reservation3,
Instance: ec2Instance,
}
// Subscribe to handle DescribeInstances
sub, err := daemon.natsConn.Subscribe("ec2.DescribeInstances", daemon.handleEC2DescribeInstances)
require.NoError(t, err)
defer sub.Unsubscribe()
t.Run("GroupsInstancesByReservationID", func(t *testing.T) {
input := &ec2.DescribeInstancesInput{}
inputJSON, _ := json.Marshal(input)
resp, err := daemon.natsConn.Request("ec2.DescribeInstances", inputJSON, 5*time.Second)
require.NoError(t, err)
var output ec2.DescribeInstancesOutput
err = json.Unmarshal(resp.Data, &output)
require.NoError(t, err)
// Should have exactly 3 reservations
assert.Len(t, output.Reservations, 3, "Should have 3 reservations")
// Build a map of reservation ID -> instance count
resMap := make(map[string]int)
for _, res := range output.Reservations {
resID := *res.ReservationId
resMap[resID] = len(res.Instances)
t.Logf("Reservation %s has %d instances", resID, len(res.Instances))
}
assert.Equal(t, 3, resMap["r-shared-001"], "r-shared-001 should have 3 instances")
assert.Equal(t, 2, resMap["r-shared-002"], "r-shared-002 should have 2 instances")
assert.Equal(t, 1, resMap["r-single-003"], "r-single-003 should have 1 instance")
})
t.Run("FilterByInstanceID_PreservesReservation", func(t *testing.T) {
// Request only one instance from a multi-instance reservation
input := &ec2.DescribeInstancesInput{
InstanceIds: []*string{aws.String("i-group1-001")},
}
inputJSON, _ := json.Marshal(input)
resp, err := daemon.natsConn.Request("ec2.DescribeInstances", inputJSON, 5*time.Second)
require.NoError(t, err)
var output ec2.DescribeInstancesOutput
err = json.Unmarshal(resp.Data, &output)
require.NoError(t, err)
// Should have 1 reservation with 1 instance
require.Len(t, output.Reservations, 1)
assert.Equal(t, "r-shared-001", *output.Reservations[0].ReservationId)
assert.Len(t, output.Reservations[0].Instances, 1)
assert.Equal(t, "i-group1-001", *output.Reservations[0].Instances[0].InstanceId)
})
t.Run("FilterMultipleInstances_SameReservation", func(t *testing.T) {
// Request 2 instances from the same reservation
input := &ec2.DescribeInstancesInput{
InstanceIds: []*string{
aws.String("i-group1-001"),
aws.String("i-group1-003"),
},
}
inputJSON, _ := json.Marshal(input)
resp, err := daemon.natsConn.Request("ec2.DescribeInstances", inputJSON, 5*time.Second)
require.NoError(t, err)
var output ec2.DescribeInstancesOutput
err = json.Unmarshal(resp.Data, &output)
require.NoError(t, err)
// Should have 1 reservation with 2 instances
require.Len(t, output.Reservations, 1)
assert.Equal(t, "r-shared-001", *output.Reservations[0].ReservationId)
assert.Len(t, output.Reservations[0].Instances, 2)
})
t.Run("FilterMultipleInstances_DifferentReservations", func(t *testing.T) {
// Request instances from different reservations
input := &ec2.DescribeInstancesInput{
InstanceIds: []*string{
aws.String("i-group1-001"),
aws.String("i-group2-001"),
aws.String("i-single-001"),
},
}
inputJSON, _ := json.Marshal(input)
resp, err := daemon.natsConn.Request("ec2.DescribeInstances", inputJSON, 5*time.Second)
require.NoError(t, err)
var output ec2.DescribeInstancesOutput
err = json.Unmarshal(resp.Data, &output)
require.NoError(t, err)
// Should have 3 reservations, each with 1 instance
assert.Len(t, output.Reservations, 3)
for _, res := range output.Reservations {
assert.Len(t, res.Instances, 1, "Each reservation should have 1 instance when filtered")
}
})
t.Run("InstanceStates_AreCorrect", func(t *testing.T) {
input := &ec2.DescribeInstancesInput{}
inputJSON, _ := json.Marshal(input)
resp, err := daemon.natsConn.Request("ec2.DescribeInstances", inputJSON, 5*time.Second)
require.NoError(t, err)
var output ec2.DescribeInstancesOutput
err = json.Unmarshal(resp.Data, &output)
require.NoError(t, err)
// Find the stopped instance and verify its state
for _, res := range output.Reservations {
for _, inst := range res.Instances {
if *inst.InstanceId == "i-single-001" {
assert.Equal(t, int64(80), *inst.State.Code, "Stopped instance should have code 80")
assert.Equal(t, "stopped", *inst.State.Name)
} else {
assert.Equal(t, int64(16), *inst.State.Code, "Running instance should have code 16")
assert.Equal(t, "running", *inst.State.Name)
}
}
}
})
}
// TestRunInstances_CountValidation tests MinCount/MaxCount validation scenarios
func TestRunInstances_CountValidation(t *testing.T) {
ns, natsURL := startTestNATSServer(t)
defer ns.Shutdown()
daemon := createTestDaemon(t, natsURL)
// Subscribe to handle RunInstances
sub, err := daemon.natsConn.Subscribe("ec2.RunInstances", daemon.handleEC2RunInstances)
require.NoError(t, err)
defer sub.Unsubscribe()
t.Run("MinCount_greater_than_MaxCount", func(t *testing.T) {
input := &ec2.RunInstancesInput{
ImageId: aws.String("ami-test"),
InstanceType: aws.String(getTestInstanceType()),
MinCount: aws.Int64(5),
MaxCount: aws.Int64(3), // Invalid: min > max
}
inputJSON, _ := json.Marshal(input)
resp, err := daemon.natsConn.Request("ec2.RunInstances", inputJSON, 5*time.Second)
require.NoError(t, err)
// Should return validation error
var errResp map[string]interface{}
err = json.Unmarshal(resp.Data, &errResp)
require.NoError(t, err)
assert.Contains(t, errResp, "Code", "Should return error response")
t.Logf("Error response: %v", errResp)
})
t.Run("MinCount_zero", func(t *testing.T) {
input := &ec2.RunInstancesInput{
ImageId: aws.String("ami-test"),
InstanceType: aws.String(getTestInstanceType()),
MinCount: aws.Int64(0), // Invalid
MaxCount: aws.Int64(1),
}
inputJSON, _ := json.Marshal(input)
resp, err := daemon.natsConn.Request("ec2.RunInstances", inputJSON, 5*time.Second)
require.NoError(t, err)
var errResp map[string]interface{}
err = json.Unmarshal(resp.Data, &errResp)
require.NoError(t, err)
assert.Contains(t, errResp, "Code")
})
t.Run("MaxCount_zero", func(t *testing.T) {
input := &ec2.RunInstancesInput{
ImageId: aws.String("ami-test"),
InstanceType: aws.String(getTestInstanceType()),
MinCount: aws.Int64(1),
MaxCount: aws.Int64(0), // Invalid
}
inputJSON, _ := json.Marshal(input)
resp, err := daemon.natsConn.Request("ec2.RunInstances", inputJSON, 5*time.Second)
require.NoError(t, err)
var errResp map[string]interface{}
err = json.Unmarshal(resp.Data, &errResp)
require.NoError(t, err)
assert.Contains(t, errResp, "Code")
})
t.Run("InsufficientCapacity_for_MinCount", func(t *testing.T) {
// Request more instances than could possibly fit
input := &ec2.RunInstancesInput{
ImageId: aws.String("ami-test"),
InstanceType: aws.String(getTestInstanceType()),
MinCount: aws.Int64(10000), // Way more than available
MaxCount: aws.Int64(10000),
}
inputJSON, _ := json.Marshal(input)
resp, err := daemon.natsConn.Request("ec2.RunInstances", inputJSON, 5*time.Second)
require.NoError(t, err)
var errResp map[string]interface{}
err = json.Unmarshal(resp.Data, &errResp)
require.NoError(t, err)
assert.Equal(t, "InsufficientInstanceCapacity", errResp["Code"])
t.Logf("Got expected error: %v", errResp["Code"])
})
}
// TestResourceManager_ConcurrentAccess tests thread safety of resource manager
func TestResourceManager_ConcurrentAccess(t *testing.T) {
rm := NewResourceManager()
var microType *ec2.InstanceTypeInfo
for key, it := range rm.instanceTypes {
if strings.HasSuffix(key, ".micro") {
microType = it
break
}
}
require.NotNil(t, microType)
// Run concurrent allocations and deallocations
done := make(chan bool)
iterations := 100
// Goroutine 1: Allocate and deallocate
go func() {
for i := 0; i < iterations; i++ {
if rm.canAllocate(microType, 1) >= 1 {
rm.allocate(microType)
rm.deallocate(microType)
}
}
done <- true
}()
// Goroutine 2: Check capacity
go func() {
for i := 0; i < iterations; i++ {
_ = rm.canAllocate(microType, 10)
}
done <- true
}()
// Goroutine 3: Allocate and deallocate
go func() {
for i := 0; i < iterations; i++ {
if rm.canAllocate(microType, 1) >= 1 {
rm.allocate(microType)
rm.deallocate(microType)
}
}
done <- true
}()
// Wait for all goroutines
<-done
<-done
<-done
// Final state should be clean (no allocations)
assert.Equal(t, 0, rm.allocatedVCPU, "All resources should be deallocated")
assert.Equal(t, float64(0), rm.allocatedMem, "All memory should be deallocated")
}
+66 -36
View File
@@ -75,6 +75,15 @@ type CloudInitMetaData struct {
Hostname string
}
// VolumeInfo holds volume information returned from GenerateVolumes
// for populating BlockDeviceMappings in the EC2 API response
type VolumeInfo struct {
VolumeId string
DeviceName string
AttachTime time.Time
DeleteOnTermination bool
}
// InstanceServiceImpl handles daemon-side EC2 instance operations
type InstanceServiceImpl struct {
config *config.Config
@@ -93,18 +102,13 @@ func NewInstanceServiceImpl(cfg *config.Config, instanceTypes map[string]*ec2.In
}
}
// RunInstances handles the business logic for launching EC2 instances
// This prepares all volumes (root, EFI, cloud-init) and returns the instance ready to launch
func (s *InstanceServiceImpl) RunInstances(input *ec2.RunInstancesInput) (*vm.VM, ec2.Reservation, error) {
var reservation ec2.Reservation
// Validate input (validation is already done in daemon.handleEC2Launch)
// We can skip re-validation here for performance
// RunInstance creates a single EC2 instance (called per-instance by daemon)
// Returns the VM struct and EC2 instance metadata
func (s *InstanceServiceImpl) RunInstance(input *ec2.RunInstancesInput) (*vm.VM, *ec2.Instance, error) {
// Validate instance type exists
_, exists := s.instanceTypes[*input.InstanceType]
if !exists {
return nil, reservation, errors.New(awserrors.ErrorInvalidInstanceType)
return nil, nil, errors.New(awserrors.ErrorInvalidInstanceType)
}
instanceId := vm.GenerateEC2InstanceID()
@@ -116,12 +120,7 @@ func (s *InstanceServiceImpl) RunInstances(input *ec2.RunInstancesInput) (*vm.VM
InstanceType: *input.InstanceType,
}
// Create reservation response
reservation.SetReservationId(vm.GenerateEC2ReservationID())
reservation.SetOwnerId("123456789012") // TODO: Use actual owner ID from config
// TODO: Loop through multiple instance creation based on MinCount / MaxCount
reservation.Instances = make([]*ec2.Instance, 1)
// Create EC2 instance metadata
ec2Instance := &ec2.Instance{
State: &ec2.InstanceState{},
}
@@ -135,40 +134,50 @@ func (s *InstanceServiceImpl) RunInstances(input *ec2.RunInstancesInput) (*vm.VM
ec2Instance.State.SetCode(0)
ec2Instance.State.SetName("pending")
reservation.Instances[0] = ec2Instance
// Store EC2 API metadata in VM for DescribeInstances compatibility
instance.RunInstancesInput = input
instance.Reservation = &reservation
instance.Instance = ec2Instance
// Return instance attributes, defer disk preparation to later step
return instance, reservation, nil
return instance, ec2Instance, nil
}
func (s *InstanceServiceImpl) GenerateVolumes(input *ec2.RunInstancesInput, instance *vm.VM) (err error) {
func (s *InstanceServiceImpl) GenerateVolumes(input *ec2.RunInstancesInput, instance *vm.VM) ([]VolumeInfo, error) {
var size int = 4 * 1024 * 1024 * 1024 // 4GB default size
var deviceName string
var deviceName = "/dev/xvda" // Default device name
var volumeType string
var iops int
var imageId string
var snapshotId string
var deleteOnTermination = true // Default to true
// Handle block device mappings
if len(input.BlockDeviceMappings) > 0 {
size = int(*input.BlockDeviceMappings[0].Ebs.VolumeSize)
deviceName = *input.BlockDeviceMappings[0].DeviceName
volumeType = *input.BlockDeviceMappings[0].Ebs.VolumeType
iops = int(*input.BlockDeviceMappings[0].Ebs.Iops)
bdm := input.BlockDeviceMappings[0]
if bdm.DeviceName != nil {
deviceName = *bdm.DeviceName
}
if bdm.Ebs != nil {
if bdm.Ebs.VolumeSize != nil {
size = int(*bdm.Ebs.VolumeSize)
}
if bdm.Ebs.VolumeType != nil {
volumeType = *bdm.Ebs.VolumeType
}
if bdm.Ebs.Iops != nil {
iops = int(*bdm.Ebs.Iops)
}
if bdm.Ebs.DeleteOnTermination != nil {
deleteOnTermination = *bdm.Ebs.DeleteOnTermination
}
}
}
// Determine image ID and snapshot ID
if strings.HasPrefix(*input.ImageId, "ami-") {
randomNumber, err := rand.Int(rand.Reader, big.NewInt(100_000_000))
if err != nil {
return err
return nil, err
}
imageId = viperblock.GenerateVolumeID("vol", fmt.Sprintf("%d-%s", randomNumber, *input.ImageId), "predastore", time.Now().Unix())
snapshotId = *input.ImageId
@@ -176,11 +185,14 @@ func (s *InstanceServiceImpl) GenerateVolumes(input *ec2.RunInstancesInput, inst
imageId = *input.ImageId
}
// Capture attach time for the root volume
attachTime := time.Now()
volumeConfig := viperblock.VolumeConfig{
VolumeMetadata: viperblock.VolumeMetadata{
VolumeID: imageId,
SizeGiB: utils.SafeIntToUint64(size / 1024 / 1024 / 1024),
CreatedAt: time.Now(),
CreatedAt: attachTime,
DeviceName: deviceName,
VolumeType: volumeType,
IOPS: iops,
@@ -189,26 +201,36 @@ func (s *InstanceServiceImpl) GenerateVolumes(input *ec2.RunInstancesInput, inst
}
// Step 1: Create or validate root volume
err = s.prepareRootVolume(input, imageId, size, volumeConfig, instance)
err := s.prepareRootVolume(input, imageId, size, volumeConfig, instance)
if err != nil {
return err
return nil, err
}
// Step 2: Create EFI partition
err = s.prepareEFIVolume(imageId, volumeConfig, instance)
if err != nil {
return err
return nil, err
}
// Step 3: Create cloud-init volume if needed
if input.KeyName != nil && *input.KeyName != "" || (input.UserData != nil && *input.UserData != "") {
err = s.prepareCloudInitVolume(input, imageId, volumeConfig, instance)
if err != nil {
return err
return nil, err
}
}
return nil
// Return volume info for the root volume only (EFI and cloud-init are internal)
volumeInfos := []VolumeInfo{
{
VolumeId: imageId,
DeviceName: deviceName,
AttachTime: attachTime,
DeleteOnTermination: deleteOnTermination,
},
}
return volumeInfos, nil
}
@@ -402,12 +424,16 @@ func (s *InstanceServiceImpl) prepareEFIVolume(imageId string, volumeConfig vipe
Host: s.config.Predastore.Host,
}
// Update VolumeID to match the EFI volume name
efiVolumeConfig := volumeConfig
efiVolumeConfig.VolumeMetadata.VolumeID = efiVolumeName
efiVbConfig := viperblock.VB{
VolumeName: efiVolumeName,
VolumeSize: utils.SafeIntToUint64(efiSize),
BaseDir: s.config.WalDir,
Cache: viperblock.Cache{Config: viperblock.CacheConfig{Size: 0}},
VolumeConfig: volumeConfig,
VolumeConfig: efiVolumeConfig,
}
efiVb, err := viperblock.New(&efiVbConfig, "s3", efiCfg)
@@ -493,12 +519,16 @@ func (s *InstanceServiceImpl) prepareCloudInitVolume(input *ec2.RunInstancesInpu
Host: s.config.Predastore.Host,
}
// Update VolumeID to match the cloud-init volume name
cloudInitVolumeConfig := volumeConfig
cloudInitVolumeConfig.VolumeMetadata.VolumeID = cloudInitVolumeName
cloudInitVbConfig := viperblock.VB{
VolumeName: cloudInitVolumeName,
VolumeSize: utils.SafeIntToUint64(cloudInitSize),
BaseDir: s.config.WalDir,
Cache: viperblock.Cache{Config: viperblock.CacheConfig{Size: 0}},
VolumeConfig: volumeConfig,
VolumeConfig: cloudInitVolumeConfig,
}
cloudInitVb, err := viperblock.New(&cloudInitVbConfig, "s3", cloudInitCfg)
+3 -2
View File
@@ -2,6 +2,7 @@ package handlers_ec2_instance
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"time"
@@ -40,9 +41,9 @@ func (s *NATSInstanceService) RunInstances(input *ec2.RunInstancesInput) (*ec2.R
// Check if the response is an error
responseError, err := utils.ValidateErrorPayload(msg.Data)
if err != nil {
// Response is an error payload
// Response is an error payload - return just the error code for gateway lookup
slog.Error("NATSInstanceService: Received error response from daemon", "code", responseError.Code)
return nil, fmt.Errorf("daemon returned error: %s", *responseError.Code)
return nil, errors.New(*responseError.Code)
}
// Unmarshal successful response
@@ -172,10 +172,10 @@ func TestNATSInstanceService_RunInstances_DaemonError(t *testing.T) {
// Call RunInstances
reservation, err := service.RunInstances(input)
// Verify error handling
// Verify error handling - error should be just the AWS error code for gateway lookup
require.Error(t, err, "Should return error when daemon returns error")
assert.Nil(t, reservation, "Reservation should be nil on error")
assert.Contains(t, err.Error(), "daemon returned error", "Error should indicate daemon error")
assert.Equal(t, "InvalidInstanceType", err.Error(), "Error should be the AWS error code")
}
// TestNATSInstanceService_RunInstances_Timeout tests timeout handling
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"github.com/aws/aws-sdk-go/service/s3"
"github.com/mulgadc/hive/hive/awserrors"
"github.com/mulgadc/hive/hive/config"
"github.com/mulgadc/hive/hive/utils"
"github.com/mulgadc/viperblock/viperblock"
"golang.org/x/net/http2"
)
@@ -207,7 +208,7 @@ func (s *VolumeServiceImpl) getVolumeByID(volumeID string) (*ec2.Volume, error)
volume := &ec2.Volume{
VolumeId: aws.String(volMeta.VolumeID),
Size: aws.Int64(int64(volMeta.SizeGiB)),
Size: aws.Int64(utils.SafeUint64ToInt64(volMeta.SizeGiB)),
State: aws.String(state),
AvailabilityZone: aws.String(volMeta.AvailabilityZone),
CreateTime: aws.Time(volMeta.CreatedAt),
+4 -6
View File
@@ -2,7 +2,9 @@
Full-stack application for managing Hive AWS compatible products (EC2, S3)
## Setup
## Development Setup
Note: This is not needed for actually building hive-ui and running it. We commit the assets so that when you build hive, it will include hive-ui without you needing to install node and build frontend from source.
1. **Install Node.js using nvm**
@@ -35,7 +37,7 @@ Full-stack application for managing Hive AWS compatible products (EC2, S3)
4. **Accept Certs In Browser**
If you have added the CA to your machine you do not need to do this. But if you are sshd into a hive machine and want to view the ui, simply go to `https://localhost:9999` and `https://localhost:8443` and accept the certificates.
If you have added the CA to your machine you do not need to do this. But if you are sshd into a hive machine and want to view the ui, go to [https://localhost:9999](https://localhost:9999) and [https://localhost:8443](https://localhost:8443) and accept the certificates
5. **Launch Server**
@@ -51,7 +53,3 @@ Full-stack application for managing Hive AWS compatible products (EC2, S3)
cd frontend
pnpm dev
```
6. **Visit WebUI**
View [https://localhost:3000](https://localhost:3000) in your browser to continue.
-1
View File
@@ -1,5 +1,4 @@
node_modules
dist
.tanstack
*.tsbuildinfo
.pnpm-store
+1
View File
@@ -0,0 +1 @@
import{c as k,aN as v,aO as $,aJ as B,j as s,aI as C,aP as P,aQ as y,aL as K}from"./index.js";import{u as F}from"./useSuspenseQuery.js";import{B as L}from"./back-link.js";import{F as O,O as S,U as I}from"./upload-button.js";import"./alert-dialog.js";import"./download.js";import"./trash-2.js";import"./useMutation.js";function q(){const e=k.c(20),{bucket:n,_splat:b}=v.useParams(),a=$(b||""),{data:o}=F(B(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(O,{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(S,{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{q as component};
+1
View File
@@ -0,0 +1 @@
import{c as B,bk as _,bl as K,j as l,bm as k}from"./index.js";import{u as E}from"./useSuspenseQuery.js";import{B as C}from"./back-link.js";import{D as j,a as s}from"./detail-row.js";import{P as N}from"./page-heading.js";import{B as R}from"./badge.js";function Y(){const e=B.c(43),{id:V}=_.useParams();let v;e[0]!==V?(v=k(V),e[0]=V,e[1]=v):v=e[1];const{data:g}=E(v),t=g.Volumes?.[0];if(!t?.VolumeId){let S;return e[2]===Symbol.for("react.memo_cache_sentinel")?(S=l.jsxs(l.Fragment,{children:[l.jsx(C,{to:"/ec2/describe-volumes",children:"Back to volumes"}),l.jsx("p",{className:"text-muted-foreground",children:"Volume not found."})]}),e[2]=S):S=e[2],S}let p;e[3]!==t.CreateTime?(p=K(t.CreateTime),e[3]=t.CreateTime,e[4]=p):p=e[4];const T=p;let h;e[5]===Symbol.for("react.memo_cache_sentinel")?(h=l.jsx(C,{to:"/ec2/describe-volumes",children:"Back to volumes"}),e[5]=h):h=e[5];let a;e[6]!==t.State?(a=t.State?l.jsx(R,{variant:t.State==="available"?"default":"outline",children:t.State}):void 0,e[6]=t.State,e[7]=a):a=e[7];let i;e[8]!==a||e[9]!==t.VolumeId?(i=l.jsx(N,{actions:a,subtitle:"EC2 Volume Details",title:t.VolumeId}),e[8]=a,e[9]=t.VolumeId,e[10]=i):i=e[10];let y;e[11]===Symbol.for("react.memo_cache_sentinel")?(y=l.jsx(j.Header,{children:"Volume Information"}),e[11]=y):y=e[11];let o;e[12]!==t.VolumeId?(o=l.jsx(s,{label:"Volume ID",value:t.VolumeId}),e[12]=t.VolumeId,e[13]=o):o=e[13];const D=`${t.Size} GiB`;let n;e[14]!==D?(n=l.jsx(s,{label:"Size",value:D}),e[14]=D,e[15]=n):n=e[15];let m;e[16]!==t.VolumeType?(m=l.jsx(s,{label:"Volume Type",value:t.VolumeType}),e[16]=t.VolumeType,e[17]=m):m=e[17];let r;e[18]!==t.State?(r=l.jsx(s,{label:"State",value:t.State}),e[18]=t.State,e[19]=r):r=e[19];let c;e[20]!==t.AvailabilityZone?(c=l.jsx(s,{label:"Availability Zone",value:t.AvailabilityZone}),e[20]=t.AvailabilityZone,e[21]=c):c=e[21];let u;e[22]!==T?(u=l.jsx(s,{label:"Create Time",value:T}),e[22]=T,e[23]=u):u=e[23];const A=t.Encrypted?"Yes":"No";let d;e[24]!==A?(d=l.jsx(s,{label:"Encrypted",value:A}),e[24]=A,e[25]=d):d=e[25];let f;e[26]!==t.KmsKeyId?(f=l.jsx(s,{label:"KMS Key ID",value:t.KmsKeyId}),e[26]=t.KmsKeyId,e[27]=f):f=e[27];let x;e[28]!==r||e[29]!==c||e[30]!==u||e[31]!==d||e[32]!==f||e[33]!==o||e[34]!==n||e[35]!==m?(x=l.jsxs(j,{children:[y,l.jsxs(j.Content,{children:[o,n,m,r,c,u,d,f]})]}),e[28]=r,e[29]=c,e[30]=u,e[31]=d,e[32]=f,e[33]=o,e[34]=n,e[35]=m,e[36]=x):x=e[36];let b;e[37]!==t.Attachments?(b=t.Attachments&&t.Attachments.length>0&&l.jsxs(j,{children:[l.jsx(j.Header,{children:"Attachments"}),l.jsx(j.Content,{children:t.Attachments.map(Z)})]}),e[37]=t.Attachments,e[38]=b):b=e[38];let I;return e[39]!==x||e[40]!==b||e[41]!==i?(I=l.jsxs(l.Fragment,{children:[h,l.jsxs("div",{className:"space-y-6",children:[i,x,b]})]}),e[39]=x,e[40]=b,e[41]=i,e[42]=I):I=e[42],I}function Z(e){return l.jsxs("div",{className:"space-y-2 border-b pb-2 last:border-0 last:pb-0",children:[l.jsx(s,{label:"Instance ID",value:e.InstanceId}),l.jsx(s,{label:"Device",value:e.Device}),l.jsx(s,{label:"Status",value:e.State}),l.jsx(s,{label:"Attach Time",value:K(e.AttachTime)}),l.jsx(s,{label:"Delete on Termination",value:e.DeleteOnTermination?"Yes":"No"})]},e.InstanceId)}export{Y as component};
+1
View File
@@ -0,0 +1 @@
import{c as O,bn as Q,u as L,r as M,bl as V,j as t,bo 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{c 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 F=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 B=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 _;e[11]===Symbol.for("react.memo_cache_sentinel")?(_=t.jsxs(q,{onClick:T,size:"sm",variant:"destructive",children:[t.jsx(ae,{className:"size-4"}),"Delete"]}),e[11]=_):_=e[11];const I=s.KeyName??"";let l;e[12]!==I?(l=t.jsx(J,{actions:_,subtitle:"EC2 Key Pair Details",title:I}),e[12]=I,e[13]=l):l=e[13];let k;e[14]===Symbol.for("react.memo_cache_sentinel")?(k=t.jsx(x.Header,{children:"Key Pair Information"}),e[14]=k):k=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]!==B?(m=t.jsx(j,{label:"Creation Date",value:B}),e[23]=B,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:[k,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 C;e[37]===Symbol.for("react.memo_cache_sentinel")?(C=t.jsx(Z,{children:"Delete Key Pair"}),e[37]=C):C=e[37];let p;e[38]!==s.KeyName?(p=t.jsxs(U,{children:[C,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]!==F||e[43]!==E?(h=t.jsxs(X,{children:[A,t.jsx(Y,{disabled:i.isPending,onClick:F,children:E})]}),e[41]=i.isPending,e[42]=F,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
View File
@@ -0,0 +1 @@
import{c as g,bT as u,j as s,bB 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};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{c as S,bl 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(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};
@@ -0,0 +1 @@
import{g as o,c as r,j as l,aL as a}from"./index.js";const m=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],x=o("arrow-left",m);function p(i){const e=r.c(7);let s,t;e[0]!==i?({children:s,...t}=i,e[0]=i,e[1]=s,e[2]=t):(s=e[1],t=e[2]);let n;e[3]===Symbol.for("react.memo_cache_sentinel")?(n=l.jsx(x,{className:"size-4"}),e[3]=n):n=e[3];let c;return e[4]!==s||e[5]!==t?(c=l.jsxs(a,{className:"mb-6 inline-flex items-center gap-2 text-sm",...t,children:[n,s]}),e[4]=s,e[5]=t,e[6]=c):c=e[6],c}export{p as B};
+1
View File
@@ -0,0 +1 @@
import{c as g,a5 as b,a as f,b as p,bU 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=b({className:f(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};
+1
View File
@@ -0,0 +1 @@
import{g as c}from"./index.js";const e=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],t=c("check",e);export{t as C};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{c as i,j as n}from"./index.js";function d(l){const e=i.c(2),{children:s}=l;let t;return e[0]!==s?(t=n.jsx("div",{className:"rounded-lg border bg-card",children:s}),e[0]=s,e[1]=t):t=e[1],t}function m(l){const e=i.c(2),{children:s}=l;let t;return e[0]!==s?(t=n.jsx("div",{className:"border-b p-4",children:n.jsx("h2",{className:"font-semibold",children:s})}),e[0]=s,e[1]=t):t=e[1],t}function f(l){const e=i.c(2),{children:s}=l;let t;return e[0]!==s?(t=n.jsx("div",{className:"grid gap-4 p-4 sm:grid-cols-2",children:s}),e[0]=s,e[1]=t):t=e[1],t}const x=Object.assign(d,{Header:m,Content:f});function j(l){const e=i.c(9),{label:s,value:t}=l;let r;e[0]!==s?(r=n.jsx("dt",{className:"text-muted-foreground text-sm",children:s}),e[0]=s,e[1]=r):r=e[1];let c;e[2]!==t?(c=t||n.jsx("span",{className:"text-muted-foreground",children:"—"}),e[2]=t,e[3]=c):c=e[3];let o;e[4]!==c?(o=n.jsx("dd",{className:"mt-1 font-mono text-sm",children:c}),e[4]=c,e[5]=o):o=e[5];let a;return e[6]!==r||e[7]!==o?(a=n.jsxs("div",{children:[r,o]}),e[6]=r,e[7]=o,e[8]=a):a=e[8],a}export{x as D,j as a};
+1
View File
@@ -0,0 +1 @@
import{g as o}from"./index.js";const a=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],e=o("download",a);export{e as D};
+1
View File
@@ -0,0 +1 @@
import{ba as c,bC as i,bc as o,bD as C,bE as p,bF as f,bG as E,bH as K,bI as b,bJ as P,bK as w,c as r,bh as u,bL as m,aM as l,ax as y}from"./index.js";import{u as d}from"./useMutation.js";class S extends c.classBuilder().ep(i).m(function(n,t,s,a){return[o(s,n.getEndpointParameterInstructions())]}).s("AmazonEC2","CreateKeyPair",{}).n("EC2Client","CreateKeyPairCommand").sc(C).build(){}class R extends c.classBuilder().ep(i).m(function(n,t,s,a){return[o(s,n.getEndpointParameterInstructions())]}).s("AmazonEC2","DeleteKeyPair",{}).n("EC2Client","DeleteKeyPairCommand").sc(p).build(){}class q extends c.classBuilder().ep(i).m(function(n,t,s,a){return[o(s,n.getEndpointParameterInstructions())]}).s("AmazonEC2","ImportKeyPair",{}).n("EC2Client","ImportKeyPairCommand").sc(f).build(){}class $ extends c.classBuilder().ep(i).m(function(n,t,s,a){return[o(s,n.getEndpointParameterInstructions())]}).s("AmazonEC2","RebootInstances",{}).n("EC2Client","RebootInstancesCommand").sc(E).build(){}class g extends c.classBuilder().ep(i).m(function(n,t,s,a){return[o(s,n.getEndpointParameterInstructions())]}).s("AmazonEC2","RunInstances",{}).n("EC2Client","RunInstancesCommand").sc(K).build(){}class v extends c.classBuilder().ep(i).m(function(n,t,s,a){return[o(s,n.getEndpointParameterInstructions())]}).s("AmazonEC2","StartInstances",{}).n("EC2Client","StartInstancesCommand").sc(b).build(){}class Q extends c.classBuilder().ep(i).m(function(n,t,s,a){return[o(s,n.getEndpointParameterInstructions())]}).s("AmazonEC2","StopInstances",{}).n("EC2Client","StopInstancesCommand").sc(P).build(){}class x extends c.classBuilder().ep(i).m(function(n,t,s,a){return[o(s,n.getEndpointParameterInstructions())]}).s("AmazonEC2","TerminateInstances",{}).n("EC2Client","TerminateInstancesCommand").sc(w).build(){}const T=/\s+/;function O(){const e=r.c(2),n=u();let t;return e[0]!==n?(t={mutationFn:A,onSuccess:()=>{n.invalidateQueries({queryKey:["ec2","instances"]})}},e[0]=n,e[1]=t):t=e[1],d(t)}async function A(e){const n=new v({InstanceIds:[e]});return await m().send(n)}function H(){const e=r.c(2),n=u();let t;return e[0]!==n?(t={mutationFn:D,onSuccess:()=>{n.invalidateQueries({queryKey:["ec2","instances"]})}},e[0]=n,e[1]=t):t=e[1],d(t)}async function D(e){const n=new Q({InstanceIds:[e]});return await m().send(n)}function h(){const e=r.c(2),n=u();let t;return e[0]!==n?(t={mutationFn:F,onSuccess:()=>{n.invalidateQueries({queryKey:["ec2","instances"]})}},e[0]=n,e[1]=t):t=e[1],d(t)}async function F(e){const n=new x({InstanceIds:[e]});return await m().send(n)}function j(){const e=r.c(2),n=u();let t;return e[0]!==n?(t={mutationFn:_,onSuccess:()=>{n.invalidateQueries({queryKey:["ec2","instances"]})}},e[0]=n,e[1]=t):t=e[1],d(t)}async function _(e){const n=new $({InstanceIds:[e]});return await m().send(n)}function J(){const e=r.c(2),n=u();let t;return e[0]!==n?(t={mutationFn:z,onSuccess:async s=>{await n.cancelQueries(l);const a=n.getQueryData(l.queryKey);if(a&&s.Instances){const I={Instances:s.Instances,OwnerId:s.OwnerId,RequesterId:s.RequesterId,ReservationId:s.ReservationId,Groups:[]};n.setQueryData(l.queryKey,{...a,Reservations:[...a.Reservations||[],I]})}n.invalidateQueries({queryKey:["ec2","instances"]})}},e[0]=n,e[1]=t):t=e[1],d(t)}async function z(e){const n=new g({ImageId:e.imageId,InstanceType:e.instanceType,KeyName:e.keyName,MinCount:e.count,MaxCount:e.count,SecurityGroupIds:e.securityGroupIds,SubnetId:e.subnetId});return await m().send(n)}function L(){const e=r.c(2),n=u();let t;return e[0]!==n?(t={mutationFn:B,onSuccess:()=>{n.invalidateQueries(y)}},e[0]=n,e[1]=t):t=e[1],d(t)}async function B(e){const n=new S({KeyName:e.keyName,KeyType:"rsa"});return await m().send(n)}function W(){const e=r.c(2),n=u();let t;return e[0]!==n?(t={mutationFn:M,onSuccess:()=>{n.invalidateQueries(y)}},e[0]=n,e[1]=t):t=e[1],d(t)}async function M(e){const t=e.publicKeyMaterial.trim().split(T).slice(0,2).join(" "),s=new q({KeyName:e.keyName,PublicKeyMaterial:new TextEncoder().encode(t)});return await m().send(s)}function X(){const e=r.c(2),n=u();let t;return e[0]!==n?(t={mutationFn:N,onSuccess:()=>{n.invalidateQueries(y)}},e[0]=n,e[1]=t):t=e[1],d(t)}async function N(e){const n=new R({KeyPairId:e});return await m().send(n)}export{L as a,J as b,X as c,O as d,H as e,j as f,h as g,W as u};
@@ -0,0 +1 @@
import{c as o,a as d,j as n}from"./index.js";function m(a){const e=o.c(8);let s,r;e[0]!==a?({className:s,...r}=a,e[0]=a,e[1]=s,e[2]=r):(s=e[1],r=e[2]);let t;e[3]!==s?(t=d("flex select-none items-center gap-2 font-medium text-sm/relaxed leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",s),e[3]=s,e[4]=t):t=e[4];let l;return e[5]!==r||e[6]!==t?(l=n.jsx("label",{className:t,"data-slot":"label",...r}),e[5]=r,e[6]=t,e[7]=l):l=e[7],l}function p(a){const e=o.c(7),{msg:s,error:r}=a;let t;e[0]!==s?(t=s&&n.jsx("h2",{className:"text-destructive text-sm",children:s}),e[0]=s,e[1]=t):t=e[1];let l;e[2]!==r?(l=r&&n.jsx("p",{className:"text-destructive text-sm",children:r.message}),e[2]=r,e[3]=l):l=e[3];let i;return e[4]!==t||e[5]!==l?(i=n.jsxs("div",{className:"mb-6 max-w-4xl rounded-md border border-destructive bg-destructive/10 p-4",children:[t,l]}),e[4]=t,e[5]=l,e[6]=i):i=e[6],i}export{p as E,m as L};
@@ -0,0 +1 @@
import{c as R,u as T,j as t,i as z,B as w}from"./index.js";import{a as D,u as O,I as $}from"./zod.js";import{B as q}from"./back-link.js";import{E as G,L}from"./error-banner.js";import{P as J}from"./page-heading.js";import{T as U}from"./textarea.js";import{u as V}from"./ec2.js";import"./useMutation.js";function re(){const e=R.c(51),r=T(),s=V();let g;e[0]===Symbol.for("react.memo_cache_sentinel")?(g={resolver:D(z)},e[0]=g):g=e[0];const{handleSubmit:_,register:a,formState:H}=O(g),{errors:i,isSubmitting:M}=H;let N;e[1]!==s||e[2]!==r?(N=async Q=>{await s.mutateAsync(Q),r({to:"/ec2/describe-key-pairs"})},e[1]=s,e[2]=r,e[3]=N):N=e[3];const I=N;let A,K;e[4]===Symbol.for("react.memo_cache_sentinel")?(A=t.jsx(q,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),K=t.jsx(J,{title:"Import Key Pair"}),e[4]=A,e[5]=K):(A=e[4],K=e[5]);let l;e[6]!==s.error?(l=s.error&&t.jsx(G,{error:s.error,msg:"Failed to import key pair"}),e[6]=s.error,e[7]=l):l=e[7];let o;e[8]!==_||e[9]!==I?(o=_(I),e[8]=_,e[9]=I,e[10]=o):o=e[10];let P;e[11]===Symbol.for("react.memo_cache_sentinel")?(P=t.jsx(L,{htmlFor:"keyName",children:"Key Pair Name"}),e[11]=P):P=e[11];let c;e[12]!==a?(c=a("keyName"),e[12]=a,e[13]=c):c=e[13];let m;e[14]!==c?(m=t.jsx($,{id:"keyName",placeholder:"my-key-pair…",...c}),e[14]=c,e[15]=m):m=e[15];let n;e[16]!==i.keyName?(n=i.keyName&&t.jsx("p",{className:"text-destructive text-xs",children:i.keyName.message}),e[16]=i.keyName,e[17]=n):n=e[17];let p;e[18]!==n||e[19]!==m?(p=t.jsxs("div",{className:"space-y-2",children:[P,m,n]}),e[18]=n,e[19]=m,e[20]=p):p=e[20];let S,v;e[21]===Symbol.for("react.memo_cache_sentinel")?(S=t.jsx(L,{htmlFor:"publicKeyMaterial",children:"Public Key"}),v=t.jsx("p",{className:"text-muted-foreground text-xs",id:"publicKey-description",children:"Paste your OpenSSH public key (e.g. the contents of id_rsa.pub)"}),e[21]=S,e[22]=v):(S=e[21],v=e[22]);let y;e[23]!==a?(y=a("publicKeyMaterial"),e[23]=a,e[24]=y):y=e[24];let d;e[25]!==y?(d=t.jsx(U,{id:"publicKeyMaterial",placeholder:"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC…",rows:8,...y}),e[25]=y,e[26]=d):d=e[26];let f;e[27]!==i.publicKeyMaterial?(f=i.publicKeyMaterial&&t.jsx("p",{className:"text-destructive text-sm",children:i.publicKeyMaterial.message}),e[27]=i.publicKeyMaterial,e[28]=f):f=e[28];let u;e[29]!==d||e[30]!==f?(u=t.jsxs("div",{className:"space-y-2",children:[S,v,d,f]}),e[29]=d,e[30]=f,e[31]=u):u=e[31];const E=M||s.isPending;let x;e[32]!==r?(x=()=>r({to:"/ec2/describe-key-pairs"}),e[32]=r,e[33]=x):x=e[33];let b;e[34]!==E||e[35]!==x?(b=t.jsx(w,{disabled:E,onClick:x,type:"button",variant:"outline",children:"Cancel"}),e[34]=E,e[35]=x,e[36]=b):b=e[36];const F=M||s.isPending,C=M||s.isPending?"Importing…":"Import Key Pair";let h;e[37]!==F||e[38]!==C?(h=t.jsx(w,{disabled:F,type:"submit",children:C}),e[37]=F,e[38]=C,e[39]=h):h=e[39];let j;e[40]!==b||e[41]!==h?(j=t.jsxs("div",{className:"flex gap-2",children:[b,h]}),e[40]=b,e[41]=h,e[42]=j):j=e[42];let k;e[43]!==p||e[44]!==u||e[45]!==j||e[46]!==o?(k=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:o,children:[p,u,j]}),e[43]=p,e[44]=u,e[45]=j,e[46]=o,e[47]=k):k=e[47];let B;return e[48]!==k||e[49]!==l?(B=t.jsxs(t.Fragment,{children:[A,K,l,k]}),e[48]=k,e[49]=l,e[50]=B):B=e[50],B}export{re as component};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{c3 as z,c4 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
View File
@@ -0,0 +1 @@
import{c as n,e as r,j as i}from"./index.js";import{u as a}from"./useSuspenseQuery.js";import{P as p}from"./page-heading.js";function g(){const e=n.c(2),{data:t}=a(r),s=`${t?.Regions?.[0]?.RegionName} Dashboard`;let o;return e[0]!==s?(o=i.jsx(p,{title:s}),e[0]=s,e[1]=o):o=e[1],o}export{g as component};
+1
View File
@@ -0,0 +1 @@
import{c as i,f as m,j as t}from"./index.js";import{u as c}from"./useSuspenseQuery.js";import{L as l}from"./list-card.js";import{P as u}from"./page-heading.js";function N(){const e=i.c(5),{data:n}=c(m);let s;e[0]!==n.Buckets?(s=n.Buckets||[],e[0]=n.Buckets,e[1]=s):s=e[1];const r=s;let a;e[2]===Symbol.for("react.memo_cache_sentinel")?(a=t.jsx(u,{title:"S3 Buckets"}),e[2]=a):a=e[2];let o;return e[3]!==r?(o=t.jsxs(t.Fragment,{children:[a,r.length>0?t.jsx("div",{className:"space-y-4",children:r.map(p)}):t.jsx("p",{className:"text-muted-foreground",children:"No buckets found."})]}),e[3]=r,e[4]=o):o=e[4],o}function p(e){return e.Name?t.jsx(l,{params:{bucket:e.Name},subtitle:e.CreationDate?`Created: ${e.CreationDate.toLocaleString()}`:void 0,title:e.Name,to:"/s3/ls/$bucket"},e.Name):null}export{N as component};
+1
View File
@@ -0,0 +1 @@
import{c as g,aH as b,j as s,aI as P,aJ as y}from"./index.js";import{u as k}from"./useSuspenseQuery.js";import{B as N}from"./back-link.js";import{P as B}from"./page-heading.js";import{F as C,O,U as K}from"./upload-button.js";import"./alert-dialog.js";import"./download.js";import"./trash-2.js";import"./useMutation.js";function U(){const e=g.c(26),{bucket:t}=b.useParams();let u;e[0]!==t?(u=y(t),e[0]=t,e[1]=u):u=e[1];const{data:i}=k(u);let x;e[2]!==i.Contents?(x=i.Contents||[],e[2]=i.Contents,e[3]=x):x=e[3];const n=x;let d;e[4]!==i.CommonPrefixes?(d=i.CommonPrefixes||[],e[4]=i.CommonPrefixes,e[5]=d):d=e[5];const o=d;let h;e[6]===Symbol.for("react.memo_cache_sentinel")?(h=s.jsx(N,{to:"/s3/ls",children:"Back to buckets"}),e[6]=h):h=e[6];let r;e[7]!==t?(r=s.jsx(K,{bucket:t}),e[7]=t,e[8]=r):r=e[8];let m;e[9]!==t||e[10]!==r?(m=s.jsx(B,{actions:r,title:t}),e[9]=t,e[10]=r,e[11]=m):m=e[11];let a;e[12]!==t||e[13]!==o?(a=o.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:o.map(l=>{if(!l.Prefix)return null;const j=P(l.Prefix);return s.jsx(C,{bucketName:t,folderName:j,fullPrefix:l.Prefix},l.Prefix)})})]}),e[12]=t,e[13]=o,e[14]=a):a=e[14];let c;e[15]!==t||e[16]!==n?(c=n.length>0&&s.jsxs("div",{className:"space-y-2",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Objects"}),n.map(l=>l.Key?s.jsx(O,{bucketName:t,displayName:l.Key,fullKey:l.Key,object:l},l.Key):null)]}),e[15]=t,e[16]=n,e[17]=c):c=e[17];let f;e[18]!==o.length||e[19]!==n.length?(f=n.length===0&&o.length===0&&s.jsx("p",{className:"text-muted-foreground",children:"No objects found in this bucket."}),e[18]=o.length,e[19]=n.length,e[20]=f):f=e[20];let p;return e[21]!==m||e[22]!==a||e[23]!==c||e[24]!==f?(p=s.jsxs(s.Fragment,{children:[h,m,a,c,f]}),e[21]=m,e[22]=a,e[23]=c,e[24]=f,e[25]=p):p=e[25],p}export{U as component};
+1
View File
@@ -0,0 +1 @@
import{c as l,aK as m,j as t}from"./index.js";import{u as n}from"./useSuspenseQuery.js";import{L as u}from"./list-card.js";import{P as c}from"./page-heading.js";import{B as d}from"./badge.js";function h(){const e=l.c(5),{data:r}=n(m);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(c,{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(p)}):t.jsx("p",{className:"text-muted-foreground",children:"No volumes found."})]}),e[3]=o,e[4]=i):i=e[4],i}function p(e){return e.VolumeId?t.jsx(u,{badge:e.State?t.jsx(d,{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{h as component};
+1
View File
@@ -0,0 +1 @@
import{c as m,ax as l,j as t,aL as n,B as c}from"./index.js";import{u as p}from"./useSuspenseQuery.js";import{L as d}from"./list-card.js";import{P as y}from"./page-heading.js";function P(){const e=m.c(4),{data:r}=p(l);let s;if(e[0]!==r.KeyPairs){const a=[...r.KeyPairs||[]].sort(x);let i;e[2]===Symbol.for("react.memo_cache_sentinel")?(i=t.jsx(n,{to:"/ec2/import-key-pair",children:t.jsx(c,{variant:"outline",children:"Import Key Pair"})}),e[2]=i):i=e[2];let o;e[3]===Symbol.for("react.memo_cache_sentinel")?(o=t.jsx(y,{actions:t.jsxs("div",{className:"flex gap-2",children:[i,t.jsx(n,{to:"/ec2/create-key-pair",children:t.jsx(c,{children:"Create Key Pair"})})]}),title:"EC2 Key Pairs"}),e[3]=o):o=e[3],s=t.jsxs(t.Fragment,{children:[o,a.length>0?t.jsx("div",{className:"space-y-4",children:a.map(u)}):t.jsx("p",{className:"text-muted-foreground",children:"No key pairs found."})]}),e[0]=r.KeyPairs,e[1]=s}else s=e[1];return s}function u(e){return e.KeyPairId?t.jsx(d,{params:{id:e.KeyPairId},subtitle:`${e.KeyPairId} • ${e.KeyType}`,title:e.KeyName??"",to:"/ec2/describe-key-pairs/$id"},e.KeyPairId):null}function x(e,r){const s=e.KeyName?.toLowerCase()??"",a=r.KeyName?.toLowerCase()??"";return s.localeCompare(a)}export{P as component};
+1
View File
@@ -0,0 +1 @@
import{c as o,aM as c,j as t,aL 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};
+1
View File
@@ -0,0 +1 @@
import{c as n,aw as i,j as t}from"./index.js";import{u as l}from"./useSuspenseQuery.js";import{L as c}from"./list-card.js";import{P as d}from"./page-heading.js";import{B as u}from"./badge.js";function h(){const e=n.c(5),{data:m}=l(i);let s;e[0]!==m.Images?(s=m.Images||[],e[0]=m.Images,e[1]=s):s=e[1];const a=s;let r;e[2]===Symbol.for("react.memo_cache_sentinel")?(r=t.jsx(d,{title:"EC2 Images"}),e[2]=r):r=e[2];let o;return e[3]!==a?(o=t.jsxs(t.Fragment,{children:[r,a.length>0?t.jsx("div",{className:"space-y-4",children:a.map(p)}):t.jsx("p",{className:"text-muted-foreground",children:"No images found."})]}),e[3]=a,e[4]=o):o=e[4],o}function p(e){return e.ImageId?t.jsx(c,{badge:e.State?t.jsx(u,{variant:e.State==="available"?"default":"outline",children:e.State}):void 0,params:{id:e.ImageId},subtitle:e.Name,title:e.ImageId,to:"/ec2/describe-images/$id"},e.ImageId):null}export{h as component};
@@ -0,0 +1 @@
import{c as u,j as m,aL as h}from"./index.js";function b(x){const e=u.c(22);let s,l,i,t,r;e[0]!==x?({title:r,subtitle:t,badge:s,children:l,...i}=x,e[0]=x,e[1]=s,e[2]=l,e[3]=i,e[4]=t,e[5]=r):(s=e[1],l=e[2],i=e[3],t=e[4],r=e[5]);let a;e[6]!==r?(a=m.jsx("h3",{className:"font-semibold",children:r}),e[6]=r,e[7]=a):a=e[7];let c;e[8]!==t?(c=t&&m.jsx("p",{className:"mt-1 font-medium text-muted-foreground text-sm",children:t}),e[8]=t,e[9]=c):c=e[9];let n;e[10]!==l||e[11]!==a||e[12]!==c?(n=m.jsxs("div",{className:"flex-1",children:[a,c,l]}),e[10]=l,e[11]=a,e[12]=c,e[13]=n):n=e[13];let o;e[14]!==s?(o=s&&m.jsx("div",{className:"ml-4",children:s}),e[14]=s,e[15]=o):o=e[15];let d;e[16]!==n||e[17]!==o?(d=m.jsxs("div",{className:"flex items-start justify-between",children:[n,o]}),e[16]=n,e[17]=o,e[18]=d):d=e[18];let f;return e[19]!==i||e[20]!==d?(f=m.jsx(h,{className:"block rounded-lg border bg-card p-4 transition-colors hover:bg-accent",...i,children:d}),e[19]=i,e[20]=d,e[21]=f):f=e[21],f}export{b as L};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{c as o,j as n}from"./index.js";function f(m){const e=o.c(12),{title:x,subtitle:r,actions:c}=m;let t;e[0]!==x?(t=n.jsx("h1",{className:"text-3xl",children:x}),e[0]=x,e[1]=t):t=e[1];let s;e[2]!==r?(s=r&&n.jsx("p",{className:"mt-1 text-muted-foreground",children:r}),e[2]=r,e[3]=s):s=e[3];let i;e[4]!==t||e[5]!==s?(i=n.jsxs("div",{children:[t,s]}),e[4]=t,e[5]=s,e[6]=i):i=e[6];let l;e[7]!==c?(l=c&&n.jsx("div",{children:c}),e[7]=c,e[8]=l):l=e[8];let a;return e[9]!==i||e[10]!==l?(a=n.jsxs("div",{className:"mb-6 flex items-start justify-between",children:[i,l]}),e[9]=i,e[10]=l,e[11]=a):a=e[11],a}export{f as P};
+1
View File
@@ -0,0 +1 @@
import{c as o,j as s,O as n}from"./index.js";const r=()=>{const e=o.c(1);let t;return e[0]===Symbol.for("react.memo_cache_sentinel")?(t=s.jsx(n,{}),e[0]=t):t=e[0],t};export{r as component};
+1
View File
@@ -0,0 +1 @@
import{c as o,j as s,O as n}from"./index.js";const r=()=>{const e=o.c(1);let t;return e[0]===Symbol.for("react.memo_cache_sentinel")?(t=s.jsx(n,{}),e[0]=t):t=e[0],t};export{r as component};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{c as d,j as g}from"./index.js";function o(n){const e=d.c(3),{state:t}=n;let a="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100";t==="running"?a="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100":t==="stopped"&&(a="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100");const s=`rounded-full px-2 py-1 text-xs ${a}`;let r;return e[0]!==t||e[1]!==s?(r=g.jsx("div",{className:s,children:t}),e[0]=t,e[1]=s,e[2]=r):r=e[2],r}export{o as S};
+1
View File
@@ -0,0 +1 @@
import{c as n,a as o,j as d}from"./index.js";function c(s){const e=n.c(8);let i,r;e[0]!==s?({className:i,...r}=s,e[0]=s,e[1]=i,e[2]=r):(i=e[1],r=e[2]);let t;e[3]!==i?(t=o("field-sizing-content flex min-h-16 w-full resize-none rounded-md border border-input bg-input/20 px-2 py-2 text-sm outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-xs/relaxed dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",i),e[3]=i,e[4]=t):t=e[4];let a;return e[5]!==r||e[6]!==t?(a=d.jsx("textarea",{className:t,"data-slot":"textarea",...r}),e[5]=r,e[6]=t,e[7]=a):a=e[7],a}export{c as T};
+1
View File
@@ -0,0 +1 @@
import{g as a}from"./index.js";const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],o=a("trash-2",t);export{o as T};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{bM as l,bN as b,bO as a,bP as d,bQ as h,bh as p,r as o,bR as m,bS 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),b(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??d();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=p(),[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
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<meta name="theme-color" content="#000000">
<meta name="description" content="Hive Control Panel">
<script type="module" crossorigin src="/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
<svg width="364" height="463" viewBox="0 0 364 463" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M272.523 171.205C247.773 197.595 211.323 215.815 182.103 237.425C165.663 249.575 122.633 281.165 151.093 301.825C185.533 326.835 271.333 304.985 301.013 277.585C317.193 262.645 316.693 242.485 294.703 234.075C270.913 224.975 243.483 231.925 219.353 235.755C218.543 235.885 216.823 236.805 216.993 235.345C227.053 228.885 236.103 220.925 245.933 214.135C255.403 207.585 267.053 198.225 278.643 198.015C314.693 197.345 351.803 215.135 361.203 252.215C376.833 313.885 303.333 362.255 249.833 366.635L280.913 352.235C292.563 345.305 304.353 338.105 314.653 329.215C317.213 327.005 319.073 324.145 321.243 322.035C321.753 321.535 327.053 319.265 324.003 319.125C296.253 337.335 263.793 349.415 231.323 356.335C220.173 358.715 208.323 360.995 196.963 361.285C197.243 362.845 198.643 362.525 199.663 362.845C208.823 365.795 219.313 367.545 228.843 369.005C230.163 370.005 228.733 370.415 227.683 370.555C219.063 371.665 209.923 373.305 201.293 373.745C176.843 374.985 152.203 372.435 128.573 366.425L115.293 328.755C114.153 328.865 114.073 329.895 113.783 330.725C112.613 334.015 107.893 358.025 106.313 358.365C93.6234 354.515 82.2334 345.915 72.9034 336.575L95.0434 296.915L59.7134 316.375C54.7434 305.145 53.8334 292.435 55.4934 280.355L101.213 257.645C86.9934 256.705 72.6134 258.955 58.4734 260.425C60.8734 253.495 63.8034 246.615 67.3634 240.195C68.6634 237.855 75.5634 226.305 77.3834 226.245C93.9334 229.415 111.773 230.895 128.363 227.675L92.3534 212.825L91.0334 211.045C98.3834 204.125 105.843 197.235 114.113 191.325C129.603 195.825 145.523 199.455 161.733 199.655L130.633 179.885C134.133 177.125 137.813 174.575 141.553 172.165C144.643 170.175 153.283 164.425 156.423 164.835C168.933 169.105 181.923 172.125 195.123 173.185C189.093 166.935 177.633 162.325 171.833 156.465C171.203 155.825 170.713 155.785 171.003 154.585C186.153 145.885 203.003 137.235 216.053 125.375C229.363 113.265 243.423 93.4848 233.853 75.2048C244.893 78.7148 254.393 92.6648 254.153 104.325C253.493 136.595 219.073 165.375 197.483 185.615C230.833 171.615 268.993 142.685 268.923 102.735C268.863 70.2248 239.263 57.1948 211.283 54.2348C210.103 61.3848 207.573 68.9448 203.863 75.1748C195.623 89.0148 178.803 97.6648 170.013 112.055C164.023 121.865 162.883 135.065 150.163 138.825C144.973 140.365 138.293 140.595 132.903 140.615L144.743 134.365C158.003 124.625 157.773 107.025 165.893 93.9248C164.953 92.5948 155.413 90.0748 153.183 89.5248C143.363 87.1048 126.713 84.0248 116.883 84.6948C113.843 84.9048 100.143 87.0348 98.9634 89.3748C97.2634 92.7648 105.533 107.255 107.973 110.575C108.463 111.245 109.463 111.075 109.513 111.145C109.973 111.745 109.663 113.165 108.373 112.675C107.443 112.315 101.853 108.205 100.593 107.235C90.8934 99.7648 79.8234 88.4748 71.7234 79.3648C70.4034 76.1748 68.6334 72.6148 71.7634 69.9848C88.8634 59.4048 105.793 48.3148 123.163 38.1948C174.603 8.20482 233.143 -25.6952 282.353 29.2448C323.153 74.8048 311.503 129.615 272.493 171.195L272.523 171.205ZM114.153 67.5948C127.613 65.2648 153.013 78.6248 157.193 58.8348C157.603 56.9048 156.493 55.7948 157.583 53.9148C158.403 52.5048 163.083 49.1048 164.713 47.7848C169.903 43.6048 175.823 40.3348 180.923 36.0148C161.113 39.2048 141.163 50.5648 123.853 60.5948C120.983 62.2548 117.873 63.9948 115.153 65.8548C114.453 66.3348 113.713 65.7348 114.143 67.5848L114.153 67.5948ZM21.1634 276.415C-11.8666 312.635 -4.35661 364.395 30.6734 396.135C65.5434 427.725 120.403 438.925 166.373 436.725C207.813 434.735 249.893 421.075 290.773 434.965C306.663 440.365 320.533 450.465 332.363 462.135C330.773 454.415 326.123 446.365 321.623 439.825C280.593 380.225 200.893 402.565 140.123 390.665C107.163 384.215 72.8934 369.135 52.8234 341.225C35.4734 317.105 32.7734 288.665 41.3634 260.475C39.6134 258.725 23.0334 274.355 21.1634 276.415ZM143.743 109.535C136.373 116.045 128.573 122.235 120.403 127.775L97.1634 139.135C116.843 139.885 137.193 128.635 143.753 109.545L143.743 109.535Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow: /
@@ -114,6 +114,42 @@ function InstanceDetail() {
</DetailCard.Content>
</DetailCard> */}
{/* Block Device Mappings */}
{instance.BlockDeviceMappings &&
instance.BlockDeviceMappings.length > 0 && (
<DetailCard>
<DetailCard.Header>Block Device Mappings</DetailCard.Header>
<DetailCard.Content>
<DetailRow
label="Device Name"
value={instance.BlockDeviceMappings[0]?.DeviceName}
/>
<DetailRow
label="Volume ID"
value={instance.BlockDeviceMappings[0]?.Ebs?.VolumeId}
/>
<DetailRow
label="Status"
value={instance.BlockDeviceMappings[0]?.Ebs?.Status}
/>
<DetailRow
label="Attach Time"
value={formatDateTime(
instance.BlockDeviceMappings[0]?.Ebs?.AttachTime,
)}
/>
<DetailRow
label="Delete on Termination"
value={
instance.BlockDeviceMappings[0]?.Ebs?.DeleteOnTermination
? "Yes"
: "No"
}
/>
</DetailCard.Content>
</DetailCard>
)}
{/* AMI Details */}
{image && <AmiDetails image={image} />}
</div>
@@ -75,6 +75,13 @@ function CreateInstance() {
const uniqueInstanceTypes = Object.keys(instanceTypeCounts).sort()
// Compute default values from loaded data
const defaultImageId = images[0]?.ImageId
const defaultKeyName = keyPairs[0]?.KeyName
const defaultInstanceType =
uniqueInstanceTypes.find((type) => type.endsWith(".nano")) ??
uniqueInstanceTypes[0]
const {
control,
handleSubmit,
@@ -93,6 +100,9 @@ function CreateInstance() {
),
defaultValues: {
count: 1,
imageId: defaultImageId,
keyName: defaultKeyName,
instanceType: defaultInstanceType,
},
})
@@ -10,6 +10,13 @@ export default defineConfig({
envDir: "../",
build: {
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
entryFileNames: "assets/[name].js",
chunkFileNames: "assets/[name].js",
assetFileNames: "assets/[name].[ext]",
},
},
},
plugins: [
basicSsl(),
+3 -7
View File
@@ -151,13 +151,9 @@ func (svc *Service) launchService() error {
file, err := contentFS.Open(path)
if err == nil {
_ = file.Close()
// File exists, check if it's not an HTML file for caching
if filepath.Ext(path) != ".html" {
// Vite outputs unique hashes for files so we can cache them forever
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
} else {
w.Header().Set("Cache-Control", "no-cache")
}
// Use no-cache to force revalidation; http.FileServer sets ETags
// so browsers will get 304 Not Modified when files haven't changed
w.Header().Set("Cache-Control", "no-cache")
fileServer.ServeHTTP(w, r)
return
}
+2 -2
View File
@@ -62,7 +62,7 @@ for cmd in "${optional_commands[@]}"; do
echo "⚠️ $cmd not found (optional)"
case "$cmd" in
"air")
echo " Install air for hot reloading: go install github.com/cosmtrek/air@latest"
echo " Install air for hot reloading: go install github.com/air-verse/air@latest"
;;
"nbdkit")
echo " Install nbdkit: sudo apt-get install nbdkit (Ubuntu/Debian)"
@@ -172,4 +172,4 @@ echo "📊 Monitor logs:"
echo " tail -f $DATA_DIR/logs/*.log"
echo ""
echo "🧪 Test with AWS CLI:"
echo " aws --endpoint-url https://localhost:$HIVE_GATEWAY_PORT --no-verify-ssl ec2 describe-instances"
echo " aws --endpoint-url https://localhost:$HIVE_GATEWAY_PORT --no-verify-ssl ec2 describe-instances"
+2
View File
@@ -16,6 +16,8 @@ rm -rf ~/hive/viperblock/*
rm -rf ~/hive/nats/*
rm -rf ~/hive/images/*
# Enable pprof for development
PPROF_ENABLED=1 PPROF_OUTPUT=/tmp/hive-vm.prof ./scripts/start-dev.sh
+21
View File
@@ -4,6 +4,10 @@
# This script starts all required services in the correct order using Hive service commands
# Usage: ./scripts/start-dev.sh [data-dir]
# data-dir: Optional data directory path (default: ~/hive)
#
# Environment variables:
# UI=false Skip starting Hive UI (e.g., UI=false ./scripts/start-dev.sh)
# HIVE_SKIP_BUILD=true Skip building binaries before starting
set -e
@@ -270,8 +274,25 @@ start_service "awsgw" "$AWSGW_CMD"
#check_service "awsgw" "9999"
# 6️⃣ Start Hive UI (skip with UI=false)
if [ "${UI}" != "false" ]; then
echo ""
echo "6️⃣. Starting Hive UI..."
HIVEUI_CMD="./bin/hive service hive-ui start"
start_service "hive-ui" "$HIVEUI_CMD"
else
echo ""
echo "6️⃣. Skipping Hive UI (UI=false)"
fi
echo ""
echo "🔗 Service endpoints will be:"
if [ "${UI}" != "false" ]; then
echo " - Hive UI: https://localhost:3000"
fi
echo " - NATS: nats://localhost:4222"
echo " - Predastore: https://localhost:8443"
echo " - AWS Gateway: https://localhost:9999"
+5 -2
View File
@@ -59,7 +59,10 @@ stop_service() {
# Stop services in reverse order
echo "Stopping services..."
echo ""
# Stop Hive daemon/gateway first (it will terminate running instances, unmount nbd devices)
# Stop Hive UI first (last to start)
stop_service "hive-ui" "$PID_DIR"
# Stop Hive daemon/gateway (it will terminate running instances, unmount nbd devices)
stop_service "hive" "$PID_DIR"
# Stop AWSGW
@@ -78,7 +81,7 @@ echo ""
echo "✅ Hive development environment stopped"
# Show any remaining related processes
remaining=$(ps aux | grep -E "(hive|nats|predastore|viperblock)" | grep -v grep | grep -v "stop-dev.sh" || true)
remaining=$(ps aux | grep -E "(hive|hive-ui|nats|predastore|viperblock)" | grep -v grep | grep -v "stop-dev.sh" || true)
if [[ -n "$remaining" ]]; then
echo ""
echo "⚠️ Some related processes may still be running:"