From 896b70e508b67b147fa378f0bf3e244e2e8e03b8 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:07:16 +1100 Subject: [PATCH 1/8] feat: describe-volumes --- hive/daemon/daemon.go | 56 ++++ hive/gateway/ec2.go | 24 ++ hive/gateway/ec2/volume/DescribeVolumes.go | 51 ++++ .../ec2/volume/DescribeVolumes_test.go | 102 ++++++++ hive/handlers/ec2/volume/service.go | 8 + hive/handlers/ec2/volume/service_impl.go | 246 ++++++++++++++++++ hive/handlers/ec2/volume/service_nats.go | 51 ++++ 7 files changed, 538 insertions(+) create mode 100644 hive/gateway/ec2/volume/DescribeVolumes.go create mode 100644 hive/gateway/ec2/volume/DescribeVolumes_test.go create mode 100644 hive/handlers/ec2/volume/service.go create mode 100644 hive/handlers/ec2/volume/service_impl.go create mode 100644 hive/handlers/ec2/volume/service_nats.go diff --git a/hive/daemon/daemon.go b/hive/daemon/daemon.go index 8ced8692a..61e8284c4 100644 --- a/hive/daemon/daemon.go +++ b/hive/daemon/daemon.go @@ -30,6 +30,7 @@ import ( handlers_ec2_image "github.com/mulgadc/hive/hive/handlers/ec2/image" handlers_ec2_instance "github.com/mulgadc/hive/hive/handlers/ec2/instance" handlers_ec2_key "github.com/mulgadc/hive/hive/handlers/ec2/key" + handlers_ec2_volume "github.com/mulgadc/hive/hive/handlers/ec2/volume" "github.com/mulgadc/hive/hive/qmp" "github.com/mulgadc/hive/hive/utils" "github.com/mulgadc/hive/hive/vm" @@ -76,6 +77,7 @@ type Daemon struct { instanceService *handlers_ec2_instance.InstanceServiceImpl keyService *handlers_ec2_key.KeyServiceImpl imageService *handlers_ec2_image.ImageServiceImpl + volumeService *handlers_ec2_volume.VolumeServiceImpl ctx context.Context cancel context.CancelFunc shutdownWg sync.WaitGroup @@ -461,6 +463,9 @@ func (d *Daemon) Start() error { // Create image service for handling EC2 AMI operations d.imageService = handlers_ec2_image.NewImageServiceImpl(d.config) + // Create volume service for handling EC2 EBS volume operations + d.volumeService = handlers_ec2_volume.NewVolumeServiceImpl(d.config) + log.Printf("Subscribing to subject pattern: %s", "ec2.launch") // Subscribe to EC2 events with queue group (legacy topic for backward compatibility) @@ -526,6 +531,15 @@ func (d *Daemon) Start() error { return fmt.Errorf("failed to subscribe to NATS ec2.DescribeImages: %w", err) } + log.Printf("Subscribing to subject pattern: %s", "ec2.DescribeVolumes") + + // Subscribe to EC2 DescribeVolumes with queue group + d.natsSubscriptions["ec2.DescribeVolumes"], err = d.natsConn.QueueSubscribe("ec2.DescribeVolumes", "hive-workers", d.handleEC2DescribeVolumes) + + if err != nil { + return fmt.Errorf("failed to subscribe to NATS ec2.DescribeVolumes: %w", err) + } + log.Printf("Subscribing to subject pattern: %s", "ec2.DescribeInstances") // Subscribe to EC2 DescribeInstances - no queue group for multi-node fan-out @@ -2095,6 +2109,48 @@ func (d *Daemon) handleEC2DescribeImages(msg *nats.Msg) { slog.Info("handleEC2DescribeImages completed", "count", len(output.Images)) } +// handleEC2DescribeVolumes processes incoming EC2 DescribeVolumes requests +func (d *Daemon) handleEC2DescribeVolumes(msg *nats.Msg) { + log.Printf("Received message on subject: %s", msg.Subject) + log.Printf("Message data: %s", string(msg.Data)) + + // Initialize describeVolumesInput before unmarshaling into it + describeVolumesInput := &ec2.DescribeVolumesInput{} + var errResp []byte + + errResp = utils.UnmarshalJsonPayload(describeVolumesInput, msg.Data) + + if errResp != nil { + msg.Respond(errResp) + slog.Error("Request does not match DescribeVolumesInput") + return + } + + slog.Info("Processing DescribeVolumes request") + + // Delegate to volume service for business logic (S3 listing) + output, err := d.volumeService.DescribeVolumes(describeVolumesInput) + + if err != nil { + slog.Error("handleEC2DescribeVolumes service.DescribeVolumes failed", "err", err) + errResp = utils.GenerateErrorPayload(err.Error()) + msg.Respond(errResp) + return + } + + // Respond to NATS with DescribeVolumesOutput + jsonResponse, err := json.Marshal(output) + if err != nil { + slog.Error("handleEC2DescribeVolumes failed to marshal output", "err", err) + errResp = utils.GenerateErrorPayload(awserrors.ErrorServerInternal) + msg.Respond(errResp) + return + } + msg.Respond(jsonResponse) + + slog.Info("handleEC2DescribeVolumes completed", "count", len(output.Volumes)) +} + // handleEC2DescribeInstanceTypes processes incoming EC2 DescribeInstanceTypes requests // This handler responds with instance types that can currently be provisioned on this node // based on available resources (CPU and memory not already allocated to running instances) diff --git a/hive/gateway/ec2.go b/hive/gateway/ec2.go index af9d06e7b..7d513829d 100644 --- a/hive/gateway/ec2.go +++ b/hive/gateway/ec2.go @@ -11,6 +11,7 @@ import ( gateway_ec2_instance "github.com/mulgadc/hive/hive/gateway/ec2/instance" gateway_ec2_key "github.com/mulgadc/hive/hive/gateway/ec2/key" gateway_ec2_regions "github.com/mulgadc/hive/hive/gateway/ec2/regions" + gateway_ec2_volume "github.com/mulgadc/hive/hive/gateway/ec2/volume" "github.com/mulgadc/hive/hive/utils" ) @@ -301,6 +302,29 @@ func (gw *GatewayConfig) EC2_Request(ctx *fiber.Ctx) error { if err != nil { return errors.New("failed to marshal response to XML") } + + case "DescribeVolumes": + var input = &ec2.DescribeVolumesInput{} + err = awsec2query.QueryParamsToStruct(queryArgs, input) + + if err != nil { + return err + } + + output, err := gateway_ec2_volume.DescribeVolumes(input, gw.NATSConn) + + if err != nil { + return err + } + + // Convert to XML + payload := utils.GenerateXMLPayload("DescribeVolumesResponse", output) + xmlOutput, err = utils.MarshalToXML(payload) + + if err != nil { + return errors.New("failed to marshal response to XML") + } + default: err = errors.New("InvalidAction") } diff --git a/hive/gateway/ec2/volume/DescribeVolumes.go b/hive/gateway/ec2/volume/DescribeVolumes.go new file mode 100644 index 000000000..66fb44aaf --- /dev/null +++ b/hive/gateway/ec2/volume/DescribeVolumes.go @@ -0,0 +1,51 @@ +package gateway_ec2_volume + +import ( + "errors" + "strings" + + "github.com/aws/aws-sdk-go/service/ec2" + handlers_ec2_volume "github.com/mulgadc/hive/hive/handlers/ec2/volume" + "github.com/nats-io/nats.go" +) + +// ValidateDescribeVolumesInput validates the input parameters +func ValidateDescribeVolumesInput(input *ec2.DescribeVolumesInput) error { + if input == nil { + return nil + } + + // Validate VolumeId format if provided + if input.VolumeIds != nil { + for _, volumeId := range input.VolumeIds { + if volumeId != nil && !strings.HasPrefix(*volumeId, "vol-") { + return errors.New("InvalidVolume.Malformed") + } + } + } + + return nil +} + +// DescribeVolumes handles the DescribeVolumes API call +func DescribeVolumes(input *ec2.DescribeVolumesInput, natsConn *nats.Conn) (ec2.DescribeVolumesOutput, error) { + var output ec2.DescribeVolumesOutput + + // Validate input + err := ValidateDescribeVolumesInput(input) + if err != nil { + return output, err + } + + // Create NATS service and call handler + volumeService := handlers_ec2_volume.NewNATSVolumeService(natsConn) + result, err := volumeService.DescribeVolumes(input) + + if err != nil { + return output, err + } + + // Return result + output = *result + return output, nil +} diff --git a/hive/gateway/ec2/volume/DescribeVolumes_test.go b/hive/gateway/ec2/volume/DescribeVolumes_test.go new file mode 100644 index 000000000..abff51610 --- /dev/null +++ b/hive/gateway/ec2/volume/DescribeVolumes_test.go @@ -0,0 +1,102 @@ +package gateway_ec2_volume + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/stretchr/testify/assert" +) + +func TestValidateDescribeVolumesInput(t *testing.T) { + tests := []struct { + name string + input *ec2.DescribeVolumesInput + wantErr bool + errMsg string + }{ + { + name: "NilInput", + input: nil, + wantErr: false, + }, + { + name: "EmptyInput", + input: &ec2.DescribeVolumesInput{}, + wantErr: false, + }, + { + name: "ValidSingleVolumeId", + input: &ec2.DescribeVolumesInput{ + VolumeIds: []*string{aws.String("vol-0123456789abcdef0")}, + }, + wantErr: false, + }, + { + name: "ValidMultipleVolumeIds", + input: &ec2.DescribeVolumesInput{ + VolumeIds: []*string{ + aws.String("vol-abc123"), + aws.String("vol-def456"), + aws.String("vol-ghi789"), + }, + }, + wantErr: false, + }, + { + name: "InvalidVolumeId_NoPrefix", + input: &ec2.DescribeVolumesInput{ + VolumeIds: []*string{aws.String("invalid-id")}, + }, + wantErr: true, + errMsg: "InvalidVolume.Malformed", + }, + { + name: "InvalidVolumeId_WrongPrefix", + input: &ec2.DescribeVolumesInput{ + VolumeIds: []*string{aws.String("ami-0123456789abcdef0")}, + }, + wantErr: true, + errMsg: "InvalidVolume.Malformed", + }, + { + name: "InvalidVolumeId_EmptyString", + input: &ec2.DescribeVolumesInput{ + VolumeIds: []*string{aws.String("")}, + }, + wantErr: true, + errMsg: "InvalidVolume.Malformed", + }, + { + name: "MixedValidAndInvalid", + input: &ec2.DescribeVolumesInput{ + VolumeIds: []*string{ + aws.String("vol-valid123"), + aws.String("invalid-id"), + }, + }, + wantErr: true, + errMsg: "InvalidVolume.Malformed", + }, + { + name: "NilVolumeIdInList", + input: &ec2.DescribeVolumesInput{ + VolumeIds: []*string{nil, aws.String("vol-abc123")}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateDescribeVolumesInput(tt.input) + + if tt.wantErr { + assert.Error(t, err) + assert.Equal(t, tt.errMsg, err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/hive/handlers/ec2/volume/service.go b/hive/handlers/ec2/volume/service.go new file mode 100644 index 000000000..fb6f62463 --- /dev/null +++ b/hive/handlers/ec2/volume/service.go @@ -0,0 +1,8 @@ +package handlers_ec2_volume + +import "github.com/aws/aws-sdk-go/service/ec2" + +// VolumeService defines the interface for EBS volume operations +type VolumeService interface { + DescribeVolumes(input *ec2.DescribeVolumesInput) (*ec2.DescribeVolumesOutput, error) +} diff --git a/hive/handlers/ec2/volume/service_impl.go b/hive/handlers/ec2/volume/service_impl.go new file mode 100644 index 000000000..c41bd7eb4 --- /dev/null +++ b/hive/handlers/ec2/volume/service_impl.go @@ -0,0 +1,246 @@ +package handlers_ec2_volume + +import ( + "crypto/tls" + "encoding/json" + "errors" + "io" + "log/slog" + "net/http" + "strings" + "sync" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/mulgadc/hive/hive/awserrors" + "github.com/mulgadc/hive/hive/config" + "github.com/mulgadc/viperblock/viperblock" +) + +// VolumeServiceImpl handles EBS volume operations with S3 storage +type VolumeServiceImpl struct { + config *config.Config + s3Client *s3.S3 +} + +// NewVolumeServiceImpl creates a new daemon-side volume service +func NewVolumeServiceImpl(cfg *config.Config) *VolumeServiceImpl { + // Create HTTP client with TLS verification disabled for local S3-compatible endpoints + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + + // Create AWS SDK S3 client configured for Predastore endpoint + sess := session.Must(session.NewSession(&aws.Config{ + Endpoint: aws.String(cfg.Predastore.Host), + Region: aws.String(cfg.Predastore.Region), + Credentials: credentials.NewStaticCredentials(cfg.Predastore.AccessKey, cfg.Predastore.SecretKey, ""), + S3ForcePathStyle: aws.Bool(true), + HTTPClient: httpClient, + })) + + s3Client := s3.New(sess) + + return &VolumeServiceImpl{ + config: cfg, + s3Client: s3Client, + } +} + +// DescribeVolumes lists EBS volumes by reading config.json files from S3 +func (s *VolumeServiceImpl) DescribeVolumes(input *ec2.DescribeVolumesInput) (*ec2.DescribeVolumesOutput, error) { + if input == nil { + input = &ec2.DescribeVolumesInput{} + } + + slog.Info("Describing volumes", "volumeIds", input.VolumeIds) + + var volumes []*ec2.Volume + + // Fast path: if specific volume IDs are requested, fetch them directly + if len(input.VolumeIds) > 0 { + volumes = s.fetchVolumesByIDs(input.VolumeIds) + slog.Info("DescribeVolumes completed", "count", len(volumes)) + return &ec2.DescribeVolumesOutput{Volumes: volumes}, nil + } + + // Slow path: list all volumes (no filter provided) + result, err := s.s3Client.ListObjects(&s3.ListObjectsInput{ + Bucket: aws.String(s.config.Predastore.Bucket), + Delimiter: aws.String("/"), + }) + + if err != nil { + slog.Error("Failed to list S3 objects", "err", err) + return nil, errors.New(awserrors.ErrorServerInternal) + } + + // Iterate over CommonPrefixes to find vol-* directories + for _, prefix := range result.CommonPrefixes { + if prefix.Prefix == nil { + continue + } + + prefixStr := *prefix.Prefix + slog.Debug("Processing S3 prefix", "prefix", prefixStr) + + // Only check prefixes that match pattern: vol-/ + if !strings.HasPrefix(prefixStr, "vol-") { + continue + } + + // Extract volume ID from prefix (remove trailing slash) + volumeID := strings.TrimSuffix(prefixStr, "/") + + vol, err := s.getVolumeByID(volumeID) + if err != nil { + slog.Debug("Failed to get volume", "volumeId", volumeID, "err", err) + continue + } + + volumes = append(volumes, vol) + } + + slog.Info("DescribeVolumes completed", "count", len(volumes)) + + return &ec2.DescribeVolumesOutput{ + Volumes: volumes, + }, nil +} + +// fetchVolumesByIDs fetches multiple volumes in parallel by their IDs +func (s *VolumeServiceImpl) fetchVolumesByIDs(volumeIDs []*string) []*ec2.Volume { + var ( + volumes []*ec2.Volume + mu sync.Mutex + wg sync.WaitGroup + ) + + for _, volumeID := range volumeIDs { + if volumeID == nil { + continue + } + + wg.Add(1) + go func(volID string) { + defer wg.Done() + + vol, err := s.getVolumeByID(volID) + if err != nil { + slog.Debug("Volume not found", "volumeId", volID, "err", err) + return + } + + mu.Lock() + volumes = append(volumes, vol) + mu.Unlock() + }(*volumeID) + } + + wg.Wait() + return volumes +} + +// getVolumeByID fetches a single volume's config from S3 and builds an EC2 Volume +func (s *VolumeServiceImpl) getVolumeByID(volumeID string) (*ec2.Volume, error) { + configKey := volumeID + "/config.json" + + getResult, err := s.s3Client.GetObject(&s3.GetObjectInput{ + Bucket: aws.String(s.config.Predastore.Bucket), + Key: aws.String(configKey), + }) + if err != nil { + if aerr, ok := err.(awserr.Error); ok && (aerr.Code() == s3.ErrCodeNoSuchKey || aerr.Code() == "NotFound") { + slog.Debug("Config file not found", "key", configKey) + return nil, errors.New("volume not found") + } + slog.Debug("Failed to get config file", "key", configKey, "err", err) + return nil, err + } + defer getResult.Body.Close() + + body, err := io.ReadAll(getResult.Body) + if err != nil { + slog.Debug("Failed to read config body", "key", configKey, "err", err) + return nil, err + } + + var vbConfig struct { + VolumeConfig viperblock.VolumeConfig `json:"VolumeConfig"` + } + if err := json.Unmarshal(body, &vbConfig); err != nil { + slog.Debug("Failed to unmarshal config", "key", configKey, "err", err) + return nil, err + } + + volMeta := vbConfig.VolumeConfig.VolumeMetadata + + if volMeta.VolumeID == "" { + slog.Debug("Volume ID is empty in config", "key", configKey) + return nil, errors.New("volume ID is empty") + } + + state := volMeta.State + if state == "" { + state = "available" + } + volumeType := volMeta.VolumeType + if volumeType == "" { + volumeType = "gp3" + } + + volume := &ec2.Volume{ + VolumeId: aws.String(volMeta.VolumeID), + Size: aws.Int64(int64(volMeta.SizeGiB)), + State: aws.String(state), + AvailabilityZone: aws.String(volMeta.AvailabilityZone), + CreateTime: aws.Time(volMeta.CreatedAt), + VolumeType: aws.String(volumeType), + Encrypted: aws.Bool(volMeta.IsEncrypted), + } + + if volMeta.IOPS > 0 { + volume.Iops = aws.Int64(int64(volMeta.IOPS)) + } + + if volMeta.SnapshotID != "" { + volume.SnapshotId = aws.String(volMeta.SnapshotID) + } + + if volMeta.AttachedInstance != "" { + attachState := "attached" + if volMeta.State != "in-use" { + attachState = "detached" + } + volume.Attachments = []*ec2.VolumeAttachment{ + { + VolumeId: aws.String(volMeta.VolumeID), + InstanceId: aws.String(volMeta.AttachedInstance), + Device: aws.String(volMeta.DeviceName), + State: aws.String(attachState), + DeleteOnTermination: aws.Bool(true), + }, + } + } + + if len(volMeta.Tags) > 0 { + tags := make([]*ec2.Tag, 0, len(volMeta.Tags)) + for key, value := range volMeta.Tags { + tags = append(tags, &ec2.Tag{ + Key: aws.String(key), + Value: aws.String(value), + }) + } + volume.Tags = tags + } + + return volume, nil +} diff --git a/hive/handlers/ec2/volume/service_nats.go b/hive/handlers/ec2/volume/service_nats.go new file mode 100644 index 000000000..65ebf4d95 --- /dev/null +++ b/hive/handlers/ec2/volume/service_nats.go @@ -0,0 +1,51 @@ +package handlers_ec2_volume + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/mulgadc/hive/hive/utils" + "github.com/nats-io/nats.go" +) + +// NATSVolumeService handles volume operations via NATS messaging +type NATSVolumeService struct { + natsConn *nats.Conn +} + +// NewNATSVolumeService creates a new NATS-based volume service +func NewNATSVolumeService(conn *nats.Conn) VolumeService { + return &NATSVolumeService{natsConn: conn} +} + +// DescribeVolumes sends a request via NATS and waits for response +func (s *NATSVolumeService) DescribeVolumes(input *ec2.DescribeVolumesInput) (*ec2.DescribeVolumesOutput, error) { + // Marshal input to JSON + jsonData, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal input: %w", err) + } + + // Send NATS request with 30 second timeout + msg, err := s.natsConn.Request("ec2.DescribeVolumes", jsonData, 30*time.Second) + if err != nil { + return nil, fmt.Errorf("NATS request failed: %w", err) + } + + // Validate error response + responseError, err := utils.ValidateErrorPayload(msg.Data) + if err != nil { + return nil, fmt.Errorf("daemon returned error: %s", *responseError.Code) + } + + // Unmarshal successful response + var output ec2.DescribeVolumesOutput + err = json.Unmarshal(msg.Data, &output) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return &output, nil +} From e8e9b72cd3630bd3862415da1c60cb52c949bca9 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Tue, 20 Jan 2026 04:40:38 +0000 Subject: [PATCH 2/8] feat: e2e test --- INSTALL.md | 20 ++- Makefile | 38 +++++- scripts/start-dev.sh | 14 +- tests/e2e/Dockerfile.base | 19 +++ tests/e2e/Dockerfile.e2e | 45 +++++++ tests/e2e/run-e2e.sh | 263 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 386 insertions(+), 13 deletions(-) create mode 100644 tests/e2e/Dockerfile.base create mode 100644 tests/e2e/Dockerfile.e2e create mode 100644 tests/e2e/run-e2e.sh diff --git a/INSTALL.md b/INSTALL.md index 56723f983..64c2dd108 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,9 +6,17 @@ Notes for development environment installation. For running on Ubuntu 22.04, 24.04 and 25.10. +### Quick Install + +```bash +make quickinstall +``` + +### Manual Install + ```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 +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, recommended to install the latest directly from [https://go.dev/dl/](https://go.dev/dl/). @@ -22,10 +30,12 @@ go version Hive provides AWS API/SDK layer functionality, which requires the AWS CLI tool to be installed to interface with the system. ```bash -sudo apt install awscli +curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" +unzip awscliv2.zip +sudo ./aws/install ``` -Confirm awscli version > 2.0 is installed which is required by Hive. If your OS package manager installed an older version, e.g 1.X, install the latest version of the AWS CLI tool directly. +Confirm awscli version > 2.0 is installed which is required by Hive. [https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) @@ -33,10 +43,6 @@ Confirm awscli version > 2.0 is installed which is required by Hive. If your OS aws --version ``` -``` -aws-cli/2.32.4 -``` - ## Build Create the base directory for the Hive development environment. diff --git a/Makefile b/Makefile index 789aef24b..16abb4fcb 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,29 @@ run: clean: rm ./bin/$(GO_PROJECT_NAME) +quickinstall: + @echo "\n....Installing system dependencies...." + sudo apt-get update && sudo apt-get install -y \ + nbdkit nbdkit-plugin-dev pkg-config qemu-system-x86 qemu-utils qemu-kvm \ + libvirt-daemon-system libvirt-clients libvirt-dev make gcc jq curl \ + iproute2 netcat-openbsd openssh-client wget git unzip sudo xz-utils file + @echo "\n....Installing AWS CLI v2...." + @if ! command -v aws >/dev/null 2>&1; then \ + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"; \ + unzip -o awscliv2.zip; \ + sudo ./aws/install; \ + rm -rf awscliv2.zip aws/; \ + else \ + echo "AWS CLI already installed"; \ + fi + @echo "\n....Installing Go 1.25.5...." + @if [ ! -d "/usr/local/go" ]; then \ + curl -L https://go.dev/dl/go1.25.5.linux-amd64.tar.gz | sudo tar -C /usr/local -xz; \ + else \ + echo "Go already installed in /usr/local/go"; \ + fi + @echo "\nāœ… Quickinstall complete. Please ensure /usr/local/go/bin is in your PATH." + security: $(GOVULNCHECK) $(GOSECCHECK) $(GOSTATICCHECK) @echo "\n....Running security checks for $(GO_PROJECT_NAME)...." @@ -86,4 +109,17 @@ security: $(GOVULNCHECK) $(GOSECCHECK) $(GOSTATICCHECK) go vet ./... 2>&1 | tee tests/govet-report.txt || true @echo "Go vet report saved to tests/govet-report.txt" -.PHONY: go_build go_run build run test clean +e2e-test: + @echo "\n....Ensuring E2E Base Image exists...." + @if ! sudo docker image inspect hive-base:latest >/dev/null 2>&1; then \ + echo "Building hive-base:latest..."; \ + sudo docker build -t hive-base -f tests/e2e/Dockerfile.base .; \ + fi + @echo "\n....Removing old E2E image if it exists...." + -sudo docker rmi hive-e2e:latest 2>/dev/null || true + @echo "\n....Building E2E Docker image (building everything inside)...." + cd .. && sudo docker build -t hive-e2e -f hive/tests/e2e/Dockerfile.e2e . + @echo "\n....Running E2E Docker container...." + sudo docker run --privileged --rm -v /dev/kvm:/dev/kvm --name hive-e2e-test hive-e2e + +.PHONY: build go_build go_run test bench run clean quickinstall security e2e-test diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh index 05190c62d..76a598a08 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -36,7 +36,7 @@ fi if [ -d "/mnt/ramdisk" ]; then - + # Check if /mnt/ramdisk is mounted, if not mount it as tmpfs if ! mountpoint -q /mnt/ramdisk; then echo "šŸ’¾ Mounting /mnt/ramdisk as tmpfs" @@ -111,12 +111,16 @@ check_service() { done echo " āš ļø $name may not be responding on port $port (continuing anyway)" - + } # Pre-flight, compile latest -echo "1ļøāœˆļø Pre-flight, compiling latest..." -make build +if [ "$HIVE_SKIP_BUILD" != "true" ]; then + echo "1ļøāœˆļø Pre-flight, compiling latest..." + make build +else + echo "1ļøāœˆļø Skipping build (HIVE_SKIP_BUILD=true)" +fi # 1ļøāƒ£ Start NATS server echo "" @@ -268,4 +272,4 @@ echo "" # This will only be reached if air/daemon exits normally #echo "" -#echo "šŸ›‘ Hive development environment stopped" \ No newline at end of file +#echo "šŸ›‘ Hive development environment stopped" diff --git a/tests/e2e/Dockerfile.base b/tests/e2e/Dockerfile.base new file mode 100644 index 000000000..38fc02f07 --- /dev/null +++ b/tests/e2e/Dockerfile.base @@ -0,0 +1,19 @@ +FROM ubuntu:24.04 + +# Avoid prompts +ENV DEBIAN_FRONTEND=noninteractive + +# Install make first so we can use the Makefile +RUN apt-get update && apt-get install -y make sudo && rm -rf /var/lib/apt/lists/* + +# Set up working directory +WORKDIR /app/hive + +# Copy only the Makefile to run quickinstall +COPY Makefile . + +# Run the quickinstall target from the Makefile +RUN make quickinstall + +# Ensure Go is in the PATH +ENV PATH="/usr/local/go/bin:${PATH}" diff --git a/tests/e2e/Dockerfile.e2e b/tests/e2e/Dockerfile.e2e new file mode 100644 index 000000000..8caee7037 --- /dev/null +++ b/tests/e2e/Dockerfile.e2e @@ -0,0 +1,45 @@ +# syntax=docker/dockerfile:1 +# This image assumes hive-base:latest exists locally or in a registry +FROM hive-base:latest + +# Set up working directory structure to match INSTALL.md +WORKDIR /root/Development/mulga + +# 1. Pre-copy dependency manifests +# We copy these first to create a dedicated dependency layer +COPY hive/go.mod hive/go.sum hive/go.work hive/go.work.sum ./hive/ +COPY viperblock/go.mod viperblock/go.sum ./viperblock/ +COPY viperblock/nbd/go.mod viperblock/nbd/go.sum ./viperblock/nbd/ +COPY viperblock/nbd/libguestfs.org/nbdkit/go.mod ./viperblock/nbd/libguestfs.org/nbdkit/ +COPY predastore/go.mod predastore/go.sum ./predastore/ + +# 2. Download dependencies (Using Cache Mount) +RUN --mount=type=cache,target=/root/go/pkg/mod \ + (cd hive && go mod download) && \ + (cd viperblock && go mod download) && \ + (cd viperblock/nbd && go mod download) && \ + (cd viperblock/nbd/libguestfs.org/nbdkit && go mod download) && \ + (cd predastore && go mod download) + +# 3. Copy the rest of the source code +COPY hive/ ./hive/ +COPY viperblock/ ./viperblock/ +COPY predastore/ ./predastore/ + +# Set working directory to hive +WORKDIR /root/Development/mulga/hive + +# Ensure scripts are executable +RUN chmod +x ./scripts/*.sh ./tests/e2e/run-e2e.sh + +# 4. Build everything +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/root/go/pkg/mod \ + ./scripts/dev-setup.sh + +# Set environment variables for runtime +ENV PATH="/root/Development/mulga/hive/bin:${PATH}" +ENV HOME="/root" + +# Run the E2E test runner +CMD ["./tests/e2e/run-e2e.sh"] diff --git a/tests/e2e/run-e2e.sh b/tests/e2e/run-e2e.sh new file mode 100644 index 000000000..0219a6650 --- /dev/null +++ b/tests/e2e/run-e2e.sh @@ -0,0 +1,263 @@ +#!/bin/bash +set -ex + +# Use Hive profile +export AWS_PROFILE=hive + +# Ensure we are in the project root +cd "$(dirname "$0")/../.." + +# Phase 1: Environment Setup +echo "Phase 1: Environment Setup" + +# Check for KVM support inside the container +echo "Checking for KVM support..." +if [ -e /dev/kvm ]; then + echo "āœ… /dev/kvm exists" + if [ -w /dev/kvm ]; then + echo "āœ… /dev/kvm is writable" + else + echo "āŒ /dev/kvm is NOT writable. QEMU will fail." + exit 1 + fi +else + echo "āŒ /dev/kvm does NOT exist. Ensure --privileged and -v /dev/kvm:/dev/kvm are used." + exit 1 +fi + +./bin/hive admin init --region ap-southeast-2 --az ap-southeast-2a --node node1 --nodes 1 + +# Start all services +# Ensure logs directory exists for start-dev.sh +mkdir -p ~/hive/logs +HIVE_SKIP_BUILD=true ./scripts/start-dev.sh + +# Wait for health checks on https://localhost:9999 (AWS Gateway) +echo "Waiting for AWS Gateway..." +MAX_RETRIES=30 +COUNT=0 +# Using -k because of self-signed certs +until curl -k -s https://localhost:9999 > /dev/null || [ $COUNT -eq $MAX_RETRIES ]; do + echo "Waiting for gateway... ($COUNT/$MAX_RETRIES)" + sleep 2 + COUNT=$((COUNT + 1)) +done + +if [ $COUNT -eq $MAX_RETRIES ]; then + echo "Gateway failed to start" + ./scripts/stop-dev.sh + exit 1 +fi + +# Define common AWS CLI args +# Use --endpoint-url and --no-verify-ssl to be safe in the container environment +# Suppress InsecureRequestWarning from urllib3 +export PYTHONWARNINGS="ignore:Unverified HTTPS request" +AWS_EC2="aws --endpoint-url https://localhost:9999 --no-verify-ssl ec2" + +# Phase 2: Discovery & Metadata +echo "Phase 2: Discovery & Metadata" +# Verify describe-regions (just ensure it returns at least one region) +$AWS_EC2 describe-regions | jq -e '.Regions | length > 0' + +# Discover available instance types from Hive +# Hive generates these based on the host CPU (e.g., m7i.micro, m8g.small, etc.) +echo "Discovering available instance types..." +AVAILABLE_TYPES=$($AWS_EC2 describe-instance-types --query 'InstanceTypes[*].InstanceType' --output text) +echo "Available instance types: $AVAILABLE_TYPES" + +# Pick the first one as our primary test type +INSTANCE_TYPE=$(echo $AVAILABLE_TYPES | awk '{print $1}') +if [ -z "$INSTANCE_TYPE" ] || [ "$INSTANCE_TYPE" == "None" ]; then + echo "No instance types found!" + exit 1 +fi +echo "Selected instance type for test: $INSTANCE_TYPE" + +# Get architecture for the selected instance type +ARCH=$($AWS_EC2 describe-instance-types --instance-types "$INSTANCE_TYPE" --query 'InstanceTypes[0].ProcessorInfo.SupportedArchitectures[0]' --output text) +echo "Detected architecture for $INSTANCE_TYPE: $ARCH" + +# Verify describe-instance-types (ensure the chosen type is available) +$AWS_EC2 describe-instance-types | jq -e ".InstanceTypes[] | select(.InstanceType==\"$INSTANCE_TYPE\")" + +# Phase 3: SSH Key Management +echo "Phase 3: SSH Key Management" +# Create test-key-1 (create-key-pair) and verify private key material is returned +KEY_MATERIAL=$($AWS_EC2 create-key-pair --key-name test-key-1 --query 'KeyMaterial' --output text) +if [ -z "$KEY_MATERIAL" ] || [ "$KEY_MATERIAL" == "None" ]; then + echo "Failed to create key pair test-key-1" + exit 1 +fi +echo "$KEY_MATERIAL" > test-key-1.pem +chmod 600 test-key-1.pem + +# Generate a local RSA key and import it as test-key-2 (import-key-pair) +ssh-keygen -t rsa -b 2048 -f test-key-2-local -N "" +$AWS_EC2 import-key-pair --key-name test-key-2 --public-key-material "fileb://test-key-2-local.pub" + +# Verify both keys are present (describe-key-pairs) +$AWS_EC2 describe-key-pairs --query 'KeyPairs[*].KeyName' --output text | grep test-key-1 +$AWS_EC2 describe-key-pairs --query 'KeyPairs[*].KeyName' --output text | grep test-key-2 + +# Delete test-key-2 (delete-key-pair) and verify only one remains +$AWS_EC2 delete-key-pair --key-name test-key-2 +REMAINING_KEYS=$($AWS_EC2 describe-key-pairs --query 'KeyPairs[*].KeyName' --output text) +echo "Remaining keys: $REMAINING_KEYS" +echo "$REMAINING_KEYS" | grep test-key-1 +if echo "$REMAINING_KEYS" | grep -q test-key-2; then + echo "test-key-2 was not deleted" + exit 1 +fi + +# Phase 4: Image Management +echo "Phase 4: Image Management" +# Detect correct image name based on architecture +if [ "$ARCH" = "x86_64" ]; then + IMAGE_NAME="ubuntu-24.04-x86_64" +else + IMAGE_NAME="ubuntu-24.04-arm64" +fi +echo "Using image: $IMAGE_NAME" + +# Import a reliable Ubuntu image and capture the AMI ID from the output +echo "Importing image $IMAGE_NAME..." +echo "May appear stalled here, just takes a while to import..." +IMPORT_LOG=$(./bin/hive admin images import --name "$IMAGE_NAME" --force) +AMI_ID=$(echo "$IMPORT_LOG" | grep -o 'ami-[a-z0-9]\+') + +if [ -z "$AMI_ID" ]; then + echo "Failed to capture AMI ID from import command" + ./scripts/stop-dev.sh + exit 1 +fi +echo "Captured AMI ID: $AMI_ID" + +# Verify the AMI exists using its ID (describe-images) +echo "Verifying AMI availability..." +$AWS_EC2 describe-images --image-ids "$AMI_ID" | jq -e ".Images[0] | select(.ImageId==\"$AMI_ID\")" + +# Phase 5: Instance Lifecycle +# echo "Phase 5: Instance Lifecycle" +# # Launch a VM (run-instances) +# INSTANCE_ID=$($AWS_EC2 run-instances --image-id "$AMI_ID" --instance-type "$INSTANCE_TYPE" --key-name test-key-1 --query 'Instances[0].InstanceId' --output text) +# if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" == "None" ]; then +# echo "Failed to launch instance" +# ./scripts/stop-dev.sh +# exit 1 +# fi +# echo "Launched Instance ID: $INSTANCE_ID" + +# # Poll until state is running (describe-instances) +# echo "Polling for instance running state..." +# COUNT=0 +# STATE="unknown" +# while [ $COUNT -lt 60 ]; do +# # Capture full output to check if instance even exists in the response +# DESCRIBE_OUTPUT=$($AWS_EC2 describe-instances --instance-ids "$INSTANCE_ID") || { +# echo "āš ļø Gateway request failed, retrying... ($COUNT/60)" +# sleep 5 +# COUNT=$((COUNT + 1)) +# continue +# } + +# if [ -z "$DESCRIBE_OUTPUT" ]; then +# echo "āš ļø Gateway returned empty response, retrying..." +# sleep 5 +# COUNT=$((COUNT + 1)) +# continue +# fi + +# # Extract state using jq +# STATE=$(echo "$DESCRIBE_OUTPUT" | jq -r '.Reservations[0].Instances[0].State.Name // "not-found"') + +# echo "Instance state: $STATE" +# if [ "$STATE" == "running" ]; then +# break +# fi + +# if [ "$STATE" == "terminated" ]; then +# echo "āŒ Instance terminated unexpectedly!" +# ./scripts/stop-dev.sh +# exit 1 +# fi + +# sleep 5 +# COUNT=$((COUNT + 1)) +# done + +# if [ "$STATE" != "running" ]; then +# echo "Instance failed to reach running state" +# ./scripts/stop-dev.sh +# exit 1 +# fi + +# # Verify root volume attached to the instance (describe-volumes) +# VOLUME_ID=$($AWS_EC2 describe-volumes --query 'Volumes[0].VolumeId' --output text) +# if [ -z "$VOLUME_ID" ] || [ "$VOLUME_ID" == "None" ]; then +# echo "Failed to find volume for instance $INSTANCE_ID" +# ./scripts/stop-dev.sh +# exit 1 +# fi +# echo "Volume ID: $VOLUME_ID" + +# # Stop instance (stop-instances) and verify transition to stopped (describe-instances) +# echo "Stopping instance..." +# $AWS_EC2 stop-instances --instance-ids "$INSTANCE_ID" +# COUNT=0 +# while [ $COUNT -lt 30 ]; do +# STATE=$($AWS_EC2 describe-instances --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].State.Name' --output text) +# echo "Instance state: $STATE" +# if [ "$STATE" == "stopped" ]; then +# break +# fi +# sleep 5 +# COUNT=$((COUNT + 1)) +# done + +# if [ "$STATE" != "stopped" ]; then +# echo "Instance failed to reach stopped state" +# ./scripts/stop-dev.sh +# exit 1 +# fi + +# # Start instance (start-instances) and verify transition back to running (describe-instances) +# echo "Starting instance..." +# $AWS_EC2 start-instances --instance-ids "$INSTANCE_ID" +# COUNT=0 +# while [ $COUNT -lt 30 ]; do +# STATE=$($AWS_EC2 describe-instances --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].State.Name' --output text) +# echo "Instance state: $STATE" +# if [ "$STATE" == "running" ]; then +# break +# fi +# sleep 5 +# COUNT=$((COUNT + 1)) +# done + +# if [ "$STATE" != "running" ]; then +# echo "Instance failed to reach running state after restart" +# ./scripts/stop-dev.sh +# exit 1 +# fi + +# # Terminate instance (terminate-instances) and verify termination (describe-instances) +# echo "Terminating instance..." +# $AWS_EC2 terminate-instances --instance-ids "$INSTANCE_ID" +# COUNT=0 +# while [ $COUNT -lt 30 ]; do +# STATE=$($AWS_EC2 describe-instances --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].State.Name' --output text) +# echo "Instance state: $STATE" +# if [ "$STATE" == "terminated" ] || [ "$STATE" == "None" ]; then +# break +# fi +# sleep 5 +# COUNT=$((COUNT + 1)) +# done + +# Phase 6: Cleanup (Inside Container) +echo "Phase 6: Cleanup" +./scripts/stop-dev.sh + +echo "E2E Test Completed Successfully" +exit 0 From 217fcf221a015fc642adbce6cdea4ccbffc4fdd2 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Wed, 21 Jan 2026 01:34:04 +0000 Subject: [PATCH 3/8] chore: use go tool for linters --- .github/workflows/hive.yml | 26 +++---- Makefile | 47 +++--------- go.mod | 58 ++++++++++++-- go.sum | 152 +++++++++++++++++++++++++++++++------ 4 files changed, 203 insertions(+), 80 deletions(-) diff --git a/.github/workflows/hive.yml b/.github/workflows/hive.yml index 9d9058a8e..d6e68dfe1 100644 --- a/.github/workflows/hive.yml +++ b/.github/workflows/hive.yml @@ -9,17 +9,18 @@ on: jobs: hive_build_and_test: - runs-on: ubuntu-latest + name: Hive Build and Test + runs-on: ubuntu-24.04 steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version-file: "go.mod" cache-dependency-path: "go.sum" + go-version-file: "go.mod" - name: Run Tests run: LOG_IGNORE=1 go test -v -timeout 120s ./... @@ -28,18 +29,13 @@ jobs: run: go vet ./... - name: Static Check - uses: dominikh/staticcheck-action@v1 - with: - version: 2025.1.1 - install-go: false - checks: "all,-ST1000,-ST1003,-ST1016,-ST1020,-ST1021,-ST1022,-SA1019,-SA9005" + run: go tool staticcheck -checks="all,-ST1000,-ST1003,-ST1016,-ST1020,-ST1021,-ST1022,-SA1019,-SA9005" ./... - name: Go Vulnerability Check - uses: golang/govulncheck-action@v1 - with: - repo-checkout: false + run: go tool govulncheck ./... # - name: Gosec Security Scanner - # run: | - # go install github.com/securego/gosec/v2/cmd/gosec@latest - # gosec -exclude-generated ./... + # run: go tool gosec -exclude-generated ./... + + - name: Build + run: make go_build diff --git a/Makefile b/Makefile index 16abb4fcb..fffda0e3c 100644 --- a/Makefile +++ b/Makefile @@ -12,35 +12,6 @@ else GO_BUILD_MOD := endif -# Where to install Go tools -GOBIN ?= $(shell go env GOBIN) -ifeq ($(GOBIN),) - GOBIN := $(shell go env GOPATH)/bin -endif - -# Where to install Go tools -GOVULNCHECK := $(GOBIN)/govulncheck - -# Install govulncheck only if the binary is missing / out of date -$(GOVULNCHECK): - go install golang.org/x/vuln/cmd/govulncheck@latest - -GOSECCHECK := $(GOBIN)/gosec - -# Install gosec only if the binary is missing / out of date -$(GOSECCHECK): - go install github.com/securego/gosec/v2/cmd/gosec@latest - -GOSTATICCHECK := $(GOBIN)/staticcheck - -# Install govulncheck only if the binary is missing / out of date -$(GOSTATICCHECK): - go install honnef.co/go/tools/cmd/staticcheck@latest - -# Install govulncheck only if the binary is missing / out of date -$(GOVULNCHECK): - go install golang.org/x/vuln/cmd/govulncheck@latest - build: $(MAKE) go_build @@ -53,10 +24,9 @@ go_run: @echo "\n....Running $(GO_PROJECT_NAME)...." $(GOPATH)/bin/$(GO_PROJECT_NAME) -test: $(GOVULNCHECK) +test: @echo "\n....Running tests for $(GO_PROJECT_NAME)...." LOG_IGNORE=1 go test -v -timeout 120s ./... - $(GOVULNCHECK) ./... bench: @echo "\n....Running benchmarks for $(GO_PROJECT_NAME)...." @@ -76,6 +46,7 @@ quickinstall: nbdkit nbdkit-plugin-dev pkg-config qemu-system-x86 qemu-utils qemu-kvm \ libvirt-daemon-system libvirt-clients libvirt-dev make gcc jq curl \ iproute2 netcat-openbsd openssh-client wget git unzip sudo xz-utils file + @echo "\n....Installing AWS CLI v2...." @if ! command -v aws >/dev/null 2>&1; then \ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"; \ @@ -85,25 +56,26 @@ quickinstall: else \ echo "AWS CLI already installed"; \ fi + @echo "\n....Installing Go 1.25.5...." @if [ ! -d "/usr/local/go" ]; then \ curl -L https://go.dev/dl/go1.25.5.linux-amd64.tar.gz | sudo tar -C /usr/local -xz; \ else \ echo "Go already installed in /usr/local/go"; \ fi + @echo "\nāœ… Quickinstall complete. Please ensure /usr/local/go/bin is in your PATH." -security: $(GOVULNCHECK) $(GOSECCHECK) $(GOSTATICCHECK) +security: @echo "\n....Running security checks for $(GO_PROJECT_NAME)...." - $(GOVULNCHECK) ./... > tests/govulncheck-report.txt || true + go tool govulncheck ./... > tests/govulncheck-report.txt || true @echo "Govulncheck report saved to tests/govulncheck-report.txt" - $(GOSECCHECK) ./... > tests/gosec-report.txt || true + go tool gosec ./... > tests/gosec-report.txt || true @echo "Gosec report saved to tests/gosec-report.txt" - # default config + disable dep warning since we are using aws sdk v1 - $(GOSTATICCHECK) -checks="all,-ST1000,-ST1003,-ST1016,-ST1020,-ST1021,-ST1022,-SA1019,-SA9005" ./... > tests/staticcheck-report.txt || true + go tool staticcheck -checks="all,-ST1000,-ST1003,-ST1016,-ST1020,-ST1021,-ST1022,-SA1019,-SA9005" ./... > tests/staticcheck-report.txt || true @echo "Staticcheck report saved to tests/staticcheck-report.txt" go vet ./... 2>&1 | tee tests/govet-report.txt || true @@ -115,10 +87,13 @@ e2e-test: echo "Building hive-base:latest..."; \ sudo docker build -t hive-base -f tests/e2e/Dockerfile.base .; \ fi + @echo "\n....Removing old E2E image if it exists...." -sudo docker rmi hive-e2e:latest 2>/dev/null || true + @echo "\n....Building E2E Docker image (building everything inside)...." cd .. && sudo docker build -t hive-e2e -f hive/tests/e2e/Dockerfile.e2e . + @echo "\n....Running E2E Docker container...." sudo docker run --privileged --rm -v /dev/kvm:/dev/kvm --name hive-e2e-test hive-e2e diff --git a/go.mod b/go.mod index d7c8ee9e0..d6341c9cc 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/pterm/pterm v0.12.82 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 go.uber.org/automaxprocs v1.6.0 gopkg.in/ini.v1 v1.67.0 ) @@ -24,13 +24,27 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect + cloud.google.com/go v0.121.2 // indirect + cloud.google.com/go/auth v0.16.5 // indirect + cloud.google.com/go/compute/metadata v0.8.0 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/andybalholm/brotli v1.1.1 // indirect + github.com/anthropics/anthropic-sdk-go v1.19.0 // indirect + github.com/ccojocar/zxcvbn-go v1.0.4 // indirect github.com/containerd/console v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-tpm v0.9.5 // indirect - github.com/gookit/color v1.5.4 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/gookit/color v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -43,24 +57,56 @@ require ( github.com/nats-io/jwt/v2 v2.7.4 // indirect github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/openai/openai-go/v3 v3.8.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/securego/gosec/v2 v2.22.11 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.57.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.26.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools/go/expect v0.1.1-deprecated // indirect + golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect + golang.org/x/vuln v1.1.4 // indirect + google.golang.org/genai v1.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect + google.golang.org/grpc v1.75.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + honnef.co/go/tools v0.6.1 // indirect +) + +tool ( + github.com/securego/gosec/v2/cmd/gosec + golang.org/x/vuln/cmd/govulncheck + honnef.co/go/tools/cmd/staticcheck ) diff --git a/go.sum b/go.sum index 390cdeccb..eb12eb344 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,14 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +cloud.google.com/go v0.121.2 h1:v2qQpN6Dx9x2NmwrqlesOt3Ys4ol5/lFZ6Mg1B7OJCg= +cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -15,13 +23,19 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/ github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/anthropics/anthropic-sdk-go v1.19.0 h1:mO6E+ffSzLRvR/YUH9KJC0uGw0uV8GjISIuzem//3KE= +github.com/anthropics/anthropic-sdk-go v1.19.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE= github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op h1:+OSa/t11TFhqfrX0EOSqQBDJ0YlpmK0rDSiB19dg9M0= github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= +github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= @@ -29,24 +43,51 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU= +github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= +github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= -github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= -github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= +github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -85,10 +126,6 @@ github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/mulgadc/predastore v0.1.5 h1:7SySkQQ5MQ1NTcKcB14pCUIJipq0YVgCJZeKM6ytrLM= github.com/mulgadc/predastore v0.1.5/go.mod h1:BhMymRV3zMTu2S+5pJnNDDm5+AGakjhSc/PKX0q8Xfg= -github.com/mulgadc/viperblock v0.1.2 h1:h5yAha2CIsSp/x7oPvqByqlsYzZYSLLim15AL1o7Dm0= -github.com/mulgadc/viperblock v0.1.2/go.mod h1:zCjdQ76fi2Qs2a5ign9A/Uqgbz77xts5WV0OR1be48I= -github.com/mulgadc/viperblock v0.1.3-0.20260116051939-cca6f6a8a562 h1:uXQUXuHsnZZW0NBYiv6bFDRKIluWKKpQGPrfsDJme9k= -github.com/mulgadc/viperblock v0.1.3-0.20260116051939-cca6f6a8a562/go.mod h1:eFolZVfX4iBFBnHAfOoYILfiIjNoI/NhmKht3OIPRR8= github.com/mulgadc/viperblock v0.1.3 h1:xooK1uTUI0RLK23PHAuVInirC3TrBo6/6ezkt+1vMJo= github.com/mulgadc/viperblock v0.1.3/go.mod h1:eFolZVfX4iBFBnHAfOoYILfiIjNoI/NhmKht3OIPRR8= github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI= @@ -101,6 +138,12 @@ github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/openai/openai-go/v3 v3.8.1 h1:b+YWsmwqXnbpSHWQEntZAkKciBZ5CJXwL68j+l59UDg= +github.com/openai/openai-go/v3 v3.8.1/go.mod h1:UOpNxkqC9OdNXNUfpNByKOtB4jAL0EssQXq5p8gO0Xs= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -119,11 +162,15 @@ github.com/pterm/pterm v0.12.82/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/Tak github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/securego/gosec/v2 v2.22.11 h1:tW+weM/hCM/GX3iaCV91d5I6hqaRT2TPsFM1+USPXwg= +github.com/securego/gosec/v2 v2.22.11/go.mod h1:KE4MW/eH0GLWztkbt4/7XpyH0zJBBnu7sYB4l6Wn7Mw= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -139,14 +186,26 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg= @@ -159,27 +218,51 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= +golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -194,32 +277,53 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= +golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= +golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genai v1.37.0 h1:dgp71k1wQ+/+APdZrN3LFgAGnVnr5IdTF1Oj0Dg+BQc= +google.golang.org/genai v1.37.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -230,3 +334,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= +honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= From 72b706a00d1ee989d77e4ea5063a0bda8050b480 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Wed, 21 Jan 2026 01:48:31 +0000 Subject: [PATCH 4/8] remove paths in action --- .github/workflows/hive.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/hive.yml b/.github/workflows/hive.yml index d6e68dfe1..078424326 100644 --- a/.github/workflows/hive.yml +++ b/.github/workflows/hive.yml @@ -3,7 +3,6 @@ name: Build and Test Hive on: push: branches: [ main ] - paths: ["hive/**", ".github/workflows/**", "go.mod", "go.sum"] pull_request: workflow_dispatch: From d29069db830fe9e25bb2413c8e26c78f075da613 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Wed, 21 Jan 2026 04:02:20 +0000 Subject: [PATCH 5/8] fix: match new viperblock err type --- cmd/hive/cmd/admin.go | 4 ++-- go.mod | 2 +- go.sum | 2 ++ hive/handlers/ec2/instance/service_impl.go | 2 +- hive/handlers/ec2/key/service_impl.go | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/hive/cmd/admin.go b/cmd/hive/cmd/admin.go index adf9b16ae..7415342a9 100644 --- a/cmd/hive/cmd/admin.go +++ b/cmd/hive/cmd/admin.go @@ -36,7 +36,7 @@ import ( "github.com/mulgadc/hive/hive/utils" "github.com/mulgadc/viperblock/viperblock" "github.com/mulgadc/viperblock/viperblock/backends/s3" - vutils "github.com/mulgadc/viperblock/viperblock/utils" + "github.com/mulgadc/viperblock/viperblock/v_utils" "github.com/pelletier/go-toml/v2" "github.com/pterm/pterm" "github.com/spf13/cobra" @@ -409,7 +409,7 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) { VolumeConfig: manifest, } - err = vutils.ImportDiskImage(&s3Config, &vbConfig, extractedImagePath) + err = v_utils.ImportDiskImage(&s3Config, &vbConfig, extractedImagePath) if err != nil { fmt.Fprintf(os.Stderr, "Could not import image to predastore: %v\n", err) diff --git a/go.mod b/go.mod index d6341c9cc..ee5126269 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/uuid v1.6.0 github.com/kdomanski/iso9660 v0.4.0 github.com/mulgadc/predastore v0.1.5 - github.com/mulgadc/viperblock v0.1.3 + github.com/mulgadc/viperblock v0.1.4 github.com/nats-io/nats-server/v2 v2.11.6 github.com/nats-io/nats.go v1.43.0 github.com/pelletier/go-toml/v2 v2.2.4 diff --git a/go.sum b/go.sum index eb12eb344..e7b383e78 100644 --- a/go.sum +++ b/go.sum @@ -128,6 +128,8 @@ github.com/mulgadc/predastore v0.1.5 h1:7SySkQQ5MQ1NTcKcB14pCUIJipq0YVgCJZeKM6yt github.com/mulgadc/predastore v0.1.5/go.mod h1:BhMymRV3zMTu2S+5pJnNDDm5+AGakjhSc/PKX0q8Xfg= github.com/mulgadc/viperblock v0.1.3 h1:xooK1uTUI0RLK23PHAuVInirC3TrBo6/6ezkt+1vMJo= github.com/mulgadc/viperblock v0.1.3/go.mod h1:eFolZVfX4iBFBnHAfOoYILfiIjNoI/NhmKht3OIPRR8= +github.com/mulgadc/viperblock v0.1.4 h1:YnRHEC6XNbhnSk6S+ET3X7DTMRezdO3o1O3nO/R9Syg= +github.com/mulgadc/viperblock v0.1.4/go.mod h1:JNooDHb6ZuPvNPWaf0gChlft/D8xs5jHJFln+P3B9lQ= github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI= github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= github.com/nats-io/nats-server/v2 v2.11.6 h1:4VXRjbTUFKEB+7UoaKL3F5Y83xC7MxPoIONOnGgpkHw= diff --git a/hive/handlers/ec2/instance/service_impl.go b/hive/handlers/ec2/instance/service_impl.go index 271dea505..0f498367f 100644 --- a/hive/handlers/ec2/instance/service_impl.go +++ b/hive/handlers/ec2/instance/service_impl.go @@ -346,7 +346,7 @@ func (s *InstanceServiceImpl) cloneAMIToVolume(input *ec2.RunInstancesInput, siz // Read 1MB data, err := amiVb.ReadAt(block*uint64(destVb.BlockSize), uint64(destVb.BlockSize)*1024) - if err != nil && err != viperblock.ZeroBlock { + if err != nil && err != viperblock.ErrZeroBlock { slog.Error("Failed to read block from AMI source", "err", err) return errors.New(awserrors.ErrorServerInternal) } diff --git a/hive/handlers/ec2/key/service_impl.go b/hive/handlers/ec2/key/service_impl.go index a273abef2..2a35027fa 100644 --- a/hive/handlers/ec2/key/service_impl.go +++ b/hive/handlers/ec2/key/service_impl.go @@ -242,7 +242,7 @@ func formatFingerprint(hash []byte, algorithm string) string { // generateKeyPairID generates a unique key pair ID (similar to AWS key-xxxxx format) func generateKeyPairID() string { hash := sha1.New() //#nosec G401 - need sha1 for AWS compatibility - hash.Write([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))) + hash.Write(fmt.Appendf(nil, "%d", time.Now().UnixNano())) return fmt.Sprintf("%x", hash.Sum(nil))[:16] } From ca3427d64b002c26fcf67d681be252b2d1f1bc3b Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:37:45 +1100 Subject: [PATCH 6/8] feat: E2E tests in GitHub Actions --- .github/workflows/e2e.yml | 43 +++++++++++++++++++++++++++++++++++ Makefile | 42 +++++++++++++--------------------- compute/geniso.sh | 0 compute/nbdkit.sh | 0 compute/vm-nbd-daemon.sh | 0 go.mod | 2 -- go.sum | 2 -- scripts/start-multi-nats.sh | 0 tests/e2e/Dockerfile.base | 19 ---------------- tests/e2e/Dockerfile.e2e | 35 +++++++++++++++++++++-------- tests/e2e/run-e2e.sh | 45 ++++++++++++++++++------------------- 11 files changed, 106 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/e2e.yml mode change 100644 => 100755 compute/geniso.sh mode change 100644 => 100755 compute/nbdkit.sh mode change 100644 => 100755 compute/vm-nbd-daemon.sh mode change 100644 => 100755 scripts/start-multi-nats.sh delete mode 100644 tests/e2e/Dockerfile.base mode change 100644 => 100755 tests/e2e/run-e2e.sh diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..12e7c6a50 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,43 @@ +name: E2E Test Hive + +on: + push: + branches: [ main, e2e ] + workflow_dispatch: + +jobs: + hive_e2e_test: + name: Hive E2E Test + runs-on: self-hosted + + steps: + - name: Checkout Code + uses: actions/checkout@v6 + with: + path: hive + + - name: Clone Dependencies + run: ./hive/scripts/clone-deps.sh + + - name: Cleanup Stale Containers + run: docker rm -f hive-e2e-test 2>/dev/null || true + + - name: Build E2E Image + run: | + docker build \ + -t hive-e2e:latest \ + -f hive/tests/e2e/Dockerfile.e2e \ + . + + - name: Run E2E Tests + run: | + docker run --privileged --rm \ + -v /dev/kvm:/dev/kvm \ + --name hive-e2e-test \ + hive-e2e:latest + + - name: Cleanup + if: always() + run: | + docker rm -f hive-e2e-test 2>/dev/null || true + docker image prune -f diff --git a/Makefile b/Makefile index fffda0e3c..ea275d3c4 100644 --- a/Makefile +++ b/Makefile @@ -40,30 +40,34 @@ run: clean: rm ./bin/$(GO_PROJECT_NAME) -quickinstall: +install-system: @echo "\n....Installing system dependencies...." - sudo apt-get update && sudo apt-get install -y \ + apt-get update && apt-get install -y \ nbdkit nbdkit-plugin-dev pkg-config qemu-system-x86 qemu-utils qemu-kvm \ libvirt-daemon-system libvirt-clients libvirt-dev make gcc jq curl \ iproute2 netcat-openbsd openssh-client wget git unzip sudo xz-utils file +install-go: + @echo "\n....Installing Go 1.25.5...." + @if [ ! -d "/usr/local/go" ]; then \ + curl -L https://go.dev/dl/go1.25.5.linux-amd64.tar.gz | tar -C /usr/local -xz; \ + else \ + echo "Go already installed in /usr/local/go"; \ + fi + @echo "Go version: $$(go version)" + +install-aws: @echo "\n....Installing AWS CLI v2...." @if ! command -v aws >/dev/null 2>&1; then \ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"; \ unzip -o awscliv2.zip; \ - sudo ./aws/install; \ + ./aws/install; \ rm -rf awscliv2.zip aws/; \ else \ echo "AWS CLI already installed"; \ fi - @echo "\n....Installing Go 1.25.5...." - @if [ ! -d "/usr/local/go" ]; then \ - curl -L https://go.dev/dl/go1.25.5.linux-amd64.tar.gz | sudo tar -C /usr/local -xz; \ - else \ - echo "Go already installed in /usr/local/go"; \ - fi - +quickinstall: install-system install-go install-aws @echo "\nāœ… Quickinstall complete. Please ensure /usr/local/go/bin is in your PATH." security: @@ -81,20 +85,4 @@ security: go vet ./... 2>&1 | tee tests/govet-report.txt || true @echo "Go vet report saved to tests/govet-report.txt" -e2e-test: - @echo "\n....Ensuring E2E Base Image exists...." - @if ! sudo docker image inspect hive-base:latest >/dev/null 2>&1; then \ - echo "Building hive-base:latest..."; \ - sudo docker build -t hive-base -f tests/e2e/Dockerfile.base .; \ - fi - - @echo "\n....Removing old E2E image if it exists...." - -sudo docker rmi hive-e2e:latest 2>/dev/null || true - - @echo "\n....Building E2E Docker image (building everything inside)...." - cd .. && sudo docker build -t hive-e2e -f hive/tests/e2e/Dockerfile.e2e . - - @echo "\n....Running E2E Docker container...." - sudo docker run --privileged --rm -v /dev/kvm:/dev/kvm --name hive-e2e-test hive-e2e - -.PHONY: build go_build go_run test bench run clean quickinstall security e2e-test +.PHONY: build go_build go_run test bench run clean install-system install-go install-aws quickinstall security diff --git a/compute/geniso.sh b/compute/geniso.sh old mode 100644 new mode 100755 diff --git a/compute/nbdkit.sh b/compute/nbdkit.sh old mode 100644 new mode 100755 diff --git a/compute/vm-nbd-daemon.sh b/compute/vm-nbd-daemon.sh old mode 100644 new mode 100755 diff --git a/go.mod b/go.mod index ee5126269..755932545 100644 --- a/go.mod +++ b/go.mod @@ -94,8 +94,6 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.39.0 // indirect - golang.org/x/tools/go/expect v0.1.1-deprecated // indirect - golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect golang.org/x/vuln v1.1.4 // indirect google.golang.org/genai v1.37.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect diff --git a/go.sum b/go.sum index e7b383e78..6f2c3b9a0 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,6 @@ github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/mulgadc/predastore v0.1.5 h1:7SySkQQ5MQ1NTcKcB14pCUIJipq0YVgCJZeKM6ytrLM= github.com/mulgadc/predastore v0.1.5/go.mod h1:BhMymRV3zMTu2S+5pJnNDDm5+AGakjhSc/PKX0q8Xfg= -github.com/mulgadc/viperblock v0.1.3 h1:xooK1uTUI0RLK23PHAuVInirC3TrBo6/6ezkt+1vMJo= -github.com/mulgadc/viperblock v0.1.3/go.mod h1:eFolZVfX4iBFBnHAfOoYILfiIjNoI/NhmKht3OIPRR8= github.com/mulgadc/viperblock v0.1.4 h1:YnRHEC6XNbhnSk6S+ET3X7DTMRezdO3o1O3nO/R9Syg= github.com/mulgadc/viperblock v0.1.4/go.mod h1:JNooDHb6ZuPvNPWaf0gChlft/D8xs5jHJFln+P3B9lQ= github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI= diff --git a/scripts/start-multi-nats.sh b/scripts/start-multi-nats.sh old mode 100644 new mode 100755 diff --git a/tests/e2e/Dockerfile.base b/tests/e2e/Dockerfile.base deleted file mode 100644 index 38fc02f07..000000000 --- a/tests/e2e/Dockerfile.base +++ /dev/null @@ -1,19 +0,0 @@ -FROM ubuntu:24.04 - -# Avoid prompts -ENV DEBIAN_FRONTEND=noninteractive - -# Install make first so we can use the Makefile -RUN apt-get update && apt-get install -y make sudo && rm -rf /var/lib/apt/lists/* - -# Set up working directory -WORKDIR /app/hive - -# Copy only the Makefile to run quickinstall -COPY Makefile . - -# Run the quickinstall target from the Makefile -RUN make quickinstall - -# Ensure Go is in the PATH -ENV PATH="/usr/local/go/bin:${PATH}" diff --git a/tests/e2e/Dockerfile.e2e b/tests/e2e/Dockerfile.e2e index 8caee7037..94768ed3a 100644 --- a/tests/e2e/Dockerfile.e2e +++ b/tests/e2e/Dockerfile.e2e @@ -1,19 +1,36 @@ -# syntax=docker/dockerfile:1 -# This image assumes hive-base:latest exists locally or in a registry -FROM hive-base:latest +FROM ubuntu:24.04 -# Set up working directory structure to match INSTALL.md +# Avoid prompts +ENV DEBIAN_FRONTEND=noninteractive + +# Install make first so we can use the Makefile +RUN apt-get update && apt-get install -y make sudo && rm -rf /var/lib/apt/lists/* + +# Set up working directory for Makefile targets +WORKDIR /tmp/hive-setup + +# Copy only the Makefile for dependency installation +COPY hive/Makefile . + +# Install system dependencies in separate layers for better caching +RUN make install-system +RUN make install-go +RUN make install-aws + +# Ensure Go is in the PATH +ENV PATH="/usr/local/go/bin:/root/go/bin:${PATH}" + +# Set up working directory structure to match development layout WORKDIR /root/Development/mulga -# 1. Pre-copy dependency manifests -# We copy these first to create a dedicated dependency layer -COPY hive/go.mod hive/go.sum hive/go.work hive/go.work.sum ./hive/ +# 1. Pre-copy dependency manifests for cache layer +COPY hive/go.mod hive/go.sum ./hive/ COPY viperblock/go.mod viperblock/go.sum ./viperblock/ COPY viperblock/nbd/go.mod viperblock/nbd/go.sum ./viperblock/nbd/ COPY viperblock/nbd/libguestfs.org/nbdkit/go.mod ./viperblock/nbd/libguestfs.org/nbdkit/ COPY predastore/go.mod predastore/go.sum ./predastore/ -# 2. Download dependencies (Using Cache Mount) +# 2. Download dependencies (using cache mount) RUN --mount=type=cache,target=/root/go/pkg/mod \ (cd hive && go mod download) && \ (cd viperblock && go mod download) && \ @@ -32,7 +49,7 @@ WORKDIR /root/Development/mulga/hive # Ensure scripts are executable RUN chmod +x ./scripts/*.sh ./tests/e2e/run-e2e.sh -# 4. Build everything +# 4. Build everything (using cache mounts) RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/root/go/pkg/mod \ ./scripts/dev-setup.sh diff --git a/tests/e2e/run-e2e.sh b/tests/e2e/run-e2e.sh old mode 100644 new mode 100755 index 0219a6650..e902b029b --- a/tests/e2e/run-e2e.sh +++ b/tests/e2e/run-e2e.sh @@ -1,5 +1,18 @@ #!/bin/bash -set -ex +set -e + +# Ensure services are stopped on exit and print logs on failure +cleanup() { + EXIT_CODE=$? + if [ $EXIT_CODE -ne 0 ]; then + echo "=== Hive Service Log ===" + if [ -f ~/hive/logs/hive.log ]; then + cat ~/hive/logs/hive.log 2>/dev/null + fi + fi + ./scripts/stop-dev.sh +} +trap cleanup EXIT # Use Hive profile export AWS_PROFILE=hive @@ -30,6 +43,7 @@ fi # Start all services # Ensure logs directory exists for start-dev.sh mkdir -p ~/hive/logs +mkdir -p /mnt/ramdisk HIVE_SKIP_BUILD=true ./scripts/start-dev.sh # Wait for health checks on https://localhost:9999 (AWS Gateway) @@ -45,15 +59,11 @@ done if [ $COUNT -eq $MAX_RETRIES ]; then echo "Gateway failed to start" - ./scripts/stop-dev.sh exit 1 fi # Define common AWS CLI args -# Use --endpoint-url and --no-verify-ssl to be safe in the container environment -# Suppress InsecureRequestWarning from urllib3 -export PYTHONWARNINGS="ignore:Unverified HTTPS request" -AWS_EC2="aws --endpoint-url https://localhost:9999 --no-verify-ssl ec2" +AWS_EC2="aws --endpoint-url https://localhost:9999 ec2" # Phase 2: Discovery & Metadata echo "Phase 2: Discovery & Metadata" @@ -66,8 +76,8 @@ echo "Discovering available instance types..." AVAILABLE_TYPES=$($AWS_EC2 describe-instance-types --query 'InstanceTypes[*].InstanceType' --output text) echo "Available instance types: $AVAILABLE_TYPES" -# Pick the first one as our primary test type -INSTANCE_TYPE=$(echo $AVAILABLE_TYPES | awk '{print $1}') +# Pick the nano instance type for minimal resource usage in tests +INSTANCE_TYPE=$(echo $AVAILABLE_TYPES | tr ' ' '\n' | grep -m1 'nano') if [ -z "$INSTANCE_TYPE" ] || [ "$INSTANCE_TYPE" == "None" ]; then echo "No instance types found!" exit 1 @@ -123,12 +133,11 @@ echo "Using image: $IMAGE_NAME" # Import a reliable Ubuntu image and capture the AMI ID from the output echo "Importing image $IMAGE_NAME..." echo "May appear stalled here, just takes a while to import..." -IMPORT_LOG=$(./bin/hive admin images import --name "$IMAGE_NAME" --force) +IMPORT_LOG=$(./bin/hive admin images import --name "$IMAGE_NAME" --force 2>/dev/null) AMI_ID=$(echo "$IMPORT_LOG" | grep -o 'ami-[a-z0-9]\+') if [ -z "$AMI_ID" ]; then echo "Failed to capture AMI ID from import command" - ./scripts/stop-dev.sh exit 1 fi echo "Captured AMI ID: $AMI_ID" @@ -138,17 +147,16 @@ echo "Verifying AMI availability..." $AWS_EC2 describe-images --image-ids "$AMI_ID" | jq -e ".Images[0] | select(.ImageId==\"$AMI_ID\")" # Phase 5: Instance Lifecycle -# echo "Phase 5: Instance Lifecycle" -# # Launch a VM (run-instances) +echo "Phase 5: Instance Lifecycle" +# Launch a VM (run-instances) # INSTANCE_ID=$($AWS_EC2 run-instances --image-id "$AMI_ID" --instance-type "$INSTANCE_TYPE" --key-name test-key-1 --query 'Instances[0].InstanceId' --output text) # if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" == "None" ]; then # echo "Failed to launch instance" -# ./scripts/stop-dev.sh # exit 1 # fi # echo "Launched Instance ID: $INSTANCE_ID" -# # Poll until state is running (describe-instances) +# Poll until state is running (describe-instances) # echo "Polling for instance running state..." # COUNT=0 # STATE="unknown" @@ -178,7 +186,6 @@ $AWS_EC2 describe-images --image-ids "$AMI_ID" | jq -e ".Images[0] | select(.Ima # if [ "$STATE" == "terminated" ]; then # echo "āŒ Instance terminated unexpectedly!" -# ./scripts/stop-dev.sh # exit 1 # fi @@ -188,7 +195,6 @@ $AWS_EC2 describe-images --image-ids "$AMI_ID" | jq -e ".Images[0] | select(.Ima # if [ "$STATE" != "running" ]; then # echo "Instance failed to reach running state" -# ./scripts/stop-dev.sh # exit 1 # fi @@ -196,7 +202,6 @@ $AWS_EC2 describe-images --image-ids "$AMI_ID" | jq -e ".Images[0] | select(.Ima # VOLUME_ID=$($AWS_EC2 describe-volumes --query 'Volumes[0].VolumeId' --output text) # if [ -z "$VOLUME_ID" ] || [ "$VOLUME_ID" == "None" ]; then # echo "Failed to find volume for instance $INSTANCE_ID" -# ./scripts/stop-dev.sh # exit 1 # fi # echo "Volume ID: $VOLUME_ID" @@ -217,7 +222,6 @@ $AWS_EC2 describe-images --image-ids "$AMI_ID" | jq -e ".Images[0] | select(.Ima # if [ "$STATE" != "stopped" ]; then # echo "Instance failed to reach stopped state" -# ./scripts/stop-dev.sh # exit 1 # fi @@ -237,7 +241,6 @@ $AWS_EC2 describe-images --image-ids "$AMI_ID" | jq -e ".Images[0] | select(.Ima # if [ "$STATE" != "running" ]; then # echo "Instance failed to reach running state after restart" -# ./scripts/stop-dev.sh # exit 1 # fi @@ -255,9 +258,5 @@ $AWS_EC2 describe-images --image-ids "$AMI_ID" | jq -e ".Images[0] | select(.Ima # COUNT=$((COUNT + 1)) # done -# Phase 6: Cleanup (Inside Container) -echo "Phase 6: Cleanup" -./scripts/stop-dev.sh - echo "E2E Test Completed Successfully" exit 0 From bfb909b3659ee8e154643a18ee9a4b63330aef6c Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Thu, 22 Jan 2026 04:08:45 +0000 Subject: [PATCH 7/8] feat: jetstream kv for instance state --- cmd/hive/cmd/templates/nats.conf | 12 +- go.mod | 1 + go.sum | 4 +- hive/daemon/daemon.go | 75 +++++- hive/daemon/jetstream.go | 140 +++++++++++ hive/daemon/jetstream_test.go | 386 +++++++++++++++++++++++++++++++ hive/services/nats/nats.go | 1 + 7 files changed, 601 insertions(+), 18 deletions(-) create mode 100644 hive/daemon/jetstream.go create mode 100644 hive/daemon/jetstream_test.go diff --git a/cmd/hive/cmd/templates/nats.conf b/cmd/hive/cmd/templates/nats.conf index 5ba94281a..6d3054536 100644 --- a/cmd/hive/cmd/templates/nats.conf +++ b/cmd/hive/cmd/templates/nats.conf @@ -32,12 +32,12 @@ authorization { # token: "{{.NatsToken}}" } -# JetStream for persistent messaging -#jetstream { -# store_dir: "{{.DataDir}}/nats/" -# max_memory_store: 1GB -# max_file_store: 10GB -#} +# JetStream for persistent messaging and KV store +jetstream { + store_dir: "{{.DataDir}}/nats/" + max_memory_store: 1GB + max_file_store: 10GB +} # Logging debug: false diff --git a/go.mod b/go.mod index 755932545..c6a57453e 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 6f2c3b9a0..41eede855 100644 --- a/go.sum +++ b/go.sum @@ -103,8 +103,8 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= -github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= diff --git a/hive/daemon/daemon.go b/hive/daemon/daemon.go index 61e8284c4..afc892592 100644 --- a/hive/daemon/daemon.go +++ b/hive/daemon/daemon.go @@ -93,6 +93,9 @@ type Daemon struct { startTime time.Time configPath string + // JetStream manager for KV state storage (nil if JetStream disabled) + jsManager *JetStreamManager + mu sync.Mutex } @@ -405,8 +408,19 @@ func (d *Daemon) Start() error { log.Printf("Connected to NATS server at %s", d.config.NATS.Host) - // Load existing state for VMs - // Load state from disk + // Initialize JetStream for KV state storage (falls back to disk if unavailable) + d.jsManager, err = NewJetStreamManager(d.natsConn) + if err != nil { + slog.Warn("Failed to init JetStream, falling back to file", "error", err) + d.jsManager = nil + } else if err := d.jsManager.InitKVBucket(); err != nil { + slog.Warn("Failed to init KV bucket, falling back to file", "error", err) + d.jsManager = nil + } else { + slog.Info("JetStream KV store initialized successfully") + } + + // 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) @@ -773,8 +787,20 @@ func (d *Daemon) ClusterManager() error { return nil } -// Write the state to disk +// WriteState writes the instance state to JetStream KV store (if enabled) with disk fallback func (d *Daemon) WriteState() error { + if d.jsManager != nil { + if err := d.jsManager.WriteState(d.node, &d.Instances); err != nil { + slog.Error("JetStream write failed, falling back to disk", "error", err) + return d.writeStateToDisk() + } + return nil + } + return d.writeStateToDisk() +} + +// writeStateToDisk writes the instance state to disk (original WriteState behavior) +func (d *Daemon) writeStateToDisk() error { d.Instances.Mu.Lock() defer d.Instances.Mu.Unlock() @@ -828,8 +854,37 @@ func (d *Daemon) InitaliseVMs() { // Step 2: Loop through each instance and start it } -// Load state from disk +// LoadState loads the instance state from JetStream KV store (if enabled) with disk fallback func (d *Daemon) LoadState() error { + if d.jsManager != nil { + instances, err := d.jsManager.LoadState(d.node) + if err != nil { + slog.Warn("JetStream load failed, trying disk", "error", err) + return d.loadStateFromDisk() + } + + // If JetStream returned empty state, check if disk has data to migrate + if len(instances.VMS) == 0 { + diskErr := d.loadStateFromDisk() + if diskErr == nil && len(d.Instances.VMS) > 0 { + slog.Info("Migrating state from disk to JetStream", "instances", len(d.Instances.VMS)) + // Write existing disk state to JetStream for future use + if writeErr := d.jsManager.WriteState(d.node, &d.Instances); writeErr != nil { + slog.Warn("Failed to migrate state to JetStream", "error", writeErr) + } + return nil // Use the disk state we just loaded + } + } + + // Copy only the VMS map, not the mutex + d.Instances.VMS = instances.VMS + return nil + } + return d.loadStateFromDisk() +} + +// loadStateFromDisk loads the instance state from disk (original LoadState behavior) +func (d *Daemon) loadStateFromDisk() error { configPath := fmt.Sprintf("%s/%s", d.config.BaseDir, "instances.json") jsonData, err := os.ReadFile(configPath) if err != nil { @@ -1420,6 +1475,12 @@ func (d *Daemon) setupShutdown() { } + // Write state to JetStream before closing NATS connection + err := d.WriteState() + if err != nil { + slog.Error("Failed to write state", "err", err) + } + // Close NATS connection d.natsConn.Close() @@ -1431,12 +1492,6 @@ func (d *Daemon) setupShutdown() { } } - // Write the state to disk - err := d.WriteState() - if err != nil { - slog.Error("Failed to write state to disk", "err", err) - } - // Wait for any ongoing operations to complete // TODO: Implement cleanup of running instances log.Println("Shutdown complete") diff --git a/hive/daemon/jetstream.go b/hive/daemon/jetstream.go new file mode 100644 index 000000000..37c1ddfa1 --- /dev/null +++ b/hive/daemon/jetstream.go @@ -0,0 +1,140 @@ +package daemon + +import ( + "encoding/json" + "errors" + "log/slog" + + "github.com/mulgadc/hive/hive/vm" + "github.com/nats-io/nats.go" +) + +const ( + // InstanceStateBucket is the name of the KV bucket for storing instance state + InstanceStateBucket = "hive-instance-state" + // InstanceStatePrefix is the key prefix for instance state entries + InstanceStatePrefix = "node." +) + +// JetStreamManager manages JetStream KV store operations for instance state +type JetStreamManager struct { + js nats.JetStreamContext + kv nats.KeyValue +} + +// NewJetStreamManager creates a new JetStreamManager from a NATS connection +func NewJetStreamManager(nc *nats.Conn) (*JetStreamManager, error) { + js, err := nc.JetStream() + if err != nil { + return nil, err + } + + return &JetStreamManager{ + js: js, + }, nil +} + +// InitKVBucket initializes the KV bucket, creating it if it doesn't exist +func (m *JetStreamManager) InitKVBucket() error { + // Try to get the existing bucket first + kv, err := m.js.KeyValue(InstanceStateBucket) + if err != nil { + if errors.Is(err, nats.ErrBucketNotFound) { + // Bucket doesn't exist, create it + slog.Info("Creating JetStream KV bucket", "bucket", InstanceStateBucket) + kv, err = m.js.CreateKeyValue(&nats.KeyValueConfig{ + Bucket: InstanceStateBucket, + Description: "Hive instance state storage", + History: 1, // Only keep latest value + }) + if err != nil { + return err + } + } else { + return err + } + } else { + slog.Info("Connected to existing JetStream KV bucket", "bucket", InstanceStateBucket) + } + + m.kv = kv + return nil +} + +// WriteState writes the instance state to the KV store for the given node +func (m *JetStreamManager) WriteState(nodeID string, instances *vm.Instances) error { + if m.kv == nil { + return errors.New("KV bucket not initialized") + } + + instances.Mu.Lock() + defer instances.Mu.Unlock() + + // Create a struct without the mutex to avoid copying the lock + state := struct { + VMS map[string]*vm.VM `json:"vms"` + }{ + VMS: instances.VMS, + } + + jsonData, err := json.Marshal(state) + if err != nil { + return err + } + + key := InstanceStatePrefix + nodeID + _, err = m.kv.Put(key, jsonData) + if err != nil { + return err + } + + slog.Debug("Wrote state to JetStream KV", "key", key, "instances", len(instances.VMS)) + return nil +} + +// LoadState loads the instance state from the KV store for the given node +func (m *JetStreamManager) LoadState(nodeID string) (*vm.Instances, error) { + if m.kv == nil { + return nil, errors.New("KV bucket not initialized") + } + + key := InstanceStatePrefix + nodeID + entry, err := m.kv.Get(key) + if err != nil { + if errors.Is(err, nats.ErrKeyNotFound) { + // Key doesn't exist, return empty state + slog.Debug("No existing state in JetStream KV, returning empty state", "key", key) + return &vm.Instances{VMS: make(map[string]*vm.VM)}, nil + } + return nil, err + } + + var instances vm.Instances + if err := json.Unmarshal(entry.Value(), &instances); err != nil { + return nil, err + } + + // Ensure the VMS map is initialized + if instances.VMS == nil { + instances.VMS = make(map[string]*vm.VM) + } + + slog.Debug("Loaded state from JetStream KV", "key", key, "instances", len(instances.VMS)) + return &instances, nil +} + +// DeleteState removes the instance state from the KV store for the given node +func (m *JetStreamManager) DeleteState(nodeID string) error { + if m.kv == nil { + return errors.New("KV bucket not initialized") + } + + key := InstanceStatePrefix + nodeID + err := m.kv.Delete(key) + if err != nil && !errors.Is(err, nats.ErrKeyNotFound) { + return err + } + + slog.Debug("Deleted state from JetStream KV", "key", key) + return nil +} diff --git a/hive/daemon/jetstream_test.go b/hive/daemon/jetstream_test.go new file mode 100644 index 000000000..c63e00a52 --- /dev/null +++ b/hive/daemon/jetstream_test.go @@ -0,0 +1,386 @@ +package daemon + +import ( + "os" + "testing" + "time" + + "github.com/mulgadc/hive/hive/vm" + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats.go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// startTestNATSServerWithJetStream starts an embedded NATS server with JetStream enabled for testing +func startTestNATSServerWithJetStream(t *testing.T) (*server.Server, string) { + // Create a temporary directory for JetStream storage + tmpDir, err := os.MkdirTemp("", "nats-js-test-*") + require.NoError(t, err, "Failed to create temp directory for JetStream") + + t.Cleanup(func() { + os.RemoveAll(tmpDir) + }) + + opts := &server.Options{ + Host: "127.0.0.1", + Port: -1, // Let NATS auto-allocate an available port + JetStream: true, + StoreDir: tmpDir, + NoLog: true, + NoSigs: true, + } + + ns, err := server.NewServer(opts) + require.NoError(t, err, "Failed to create NATS server with JetStream") + + // Start server in a goroutine + go ns.Start() + + // Wait for server to be ready + if !ns.ReadyForConnections(5 * time.Second) { + t.Fatal("NATS server with JetStream failed to start") + } + + // Get the actual URL that was assigned + url := ns.ClientURL() + t.Logf("Test NATS server with JetStream started at: %s", url) + + return ns, url +} + +// TestJetStreamManager_WriteAndLoadState tests round-trip write and load of instance state +func TestJetStreamManager_WriteAndLoadState(t *testing.T) { + ns, natsURL := startTestNATSServerWithJetStream(t) + defer ns.Shutdown() + + // Connect to NATS + nc, err := nats.Connect(natsURL) + require.NoError(t, err, "Failed to connect to NATS") + defer nc.Close() + + // Create JetStreamManager + jsm, err := NewJetStreamManager(nc) + require.NoError(t, err, "Failed to create JetStreamManager") + + // Initialize the KV bucket + err = jsm.InitKVBucket() + require.NoError(t, err, "Failed to init KV bucket") + + // Create test instances + testNodeID := "test-node-1" + testInstances := &vm.Instances{ + VMS: map[string]*vm.VM{ + "i-test-001": { + ID: "i-test-001", + Status: "running", + InstanceType: "t3.micro", + }, + "i-test-002": { + ID: "i-test-002", + Status: "stopped", + InstanceType: "t3.small", + }, + }, + } + + // Write state + err = jsm.WriteState(testNodeID, testInstances) + require.NoError(t, err, "Failed to write state") + + // Load state + loadedInstances, err := jsm.LoadState(testNodeID) + require.NoError(t, err, "Failed to load state") + require.NotNil(t, loadedInstances, "Loaded instances should not be nil") + + // Verify the loaded state matches + assert.Len(t, loadedInstances.VMS, 2, "Should have 2 instances") + assert.NotNil(t, loadedInstances.VMS["i-test-001"], "Should have i-test-001") + assert.NotNil(t, loadedInstances.VMS["i-test-002"], "Should have i-test-002") + assert.Equal(t, "running", loadedInstances.VMS["i-test-001"].Status) + assert.Equal(t, "stopped", loadedInstances.VMS["i-test-002"].Status) + assert.Equal(t, "t3.micro", loadedInstances.VMS["i-test-001"].InstanceType) + assert.Equal(t, "t3.small", loadedInstances.VMS["i-test-002"].InstanceType) +} + +// TestJetStreamManager_LoadState_KeyNotFound tests that LoadState returns empty state when key doesn't exist +func TestJetStreamManager_LoadState_KeyNotFound(t *testing.T) { + ns, natsURL := startTestNATSServerWithJetStream(t) + defer ns.Shutdown() + + nc, err := nats.Connect(natsURL) + require.NoError(t, err) + defer nc.Close() + + jsm, err := NewJetStreamManager(nc) + require.NoError(t, err) + + err = jsm.InitKVBucket() + require.NoError(t, err) + + // Load state for a non-existent node + instances, err := jsm.LoadState("non-existent-node") + require.NoError(t, err, "Should not error when key not found") + require.NotNil(t, instances, "Should return non-nil instances") + assert.Empty(t, instances.VMS, "Should return empty VMS map") +} + +// TestJetStreamManager_BucketCreation tests that InitKVBucket creates the bucket when it doesn't exist +func TestJetStreamManager_BucketCreation(t *testing.T) { + ns, natsURL := startTestNATSServerWithJetStream(t) + defer ns.Shutdown() + + nc, err := nats.Connect(natsURL) + require.NoError(t, err) + defer nc.Close() + + jsm, err := NewJetStreamManager(nc) + require.NoError(t, err) + + // InitKVBucket should create the bucket + err = jsm.InitKVBucket() + require.NoError(t, err, "Should create bucket without error") + + // Verify the bucket exists by checking jsm.kv is set + assert.NotNil(t, jsm.kv, "KV bucket should be initialized") +} + +// TestJetStreamManager_BucketReconnection tests that InitKVBucket connects to existing bucket +func TestJetStreamManager_BucketReconnection(t *testing.T) { + ns, natsURL := startTestNATSServerWithJetStream(t) + defer ns.Shutdown() + + // First connection - create the bucket + nc1, err := nats.Connect(natsURL) + require.NoError(t, err) + + jsm1, err := NewJetStreamManager(nc1) + require.NoError(t, err) + + err = jsm1.InitKVBucket() + require.NoError(t, err, "First InitKVBucket should succeed") + + // Write some test data + testInstances := &vm.Instances{ + VMS: map[string]*vm.VM{ + "i-persist": { + ID: "i-persist", + Status: "running", + }, + }, + } + err = jsm1.WriteState("persist-node", testInstances) + require.NoError(t, err) + + nc1.Close() + + // Second connection - should connect to existing bucket + nc2, err := nats.Connect(natsURL) + require.NoError(t, err) + defer nc2.Close() + + jsm2, err := NewJetStreamManager(nc2) + require.NoError(t, err) + + err = jsm2.InitKVBucket() + require.NoError(t, err, "Second InitKVBucket should succeed (reconnect)") + + // Verify data persisted + loadedInstances, err := jsm2.LoadState("persist-node") + require.NoError(t, err) + assert.NotEmpty(t, loadedInstances.VMS, "Should have persisted instances") + assert.NotNil(t, loadedInstances.VMS["i-persist"], "Should have i-persist") + assert.Equal(t, "running", loadedInstances.VMS["i-persist"].Status) +} + +// TestJetStreamManager_DeleteState tests deleting state from the KV store +func TestJetStreamManager_DeleteState(t *testing.T) { + ns, natsURL := startTestNATSServerWithJetStream(t) + defer ns.Shutdown() + + nc, err := nats.Connect(natsURL) + require.NoError(t, err) + defer nc.Close() + + jsm, err := NewJetStreamManager(nc) + require.NoError(t, err) + + err = jsm.InitKVBucket() + require.NoError(t, err) + + // Write state + testNodeID := "delete-test-node" + testInstances := &vm.Instances{ + VMS: map[string]*vm.VM{ + "i-delete-me": { + ID: "i-delete-me", + Status: "running", + }, + }, + } + err = jsm.WriteState(testNodeID, testInstances) + require.NoError(t, err) + + // Verify state exists + loadedInstances, err := jsm.LoadState(testNodeID) + require.NoError(t, err) + assert.NotEmpty(t, loadedInstances.VMS) + + // Delete state + err = jsm.DeleteState(testNodeID) + require.NoError(t, err, "Should delete state without error") + + // Verify state is gone (should return empty state) + loadedInstances, err = jsm.LoadState(testNodeID) + require.NoError(t, err) + assert.Empty(t, loadedInstances.VMS, "Should return empty state after deletion") +} + +// TestJetStreamManager_DeleteState_NonExistent tests deleting state that doesn't exist +func TestJetStreamManager_DeleteState_NonExistent(t *testing.T) { + ns, natsURL := startTestNATSServerWithJetStream(t) + defer ns.Shutdown() + + nc, err := nats.Connect(natsURL) + require.NoError(t, err) + defer nc.Close() + + jsm, err := NewJetStreamManager(nc) + require.NoError(t, err) + + err = jsm.InitKVBucket() + require.NoError(t, err) + + // Delete non-existent state should not error + err = jsm.DeleteState("non-existent-node") + require.NoError(t, err, "Deleting non-existent state should not error") +} + +// TestJetStreamManager_WriteState_UpdateExisting tests that writing state updates existing entry +func TestJetStreamManager_WriteState_UpdateExisting(t *testing.T) { + ns, natsURL := startTestNATSServerWithJetStream(t) + defer ns.Shutdown() + + nc, err := nats.Connect(natsURL) + require.NoError(t, err) + defer nc.Close() + + jsm, err := NewJetStreamManager(nc) + require.NoError(t, err) + + err = jsm.InitKVBucket() + require.NoError(t, err) + + testNodeID := "update-test-node" + + // Write initial state + initialInstances := &vm.Instances{ + VMS: map[string]*vm.VM{ + "i-initial": { + ID: "i-initial", + Status: "running", + }, + }, + } + err = jsm.WriteState(testNodeID, initialInstances) + require.NoError(t, err) + + // Update state with different instances + updatedInstances := &vm.Instances{ + VMS: map[string]*vm.VM{ + "i-initial": { + ID: "i-initial", + Status: "stopped", // Changed status + }, + "i-new": { // Added new instance + ID: "i-new", + Status: "running", + }, + }, + } + err = jsm.WriteState(testNodeID, updatedInstances) + require.NoError(t, err) + + // Load and verify updated state + loadedInstances, err := jsm.LoadState(testNodeID) + require.NoError(t, err) + assert.Len(t, loadedInstances.VMS, 2, "Should have 2 instances") + assert.Equal(t, "stopped", loadedInstances.VMS["i-initial"].Status, "Status should be updated") + assert.NotNil(t, loadedInstances.VMS["i-new"], "Should have new instance") +} + +// TestJetStreamManager_MultipleNodes tests storing state for multiple nodes +func TestJetStreamManager_MultipleNodes(t *testing.T) { + ns, natsURL := startTestNATSServerWithJetStream(t) + defer ns.Shutdown() + + nc, err := nats.Connect(natsURL) + require.NoError(t, err) + defer nc.Close() + + jsm, err := NewJetStreamManager(nc) + require.NoError(t, err) + + err = jsm.InitKVBucket() + require.NoError(t, err) + + // Write state for node-1 + node1Instances := &vm.Instances{ + VMS: map[string]*vm.VM{ + "i-node1-001": {ID: "i-node1-001", Status: "running"}, + }, + } + err = jsm.WriteState("node-1", node1Instances) + require.NoError(t, err) + + // Write state for node-2 + node2Instances := &vm.Instances{ + VMS: map[string]*vm.VM{ + "i-node2-001": {ID: "i-node2-001", Status: "stopped"}, + "i-node2-002": {ID: "i-node2-002", Status: "running"}, + }, + } + err = jsm.WriteState("node-2", node2Instances) + require.NoError(t, err) + + // Load and verify node-1 state + loadedNode1, err := jsm.LoadState("node-1") + require.NoError(t, err) + assert.Len(t, loadedNode1.VMS, 1) + assert.NotNil(t, loadedNode1.VMS["i-node1-001"]) + + // Load and verify node-2 state + loadedNode2, err := jsm.LoadState("node-2") + require.NoError(t, err) + assert.Len(t, loadedNode2.VMS, 2) + assert.NotNil(t, loadedNode2.VMS["i-node2-001"]) + assert.NotNil(t, loadedNode2.VMS["i-node2-002"]) + + // Verify node isolation - node-1 doesn't have node-2's instances + _, exists := loadedNode1.VMS["i-node2-001"] + assert.False(t, exists, "Node-1 should not have node-2's instances") +} + +// TestJetStreamManager_KVNotInitialized tests error handling when KV is not initialized +func TestJetStreamManager_KVNotInitialized(t *testing.T) { + ns, natsURL := startTestNATSServerWithJetStream(t) + defer ns.Shutdown() + + nc, err := nats.Connect(natsURL) + require.NoError(t, err) + defer nc.Close() + + // Create JetStreamManager but don't call InitKVBucket + jsm, err := NewJetStreamManager(nc) + require.NoError(t, err) + + testInstances := &vm.Instances{VMS: make(map[string]*vm.VM)} + err = jsm.WriteState("test-node", testInstances) + assert.Error(t, err, "WriteState should error when KV not initialized") + + _, err = jsm.LoadState("test-node") + assert.Error(t, err, "LoadState should error when KV not initialized") + + err = jsm.DeleteState("test-node") + assert.Error(t, err, "DeleteState should error when KV not initialized") +} diff --git a/hive/services/nats/nats.go b/hive/services/nats/nats.go index eb2dac5ed..57facab00 100644 --- a/hive/services/nats/nats.go +++ b/hive/services/nats/nats.go @@ -79,6 +79,7 @@ func launchService(config *Config) (err error) { Debug: config.Debug, LogFile: config.LogFile, JetStream: config.JetStream, + StoreDir: config.DataDir, } // Set defaults if not provided From 6677b741e1df5daa801a50f7c7fa251e82df2068 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Thu, 22 Jan 2026 04:46:25 +0000 Subject: [PATCH 8/8] fix: stop-instances --- hive/daemon/daemon.go | 28 ++++++++++++++++++++++ hive/gateway/ec2/instance/StopInstances.go | 6 ++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/hive/daemon/daemon.go b/hive/daemon/daemon.go index afc892592..3cc374019 100644 --- a/hive/daemon/daemon.go +++ b/hive/daemon/daemon.go @@ -1109,6 +1109,34 @@ func (d *Daemon) handleEC2Events(msg *nats.Msg) { } + // If a stop command, clean up resources but keep instance state + if command.Attributes.StopInstance && command.QMPCommand.Execute == "system_powerdown" { + slog.Info("Stopping instance", "id", command.ID) + + // Update status to stopping + d.Instances.Mu.Lock() + instance.Status = "stopping" + d.Instances.Mu.Unlock() + + // Stop the instance (keep volumes) + stopInstance := make(map[string]*vm.VM) + stopInstance[instance.ID] = instance + err = d.stopInstance(stopInstance, false) + + if err != nil { + slog.Error("Failed to stop instance", "err", err) + return + } + + // Update status to stopped + d.Instances.Mu.Lock() + instance.Status = "stopped" + instance.Attributes = command.Attributes + d.Instances.Mu.Unlock() + + slog.Info("Instance stopped", "id", command.ID) + } + // If a terminate command, clean up resources if command.Attributes.TerminateInstance { slog.Info("Terminating instance", "id", command.ID) diff --git a/hive/gateway/ec2/instance/StopInstances.go b/hive/gateway/ec2/instance/StopInstances.go index bac5669ec..799db7643 100644 --- a/hive/gateway/ec2/instance/StopInstances.go +++ b/hive/gateway/ec2/instance/StopInstances.go @@ -12,7 +12,7 @@ import ( ) // StopInstances sends stop commands to specified instances via NATS -// Uses system_powerdown without stop_instance attribute to allow restart +// Uses system_powerdown with stop_instance attribute to prevent auto-restart on daemon boot func StopInstances(input *ec2.StopInstancesInput, natsConn *nats.Conn) (*ec2.StopInstancesOutput, error) { if len(input.InstanceIds) == 0 { return nil, fmt.Errorf("no instance IDs provided") @@ -30,7 +30,7 @@ func StopInstances(input *ec2.StopInstancesInput, natsConn *nats.Conn) (*ec2.Sto instanceID := *instanceIDPtr // Build the QMP command to stop the instance - // Note: system_powerdown without stop_instance allows restart + // Note: system_powerdown with stop_instance=true prevents auto-restart on daemon boot command := qmp.Command{ ID: instanceID, QMPCommand: qmp.QMPCommand{ @@ -38,7 +38,7 @@ func StopInstances(input *ec2.StopInstancesInput, natsConn *nats.Conn) (*ec2.Sto Arguments: map[string]any{}, }, Attributes: qmp.Attributes{ - StopInstance: false, // Allow restart on daemon/node restart + StopInstance: true, // Don't auto-restart on daemon boot TerminateInstance: false, }, }