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

This commit is contained in:
Ben Duncan
2026-01-22 19:53:00 +10:00
28 changed files with 1792 additions and 122 deletions
+43
View File
@@ -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
+11 -16
View File
@@ -3,23 +3,23 @@ name: Build and Test Hive
on:
push:
branches: [ main ]
paths: ["hive/**", ".github/workflows/**", "go.mod", "go.sum"]
pull_request:
workflow_dispatch:
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 +28,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
+13 -7
View File
@@ -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.
+36 -37
View File
@@ -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)...."
@@ -70,20 +40,49 @@ run:
clean:
rm ./bin/$(GO_PROJECT_NAME)
security: $(GOVULNCHECK) $(GOSECCHECK) $(GOSTATICCHECK)
install-system:
@echo "\n....Installing system dependencies...."
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; \
./aws/install; \
rm -rf awscliv2.zip aws/; \
else \
echo "AWS CLI already installed"; \
fi
quickinstall: install-system install-go install-aws
@echo "\n✅ Quickinstall complete. Please ensure /usr/local/go/bin is in your PATH."
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
@echo "Go vet report saved to tests/govet-report.txt"
.PHONY: go_build go_run build run test clean
.PHONY: build go_build go_run test bench run clean install-system install-go install-aws quickinstall security
+2 -2
View File
@@ -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)
+6 -6
View File
@@ -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
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
+52 -7
View File
@@ -8,14 +8,14 @@ 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
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,17 +24,32 @@ 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
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
@@ -43,24 +58,54 @@ 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/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
)
+133 -27
View File
@@ -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=
@@ -62,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=
@@ -85,12 +126,8 @@ 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/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=
@@ -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=
+149 -10
View File
@@ -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
@@ -91,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
}
@@ -403,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)
@@ -461,6 +477,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 +545,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
@@ -759,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()
@@ -814,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 {
@@ -1040,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)
@@ -1406,6 +1503,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()
@@ -1417,12 +1520,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")
@@ -2095,6 +2192,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)
+140
View File
@@ -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
}
+386
View File
@@ -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")
}
+24
View File
@@ -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")
}
+3 -3
View File
@@ -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,
},
}
@@ -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
}
@@ -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)
}
})
}
}
+1 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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]
}
+8
View File
@@ -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)
}
+246
View File
@@ -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-<id>/
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
}
+51
View File
@@ -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
}
+1
View File
@@ -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
+9 -5
View File
@@ -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"
#echo "🛑 Hive development environment stopped"
Regular → Executable
View File
+62
View File
@@ -0,0 +1,62 @@
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 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 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)
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 (using cache mounts)
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"]
+262
View File
@@ -0,0 +1,262 @@
#!/bin/bash
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
# 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
mkdir -p /mnt/ramdisk
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"
exit 1
fi
# Define common AWS CLI args
AWS_EC2="aws --endpoint-url https://localhost:9999 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 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
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 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"
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"
# 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!"
# exit 1
# fi
# sleep 5
# COUNT=$((COUNT + 1))
# done
# if [ "$STATE" != "running" ]; then
# echo "Instance failed to reach running state"
# 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"
# 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"
# 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"
# 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
echo "E2E Test Completed Successfully"
exit 0