mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-23 20:57:02 +00:00
Compare commits
265 Commits
v0.2.3
...
v1.0.0-hotfix
| Author | SHA1 | Date | |
|---|---|---|---|
| 868a92bd55 | |||
| af52d881b9 | |||
| c51ea0c29d | |||
| 8cf0cc332f | |||
| 1d6cefd9d2 | |||
| 0c160d7f45 | |||
| 91e4efb0ef | |||
| f5ec8c3bae | |||
| 8795a7b926 | |||
| 39e9fa6fb8 | |||
| 853b5bfd32 | |||
| d7c72e80af | |||
| 475e632404 | |||
| 92e25a9653 | |||
| ec525d3a6d | |||
| 94978f517b | |||
| 4d7239e645 | |||
| acc8dc1994 | |||
| a7ed93c6f3 | |||
| 67075c6600 | |||
| ae223cdf15 | |||
| b73a630f41 | |||
| c283100fcf | |||
| 355decd406 | |||
| b76953bfb4 | |||
| c9e83380df | |||
| e421840541 | |||
| 406ba9431e | |||
| 7882f77a36 | |||
| f7b99f0c50 | |||
| f3ee50e9b8 | |||
| 4f3b54abc2 | |||
| a7925d65f2 | |||
| 0d2e91da37 | |||
| e2c7a64aff | |||
| 588141cf67 | |||
| 9e3076727e | |||
| c1df418c5c | |||
| 5e0d819393 | |||
| 6d3b2f7e5d | |||
| f3c5bce811 | |||
| 9b8863a182 | |||
| 6e807f88cf | |||
| 32f5ec92c0 | |||
| 6a4b823de7 | |||
| 91483d632e | |||
| 5426dc1bb0 | |||
| 15f88caee7 | |||
| 5d194098ca | |||
| 94a80a3be7 | |||
| 3f642524a8 | |||
| de1340a331 | |||
| ae094cb936 | |||
| c2ab63fdef | |||
| 04bea48a39 | |||
| a92ea13798 | |||
| 46670ee8af | |||
| cbc04a2437 | |||
| 216842cc1b | |||
| 8ea40a1973 | |||
| 5bfc680a68 | |||
| 98d5ec92ed | |||
| 2244ba56b0 | |||
| 9e8e944faa | |||
| 3129c9eb70 | |||
| f744aedede | |||
| e5801aa6ba | |||
| 77a4a40ca6 | |||
| ab84905297 | |||
| 2a3ada5e93 | |||
| adfd20dee5 | |||
| 4ffc7785a5 | |||
| e91b67a7c6 | |||
| 051ecbd4ce | |||
| ef68c5bffe | |||
| 64c7305b37 | |||
| d025cab50e | |||
| 1c194da915 | |||
| 4ad5843ff5 | |||
| 7ea0353d2f | |||
| f8fc2b2cbb | |||
| 74671a05a5 | |||
| 90a3da2cc2 | |||
| 06a8da8fb5 | |||
| 7c6f03f0d0 | |||
| e6b48ce170 | |||
| fca9d018fc | |||
| 4a2ae33dd9 | |||
| 9e53443f7a | |||
| bf6b25842f | |||
| 5b442e8eef | |||
| a3c4819c1c | |||
| 70199f410a | |||
| 7cdf630cf7 | |||
| fb8c42e934 | |||
| cc46cf746e | |||
| e65f00c86a | |||
| 85f51b8084 | |||
| 4749a37725 | |||
| 982a7b6b45 | |||
| b81fdb5bc0 | |||
| 71e82b22a7 | |||
| 21c4754336 | |||
| ec29cc2745 | |||
| 5d31f05dba | |||
| 73e5905092 | |||
| edcc53b947 | |||
| 820f620e34 | |||
| 3d73dd180b | |||
| 7bfb032f90 | |||
| 15d6b0aa20 | |||
| a66b1a3d48 | |||
| cbbd81bc76 | |||
| 8c4adf08a3 | |||
| c0b89e6d8c | |||
| dd86805514 | |||
| ad1339505d | |||
| ba7e6a5c67 | |||
| 4d56565c3f | |||
| ae316dba47 | |||
| bdf0522895 | |||
| e347f0a75e | |||
| c2992ebe5e | |||
| ffca7330c5 | |||
| a21a275776 | |||
| 78bc0d989d | |||
| 8c6f66670a | |||
| 1fca43c9f4 | |||
| 34b50b11fb | |||
| 8f81ac29a4 | |||
| eefed70658 | |||
| 216ca9e710 | |||
| fefd5ed9e8 | |||
| d47f2ab4fd | |||
| 58a9d5741f | |||
| 625a78c529 | |||
| 88baf5e202 | |||
| 6379a2098a | |||
| 638828f636 | |||
| 0fbf4537c0 | |||
| 1b5108d379 | |||
| bab7b1dea3 | |||
| 30b08ca984 | |||
| 3467e0cf22 | |||
| 35d0df5b56 | |||
| 648b36d672 | |||
| d6062ed550 | |||
| 93aaf0f991 | |||
| f34452883c | |||
| c21f3dc188 | |||
| daae6ca6b8 | |||
| 32276c7cc7 | |||
| 30394b735b | |||
| a25a4cec34 | |||
| f6c6660f15 | |||
| 704874c0c7 | |||
| 665093343c | |||
| bb9e8db386 | |||
| 0d4a70ef28 | |||
| 946429d792 | |||
| a554fe4ecf | |||
| 2af3bf5287 | |||
| 1f38213d4b | |||
| 1ee7012ba9 | |||
| b6e99947e1 | |||
| 5481ef4361 | |||
| ee06105c56 | |||
| 150bf246a4 | |||
| 9b64c1911f | |||
| c1cdfc07c3 | |||
| c6af189638 | |||
| 02931567ac | |||
| c4890c80d3 | |||
| d2d3e52522 | |||
| 42cb6dbed0 | |||
| 6cdeea3aa8 | |||
| 4884b68b51 | |||
| 726528bd9d | |||
| 593920ce7d | |||
| 5e30d12f76 | |||
| ea8700df99 | |||
| 857cbd75ac | |||
| 0fa38f7986 | |||
| 1a4d590bf3 | |||
| b7678ab7c5 | |||
| 11730be685 | |||
| a4bc8bee74 | |||
| fafc104c30 | |||
| efa6257850 | |||
| 7e46f0012b | |||
| 58e6817efe | |||
| 71ec2b7ee5 | |||
| 9c430eea07 | |||
| 1be6c69e51 | |||
| c523f78403 | |||
| 2918a02fac | |||
| 8e3210f023 | |||
| 6ba9d3dbf0 | |||
| 27cdfc4653 | |||
| adadb5ef4c | |||
| d0fc2a2120 | |||
| a09a8efe3c | |||
| 7d43d0f782 | |||
| ba0d8b67ff | |||
| 454f381826 | |||
| b95ff42a2f | |||
| 2064c338b3 | |||
| 4f00cf9e4c | |||
| 48012a4058 | |||
| 55186d95f2 | |||
| c11ac04892 | |||
| 9e744e5be6 | |||
| 5d93786995 | |||
| b97a98e2b7 | |||
| ee0b92c192 | |||
| 00f5b6590e | |||
| 06804f5775 | |||
| 767ad0bf5c | |||
| 4152e4422f | |||
| 48bbe423df | |||
| e6b5b3b135 | |||
| f526972607 | |||
| ddcd7f622f | |||
| 2459e5f7ac | |||
| 120bfa8953 | |||
| d8d5fd7d2c | |||
| b857c7982e | |||
| 965e65df3b | |||
| 4c4683ba0c | |||
| a458a28406 | |||
| 2351d4ff81 | |||
| e7a3b5b712 | |||
| 36f820c859 | |||
| c60a691ac1 | |||
| 21fd5a7081 | |||
| 2b455e365e | |||
| e6a09d371c | |||
| 6f3f348287 | |||
| 8e8b17c599 | |||
| 72aa3e7563 | |||
| 096b3380fc | |||
| 663805cd23 | |||
| 380ab2a870 | |||
| 98c75cbb83 | |||
| 3ffe67271f | |||
| 4466de04d3 | |||
| fd91b80851 | |||
| 0853eb5068 | |||
| 3d8c9f3812 | |||
| 2cac5e2c37 | |||
| 0ec13f33eb | |||
| 7af16b75b9 | |||
| 40e09f0886 | |||
| e26cd7fd82 | |||
| 8f223d35d3 | |||
| b0dbc76b22 | |||
| 506bced4fa | |||
| 438ec3c973 | |||
| 11a1f18cb0 | |||
| 1ff08ab80a | |||
| d17eef2ba9 | |||
| b1059899a2 | |||
| 17da8b2eb2 | |||
| ee3df227a7 | |||
| 015dbbc272 |
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Only enforce preflight on dev and main
|
||||
if [ "$branch" != "dev" ] && [ "$branch" != "main" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Only run preflight if Go files were staged
|
||||
if git diff --cached --name-only | grep -q '\.go$'; then
|
||||
echo "Go files staged on $branch — running make preflight..."
|
||||
make preflight
|
||||
else
|
||||
echo "No Go files staged, skipping preflight."
|
||||
fi
|
||||
@@ -0,0 +1,52 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
target-branch: main
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
timezone: Australia/Sydney
|
||||
cooldown:
|
||||
default-days: 1
|
||||
groups:
|
||||
go-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: main
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
timezone: Australia/Sydney
|
||||
cooldown:
|
||||
default-days: 1
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- "version-update:semver-minor"
|
||||
- "version-update:semver-patch"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: npm
|
||||
target-branch: main
|
||||
directory: /spinifex/services/spinifexui/frontend
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
timezone: Australia/Sydney
|
||||
cooldown:
|
||||
default-days: 1
|
||||
ignore:
|
||||
- dependency-name: "@types/node"
|
||||
update-types:
|
||||
- "version-update:semver-major"
|
||||
groups:
|
||||
npm-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
+218
-48
@@ -1,80 +1,250 @@
|
||||
name: Hive E2E Test
|
||||
name: Spinifex E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- go.mod
|
||||
- go.sum
|
||||
- Makefile
|
||||
- tests/e2e/**
|
||||
- scripts/**
|
||||
- .github/workflows/e2e.yml
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sha:
|
||||
description: "PR head SHA to report status on"
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
statuses: write
|
||||
|
||||
env:
|
||||
GIT_BRANCH: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
build_e2e_image:
|
||||
name: Build E2E Image
|
||||
runs-on: self-hosted
|
||||
|
||||
e2e_single:
|
||||
name: Single-Node E2E
|
||||
runs-on: [self-hosted, ci-single]
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
TOFU_LOG: /tmp/e2e-tofu-single-${{ github.run_id }}.log
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Clean Workspace
|
||||
run: rm -rf "$GITHUB_WORKSPACE"/*
|
||||
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
path: hive
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
repositories: mulga
|
||||
|
||||
- name: Clone Dependencies
|
||||
run: ./hive/scripts/clone-deps.sh
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: mulgadc/mulga
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
sparse-checkout: scripts/tofu-cluster
|
||||
path: mulga
|
||||
|
||||
- name: Build E2E Image
|
||||
- name: Clone and build
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: ./build-artifacts.sh "${GIT_BRANCH}" "${RUNNER_TEMP}/spinifex-artifacts.tar"
|
||||
|
||||
- name: Provision VM
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
docker build \
|
||||
-t hive-e2e:latest \
|
||||
-f hive/tests/e2e/Dockerfile.e2e \
|
||||
.
|
||||
export TF_VAR_git_branch="${GIT_BRANCH}"
|
||||
tofu init -input=false >> "$TOFU_LOG" 2>&1
|
||||
tofu workspace select -or-create env3 >> "$TOFU_LOG" 2>&1
|
||||
tofu destroy -var-file=envs/env3.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1 || true
|
||||
./scrub-env.sh env3 >> "$TOFU_LOG" 2>&1 || true
|
||||
tofu apply -var-file=envs/env3.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1
|
||||
|
||||
hive_e2e_test:
|
||||
name: Hive Single-Node E2E Test
|
||||
runs-on: self-hosted
|
||||
needs: build_e2e_image
|
||||
- name: Bootstrap Cluster
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
SPX_PREBUILT=true \
|
||||
SPX_ARTIFACT_TAR="${RUNNER_TEMP}/spinifex-artifacts.tar" \
|
||||
SPX_EXTERNAL_POOL="192.168.1.230-192.168.1.240" \
|
||||
./bootstrap.sh env3 >> "$TOFU_LOG" 2>&1
|
||||
|
||||
steps:
|
||||
- name: Run E2E Tests
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
docker run --privileged --rm \
|
||||
-v /dev/kvm:/dev/kvm \
|
||||
--name hive-e2e-test \
|
||||
hive-e2e:latest
|
||||
INVENTORY=$(tofu output -json inventory)
|
||||
WAN_IP=$(echo "$INVENTORY" | jq -r '.nodes[0].wan_ip')
|
||||
SSH_KEY=$(echo "$INVENTORY" | jq -r '.ssh_key_path')
|
||||
SSH_KEY="${SSH_KEY/#\~/$HOME}"
|
||||
ssh -o StrictHostKeyChecking=no -i "$SSH_KEY" tf-user@${WAN_IP} \
|
||||
'cd ~/Development/mulga/spinifex && ./tests/e2e/run-e2e.sh && ./tests/e2e/run-cert-e2e.sh'
|
||||
|
||||
hive_multinode_e2e_test:
|
||||
name: Hive Multi-Node E2E Test
|
||||
runs-on: self-hosted
|
||||
needs: build_e2e_image
|
||||
- name: Teardown
|
||||
if: always()
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
tofu workspace select -or-create env3 >> "$TOFU_LOG" 2>&1
|
||||
tofu destroy -var-file=envs/env3.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1
|
||||
./scrub-env.sh env3 >> "$TOFU_LOG" 2>&1 || true
|
||||
|
||||
e2e_pseudo_multi:
|
||||
name: Pseudo Multi-Node E2E
|
||||
runs-on: [self-hosted, ci-pseudo]
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
TOFU_LOG: /tmp/e2e-tofu-pseudo-${{ github.run_id }}.log
|
||||
steps:
|
||||
- name: Run Multi-Node E2E Tests
|
||||
- name: Clean Workspace
|
||||
run: rm -rf "$GITHUB_WORKSPACE"/*
|
||||
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
repositories: mulga
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: mulgadc/mulga
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
sparse-checkout: scripts/tofu-cluster
|
||||
path: mulga
|
||||
|
||||
- name: Clone and build
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: ./build-artifacts.sh "${GIT_BRANCH}" "${RUNNER_TEMP}/spinifex-artifacts.tar"
|
||||
|
||||
- name: Provision VM
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
docker run --privileged --rm \
|
||||
-v /dev/kvm:/dev/kvm \
|
||||
--cap-add=NET_ADMIN \
|
||||
--name hive-multinode-e2e-test \
|
||||
hive-e2e:latest \
|
||||
./tests/e2e/run-multinode-e2e.sh
|
||||
export TF_VAR_git_branch="${GIT_BRANCH}"
|
||||
tofu init -input=false >> "$TOFU_LOG" 2>&1
|
||||
tofu workspace select -or-create env4 >> "$TOFU_LOG" 2>&1
|
||||
tofu destroy -var-file=envs/env4.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1 || true
|
||||
./scrub-env.sh env4 >> "$TOFU_LOG" 2>&1 || true
|
||||
tofu apply -var-file=envs/env4.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
runs-on: self-hosted
|
||||
needs: [hive_e2e_test, hive_multinode_e2e_test]
|
||||
if: always()
|
||||
- name: Bootstrap Cluster
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
SPX_PREBUILT=true \
|
||||
SPX_ARTIFACT_TAR="${RUNNER_TEMP}/spinifex-artifacts.tar" \
|
||||
./bootstrap.sh env4 >> "$TOFU_LOG" 2>&1
|
||||
|
||||
- name: Run E2E Tests
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
INVENTORY=$(tofu output -json inventory)
|
||||
WAN_IP=$(echo "$INVENTORY" | jq -r '.nodes[0].wan_ip')
|
||||
SSH_KEY=$(echo "$INVENTORY" | jq -r '.ssh_key_path')
|
||||
SSH_KEY="${SSH_KEY/#\~/$HOME}"
|
||||
ssh -o StrictHostKeyChecking=no -i "$SSH_KEY" tf-user@${WAN_IP} \
|
||||
'cd ~/Development/mulga/spinifex && SPX_EXTERNAL_POOL="192.168.1.220-192.168.1.229" ./tests/e2e/run-pseudo-multinode-e2e.sh'
|
||||
|
||||
- name: Teardown
|
||||
if: always()
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
tofu workspace select -or-create env4 >> "$TOFU_LOG" 2>&1
|
||||
tofu destroy -var-file=envs/env4.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1
|
||||
./scrub-env.sh env4 >> "$TOFU_LOG" 2>&1 || true
|
||||
|
||||
e2e_real_multi:
|
||||
name: Real Multi-Node E2E
|
||||
runs-on: [self-hosted, ci-multi]
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
TOFU_LOG: /tmp/e2e-tofu-multi-${{ github.run_id }}.log
|
||||
steps:
|
||||
- name: Remove Stale Containers
|
||||
run: |
|
||||
docker rm -f hive-e2e-test 2>/dev/null || true
|
||||
docker rm -f hive-multinode-e2e-test 2>/dev/null || true
|
||||
- name: Clean Workspace
|
||||
run: rm -rf "$GITHUB_WORKSPACE"/*
|
||||
|
||||
- name: Prune Docker Images
|
||||
run: docker image prune -f
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
repositories: mulga
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: mulgadc/mulga
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
sparse-checkout: scripts/tofu-cluster
|
||||
path: mulga
|
||||
|
||||
- name: Clone and build
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: ./build-artifacts.sh "${GIT_BRANCH}" "${RUNNER_TEMP}/spinifex-artifacts.tar"
|
||||
|
||||
- name: Provision VMs
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
export TF_VAR_git_branch="${GIT_BRANCH}"
|
||||
tofu init -input=false >> "$TOFU_LOG" 2>&1
|
||||
tofu workspace select -or-create env2 >> "$TOFU_LOG" 2>&1
|
||||
tofu destroy -var-file=envs/env2.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1 || true
|
||||
./scrub-env.sh env2 >> "$TOFU_LOG" 2>&1 || true
|
||||
tofu apply -var-file=envs/env2.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1
|
||||
|
||||
- name: Bootstrap Cluster
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
SPX_PREBUILT=true \
|
||||
SPX_ARTIFACT_TAR="${RUNNER_TEMP}/spinifex-artifacts.tar" \
|
||||
SPX_EXTERNAL_POOL="192.168.1.240-192.168.1.250" \
|
||||
./bootstrap.sh env2 >> "$TOFU_LOG" 2>&1
|
||||
|
||||
- name: Run E2E Tests
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
INVENTORY=$(tofu output -json inventory)
|
||||
NODE1_IP=$(echo "$INVENTORY" | jq -r '.nodes[0].wan_ip')
|
||||
SSH_KEY=$(echo "$INVENTORY" | jq -r '.ssh_key_path')
|
||||
SSH_KEY="${SSH_KEY/#\~/$HOME}"
|
||||
|
||||
# Build node IP list
|
||||
NODE_IPS=""
|
||||
NODE_COUNT=$(echo "$INVENTORY" | jq '.nodes | length')
|
||||
for i in $(seq 0 $((NODE_COUNT - 1))); do
|
||||
NODE_IPS="${NODE_IPS} $(echo "$INVENTORY" | jq -r ".nodes[$i].wan_ip")"
|
||||
done
|
||||
|
||||
ssh -o StrictHostKeyChecking=no -i "$SSH_KEY" tf-user@${NODE1_IP} \
|
||||
"cd ~/Development/mulga/spinifex && ./tests/e2e/run-multinode-e2e.sh ${NODE_IPS} && ./tests/e2e/run-cert-e2e.sh --multinode ${NODE_IPS}"
|
||||
|
||||
- name: Teardown
|
||||
if: always()
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
tofu workspace select -or-create env2 >> "$TOFU_LOG" 2>&1
|
||||
tofu destroy -var-file=envs/env2.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1
|
||||
./scrub-env.sh env2 >> "$TOFU_LOG" 2>&1 || true
|
||||
|
||||
e2e_status:
|
||||
name: Report E2E Status
|
||||
if: always() && inputs.sha
|
||||
needs: [e2e_single, e2e_pseudo_multi, e2e_real_multi]
|
||||
runs-on: [self-hosted, ci-single]
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Report status to PR
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if [[ "${{ needs.e2e_single.result }}" == "success" && \
|
||||
"${{ needs.e2e_pseudo_multi.result }}" == "success" && \
|
||||
"${{ needs.e2e_real_multi.result }}" == "success" ]]; then
|
||||
STATE="success"
|
||||
DESC="All E2E suites passed"
|
||||
else
|
||||
STATE="failure"
|
||||
DESC="E2E failed: single=${{ needs.e2e_single.result }} pseudo=${{ needs.e2e_pseudo_multi.result }} multi=${{ needs.e2e_real_multi.result }}"
|
||||
fi
|
||||
gh api "repos/${{ github.repository }}/statuses/${{ inputs.sha }}" \
|
||||
-f state="$STATE" \
|
||||
-f context=e2e \
|
||||
-f description="$DESC" \
|
||||
-f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
distro-amd64:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout Spinifex
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: spinifex
|
||||
|
||||
- name: Checkout Predastore
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: mulgadc/predastore
|
||||
path: predastore
|
||||
|
||||
- name: Checkout Viperblock
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: mulgadc/viperblock
|
||||
path: viperblock
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build amd64 Tarball
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
run: cd spinifex && make distro-amd64 VERSION="$VERSION"
|
||||
|
||||
- name: Upload amd64 Artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: distro-amd64
|
||||
path: |
|
||||
spinifex/dist/spinifex-${{ github.ref_name }}-linux-amd64.tar.gz
|
||||
spinifex/dist/spinifex-${{ github.ref_name }}-linux-amd64.tar.gz.sha256
|
||||
spinifex/scripts/setup.sh
|
||||
|
||||
distro-arm64:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout Spinifex
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: spinifex
|
||||
|
||||
- name: Checkout Predastore
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: mulgadc/predastore
|
||||
path: predastore
|
||||
|
||||
- name: Checkout Viperblock
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: mulgadc/viperblock
|
||||
path: viperblock
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build arm64 Tarball
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
run: cd spinifex && make distro-arm64 VERSION="$VERSION"
|
||||
|
||||
- name: Upload arm64 Artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: distro-arm64
|
||||
path: |
|
||||
spinifex/dist/spinifex-${{ github.ref_name }}-linux-arm64.tar.gz
|
||||
spinifex/dist/spinifex-${{ github.ref_name }}-linux-arm64.tar.gz.sha256
|
||||
|
||||
release:
|
||||
needs: [distro-amd64, distro-arm64]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Download amd64 Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: distro-amd64
|
||||
path: artifacts
|
||||
|
||||
- name: Download arm64 Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: distro-arm64
|
||||
path: artifacts
|
||||
|
||||
- name: Create Draft GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
artifacts/**/*.tar.gz
|
||||
artifacts/**/*.sha256
|
||||
artifacts/**/setup.sh
|
||||
generate_release_notes: true
|
||||
|
||||
- name: Upload Tarball for Verify Job
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-assets
|
||||
path: artifacts/
|
||||
|
||||
verify:
|
||||
needs: release
|
||||
runs-on: [self-hosted, ci-single]
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
TOFU_LOG: /tmp/release-verify-${{ github.run_id }}.log
|
||||
TAG: ${{ github.ref_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- name: Clean Workspace
|
||||
run: rm -rf "$GITHUB_WORKSPACE"/*
|
||||
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
repositories: mulga
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: mulgadc/mulga
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
sparse-checkout: scripts/tofu-cluster
|
||||
path: mulga
|
||||
|
||||
- name: Download Release Assets
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: release-assets
|
||||
path: release-assets
|
||||
|
||||
- name: Provision VM
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
tofu init -input=false >> "$TOFU_LOG" 2>&1
|
||||
tofu workspace select -or-create env3 >> "$TOFU_LOG" 2>&1
|
||||
tofu destroy -var-file=envs/env3.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1 || true
|
||||
./scrub-env.sh env3 >> "$TOFU_LOG" 2>&1 || true
|
||||
tofu apply -var-file=envs/env3.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1
|
||||
|
||||
- name: Bootstrap via Binary Install
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
SETUP_SH=$(find "${GITHUB_WORKSPACE}/release-assets" -name "setup.sh" | head -1)
|
||||
TARBALL=$(find "${GITHUB_WORKSPACE}/release-assets" -name "*-linux-amd64.tar.gz" | head -1)
|
||||
./bootstrap-install.sh env3 "$SETUP_SH" "$TARBALL"
|
||||
|
||||
- name: Run Smoke Test
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
INVENTORY=$(tofu output -json inventory)
|
||||
WAN_IP=$(echo "$INVENTORY" | jq -r '.nodes[0].wan_ip')
|
||||
SSH_KEY=$(echo "$INVENTORY" | jq -r '.ssh_key_path')
|
||||
SSH_KEY="${SSH_KEY/#\~/$HOME}"
|
||||
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
|
||||
-i "$SSH_KEY" \
|
||||
smoke-test-install.sh \
|
||||
"tf-user@${WAN_IP}:/tmp/smoke-test.sh"
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
|
||||
-i "$SSH_KEY" "tf-user@${WAN_IP}" \
|
||||
'bash /tmp/smoke-test.sh'
|
||||
|
||||
- name: Promote Release
|
||||
if: success()
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release edit "$TAG" --repo "$REPO" --draft=false
|
||||
echo "Release ${TAG} promoted to latest"
|
||||
|
||||
- name: Mark Release Failed
|
||||
if: failure()
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
EXISTING_BODY=$(gh release view "$TAG" --repo "$REPO" --json body -q .body)
|
||||
NOTES_FILE=$(mktemp)
|
||||
printf 'Verification failed — see workflow run for details.\n\n%s' "$EXISTING_BODY" > "$NOTES_FILE"
|
||||
gh release edit "$TAG" --repo "$REPO" --notes-file "$NOTES_FILE"
|
||||
rm -f "$NOTES_FILE"
|
||||
|
||||
- name: Teardown
|
||||
if: always()
|
||||
working-directory: mulga/scripts/tofu-cluster
|
||||
run: |
|
||||
tofu workspace select -or-create env3 >> "$TOFU_LOG" 2>&1
|
||||
tofu destroy -var-file=envs/env3.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1
|
||||
./scrub-env.sh env3 >> "$TOFU_LOG" 2>&1 || true
|
||||
@@ -1,28 +1,30 @@
|
||||
name: Hive Build and Test
|
||||
name: Spinifex Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- "**/*.go"
|
||||
- go.mod
|
||||
- go.sum
|
||||
- Makefile
|
||||
- .github/workflows/hive.yml
|
||||
- .github/workflows/spinifex.yml
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
name: Hive Build and Test
|
||||
name: Spinifex Build and Test
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout Hive
|
||||
- name: Checkout Spinifex
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: hive
|
||||
path: spinifex
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Checkout Viperblock
|
||||
@@ -40,30 +42,30 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
cache-dependency-path: "hive/go.sum"
|
||||
go-version-file: "hive/go.mod"
|
||||
cache-dependency-path: "spinifex/go.sum"
|
||||
go-version-file: "spinifex/go.mod"
|
||||
|
||||
- name: Setup Go Workspace
|
||||
run: go work init ./hive ./viperblock ./predastore
|
||||
run: go work init ./spinifex ./viperblock ./predastore
|
||||
|
||||
- name: Build Hive
|
||||
run: make -C hive build
|
||||
- name: Build Spinifex
|
||||
run: make -C spinifex build
|
||||
|
||||
- name: Run Tests with Coverage
|
||||
run: make -C hive test-cover
|
||||
run: make -C spinifex test-cover
|
||||
|
||||
- name: Check Diff Coverage
|
||||
run: cd hive && scripts/diff-coverage.sh coverage.out --base HEAD~1
|
||||
run: cd spinifex && scripts/diff-coverage.sh coverage.out --base HEAD~1
|
||||
|
||||
race_detection:
|
||||
name: Hive Race Detection
|
||||
name: Spinifex Race Detection
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout Hive
|
||||
- name: Checkout Spinifex
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: hive
|
||||
path: spinifex
|
||||
|
||||
- name: Checkout Viperblock
|
||||
uses: actions/checkout@v6
|
||||
@@ -80,24 +82,24 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
cache-dependency-path: "hive/go.sum"
|
||||
go-version-file: "hive/go.mod"
|
||||
cache-dependency-path: "spinifex/go.sum"
|
||||
go-version-file: "spinifex/go.mod"
|
||||
|
||||
- name: Setup Go Workspace
|
||||
run: go work init ./hive ./viperblock ./predastore
|
||||
run: go work init ./spinifex ./viperblock ./predastore
|
||||
|
||||
- name: Run Tests with Race Detector
|
||||
run: make -C hive test-race
|
||||
run: make -C spinifex test-race
|
||||
|
||||
lint_and_security:
|
||||
name: Hive Lint and Security
|
||||
name: Spinifex Lint and Security
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout Hive
|
||||
- name: Checkout Spinifex
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: hive
|
||||
path: spinifex
|
||||
|
||||
- name: Checkout Viperblock
|
||||
uses: actions/checkout@v6
|
||||
@@ -114,20 +116,17 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
cache-dependency-path: "hive/go.sum"
|
||||
go-version-file: "hive/go.mod"
|
||||
cache-dependency-path: "spinifex/go.sum"
|
||||
go-version-file: "spinifex/go.mod"
|
||||
|
||||
- name: Setup Go Workspace
|
||||
run: go work init ./hive ./viperblock ./predastore
|
||||
run: go work init ./spinifex ./viperblock ./predastore
|
||||
|
||||
- name: Check Format
|
||||
run: make -C hive check-format
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.11.3
|
||||
working-directory: spinifex
|
||||
|
||||
- name: Check Modernize
|
||||
run: make -C hive check-modernize
|
||||
|
||||
- name: Go Vet
|
||||
run: make -C hive vet
|
||||
|
||||
- name: Security Checks
|
||||
run: make -C hive security-check
|
||||
- name: Govulncheck
|
||||
run: make -C spinifex govulncheck
|
||||
@@ -4,9 +4,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- hive/services/hiveui/frontend/**
|
||||
- spinifex/services/spinifexui/frontend/**
|
||||
- .github/workflows/ui.yml
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- spinifex/services/spinifexui/frontend/**
|
||||
- .github/workflows/ui.yml
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -16,31 +21,34 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: hive/services/hiveui/frontend
|
||||
working-directory: spinifex/services/spinifexui/frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: hive/services/hiveui/frontend
|
||||
sparse-checkout: spinifex/services/spinifexui/frontend
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
package_json_file: hive/services/hiveui/frontend/package.json
|
||||
package_json_file: spinifex/services/spinifexui/frontend/package.json
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: hive/services/hiveui/frontend/pnpm-lock.yaml
|
||||
node-version-file: hive/services/hiveui/frontend/.nvmrc
|
||||
cache-dependency-path: spinifex/services/spinifexui/frontend/pnpm-lock.yaml
|
||||
node-version-file: spinifex/services/spinifexui/frontend/.nvmrc
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run Biome Check
|
||||
- name: Run Check
|
||||
run: pnpm check
|
||||
|
||||
- name: Run Tests with Coverage
|
||||
run: pnpm test:coverage
|
||||
|
||||
- name: Build Frontend
|
||||
run: pnpm build
|
||||
|
||||
+3
-1
@@ -13,6 +13,8 @@ go.work.sum
|
||||
|
||||
# Build outputs
|
||||
bin/
|
||||
dist/
|
||||
!spinifex/services/spinifexui/frontend/dist/
|
||||
tmp/
|
||||
*.test
|
||||
*.out
|
||||
@@ -34,8 +36,8 @@ Thumbs.db
|
||||
docs/INTERNAL.md
|
||||
docs/PRIVATE.md
|
||||
docs/DEVNOTES.md
|
||||
docs/HIVE_DEVELOPMENT_PLAN.md
|
||||
docs/PLAN.md
|
||||
docs/TODO.md
|
||||
docs/development/
|
||||
docs/internal/
|
||||
scripts/iac/aws/spinifex-test.pem
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
version: "2"
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
settings:
|
||||
gofmt:
|
||||
simplify: false # modernize linter covers gofmt -s simplifications (Go 1.23+)
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- asasalint # catches pass []any as any in variadic func(...any)
|
||||
- asciicheck # checks that code does not contain non-ASCII identifiers
|
||||
- bidichk # checks for dangerous unicode bidi character sequences
|
||||
- bodyclose # checks whether HTTP response body is closed successfully
|
||||
- canonicalheader # checks whether net/http.Header uses canonical header keys
|
||||
- decorder # checks declaration order of types, consts, vars, and funcs
|
||||
- durationcheck # checks for two durations multiplied together
|
||||
- embeddedstructfieldcheck # checks embedded sync.Mutex/RWMutex in structs
|
||||
- errcheck # checks for unchecked errors in type assertions and I/O
|
||||
- errchkjson # checks types passed to json encoding functions, flags omittable error checks
|
||||
- errname # checks that sentinel errors are prefixed with Err, error types suffixed with Error
|
||||
- exptostd # detects golang.org/x/exp/ functions replaceable by stdlib
|
||||
- fatcontext # detects nested contexts in loops
|
||||
- gocheckcompilerdirectives # validates go compiler directive comments (//go:)
|
||||
- goprintffuncname # checks that printf-like functions are named with f at the end
|
||||
- gosec # inspects source code for security problems
|
||||
- govet # reports suspicious constructs (Printf alignment, shadow, etc.)
|
||||
- ineffassign # detects assignments to existing variables that are not used
|
||||
- makezero # finds slice declarations with non-zero initial length followed by append
|
||||
- mirror # catches wrong mirror patterns in bytes/strings usage
|
||||
- modernize # modernizes code to use newer Go idioms (go fix analyzers)
|
||||
- nakedret # finds naked returns in functions
|
||||
- nilerr # catches code that returns nil even after checking err != nil
|
||||
- nilnesserr # catches returning a different nil error variable after err != nil check
|
||||
- nolintlint # reports ill-formed or insufficient nolint directives
|
||||
- predeclared # catches shadowing of Go builtins (len, cap, error, etc.)
|
||||
- reassign # checks that package variables are not reassigned
|
||||
- recvcheck # checks for receiver type consistency (pointer vs value)
|
||||
- sloglint # enforces consistent log/slog usage (key-value pairs, no globals)
|
||||
- staticcheck # extensive static analysis checks
|
||||
- unconvert # removes unnecessary type conversions
|
||||
- usestdlibvars # detects hardcoded values that should use stdlib constants
|
||||
- usetesting # catches os.TempDir() etc. in tests — should use t.TempDir()
|
||||
- wastedassign # finds assignments to variables that are never subsequently read
|
||||
- whitespace # detects leading and trailing whitespace in functions
|
||||
|
||||
settings:
|
||||
embeddedstructfieldcheck:
|
||||
forbid-mutex: true
|
||||
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
exclude-functions:
|
||||
- (*github.com/nats-io/nats.go.Subscription).Unsubscribe # cleanup-only, errors not actionable
|
||||
- (*github.com/kdomanski/iso9660.ImageWriter).Cleanup # deferred cleanup, errors not actionable
|
||||
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
- shadow # too noisy to retrofit — all 50 findings are err re-declarations
|
||||
- unusedwrite # false positives on struct field assignments before marshal/send
|
||||
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -ST1000 # package comment
|
||||
- -ST1003 # naming conventions (Id→ID etc.) — too noisy to retrofit
|
||||
- -ST1012 # error var naming — errname covers this + error type suffixes
|
||||
- -ST1013 # HTTP status constants — usestdlibvars is broader
|
||||
- -ST1016 # receiver name consistency (too noisy to retrofit)
|
||||
- -ST1020 # godoc on exported types — not enforcing yet
|
||||
- -ST1021 # godoc on exported type groups — not enforcing yet
|
||||
- -ST1022 # godoc on exported vars/consts — not enforcing yet
|
||||
- -SA1019 # deprecated usage (handle separately)
|
||||
- -SA4006 # value overwritten — ineffassign + wastedassign cover this better
|
||||
- -SA9005 # ineffective break (staticcheck false positives)
|
||||
|
||||
gosec:
|
||||
excludes:
|
||||
- G104 # unchecked errors — errcheck is stricter
|
||||
- G116 # bidi chars — bidichk is more thorough
|
||||
- G117 # use of net/http serve functions (we handle TLS separately)
|
||||
- G703 # path traversal taint analysis — false positives on config file loading
|
||||
- G705 # XSS taint analysis — false positives on internal error responses
|
||||
- G706 # log injection taint analysis — false positives on structured slog usage
|
||||
|
||||
modernize:
|
||||
disable:
|
||||
- newexpr # don't replace aws.String() with new() — AWS SDK idiom
|
||||
- stringsbuilder # don't replace small string concatenations with strings.Builder
|
||||
|
||||
nakedret:
|
||||
max-func-lines: 0
|
||||
|
||||
nolintlint:
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
|
||||
reassign:
|
||||
patterns:
|
||||
- ".*"
|
||||
|
||||
exclusions:
|
||||
presets:
|
||||
- std-error-handling
|
||||
- common-false-positives
|
||||
rules:
|
||||
- path: '_test\.go'
|
||||
linters:
|
||||
- bodyclose
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- gosec
|
||||
|
||||
# spinifex-specific: gosec exclusions
|
||||
- path: "cmd/"
|
||||
linters:
|
||||
- errcheck # CLI glue code — viper/cobra/pterm errors not actionable
|
||||
- gosec # matches current -exclude-dir=cmd
|
||||
|
||||
- text: "G204"
|
||||
linters: [gosec] # subprocess execution — legitimate in VM orchestration
|
||||
- text: "G304"
|
||||
linters: [gosec] # file path from variable — necessary for config loading
|
||||
- text: "G402"
|
||||
linters: [gosec] # TLS config — we handle TLS ourselves
|
||||
+2
-1258
File diff suppressed because it is too large
Load Diff
@@ -1,201 +1,661 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
1. Definitions.
|
||||
Preamble
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
0. Definitions.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
1. Source Code.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
2. Basic Permissions.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
GO_PROJECT_NAME := hive
|
||||
GO_PROJECT_NAME := spx
|
||||
SHELL := /bin/bash
|
||||
|
||||
# Detect architecture for cross-platform support
|
||||
@@ -36,12 +36,12 @@ endif
|
||||
# Quiet-mode filters (active when QUIET=1, set by preflight via recursive make)
|
||||
ifdef QUIET
|
||||
_Q = @
|
||||
_COVQ = 2>&1 | grep -Ev '^\s*(ok|PASS|\?|=== RUN|--- PASS:)\s' | grep -v 'coverage: 0\.0%' || true
|
||||
_COVQ = 2>&1 | { grep -Ev '^\s*(ok|PASS|\?|=== RUN|--- PASS:)\s' | grep -v 'coverage: 0\.0%' || true; }; exit $${PIPESTATUS[0]}
|
||||
_RACEQ = 2>&1 | { grep -Ev '^\s*(ok|PASS|\?|=== RUN|--- PASS:)\s' || true; }; exit $${PIPESTATUS[0]}
|
||||
_SECQ = >
|
||||
else
|
||||
_Q =
|
||||
_COVQ = || true
|
||||
_COVQ =
|
||||
_RACEQ =
|
||||
_SECQ = 2>&1 | tee
|
||||
endif
|
||||
@@ -49,44 +49,59 @@ endif
|
||||
build:
|
||||
$(MAKE) go_build
|
||||
|
||||
# Build hive-ui frontend (requires pnpm)
|
||||
# Build spinifex-ui frontend (requires pnpm)
|
||||
build-ui:
|
||||
@echo -e "\n....Building hive-ui frontend...."
|
||||
cd hive/services/hiveui/frontend && pnpm build
|
||||
@echo -e "\n....Building spinifex-ui frontend...."
|
||||
cd spinifex/services/spinifexui/frontend && pnpm build
|
||||
|
||||
# GO commands
|
||||
VERSION ?= $(shell git describe --tags --always --dirty)
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD)
|
||||
LDFLAGS := -s -w -X github.com/mulgadc/spinifex/cmd/spinifex/cmd.Version=$(VERSION) -X github.com/mulgadc/spinifex/cmd/spinifex/cmd.Commit=$(COMMIT)
|
||||
|
||||
go_build:
|
||||
@echo -e "\n....Building $(GO_PROJECT_NAME)"
|
||||
go build $(GO_BUILD_MOD) -ldflags "-s -w" -o ./bin/$(GO_PROJECT_NAME) cmd/hive/main.go
|
||||
go build $(GO_BUILD_MOD) -ldflags "$(LDFLAGS)" -o ./bin/$(GO_PROJECT_NAME) cmd/spinifex/main.go
|
||||
|
||||
build-alb-agent:
|
||||
@echo -e "\n....Building alb-agent (static)"
|
||||
CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/alb-agent cmd/alb-agent/main.go
|
||||
|
||||
build-system-image: ## Build a system image from manifest (use IMAGE=alb)
|
||||
ifndef IMAGE
|
||||
$(error IMAGE is required. Usage: make build-system-image IMAGE=alb)
|
||||
endif
|
||||
./scripts/build-system-image.sh scripts/images/$(IMAGE).conf
|
||||
|
||||
build-alb-image: ## Build ALB Alpine image
|
||||
$(MAKE) build-system-image IMAGE=alb
|
||||
|
||||
go_run:
|
||||
@echo -e "\n....Running $(GO_PROJECT_NAME)...."
|
||||
$(GOPATH)/bin/$(GO_PROJECT_NAME)
|
||||
|
||||
# Preflight — runs the same checks as GitHub Actions (format + lint + security + tests).
|
||||
# Preflight — runs the same checks as GitHub Actions (lint + vuln + tests).
|
||||
# Use this before committing to catch CI failures locally.
|
||||
preflight:
|
||||
@$(MAKE) --no-print-directory QUIET=1 check-format check-modernize vet security-check test-cover diff-coverage test-race
|
||||
@$(MAKE) --no-print-directory QUIET=1 lint govulncheck test-cover diff-coverage test-race
|
||||
@echo -e "\n ✅ Preflight passed — safe to commit."
|
||||
|
||||
# Run unit tests
|
||||
test:
|
||||
@echo -e "\n....Running tests for $(GO_PROJECT_NAME)...."
|
||||
LOG_IGNORE=1 go test -timeout 120s ./hive/...
|
||||
LOG_IGNORE=1 go test -timeout 120s ./spinifex/...
|
||||
|
||||
# Run unit tests with coverage profile
|
||||
# Note: go test may exit non-zero due to Go version mismatch in coverage instrumentation
|
||||
# for packages without test files. We check actual test results + coverage threshold instead.
|
||||
COVERPROFILE ?= coverage.out
|
||||
test-cover:
|
||||
@echo -e "\n....Running tests with coverage for $(GO_PROJECT_NAME)...."
|
||||
$(_Q)LOG_IGNORE=1 go test -timeout 120s -coverprofile=$(COVERPROFILE) -covermode=atomic ./hive/... $(_COVQ)
|
||||
$(_Q)LOG_IGNORE=1 go test -timeout 120s -coverprofile=$(COVERPROFILE) -covermode=atomic ./spinifex/... $(_COVQ)
|
||||
@scripts/check-coverage.sh $(COVERPROFILE) $(QUIET)
|
||||
|
||||
# Run unit tests with race detector
|
||||
test-race:
|
||||
@echo -e "\n....Running tests with race detector for $(GO_PROJECT_NAME)...."
|
||||
$(_Q)LOG_IGNORE=1 go test -race -timeout 300s ./hive/... $(_RACEQ)
|
||||
$(_Q)LOG_IGNORE=1 go test -race -timeout 300s ./spinifex/... $(_RACEQ)
|
||||
|
||||
# Check that new/changed code meets coverage threshold (runs tests first)
|
||||
diff-coverage: test-cover
|
||||
@@ -103,16 +118,18 @@ run:
|
||||
|
||||
clean:
|
||||
rm -f ./bin/$(GO_PROJECT_NAME)
|
||||
rm -rf hive/services/hiveui/frontend/dist
|
||||
rm -rf spinifex/services/spinifexui/frontend/dist
|
||||
|
||||
install-system:
|
||||
@echo -e "\n....Installing system dependencies for $(ARCH)...."
|
||||
@echo "QEMU packages: $(QEMU_PACKAGES)"
|
||||
apt-get update && sudo apt-get install -y \
|
||||
sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
-o Dpkg::Options::="--force-confdef" \
|
||||
-o Dpkg::Options::="--force-confold" \
|
||||
nbdkit nbdkit-plugin-dev pkg-config $(QEMU_PACKAGES) 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 \
|
||||
ovn-central ovn-host openvswitch-switch
|
||||
ovn-central ovn-host openvswitch-switch dhcpcd-base
|
||||
|
||||
install-go:
|
||||
@echo -e "\n....Installing Go 1.26.1 for $(ARCH) ($(GO_ARCH))...."
|
||||
@@ -139,93 +156,58 @@ quickinstall: install-system install-go install-aws
|
||||
@echo " Please ensure /usr/local/go/bin is in your PATH."
|
||||
@echo " Installed: Go ($(GO_ARCH)), AWS CLI ($(AWS_ARCH)), QEMU ($(QEMU_PACKAGES))"
|
||||
|
||||
# Format all Go files in place
|
||||
format:
|
||||
gofmt -w .
|
||||
# Lint all Go code via golangci-lint (replaces check-format, vet, gosec, staticcheck)
|
||||
lint:
|
||||
@echo "Running golangci-lint..."
|
||||
$(_Q)golangci-lint run ./...
|
||||
@echo " golangci-lint ok"
|
||||
|
||||
# Check that all Go files are formatted (CI-compatible, fails on diff)
|
||||
check-format:
|
||||
@echo "Checking gofmt..."
|
||||
@UNFORMATTED=$$(gofmt -l .); \
|
||||
if [ -n "$$UNFORMATTED" ]; then \
|
||||
echo "Files not formatted:"; \
|
||||
echo "$$UNFORMATTED"; \
|
||||
echo "Run 'make format' to fix."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo " gofmt ok"
|
||||
# Auto-fix all linter issues that have fixers
|
||||
fix:
|
||||
golangci-lint run --fix ./...
|
||||
|
||||
# Go vet (fails on issues, matches CI)
|
||||
vet:
|
||||
@echo "Running go vet..."
|
||||
$(_Q)go vet ./...
|
||||
@echo " go vet ok"
|
||||
|
||||
# Excluded: newexpr (replaces aws.String with new, not idiomatic for AWS SDK)
|
||||
# Excluded: stringsbuilder (replaces string += in loops with strings.Builder, not worth the complexity for small loops)
|
||||
GOFIX_EXCLUDE := -newexpr=false -stringsbuilder=false
|
||||
|
||||
# Apply go fix modernizations
|
||||
modernize:
|
||||
@echo "Applying go fix modernizations..."
|
||||
go fix $(GOFIX_EXCLUDE) ./...
|
||||
@echo " go fix applied"
|
||||
|
||||
# Check that code is modernized (CI-compatible, fails on diff)
|
||||
check-modernize:
|
||||
@echo "Checking go fix modernizations..."
|
||||
@DIFF=$$(go fix $(GOFIX_EXCLUDE) -diff ./...); \
|
||||
if [ -n "$$DIFF" ]; then \
|
||||
echo "$$DIFF"; \
|
||||
echo "Run 'make modernize' to fix."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo " go fix ok"
|
||||
|
||||
# Security checks — each tool fails the build on findings (matches CI).
|
||||
# Reports are also saved to tests/ for review.
|
||||
security-check:
|
||||
@echo -e "\n....Running security checks for $(GO_PROJECT_NAME)...."
|
||||
$(_Q)set -o pipefail && go tool govulncheck ./... $(_SECQ) tests/govulncheck-report.txt $(if $(QUIET),|| { cat tests/govulncheck-report.txt; exit 1; })
|
||||
# Govulncheck — dependency vulnerability scanning (not covered by golangci-lint)
|
||||
govulncheck:
|
||||
@echo "Running govulncheck..."
|
||||
$(_Q)go tool govulncheck ./...
|
||||
@echo " govulncheck ok"
|
||||
$(_Q)set -o pipefail && go tool gosec -quiet -exclude=G204,G304,G402,G117,G703,G705,G706 -exclude-generated -exclude-dir=cmd ./... $(_SECQ) tests/gosec-report.txt $(if $(QUIET),|| { cat tests/gosec-report.txt; exit 1; })
|
||||
@echo " gosec ok"
|
||||
$(_Q)set -o pipefail && go tool staticcheck -checks="all,-ST1000,-ST1003,-ST1016,-ST1020,-ST1021,-ST1022,-SA1019,-SA9005" ./... $(_SECQ) tests/staticcheck-report.txt $(if $(QUIET),|| { cat tests/staticcheck-report.txt; exit 1; })
|
||||
@echo " staticcheck ok"
|
||||
|
||||
# Docker E2E tests (mirrors GitHub Actions e2e.yml)
|
||||
# Usage: make test-docker # both suites
|
||||
# make test-docker-single # single-node only
|
||||
# make test-docker-multi # multi-node only
|
||||
PARENT_DIR := $(shell cd .. && pwd)
|
||||
E2E_IMAGE := hive-e2e:latest
|
||||
# Build release tarballs — use distro-ARCH for single arch, distro for both
|
||||
distro: distro-amd64 distro-arm64
|
||||
@echo ""
|
||||
@echo "Distribution tarballs:"
|
||||
@ls -lh dist/*.tar.gz
|
||||
@echo ""
|
||||
@cat dist/*.sha256
|
||||
|
||||
test-docker-build:
|
||||
@echo -e "\n....Building E2E Docker image...."
|
||||
@for dep in viperblock predastore; do \
|
||||
if [ ! -d "$(PARENT_DIR)/$$dep" ]; then \
|
||||
echo "Missing sibling repo $$dep — running clone-deps.sh"; \
|
||||
./scripts/clone-deps.sh; \
|
||||
break; \
|
||||
fi; \
|
||||
done
|
||||
docker build -t $(E2E_IMAGE) -f tests/e2e/Dockerfile.e2e $(PARENT_DIR)
|
||||
distro-amd64:
|
||||
@echo "Building spinifex $(VERSION) linux/amd64..."
|
||||
@mkdir -p dist/
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
-f build/Dockerfile.distro \
|
||||
--output type=local,dest=dist/amd64/ \
|
||||
../
|
||||
tar -czf dist/spinifex-$(VERSION)-linux-amd64.tar.gz -C dist/amd64 .
|
||||
sha256sum dist/spinifex-$(VERSION)-linux-amd64.tar.gz > dist/spinifex-$(VERSION)-linux-amd64.tar.gz.sha256
|
||||
|
||||
test-docker-single: test-docker-build
|
||||
@echo -e "\n....Running Single-Node E2E Tests...."
|
||||
docker run --privileged --rm -v /dev/kvm:/dev/kvm $(E2E_IMAGE)
|
||||
distro-arm64:
|
||||
@echo "Building spinifex $(VERSION) linux/arm64..."
|
||||
@mkdir -p dist/
|
||||
docker buildx build \
|
||||
--platform linux/arm64 \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
-f build/Dockerfile.distro \
|
||||
--output type=local,dest=dist/arm64/ \
|
||||
../
|
||||
tar -czf dist/spinifex-$(VERSION)-linux-arm64.tar.gz -C dist/arm64 .
|
||||
sha256sum dist/spinifex-$(VERSION)-linux-arm64.tar.gz > dist/spinifex-$(VERSION)-linux-arm64.tar.gz.sha256
|
||||
|
||||
test-docker-multi: test-docker-build
|
||||
@echo -e "\n....Running Multi-Node E2E Tests...."
|
||||
docker run --privileged --rm -v /dev/kvm:/dev/kvm --cap-add=NET_ADMIN $(E2E_IMAGE) ./tests/e2e/run-multinode-e2e.sh
|
||||
distro-clean:
|
||||
rm -rf dist/
|
||||
|
||||
test-docker: test-docker-build
|
||||
@echo -e "\n....Running Single-Node E2E Tests...."
|
||||
docker run --privileged --rm -v /dev/kvm:/dev/kvm $(E2E_IMAGE)
|
||||
@echo -e "\n....Running Multi-Node E2E Tests...."
|
||||
docker run --privileged --rm -v /dev/kvm:/dev/kvm --cap-add=NET_ADMIN $(E2E_IMAGE) ./tests/e2e/run-multinode-e2e.sh
|
||||
|
||||
.PHONY: build build-ui go_build go_run preflight test test-cover test-race diff-coverage bench run clean \
|
||||
.PHONY: build build-ui build-alb-agent build-system-image build-alb-image go_build go_run preflight test test-cover test-race diff-coverage bench run clean \
|
||||
install-system install-go install-aws quickinstall \
|
||||
format check-format modernize check-modernize vet security-check \
|
||||
test-docker-build test-docker-single test-docker-multi test-docker
|
||||
lint fix govulncheck \
|
||||
distro distro-amd64 distro-arm64 distro-clean
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Hive: An Open Source AWS-Compatible Stack for Bare-Metal, Edge, and On-Prem Deployments
|
||||
# Spinifex: An Open Source AWS-Compatible Stack for Bare-Metal, Edge, and On-Prem Deployments
|
||||
|
||||
(Developer Preview) - Hive developed by [Mulga Defense Corporation](https://mulgadc.com/) is an open source infrastructure platform that brings the core services of AWS—like EC2, VPC, EBS, and S3—to environments where running in the cloud isn't an option. Whether you're deploying to edge sites, private data-centers, or need to operate in low-connectivity or highly contested environments, Hive gives you AWS-style workflows on your own hardware.
|
||||
(Developer Preview) - Spinifex developed by [Mulga Defense Corporation](https://mulgadc.com/) is an open source infrastructure platform that brings the core services of AWS—like EC2, VPC, EBS, and S3—to environments where running in the cloud isn't an option. Whether you're deploying to edge sites, private data-centers, or need to operate in low-connectivity or highly contested environments, Spinifex gives you AWS-style workflows on your own hardware.
|
||||
|
||||
## What is Hive?
|
||||
## What is Spinifex?
|
||||
|
||||
Hive replicates essential AWS primitives—virtual machines, block volumes, and object storage—using lightweight, self-contained components that are easy to deploy and integrate.
|
||||
Spinifex replicates essential AWS primitives—virtual machines, block volumes, and object storage—using lightweight, self-contained components that are easy to deploy and integrate.
|
||||
|
||||
It’s designed for developers and operators who need:
|
||||
|
||||
@@ -13,13 +13,13 @@ It’s designed for developers and operators who need:
|
||||
- Drop-in compatibility with tools like the AWS CLI, SDKs, and Terraform
|
||||
- A secure cloud environment you control and own the entire hardware, network and software stack
|
||||
|
||||
You can run Hive on a few servers in a rack, a field site, or anywhere centralized cloud services aren’t feasible.
|
||||
You can run Spinifex on a few servers in a rack, a field site, or anywhere centralized cloud services aren’t feasible.
|
||||
|
||||
## Core Components
|
||||
|
||||
### Hive (Compute Service – EC2 Alternative)
|
||||
### Spinifex (Compute Service – EC2 Alternative)
|
||||
|
||||
Hive is a minimal VM orchestration layer built on top of QEMU, exposing APIs similar to EC2. It manages lifecycle operations like start, stop, and terminate, using QEMU’s QMP interface. Designed to be straightforward and scriptable, Hive lets you launch VMs using the AWS CLI, SDKs, or Terraform—without needing Kubernetes or heavyweight orchestrators. Keep in mind, you can also setup a Kubernetes environment using Hive with underlying instances.
|
||||
Spinifex is a minimal VM orchestration layer built on top of QEMU, exposing APIs similar to EC2. It manages lifecycle operations like start, stop, and terminate, using QEMU’s QMP interface. Designed to be straightforward and scriptable, Spinifex lets you launch VMs using the AWS CLI, SDKs, or Terraform—without needing Kubernetes or heavyweight orchestrators. Keep in mind, you can also setup a Kubernetes environment using Spinifex with underlying instances.
|
||||
|
||||
- EC2-like VM management on bare metal
|
||||
- Launches with cloud-init metadata support
|
||||
@@ -50,46 +50,48 @@ AWS-Compatible Interfaces – Provision infrastructure with awscli, Terraform, o
|
||||
- Designed for Control – Run on your own terms, whether that’s in a datacenter, remote site, or sensitive environment.
|
||||
- Minimal Dependencies – Each service is standalone and avoids complex orchestration layers.
|
||||
- Works Offline – No reliance on centralized cloud services or external networks.
|
||||
- Open Source – Licensed under Apache 2.0. Fork it, extend it, or deploy it as-is.
|
||||
- Open Source – Licensed under AGPL 3.0. Fork it, extend it, or deploy it as-is.
|
||||
|
||||
## Development Setup
|
||||
## Installation
|
||||
|
||||
### For Developers
|
||||
Installation requires an Ubuntu / Debian system. See the detailed documentation at [https://docs.mulgadc.com](https://docs.mulgadc.com) for maintaining and installing Spinifex.
|
||||
|
||||
Get started with Hive development in minutes:
|
||||
### Single Node Install
|
||||
|
||||
The installation is straightforward to set up and running on a single node for testing purposes.
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/mulgadc/hive.git
|
||||
cd hive
|
||||
curl https://install.mulgadc.com | bash
|
||||
|
||||
# Setup dependencies and development environment
|
||||
./scripts/clone-deps.sh # Clone viperblock + predastore repositories
|
||||
./scripts/dev-setup.sh # Setup complete development environment
|
||||
sudo /usr/local/share/spinifex/setup-ovn.sh --management
|
||||
|
||||
sudo spx admin init
|
||||
|
||||
sudo systemctl start spinifex.target
|
||||
|
||||
export AWS_PROFILE=spinifex
|
||||
|
||||
aws ec2 describe-instance-types
|
||||
```
|
||||
|
||||
Next, follow the detailed [Installation Guide](./INSTALL.md) to bootstrap a new environment with SSH keys, AMI (OS images) and how to spin up new EC2 instances using Hive.
|
||||
### Development Setup
|
||||
|
||||
For a complete development environment see the [Source Install](https://docs.mulgadc.com/docs/source-install/) documentation
|
||||
|
||||
### Component Repositories
|
||||
|
||||
Hive coordinates these independent components:
|
||||
Spinifex coordinates these independent components:
|
||||
|
||||
- **[Predastore](https://github.com/mulgadc/predastore)** - S3-compatible object storage
|
||||
- **[Viperblock](https://github.com/mulgadc/viperblock)** - EBS-compatible block storage
|
||||
|
||||
Each component can be developed independently. See component-specific documentation for focused development guides.
|
||||
|
||||
## Detailed Installation Method
|
||||
|
||||
See [Installation Guide](INSTALL.md) on how to setup a Hive node to get started.
|
||||
|
||||
Then configure your AWS CLI to point to Hive's endpoints and start launching VMs, buckets, and volumes using familiar commands and Terraform scripts.
|
||||
|
||||
## Development Philosophy
|
||||
|
||||
### Built by Engineers, For Engineers
|
||||
|
||||
Hive is developed by experienced infrastructure engineers with deep AWS expertise, including former AWS team members who understand the intricacies of building production-grade cloud services. Our team brings decades of combined experience from AWS, enterprise infrastructure, and edge computing environments.
|
||||
Spinifex is developed by experienced infrastructure engineers with deep AWS expertise, including former AWS team members who understand the intricacies of building production-grade cloud services. Our team brings decades of combined experience from AWS, enterprise infrastructure, and edge computing environments.
|
||||
|
||||
**Real-World Experience:**
|
||||
|
||||
@@ -100,7 +102,7 @@ Hive is developed by experienced infrastructure engineers with deep AWS expertis
|
||||
|
||||
### AI-Assisted Development
|
||||
|
||||
While Hive is architected and implemented by experienced engineers, we leverage **Claude Code** (Anthropic's AI coding assistant) to accelerate certain development tasks. This approach combines human expertise with AI efficiency:
|
||||
While Spinifex is architected and implemented by experienced engineers, we leverage **Claude Code** (Anthropic's AI coding assistant) to accelerate certain development tasks. This approach combines human expertise with AI efficiency:
|
||||
|
||||
**How We Use Claude Code:**
|
||||
|
||||
@@ -116,8 +118,8 @@ While Hive is architected and implemented by experienced engineers, we leverage
|
||||
- **Performance Optimization**: Real-world performance tuning and benchmarking
|
||||
- **Production Operations**: Deployment strategies and operational procedures
|
||||
|
||||
This hybrid approach ensures Hive benefits from both proven engineering expertise and modern development acceleration, while maintaining the quality and reliability standards required for production infrastructure.
|
||||
This hybrid approach ensures Spinifex benefits from both proven engineering expertise and modern development acceleration, while maintaining the quality and reliability standards required for production infrastructure.
|
||||
|
||||
## License
|
||||
|
||||
Hive is open source under the Apache 2.0 License. You're free to use, modify, and deploy it—anywhere you need reliable infrastructure without depending on centralized cloud platforms.
|
||||
Spinifex is open source under the [GNU Affero General Public License v3.0](LICENSE). You're free to use, modify, and deploy it—anywhere you need reliable infrastructure without depending on centralized cloud platforms.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
ARG VERSION=dev
|
||||
|
||||
FROM golang:1.26-bookworm AS builder
|
||||
|
||||
ARG VERSION
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
nbdkit nbdkit-plugin-dev pkg-config gcc make \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy module files for dependency caching
|
||||
COPY spinifex/go.mod spinifex/go.sum ./spinifex/
|
||||
COPY predastore/go.mod predastore/go.sum ./predastore/
|
||||
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/
|
||||
|
||||
# Generate go.work (gitignored, so not in checkout)
|
||||
RUN cd spinifex && go work init . ../predastore ../viperblock ../viperblock/nbd ../viperblock/nbd/libguestfs.org/nbdkit
|
||||
|
||||
# Download dependencies (cached layer)
|
||||
RUN cd spinifex && go mod download
|
||||
|
||||
# Copy full source
|
||||
COPY spinifex/ ./spinifex/
|
||||
COPY predastore/ ./predastore/
|
||||
COPY viperblock/ ./viperblock/
|
||||
|
||||
# Build spinifex binary (spx)
|
||||
RUN COMMIT=$(cd spinifex && git rev-parse --short HEAD 2>/dev/null || echo "unknown") && \
|
||||
cd spinifex && go build -ldflags "-s -w \
|
||||
-X github.com/mulgadc/spinifex/cmd/spinifex/cmd.Version=${VERSION} \
|
||||
-X github.com/mulgadc/spinifex/cmd/spinifex/cmd.Commit=${COMMIT}" \
|
||||
-o /out/spx cmd/spinifex/main.go
|
||||
|
||||
# Build nbdkit viperblock plugin (.so)
|
||||
RUN cd viperblock && go build -o /out/nbdkit-viperblock-plugin.so -buildmode=c-shared nbd/viperblock.go
|
||||
|
||||
# Copy setup scripts and systemd units
|
||||
RUN cp spinifex/scripts/setup.sh /out/setup.sh 2>/dev/null || true
|
||||
RUN cp spinifex/scripts/setup-ovn.sh /out/setup-ovn.sh
|
||||
RUN mkdir -p /out/systemd && cp spinifex/build/systemd/* /out/systemd/
|
||||
RUN cp spinifex/build/logrotate/spinifex /out/logrotate-spinifex
|
||||
|
||||
FROM scratch AS export
|
||||
COPY --from=builder /out/ /
|
||||
@@ -0,0 +1,9 @@
|
||||
/var/log/spinifex/*.log {
|
||||
daily
|
||||
rotate 14
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=Spinifex AWS Gateway
|
||||
PartOf=spinifex.target
|
||||
After=spinifex-nats.service spinifex-daemon.service
|
||||
Requires=spinifex-nats.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=spinifex
|
||||
Group=spinifex
|
||||
ExecStart=/usr/local/bin/spx service awsgw start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
OOMScoreAdjust=-500
|
||||
Environment=SPINIFEX_CONFIG_PATH=/etc/spinifex/spinifex.toml
|
||||
Environment=SPINIFEX_BASE_DIR=/var/lib/spinifex/
|
||||
Environment=SPINIFEX_AWSGW_TLS_CERT=/etc/spinifex/server.pem
|
||||
Environment=SPINIFEX_AWSGW_TLS_KEY=/etc/spinifex/server.key
|
||||
|
||||
[Install]
|
||||
WantedBy=spinifex.target
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Spinifex Daemon (VM Orchestration)
|
||||
PartOf=spinifex.target
|
||||
After=spinifex-nats.service spinifex-viperblock.service
|
||||
Requires=spinifex-nats.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=spinifex
|
||||
Group=spinifex
|
||||
ExecStart=/usr/local/bin/spx service spinifex start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
OOMScoreAdjust=-500
|
||||
Environment=SPINIFEX_CONFIG_PATH=/etc/spinifex/spinifex.toml
|
||||
Environment=SPINIFEX_BASE_DIR=/var/lib/spinifex/spinifex/
|
||||
Environment=SPINIFEX_WAL_DIR=/var/lib/spinifex/spinifex/
|
||||
|
||||
[Install]
|
||||
WantedBy=spinifex.target
|
||||
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Spinifex NATS Message Bus
|
||||
PartOf=spinifex.target
|
||||
Before=spinifex-predastore.service spinifex-daemon.service spinifex-awsgw.service spinifex-vpcd.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=spinifex
|
||||
Group=spinifex
|
||||
ExecStart=/usr/local/bin/spx service nats start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
OOMScoreAdjust=-500
|
||||
Environment=SPINIFEX_CONFIG_PATH=/etc/spinifex/nats/nats.conf
|
||||
|
||||
[Install]
|
||||
WantedBy=spinifex.target
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=Spinifex Predastore (S3-compatible Object Storage)
|
||||
PartOf=spinifex.target
|
||||
After=spinifex-nats.service
|
||||
Requires=spinifex-nats.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=spinifex
|
||||
Group=spinifex
|
||||
ExecStart=/usr/local/bin/spx service predastore start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
OOMScoreAdjust=-500
|
||||
Environment=SPINIFEX_PREDASTORE_CONFIG_PATH=/etc/spinifex/predastore/predastore.toml
|
||||
Environment=SPINIFEX_PREDASTORE_BASE_PATH=/var/lib/spinifex/predastore/
|
||||
Environment=SPINIFEX_PREDASTORE_TLS_CERT=/etc/spinifex/server.pem
|
||||
Environment=SPINIFEX_PREDASTORE_TLS_KEY=/etc/spinifex/server.key
|
||||
|
||||
[Install]
|
||||
WantedBy=spinifex.target
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Spinifex Web UI
|
||||
PartOf=spinifex.target
|
||||
After=spinifex-awsgw.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=spinifex
|
||||
Group=spinifex
|
||||
ExecStart=/usr/local/bin/spx service spinifex-ui start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
OOMScoreAdjust=-500
|
||||
|
||||
[Install]
|
||||
WantedBy=spinifex.target
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Spinifex Viperblock (Block Storage)
|
||||
PartOf=spinifex.target
|
||||
After=spinifex-nats.service spinifex-predastore.service
|
||||
Requires=spinifex-nats.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=spinifex
|
||||
Group=spinifex
|
||||
ExecStart=/usr/local/bin/spx service viperblock start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
OOMScoreAdjust=-500
|
||||
EnvironmentFile=/etc/spinifex/systemd.env
|
||||
Environment=SPINIFEX_CONFIG_PATH=/etc/spinifex/spinifex.toml
|
||||
Environment=SPINIFEX_BASE_DIR=/var/lib/spinifex/viperblock/
|
||||
|
||||
[Install]
|
||||
WantedBy=spinifex.target
|
||||
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=Spinifex VPC Daemon (OVN Networking)
|
||||
PartOf=spinifex.target
|
||||
After=spinifex-nats.service spinifex-daemon.service
|
||||
Requires=spinifex-nats.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=spinifex
|
||||
Group=spinifex
|
||||
ExecStart=/usr/local/bin/spx service vpcd start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
OOMScoreAdjust=-500
|
||||
Environment=SPINIFEX_CONFIG_PATH=/etc/spinifex/spinifex.toml
|
||||
|
||||
[Install]
|
||||
WantedBy=spinifex.target
|
||||
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Spinifex Infrastructure Platform
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Wants=spinifex-nats.service spinifex-predastore.service spinifex-viperblock.service spinifex-daemon.service spinifex-awsgw.service spinifex-vpcd.service spinifex-ui.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,62 @@
|
||||
// alb-agent runs inside ALB VMs and manages HAProxy configuration.
|
||||
//
|
||||
// It connects to NATS, subscribes to config updates for its load balancer,
|
||||
// writes HAProxy configs to disk, and reloads HAProxy on changes. It also
|
||||
// responds to health pings so the daemon can verify the agent is alive.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// alb-agent --lb-id=lb-xxxxx --nats=nats://10.0.0.1:4222
|
||||
//
|
||||
// The LB ID defaults to the hostname (set by cloud-init to the LB ID).
|
||||
// The NATS URL defaults to nats://127.0.0.1:4222.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/albagent"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
lbID string
|
||||
natsURL string
|
||||
)
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
flag.StringVar(&lbID, "lb-id", hostname, "Load balancer ID (defaults to hostname)")
|
||||
flag.StringVar(&natsURL, "nats", "nats://127.0.0.1:4222", "NATS server URL")
|
||||
flag.Parse()
|
||||
|
||||
if lbID == "" {
|
||||
slog.Error("--lb-id is required (or set hostname to the LB ID)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.Info("Starting ALB agent", "lbId", lbID, "nats", natsURL)
|
||||
|
||||
agent, err := albagent.New(lbID, natsURL)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create agent", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := agent.Start(); err != nil {
|
||||
slog.Error("Failed to start agent", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Wait for shutdown signal
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
|
||||
sig := <-sigCh
|
||||
|
||||
slog.Info("Received signal, shutting down", "signal", sig)
|
||||
agent.Stop()
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
Copyright © 2025 Mulga Defense Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package main
|
||||
|
||||
import "github.com/mulgadc/hive/cmd/hive/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/hive/hive/daemon"
|
||||
"github.com/mulgadc/spinifex/spinifex/daemon"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -77,7 +77,7 @@ func runClusterShutdown(cmd *cobra.Command, args []string) {
|
||||
|
||||
// Execute phases sequentially (except INFRA which is fire-and-forget)
|
||||
for _, phase := range phases {
|
||||
topic := "hive.cluster.shutdown." + phase
|
||||
topic := "spinifex.cluster.shutdown." + phase
|
||||
req := daemon.ShutdownRequest{
|
||||
Phase: phase,
|
||||
Force: force,
|
||||
@@ -106,7 +106,7 @@ func runClusterShutdown(cmd *cobra.Command, args []string) {
|
||||
// For DRAIN phase, subscribe to progress updates
|
||||
var progressSub *nats.Subscription
|
||||
if phase == "drain" {
|
||||
progressSub, err = nc.Subscribe("hive.cluster.shutdown.progress", func(msg *nats.Msg) {
|
||||
progressSub, err = nc.Subscribe("spinifex.cluster.shutdown.progress", func(msg *nats.Msg) {
|
||||
var progress daemon.ShutdownProgress
|
||||
if err := json.Unmarshal(msg.Data, &progress); err != nil {
|
||||
return
|
||||
@@ -1,17 +1,18 @@
|
||||
/*
|
||||
Copyright © 2025 Mulga Defense Corporation
|
||||
Copyright © 2026 Mulga Defense Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
@@ -24,9 +25,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/hive/hive/config"
|
||||
"github.com/mulgadc/hive/hive/types"
|
||||
"github.com/mulgadc/hive/hive/utils"
|
||||
"github.com/mulgadc/spinifex/spinifex/config"
|
||||
"github.com/mulgadc/spinifex/spinifex/types"
|
||||
"github.com/mulgadc/spinifex/spinifex/utils"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -66,8 +67,7 @@ func init() {
|
||||
func loadConfigAndConnect() (*config.ClusterConfig, *nats.Conn, error) {
|
||||
cfgPath := viper.GetString("config")
|
||||
if cfgPath == "" {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
cfgPath = fmt.Sprintf("%s/hive/config/hive.toml", homeDir)
|
||||
cfgPath = DefaultConfigFile()
|
||||
}
|
||||
|
||||
cfg, err := config.LoadConfig(cfgPath)
|
||||
@@ -76,7 +76,7 @@ func loadConfigAndConnect() (*config.ClusterConfig, *nats.Conn, error) {
|
||||
}
|
||||
|
||||
// Backfill BaseDir from config file path when not set in the config.
|
||||
// Config lives at <baseDir>/config/hive.toml, so baseDir is two levels up.
|
||||
// Config lives at <baseDir>/config/spinifex.toml, so baseDir is two levels up.
|
||||
if nodeConfig, ok := cfg.Nodes[cfg.Node]; ok && nodeConfig.BaseDir == "" {
|
||||
nodeConfig.BaseDir = filepath.Dir(filepath.Dir(cfgPath))
|
||||
cfg.Nodes[cfg.Node] = nodeConfig
|
||||
@@ -139,6 +139,20 @@ func formatUptime(seconds int64) string {
|
||||
return fmt.Sprintf("%dm", mins)
|
||||
}
|
||||
|
||||
func formatRoles(resp types.NodeStatusResponse) string {
|
||||
var roles []string
|
||||
if resp.NATSRole != "" {
|
||||
roles = append(roles, "nats:"+resp.NATSRole)
|
||||
}
|
||||
if resp.PredastoreRole != "" {
|
||||
roles = append(roles, "predastore:"+resp.PredastoreRole)
|
||||
}
|
||||
if len(roles) == 0 {
|
||||
return "-"
|
||||
}
|
||||
return strings.Join(roles, ",")
|
||||
}
|
||||
|
||||
func formatMemGB(gb float64) string {
|
||||
if gb >= 1.0 {
|
||||
return fmt.Sprintf("%.1fGi", gb)
|
||||
@@ -155,7 +169,7 @@ func runGetNodes(cmd *cobra.Command, args []string) {
|
||||
defer nc.Close()
|
||||
|
||||
timeout, _ := cmd.Flags().GetDuration("timeout")
|
||||
responses, err := collectResponses(nc, "hive.node.status", timeout)
|
||||
responses, err := collectResponses(nc, "spinifex.node.status", timeout)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -173,7 +187,7 @@ func runGetNodes(cmd *cobra.Command, args []string) {
|
||||
|
||||
// Build table: union of config-known nodes + NATS responders
|
||||
tableData := pterm.TableData{
|
||||
{"NAME", "STATUS", "IP", "REGION", "AZ", "UPTIME", "VMs", "SERVICES"},
|
||||
{"NAME", "STATUS", "ROLES", "IP", "REGION", "AZ", "UPTIME", "VMs", "SERVICES"},
|
||||
}
|
||||
|
||||
// Collect all node names: config + responded (union)
|
||||
@@ -196,6 +210,7 @@ func runGetNodes(cmd *cobra.Command, args []string) {
|
||||
tableData = append(tableData, []string{
|
||||
resp.Node,
|
||||
resp.Status,
|
||||
formatRoles(resp),
|
||||
resp.Host,
|
||||
resp.Region,
|
||||
resp.AZ,
|
||||
@@ -207,6 +222,7 @@ func runGetNodes(cmd *cobra.Command, args []string) {
|
||||
tableData = append(tableData, []string{
|
||||
name,
|
||||
"NotReady",
|
||||
"-",
|
||||
nodeCfg.Host,
|
||||
nodeCfg.Region,
|
||||
nodeCfg.AZ,
|
||||
@@ -229,7 +245,7 @@ func runGetVMs(cmd *cobra.Command, args []string) {
|
||||
defer nc.Close()
|
||||
|
||||
timeout, _ := cmd.Flags().GetDuration("timeout")
|
||||
responses, err := collectResponses(nc, "hive.node.vms", timeout)
|
||||
responses, err := collectResponses(nc, "spinifex.node.vms", timeout)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -237,6 +253,7 @@ func runGetVMs(cmd *cobra.Command, args []string) {
|
||||
|
||||
type vmRow struct {
|
||||
types.VMInfo
|
||||
|
||||
Node string
|
||||
Host string
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFormatRoles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
resp types.NodeStatusResponse
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no roles",
|
||||
resp: types.NodeStatusResponse{},
|
||||
want: "-",
|
||||
},
|
||||
{
|
||||
name: "nats leader only",
|
||||
resp: types.NodeStatusResponse{NATSRole: "leader"},
|
||||
want: "nats:leader",
|
||||
},
|
||||
{
|
||||
name: "predastore follower only",
|
||||
resp: types.NodeStatusResponse{PredastoreRole: "follower"},
|
||||
want: "predastore:follower",
|
||||
},
|
||||
{
|
||||
name: "both roles",
|
||||
resp: types.NodeStatusResponse{NATSRole: "leader", PredastoreRole: "follower"},
|
||||
want: "nats:leader,predastore:follower",
|
||||
},
|
||||
{
|
||||
name: "both leaders",
|
||||
resp: types.NodeStatusResponse{NATSRole: "leader", PredastoreRole: "leader"},
|
||||
want: "nats:leader,predastore:leader",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := formatRoles(tt.resp)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// DefaultConfigDir returns the default configuration directory.
|
||||
// Production: /etc/spinifex (when running as root AND /etc/spinifex exists)
|
||||
// Development: ~/spinifex/config
|
||||
func DefaultConfigDir() string {
|
||||
if isProductionLayout() {
|
||||
return "/etc/spinifex"
|
||||
}
|
||||
return filepath.Join(realUserHomeDir(), "spinifex", "config")
|
||||
}
|
||||
|
||||
// DefaultDataDir returns the default data directory.
|
||||
// Production: /var/lib/spinifex
|
||||
// Development: ~/spinifex
|
||||
func DefaultDataDir() string {
|
||||
if isProductionLayout() {
|
||||
return "/var/lib/spinifex"
|
||||
}
|
||||
return filepath.Join(realUserHomeDir(), "spinifex")
|
||||
}
|
||||
|
||||
// realUserHomeDir returns the home directory of the real (non-sudo) user.
|
||||
// When running under sudo, SUDO_USER is set to the invoking user — resolve
|
||||
// their home directory so config/data land in the right place.
|
||||
func realUserHomeDir() string {
|
||||
if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
|
||||
if u, err := user.Lookup(sudoUser); err == nil {
|
||||
return u.HomeDir
|
||||
}
|
||||
}
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
return homeDir
|
||||
}
|
||||
|
||||
// DefaultConfigFile returns the default path to spinifex.toml.
|
||||
func DefaultConfigFile() string {
|
||||
return filepath.Join(DefaultConfigDir(), "spinifex.toml")
|
||||
}
|
||||
|
||||
// isProductionLayout returns true when running in a production install.
|
||||
// Detected by: running as root AND /etc/spinifex directory already exists
|
||||
// (created by setup.sh during binary install).
|
||||
func isProductionLayout() bool {
|
||||
if os.Getuid() == 0 {
|
||||
if info, err := os.Stat("/etc/spinifex"); err == nil && info.IsDir() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultConfigDir_Dev(t *testing.T) {
|
||||
// Non-root user without /etc/spinifex should get dev paths
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("test requires non-root user")
|
||||
}
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
expected := filepath.Join(homeDir, "spinifex", "config")
|
||||
got := DefaultConfigDir()
|
||||
if got != expected {
|
||||
t.Errorf("DefaultConfigDir() = %q, want %q", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDataDir_Dev(t *testing.T) {
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("test requires non-root user")
|
||||
}
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
expected := filepath.Join(homeDir, "spinifex")
|
||||
got := DefaultDataDir()
|
||||
if got != expected {
|
||||
t.Errorf("DefaultDataDir() = %q, want %q", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfigFile(t *testing.T) {
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("test requires non-root user")
|
||||
}
|
||||
|
||||
got := DefaultConfigFile()
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
expected := filepath.Join(homeDir, "spinifex", "config", "spinifex.toml")
|
||||
if got != expected {
|
||||
t.Errorf("DefaultConfigFile() = %q, want %q", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsProductionLayout_NonRoot(t *testing.T) {
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("test requires non-root user")
|
||||
}
|
||||
|
||||
if isProductionLayout() {
|
||||
t.Error("isProductionLayout() = true for non-root user, want false")
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
/*
|
||||
Copyright © 2025 Mulga Defense Corporation
|
||||
Copyright © 2026 Mulga Defense Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
@@ -19,7 +20,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mulgadc/hive/hive/config"
|
||||
"github.com/mulgadc/spinifex/spinifex/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -32,20 +33,17 @@ var (
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "hive",
|
||||
Short: "Hive - Open source AWS-compatible platform for secure edge deployments",
|
||||
Use: "spx",
|
||||
Short: "Spinifex - Open source AWS-compatible platform for secure edge deployments",
|
||||
Long: `
|
||||
__/\\\________/\\\__/\\\\\\\\\\\__/\\\________/\\\__/\\\\\\\\\\\\\\\_
|
||||
_\/\\\_______\/\\\_\/////\\\///__\/\\\_______\/\\\_\/\\\///////////__
|
||||
_\/\\\_______\/\\\_____\/\\\_____\//\\\______/\\\__\/\\\_____________
|
||||
_\/\\\\\\\\\\\\\\\_____\/\\\______\//\\\____/\\\___\/\\\\\\\\\\\_____
|
||||
_\/\\\/////////\\\_____\/\\\_______\//\\\__/\\\____\/\\\///////______
|
||||
_\/\\\_______\/\\\_____\/\\\________\//\\\/\\\_____\/\\\_____________
|
||||
_\/\\\_______\/\\\_____\/\\\_________\//\\\\\______\/\\\_____________
|
||||
_\/\\\_______\/\\\__/\\\\\\\\\\\______\//\\\_______\/\\\\\\\\\\\\\\\_
|
||||
_\///________\///__\///////////________\///________\///////////////__
|
||||
_____ _____ _____ _ _ _____ ______ ________ __
|
||||
/ ____| __ \_ _| \ | |_ _| ____| ____\ \ / /
|
||||
| (___ | |__) || | | \| | | | | |__ | |__ \ V /
|
||||
\___ \| ___/ | | | . ‘ | | | | __| | __| > <
|
||||
____) | | _| |_| |\ |_| |_| | | |____ / . \
|
||||
|_____/|_| |_____|_| \_|_____|_| |______/_/ \_\
|
||||
|
||||
Hive – Open source AWS-compatible platform for secure edge deployments.
|
||||
Spinifex – Open source AWS-compatible platform for secure edge deployments.
|
||||
Run EC2, VPC, S3, and EBS-like services on bare metal with full control.
|
||||
Built for environments where running in the cloud isn’t an option.
|
||||
Whether you’re deploying to edge sites, private data-centers, or operating
|
||||
@@ -67,45 +65,44 @@ func init() {
|
||||
|
||||
// Global flags
|
||||
rootCmd.PersistentFlags().String("config", "", "config file (required)")
|
||||
viper.BindEnv("config", "HIVE_CONFIG_PATH")
|
||||
viper.BindEnv("config", "SPINIFEX_CONFIG_PATH")
|
||||
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
|
||||
|
||||
// Authentication (access_key, secret)
|
||||
rootCmd.PersistentFlags().String("access-key", "", "AWS access key (overrides config file and env)")
|
||||
viper.BindEnv("access-key", "HIVE_ACCESS_KEY")
|
||||
viper.BindEnv("access-key", "SPINIFEX_ACCESS_KEY")
|
||||
viper.BindPFlag("access-key", rootCmd.PersistentFlags().Lookup("access-key"))
|
||||
|
||||
rootCmd.PersistentFlags().String("secret-key", "", "AWS secret key (overrides config file and env)")
|
||||
viper.BindEnv("secret-key", "HIVE_SECRET_KEY")
|
||||
viper.BindEnv("secret-key", "SPINIFEX_SECRET_KEY")
|
||||
viper.BindPFlag("secret-key", rootCmd.PersistentFlags().Lookup("secret-key"))
|
||||
|
||||
rootCmd.PersistentFlags().String("host", "", "AWS Endpoint (overrides config file and env)")
|
||||
viper.BindEnv("host", "HIVE_HOST")
|
||||
viper.BindEnv("host", "SPINIFEX_HOST")
|
||||
viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("host"))
|
||||
|
||||
// Viperblock config
|
||||
rootCmd.PersistentFlags().String("base-dir", "", "Viperblock base directory (overrides config file and env)")
|
||||
viper.BindEnv("base-dir", "HIVE_BASE_DIR")
|
||||
viper.BindEnv("base-dir", "SPINIFEX_BASE_DIR")
|
||||
viper.BindPFlag("base-dir", rootCmd.PersistentFlags().Lookup("base-dir"))
|
||||
|
||||
// NATS specific flags
|
||||
rootCmd.PersistentFlags().String("nats-host", "", "NATS server host (overrides config file and env)")
|
||||
viper.BindEnv("nats-host", "HIVE_NATS_HOST")
|
||||
viper.BindEnv("nats-host", "SPINIFEX_NATS_HOST")
|
||||
viper.BindPFlag("nats-host", rootCmd.PersistentFlags().Lookup("nats-host"))
|
||||
|
||||
rootCmd.PersistentFlags().String("nats-token", "", "NATS authentication token (overrides config file and env)")
|
||||
viper.BindEnv("nats-token", "HIVE_NATS_TOKEN")
|
||||
viper.BindEnv("nats-token", "SPINIFEX_NATS_TOKEN")
|
||||
viper.BindPFlag("nats-token", rootCmd.PersistentFlags().Lookup("nats-token"))
|
||||
|
||||
rootCmd.PersistentFlags().String("nats-subject", "", "NATS subscription subject (overrides config file and env)")
|
||||
viper.BindEnv("nats-subject", "HIVE_NATS_SUBJECT")
|
||||
viper.BindEnv("nats-subject", "SPINIFEX_NATS_SUBJECT")
|
||||
viper.BindPFlag("nats-subject", rootCmd.PersistentFlags().Lookup("nats-subject"))
|
||||
|
||||
// Bind flags to viper
|
||||
//viper.BindPFlag("nats.host", rootCmd.PersistentFlags().Lookup("nats-host"))
|
||||
//viper.BindPFlag("nats.acl.token", rootCmd.PersistentFlags().Lookup("nats-token"))
|
||||
//viper.BindPFlag("nats.sub.subject", rootCmd.PersistentFlags().Lookup("nats-subject"))
|
||||
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
@@ -116,7 +113,11 @@ func initConfig() {
|
||||
appConfig, err = config.LoadConfig(cfgFile)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
|
||||
fmt.Fprintln(os.Stderr, "Continuing with environment variables and defaults...")
|
||||
// If a config file was explicitly provided, treat load failure as fatal
|
||||
if cfgFile != "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to load config %s: %v\n", cfgFile, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// No config specified — continue with env/defaults (e.g., spx --help)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
/*
|
||||
Copyright © 2025 Mulga Defense Corporation
|
||||
Copyright © 2026 Mulga Defense Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
@@ -21,21 +22,22 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mulgadc/hive/hive/config"
|
||||
"github.com/mulgadc/hive/hive/service"
|
||||
"github.com/mulgadc/hive/hive/services/hiveui"
|
||||
"github.com/mulgadc/hive/hive/services/nats"
|
||||
"github.com/mulgadc/hive/hive/services/predastore"
|
||||
"github.com/mulgadc/hive/hive/services/viperblockd"
|
||||
"github.com/mulgadc/hive/hive/services/vpcd"
|
||||
"github.com/mulgadc/predastore/s3"
|
||||
"github.com/mulgadc/spinifex/spinifex/config"
|
||||
"github.com/mulgadc/spinifex/spinifex/service"
|
||||
"github.com/mulgadc/spinifex/spinifex/services/awsgw"
|
||||
"github.com/mulgadc/spinifex/spinifex/services/nats"
|
||||
"github.com/mulgadc/spinifex/spinifex/services/predastore"
|
||||
"github.com/mulgadc/spinifex/spinifex/services/spinifexui"
|
||||
"github.com/mulgadc/spinifex/spinifex/services/viperblockd"
|
||||
"github.com/mulgadc/spinifex/spinifex/services/vpcd"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var serviceCmd = &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Manage Hive services",
|
||||
Short: "Manage Spinifex services",
|
||||
}
|
||||
|
||||
var predastoreCmd = &cobra.Command{
|
||||
@@ -53,9 +55,9 @@ var natsCmd = &cobra.Command{
|
||||
Short: "Manage the nats service",
|
||||
}
|
||||
|
||||
var hiveCmd = &cobra.Command{
|
||||
Use: "hive",
|
||||
Short: "Manage the hive service",
|
||||
var spinifexCmd = &cobra.Command{
|
||||
Use: "spinifex",
|
||||
Short: "Manage the spinifex service",
|
||||
}
|
||||
|
||||
var awsgwCmd = &cobra.Command{
|
||||
@@ -68,10 +70,10 @@ var vpcdCmd = &cobra.Command{
|
||||
Short: "Manage the vpcd (VPC daemon) service",
|
||||
}
|
||||
|
||||
var hiveUICmd = &cobra.Command{
|
||||
Use: "hive-ui",
|
||||
Aliases: []string{"ui", "hiveui"},
|
||||
Short: "Manage the hive-ui service",
|
||||
var spinifexUICmd = &cobra.Command{
|
||||
Use: "spinifex-ui",
|
||||
Aliases: []string{"ui", "spinifexui"},
|
||||
Short: "Manage the spinifex-ui service",
|
||||
}
|
||||
|
||||
var predastoreStartCmd = &cobra.Command{
|
||||
@@ -148,7 +150,10 @@ var predastoreStartCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
service.Start()
|
||||
if _, err := service.Start(); err != nil {
|
||||
fmt.Println("Error starting predastore service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Predastore service started", service)
|
||||
},
|
||||
@@ -167,7 +172,10 @@ var predastoreStopCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
service.Stop()
|
||||
if err = service.Stop(); err != nil {
|
||||
fmt.Println("Error stopping predastore service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Predastore service stopped")
|
||||
|
||||
@@ -201,12 +209,11 @@ var viperblockStartCmd = &cobra.Command{
|
||||
|
||||
// TODO: Support ENV vars, CLI, otherwise revert to config.LoadConfig()
|
||||
clusterConfig, err := config.LoadConfig(cfgFile)
|
||||
nodeConfig := clusterConfig.Nodes[clusterConfig.Node]
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error loading config file:", err)
|
||||
return
|
||||
}
|
||||
nodeConfig := clusterConfig.Nodes[clusterConfig.Node]
|
||||
|
||||
natsHost := viper.GetString("nats-host")
|
||||
|
||||
@@ -320,7 +327,10 @@ var viperblockStopCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
service.Stop()
|
||||
if err = service.Stop(); err != nil {
|
||||
fmt.Println("Error stopping viperblock service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Viperblock service stopped")
|
||||
|
||||
@@ -364,7 +374,10 @@ var natsStartCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
service.Start()
|
||||
if _, err = service.Start(); err != nil {
|
||||
fmt.Println("Error starting nats service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("NATS service started")
|
||||
},
|
||||
}
|
||||
@@ -382,7 +395,10 @@ var natsStopCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
service.Stop()
|
||||
if err = service.Stop(); err != nil {
|
||||
fmt.Println("Error stopping nats service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Nats service stopped")
|
||||
},
|
||||
@@ -396,12 +412,12 @@ var natsStatusCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
// Repeat for hive
|
||||
var hiveStartCmd = &cobra.Command{
|
||||
// Repeat for spinifex
|
||||
var spinifexStartCmd = &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Start the hive service",
|
||||
Short: "Start the spinifex service",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Starting hive service...")
|
||||
fmt.Println("Starting spinifex service...")
|
||||
|
||||
cfgFile := viper.GetString("config")
|
||||
|
||||
@@ -412,12 +428,11 @@ var hiveStartCmd = &cobra.Command{
|
||||
|
||||
// TODO: Support ENV vars, CLI, otherwise revert to config.LoadConfig()
|
||||
clusterConfig, err := config.LoadConfig(cfgFile)
|
||||
nodeConfig := clusterConfig.Nodes[clusterConfig.Node]
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error loading config file:", err)
|
||||
return
|
||||
}
|
||||
nodeConfig := clusterConfig.Nodes[clusterConfig.Node]
|
||||
|
||||
// Overwrite defaults (CLI first, config second, env third)
|
||||
baseDir := viper.GetString("base-dir")
|
||||
@@ -438,47 +453,53 @@ var hiveStartCmd = &cobra.Command{
|
||||
// Apply changes back to cluster config
|
||||
clusterConfig.Nodes[clusterConfig.Node] = nodeConfig
|
||||
|
||||
svc, err := service.New("hive", clusterConfig)
|
||||
svc, err := service.New("spinifex", clusterConfig)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error starting hive service:", err)
|
||||
fmt.Println("Error starting spinifex service:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set config path for cluster manager
|
||||
if hiveSvc, ok := svc.(interface{ SetConfigPath(string) }); ok {
|
||||
hiveSvc.SetConfigPath(cfgFile)
|
||||
if spxSvc, ok := svc.(interface{ SetConfigPath(string) }); ok {
|
||||
spxSvc.SetConfigPath(cfgFile)
|
||||
}
|
||||
|
||||
svc.Start()
|
||||
fmt.Println("HIVE service started")
|
||||
if _, err = svc.Start(); err != nil {
|
||||
fmt.Println("Error starting spinifex service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Spinifex service started")
|
||||
},
|
||||
}
|
||||
|
||||
var hiveStopCmd = &cobra.Command{
|
||||
var spinifexStopCmd = &cobra.Command{
|
||||
Use: "stop",
|
||||
Short: "Stop the hive service",
|
||||
Short: "Stop the spinifex service",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Stopping hive service...")
|
||||
fmt.Println("Stopping spinifex service...")
|
||||
|
||||
service, err := service.New("hive", &config.ClusterConfig{})
|
||||
service, err := service.New("spinifex", &config.ClusterConfig{})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error stopping hive service:", err)
|
||||
fmt.Println("Error stopping spinifex service:", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Stop()
|
||||
if err = service.Stop(); err != nil {
|
||||
fmt.Println("Error stopping spinifex service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Hive service stopped")
|
||||
fmt.Println("Spinifex service stopped")
|
||||
},
|
||||
}
|
||||
|
||||
var hiveStatusCmd = &cobra.Command{
|
||||
var spinifexStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Get status of the hive service",
|
||||
Short: "Get status of the spinifex service",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Hive service status: ...")
|
||||
fmt.Println("Spinifex service status: ...")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -501,12 +522,11 @@ var awsgwStartCmd = &cobra.Command{
|
||||
|
||||
// TODO: Support ENV vars, CLI, otherwise revert to config.LoadConfig()
|
||||
clusterConfig, err := config.LoadConfig(cfgFile)
|
||||
nodeConfig := clusterConfig.Nodes[clusterConfig.Node]
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error loading config file:", err)
|
||||
return
|
||||
}
|
||||
nodeConfig := clusterConfig.Nodes[clusterConfig.Node]
|
||||
|
||||
// Overwrite defaults (CLI first, config second, env third)
|
||||
awsgwHost := viper.GetString("host")
|
||||
@@ -538,6 +558,7 @@ var awsgwStartCmd = &cobra.Command{
|
||||
// Apply changes back to cluster config
|
||||
clusterConfig.Nodes[clusterConfig.Node] = nodeConfig
|
||||
|
||||
awsgw.SetBuildInfo(Version, Commit)
|
||||
service, err := service.New("awsgw", clusterConfig)
|
||||
|
||||
if err != nil {
|
||||
@@ -545,7 +566,10 @@ var awsgwStartCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
service.Start()
|
||||
if _, err = service.Start(); err != nil {
|
||||
fmt.Println("Error starting awsgw service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("AWSGW service started")
|
||||
},
|
||||
}
|
||||
@@ -563,7 +587,10 @@ var awsgwStopCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
service.Stop()
|
||||
if err = service.Stop(); err != nil {
|
||||
fmt.Println("Error stopping awsgw service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("AWSGW service stopped")
|
||||
},
|
||||
@@ -577,18 +604,18 @@ var awsgwStatusCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var hiveUIStartCmd = &cobra.Command{
|
||||
var spinifexUIStartCmd = &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Start the hive-ui service",
|
||||
Short: "Start the spinifex-ui service",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Starting hive-ui service...")
|
||||
fmt.Println("Starting spinifex-ui service...")
|
||||
|
||||
port := viper.GetInt("hive-ui-port")
|
||||
host := viper.GetString("hive-ui-host")
|
||||
tlsCert := viper.GetString("hive-ui-tls-cert")
|
||||
tlsKey := viper.GetString("hive-ui-tls-key")
|
||||
port := viper.GetInt("spinifex-ui-port")
|
||||
host := viper.GetString("spinifex-ui-host")
|
||||
tlsCert := viper.GetString("spinifex-ui-tls-cert")
|
||||
tlsKey := viper.GetString("spinifex-ui-tls-key")
|
||||
|
||||
svc, err := service.New("hive-ui", &hiveui.Config{
|
||||
svc, err := service.New("spinifex-ui", &spinifexui.Config{
|
||||
Port: port,
|
||||
Host: host,
|
||||
TLSCert: tlsCert,
|
||||
@@ -596,51 +623,57 @@ var hiveUIStartCmd = &cobra.Command{
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error starting hive-ui service:", err)
|
||||
fmt.Println("Error starting spinifex-ui service:", err)
|
||||
return
|
||||
}
|
||||
|
||||
svc.Start()
|
||||
fmt.Println("hive-ui service started")
|
||||
if _, err = svc.Start(); err != nil {
|
||||
fmt.Println("Error starting spinifex-ui service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("spinifex-ui service started")
|
||||
},
|
||||
}
|
||||
|
||||
var hiveUIStopCmd = &cobra.Command{
|
||||
var spinifexUIStopCmd = &cobra.Command{
|
||||
Use: "stop",
|
||||
Short: "Stop the hive-ui service",
|
||||
Short: "Stop the spinifex-ui service",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Stopping hive-ui service...")
|
||||
fmt.Println("Stopping spinifex-ui service...")
|
||||
|
||||
svc, err := service.New("hive-ui", &hiveui.Config{})
|
||||
svc, err := service.New("spinifex-ui", &spinifexui.Config{})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error stopping hive-ui service:", err)
|
||||
fmt.Println("Error stopping spinifex-ui service:", err)
|
||||
return
|
||||
}
|
||||
|
||||
svc.Stop()
|
||||
fmt.Println("hive-ui service stopped")
|
||||
if err = svc.Stop(); err != nil {
|
||||
fmt.Println("Error stopping spinifex-ui service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("spinifex-ui service stopped")
|
||||
},
|
||||
}
|
||||
|
||||
var hiveUIStatusCmd = &cobra.Command{
|
||||
var spinifexUIStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Get status of the hive-ui service",
|
||||
Short: "Get status of the spinifex-ui service",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
svc, err := service.New("hive-ui", &hiveui.Config{})
|
||||
svc, err := service.New("spinifex-ui", &spinifexui.Config{})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error getting hive-ui service status:", err)
|
||||
fmt.Println("Error getting spinifex-ui service status:", err)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := svc.Status()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting hive-ui service status:", err)
|
||||
fmt.Println("Error getting spinifex-ui service status:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("hive-ui service status:", status)
|
||||
fmt.Println("spinifex-ui service status:", status)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -664,20 +697,66 @@ var vpcdStartCmd = &cobra.Command{
|
||||
|
||||
nodeConfig := clusterConfig.Nodes[clusterConfig.Node]
|
||||
|
||||
// Map cluster-wide external pools to vpcd config
|
||||
var extPools []vpcd.ExternalPoolConfig
|
||||
for _, p := range clusterConfig.Network.ExternalPools {
|
||||
extPools = append(extPools, vpcd.ExternalPoolConfig{
|
||||
Name: p.Name,
|
||||
RangeStart: p.RangeStart,
|
||||
RangeEnd: p.RangeEnd,
|
||||
Gateway: p.Gateway,
|
||||
GatewayIP: p.GatewayIP,
|
||||
PrefixLen: p.PrefixLen,
|
||||
DNSServers: p.DNSServers,
|
||||
Region: p.Region,
|
||||
AZ: p.AZ,
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback chassis names derived from config node names. vpcd prefers
|
||||
// discovering chassis from the OVN Southbound DB at startup; these are
|
||||
// only used if that discovery fails.
|
||||
var chassisNames []string
|
||||
for nodeName := range clusterConfig.Nodes {
|
||||
chassisNames = append(chassisNames, "chassis-"+nodeName)
|
||||
}
|
||||
|
||||
var bootstrap *vpcd.BootstrapVPC
|
||||
if clusterConfig.Bootstrap.VpcId != "" {
|
||||
bootstrap = &vpcd.BootstrapVPC{
|
||||
AccountID: clusterConfig.Bootstrap.AccountID,
|
||||
VpcId: clusterConfig.Bootstrap.VpcId,
|
||||
SubnetId: clusterConfig.Bootstrap.SubnetId,
|
||||
IgwId: clusterConfig.Bootstrap.IgwId,
|
||||
Cidr: clusterConfig.Bootstrap.Cidr,
|
||||
SubnetCidr: clusterConfig.Bootstrap.SubnetCidr,
|
||||
}
|
||||
}
|
||||
|
||||
svc, err := service.New("vpcd", &vpcd.Config{
|
||||
NatsHost: nodeConfig.NATS.Host,
|
||||
NatsToken: nodeConfig.NATS.ACL.Token,
|
||||
OVNNBAddr: nodeConfig.VPCD.OVNNBAddr,
|
||||
OVNSBAddr: nodeConfig.VPCD.OVNSBAddr,
|
||||
BaseDir: nodeConfig.BaseDir,
|
||||
Debug: false,
|
||||
NatsHost: nodeConfig.NATS.Host,
|
||||
NatsToken: nodeConfig.NATS.ACL.Token,
|
||||
OVNNBAddr: nodeConfig.VPCD.OVNNBAddr,
|
||||
OVNSBAddr: nodeConfig.VPCD.OVNSBAddr,
|
||||
BaseDir: nodeConfig.BaseDir,
|
||||
Debug: false,
|
||||
ExternalMode: clusterConfig.Network.ExternalMode,
|
||||
ExternalPools: extPools,
|
||||
ChassisNames: chassisNames,
|
||||
Bootstrap: bootstrap,
|
||||
ExternalInterface: nodeConfig.VPCD.ExternalInterface,
|
||||
WanBridge: nodeConfig.VPCD.WanBridge,
|
||||
BridgeMode: nodeConfig.VPCD.BridgeMode,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Error starting vpcd service:", err)
|
||||
return
|
||||
}
|
||||
|
||||
svc.Start()
|
||||
if _, err = svc.Start(); err != nil {
|
||||
fmt.Println("Error starting vpcd service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("vpcd service started")
|
||||
},
|
||||
}
|
||||
@@ -694,7 +773,10 @@ var vpcdStopCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
svc.Stop()
|
||||
if err = svc.Stop(); err != nil {
|
||||
fmt.Println("Error stopping vpcd service:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("vpcd service stopped")
|
||||
},
|
||||
}
|
||||
@@ -708,8 +790,7 @@ var vpcdStatusCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
viper.SetEnvPrefix("HIVE") // Prefix for environment variables
|
||||
viper.SetEnvPrefix("SPINIFEX") // Prefix for environment variables
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
|
||||
viper.AutomaticEnv() // Read environment variables automatically
|
||||
@@ -720,47 +801,47 @@ func init() {
|
||||
|
||||
// Predastore Port
|
||||
predastoreCmd.PersistentFlags().Int("port", 8443, "Predastore (S3) port")
|
||||
viper.BindEnv("port", "HIVE_PREDASTORE_PORT")
|
||||
viper.BindEnv("port", "SPINIFEX_PREDASTORE_PORT")
|
||||
viper.BindPFlag("port", predastoreCmd.PersistentFlags().Lookup("port"))
|
||||
|
||||
// Predastore Host
|
||||
predastoreCmd.PersistentFlags().String("host", "0.0.0.0", "Predastore (S3) host")
|
||||
viper.BindEnv("host", "HIVE_PREDASTORE_HOST")
|
||||
viper.BindEnv("host", "SPINIFEX_PREDASTORE_HOST")
|
||||
viper.BindPFlag("host", predastoreCmd.PersistentFlags().Lookup("host"))
|
||||
|
||||
// Base path
|
||||
predastoreCmd.PersistentFlags().String("base-path", "", "Predastore (S3) base path")
|
||||
viper.BindEnv("base-path", "HIVE_PREDASTORE_BASE_PATH")
|
||||
viper.BindEnv("base-path", "SPINIFEX_PREDASTORE_BASE_PATH")
|
||||
viper.BindPFlag("base-path", predastoreCmd.PersistentFlags().Lookup("base-path"))
|
||||
|
||||
// Predastore Config Path
|
||||
predastoreCmd.PersistentFlags().String("config-path", "", "Predastore (S3) config path")
|
||||
viper.BindEnv("config-path", "HIVE_PREDASTORE_CONFIG_PATH")
|
||||
viper.BindEnv("config-path", "SPINIFEX_PREDASTORE_CONFIG_PATH")
|
||||
viper.BindPFlag("config-path", predastoreCmd.PersistentFlags().Lookup("config-path"))
|
||||
|
||||
// Predastore Debug
|
||||
predastoreCmd.PersistentFlags().Bool("debug", false, "Predastore (S3) debug")
|
||||
viper.BindEnv("debug", "HIVE_PREDASTORE_DEBUG")
|
||||
viper.BindEnv("debug", "SPINIFEX_PREDASTORE_DEBUG")
|
||||
viper.BindPFlag("debug", predastoreCmd.PersistentFlags().Lookup("debug"))
|
||||
|
||||
// Predastore TLS Cert
|
||||
predastoreCmd.PersistentFlags().String("tls-cert", "", "Predastore (S3) TLS certificate")
|
||||
viper.BindEnv("tls-cert", "HIVE_PREDASTORE_TLS_CERT")
|
||||
viper.BindEnv("tls-cert", "SPINIFEX_PREDASTORE_TLS_CERT")
|
||||
viper.BindPFlag("tls-cert", predastoreCmd.PersistentFlags().Lookup("tls-cert"))
|
||||
|
||||
// Predastore TLS Key
|
||||
predastoreCmd.PersistentFlags().String("tls-key", "", "Predastore (S3) TLS key")
|
||||
viper.BindEnv("tls-key", "HIVE_PREDASTORE_TLS_KEY")
|
||||
viper.BindEnv("tls-key", "SPINIFEX_PREDASTORE_TLS_KEY")
|
||||
viper.BindPFlag("tls-key", predastoreCmd.PersistentFlags().Lookup("tls-key"))
|
||||
|
||||
// Predastore Backend
|
||||
predastoreCmd.PersistentFlags().String("backend", "distributed", "Predastore (S3) backend")
|
||||
viper.BindEnv("backend", "HIVE_PREDASTORE_BACKEND")
|
||||
viper.BindEnv("backend", "SPINIFEX_PREDASTORE_BACKEND")
|
||||
viper.BindPFlag("backend", predastoreCmd.PersistentFlags().Lookup("backend"))
|
||||
|
||||
// Predastore Node ID
|
||||
predastoreCmd.PersistentFlags().Int("node-id", 0, "Predastore (S3) node ID")
|
||||
viper.BindEnv("node-id", "HIVE_PREDASTORE_NODE_ID")
|
||||
viper.BindEnv("node-id", "SPINIFEX_PREDASTORE_NODE_ID")
|
||||
viper.BindPFlag("node-id", predastoreCmd.PersistentFlags().Lookup("node-id"))
|
||||
|
||||
// Predastore CPU Profiling
|
||||
@@ -780,19 +861,19 @@ func init() {
|
||||
serviceCmd.AddCommand(viperblockCmd)
|
||||
|
||||
viperblockCmd.PersistentFlags().String("s3-host", "0.0.0.0:8443", "Predastore (S3) host URI")
|
||||
viper.BindEnv("s3-host", "HIVE_VIPERBLOCK_S3_HOST")
|
||||
viper.BindEnv("s3-host", "SPINIFEX_VIPERBLOCK_S3_HOST")
|
||||
viper.BindPFlag("s3-host", predastoreCmd.PersistentFlags().Lookup("s3-host"))
|
||||
|
||||
viperblockCmd.PersistentFlags().String("s3-bucket", "predastore", "Predastore (S3) bucket")
|
||||
viper.BindEnv("s3-bucket", "HIVE_VIPERBLOCK_S3_BUCKET")
|
||||
viper.BindEnv("s3-bucket", "SPINIFEX_VIPERBLOCK_S3_BUCKET")
|
||||
viper.BindPFlag("s3-bucket", predastoreCmd.PersistentFlags().Lookup("s3-bucket"))
|
||||
|
||||
viperblockCmd.PersistentFlags().String("s3-region", "ap-southeast-2", "Predastore (S3) region")
|
||||
viper.BindEnv("s3-region", "HIVE_VIPERBLOCK_S3_REGION")
|
||||
viper.BindEnv("s3-region", "SPINIFEX_VIPERBLOCK_S3_REGION")
|
||||
viper.BindPFlag("s3-region", predastoreCmd.PersistentFlags().Lookup("s3-region"))
|
||||
|
||||
viperblockCmd.PersistentFlags().String("plugin-path", "/opt/hive/lib/nbdkit-viperblock-plugin.so", "Pathname to the nbdkit viperblockplugin")
|
||||
viper.BindEnv("plugin-path", "HIVE_VIPERBLOCK_PLUGIN_PATH")
|
||||
viperblockCmd.PersistentFlags().String("plugin-path", "/opt/spinifex/lib/nbdkit-viperblock-plugin.so", "Pathname to the nbdkit viperblockplugin")
|
||||
viper.BindEnv("plugin-path", "SPINIFEX_VIPERBLOCK_PLUGIN_PATH")
|
||||
viper.BindPFlag("plugin-path", predastoreCmd.PersistentFlags().Lookup("plugin-path"))
|
||||
|
||||
viperblockCmd.AddCommand(viperblockStartCmd)
|
||||
@@ -808,83 +889,83 @@ func init() {
|
||||
|
||||
// Add NATS flags
|
||||
natsCmd.PersistentFlags().Int("port", 4222, "NATS server port")
|
||||
viper.BindEnv("port", "HIVE_NATS_PORT")
|
||||
viper.BindEnv("port", "SPINIFEX_NATS_PORT")
|
||||
viper.BindPFlag("port", natsCmd.PersistentFlags().Lookup("port"))
|
||||
|
||||
natsCmd.PersistentFlags().String("host", "0.0.0.0", "NATS server host")
|
||||
viper.BindEnv("host", "HIVE_NATS_HOST")
|
||||
viper.BindEnv("host", "SPINIFEX_NATS_HOST")
|
||||
viper.BindPFlag("host", natsCmd.PersistentFlags().Lookup("host"))
|
||||
|
||||
natsCmd.PersistentFlags().Bool("debug", false, "Enable debug logging")
|
||||
viper.BindEnv("debug", "HIVE_NATS_DEBUG")
|
||||
viper.BindEnv("debug", "SPINIFEX_NATS_DEBUG")
|
||||
viper.BindPFlag("debug", natsCmd.PersistentFlags().Lookup("debug"))
|
||||
|
||||
natsCmd.PersistentFlags().String("data-dir", "", "NATS data directory")
|
||||
viper.BindEnv("data-dir", "HIVE_NATS_DATA_DIR")
|
||||
viper.BindEnv("data-dir", "SPINIFEX_NATS_DATA_DIR")
|
||||
viper.BindPFlag("data-dir", natsCmd.PersistentFlags().Lookup("data-dir"))
|
||||
|
||||
natsCmd.PersistentFlags().Bool("jetstream", false, "Enable JetStream")
|
||||
viper.BindEnv("jetstream", "HIVE_NATS_JETSTREAM")
|
||||
viper.BindEnv("jetstream", "SPINIFEX_NATS_JETSTREAM")
|
||||
viper.BindPFlag("jetstream", natsCmd.PersistentFlags().Lookup("jetstream"))
|
||||
|
||||
// Hive
|
||||
serviceCmd.AddCommand(hiveCmd)
|
||||
// Spinifex
|
||||
serviceCmd.AddCommand(spinifexCmd)
|
||||
|
||||
hiveCmd.AddCommand(hiveStartCmd)
|
||||
hiveCmd.AddCommand(hiveStopCmd)
|
||||
hiveCmd.AddCommand(hiveStatusCmd)
|
||||
spinifexCmd.AddCommand(spinifexStartCmd)
|
||||
spinifexCmd.AddCommand(spinifexStopCmd)
|
||||
spinifexCmd.AddCommand(spinifexStatusCmd)
|
||||
|
||||
hiveCmd.PersistentFlags().String("wal-dir", "", "Write-ahead log (WAL) directory. Place on high-speed NVMe disk, or tmpfs for development.")
|
||||
viper.BindEnv("wal-dir", "HIVE_WAL_DIR")
|
||||
viper.BindPFlag("wal-dir", hiveCmd.PersistentFlags().Lookup("wal-dir"))
|
||||
spinifexCmd.PersistentFlags().String("wal-dir", "", "Write-ahead log (WAL) directory. Place on high-speed NVMe disk, or tmpfs for development.")
|
||||
viper.BindEnv("wal-dir", "SPINIFEX_WAL_DIR")
|
||||
viper.BindPFlag("wal-dir", spinifexCmd.PersistentFlags().Lookup("wal-dir"))
|
||||
|
||||
// AWS GW
|
||||
serviceCmd.AddCommand(awsgwCmd)
|
||||
|
||||
awsgwCmd.PersistentFlags().String("host", "0.0.0.0:9999", "AWS Gateway server host")
|
||||
viper.BindEnv("host", "HIVE_AWSGW_HOST")
|
||||
viper.BindEnv("host", "SPINIFEX_AWSGW_HOST")
|
||||
viper.BindPFlag("host", awsgwCmd.PersistentFlags().Lookup("host"))
|
||||
|
||||
// AWS GW TLS Cert
|
||||
awsgwCmd.PersistentFlags().String("tls-cert", "", "AWS Gateway TLS certificate")
|
||||
viper.BindEnv("tls-cert", "HIVE_AWSGW_TLS_CERT")
|
||||
viper.BindEnv("tls-cert", "SPINIFEX_AWSGW_TLS_CERT")
|
||||
viper.BindPFlag("tls-cert", awsgwCmd.PersistentFlags().Lookup("tls-cert"))
|
||||
|
||||
// AWS GW TLS Key
|
||||
awsgwCmd.PersistentFlags().String("tls-key", "", "AWS Gateway TLS key")
|
||||
viper.BindEnv("tls-key", "HIVE_AWSGW_TLS_KEY")
|
||||
viper.BindEnv("tls-key", "SPINIFEX_AWSGW_TLS_KEY")
|
||||
viper.BindPFlag("tls-key", awsgwCmd.PersistentFlags().Lookup("tls-key"))
|
||||
|
||||
awsgwCmd.PersistentFlags().Bool("debug", false, "AWS Gateway Debug")
|
||||
viper.BindEnv("debug", "HIVE_AWSGW_DEBUG")
|
||||
viper.BindEnv("debug", "SPINIFEX_AWSGW_DEBUG")
|
||||
viper.BindPFlag("debug", awsgwCmd.PersistentFlags().Lookup("debug"))
|
||||
|
||||
awsgwCmd.AddCommand(awsgwStartCmd)
|
||||
awsgwCmd.AddCommand(awsgwStopCmd)
|
||||
awsgwCmd.AddCommand(awsgwStatusCmd)
|
||||
|
||||
// hive-ui
|
||||
serviceCmd.AddCommand(hiveUICmd)
|
||||
// spinifex-ui
|
||||
serviceCmd.AddCommand(spinifexUICmd)
|
||||
|
||||
hiveUICmd.PersistentFlags().Int("port", 3000, "hive-ui server port")
|
||||
viper.BindEnv("hive-ui-port", "HIVE_UI_PORT")
|
||||
viper.BindPFlag("hive-ui-port", hiveUICmd.PersistentFlags().Lookup("port"))
|
||||
spinifexUICmd.PersistentFlags().Int("port", 3000, "spinifex-ui server port")
|
||||
viper.BindEnv("spinifex-ui-port", "SPINIFEX_UI_PORT")
|
||||
viper.BindPFlag("spinifex-ui-port", spinifexUICmd.PersistentFlags().Lookup("port"))
|
||||
|
||||
hiveUICmd.PersistentFlags().String("host", "0.0.0.0", "hive-ui server host")
|
||||
viper.BindEnv("hive-ui-host", "HIVE_UI_HOST")
|
||||
viper.BindPFlag("hive-ui-host", hiveUICmd.PersistentFlags().Lookup("host"))
|
||||
spinifexUICmd.PersistentFlags().String("host", "0.0.0.0", "spinifex-ui server host")
|
||||
viper.BindEnv("spinifex-ui-host", "SPINIFEX_UI_HOST")
|
||||
viper.BindPFlag("spinifex-ui-host", spinifexUICmd.PersistentFlags().Lookup("host"))
|
||||
|
||||
hiveUICmd.PersistentFlags().String("tls-cert", "", "TLS certificate path")
|
||||
viper.BindEnv("hive-ui-tls-cert", "HIVE_UI_TLS_CERT")
|
||||
viper.BindPFlag("hive-ui-tls-cert", hiveUICmd.PersistentFlags().Lookup("tls-cert"))
|
||||
spinifexUICmd.PersistentFlags().String("tls-cert", "", "TLS certificate path")
|
||||
viper.BindEnv("spinifex-ui-tls-cert", "SPINIFEX_UI_TLS_CERT")
|
||||
viper.BindPFlag("spinifex-ui-tls-cert", spinifexUICmd.PersistentFlags().Lookup("tls-cert"))
|
||||
|
||||
hiveUICmd.PersistentFlags().String("tls-key", "", "TLS key path")
|
||||
viper.BindEnv("hive-ui-tls-key", "HIVE_UI_TLS_KEY")
|
||||
viper.BindPFlag("hive-ui-tls-key", hiveUICmd.PersistentFlags().Lookup("tls-key"))
|
||||
spinifexUICmd.PersistentFlags().String("tls-key", "", "TLS key path")
|
||||
viper.BindEnv("spinifex-ui-tls-key", "SPINIFEX_UI_TLS_KEY")
|
||||
viper.BindPFlag("spinifex-ui-tls-key", spinifexUICmd.PersistentFlags().Lookup("tls-key"))
|
||||
|
||||
hiveUICmd.AddCommand(hiveUIStartCmd)
|
||||
hiveUICmd.AddCommand(hiveUIStopCmd)
|
||||
hiveUICmd.AddCommand(hiveUIStatusCmd)
|
||||
spinifexUICmd.AddCommand(spinifexUIStartCmd)
|
||||
spinifexUICmd.AddCommand(spinifexUIStopCmd)
|
||||
spinifexUICmd.AddCommand(spinifexUIStatusCmd)
|
||||
|
||||
// vpcd
|
||||
serviceCmd.AddCommand(vpcdCmd)
|
||||
@@ -892,5 +973,4 @@ func init() {
|
||||
vpcdCmd.AddCommand(vpcdStartCmd)
|
||||
vpcdCmd.AddCommand(vpcdStopCmd)
|
||||
vpcdCmd.AddCommand(vpcdStatusCmd)
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
# NATS Server Configuration
|
||||
# Generated by Hive admin init
|
||||
# Generated by Spinifex admin init
|
||||
|
||||
# Server configuration
|
||||
{{- if .BindIP }}
|
||||
@@ -22,7 +22,7 @@ cluster {
|
||||
}
|
||||
{{- end }}
|
||||
|
||||
server_name: hive-nats-{{.Node}}
|
||||
server_name: spinifex-nats-{{.Node}}
|
||||
|
||||
# Authorization
|
||||
authorization {
|
||||
+7
@@ -46,3 +46,10 @@ secret_access_key = "{{.SecretKey}}"
|
||||
policy = [
|
||||
{ bucket = "predastore", actions = ["s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListAllMyBuckets"] },
|
||||
]
|
||||
|
||||
# IAM authentication via NATS KV (enables multi-account S3 access)
|
||||
[iam]
|
||||
nats_url = "nats://{{.BindIP}}:4222"
|
||||
nats_token = "{{.NatsToken}}"
|
||||
master_key_path = "{{.ConfigDir}}/master.key"
|
||||
access_keys_bucket = "spinifex-iam-access-keys"
|
||||
@@ -79,4 +79,11 @@ access_key_id = "{{.AccessKey}}"
|
||||
secret_access_key = "{{.SecretKey}}"
|
||||
policy = [
|
||||
{ bucket = "predastore", actions = ["s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListAllMyBuckets"] },
|
||||
]
|
||||
]
|
||||
|
||||
# IAM authentication via NATS KV (enables multi-account S3 access)
|
||||
[iam]
|
||||
nats_url = "nats://{{if eq .BindIP "0.0.0.0"}}localhost{{else}}{{.BindIP}}{{end}}:4222"
|
||||
nats_token = "{{.NatsToken}}"
|
||||
master_key_path = "{{.ConfigDir}}/master.key"
|
||||
access_keys_bucket = "spinifex-iam-access-keys"
|
||||
@@ -2,6 +2,42 @@
|
||||
version = "1.0"
|
||||
epoch = 1
|
||||
node = "{{.Node}}"
|
||||
{{- if .ExternalMode}}
|
||||
|
||||
[network]
|
||||
external_mode = "{{.ExternalMode}}"
|
||||
{{- if .ExternalDHCP}}
|
||||
external_dhcp = true
|
||||
{{- end}}
|
||||
|
||||
[[network.external_pools]]
|
||||
name = "{{.PoolName}}"
|
||||
{{- if .PoolSource}}
|
||||
source = "{{.PoolSource}}"
|
||||
{{- end}}
|
||||
{{- if .PoolStart}}
|
||||
range_start = "{{.PoolStart}}"
|
||||
range_end = "{{.PoolEnd}}"
|
||||
{{- end}}
|
||||
gateway = "{{.PoolGateway}}"
|
||||
{{- if .PoolGatewayIP}}
|
||||
gateway_ip = "{{.PoolGatewayIP}}"
|
||||
{{- end}}
|
||||
prefix_len = {{if .PoolPrefixLen}}{{.PoolPrefixLen}}{{else}}24{{end}}
|
||||
{{- if .PoolDNSServers}}
|
||||
dns_servers = [{{range $i, $s := .PoolDNSServers}}{{if $i}}, {{end}}"{{$s}}"{{end}}]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- if .BootstrapVpcId}}
|
||||
|
||||
[bootstrap]
|
||||
account_id = "{{.BootstrapAccountId}}"
|
||||
vpc_id = "{{.BootstrapVpcId}}"
|
||||
subnet_id = "{{.BootstrapSubnetId}}"
|
||||
igw_id = "{{.BootstrapIgwId}}"
|
||||
cidr = "{{.BootstrapCidr}}"
|
||||
subnet_cidr = "{{.BootstrapSubnetCidr}}"
|
||||
{{- end}}
|
||||
|
||||
[nodes.{{.Node}}]
|
||||
node = "{{.Node}}"
|
||||
@@ -18,7 +54,11 @@ account_id = "{{.AccountID}}"
|
||||
host = "{{.BindIP}}:{{.Port}}"
|
||||
tlskey = "config/server.key"
|
||||
tlscert = "config/server.pem"
|
||||
{{- if .ExternalMode}}
|
||||
dev_networking = false
|
||||
{{- else}}
|
||||
dev_networking = true
|
||||
{{- end}}
|
||||
|
||||
[nodes.{{.Node}}.awsgw]
|
||||
host = "{{.BindIP}}:9999"
|
||||
@@ -39,8 +79,14 @@ token = "{{.NatsToken}}"
|
||||
shardwal = false
|
||||
|
||||
[nodes.{{.Node}}.vpcd]
|
||||
ovn_nb_addr = "tcp:127.0.0.1:6641"
|
||||
ovn_sb_addr = "tcp:127.0.0.1:6642"
|
||||
ovn_nb_addr = "{{.OVNNBAddr}}"
|
||||
ovn_sb_addr = "{{.OVNSBAddr}}"
|
||||
{{- if .ExternalIface}}
|
||||
external_interface = "{{.ExternalIface}}"
|
||||
{{- end}}
|
||||
{{- if .WanBridge}}
|
||||
wan_bridge = "{{.WanBridge}}"
|
||||
{{- end}}
|
||||
|
||||
[nodes.{{.Node}}.predastore]
|
||||
host = "{{.BindIP}}:8443"
|
||||
@@ -61,4 +107,4 @@ az = "{{.AZ}}"
|
||||
{{- if .Services}}
|
||||
services = [{{range $i, $s := .Services}}{{if $i}}, {{end}}"{{$s}}"{{end}}]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
@@ -1,17 +1,18 @@
|
||||
/*
|
||||
Copyright © 2025 Mulga Defense Corporation
|
||||
Copyright © 2026 Mulga Defense Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
@@ -22,7 +23,7 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/hive/hive/types"
|
||||
"github.com/mulgadc/spinifex/spinifex/types"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -56,7 +57,7 @@ func runTopNodes(cmd *cobra.Command, args []string) {
|
||||
defer nc.Close()
|
||||
|
||||
timeout, _ := cmd.Flags().GetDuration("timeout")
|
||||
responses, err := collectResponses(nc, "hive.node.status", timeout)
|
||||
responses, err := collectResponses(nc, "spinifex.node.status", timeout)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -0,0 +1,27 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Version variables set via ldflags at build time.
|
||||
// Example: go build -ldflags "-X github.com/mulgadc/spinifex/cmd/spinifex/cmd.Version=v1.0.0"
|
||||
var (
|
||||
Version = "dev"
|
||||
Commit = "unknown"
|
||||
)
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the Spinifex version",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("spinifex %s (%s) %s/%s\n", Version, Commit, runtime.GOOS, runtime.GOARCH)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersionVarsExist(t *testing.T) {
|
||||
// Verify the version variables are set (defaults when not injected via ldflags)
|
||||
if Version == "" {
|
||||
t.Error("Version should not be empty")
|
||||
}
|
||||
if Commit == "" {
|
||||
t.Error("Commit should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionCommand(t *testing.T) {
|
||||
// Verify the version command is registered
|
||||
found := false
|
||||
for _, cmd := range rootCmd.Commands() {
|
||||
if cmd.Use == "version" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("version command not registered on rootCmd")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright © 2026 Mulga Defense Corporation
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package main
|
||||
|
||||
import "github.com/mulgadc/spinifex/cmd/spinifex/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Linux
|
||||
#genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-data
|
||||
|
||||
# MacOS
|
||||
mkisofs -output seed.iso -volid cidata -joliet -rock meta-data user-data
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Example to launch nbdkit
|
||||
|
||||
./server/nbdkit -p 10810 --pidfile /tmp/vb-vol-1-efi.pid ./plugins/golang/examples/ramdisk/nbdkit-goramdisk-plugin.so -v -f size=67108864 volume=vol-50c968e8dbcc74bd2-efi bucket=predastore region=ap-southeast-2 access_key="AKIAIOSFODNN7EXAMPLE" secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" base_dir="/tmp/vb/" host="https://127.0.0.1:8443" cache_size=1
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
qemu-system-aarch64 \
|
||||
-nographic \
|
||||
-M virt,highmem=off \
|
||||
-accel hvf \
|
||||
-cpu host \
|
||||
-smp 4 \
|
||||
-m 3000 \
|
||||
-drive file=/Users/benduncan/qemu/QEMU_EFI.img,if=pflash,format=raw \
|
||||
-device virtio-blk-pci,drive=debian,bootindex=1 \
|
||||
-drive if=none,media=disk,id=debian,format=raw,file=nbd://192.168.64.5:10809/default \
|
||||
-drive format=raw,file=nbd://192.168.64.5:10810/default,if=pflash \
|
||||
-drive file=nbd://192.168.64.5:10811/default,format=raw,if=virtio \
|
||||
-device qemu-xhci \
|
||||
-device usb-kbd \
|
||||
-device usb-tablet \
|
||||
-device intel-hda \
|
||||
-device hda-duplex \
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22 -device virtio-net-device,netdev=net0 \
|
||||
-device virtio-rng-pci
|
||||
|
||||
|
||||
# ./server/nbdkit -p 10809 --pidfile /tmp/vb-vol-1.pid ./plugins/golang/examples/ramdisk/nbdkit-goramdisk-plugin.so -v -f size=4294967296 volume=vol-e660ff0065ad21f74 bucket=predastore region=ap-southeast-2 access_key="AKIAIOSFODNN7EXAMPLE" secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" base_dir="/tmp/vb/" host="https://127.0.0.1:8443" cache_size=20
|
||||
|
||||
# ./server/nbdkit -p 10810 --pidfile /tmp/vb-vol-1-efi.pid ./plugins/golang/examples/ramdisk/nbdkit-goramdisk-plugin.so -v -f size=67108864 volume=vol-e660ff0065ad21f74-efi bucket=predastore region=ap-southeast-2 access_key="AKIAIOSFODNN7EXAMPLE" secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" base_dir="/tmp/vb/" host="https://127.0.0.1:8443" cache_size=1
|
||||
|
||||
# ./server/nbdkit -p 10811 --pidfile /tmp/vb-vol-1-cloudinit.pid ./plugins/golang/examples/ramdisk/nbdkit-goramdisk-plugin.so -v -f size=1048576 volume=vol-e660ff0065ad21f74-cloudinit bucket=predastore region=ap-southeast-2 access_key="AKIAIOSFODNN7EXAMPLE" secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" base_dir="/tmp/vb/" host="https://127.0.0.1:8443" cache_size=1
|
||||
+44
-44
@@ -1,71 +1,71 @@
|
||||
# Hive Systemd Service
|
||||
# Spinifex Systemd Service
|
||||
|
||||
This directory contains the systemd service configuration for running Hive services on Ubuntu and Debian systems.
|
||||
This directory contains the systemd service configuration for running Spinifex services on Ubuntu and Debian systems.
|
||||
|
||||
## Files
|
||||
|
||||
- `hive.service` - The systemd service file that manages all Hive services
|
||||
- `install-hive-service.sh` - Installation script to set up the service
|
||||
- `spinifex.service` - The systemd service file that manages all Spinifex services
|
||||
- `install-spinifex-service.sh` - Installation script to set up the service
|
||||
- `README.md` - This documentation file
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Ensure Hive is installed in `/opt/hive/`
|
||||
2. Ensure the `hive` binary is available at `/opt/hive/bin/hive`
|
||||
1. Ensure Spinifex is installed in `/opt/spinifex/`
|
||||
2. Ensure the `spx` binary is available at `/opt/spinifex/bin/spx`
|
||||
3. Run the installation script as root
|
||||
|
||||
### Setup Configuration
|
||||
|
||||
```
|
||||
/opt/hive/bin/hive admin init
|
||||
/opt/spinifex/bin/spx admin init
|
||||
```
|
||||
|
||||
This will init the Hive environment and create the default configuration files in the home directory, e.g `~/hive/`.
|
||||
This will init the Spinifex environment and create the default configuration files in the home directory, e.g `~/spinifex/`.
|
||||
|
||||
```
|
||||
/home/username/hive:
|
||||
/home/username/spinifex:
|
||||
amis config images logs nats predastore state viperblock volumes
|
||||
|
||||
/home/username/hive/amis:
|
||||
/home/username/spinifex/amis:
|
||||
|
||||
/home/username/hive/config:
|
||||
awsgw hive.toml nats predastore server.key server.pem
|
||||
/home/username/spinifex/config:
|
||||
awsgw spinifex.toml nats predastore server.key server.pem
|
||||
|
||||
/home/username/hive/config/awsgw:
|
||||
/home/username/spinifex/config/awsgw:
|
||||
awsgw.toml
|
||||
|
||||
/home/username/hive/config/nats:
|
||||
/home/username/spinifex/config/nats:
|
||||
nats.conf
|
||||
|
||||
/home/username/hive/config/predastore:
|
||||
/home/username/spinifex/config/predastore:
|
||||
predastore.toml
|
||||
|
||||
/home/username/hive/images:
|
||||
/home/username/spinifex/images:
|
||||
|
||||
/home/username/hive/logs:
|
||||
awsgw.log awsgw.pid hive.log hive.pid nats.log nats.pid predastore.log predastore.pid viperblock.log viperblock.pid
|
||||
/home/username/spinifex/logs:
|
||||
awsgw.log awsgw.pid spinifex.log spinifex.pid nats.log nats.pid predastore.log predastore.pid viperblock.log viperblock.pid
|
||||
|
||||
/home/username/hive/nats:
|
||||
/home/username/spinifex/nats:
|
||||
|
||||
/home/username/hive/predastore:
|
||||
/home/username/spinifex/predastore:
|
||||
data
|
||||
|
||||
/home/username/hive/predastore/data:
|
||||
/home/username/spinifex/predastore/data:
|
||||
predastore
|
||||
|
||||
/home/ben/hive/state:
|
||||
/home/ben/spinifex/state:
|
||||
|
||||
/home/ben/hive/viperblock:
|
||||
/home/ben/spinifex/viperblock:
|
||||
|
||||
/home/ben/hive/volumes:
|
||||
/home/ben/spinifex/volumes:
|
||||
```
|
||||
|
||||
### Quick Installation
|
||||
|
||||
```bash
|
||||
sudo ./config/install-hive-service.sh
|
||||
sudo ./config/install-spinifex-service.sh
|
||||
```
|
||||
|
||||
### Manual Installation
|
||||
@@ -74,7 +74,7 @@ If you prefer to install manually:
|
||||
|
||||
1. Copy the service file:
|
||||
```bash
|
||||
sudo cp config/hive.service /etc/systemd/system/
|
||||
sudo cp config/spinifex.service /etc/systemd/system/
|
||||
```
|
||||
|
||||
2. Reload systemd:
|
||||
@@ -84,44 +84,44 @@ If you prefer to install manually:
|
||||
|
||||
3. Enable the service:
|
||||
```bash
|
||||
sudo systemctl enable hive.service
|
||||
sudo systemctl enable spinifex.service
|
||||
```
|
||||
|
||||
## Service Management
|
||||
|
||||
### Start the service
|
||||
```bash
|
||||
sudo systemctl start hive
|
||||
sudo systemctl start spinifex
|
||||
```
|
||||
|
||||
### Stop the service
|
||||
```bash
|
||||
sudo systemctl stop hive
|
||||
sudo systemctl stop spinifex
|
||||
```
|
||||
|
||||
### Check service status
|
||||
```bash
|
||||
sudo systemctl status hive
|
||||
sudo systemctl status spinifex
|
||||
```
|
||||
|
||||
### Restart the service
|
||||
```bash
|
||||
sudo systemctl restart hive
|
||||
sudo systemctl restart spinifex
|
||||
```
|
||||
|
||||
### View service logs
|
||||
```bash
|
||||
sudo journalctl -u hive -f
|
||||
sudo journalctl -u spinifex -f
|
||||
```
|
||||
|
||||
### Disable auto-start
|
||||
```bash
|
||||
sudo systemctl disable hive
|
||||
sudo systemctl disable spinifex
|
||||
```
|
||||
|
||||
## Service Details
|
||||
|
||||
The `hive.service` file:
|
||||
The `spinifex.service` file:
|
||||
|
||||
- Starts services in the correct order: NATS → Predastore → Viperblock
|
||||
- Includes proper delays between service starts to ensure dependencies are ready
|
||||
@@ -143,41 +143,41 @@ The service starts the following components in order:
|
||||
|
||||
### Check if the service is running
|
||||
```bash
|
||||
sudo systemctl is-active hive
|
||||
sudo systemctl is-active spinifex
|
||||
```
|
||||
|
||||
### View recent logs
|
||||
```bash
|
||||
sudo journalctl -u hive --since "1 hour ago"
|
||||
sudo journalctl -u spinifex --since "1 hour ago"
|
||||
```
|
||||
|
||||
### Check service configuration
|
||||
```bash
|
||||
sudo systemctl cat hive
|
||||
sudo systemctl cat spinifex
|
||||
```
|
||||
|
||||
### Verify binary exists
|
||||
```bash
|
||||
ls -la /opt/hive/bin/hive
|
||||
ls -la /opt/spinifex/bin/spx
|
||||
```
|
||||
|
||||
### Test service commands manually
|
||||
```bash
|
||||
/opt/hive/bin/hive service nats start
|
||||
/opt/hive/bin/hive service predastore start
|
||||
/opt/hive/bin/hive service viperblock start
|
||||
/opt/spinifex/bin/spx service nats start
|
||||
/opt/spinifex/bin/spx service predastore start
|
||||
/opt/spinifex/bin/spx service viperblock start
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
If you need to modify the service configuration:
|
||||
|
||||
1. Edit the service file: `sudo nano /etc/systemd/system/hive.service`
|
||||
1. Edit the service file: `sudo nano /etc/systemd/system/spinifex.service`
|
||||
2. Reload systemd: `sudo systemctl daemon-reload`
|
||||
3. Restart the service: `sudo systemctl restart hive`
|
||||
3. Restart the service: `sudo systemctl restart spinifex`
|
||||
|
||||
## Security Notes
|
||||
|
||||
- The service runs as root, which is necessary for system-level services
|
||||
- Security hardening is enabled with `ProtectSystem=strict` and `NoNewPrivileges=true`
|
||||
- Only specific paths are writable: `/opt/hive`, `/var/log`, `/var/run`
|
||||
- Only specific paths are writable: `/opt/spinifex`, `/var/log`, `/var/run`
|
||||
@@ -1,49 +0,0 @@
|
||||
[Unit]
|
||||
Description=Hive Services
|
||||
Documentation=https://github.com/mulgadc/hive
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/opt/hive
|
||||
Environment=PATH=/opt/hive/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
# Start NATS service
|
||||
ExecStart=/opt/hive/bin/hive service nats start
|
||||
ExecStartPost=/bin/sleep 3
|
||||
|
||||
# Start Predastore service
|
||||
ExecStart=/opt/hive/bin/hive service predastore start
|
||||
ExecStartPost=/bin/sleep 2
|
||||
|
||||
# Start Viperblock service
|
||||
ExecStart=/opt/hive/bin/hive service viperblock start
|
||||
|
||||
# Stop services in reverse order
|
||||
ExecStop=/opt/hive/bin/hive service viperblock stop
|
||||
ExecStopPost=/bin/sleep 2
|
||||
ExecStop=/opt/hive/bin/hive service predastore stop
|
||||
ExecStopPost=/bin/sleep 2
|
||||
ExecStop=/opt/hive/bin/hive service nats stop
|
||||
|
||||
# Restart policy
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/opt/hive /var/log /var/run
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=hive
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Hive Service Installation Script
|
||||
# This script installs the hive systemd service on Ubuntu/Debian systems
|
||||
# Spinifex Service Installation Script
|
||||
# This script installs the spinifex systemd service on Ubuntu/Debian systems
|
||||
|
||||
set -e
|
||||
|
||||
@@ -38,7 +38,7 @@ fi
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SERVICE_FILE="$SCRIPT_DIR/hive.service"
|
||||
SERVICE_FILE="$SCRIPT_DIR/spinifex.service"
|
||||
|
||||
# Check if service file exists
|
||||
if [[ ! -f "$SERVICE_FILE" ]]; then
|
||||
@@ -46,7 +46,7 @@ if [[ ! -f "$SERVICE_FILE" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_status "Installing Hive systemd service..."
|
||||
print_status "Installing Spinifex systemd service..."
|
||||
|
||||
# Copy service file to systemd directory
|
||||
cp "$SERVICE_FILE" /etc/systemd/system/
|
||||
@@ -55,20 +55,20 @@ cp "$SERVICE_FILE" /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
|
||||
# Enable the service to start on boot
|
||||
systemctl enable hive.service
|
||||
systemctl enable spinifex.service
|
||||
|
||||
print_status "Service installed successfully!"
|
||||
print_status "You can now use the following commands:"
|
||||
echo " systemctl start hive # Start the service"
|
||||
echo " systemctl stop hive # Stop the service"
|
||||
echo " systemctl status hive # Check service status"
|
||||
echo " systemctl restart hive # Restart the service"
|
||||
echo " journalctl -u hive -f # View service logs"
|
||||
echo " systemctl start spinifex # Start the service"
|
||||
echo " systemctl stop spinifex # Stop the service"
|
||||
echo " systemctl status spinifex # Check service status"
|
||||
echo " systemctl restart spinifex # Restart the service"
|
||||
echo " journalctl -u spinifex -f # View service logs"
|
||||
|
||||
# Check if hive binary exists
|
||||
if [[ ! -f "/opt/hive/bin/hive" ]]; then
|
||||
print_warning "Hive binary not found at /opt/hive/bin/hive"
|
||||
print_warning "Please ensure hive is installed in /opt/hive before starting the service"
|
||||
# Check if spinifex binary exists
|
||||
if [[ ! -f "/opt/spinifex/bin/spinifex" ]]; then
|
||||
print_warning "Spinifex binary not found at /opt/spinifex/bin/spinifex"
|
||||
print_warning "Please ensure spinifex is installed in /opt/spinifex before starting the service"
|
||||
fi
|
||||
|
||||
print_status "Installation complete!"
|
||||
@@ -0,0 +1,49 @@
|
||||
[Unit]
|
||||
Description=Spinifex Services
|
||||
Documentation=https://github.com/mulgadc/spinifex
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/opt/spinifex
|
||||
Environment=PATH=/opt/spinifex/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
# Start NATS service
|
||||
ExecStart=/opt/spinifex/bin/spinifex service nats start
|
||||
ExecStartPost=/bin/sleep 3
|
||||
|
||||
# Start Predastore service
|
||||
ExecStart=/opt/spinifex/bin/spinifex service predastore start
|
||||
ExecStartPost=/bin/sleep 2
|
||||
|
||||
# Start Viperblock service
|
||||
ExecStart=/opt/spinifex/bin/spinifex service viperblock start
|
||||
|
||||
# Stop services in reverse order
|
||||
ExecStop=/opt/spinifex/bin/spinifex service viperblock stop
|
||||
ExecStopPost=/bin/sleep 2
|
||||
ExecStop=/opt/spinifex/bin/spinifex service predastore stop
|
||||
ExecStopPost=/bin/sleep 2
|
||||
ExecStop=/opt/spinifex/bin/spinifex service nats stop
|
||||
|
||||
# Restart policy
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/opt/spinifex /var/log /var/run
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=spinifex
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+259
-112
@@ -1,36 +1,36 @@
|
||||
# Command Implementation Matrix
|
||||
|
||||
## Hive Admin CLI
|
||||
## Spinifex Admin CLI
|
||||
|
||||
Platform management commands not exposed via the AWS gateway API. These are CLI-only commands.
|
||||
|
||||
### Service Management
|
||||
|
||||
Service lifecycle commands for starting, stopping, and checking status of all Hive cluster services. Each service subcommand supports `start`, `stop`, and `status` operations.
|
||||
Service lifecycle commands for starting, stopping, and checking status of all Spinifex cluster services. Each service subcommand supports `start`, `stop`, and `status` operations.
|
||||
|
||||
| Command | Flags | Prerequisites | Basic Logic | Status |
|
||||
|---------|-------|---------------|-------------|--------|
|
||||
| `hive service predastore start` | `--port` (default: 8443), `--host` (default: 0.0.0.0), `--base-path`, `--config-path`, `--debug`, `--tls-cert`, `--tls-key`, `--backend` (distributed/filesystem, default: distributed), `--node-id` (default: 0), `--pprof`, `--pprof-output` | TLS cert/key, base-path, config-path required | Creates predastore service instance with S3-compatible storage backend → starts service | **DONE** |
|
||||
| `hive service predastore stop` | — | Predastore must be running | Stops the predastore service | **DONE** |
|
||||
| `hive service predastore status` | — | None | Reports predastore service status | **DONE** |
|
||||
| `hive service viperblock start` | `--s3-host` (default: 0.0.0.0:8443), `--s3-bucket` (default: predastore), `--s3-region` (default: ap-southeast-2), `--plugin-path` (default: /opt/hive/lib/nbdkit-viperblock-plugin.so) | `--config` required, plugin-path must exist on disk | Loads cluster config → connects to NATS and Predastore → starts viperblock block storage service with NBD plugin | **DONE** |
|
||||
| `hive service viperblock stop` | — | Viperblock must be running | Stops the viperblock service | **DONE** |
|
||||
| `hive service viperblock status` | — | None | Reports viperblock service status | **DONE** |
|
||||
| `hive service nats start` | `--port` (default: 4222), `--host` (default: 0.0.0.0), `--debug`, `--data-dir`, `--jetstream` | None | Starts embedded NATS server with optional JetStream | **DONE** |
|
||||
| `hive service nats stop` | — | NATS must be running | Stops the NATS service | **DONE** |
|
||||
| `hive service nats status` | — | None | Reports NATS service status | **DONE** |
|
||||
| `hive service hive start` | `--wal-dir` | `--config` required | Loads cluster config → starts hive daemon (VM orchestration, NATS subscriptions, health endpoint) | **DONE** |
|
||||
| `hive service hive stop` | — | Hive daemon must be running | Stops the hive daemon service | **DONE** |
|
||||
| `hive service hive status` | — | None | Reports hive daemon service status | **DONE** |
|
||||
| `hive service awsgw start` | `--host` (default: 0.0.0.0:9999), `--tls-cert`, `--tls-key`, `--debug` | `--config` required | Loads cluster config → starts AWS-compatible gateway with SigV4 auth, IAM policy enforcement, TLS | **DONE** |
|
||||
| `hive service awsgw stop` | — | AWS gateway must be running | Stops the AWS gateway service | **DONE** |
|
||||
| `hive service awsgw status` | — | None | Reports AWS gateway service status | **DONE** |
|
||||
| `hive service hive-ui start` | `--port` (default: 3000), `--host` (default: 0.0.0.0), `--tls-cert`, `--tls-key` | None | Starts embedded web UI server serving the React frontend. Aliases: `ui`, `hiveui` | **DONE** |
|
||||
| `hive service hive-ui stop` | — | hive-ui must be running | Stops the hive-ui service | **DONE** |
|
||||
| `hive service hive-ui status` | — | None | Reports hive-ui service status | **DONE** |
|
||||
| `hive service vpcd start` | — | `--config` required, OVN/OVS installed | Loads cluster config → starts VPC daemon (subscribes to `vpc.*` NATS events, translates to OVN logical switches/ports/routers) | **DONE** |
|
||||
| `hive service vpcd stop` | — | vpcd must be running | Stops the vpcd service | **DONE** |
|
||||
| `hive service vpcd status` | — | None | Reports vpcd service status | **DONE** |
|
||||
| `spx service predastore start` | `--port` (default: 8443), `--host` (default: 0.0.0.0), `--base-path`, `--config-path`, `--debug`, `--tls-cert`, `--tls-key`, `--backend` (distributed/filesystem, default: distributed), `--node-id` (default: 0), `--pprof`, `--pprof-output` | TLS cert/key, base-path, config-path required | Creates predastore service instance with S3-compatible storage backend → starts service | **DONE** |
|
||||
| `spx service predastore stop` | — | Predastore must be running | Stops the predastore service | **DONE** |
|
||||
| `spx service predastore status` | — | None | Reports predastore service status | **DONE** |
|
||||
| `spx service viperblock start` | `--s3-host` (default: 0.0.0.0:8443), `--s3-bucket` (default: predastore), `--s3-region` (default: ap-southeast-2), `--plugin-path` (default: /opt/spinifex/lib/nbdkit-viperblock-plugin.so) | `--config` required, plugin-path must exist on disk | Loads cluster config → connects to NATS and Predastore → starts viperblock block storage service with NBD plugin | **DONE** |
|
||||
| `spx service viperblock stop` | — | Viperblock must be running | Stops the viperblock service | **DONE** |
|
||||
| `spx service viperblock status` | — | None | Reports viperblock service status | **DONE** |
|
||||
| `spx service nats start` | `--port` (default: 4222), `--host` (default: 0.0.0.0), `--debug`, `--data-dir`, `--jetstream` | None | Starts embedded NATS server with optional JetStream | **DONE** |
|
||||
| `spx service nats stop` | — | NATS must be running | Stops the NATS service | **DONE** |
|
||||
| `spx service nats status` | — | None | Reports NATS service status | **DONE** |
|
||||
| `spx service spinifex start` | `--wal-dir` | `--config` required | Loads cluster config → starts spinifex daemon (VM orchestration, NATS subscriptions, health endpoint) | **DONE** |
|
||||
| `spx service spinifex stop` | — | Spinifex daemon must be running | Stops the spinifex daemon service | **DONE** |
|
||||
| `spx service spinifex status` | — | None | Reports spinifex daemon service status | **DONE** |
|
||||
| `spx service awsgw start` | `--host` (default: 0.0.0.0:9999), `--tls-cert`, `--tls-key`, `--debug` | `--config` required | Loads cluster config → starts AWS-compatible gateway with SigV4 auth, IAM policy enforcement, TLS | **DONE** |
|
||||
| `spx service awsgw stop` | — | AWS gateway must be running | Stops the AWS gateway service | **DONE** |
|
||||
| `spx service awsgw status` | — | None | Reports AWS gateway service status | **DONE** |
|
||||
| `spx service spinifex-ui start` | `--port` (default: 3000), `--host` (default: 0.0.0.0), `--tls-cert`, `--tls-key` | None | Starts embedded web UI server serving the React frontend. Aliases: `ui`, `spinifexui` | **DONE** |
|
||||
| `spx service spinifex-ui stop` | — | spinifex-ui must be running | Stops the spinifex-ui service | **DONE** |
|
||||
| `spx service spinifex-ui status` | — | None | Reports spinifex-ui service status | **DONE** |
|
||||
| `spx service vpcd start` | — | `--config` required, OVN/OVS installed | Loads cluster config → starts VPC daemon (subscribes to `vpc.*` NATS events, translates to OVN logical switches/ports/routers) | **DONE** |
|
||||
| `spx service vpcd stop` | — | vpcd must be running | Stops the vpcd service | **DONE** |
|
||||
| `spx service vpcd status` | — | None | Reports vpcd service status | **DONE** |
|
||||
|
||||
### Cluster Inspection
|
||||
|
||||
@@ -38,41 +38,53 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
|
||||
| Command | Flags | Prerequisites | Basic Logic | Output Columns | Status |
|
||||
|---------|-------|---------------|-------------|----------------|--------|
|
||||
| `hive get nodes` | `--timeout` (default: 3s) | Cluster must be running (NATS) | Loads config → publishes to `hive.node.status` fan-out topic → collects responses within timeout → merges config-known nodes with NATS responders → nodes that don't respond shown as `NotReady` | NAME, STATUS, IP, REGION, AZ, UPTIME, VMs, SERVICES | **DONE** |
|
||||
| `hive get vms` | `--timeout` (default: 3s) | Cluster must be running (NATS) | Publishes to `hive.node.vms` fan-out topic → collects VM info from all nodes → sorts by node then instance ID → prints table. Alias: `hive get instances` | INSTANCE, STATUS, TYPE, VCPU, MEM, NODE, IP, AGE | **DONE** |
|
||||
| `spx get nodes` | `--timeout` (default: 3s) | Cluster must be running (NATS) | Loads config → publishes to `spinifex.node.status` fan-out topic → collects responses within timeout → merges config-known nodes with NATS responders → nodes that don't respond shown as `NotReady` | NAME, STATUS, IP, REGION, AZ, UPTIME, VMs, SERVICES | **DONE** |
|
||||
| `spx get vms` | `--timeout` (default: 3s) | Cluster must be running (NATS) | Publishes to `spinifex.node.vms` fan-out topic → collects VM info from all nodes → sorts by node then instance ID → prints table. Alias: `spx get instances` | INSTANCE, STATUS, TYPE, VCPU, MEM, NODE, IP, AGE | **DONE** |
|
||||
|
||||
### Resource Monitoring
|
||||
|
||||
| Command | Flags | Prerequisites | Basic Logic | Output | Status |
|
||||
|---------|-------|---------------|-------------|--------|--------|
|
||||
| `hive top nodes` | `--timeout` (default: 3s) | Cluster must be running (NATS) | Publishes to `hive.node.status` fan-out topic → collects CPU/memory usage per node → aggregates instance type capacity across all nodes → prints two tables: per-node resource usage and cluster-wide instance type availability | Table 1: NAME, CPU (used/total), MEM (used/total), VMs. Table 2: INSTANCE TYPE, AVAILABLE, VCPU, MEMORY | **DONE** |
|
||||
| `spx top nodes` | `--timeout` (default: 3s) | Cluster must be running (NATS) | Publishes to `spinifex.node.status` fan-out topic → collects CPU/memory usage per node → aggregates instance type capacity across all nodes → prints two tables: per-node resource usage and cluster-wide instance type availability | Table 1: NAME, CPU (used/total), MEM (used/total), VMs. Table 2: INSTANCE TYPE, AVAILABLE, VCPU, MEMORY | **DONE** |
|
||||
|
||||
### Cluster Initialization
|
||||
|
||||
| Command | Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------|---------------|-------------|------------|--------|
|
||||
| `hive admin init` | `--nodes`, `--node`, `--bind`, `--port`, `--region`, `--az`, `--cluster-name`, `--cluster-bind`, `--cluster-routes`, `--predastore-nodes`, `--services`, `--formation-timeout`, `--force` | None (first-time setup) | Generates root IAM credentials (AKIA-prefixed access key + secret) → creates master.key (AES-256, 32 bytes, 0600) → writes bootstrap.json (consumed on first start) → generates CA + server TLS certificates → creates NATS config with auth token → writes hive.toml, awsgw.toml, predastore.toml → configures AWS CLI `hive` profile → creates directory structure under `~/hive/` | 1. Init creates all config files<br>2. Root credentials printed once<br>3. master.key is 32 bytes, mode 0600<br>4. bootstrap.json consumed on first start<br>5. `--force` re-initializes existing config<br>6. AWS CLI profile `hive` auto-configured | **DONE** |
|
||||
| `hive admin join` | `--host` (required), `--node` (required), `--bind`, `--port`, `--region`, `--az`, `--cluster-bind`, `--cluster-routes`, `--data-dir`, `--services` | Leader node must be running | Connects to leader node → retrieves cluster configuration → configures local node to join cluster and participate in distributed operations | 1. Join existing cluster<br>2. Missing host (error)<br>3. Missing node name (error) | **DONE** |
|
||||
| `spx admin init` | `--nodes`, `--node`, `--bind`, `--port`, `--region`, `--az`, `--cluster-name`, `--cluster-bind`, `--cluster-routes`, `--predastore-nodes`, `--services`, `--formation-timeout`, `--force` | None (first-time setup) | Generates root IAM credentials (AKIA-prefixed access key + secret) → creates master.key (AES-256, 32 bytes, 0600) → writes bootstrap.json (consumed on first start) → generates CA + server TLS certificates → creates NATS config with auth token → writes spinifex.toml, awsgw.toml, predastore.toml → configures AWS CLI `spx` profile → creates directory structure under `~/spinifex/` | 1. Init creates all config files<br>2. Root credentials printed once<br>3. master.key is 32 bytes, mode 0600<br>4. bootstrap.json consumed on first start<br>5. `--force` re-initializes existing config<br>6. AWS CLI profile `spx` auto-configured | **DONE** |
|
||||
| `spx admin join` | `--host` (required), `--node` (required), `--bind`, `--port`, `--region`, `--az`, `--cluster-bind`, `--cluster-routes`, `--data-dir`, `--services` | Leader node must be running | Connects to leader node → retrieves cluster configuration → configures local node to join cluster and participate in distributed operations | 1. Join existing cluster<br>2. Missing host (error)<br>3. Missing node name (error) | **DONE** |
|
||||
|
||||
### Version
|
||||
|
||||
| Command | Flags | Prerequisites | Basic Logic | Status |
|
||||
|---------|-------|---------------|-------------|--------|
|
||||
| `spx version` | — | None | Prints Spinifex version, commit hash, OS, and architecture (populated via build-time ldflags) | **DONE** |
|
||||
|
||||
### Cluster Operations
|
||||
|
||||
| Command | Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------|---------------|-------------|------------|--------|
|
||||
| `hive admin cluster shutdown` | — | Cluster must be running | Gracefully shuts down the entire cluster | 1. Shutdown running cluster<br>2. All nodes stop cleanly | **DONE** |
|
||||
| `spx admin cluster shutdown` | `--force` (shutdown even if nodes don't respond), `--timeout` (max wait per phase, default 120s), `--dry-run` (print phase plan without executing) | Cluster must be running | Performs coordinated, phased shutdown of entire cluster. Phases execute in order: GATE (stop API/UI) → DRAIN (stop VMs) → STORAGE (stop viperblock) → PERSIST (stop predastore) → INFRA (stop NATS/daemon). Each phase waits for all nodes to ACK before proceeding. Uses JetStream state tracking. | 1. Shutdown running cluster<br>2. All nodes stop cleanly<br>3. Force shutdown with unresponsive nodes<br>4. Dry-run prints plan | **DONE** |
|
||||
|
||||
### Certificate Management
|
||||
|
||||
| Command | Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------|---------------|-------------|------------|--------|
|
||||
| `spx admin cert renew` | `--extra-ip` (additional IPs for SANs), `--extra-dns` (additional DNS names for SANs) | Existing CA from `spx admin init` | Reads existing CA → regenerates server certificate with all current network interface IPs and machine hostname in SANs → writes new cert. Use after adding a new network interface or changing IP addresses. | 1. Renew with auto-detected IPs<br>2. Renew with extra IPs<br>3. Renew with extra DNS names | **DONE** |
|
||||
|
||||
### Account Management
|
||||
|
||||
| Command | Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------|---------------|-------------|------------|--------|
|
||||
| `hive admin account create` | `--name` | Cluster must be running (NATS), master key must exist | Connects to NATS → CAS loop on `hive-account-counter:next_id` for sequential 12-digit ID → creates Account record in `hive-accounts` KV → creates `admin` user in new account → creates access key for admin → creates AdministratorAccess policy (Action:*, Resource:*) → attaches policy → prints credentials | 1. Create first account (ID 000000000001)<br>2. Sequential IDs (000000000002, etc.)<br>3. Admin user has full access<br>4. Credentials printed once | **DONE** |
|
||||
| `hive admin account list` | — | Cluster must be running (NATS), master key must exist | Connects to NATS → IAMService.ListAccounts() → prints table with Account ID, Name, Status, Created | 1. List all accounts<br>2. Empty list (only global account) | **DONE** |
|
||||
| `spx admin account create` | `--name` | Cluster must be running (NATS), master key must exist | Connects to NATS → CAS loop on `spinifex-account-counter:next_id` for sequential 12-digit ID → creates Account record in `spinifex-accounts` KV → creates `admin` user in new account → creates access key for admin → creates AdministratorAccess policy (Action:*, Resource:*) → attaches policy → prints credentials | 1. Create first account (ID 000000000001)<br>2. Sequential IDs (000000000002, etc.)<br>3. Admin user has full access<br>4. Credentials printed once | **DONE** |
|
||||
| `spx admin account list` | — | Cluster must be running (NATS), master key must exist | Connects to NATS → IAMService.ListAccounts() → prints table with Account ID, Name, Status, Created | 1. List all accounts<br>2. Empty list (only global account) | **DONE** |
|
||||
|
||||
### Image Management
|
||||
|
||||
| Command | Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------|---------------|-------------|------------|--------|
|
||||
| `hive admin images import` | `--name` | Cluster must be running, image file must exist locally | Imports a local disk image (QCOW2) as an AMI → uploads to Predastore S3 → registers as available AMI | 1. Import valid QCOW2 image<br>2. Image appears in `describe-images` | **DONE** |
|
||||
| `hive admin images list` | — | None | Lists available OS images that can be imported or downloaded | 1. List available images | **DONE** |
|
||||
| `spx admin images import` | `--name` | Cluster must be running, image file must exist locally | Imports a local disk image (QCOW2) as an AMI → uploads to Predastore S3 → registers as available AMI | 1. Import valid QCOW2 image<br>2. Image appears in `describe-images` | **DONE** |
|
||||
| `spx admin images list` | — | None | Lists available OS images that can be imported or downloaded | 1. List available images | **DONE** |
|
||||
|
||||
## AWS Commands
|
||||
|
||||
@@ -80,15 +92,17 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `run-instances` | `--image-id`, `--instance-type`, `--count` (Min/MaxCount), `--key-name`, `--user-data`, `--subnet-id` (auto-creates ENI, assigns private IP), `--block-device-mappings` (DeviceName, VolumeSize, VolumeType, Iops, DeleteOnTermination) | `--security-group-ids`, `--placement`, `--tag-specifications`, `--dry-run`, `--client-token`, `--disable-api-termination`, `--ebs-optimized`, `--iam-instance-profile`, `--network-interfaces`, `--private-ip-address`, `--monitoring`, `--credit-specification`, `--cpu-options`, `--metadata-options`, `--launch-template`, `--hibernate-options` | `describe-images` (AMI must exist), `create-key-pair` (optional), VPC/SG (optional) | Gateway parses AWS query → NATS `ec2.runinstances` → daemon creates QEMU/KVM VM with viperblock-backed root volume via NBD → if SubnetId provided, auto-creates ENI with private IP → cloud-init injects user-data/keys → returns reservation with instance ID | 1. Launch with valid AMI and key pair<br>2. Launch with invalid AMI ID (error)<br>3. Launch with block device mappings (custom volume size)<br>4. Launch multiple instances (MinCount/MaxCount)<br>5. Launch with subnet-id (auto-creates ENI)<br>6. Invalid instance type returns error | **DONE** |
|
||||
| `describe-instances` | `--instance-ids` | `--filters`, `--max-results`, `--next-token`, `--dry-run` | None | Gateway fans out NATS `ec2.DescribeInstances` to all nodes (no queue group) → each daemon returns local instances → gateway aggregates and returns combined list | 1. Describe all instances (no filter)<br>2. Describe by instance ID<br>3. Describe with filters (e.g. instance-state-name)<br>4. Instance not found returns empty set<br>5. Multi-node aggregation returns instances from all nodes | **DONE** |
|
||||
| `run-instances` | `--image-id`, `--instance-type`, `--count` (Min/MaxCount), `--key-name`, `--user-data`, `--subnet-id` (auto-creates ENI, assigns private IP), `--block-device-mappings` (DeviceName, VolumeSize, VolumeType, Iops, DeleteOnTermination), `--placement` (GroupName only — routes via spread or cluster strategy) | `--security-group-ids`, `--tag-specifications`, `--dry-run`, `--client-token`, `--disable-api-termination`, `--ebs-optimized`, `--iam-instance-profile`, `--network-interfaces`, `--private-ip-address`, `--monitoring`, `--credit-specification`, `--cpu-options`, `--metadata-options`, `--launch-template`, `--hibernate-options` | `describe-images` (AMI must exist), `create-key-pair` (optional), VPC/SG (optional) | Gateway parses AWS query → if Placement.GroupName set, looks up strategy: spread → `distributeInstancesSpread()` (1 instance per node, atomic CAS reservation), cluster → `distributeInstancesCluster()` (pin all to single node); otherwise NATS `ec2.runinstances` → daemon creates QEMU/KVM VM with viperblock-backed root volume via NBD → if SubnetId provided, auto-creates ENI with private IP → cloud-init injects user-data/keys → on termination, removes instance from placement group → returns reservation with instance ID | 1. Launch with valid AMI and key pair<br>2. Launch with invalid AMI ID (error)<br>3. Launch with block device mappings (custom volume size)<br>4. Launch multiple instances (MinCount/MaxCount)<br>5. Launch with subnet-id (auto-creates ENI)<br>6. Invalid instance type returns error<br>7. Launch with spread placement group (1 per node)<br>8. Launch with cluster placement group (all on one node)<br>9. Insufficient capacity for placement group (error) | **DONE** |
|
||||
| `describe-instances` | `--instance-ids`, `--filters` (instance-state-name, instance-id, instance-type, vpc-id, subnet-id, tag:\*, tag-key, tag-value) | `--max-results`, `--next-token`, `--dry-run` | None | Gateway fans out NATS `ec2.DescribeInstances` to all nodes (no queue group) → each daemon returns local instances → gateway aggregates and returns combined list. Filters applied per-node before aggregation (reduces payload). Also applies to stopped/terminated instances via `describeInstancesFromKV()`. | 1. Describe all instances (no filter)<br>2. Describe by instance ID<br>3. Describe with filters (e.g. instance-state-name)<br>4. Instance not found returns empty set<br>5. Multi-node aggregation returns instances from all nodes<br>6. Filter by tag<br>7. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
| `start-instances` | `--instance-ids` | `--dry-run`, `--force` | `run-instances` (instance must exist in stopped state) | Gateway sends NATS `ec2.cmd.{instance-id}` → daemon restarts stopped QEMU process with same config → state transitions stopped→pending→running | 1. Start a stopped instance<br>2. Start already-running instance (error: IncorrectInstanceState)<br>3. Start with invalid instance ID<br>4. Verify volumes re-mount on start | **DONE** |
|
||||
| `stop-instances` | `--instance-ids` | `--force`, `--hibernate`, `--dry-run` | `run-instances` (instance must be running) | Gateway sends NATS to target node → daemon issues QMP `system_powerdown` for graceful shutdown → monitors heartbeat until QEMU exits → state transitions running→stopping→stopped | 1. Graceful stop of running instance<br>2. Force stop (kills QEMU process)<br>3. Stop already-stopped instance (error)<br>4. Verify ~30s heartbeat detection | **DONE** |
|
||||
| `terminate-instances` | `--instance-ids`, `DeleteOnTermination` (per-volume flag, default true) | `--dry-run` | `run-instances` (instance must exist) | Gateway sends NATS to target node → daemon kills QEMU process → cleans up NBD mounts → deletes volumes with `DeleteOnTermination=true` via `volumeService.DeleteVolume()` (S3 cleanup of vol/, vol-efi/, vol-cloudinit/) → internal volumes (EFI, cloud-init) always cleaned up via `ebs.delete` NATS → volumes with `DeleteOnTermination=false` left in available state → state→terminated | 1. Terminate running instance<br>2. Terminate stopped instance<br>3. Terminate with DeleteOnTermination=true deletes volumes<br>4. Terminate with DeleteOnTermination=false preserves volumes<br>5. Terminate already-terminated (idempotent)<br>6. Internal volumes (EFI, cloud-init) always cleaned up<br>7. Invalid instance ID | **DONE** |
|
||||
| `reboot-instances` | — | `--instance-ids`, `--dry-run` | `run-instances` (instance must be running) | Gateway sends NATS to target node → daemon issues QMP `system_reset` → instance reboots without stopping | 1. Reboot running instance<br>2. Reboot stopped instance (error)<br>3. Verify instance stays in running state after reboot | **NOT STARTED** (parser test exists) |
|
||||
| `reboot-instances` | `--instance-ids` | `--dry-run` | `run-instances` (instance must be running) | Gateway validates instance IDs → sends EC2InstanceCommand with `RebootInstance=true` via NATS `ec2.cmd.{instanceId}` → daemon validates instance is in StateRunning (returns IncorrectInstanceState if stopped) → issues QMP `system_reset` → instance reboots without stopping, stays in running state | 1. Reboot running instance<br>2. Reboot multiple instances<br>3. Reboot stopped instance (error: IncorrectInstanceState)<br>4. Instance not found (error: InvalidInstanceID.NotFound)<br>5. Verify instance stays in running state after reboot | **DONE** |
|
||||
| `describe-instance-types` | `--filters` (capacity filter only) | `--instance-types`, `--max-results`, `--next-token`, `--dry-run`, all other filters | None | Gateway fans out NATS `ec2.DescribeInstanceTypes` to all nodes → each daemon reports supported types (t3.micro/small/medium/large) with vCPU/memory specs → gateway deduplicates and returns | 1. List all instance types<br>2. Filter by specific type<br>3. Filter with `capacity=true` shows available slots<br>4. Verify vCPU/memory specs match hardware | **DONE** |
|
||||
| `modify-instance-attribute` | `--instance-id`, `--instance-type`, `--user-data` | `--disable-api-termination`, `--ebs-optimized`, `--source-dest-check`, `--instance-initiated-shutdown-behavior`, `--block-device-mappings`, `--groups`, `--ena-support`, `--sriov-net-support` | Instance must be stopped (in NATS KV) | Gateway validates input (exactly one attribute per call, instance ID format) → NATS `ec2.ModifyInstanceAttribute` with `hive-workers` queue group → daemon loads stopped instance from JetStream KV → applies attribute change → writes back to KV → returns `{}` on success. **InstanceType**: updates vm.InstanceType, Config, and Instance fields; clears StateReason (enables recovery from instance-type-missing bug). **UserData**: stores decoded content in vm.UserData and re-encodes to base64 for RunInstancesInput (cloud-init on next start). No instance type pre-validation (matches AWS — invalid types accepted, fail at StartInstances time). | 1. Change instance type while stopped<br>2. Change user data while stopped<br>3. Modify running instance (error: NotFound — running instances not in KV)<br>4. Instance not found (error: InvalidInstanceID.NotFound)<br>5. Instance not stopped (error: IncorrectInstanceState)<br>6. Invalid instance type accepted (fails on start with InsufficientInstanceCapacity)<br>7. StateReason cleared on type change (recovery from capacity-unavailable)<br>8. Missing/malformed instance ID (error: InvalidInstanceID.Malformed)<br>9. No attribute set (error: InvalidParameterValue)<br>10. Multiple attributes in one call (error: InvalidParameterValue) | **DONE** |
|
||||
| `modify-instance-attribute` | `--instance-id`, `--instance-type`, `--user-data` | `--disable-api-termination`, `--ebs-optimized`, `--source-dest-check`, `--instance-initiated-shutdown-behavior`, `--block-device-mappings`, `--groups`, `--ena-support`, `--sriov-net-support` | Instance must be stopped (in NATS KV) | Gateway validates input (exactly one attribute per call, instance ID format) → NATS `ec2.ModifyInstanceAttribute` with `spinifex-workers` queue group → daemon loads stopped instance from JetStream KV → applies attribute change → writes back to KV → returns `{}` on success. **InstanceType**: updates vm.InstanceType, Config, and Instance fields; clears StateReason (enables recovery from instance-type-missing bug). **UserData**: stores decoded content in vm.UserData and re-encodes to base64 for RunInstancesInput (cloud-init on next start). No instance type pre-validation (matches AWS — invalid types accepted, fail at StartInstances time). | 1. Change instance type while stopped<br>2. Change user data while stopped<br>3. Modify running instance (error: NotFound — running instances not in KV)<br>4. Instance not found (error: InvalidInstanceID.NotFound)<br>5. Instance not stopped (error: IncorrectInstanceState)<br>6. Invalid instance type accepted (fails on start with InsufficientInstanceCapacity)<br>7. StateReason cleared on type change (recovery from capacity-unavailable)<br>8. Missing/malformed instance ID (error: InvalidInstanceID.Malformed)<br>9. No attribute set (error: InvalidParameterValue)<br>10. Multiple attributes in one call (error: InvalidParameterValue) | **DONE** |
|
||||
| `get-console-output` | `--instance-id` | `--latest` (always returns latest), `--dry-run` | Instance must be running on a node | Gateway sends NATS `ec2.{instanceId}.GetConsoleOutput` (per-instance topic, routed to owning node) → daemon reads console log file from disk → returns last 64KB base64-encoded with timestamp. Always available regardless of serial console access setting (matches AWS behavior). | 1. Get output from running instance<br>2. Empty log file returns empty output<br>3. Instance not found (error: InvalidInstanceID.NotFound) | **DONE** |
|
||||
| `describe-instance-attribute` | `--instance-id`, `--attribute` (instanceType, userData, instanceInitiatedShutdownBehavior, disableApiTermination, disableApiStop, ebsOptimized, enaSupport, sourceDestCheck, rootDeviceName, kernel, ramdisk) | `--dry-run` | Instance must exist (running or stopped) | Gateway validates input → NATS `ec2.DescribeInstanceAttribute` with `spinifex-workers` queue group → daemon checks running instances first (`d.Instances.VMS`), then stopped instances in JetStream KV → returns single attribute per call (matches AWS behavior). Stored attributes (`instanceType`, `userData`) return real values; unstored attributes return AWS defaults (`instanceInitiatedShutdownBehavior`=stop, `disableApiTermination`=false, etc.) | 1. Get instanceType from running instance<br>2. Get userData from stopped instance<br>3. Get default disableApiTermination<br>4. Invalid attribute name (error)<br>5. Instance not found (error: InvalidInstanceID.NotFound) | **DONE** |
|
||||
| `describe-instance-credit-specifications` | `--instance-ids` | `--filters`, `--max-results`, `--dry-run` | None | Gateway-only stub — returns `CpuCredits: "standard"` for each requested instance ID. No daemon round-trip. T-series credit mode is not persisted. | 1. Get credit spec for T-series instance<br>2. Multiple instance IDs | **DONE** |
|
||||
| `monitor-instances` | — | `--instance-ids` | Instance must exist | Enable basic monitoring (CPU, disk, network) → store metrics in NATS KV → return monitoring state | 1. Enable monitoring<br>2. Verify monitoring state in describe-instances | **NOT STARTED** |
|
||||
| `unmonitor-instances` | — | `--instance-ids` | Instance must exist | Disable detailed monitoring → revert to basic monitoring → return monitoring state | 1. Disable monitoring<br>2. Verify monitoring state reverts in describe-instances | **NOT STARTED** |
|
||||
|
||||
@@ -97,7 +111,7 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-key-pair` | `--key-name`, `--key-type` (rsa/ed25519) | `--key-format` (pem/ppk), `--tag-specifications`, `--dry-run` | None | NATS `ec2.CreateKeyPair` → daemon generates SSH keypair → stores public key in Predastore S3 (`/bucket/ec2/{name}.pub`) → returns private key material (one-time) | 1. Create RSA key pair<br>2. Create ED25519 key pair<br>3. Duplicate key name (error: InvalidKeyPair.Duplicate)<br>4. Verify key material returned only on creation | **DONE** |
|
||||
| `describe-key-pairs` | `--key-names`, `--key-pair-ids` | `--filters`, `--max-results`, `--dry-run` | None | NATS `ec2.DescribeKeyPairs` → daemon lists public keys from Predastore S3 → returns key names and fingerprints | 1. List all key pairs<br>2. Filter by key name<br>3. Filter by key pair ID<br>4. Non-existent key returns empty | **DONE** |
|
||||
| `describe-key-pairs` | `--key-names`, `--key-pair-ids`, `--filters` (key-pair-id, key-name, fingerprint, tag:\*) | `--max-results`, `--dry-run` | None | NATS `ec2.DescribeKeyPairs` → daemon lists public keys from Predastore S3 → applies filters → returns key names and fingerprints | 1. List all key pairs<br>2. Filter by key name<br>3. Filter by key pair ID<br>4. Non-existent key returns empty<br>5. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
| `delete-key-pair` | `--key-name`, `--key-pair-id` | `--dry-run` | Key must exist | NATS `ec2.DeleteKeyPair` → daemon deletes public key from Predastore S3 → returns success | 1. Delete existing key pair<br>2. Delete non-existent key (idempotent, no error)<br>3. Verify key no longer in describe-key-pairs | **DONE** |
|
||||
| `import-key-pair` | `--key-name`, `--public-key-material` | `--tag-specifications`, `--dry-run` | None | NATS `ec2.ImportKeyPair` → daemon stores provided public key in Predastore S3 → returns key name and fingerprint | 1. Import valid SSH public key<br>2. Import invalid key material (error)<br>3. Import duplicate name (error)<br>4. Verify imported key usable with run-instances | **DONE** |
|
||||
|
||||
@@ -105,7 +119,7 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `describe-images` | `--image-ids` (format validation only), `--owners` (self, account ID, alias) | `--executable-users`, `--filters`, `--include-deprecated`, `--include-disabled`, `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeImages` → daemon reads AMI metadata from Predastore S3 buckets (ami-*) → filters by ImageIds and Owners → returns image list with state, architecture, block device mappings | 1. List all images<br>2. Filter by image ID<br>3. Filter by owner (self/amazon)<br>4. Non-existent AMI returns empty<br>5. Verify metadata fields (architecture, state, rootDeviceName) | **DONE** |
|
||||
| `describe-images` | `--image-ids` (format validation only), `--owners` (self, account ID, alias), `--filters` (name, state, architecture, image-id, is-public, owner-id, description, image-type, tag:\*) | `--executable-users`, `--include-deprecated`, `--include-disabled`, `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeImages` → daemon reads AMI metadata from Predastore S3 buckets (ami-*) → filters by ImageIds, Owners, and Filters → returns image list with state, architecture, block device mappings | 1. List all images<br>2. Filter by image ID<br>3. Filter by owner (self/amazon)<br>4. Non-existent AMI returns empty<br>5. Verify metadata fields (architecture, state, rootDeviceName)<br>6. Filter by name wildcard<br>7. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
| `create-image` | `--instance-id`, `--name`, `--description`, `--tag-specifications` | `--no-reboot`, `--block-device-mappings`, `--dry-run` | Instance must exist (running or stopped) | Gateway validates input → NATS `ec2.{instanceId}.CreateImage` (per-instance topic) → daemon extracts root volume → snapshots via `ebs.snapshot` NATS (running) or offline S3 copy (stopped) → creates AMI metadata in S3 (`{amiId}/config.json`) → stores tags → returns ami-ID. Duplicate AMI name validation enforced. | 1. Create image from running instance<br>2. Create image from stopped instance<br>3. Invalid instance ID (error)<br>4. Duplicate AMI name (error)<br>5. Verify new AMI appears in describe-images<br>6. Launch new instance from created AMI | **DONE** |
|
||||
| `register-image` | — | `--name`, `--description`, `--architecture`, `--root-device-name`, `--virtualization-type`, `--block-device-mappings`, `--boot-mode`, `--ena-support`, `--sriov-net-support`, `--tpm-support`, `--imds-support` | Image data must exist in S3 | Create AMI metadata entry in Predastore → assign ami-ID → set state to available → return ami-ID | 1. Register with valid metadata<br>2. Missing required name (error)<br>3. Verify registered image in describe-images | **NOT STARTED** |
|
||||
| `deregister-image` | — | `--image-id`, `--dry-run` | AMI must exist, no instances using it | Mark AMI metadata as deregistered in S3 → AMI no longer available for new launches → existing instances unaffected | 1. Deregister existing AMI<br>2. Deregister non-existent AMI (error)<br>3. Verify deregistered AMI not in describe-images<br>4. Existing instances from AMI still run | **NOT STARTED** |
|
||||
@@ -119,13 +133,13 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `describe-volumes` | `--volume-ids` (fast-path lookup), `DeleteOnTermination` (from persisted VolumeMetadata) | `--filters`, `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeVolumes` → daemon queries viperblock for volume metadata → returns volume list with state, size, attachments, type, DeleteOnTermination flag | 1. List all volumes<br>2. Filter by volume ID<br>3. Filter by attachment state<br>4. Non-existent volume returns empty<br>5. DeleteOnTermination reflects persisted value | **DONE** |
|
||||
| `describe-volumes` | `--volume-ids` (fast-path lookup), `DeleteOnTermination` (from persisted VolumeMetadata), `--filters` (volume-id, status, size, volume-type, attachment.instance-id, attachment.status, attachment.device, availability-zone, tag:\*) | `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeVolumes` → daemon queries viperblock for volume metadata → applies filters → returns volume list with state, size, attachments, type, DeleteOnTermination flag | 1. List all volumes<br>2. Filter by volume ID<br>3. Filter by attachment state<br>4. Non-existent volume returns empty<br>5. DeleteOnTermination reflects persisted value<br>6. Filter by status, size, volume-type<br>7. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
| `modify-volume` | `--volume-id`, `--size`, `--volume-type`, `--iops` | `--throughput`, `--dry-run`, `--multi-attach-enabled` | Volume must exist | NATS `ec2.ModifyVolume` → daemon sends resize request to viperblock → NBD does not support live resize, instance must be stopped → returns modification state | 1. Increase volume size<br>2. Modify volume type<br>3. Decrease size (error - not supported)<br>4. Modify attached volume (requires stop/start) | **DONE** |
|
||||
| `create-volume` | `--size`, `--availability-zone`, `--volume-type` (gp3 only), `--snapshot-id` (creates volume from snapshot) | `--iops` (hardcoded 3000), `--encrypted` (hardcoded false), `--throughput`, `--tag-specifications` | Valid AZ configured via `hive init` | Gateway validates input → NATS `ec2.CreateVolume` → daemon generates vol-ID via viperblock → creates volume (empty or from snapshot) of specified size → persists config.json to Predastore S3 → returns vol-ID with state=available | 1. Create 80GB gp3 volume<br>2. Boundary sizes (1 GiB min, 16384 GiB max)<br>3. Invalid AZ (error)<br>4. Verify volume in describe-volumes<br>5. Unsupported volume type (error - only gp3)<br>6. Size out of range (error)<br>7. Create from snapshot | **DONE** |
|
||||
| `create-volume` | `--size`, `--availability-zone`, `--volume-type` (gp3 only), `--snapshot-id` (creates volume from snapshot) | `--iops` (hardcoded 3000), `--encrypted` (hardcoded false), `--throughput`, `--tag-specifications` | Valid AZ configured via `spinifex init` | Gateway validates input → NATS `ec2.CreateVolume` → daemon generates vol-ID via viperblock → creates volume (empty or from snapshot) of specified size → persists config.json to Predastore S3 → returns vol-ID with state=available | 1. Create 80GB gp3 volume<br>2. Boundary sizes (1 GiB min, 16384 GiB max)<br>3. Invalid AZ (error)<br>4. Verify volume in describe-volumes<br>5. Unsupported volume type (error - only gp3)<br>6. Size out of range (error)<br>7. Create from snapshot | **DONE** |
|
||||
| `delete-volume` | `--volume-id` | `--dry-run` | Volume must exist and be detached (state=available) | Gateway validates vol- prefix → NATS `ec2.DeleteVolume` → daemon confirms state=available and no AttachedInstance → NATS `ebs.delete` to viperblockd (stops nbdkit/WAL) → deletes S3 objects under vol-id/, vol-id-efi/, vol-id-cloudinit/ → returns success | 1. Delete detached volume<br>2. Delete attached volume (error: VolumeInUse)<br>3. Delete non-existent volume (error: InvalidVolume.NotFound)<br>4. Verify volume gone from describe-volumes<br>5. Malformed volume ID (error: InvalidVolumeID.Malformed)<br>6. Double delete (idempotent NotFound) | **DONE** |
|
||||
| `attach-volume` | `--volume-id`, `--instance-id`, `--device` (optional, auto-assigns `/dev/sd[f-p]`) | `--dry-run` | Volume must exist (available), instance must exist (running) | Gateway sends to `ec2.cmd.{instanceId}` → daemon validates volume (Predastore) → `ebs.mount` via NATS (viperblock starts NBD server) → QMP `blockdev-add` (nbd-{volId}) → QMP `device_add` (virtio-blk-pci, vdisk-{volId}) → three-phase rollback on failure → update EBSRequests + BlockDeviceMappings → persist to JetStream + Predastore → respond with VolumeAttachment | 1. Attach volume to running instance<br>2. Auto-assign device name<br>3. Attach already-attached volume (VolumeInUse)<br>4. Attach to non-existent instance (InvalidInstanceID.NotFound)<br>5. Attach to stopped instance (IncorrectInstanceState)<br>6. Volume not found (InvalidVolume.NotFound)<br>7. All device slots full (AttachmentLimitExceeded)<br>8. Volume persists across stop/start | **DONE** |
|
||||
| `detach-volume` | `--volume-id`, `--instance-id` (optional, resolved via DescribeVolumes), `--device` (optional cross-check), `--force` | `--dry-run` | Volume must be attached, instance must be running | Gateway resolves InstanceId if omitted (via DescribeVolumes) → sends to `ec2.cmd.{instanceId}` → daemon validates (running, attached, not boot/EFI/CloudInit, device match) → three-phase hot-unplug: QMP `device_del` (force continues on failure) → QMP `blockdev-del` (abort if fails, preserves state to prevent double-attach) → `ebs.unmount` via NATS (best-effort) → remove from EBSRequests + BlockDeviceMappings → update volume metadata to available → persist state → respond with VolumeAttachment (state=detaching) | 1. Detach with explicit InstanceId<br>2. Detach without InstanceId (gateway resolution)<br>3. Detach with correct --device cross-check<br>4. Missing VolumeId (InvalidParameterValue)<br>5. Volume not attached (IncorrectState)<br>6. Nonexistent volume (InvalidVolume.NotFound)<br>7. Nonexistent instance (InvalidInstanceID.NotFound)<br>8. Instance not running (IncorrectInstanceState)<br>9. Device mismatch (InvalidParameterValue)<br>10. Boot volume protection (OperationNotPermitted)<br>11. Force flag (continues past device_del failure)<br>12. Volume reusability (re-attach after detach) | **DONE** |
|
||||
| `describe-volume-status` | `--volume-ids` | `--filters`, `--max-results`, `--next-token`, `--dry-run` | None | Gateway validates vol- prefix → NATS `ec2.DescribeVolumeStatus` → daemon fetches VolumeConfig from Predastore S3 (parallel for specific IDs, sequential list-all for no IDs) → builds VolumeStatusItem per volume (status=ok, io-enabled=passed, io-performance=not-applicable) → returns InvalidVolume.NotFound for missing explicit IDs → skips internal sub-volumes (-efi, -cloudinit) | 1. List all volume statuses<br>2. Filter by specific volume IDs (fast path)<br>3. Non-existent volume ID returns InvalidVolume.NotFound<br>4. Invalid volume ID format (InvalidVolume.Malformed)<br>5. Internal sub-volumes excluded from listing<br>6. Nil/empty input defaults to all volumes | **DONE** |
|
||||
| `describe-volume-status` | `--volume-ids`, `--filters` (volume-id, volume-status.status, availability-zone) | `--max-results`, `--next-token`, `--dry-run` | None | Gateway validates vol- prefix → NATS `ec2.DescribeVolumeStatus` → daemon fetches VolumeConfig from Predastore S3 (parallel for specific IDs, sequential list-all for no IDs) → applies filters → builds VolumeStatusItem per volume (status=ok, io-enabled=passed, io-performance=not-applicable) → returns InvalidVolume.NotFound for missing explicit IDs → skips internal sub-volumes (-efi, -cloudinit) | 1. List all volume statuses<br>2. Filter by specific volume IDs (fast path)<br>3. Non-existent volume ID returns InvalidVolume.NotFound<br>4. Invalid volume ID format (InvalidVolume.Malformed)<br>5. Internal sub-volumes excluded from listing<br>6. Nil/empty input defaults to all volumes<br>7. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
| `describe-volumes-modifications` | — | `--volume-ids`, `--filters`, `--max-results` | None | Query pending/completed volume modifications → return modification state, progress, original/target size | 1. Check in-progress modification<br>2. Check completed modification<br>3. No modifications returns empty | **NOT STARTED** |
|
||||
|
||||
### EC2 - Snapshot Management
|
||||
@@ -135,7 +149,7 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
| `create-snapshot` | `--volume-id`, `--description`, `--tag-specifications` | `--dry-run` | Volume must exist in Predastore | Gateway validates vol- prefix → NATS `ec2.CreateSnapshot` → daemon reads VolumeConfig from Predastore → generates snap-ID via viperblock → stores SnapshotConfig (metadata-only, points to source volume) as completed → returns Snapshot with state=completed, progress=100% | 1. Snapshot from valid volume<br>2. Missing volume ID (InvalidParameterValue)<br>3. Volume not found (InvalidVolume.NotFound)<br>4. Invalid volume ID format (InvalidVolume.Malformed)<br>5. Snapshot with description<br>6. Snapshot with tag specifications | **DONE** |
|
||||
| `create-snapshots` | — | `--instance-specification`, `--description`, `--tag-specifications` | Instance must exist, instance-volume attachment tracking | Create snapshots of all volumes attached to instance → return list of snapshot IDs. Blocked: requires instance-volume attachment tracking to discover which volumes to snapshot. | 1. Snapshot all volumes on instance<br>2. Instance with no volumes | **NOT STARTED** |
|
||||
| `delete-snapshot` | `--snapshot-id` | `--dry-run` | Snapshot must exist | Gateway validates snap- prefix → NATS `ec2.DeleteSnapshot` → daemon verifies snapshot exists in Predastore → lists and deletes all objects under snapshot prefix → returns success | 1. Delete existing snapshot<br>2. Delete non-existent snapshot (InvalidSnapshot.NotFound)<br>3. Missing snapshot ID (InvalidParameterValue)<br>4. Invalid snapshot ID format (InvalidSnapshot.Malformed) | **DONE** |
|
||||
| `describe-snapshots` | `--snapshot-ids` | `--owner-ids`, `--filters`, `--max-results`, `--dry-run` | None | Gateway validates snap- prefix on IDs → NATS `ec2.DescribeSnapshots` → daemon lists snap- prefixed objects in Predastore → reads SnapshotConfig for each → filters by snapshot ID if specified → returns snapshot list | 1. List all snapshots<br>2. Filter by snapshot ID<br>3. Empty snapshot list<br>4. Invalid snapshot ID format (InvalidSnapshot.Malformed) | **DONE** |
|
||||
| `describe-snapshots` | `--snapshot-ids`, `--filters` (snapshot-id, status, volume-id, volume-size, owner-id, tag:\*) | `--owner-ids`, `--max-results`, `--dry-run` | None | Gateway validates snap- prefix on IDs → NATS `ec2.DescribeSnapshots` → daemon lists snap- prefixed objects in Predastore → reads SnapshotConfig for each → applies filters → returns snapshot list | 1. List all snapshots<br>2. Filter by snapshot ID<br>3. Empty snapshot list<br>4. Invalid snapshot ID format (InvalidSnapshot.Malformed)<br>5. Filter by status, volume-id, volume-size<br>6. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
| `copy-snapshot` | `--source-snapshot-id`, `--source-region`, `--description` | `--encrypted`, `--dry-run` | Source snapshot must exist | Gateway validates snap- prefix + source region → NATS `ec2.CopySnapshot` → daemon reads source SnapshotConfig → generates new snap-ID → copies metadata (preserves tags, description override) → stores as completed → returns new snapshot ID | 1. Copy within same region<br>2. Copy non-existent snapshot (InvalidSnapshot.NotFound)<br>3. Missing source ID (InvalidParameterValue)<br>4. Missing source region (MissingParameter)<br>5. Copy preserves tags<br>6. Copy with description override | **DONE** |
|
||||
|
||||
### EC2 - Tags
|
||||
@@ -150,8 +164,8 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `describe-regions` | *(returns configured region, endpoint, opt-in status from config — input params ignored)* | `--region-names`, `--filters`, `--all-regions`, `--dry-run` | None | Return configured region from hive init config with Endpoint and OptInStatus → local-only response, no NATS | 1. List all regions<br>2. Filter by region name<br>3. Verify endpoint URL returned<br>4. Verify OptInStatus returned | **DONE** |
|
||||
| `describe-availability-zones` | *(returns configured AZ, region, zone ID, state, opt-in status from config — input params ignored)* | `--zone-names`, `--filters`, `--all-availability-zones` | None | Return configured AZ from hive init config with zone ID, state, group name, network border group → local-only response, no NATS | 1. List all AZs<br>2. Filter by zone name<br>3. Verify zone state is available<br>4. Verify region name matches config | **DONE** |
|
||||
| `describe-regions` | *(returns configured region, endpoint, opt-in status from config — input params ignored)* | `--region-names`, `--filters`, `--all-regions`, `--dry-run` | None | Return configured region from spinifex init config with Endpoint and OptInStatus → local-only response, no NATS | 1. List all regions<br>2. Filter by region name<br>3. Verify endpoint URL returned<br>4. Verify OptInStatus returned | **DONE** |
|
||||
| `describe-availability-zones` | *(returns configured AZ, region, zone ID, state, opt-in status from config — input params ignored)* | `--zone-names`, `--filters`, `--all-availability-zones` | None | Return configured AZ from spinifex init config with zone ID, state, group name, network border group → local-only response, no NATS | 1. List all AZs<br>2. Filter by zone name<br>3. Verify zone state is available<br>4. Verify region name matches config | **DONE** |
|
||||
|
||||
### EC2 - Account Attributes
|
||||
|
||||
@@ -165,12 +179,12 @@ Persistence layer works (NATS JetStream KV), but stored values are not yet enfor
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `enable-ebs-encryption-by-default` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.EnableEbsEncryptionByDefault` with `hive-workers` queue group → daemon stores `EbsEncryptionByDefault=true` in JetStream KV (`hive-ec2-account-settings` bucket) → return `EbsEncryptionByDefault=true`. Enforcement deferred: stored value not yet checked by CreateVolume/CreateSnapshot. | 1. Enable encryption<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `disable-ebs-encryption-by-default` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.DisableEbsEncryptionByDefault` with `hive-workers` queue group → daemon stores `EbsEncryptionByDefault=false` in JetStream KV → return `EbsEncryptionByDefault=false`. | 1. Disable encryption<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `get-ebs-encryption-by-default` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.GetEbsEncryptionByDefault` with `hive-workers` queue group → daemon reads `EbsEncryptionByDefault` from JetStream KV → return current state. | 1. Get default state<br>2. Verify matches last enable/disable | **STARTED** (enforcement pending) |
|
||||
| `enable-serial-console-access` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.EnableSerialConsoleAccess` with `hive-workers` queue group → daemon stores `SerialConsoleAccessEnabled=true` in JetStream KV (`hive-ec2-account-settings` bucket) → return enabled=true. Stored for future interactive serial console support (not currently enforced by any operation). | 1. Enable access<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `disable-serial-console-access` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.DisableSerialConsoleAccess` with `hive-workers` queue group → daemon stores `SerialConsoleAccessEnabled=false` in JetStream KV → return enabled=false. Stored for future interactive serial console support (not currently enforced by any operation). | 1. Disable access<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `get-serial-console-access-status` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.GetSerialConsoleAccessStatus` with `hive-workers` queue group → daemon reads `SerialConsoleAccessEnabled` from JetStream KV → return current state. | 1. Get current status<br>2. Verify matches last enable/disable | **DONE** |
|
||||
| `enable-ebs-encryption-by-default` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.EnableEbsEncryptionByDefault` with `spinifex-workers` queue group → daemon stores `EbsEncryptionByDefault=true` in JetStream KV (`spinifex-ec2-account-settings` bucket) → return `EbsEncryptionByDefault=true`. Enforcement deferred: stored value not yet checked by CreateVolume/CreateSnapshot. | 1. Enable encryption<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `disable-ebs-encryption-by-default` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.DisableEbsEncryptionByDefault` with `spinifex-workers` queue group → daemon stores `EbsEncryptionByDefault=false` in JetStream KV → return `EbsEncryptionByDefault=false`. | 1. Disable encryption<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `get-ebs-encryption-by-default` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.GetEbsEncryptionByDefault` with `spinifex-workers` queue group → daemon reads `EbsEncryptionByDefault` from JetStream KV → return current state. | 1. Get default state<br>2. Verify matches last enable/disable | **STARTED** (enforcement pending) |
|
||||
| `enable-serial-console-access` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.EnableSerialConsoleAccess` with `spinifex-workers` queue group → daemon stores `SerialConsoleAccessEnabled=true` in JetStream KV (`spinifex-ec2-account-settings` bucket) → return enabled=true. Stored for future interactive serial console support (not currently enforced by any operation). | 1. Enable access<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `disable-serial-console-access` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.DisableSerialConsoleAccess` with `spinifex-workers` queue group → daemon stores `SerialConsoleAccessEnabled=false` in JetStream KV → return enabled=false. Stored for future interactive serial console support (not currently enforced by any operation). | 1. Disable access<br>2. Verify via get | **STARTED** (enforcement pending) |
|
||||
| `get-serial-console-access-status` | *(no flags needed)* | `--dry-run` | None | Gateway validates input → NATS `ec2.GetSerialConsoleAccessStatus` with `spinifex-workers` queue group → daemon reads `SerialConsoleAccessEnabled` from JetStream KV → return current state. | 1. Get current status<br>2. Verify matches last enable/disable | **DONE** |
|
||||
| `enable-snapshot-block-public-access` | — | `--state` | None | Store snapshot block public access state in JetStream KV | 1. Enable with block-all-sharing<br>2. Enable with block-new-sharing | **NOT STARTED** (enforcement pending) |
|
||||
| `disable-snapshot-block-public-access` | — | `--dry-run` | None | Clear snapshot block public access state in JetStream KV | 1. Disable access | **NOT STARTED** (enforcement pending) |
|
||||
| `get-snapshot-block-public-access-state` | — | `--dry-run` | None | Read snapshot block public access state from JetStream KV | 1. Get current state | **NOT STARTED** (enforcement pending) |
|
||||
@@ -182,21 +196,23 @@ Persistence layer works (NATS JetStream KV), but stored values are not yet enfor
|
||||
|
||||
### EC2 - Egress-Only Internet Gateway
|
||||
|
||||
Gateway routing and KV CRUD are wired up but no OVN/OVS integration exists. EIGWs are stored in `hive-eigw` KV bucket but have no effect on network topology (no vpcd events published, unlike IGW which publishes `vpc.igw-attach`/`vpc.igw-detach`).
|
||||
Gateway routing and KV CRUD are wired up but no OVN/OVS integration exists. EIGWs are stored in `spinifex-eigw` KV bucket but have no effect on network topology (no vpcd events published, unlike IGW which publishes `vpc.igw-attach`/`vpc.igw-detach`).
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-egress-only-internet-gateway` | `--vpc-id`, `--tag-specifications` | `--client-token`, `--dry-run` | VPC must exist | Gateway validates VpcId → NATS `ec2.CreateEgressOnlyInternetGateway` → daemon generates eigw-ID → creates EgressOnlyIGWRecord in `hive-eigw` KV bucket with state=attached (always attached at creation, unlike IGW) → returns EgressOnlyInternetGateway with attachment | 1. Create with valid VpcId<br>2. Missing VpcId (error: MissingParameter)<br>3. Create with inline tags<br>4. Verify ID format eigw-{16hex} | **STARTED** (KV only, no OVN) |
|
||||
| `delete-egress-only-internet-gateway` | `--egress-only-internet-gateway-id` | `--dry-run` | EIGW must exist | NATS `ec2.DeleteEgressOnlyInternetGateway` → daemon verifies EIGW exists in KV → deletes from `hive-eigw` KV → returns ReturnCode=true. No detach required (unlike IGW). | 1. Delete existing EIGW<br>2. Delete non-existent EIGW (error)<br>3. Missing ID (error: MissingParameter) | **STARTED** (KV only, no OVN) |
|
||||
| `describe-egress-only-internet-gateways` | `--egress-only-internet-gateway-ids` | `--filters`, `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeEgressOnlyInternetGateways` → daemon lists all keys from `hive-eigw` KV bucket → filters by ID if specified → always includes Attachments array → returns list | 1. Describe all EIGWs<br>2. Filter by specific ID<br>3. Non-existent ID returns empty list | **STARTED** (KV only, no OVN) |
|
||||
| `create-egress-only-internet-gateway` | `--vpc-id`, `--tag-specifications` | `--client-token`, `--dry-run` | VPC must exist | Gateway validates VpcId → NATS `ec2.CreateEgressOnlyInternetGateway` → daemon generates eigw-ID → creates EgressOnlyIGWRecord in `spinifex-eigw` KV bucket with state=attached (always attached at creation, unlike IGW) → returns EgressOnlyInternetGateway with attachment | 1. Create with valid VpcId<br>2. Missing VpcId (error: MissingParameter)<br>3. Create with inline tags<br>4. Verify ID format eigw-{16hex} | **STARTED** (KV only, no OVN) |
|
||||
| `delete-egress-only-internet-gateway` | `--egress-only-internet-gateway-id` | `--dry-run` | EIGW must exist | NATS `ec2.DeleteEgressOnlyInternetGateway` → daemon verifies EIGW exists in KV → deletes from `spinifex-eigw` KV → returns ReturnCode=true. No detach required (unlike IGW). | 1. Delete existing EIGW<br>2. Delete non-existent EIGW (error)<br>3. Missing ID (error: MissingParameter) | **STARTED** (KV only, no OVN) |
|
||||
| `describe-egress-only-internet-gateways` | `--egress-only-internet-gateway-ids`, `--filters` (egress-only-internet-gateway-id, tag:\*) | `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeEgressOnlyInternetGateways` → daemon lists all keys from `spinifex-eigw` KV bucket → filters by ID if specified → applies filters → always includes Attachments array → returns list | 1. Describe all EIGWs<br>2. Filter by specific ID<br>3. Non-existent ID returns empty list<br>4. Unknown filter returns InvalidParameterValue | **STARTED** (KV only, no OVN) |
|
||||
|
||||
### EC2 - Placement Groups
|
||||
|
||||
Placement groups are stored in `spinifex-placement-groups` JetStream KV bucket, keyed by `{accountID}.{groupName}`. All mutations use CAS (compare-and-swap) with 5-retry limit for optimistic concurrency. Supported strategies: **spread** (strict 1 instance per node) and **cluster** (all instances pinned to single node). Partition strategy is rejected. Internal NATS operations (ReserveSpreadNodes, FinalizeSpreadInstances, ReleaseSpreadNodes, RemoveInstanceFromPlacementGroup, ReserveClusterNode, FinalizeClusterInstances) support atomic multi-node coordination for RunInstances and TerminateInstances.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-placement-group` | — | `--group-name`, `--strategy` (cluster/spread/partition), `--partition-count`, `--spread-level`, `--tag-specifications` | None | NATS `ec2.CreatePlacementGroup` → daemon stores placement group in JetStream KV with strategy, state=available → return group metadata | 1. Create with cluster strategy<br>2. Create with spread strategy<br>3. Create with partition strategy + partition count<br>4. Duplicate name (error)<br>5. Invalid strategy (error) | **NOT STARTED** |
|
||||
| `delete-placement-group` | — | `--group-name`, `--dry-run` | Group must exist, no instances in group | NATS `ec2.DeletePlacementGroup` → daemon verifies group exists and is empty → delete from KV → return success | 1. Delete empty group<br>2. Delete group with instances (error)<br>3. Delete non-existent group (error) | **NOT STARTED** |
|
||||
| `describe-placement-groups` | — | `--group-names`, `--group-ids`, `--filters` | None | NATS `ec2.DescribePlacementGroups` → daemon lists groups from KV → return list with strategy and state | 1. List all groups<br>2. Filter by name<br>3. Filter by strategy | **NOT STARTED** |
|
||||
| `create-placement-group` | `--group-name`, `--strategy` (spread/cluster) | `--partition-count`, `--spread-level` (hardcoded to "host"), `--tag-specifications`, `--dry-run` | None | Gateway validates GroupName + Strategy → NATS `ec2.CreatePlacementGroup` with `spinifex-workers` queue group → daemon generates pg-ID (pg-{16hex}) → stores PlacementGroupRecord in `spinifex-placement-groups` KV with state=available, SpreadLevel="host" for spread → rejects partition strategy (InvalidParameterValue) → rejects duplicate names (InvalidPlacementGroup.Duplicate) → returns PlacementGroup | 1. Create with spread strategy<br>2. Create with cluster strategy<br>3. Create with partition strategy (error: InvalidParameterValue)<br>4. Duplicate name (error: InvalidPlacementGroup.Duplicate)<br>5. Missing GroupName (error)<br>6. Missing Strategy (error) | **DONE** |
|
||||
| `delete-placement-group` | `--group-name` | `--dry-run` | Group must exist, no instances in group | NATS `ec2.DeletePlacementGroup` → daemon verifies group exists (InvalidPlacementGroup.Unknown) → counts instances in NodeInstances map → rejects if non-empty (InvalidPlacementGroup.InUse) → deletes from KV → returns success | 1. Delete empty group<br>2. Delete group with instances (error: InvalidPlacementGroup.InUse)<br>3. Delete non-existent group (error: InvalidPlacementGroup.Unknown) | **DONE** |
|
||||
| `describe-placement-groups` | `--group-names`, `--group-ids`, `--filters` (strategy, state, spread-level, group-name) | `--dry-run` | None | NATS `ec2.DescribePlacementGroups` → daemon lists all keys from `spinifex-placement-groups` KV → filters by GroupNames, GroupIds, or Filters → returns list with Strategy, State, SpreadLevel, GroupId | 1. List all groups<br>2. Filter by name<br>3. Filter by group ID<br>4. Filter by strategy<br>5. Filter by state<br>6. Empty result when no groups exist | **DONE** |
|
||||
|
||||
### EC2 - Dedicated Hosts
|
||||
|
||||
@@ -234,67 +250,76 @@ Gateway routing and KV CRUD are wired up but no OVN/OVS integration exists. EIGW
|
||||
|
||||
### EC2 - Elastic IP
|
||||
|
||||
EIP operations are conditionally available — only registered when external IPAM is configured (pool mode). EIPs are stored in `spinifex-eip-allocations` KV bucket. Association publishes `vpc.add-nat`/`vpc.delete-nat` events to vpcd for OVN NAT configuration.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `allocate-address` | — | `--domain` (vpc), `--tag-specifications`, `--dry-run` | None | NATS `ec2.AllocateAddress` → daemon allocates IP from pool → stores in JetStream KV with eipalloc-ID → return AllocationId, PublicIp, Domain | 1. Allocate EIP<br>2. Verify in describe-addresses<br>3. Allocate without --domain uses default | **NOT STARTED** |
|
||||
| `release-address` | — | `--allocation-id`, `--dry-run` | EIP must exist and be disassociated | NATS `ec2.ReleaseAddress` → daemon verifies not associated → release IP back to pool → delete from KV → return success | 1. Release disassociated EIP<br>2. Release associated EIP (error)<br>3. Release non-existent (error) | **NOT STARTED** |
|
||||
| `associate-address` | — | `--allocation-id`, `--instance-id` or `--network-interface-id`, `--dry-run` | EIP and target must exist | NATS `ec2.AssociateAddress` → daemon links EIP to instance/ENI → configure OVS DNAT/SNAT rules → return eipassoc-ID | 1. Associate with instance<br>2. Re-associate (moves EIP)<br>3. Associate non-existent EIP (error) | **NOT STARTED** |
|
||||
| `disassociate-address` | — | `--association-id`, `--dry-run` | Association must exist | NATS `ec2.DisassociateAddress` → daemon unlinks EIP from instance/ENI → remove OVS DNAT/SNAT rules → return success | 1. Disassociate existing<br>2. Disassociate non-existent (error) | **NOT STARTED** |
|
||||
| `describe-addresses` | — | `--allocation-ids`, `--filters`, `--public-ips` | None | NATS `ec2.DescribeAddresses` → daemon lists EIPs from KV → return list with association info | 1. List all EIPs<br>2. Filter by allocation ID<br>3. Filter by public IP | **NOT STARTED** |
|
||||
| `allocate-address` | `--public-ipv4-pool` (optional, specific IPAM pool), `--tag-specifications` | `--domain` (hardcoded vpc), `--dry-run` | EIP service must be initialized (external IPAM configured) | Gateway validates input → NATS `ec2.AllocateAddress` with `spinifex-workers` queue group → daemon allocates IP from external IPAM pool (named or best-matching) → creates EIPRecord with state=allocated in `spinifex-eip-allocations` KV → returns AllocationId, PublicIp, Domain=vpc, PublicIpv4Pool | 1. Allocate EIP from default pool<br>2. Allocate from specific pool<br>3. Pool exhausted (InsufficientAddressCapacity)<br>4. With tag specifications<br>5. Verify in describe-addresses | **DONE** |
|
||||
| `release-address` | `--allocation-id` | `--dry-run` | EIP must exist and be disassociated | Gateway validates AllocationId non-empty → NATS `ec2.ReleaseAddress` → daemon retrieves EIP record from KV → checks state != associated (InvalidAddressLocked if still associated) → releases IP back to IPAM pool → deletes KV record → returns success | 1. Release disassociated EIP<br>2. Release associated EIP (error: InvalidAddressLocked)<br>3. Release non-existent (error: InvalidAllocationID.NotFound)<br>4. Missing AllocationId (error: MissingParameter) | **DONE** |
|
||||
| `associate-address` | `--allocation-id`, `--network-interface-id` OR `--instance-id`, `--private-ip-address` (optional override) | `--dry-run`, `--allow-reassociation` | EIP and target ENI/instance must exist | Gateway validates AllocationId + one of NetworkInterfaceId/InstanceId required → NATS `ec2.AssociateAddress` → daemon retrieves EIP → resolves target ENI (by ID or instance lookup) → generates AssociationId → updates record state=associated with ENI/instance/VPC/private IP → publishes `vpc.add-nat` event to vpcd (OVN NAT configuration) → returns AssociationId | 1. Associate with instance<br>2. Associate with specific ENI<br>3. Associate with private IP override<br>4. Missing AllocationId (MissingParameter)<br>5. Missing target (MissingParameter)<br>6. Non-existent EIP (InvalidAllocationID.NotFound)<br>7. Non-existent ENI (InvalidNetworkInterfaceID.NotFound) | **DONE** |
|
||||
| `disassociate-address` | `--association-id` | `--dry-run` | Association must exist | Gateway validates AssociationId non-empty → NATS `ec2.DisassociateAddress` → daemon scans KV to find EIP by AssociationId → publishes `vpc.delete-nat` event to vpcd → clears association fields → reverts state to allocated → updates KV → returns success | 1. Disassociate existing<br>2. Non-existent association (InvalidAssociationID.NotFound)<br>3. Missing AssociationId (MissingParameter) | **DONE** |
|
||||
| `describe-addresses` | `--allocation-ids`, `--public-ips`, `--filters` (allocation-id, public-ip, instance-id, association-id, domain, tag:\*) | `--dry-run` | None | NATS `ec2.DescribeAddresses` → daemon scans all EIP records in account → filters by AllocationIds and/or PublicIps if specified → applies filters → returns Address list with allocation, association, PublicIp, PrivateIpAddress, NetworkInterfaceId, InstanceId, Domain | 1. List all EIPs<br>2. Filter by allocation ID<br>3. Filter by public IP<br>4. Non-existent AllocationId returns error<br>5. Empty result when no EIPs<br>6. Filter by instance-id, domain<br>7. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
| `describe-addresses-attribute` | `--allocation-ids` (optional filter) | `--attribute`, `--dry-run`, `--max-results`, `--next-token` | None | Gateway validates input → NATS `ec2.DescribeAddressesAttribute` with `spinifex-workers` queue group → daemon looks up EIP records by AllocationIds (or scans all account EIPs if none specified) → returns AddressAttribute list with AllocationId and PublicIp per entry. Unlike DescribeAddresses, returns empty list (not error) for non-existent AllocationIds. PtrRecord always nil (reverse-DNS not supported). | 1. List all EIP attributes<br>2. Filter by allocation IDs<br>3. Non-existent AllocationId returns empty (not error)<br>4. Nil input returns error | **DONE** |
|
||||
|
||||
### EC2 - NAT Gateway
|
||||
|
||||
NAT gateways are stored in `spinifex-vpc-nat-gateways` KV bucket. Deleted gateways are moved to `spinifex-vpc-deleted-nat-gateways` with a 1-hour TTL for Terraform polling. SNAT rule coordination uses `vpc.add-nat-gateway` and `vpc.delete-nat-gateway` NATS events (published via route table service on CreateRoute and during DeleteNatGateway respectively).
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-nat-gateway` | — | `--subnet-id`, `--allocation-id`, `--connectivity-type`, `--tag-specifications` | Subnet must exist, EIP must be allocated (for public type) | NATS `ec2.CreateNatGateway` → daemon creates NAT gateway in JetStream KV with nat-ID → configure OVS SNAT rules → state=pending→available → return NatGateway | 1. Create public NAT gateway<br>2. Create private NAT gateway (no allocation-id)<br>3. Invalid subnet (error) | **NOT STARTED** |
|
||||
| `delete-nat-gateway` | — | `--nat-gateway-id`, `--dry-run` | NAT gateway must exist | NATS `ec2.DeleteNatGateway` → daemon marks as deleting → remove OVS SNAT rules → delete from KV → return NatGatewayId | 1. Delete existing NAT gateway<br>2. Delete non-existent (error) | **NOT STARTED** |
|
||||
| `describe-nat-gateways` | — | `--nat-gateway-ids`, `--filters`, `--max-results` | None | NATS `ec2.DescribeNatGateways` → daemon lists NAT gateways from KV → return list with state, subnet, addresses | 1. List all NAT gateways<br>2. Filter by ID<br>3. Filter by VPC ID | **NOT STARTED** |
|
||||
| `create-nat-gateway` | `--subnet-id`, `--allocation-id` | `--connectivity-type` (public only), `--tag-specifications`, `--dry-run` | Subnet must exist, EIP must be allocated and unassociated | Gateway validates SubnetId + AllocationId (required) → NATS `ec2.CreateNatGateway` with `spinifex-workers` queue group → daemon verifies subnet exists in `spinifex-vpc-subnets` KV → verifies EIP exists in `spinifex-vpc-eips` KV and has no AssociationId (Resource.AlreadyAssociated if in use) → generates nat-ID → stores NatGatewayRecord in `spinifex-vpc-nat-gateways` KV with state=available → returns NatGateway with addresses | 1. Create NAT gateway (happy path)<br>2. Subnet not found (InvalidSubnetID.NotFound)<br>3. EIP not found (InvalidAllocationID.NotFound)<br>4. EIP already associated (Resource.AlreadyAssociated)<br>5. Missing SubnetId or AllocationId (MissingParameter) | **DONE** |
|
||||
| `delete-nat-gateway` | `--nat-gateway-id` | `--dry-run` | NAT gateway must exist | Gateway validates NatGatewayId (required) → NATS `ec2.DeleteNatGateway` → daemon retrieves record from KV → scans all route tables in VPC for routes targeting this NAT gateway → publishes `vpc.delete-nat-gateway` event per affected subnet (SNAT rule cleanup) → sets state=deleted → stores in `spinifex-vpc-deleted-nat-gateways` with 1-hour TTL → deletes from active KV bucket | 1. Delete existing NAT gateway<br>2. Delete non-existent (InvalidNatGatewayID.NotFound)<br>3. Deleted gateway visible when queried by ID (state=deleted)<br>4. Deleted gateway excluded from general list queries (TTL auto-expire) | **DONE** |
|
||||
| `describe-nat-gateways` | `--nat-gateway-ids`, `--filters` (vpc-id, state) | `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeNatGateways` → daemon scans `spinifex-vpc-nat-gateways` KV → applies NatGatewayIds filter and/or vpc-id/state filters → for specific IDs not found in active bucket, checks deleted bucket → returns InvalidNatGatewayID.NotFound if ID never existed → returns list with state, addresses, subnet, VPC | 1. List all NAT gateways<br>2. Filter by specific IDs<br>3. Filter by state<br>4. Filter by vpc-id<br>5. Non-existent ID (InvalidNatGatewayID.NotFound)<br>6. Deleted gateway visible by ID query | **DONE** |
|
||||
| `assign-private-nat-gateway-address` | — | `--nat-gateway-id`, `--private-ip-addresses` | NAT gateway must exist (private type) | NATS `ec2.AssignPrivateNatGatewayAddress` → daemon assigns private IPs to NAT gateway → return assigned addresses | 1. Assign address<br>2. Missing NAT gateway ID (error) | **NOT STARTED** |
|
||||
| `associate-nat-gateway-address` | — | `--nat-gateway-id`, `--allocation-ids` | NAT gateway and EIPs must exist | NATS `ec2.AssociateNatGatewayAddress` → daemon associates EIPs with NAT gateway → return association info | 1. Associate EIP<br>2. Missing NAT gateway ID (error) | **NOT STARTED** |
|
||||
|
||||
### EC2 - VPC Networking (Core)
|
||||
|
||||
Requires OVN/OVS (`apt install openvswitch-switch ovn-host`). VPC/Subnet/ENI CRUD stores metadata in NATS KV and publishes events to vpcd (the network topology daemon) which translates to OVN logical switches, ports, and routers. Single AZ for Hive v1. IPAM uses CAS-based allocation from NATS KV (`hive-vpc-ipam` bucket).
|
||||
Requires OVN/OVS (`apt install openvswitch-switch ovn-host`). VPC/Subnet/ENI/SG CRUD stores metadata in NATS KV and publishes events to vpcd (the network topology daemon) which translates to OVN logical switches, ports, routers, and ACLs. Single AZ for Spinifex v1. IPAM uses CAS-based allocation from NATS KV (`spinifex-vpc-ipam` bucket). Security groups stored in `spinifex-vpc-security-groups` KV bucket with optimistic locking on rule updates.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-vpc` | `--cidr-block`, `--tag-specifications` | `--instance-tenancy`, `--dry-run` | None | Gateway validates CidrBlock (required, valid CIDR, /16–/28 prefix) → NATS `ec2.CreateVpc` → daemon allocates unique VNI from `hive-vpc-vni-counter` KV → generates vpc-ID → stores VPCRecord in `hive-vpc-vpcs` KV → publishes `vpc.create` event to vpcd (creates OVN logical switch) → returns VPC with state=available | 1. Create VPC with /16 CIDR<br>2. Invalid CIDR format (error: InvalidVpcRange)<br>3. CIDR prefix outside /16–/28 (error)<br>4. Verify in describe-vpcs<br>5. Tags applied at creation | **DONE** |
|
||||
| `delete-vpc` | `--vpc-id` | `--dry-run` | VPC must be empty (no subnets) | Gateway validates VpcId → NATS `ec2.DeleteVpc` → daemon checks for dependent subnets in `hive-vpc-subnets` KV → returns DependencyViolation if subnets exist → deletes from `hive-vpc-vpcs` KV → publishes `vpc.delete` event to vpcd (removes OVN resources) | 1. Delete empty VPC<br>2. Delete VPC with subnets (DependencyViolation)<br>3. Delete non-existent VPC (error) | **DONE** |
|
||||
| `describe-vpcs` | `--vpc-ids` | `--filters`, `--max-results`, `--dry-run` | None | NATS `ec2.DescribeVpcs` → daemon lists all keys from `hive-vpc-vpcs` KV → filters by VPC IDs if specified → returns error for non-existent requested IDs → returns VPC list with CIDR, state, CIDR associations | 1. List all VPCs<br>2. Filter by VPC ID<br>3. Non-existent VPC ID returns error | **DONE** |
|
||||
| `modify-vpc-attribute` | — | `--vpc-id`, `--enable-dns-support`, `--enable-dns-hostnames` | VPC must exist | NATS `ec2.ModifyVpcAttribute` → daemon updates VPC attribute in KV → return success | 1. Enable DNS support<br>2. Enable DNS hostnames<br>3. Missing VPC ID (error) | **NOT STARTED** |
|
||||
| `create-vpc` | `--cidr-block`, `--tag-specifications` | `--instance-tenancy`, `--dry-run` | None | Gateway validates CidrBlock (required, valid CIDR, /16–/28 prefix) → NATS `ec2.CreateVpc` → daemon allocates unique VNI from `spinifex-vpc-vni-counter` KV → generates vpc-ID → stores VPCRecord in `spinifex-vpc-vpcs` KV → publishes `vpc.create` event to vpcd (creates OVN logical switch) → returns VPC with state=available | 1. Create VPC with /16 CIDR<br>2. Invalid CIDR format (error: InvalidVpcRange)<br>3. CIDR prefix outside /16–/28 (error)<br>4. Verify in describe-vpcs<br>5. Tags applied at creation | **DONE** |
|
||||
| `delete-vpc` | `--vpc-id` | `--dry-run` | VPC must be empty (no subnets) | Gateway validates VpcId → NATS `ec2.DeleteVpc` → daemon checks for dependent subnets in `spinifex-vpc-subnets` KV → returns DependencyViolation if subnets exist → deletes from `spinifex-vpc-vpcs` KV → publishes `vpc.delete` event to vpcd (removes OVN resources) | 1. Delete empty VPC<br>2. Delete VPC with subnets (DependencyViolation)<br>3. Delete non-existent VPC (error) | **DONE** |
|
||||
| `describe-vpcs` | `--vpc-ids`, `--filters` (vpc-id, state, cidr-block, is-default, owner-id, tag:\*) | `--max-results`, `--dry-run` | None | NATS `ec2.DescribeVpcs` → daemon lists all keys from `spinifex-vpc-vpcs` KV → filters by VPC IDs if specified → applies filters → returns error for non-existent requested IDs → returns VPC list with CIDR, state, CIDR associations | 1. List all VPCs<br>2. Filter by VPC ID<br>3. Non-existent VPC ID returns error<br>4. Filter by state, cidr-block, is-default<br>5. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
| `modify-vpc-attribute` | `--vpc-id`, `--enable-dns-hostnames`, `--enable-dns-support`, `--enable-network-address-usage-metrics` | `--dry-run` | VPC must exist | Gateway validates VpcId non-empty → NATS `ec2.ModifyVpcAttribute` → daemon retrieves VPC record from `spinifex-vpc-vpcs` KV → updates boolean flags if provided → updates KV with optimistic locking → returns empty output | 1. Enable DNS hostnames<br>2. Enable DNS support<br>3. Enable network address usage metrics<br>4. Missing VPC ID (MissingParameter)<br>5. Non-existent VPC (InvalidVpcID.NotFound) | **DONE** |
|
||||
| `describe-vpc-attribute` | `--vpc-id`, `--attribute` (enableDnsHostnames, enableDnsSupport, enableNetworkAddressUsageMetrics) | `--dry-run` | VPC must exist | Gateway validates VpcId + Attribute non-empty → NATS `ec2.DescribeVpcAttribute` → daemon retrieves VPC record → returns single attribute as AttributeBooleanValue (one attribute per call, matches AWS behavior) → validates attribute name | 1. Get enableDnsHostnames<br>2. Get enableDnsSupport<br>3. Get enableNetworkAddressUsageMetrics<br>4. Invalid attribute name (InvalidParameterValue)<br>5. Missing VPC ID (MissingParameter)<br>6. Non-existent VPC (InvalidVpcID.NotFound) | **DONE** |
|
||||
| `associate-vpc-cidr-block` | — | `--vpc-id`, `--cidr-block` | VPC must exist | NATS `ec2.AssociateVpcCidrBlock` → daemon adds secondary CIDR to VPC metadata → return association | 1. Add secondary CIDR<br>2. Overlapping CIDR (error)<br>3. Max CIDR blocks exceeded (error) | **NOT STARTED** |
|
||||
| `disassociate-vpc-cidr-block` | — | `--association-id` | Association must exist | NATS `ec2.DisassociateVpcCidrBlock` → daemon removes secondary CIDR from VPC → return success | 1. Remove secondary CIDR<br>2. Remove primary CIDR (error) | **NOT STARTED** |
|
||||
| `create-subnet` | `--vpc-id`, `--cidr-block`, `--availability-zone`, `--tag-specifications` | `--dry-run` | VPC must exist | Gateway validates VpcId + CidrBlock (required, valid CIDR, /16–/28 prefix) → NATS `ec2.CreateSubnet` → daemon verifies parent VPC exists → validates subnet CIDR is within VPC CIDR range → checks for overlapping subnets in same VPC → calculates available IPs (total hosts - 5 AWS-reserved: .0/.1/.2/.3/.255) → generates subnet-ID → stores SubnetRecord in `hive-vpc-subnets` KV → publishes `vpc.create-subnet` event to vpcd → defaults AZ from config if omitted | 1. Create subnet within VPC CIDR<br>2. Subnet CIDR outside VPC range (error: InvalidSubnetRange)<br>3. Overlapping subnet CIDRs (error: InvalidSubnetConflict)<br>4. CIDR prefix outside /16–/28 (error)<br>5. Missing VPC or CIDR (error: MissingParameter) | **DONE** |
|
||||
| `delete-subnet` | `--subnet-id` | `--dry-run` | Subnet must exist | NATS `ec2.DeleteSubnet` → daemon verifies subnet exists → deletes from `hive-vpc-subnets` KV → publishes `vpc.delete-subnet` event to vpcd | 1. Delete existing subnet<br>2. Delete non-existent subnet (error) | **DONE** |
|
||||
| `describe-subnets` | `--subnet-ids`, `--filters` (vpc-id) | `--max-results`, `--dry-run` | None | NATS `ec2.DescribeSubnets` → daemon lists all keys from `hive-vpc-subnets` KV → applies vpc-id filter if present → filters by subnet IDs if specified → recalculates available IPs from CIDR → returns error for non-existent requested IDs | 1. List all subnets<br>2. Filter by VPC ID<br>3. Filter by subnet ID<br>4. Non-existent subnet returns error | **DONE** |
|
||||
| `modify-subnet-attribute` | — | `--subnet-id`, `--map-public-ip-on-launch`, `--assign-ipv6-address-on-creation` | Subnet must exist | NATS `ec2.ModifySubnetAttribute` → daemon updates subnet attribute in KV → return success | 1. Enable auto-assign public IP<br>2. Missing subnet ID (error) | **NOT STARTED** |
|
||||
| `create-subnet` | `--vpc-id`, `--cidr-block`, `--availability-zone`, `--tag-specifications` | `--dry-run` | VPC must exist | Gateway validates VpcId + CidrBlock (required, valid CIDR, /16–/28 prefix) → NATS `ec2.CreateSubnet` → daemon verifies parent VPC exists → validates subnet CIDR is within VPC CIDR range → checks for overlapping subnets in same VPC → calculates available IPs (total hosts - 5 AWS-reserved: .0/.1/.2/.3/.255) → generates subnet-ID → stores SubnetRecord in `spinifex-vpc-subnets` KV → publishes `vpc.create-subnet` event to vpcd → defaults AZ from config if omitted | 1. Create subnet within VPC CIDR<br>2. Subnet CIDR outside VPC range (error: InvalidSubnetRange)<br>3. Overlapping subnet CIDRs (error: InvalidSubnetConflict)<br>4. CIDR prefix outside /16–/28 (error)<br>5. Missing VPC or CIDR (error: MissingParameter) | **DONE** |
|
||||
| `delete-subnet` | `--subnet-id` | `--dry-run` | Subnet must exist | NATS `ec2.DeleteSubnet` → daemon verifies subnet exists → deletes from `spinifex-vpc-subnets` KV → publishes `vpc.delete-subnet` event to vpcd | 1. Delete existing subnet<br>2. Delete non-existent subnet (error) | **DONE** |
|
||||
| `describe-subnets` | `--subnet-ids`, `--filters` (vpc-id, subnet-id, availability-zone, cidr-block, state, default-for-az, tag:\*) | `--max-results`, `--dry-run` | None | NATS `ec2.DescribeSubnets` → daemon lists all keys from `spinifex-vpc-subnets` KV → applies filters → filters by subnet IDs if specified → recalculates available IPs from CIDR → returns error for non-existent requested IDs | 1. List all subnets<br>2. Filter by VPC ID<br>3. Filter by subnet ID<br>4. Non-existent subnet returns error<br>5. Filter by availability-zone, cidr-block, state<br>6. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
| `modify-subnet-attribute` | `--subnet-id`, `--map-public-ip-on-launch` | `--assign-ipv6-address-on-creation`, `--dry-run` | Subnet must exist | Gateway validates SubnetId non-empty → NATS `ec2.ModifySubnetAttribute` → daemon retrieves Subnet record from `spinifex-vpc-subnets` KV → updates MapPublicIpOnLaunch flag if provided → updates KV with optimistic locking → returns empty output | 1. Enable auto-assign public IP<br>2. Disable auto-assign public IP<br>3. Missing SubnetId (MissingParameter)<br>4. Non-existent subnet (InvalidSubnetID.NotFound) | **DONE** |
|
||||
| `associate-subnet-cidr-block` | — | `--subnet-id`, `--ipv6-cidr-block` | Subnet must exist | NATS `ec2.AssociateSubnetCidrBlock` → daemon adds IPv6 CIDR to subnet → return association | 1. Add IPv6 CIDR<br>2. Missing subnet ID (error) | **NOT STARTED** |
|
||||
| `disassociate-subnet-cidr-block` | — | `--association-id` | Association must exist | NATS `ec2.DisassociateSubnetCidrBlock` → daemon removes IPv6 CIDR from subnet → return success | 1. Remove IPv6 CIDR<br>2. Invalid association (error) | **NOT STARTED** |
|
||||
| `create-security-group` | — | `--group-name`, `--description`, `--vpc-id`, `--tag-specifications` | VPC must exist | Create security group metadata → create default OVS flow rules (deny all inbound, allow all outbound) → assign sg-ID | 1. Create SG in VPC<br>2. Duplicate name in same VPC (error)<br>3. Verify default rules | **NOT STARTED** |
|
||||
| `delete-security-group` | — | `--group-id` | SG must not be in use by instances | Verify no instances reference SG → remove OVS flow rules → delete metadata | 1. Delete unused SG<br>2. Delete SG in use (error) | **NOT STARTED** |
|
||||
| `describe-security-groups` | — | `--group-ids`, `--group-names`, `--filters`, `--max-results` | None | Query NATS KV for SG metadata → return SG list with rules | 1. List all SGs<br>2. Filter by VPC ID<br>3. Filter by group name | **NOT STARTED** |
|
||||
| `authorize-security-group-ingress` | — | `--group-id`, `--protocol`, `--port`, `--cidr`, `--source-group` | SG must exist | Add inbound rule → create OVS OpenFlow rule → persist to metadata | 1. Allow SSH (port 22) from 0.0.0.0/0<br>2. Allow from specific CIDR<br>3. Duplicate rule (idempotent) | **NOT STARTED** |
|
||||
| `authorize-security-group-egress` | — | `--group-id`, `--protocol`, `--port`, `--cidr` | SG must exist | Add outbound rule → create OVS OpenFlow rule → persist to metadata | 1. Allow HTTPS outbound<br>2. Restrict to specific CIDR | **NOT STARTED** |
|
||||
| `revoke-security-group-ingress` | — | `--group-id`, `--protocol`, `--port`, `--cidr` | SG must exist, rule must exist | Remove inbound rule → delete OVS OpenFlow rule → update metadata | 1. Revoke existing rule<br>2. Revoke non-existent rule (error) | **NOT STARTED** |
|
||||
| `revoke-security-group-egress` | — | `--group-id`, `--protocol`, `--port`, `--cidr` | SG must exist, rule must exist | Remove outbound rule → delete OVS OpenFlow rule → update metadata | 1. Revoke existing rule<br>2. Revoke non-existent rule (error) | **NOT STARTED** |
|
||||
| `create-internet-gateway` | `--tag-specifications` | `--dry-run` | None | NATS `ec2.CreateInternetGateway` → daemon generates igw-ID → creates IGWRecord in `hive-igw` KV bucket with state=available (not attached) → returns InternetGateway with empty Attachments | 1. Create IGW<br>2. Verify in describe-internet-gateways<br>3. Tags applied at creation | **DONE** |
|
||||
| `create-security-group` | `--group-name`, `--description`, `--vpc-id`, `--tag-specifications` | `--dry-run` | VPC must exist | Gateway validates GroupName + VpcId required → NATS `ec2.CreateSecurityGroup` → daemon verifies VPC exists → checks duplicate name in same VPC → generates sg-ID → creates SecurityGroupRecord in `spinifex-vpc-security-groups` KV with default egress rule (allow all outbound 0.0.0.0/0), no ingress rules → publishes `vpc.create-sg` event to vpcd (OVN ACL configuration) → returns GroupId | 1. Create SG in VPC<br>2. Duplicate name in same VPC (error: InvalidGroup.Duplicate)<br>3. Verify default egress rule (allow all)<br>4. Verify no default ingress rules<br>5. Missing GroupName (MissingParameter)<br>6. Non-existent VPC (InvalidVpcID.NotFound)<br>7. With tag specifications | **DONE** |
|
||||
| `delete-security-group` | `--group-id` | `--dry-run` | SG must exist | Gateway validates GroupId non-empty → NATS `ec2.DeleteSecurityGroup` → daemon retrieves SG from KV → deletes from `spinifex-vpc-security-groups` KV → publishes `vpc.delete-sg` event to vpcd → returns success | 1. Delete existing SG<br>2. Delete non-existent SG (error: InvalidGroup.NotFound)<br>3. Missing GroupId (MissingParameter) | **DONE** |
|
||||
| `describe-security-groups` | `--group-ids`, `--filters` (vpc-id, group-name, group-id, description, ip-permission.cidr, tag:\*) | `--group-names`, `--max-results`, `--dry-run` | None | NATS `ec2.DescribeSecurityGroups` → daemon scans all SG records in account → applies filters (AND logic across filter names, OR across values) → returns SecurityGroup list with ingress/egress rules → returns error for non-existent requested GroupIds | 1. List all SGs<br>2. Filter by VPC ID<br>3. Filter by group name<br>4. Filter by GroupId<br>5. Non-existent GroupId (InvalidGroup.NotFound)<br>6. Empty result when no SGs<br>7. Filter by description, ip-permission.cidr<br>8. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
| `authorize-security-group-ingress` | `--group-id`, `--ip-permissions` (IpProtocol, FromPort, ToPort, IpRanges/CidrIp, UserIdGroupPairs/GroupId) | `--dry-run` | SG must exist | Gateway validates GroupId non-empty → NATS `ec2.AuthorizeSecurityGroupIngress` → daemon retrieves SG → converts IpPermissions to SGRule objects → appends to IngressRules → updates KV with optimistic locking (revision check) → publishes `vpc.update-sg` event to vpcd → returns true | 1. Allow SSH (port 22) from 0.0.0.0/0<br>2. Allow from specific CIDR<br>3. Allow from source security group<br>4. Missing GroupId (MissingParameter)<br>5. Non-existent SG (InvalidGroup.NotFound) | **DONE** |
|
||||
| `authorize-security-group-egress` | `--group-id`, `--ip-permissions` | `--dry-run` | SG must exist | Gateway validates GroupId → NATS `ec2.AuthorizeSecurityGroupEgress` → daemon retrieves SG → appends rules to EgressRules → updates KV with optimistic locking → publishes `vpc.update-sg` event → returns true | 1. Allow HTTPS outbound<br>2. Restrict to specific CIDR<br>3. Missing GroupId (MissingParameter)<br>4. Non-existent SG (InvalidGroup.NotFound) | **DONE** |
|
||||
| `revoke-security-group-ingress` | `--group-id`, `--ip-permissions` (matching rules to remove) | `--dry-run` | SG must exist | Gateway validates GroupId → NATS `ec2.RevokeSecurityGroupIngress` → daemon retrieves SG → removes matching rules from IngressRules (set difference) → updates KV with optimistic locking → publishes `vpc.update-sg` event → returns true | 1. Revoke existing rule<br>2. Missing GroupId (MissingParameter)<br>3. Non-existent SG (InvalidGroup.NotFound) | **DONE** |
|
||||
| `revoke-security-group-egress` | `--group-id`, `--ip-permissions` (matching rules to remove) | `--dry-run` | SG must exist | Gateway validates GroupId → NATS `ec2.RevokeSecurityGroupEgress` → daemon retrieves SG → removes matching rules from EgressRules (set difference) → updates KV with optimistic locking → publishes `vpc.update-sg` event → returns true | 1. Revoke existing rule<br>2. Missing GroupId (MissingParameter)<br>3. Non-existent SG (InvalidGroup.NotFound) | **DONE** |
|
||||
| `create-internet-gateway` | `--tag-specifications` | `--dry-run` | None | NATS `ec2.CreateInternetGateway` → daemon generates igw-ID → creates IGWRecord in `spinifex-igw` KV bucket with state=available (not attached) → returns InternetGateway with empty Attachments | 1. Create IGW<br>2. Verify in describe-internet-gateways<br>3. Tags applied at creation | **DONE** |
|
||||
| `attach-internet-gateway` | `--internet-gateway-id`, `--vpc-id` | `--dry-run` | IGW and VPC must exist | NATS `ec2.AttachInternetGateway` → daemon validates IGW exists and is not already attached (ResourceAlreadyAssociated) → sets VpcId on record → state=attached → publishes `vpc.igw-attach` event to vpcd (creates OVN external switch, gateway port, SNAT rules) | 1. Attach IGW to VPC<br>2. Attach already-attached IGW (ResourceAlreadyAssociated)<br>3. Non-existent IGW (error)<br>4. Missing params (MissingParameter) | **DONE** |
|
||||
| `detach-internet-gateway` | `--internet-gateway-id`, `--vpc-id` | `--dry-run` | IGW must be attached to specified VPC | NATS `ec2.DetachInternetGateway` → daemon validates IGW exists and is attached to specified VPC (GatewayNotAttached if wrong VPC) → clears VpcId → state=available → publishes `vpc.igw-detach` event to vpcd (removes OVN resources) | 1. Detach attached IGW<br>2. Detach from wrong VPC (GatewayNotAttached)<br>3. Detach unattached IGW (error) | **DONE** |
|
||||
| `delete-internet-gateway` | `--internet-gateway-id` | `--dry-run` | IGW must be detached | NATS `ec2.DeleteInternetGateway` → daemon validates IGW exists and VpcId is empty (DependencyViolation if attached) → deletes from `hive-igw` KV | 1. Delete detached IGW<br>2. Delete attached IGW (DependencyViolation)<br>3. Delete non-existent (error) | **DONE** |
|
||||
| `describe-internet-gateways` | `--internet-gateway-ids` | `--filters`, `--max-results`, `--dry-run` | None | NATS `ec2.DescribeInternetGateways` → daemon lists all keys from `hive-igw` KV → filters by IGW IDs if specified → returns list with Attachments array (if attached) | 1. List all IGWs<br>2. Filter by IGW ID<br>3. Verify attachment info | **DONE** |
|
||||
| `delete-internet-gateway` | `--internet-gateway-id` | `--dry-run` | IGW must be detached | NATS `ec2.DeleteInternetGateway` → daemon validates IGW exists and VpcId is empty (DependencyViolation if attached) → deletes from `spinifex-igw` KV | 1. Delete detached IGW<br>2. Delete attached IGW (DependencyViolation)<br>3. Delete non-existent (error) | **DONE** |
|
||||
| `describe-internet-gateways` | `--internet-gateway-ids`, `--filters` (internet-gateway-id, attachment.vpc-id, attachment.state, tag:\*) | `--max-results`, `--dry-run` | None | NATS `ec2.DescribeInternetGateways` → daemon lists all keys from `spinifex-igw` KV → filters by IGW IDs if specified → applies filters → returns list with Attachments array (if attached) | 1. List all IGWs<br>2. Filter by IGW ID<br>3. Verify attachment info<br>4. Filter by attachment.vpc-id, attachment.state<br>5. Unknown filter returns InvalidParameterValue | **DONE** |
|
||||
|
||||
### EC2 - Route Tables
|
||||
|
||||
Route tables are stored in `spinifex-vpc-route-tables` JetStream KV bucket, keyed by `{accountID}.{routeTableId}`. Each route table has a Routes array (with local route pre-populated) and an Associations array. All operations validate VPC consistency. When a route targeting a NAT gateway is created, `vpc.add-nat-gateway` events are published per associated subnet for vpcd SNAT rule creation.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-route-table` | — | `--vpc-id`, `--tag-specifications` | VPC must exist | NATS `ec2.CreateRouteTable` → daemon creates route table in KV with rtb-ID → add default local route for VPC CIDR → return route table | 1. Create route table<br>2. Verify default local route<br>3. Missing VPC ID (error) | **NOT STARTED** |
|
||||
| `delete-route-table` | — | `--route-table-id`, `--dry-run` | Route table must exist and not be main route table | NATS `ec2.DeleteRouteTable` → daemon verifies not main table → delete from KV → return success | 1. Delete non-main route table<br>2. Delete main route table (error)<br>3. Delete non-existent (error) | **NOT STARTED** |
|
||||
| `describe-route-tables` | — | `--route-table-ids`, `--filters`, `--max-results` | None | NATS `ec2.DescribeRouteTables` → daemon lists route tables from KV → return tables with routes | 1. List all route tables<br>2. Filter by VPC | **NOT STARTED** |
|
||||
| `create-route` | — | `--route-table-id`, `--destination-cidr-block`, `--gateway-id` or `--nat-gateway-id` or `--egress-only-internet-gateway-id` | Route table and target must exist | NATS `ec2.CreateRoute` → daemon adds route entry → configure OVS flow for destination CIDR → update metadata | 1. Add internet route via IGW<br>2. Add route to NAT gateway<br>3. Add route to EIGW<br>4. Conflicting route (error) | **NOT STARTED** |
|
||||
| `delete-route` | — | `--route-table-id`, `--destination-cidr-block` | Route must exist | NATS `ec2.DeleteRoute` → daemon removes route entry → remove OVS flow → update metadata | 1. Delete existing route<br>2. Delete non-existent route (error)<br>3. Delete local route (error) | **NOT STARTED** |
|
||||
| `replace-route` | — | `--route-table-id`, `--destination-cidr-block`, `--gateway-id` or `--nat-gateway-id` | Route must exist | NATS `ec2.ReplaceRoute` → daemon replaces route target → update OVS flow → update metadata | 1. Replace IGW route with NAT<br>2. Replace non-existent route (error) | **NOT STARTED** |
|
||||
| `associate-route-table` | — | `--route-table-id`, `--subnet-id` or `--gateway-id` | Route table and target must exist | NATS `ec2.AssociateRouteTable` → daemon links route table to subnet/gateway → return rtbassoc-ID | 1. Associate with subnet<br>2. Missing route table ID (error) | **NOT STARTED** |
|
||||
| `disassociate-route-table` | — | `--association-id` | Association must exist, not main table association | NATS `ec2.DisassociateRouteTable` → daemon unlinks route table → return success | 1. Disassociate existing<br>2. Disassociate main table (error) | **NOT STARTED** |
|
||||
| `create-route-table` | `--vpc-id` | `--tag-specifications`, `--dry-run` | VPC must exist | Gateway validates VpcId (required) → NATS `ec2.CreateRouteTable` with `spinifex-workers` queue group → daemon verifies VPC exists in `spinifex-vpc-vpcs` KV → retrieves VPC CIDR → generates rtb-ID → creates RouteTableRecord with IsMain=false, local route pre-populated (destination=VPC CIDR, gateway="local"), empty associations → stores in `spinifex-vpc-route-tables` KV → returns RouteTable | 1. Create route table with local route (happy path)<br>2. VPC not found (InvalidVpcID.NotFound)<br>3. Missing VpcId (MissingParameter) | **DONE** |
|
||||
| `delete-route-table` | `--route-table-id` | `--dry-run` | Route table must exist, not main, no subnet associations | Gateway validates RouteTableId (required) → NATS `ec2.DeleteRouteTable` → daemon retrieves record → rejects if IsMain=true (DependencyViolation) → rejects if has non-main subnet associations (DependencyViolation) → deletes from KV → returns success | 1. Delete custom route table<br>2. Delete main route table (DependencyViolation)<br>3. Delete table with subnet associations (DependencyViolation)<br>4. Delete non-existent (InvalidRouteTableID.NotFound) | **DONE** |
|
||||
| `describe-route-tables` | `--route-table-ids`, `--filters` (vpc-id, route-table-id, association.main, association.route-table-association-id, association.subnet-id, route.destination-cidr-block, route.gateway-id) | `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeRouteTables` → daemon scans all keys in `spinifex-vpc-route-tables` KV → filters by RouteTableIds if specified → applies all provided filters (AND logic) → returns error for non-existent requested IDs → converts records to EC2 RouteTable objects | 1. List all route tables<br>2. Filter by vpc-id<br>3. Filter by association.main (true/false)<br>4. Non-existent ID (InvalidRouteTableID.NotFound)<br>5. Empty result when no route tables | **DONE** |
|
||||
| `create-route` | `--route-table-id`, `--destination-cidr-block`, `--gateway-id`, `--nat-gateway-id` | `--egress-only-internet-gateway-id`, `--vpc-peering-connection-id`, `--dry-run` | Route table and target must exist in same VPC | Gateway validates RouteTableId + DestinationCidrBlock (required) → NATS `ec2.CreateRoute` → daemon retrieves route table → checks for duplicate destination (RouteAlreadyExists) → validates exactly one of GatewayId/NatGatewayId provided → if GatewayId: verifies IGW exists in `spinifex-igw` KV and is attached to same VPC → if NatGatewayId: verifies NAT GW exists in `spinifex-vpc-nat-gateways` KV and is in same VPC → for NAT GW routes, publishes `vpc.add-nat-gateway` event per associated subnet → appends RouteRecord (state=active, origin=CreateRoute) → updates KV → returns Return=true | 1. Add IGW route (happy path)<br>2. Duplicate destination (RouteAlreadyExists)<br>3. IGW not found (InvalidInternetGatewayID.NotFound)<br>4. NAT GW not found (InvalidNatGatewayID.NotFound)<br>5. Gateway not in same VPC (InvalidParameterValue)<br>6. Missing RouteTableId or DestinationCidrBlock (MissingParameter) | **DONE** |
|
||||
| `delete-route` | `--route-table-id`, `--destination-cidr-block` | `--dry-run` | Route must exist | Gateway validates RouteTableId + DestinationCidrBlock (required) → NATS `ec2.DeleteRoute` → daemon retrieves route table → finds matching route → rejects if gateway is "local" (InvalidParameterValue — local routes cannot be deleted) → removes route from Routes array → updates KV → returns success | 1. Delete IGW route (happy path, local route remains)<br>2. Delete local route (InvalidParameterValue)<br>3. Route not found (InvalidRoute.NotFound)<br>4. Missing parameters (MissingParameter) | **DONE** |
|
||||
| `replace-route` | `--route-table-id`, `--destination-cidr-block`, `--gateway-id` | `--nat-gateway-id`, `--dry-run` | Route must exist, new target must exist in same VPC | Gateway validates RouteTableId + DestinationCidrBlock + GatewayId (required) → NATS `ec2.ReplaceRoute` → daemon retrieves route table → finds matching route → rejects if local route → verifies IGW exists and is attached to same VPC → updates route GatewayId, clears NatGatewayId, sets state=active → updates KV → returns success | 1. Replace route target (IGW swap)<br>2. Replace local route (InvalidParameterValue)<br>3. Route not found (InvalidRoute.NotFound)<br>4. IGW not found (InvalidInternetGatewayID.NotFound) | **DONE** |
|
||||
| `associate-route-table` | `--route-table-id`, `--subnet-id` | `--gateway-id`, `--dry-run` | Route table and subnet must exist in same VPC | Gateway validates RouteTableId + SubnetId (required) → NATS `ec2.AssociateRouteTable` → daemon retrieves route table → verifies subnet exists and is in same VPC → scans ALL route tables in VPC to check subnet isn't already explicitly associated (Resource.AlreadyAssociated) → generates rtbassoc-ID → appends AssociationRecord (Main=false) → updates KV → returns AssociationId with state=associated | 1. Associate subnet (happy path)<br>2. Duplicate subnet association (Resource.AlreadyAssociated)<br>3. Subnet not found (InvalidSubnetID.NotFound)<br>4. Subnet in different VPC (InvalidParameterValue)<br>5. Missing parameters (MissingParameter) | **DONE** |
|
||||
| `disassociate-route-table` | `--association-id` | `--dry-run` | Association must exist, not main table association | Gateway validates AssociationId (required) → NATS `ec2.DisassociateRouteTable` → daemon scans ALL route tables in account to find matching AssociationId → rejects if Main=true (InvalidParameterValue — main associations are immutable) → removes association from Associations array → updates KV → returns success | 1. Disassociate subnet (happy path)<br>2. Disassociate main table (InvalidParameterValue)<br>3. Association not found (InvalidAssociationID.NotFound) | **DONE** |
|
||||
| `replace-route-table-association` | `--association-id`, `--route-table-id` | `--dry-run` | Association and new route table must exist | Gateway validates AssociationId + RouteTableId (required) → NATS `ec2.ReplaceRouteTableAssociation` → daemon verifies new route table exists → scans all route tables to find matching AssociationId → if Main=true: atomically swaps IsMain flag between old and new table → two-phase commit: removes association from old table, creates new AssociationId on new table → compensating write on failure (logs CRITICAL if compensation fails) → returns new AssociationId with state=associated | 1. Move subnet between tables (happy path)<br>2. Replace main table association (swaps IsMain flags)<br>3. Association not found (InvalidAssociationID.NotFound)<br>4. Route table not found (InvalidRouteTableID.NotFound) | **DONE** |
|
||||
|
||||
### EC2 - VPC Peering
|
||||
|
||||
@@ -365,16 +390,16 @@ Requires OVN/OVS (`apt install openvswitch-switch ovn-host`). VPC/Subnet/ENI CRU
|
||||
|
||||
### EC2 - Network Interfaces
|
||||
|
||||
ENIs are managed via `hive-vpc-enis` KV bucket with CAS-based IPAM allocation from `hive-vpc-ipam`. ENIs are auto-created by `run-instances --subnet-id` and auto-deleted on termination. MAC addresses are deterministically generated from the ENI ID. Attach/detach are internal operations used by instance lifecycle — no standalone attach/detach API yet.
|
||||
ENIs are managed via `spinifex-vpc-enis` KV bucket with CAS-based IPAM allocation from `spinifex-vpc-ipam`. ENIs are auto-created by `run-instances --subnet-id` and auto-deleted on termination. MAC addresses are deterministically generated from the ENI ID. Attach/detach are internal operations used by instance lifecycle — no standalone attach/detach API yet.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-network-interface` | `--subnet-id`, `--private-ip-address`, `--description`, `--tag-specifications` | `--groups`, `--dry-run` | Subnet must exist | Gateway validates SubnetId (required) → NATS `ec2.CreateNetworkInterface` → daemon verifies subnet exists in `hive-vpc-subnets` KV → if no PrivateIpAddress, allocates from IPAM pool (CAS-based, up to 5 retries) → generates eni-ID → generates deterministic MAC from ENI ID → stores ENIRecord in `hive-vpc-enis` KV → publishes `vpc.create-port` event to vpcd (creates OVN logical port) → returns NetworkInterface with status=available | 1. Create in subnet (auto-allocate IP)<br>2. Create with specific private IP<br>3. Missing subnet ID (MissingParameter)<br>4. Non-existent subnet (error)<br>5. Tags applied at creation | **DONE** |
|
||||
| `delete-network-interface` | `--network-interface-id` | `--dry-run` | Must not be attached (status != in-use) | NATS `ec2.DeleteNetworkInterface` → daemon verifies ENI exists and status is not "in-use" (InvalidNetworkInterfaceInUse) → releases IP back to IPAM pool → deletes from `hive-vpc-enis` KV → publishes `vpc.delete-port` event to vpcd | 1. Delete detached ENI<br>2. Delete attached ENI (InvalidNetworkInterfaceInUse)<br>3. Delete non-existent (error) | **DONE** |
|
||||
| `describe-network-interfaces` | `--network-interface-ids`, `--filters` (subnet-id, vpc-id, attachment.instance-id) | `--max-results`, `--dry-run` | None | NATS `ec2.DescribeNetworkInterfaces` → daemon lists all keys from `hive-vpc-enis` KV → applies filters (subnet-id, vpc-id, attachment.instance-id) → filters by ENI IDs if specified → returns error for non-existent requested IDs | 1. List all ENIs<br>2. Filter by subnet-id<br>3. Filter by vpc-id<br>4. Filter by attachment.instance-id<br>5. Non-existent ENI returns error | **DONE** |
|
||||
| `create-network-interface` | `--subnet-id`, `--private-ip-address`, `--description`, `--tag-specifications` | `--groups`, `--dry-run` | Subnet must exist | Gateway validates SubnetId (required) → NATS `ec2.CreateNetworkInterface` → daemon verifies subnet exists in `spinifex-vpc-subnets` KV → if no PrivateIpAddress, allocates from IPAM pool (CAS-based, up to 5 retries) → generates eni-ID → generates deterministic MAC from ENI ID → stores ENIRecord in `spinifex-vpc-enis` KV → publishes `vpc.create-port` event to vpcd (creates OVN logical port) → returns NetworkInterface with status=available | 1. Create in subnet (auto-allocate IP)<br>2. Create with specific private IP<br>3. Missing subnet ID (MissingParameter)<br>4. Non-existent subnet (error)<br>5. Tags applied at creation | **DONE** |
|
||||
| `delete-network-interface` | `--network-interface-id` | `--dry-run` | Must not be attached (status != in-use) | NATS `ec2.DeleteNetworkInterface` → daemon verifies ENI exists and status is not "in-use" (InvalidNetworkInterfaceInUse) → releases IP back to IPAM pool → deletes from `spinifex-vpc-enis` KV → publishes `vpc.delete-port` event to vpcd | 1. Delete detached ENI<br>2. Delete attached ENI (InvalidNetworkInterfaceInUse)<br>3. Delete non-existent (error) | **DONE** |
|
||||
| `describe-network-interfaces` | `--network-interface-ids`, `--filters` (subnet-id, vpc-id, attachment.instance-id) | `--max-results`, `--dry-run` | None | NATS `ec2.DescribeNetworkInterfaces` → daemon lists all keys from `spinifex-vpc-enis` KV → applies filters (subnet-id, vpc-id, attachment.instance-id) → filters by ENI IDs if specified → returns error for non-existent requested IDs | 1. List all ENIs<br>2. Filter by subnet-id<br>3. Filter by vpc-id<br>4. Filter by attachment.instance-id<br>5. Non-existent ENI returns error | **DONE** |
|
||||
| `attach-network-interface` | — | `--network-interface-id`, `--instance-id`, `--device-index` | ENI and instance must exist | Internal operation used by `run-instances --subnet-id`. Marks ENI as "in-use", sets instance attachment, generates attachment ID. No standalone API endpoint yet. | 1. Attach ENI to instance<br>2. Already attached (error)<br>3. Missing device index (error) | **NOT STARTED** (internal only) |
|
||||
| `detach-network-interface` | — | `--attachment-id`, `--force` | ENI must be attached | Internal operation used by terminate-instances. Marks ENI as "available", clears attachment info. No standalone API endpoint yet. | 1. Detach ENI<br>2. Force detach<br>3. Not attached (error) | **NOT STARTED** (internal only) |
|
||||
| `modify-network-interface-attribute` | — | `--network-interface-id`, `--description`, `--source-dest-check`, `--groups` | ENI must exist | NATS `ec2.ModifyNetworkInterfaceAttribute` → daemon updates ENI attributes in KV → return success | 1. Modify description<br>2. Toggle source/dest check<br>3. Update security groups | **NOT STARTED** |
|
||||
| `modify-network-interface-attribute` | — | `--network-interface-id`, `--description`, `--groups` | ENI must exist; at least one attribute required | NATS `ec2.ModifyNetworkInterfaceAttribute` → daemon validates ENI exists → updates description and/or security groups in `spinifex-vpc-enis` KV (optimistic locking) → return success | 1. Modify description<br>2. Update security groups<br>3. No attributes (InvalidParameterValue)<br>4. Non-existent ENI (error) | **DONE** |
|
||||
| `assign-private-ip-addresses` | — | `--network-interface-id`, `--private-ip-addresses` or `--secondary-private-ip-address-count` | ENI must exist | NATS `ec2.AssignPrivateIpAddresses` → daemon allocates IPs from subnet pool → assign to ENI → return assigned IPs | 1. Assign specific IPs<br>2. Assign by count<br>3. Cannot specify both (error) | **NOT STARTED** |
|
||||
| `unassign-private-ip-addresses` | — | `--network-interface-id`, `--private-ip-addresses` | IPs must be assigned to ENI | NATS `ec2.UnassignPrivateIpAddresses` → daemon releases IPs → return success | 1. Unassign secondary IP<br>2. Unassign primary IP (error) | **NOT STARTED** |
|
||||
|
||||
@@ -407,19 +432,6 @@ ENIs are managed via `hive-vpc-enis` KV bucket with CAS-based IPAM allocation fr
|
||||
| `list-snapshot-blocks` | — | `--snapshot-id`, `--max-results`, `--next-token` | Snapshot must exist | List all non-empty blocks in snapshot with tokens and sizes | 1. List blocks of populated snapshot<br>2. List blocks of empty snapshot | **NOT STARTED** |
|
||||
| `list-changed-blocks` | — | `--second-snapshot-id`, `--first-snapshot-id`, `--max-results` | Snapshots must exist | Compare two snapshots → return list of differing block indexes | 1. Compare parent and child snapshots<br>2. Compare unrelated snapshots | **NOT STARTED** |
|
||||
|
||||
### S3 (via Predastore)
|
||||
|
||||
Currently S3 operations are handled directly by Predastore. Consider moving control/data plane to Hive format for AWS gateway integration.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `s3 mb` (CreateBucket) | — | `s3://bucket-name` | None | NATS `s3.createbucket` → Predastore creates bucket → return success | 1. Create valid bucket<br>2. Duplicate bucket name (error)<br>3. Invalid bucket name (error) | **NOT STARTED** (Predastore handles directly) |
|
||||
| `s3 rb` (DeleteBucket) | — | `s3://bucket-name`, `--force` | Bucket must exist | NATS `s3.deletebucket` → verify bucket empty (or force) → Predastore deletes bucket | 1. Delete empty bucket<br>2. Delete non-empty bucket (error)<br>3. Force delete non-empty bucket | **NOT STARTED** |
|
||||
| `s3 ls` (ListBuckets/Objects) | — | `s3://bucket/prefix`, `--recursive` | None | NATS `s3.listobjects` → Predastore lists objects with prefix → return list | 1. List all buckets<br>2. List objects in bucket<br>3. List with prefix filter | **NOT STARTED** |
|
||||
| `s3 cp` (PutObject/GetObject) | — | `source dest`, `--recursive`, `--acl` | Bucket must exist | NATS `s3.putobject`/`s3.getobject` → Predastore stores/retrieves with Reed-Solomon encoding | 1. Upload single file<br>2. Download single file<br>3. Recursive upload/download<br>4. Large file (multipart) | **NOT STARTED** |
|
||||
| `s3 rm` (DeleteObject) | — | `s3://bucket/key`, `--recursive` | Object must exist | NATS `s3.deleteobject` → Predastore deletes object shards | 1. Delete single object<br>2. Recursive delete<br>3. Delete non-existent (idempotent) | **NOT STARTED** |
|
||||
| `s3 sync` | — | `source dest`, `--delete`, `--exclude`, `--include` | Bucket must exist | Compare source and dest → upload changed/new files → optionally delete removed files | 1. Sync local dir to S3<br>2. Sync with --delete<br>3. Sync with exclude filter | **NOT STARTED** |
|
||||
|
||||
### IAM - User Management
|
||||
|
||||
All IAM operations are account-scoped. Users, policies, and access keys are namespaced per account. The gateway extracts the account ID from the authenticated access key via `c.Locals("sigv4.accountId")` and passes it to all IAM service methods. Root user (account `000000000000`) bypasses policy evaluation entirely.
|
||||
@@ -475,5 +487,140 @@ The gateway evaluates IAM policies on every request (except root user bypass). P
|
||||
| Explicit deny | Deny statements override Allow (deny wins) | **DONE** |
|
||||
| Wildcard matching | `*` matches all, `ec2:*` matches all EC2, `ec2:Describe*` matches prefix | **DONE** |
|
||||
| Account-scoped evaluation | Policies resolved from caller's account context | **DONE** |
|
||||
| EC2 action mapping | 54 EC2 actions mapped to IAM policy strings | **DONE** |
|
||||
| EC2 action mapping | All registered EC2 actions mapped to IAM policy strings | **DONE** |
|
||||
| IAM action mapping | 16 IAM actions mapped to IAM policy strings | **DONE** |
|
||||
|
||||
### IAM - Roles
|
||||
|
||||
IAM roles are required for instance profiles, cross-account access, and service-linked permissions. Without roles, workloads must use static access keys. Roles should be stored in a `spinifex-iam-roles` KV bucket keyed by `{accountID}.{roleName}`. Trust policies (AssumeRolePolicyDocument) define who can assume the role.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-role` | — | `--role-name`, `--assume-role-policy-document`, `--path`, `--description`, `--max-session-duration`, `--permissions-boundary`, `--tags` | None | IAMService.CreateRole(accountID, input) → validate RoleName + AssumeRolePolicyDocument (required) → validate trust policy JSON → generate AROA-prefixed RoleId → store RoleRecord in `spinifex-iam-roles` KV at `{accountID}.{roleName}` → ARN: `arn:aws:iam::{accountID}:role/{path}{roleName}` → return Role | 1. Create with valid trust policy<br>2. Malformed trust policy (MalformedPolicyDocument)<br>3. Duplicate name (EntityAlreadyExists)<br>4. Missing RoleName (InvalidInput)<br>5. With path and description | **NOT STARTED** |
|
||||
| `get-role` | — | `--role-name` | Role must exist | IAMService.GetRole(accountID, input) → reads from KV at `{accountID}.{roleName}` → returns Role with ARN, Path, AssumeRolePolicyDocument, CreateDate | 1. Get existing role<br>2. Non-existent role (NoSuchEntity) | **NOT STARTED** |
|
||||
| `list-roles` | — | `--path-prefix`, `--max-items`, `--marker` | None | IAMService.ListRoles(accountID, input) → prefix-scans KV for `{accountID}.*` → filters by path prefix → returns role list scoped to caller's account | 1. List all roles<br>2. List with path prefix<br>3. Empty list<br>4. Account isolation | **NOT STARTED** |
|
||||
| `delete-role` | — | `--role-name` | Role must exist, no attached policies, no instance profiles | IAMService.DeleteRole(accountID, input) → verify no attached policies (DeleteConflict) → verify not in any instance profile (DeleteConflict) → delete from KV | 1. Delete role with no attachments<br>2. Delete role with policies (DeleteConflict)<br>3. Delete role in instance profile (DeleteConflict)<br>4. Non-existent role (NoSuchEntity) | **NOT STARTED** |
|
||||
| `update-role` | — | `--role-name`, `--description`, `--max-session-duration` | Role must exist | IAMService.UpdateRole(accountID, input) → update description and/or session duration in KV → return success | 1. Update description<br>2. Update max session duration<br>3. Non-existent role (NoSuchEntity) | **NOT STARTED** |
|
||||
| `update-assume-role-policy` | — | `--role-name`, `--policy-document` | Role must exist | IAMService.UpdateAssumeRolePolicy(accountID, input) → validate trust policy JSON → update AssumeRolePolicyDocument in KV → return success | 1. Update trust policy<br>2. Malformed policy (MalformedPolicyDocument)<br>3. Non-existent role (NoSuchEntity) | **NOT STARTED** |
|
||||
| `attach-role-policy` | — | `--role-name`, `--policy-arn` | Role and policy must exist | IAMService.AttachRolePolicy(accountID, input) → verify policy exists → add ARN to role's AttachedPolicies → idempotent | 1. Attach policy<br>2. Already attached (idempotent)<br>3. Non-existent role (NoSuchEntity)<br>4. Non-existent policy (NoSuchEntity) | **NOT STARTED** |
|
||||
| `detach-role-policy` | — | `--role-name`, `--policy-arn` | Policy must be attached | IAMService.DetachRolePolicy(accountID, input) → remove ARN from role's AttachedPolicies | 1. Detach attached policy<br>2. Not attached (NoSuchEntity)<br>3. Non-existent role (NoSuchEntity) | **NOT STARTED** |
|
||||
| `list-attached-role-policies` | — | `--role-name`, `--path-prefix`, `--max-items`, `--marker` | Role must exist | IAMService.ListAttachedRolePolicies(accountID, input) → reads role's AttachedPolicies list → returns ARNs and names | 1. List attached policies<br>2. No policies (empty list)<br>3. Non-existent role (NoSuchEntity) | **NOT STARTED** |
|
||||
|
||||
### IAM - Instance Profiles
|
||||
|
||||
Instance profiles are containers for IAM roles that allow EC2 instances to assume a role and obtain temporary credentials. Required for `run-instances --iam-instance-profile`. Stored in `spinifex-iam-instance-profiles` KV bucket keyed by `{accountID}.{profileName}`.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-instance-profile` | — | `--instance-profile-name`, `--path`, `--tags` | None | IAMService.CreateInstanceProfile(accountID, input) → validate name (required) → generate AIPA-prefixed InstanceProfileId → store in `spinifex-iam-instance-profiles` KV with empty Roles array → ARN: `arn:aws:iam::{accountID}:instance-profile/{path}{name}` → return InstanceProfile | 1. Create profile<br>2. Duplicate name (EntityAlreadyExists)<br>3. Missing name (InvalidInput) | **NOT STARTED** |
|
||||
| `get-instance-profile` | — | `--instance-profile-name` | Profile must exist | IAMService.GetInstanceProfile(accountID, input) → reads from KV → returns InstanceProfile with Roles array | 1. Get existing profile<br>2. Non-existent (NoSuchEntity) | **NOT STARTED** |
|
||||
| `list-instance-profiles` | — | `--path-prefix`, `--max-items`, `--marker` | None | IAMService.ListInstanceProfiles(accountID, input) → prefix-scans KV → returns list | 1. List all profiles<br>2. Empty list | **NOT STARTED** |
|
||||
| `list-instance-profiles-for-role` | — | `--role-name`, `--max-items`, `--marker` | Role must exist | IAMService.ListInstanceProfilesForRole(accountID, input) → scan all profiles → filter where Roles contains role name → return list | 1. List profiles for role<br>2. Role not in any profile (empty) | **NOT STARTED** |
|
||||
| `delete-instance-profile` | — | `--instance-profile-name` | Profile must exist, Roles must be empty | IAMService.DeleteInstanceProfile(accountID, input) → verify Roles empty (DeleteConflict if not) → delete from KV | 1. Delete empty profile<br>2. Delete profile with role (DeleteConflict)<br>3. Non-existent (NoSuchEntity) | **NOT STARTED** |
|
||||
| `add-role-to-instance-profile` | — | `--instance-profile-name`, `--role-name` | Both must exist, max 1 role per profile | IAMService.AddRoleToInstanceProfile(accountID, input) → verify profile exists → verify role exists → check Roles array is empty (LimitExceeded if role already present) → append role → update KV | 1. Add role to empty profile<br>2. Profile already has role (LimitExceeded)<br>3. Non-existent profile (NoSuchEntity)<br>4. Non-existent role (NoSuchEntity) | **NOT STARTED** |
|
||||
| `remove-role-from-instance-profile` | — | `--instance-profile-name`, `--role-name` | Role must be in profile | IAMService.RemoveRoleFromInstanceProfile(accountID, input) → remove role from Roles array → update KV | 1. Remove role<br>2. Role not in profile (NoSuchEntity) | **NOT STARTED** |
|
||||
|
||||
### IAM - Groups
|
||||
|
||||
Groups provide a way to attach policies to multiple users. Stored in `spinifex-iam-groups` KV bucket keyed by `{accountID}.{groupName}`.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-group` | — | `--group-name`, `--path` | None | IAMService.CreateGroup(accountID, input) → validate GroupName (required) → generate AGPA-prefixed GroupId → store in KV → return Group | 1. Create group<br>2. Duplicate name (EntityAlreadyExists)<br>3. Missing name (InvalidInput) | **NOT STARTED** |
|
||||
| `get-group` | — | `--group-name` | Group must exist | IAMService.GetGroup(accountID, input) → reads group from KV → returns Group with member Users list | 1. Get existing group<br>2. Non-existent (NoSuchEntity) | **NOT STARTED** |
|
||||
| `list-groups` | — | `--path-prefix`, `--max-items`, `--marker` | None | IAMService.ListGroups(accountID, input) → prefix-scans KV → returns list | 1. List all groups<br>2. Empty list | **NOT STARTED** |
|
||||
| `delete-group` | — | `--group-name` | Group must exist, no members, no attached policies | IAMService.DeleteGroup(accountID, input) → verify no members → verify no attached policies → delete from KV | 1. Delete empty group<br>2. Delete group with members (DeleteConflict)<br>3. Delete group with policies (DeleteConflict)<br>4. Non-existent (NoSuchEntity) | **NOT STARTED** |
|
||||
| `add-user-to-group` | — | `--group-name`, `--user-name` | Both must exist | IAMService.AddUserToGroup(accountID, input) → verify group + user exist → add user to group's Members list → idempotent | 1. Add user<br>2. Already a member (idempotent)<br>3. Non-existent group (NoSuchEntity)<br>4. Non-existent user (NoSuchEntity) | **NOT STARTED** |
|
||||
| `remove-user-from-group` | — | `--group-name`, `--user-name` | User must be in group | IAMService.RemoveUserFromGroup(accountID, input) → remove user from Members list → update KV | 1. Remove member<br>2. Not a member (NoSuchEntity) | **NOT STARTED** |
|
||||
| `list-groups-for-user` | — | `--user-name`, `--max-items`, `--marker` | User must exist | IAMService.ListGroupsForUser(accountID, input) → scan all groups → filter where Members contains user → return list | 1. List groups for user<br>2. User in no groups (empty) | **NOT STARTED** |
|
||||
| `attach-group-policy` | — | `--group-name`, `--policy-arn` | Group and policy must exist | IAMService.AttachGroupPolicy(accountID, input) → verify policy exists → add ARN to group's AttachedPolicies → idempotent | 1. Attach policy<br>2. Already attached (idempotent)<br>3. Non-existent group (NoSuchEntity) | **NOT STARTED** |
|
||||
| `detach-group-policy` | — | `--group-name`, `--policy-arn` | Policy must be attached | IAMService.DetachGroupPolicy(accountID, input) → remove ARN from AttachedPolicies | 1. Detach policy<br>2. Not attached (NoSuchEntity) | **NOT STARTED** |
|
||||
| `list-attached-group-policies` | — | `--group-name`, `--path-prefix`, `--max-items`, `--marker` | Group must exist | IAMService.ListAttachedGroupPolicies(accountID, input) → reads group's AttachedPolicies → returns ARNs and names | 1. List attached policies<br>2. No policies (empty) | **NOT STARTED** |
|
||||
|
||||
### STS (Security Token Service)
|
||||
|
||||
STS provides temporary credentials and identity verification. `GetCallerIdentity` is called by every AWS SDK/CLI invocation. `AssumeRole` is required once IAM roles are implemented. STS needs its own gateway service endpoint (`sts` service in the `supportedServices` map) with a dedicated action map.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `get-caller-identity` | — | *(no flags)* | Valid SigV4 credentials | Gateway extracts accountID and username from authenticated request → returns Account, Arn (`arn:aws:iam::{accountID}:user/{userName}` or `arn:aws:sts::{accountID}:assumed-role/{roleName}/{sessionName}`), UserId. Note: a Spinifex-custom version exists at `spinifex.GetCallerIdentity` but needs a proper STS endpoint. | 1. Get identity for IAM user<br>2. Get identity for assumed role<br>3. Get identity for root user<br>4. Invalid credentials (AuthFailure) | **NOT STARTED** |
|
||||
| `assume-role` | — | `--role-arn`, `--role-session-name`, `--duration-seconds`, `--policy`, `--external-id`, `--serial-number`, `--token-code`, `--tags`, `--transitive-tag-keys` | Role must exist, caller must be allowed by trust policy | Gateway validates RoleArn + RoleSessionName (required) → looks up role → evaluates trust policy (AssumeRolePolicyDocument) against caller → generates temporary credentials (AccessKeyId with ASIA prefix, SecretAccessKey, SessionToken) → credentials expire after DurationSeconds (default 3600, max 43200) → return Credentials + AssumedRoleUser (ARN, AssumedRoleId) | 1. Assume role with valid trust policy<br>2. Trust policy denies caller (AccessDenied)<br>3. Non-existent role (NoSuchEntity)<br>4. Expired session token rejected at SigV4<br>5. Custom duration<br>6. Session tags | **NOT STARTED** |
|
||||
| `get-session-token` | — | `--duration-seconds`, `--serial-number`, `--token-code` | Valid IAM user credentials (not role) | Gateway validates caller is IAM user (not assumed role) → generates temporary credentials (ASIA prefix) → session inherits user's permissions → return Credentials | 1. Get token for IAM user<br>2. Duration within range<br>3. Reject if caller is assumed role | **NOT STARTED** |
|
||||
|
||||
### ELBv2 (Elastic Load Balancing v2 — Application Load Balancer)
|
||||
|
||||
All ELBv2 resources are stored in the `spinifex-elbv2` JetStream KV bucket. Key format: `lb.{lbId}`, `tg.{tgId}`, `listener.{listenerId}`. The data plane uses a system-managed ALB VM running HAProxy, launched automatically during CreateLoadBalancer. HAProxy configuration is pushed via NATS (`elbv2.alb.{lbId}.config` topic) whenever listeners or targets change. Agent readiness is polled via `elbv2.alb.{lbId}.ping`.
|
||||
|
||||
#### ELBv2 - Load Balancers
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-load-balancer` | `--name`, `--subnets`, `--security-groups`, `--scheme` (internet-facing/internal), `--tags`, `--ip-address-type` (ipv4 only) | `--type` (hardcoded application), `--customer-owned-ipv4-pool`, `--dry-run` | None (subnets optional) | Gateway validates Name (required) → NATS `elbv2.CreateLoadBalancer` with `spinifex-workers` queue group → daemon validates scheme (internet-facing or internal, InvalidScheme if other) → checks duplicate name in account (DuplicateLoadBalancerName) → generates LB ID → builds DNS name `{name}-{lbId}.{region}.elb.spinifex.local` → if subnets provided, creates one ENI per subnet with `spinifex:managed-by=elbv2` tag (rolls back on failure) → if system AMI available, launches t3.nano ALB VM with cloud-init running `/usr/local/bin/alb-agent` → state=provisioning → background goroutine polls agent ping for ~5min → on agent ready: state=active, on timeout: state=failed → if no VM launcher: state=active immediately → persists to KV → returns LoadBalancer with ARN, DNSName, State | 1. Create with name (happy path)<br>2. Internal scheme<br>3. Invalid scheme (InvalidScheme)<br>4. Duplicate name (DuplicateLoadBalancerName)<br>5. Missing name (MissingParameter)<br>6. With tags<br>7. With subnets (ENI creation) | **DONE** |
|
||||
| `delete-load-balancer` | `--load-balancer-arn` | `--dry-run` | LB must exist | Gateway validates LoadBalancerArn (required) → NATS `elbv2.DeleteLoadBalancer` → daemon verifies LB exists and belongs to account → terminates ALB VM (async background) → deletes all listeners for this LB → deletes system-managed ENIs → deletes LB record from KV → returns success | 1. Delete existing LB<br>2. Not found (LoadBalancerNotFound)<br>3. Cascades listener cleanup<br>4. ENI cleanup | **DONE** |
|
||||
| `describe-load-balancers` | `--load-balancer-arns`, `--names` | `--page-size`, `--marker`, `--dry-run` | None | NATS `elbv2.DescribeLoadBalancers` → daemon lists all LB records from KV → filters by account ID → applies ARN filter and/or Name filter → returns LoadBalancer list with State, DNSName, Scheme, VpcId, AvailabilityZones | 1. List all LBs<br>2. Filter by name<br>3. Account isolation | **DONE** |
|
||||
|
||||
#### ELBv2 - Target Groups
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-target-group` | `--name`, `--protocol` (HTTP), `--port` (default 80), `--vpc-id`, `--target-type` (instance only), `--health-check-protocol`, `--health-check-port`, `--health-check-path` (default /), `--health-check-interval-seconds` (default 30), `--health-check-timeout-seconds` (default 5), `--healthy-threshold-count` (default 5), `--unhealthy-threshold-count` (default 2), `--matcher` (HttpCode, default 200), `--tags` | `--health-check-enabled`, `--protocol-version`, `--ip-address-type`, `--dry-run` | None | Gateway validates Name (required) → NATS `elbv2.CreateTargetGroup` → daemon checks duplicate name in same VPC (DuplicateTargetGroupName) → generates TG ID → creates HealthCheckConfig with provided values or defaults → stores record with empty Targets array → returns TargetGroup with ARN | 1. Create with defaults<br>2. Custom health check<br>3. Duplicate name (DuplicateTargetGroupName)<br>4. Missing name (MissingParameter) | **DONE** |
|
||||
| `delete-target-group` | `--target-group-arn` | `--dry-run` | TG must exist, not referenced by listener | Gateway validates TargetGroupArn (required) → NATS `elbv2.DeleteTargetGroup` → daemon verifies TG exists → scans all listeners to check if any DefaultActions reference this TG (TargetGroupInUse if referenced) → deletes from KV | 1. Delete TG<br>2. TG in use by listener (TargetGroupInUse)<br>3. Not found (TargetGroupNotFound) | **DONE** |
|
||||
| `describe-target-groups` | `--target-group-arns`, `--names`, `--load-balancer-arn` | `--page-size`, `--marker`, `--dry-run` | None | NATS `elbv2.DescribeTargetGroups` → daemon lists all TG records → filters by account → applies ARN/Name filters → if LoadBalancerArn specified, lists listeners for that LB and includes only TGs referenced by those listeners → returns TargetGroup list | 1. List all TGs<br>2. Filter by LB ARN (via listener lookup)<br>3. Account isolation | **DONE** |
|
||||
|
||||
#### ELBv2 - Targets
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `register-targets` | `--target-group-arn`, `--targets` (Id=instance-id, Port=optional override) | `--dry-run` | TG must exist, instances should exist | Gateway validates TargetGroupArn + Targets (required) → NATS `elbv2.RegisterTargets` → daemon fetches TG → deduplicates against existing targets (key: `{id}:{port}`, idempotent) → resolves instance ID → private IP via VPC ENI lookup → adds target with state=initial → persists to KV → calls `pushConfigForTargetGroup()` to regenerate HAProxy config on all LBs using this TG → returns success | 1. Register targets<br>2. Idempotent re-registration<br>3. TG not found (TargetGroupNotFound)<br>4. Missing ARN or targets (MissingParameter) | **DONE** |
|
||||
| `deregister-targets` | `--target-group-arn`, `--targets` (Id, Port) | `--dry-run` | TG must exist | Gateway validates TargetGroupArn + Targets (required) → NATS `elbv2.DeregisterTargets` → daemon fetches TG → builds removal set (key: `{id}:{port}`) → filters Targets array to keep only non-matching → persists to KV → calls `pushConfigForTargetGroup()` to update HAProxy config → returns success | 1. Deregister targets<br>2. TG not found (TargetGroupNotFound)<br>3. Missing parameters (MissingParameter) | **DONE** |
|
||||
| `describe-target-health` | `--target-group-arn`, `--targets` (optional filter to specific targets) | `--include` | TG must exist | Gateway validates TargetGroupArn (required) → NATS `elbv2.DescribeTargetHealth` → daemon fetches TG → builds filter set from Targets param (if specified) → for each target, returns TargetHealthDescription with Target (Id, Port) and TargetHealth (State, Description). Note: health state is stored in records, not actively polled — HAProxy runs its own independent health checks. | 1. Describe all targets<br>2. Filter to specific targets<br>3. TG not found (TargetGroupNotFound) | **DONE** |
|
||||
|
||||
#### ELBv2 - Listeners
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-listener` | `--load-balancer-arn`, `--default-actions` (Type=forward, TargetGroupArn), `--protocol` (HTTP, default), `--port` (default 80) | `--ssl-policy`, `--certificates`, `--alpn-policy`, `--mutual-authentication`, `--dry-run` | LB must exist | Gateway validates LoadBalancerArn + DefaultActions (required) → NATS `elbv2.CreateListener` → daemon verifies LB exists → checks for duplicate port on same LB (DuplicateListener) → generates listener ID → builds ARN → stores ListenerRecord in KV → calls `pushConfig(lb)` to regenerate HAProxy config (creates frontend for listener port, backend for target group with health checks) → returns Listener with ARN, Protocol, Port, DefaultActions | 1. Create listener<br>2. Duplicate port (DuplicateListener)<br>3. LB not found (LoadBalancerNotFound)<br>4. Graceful handling when NATS unavailable | **DONE** |
|
||||
| `delete-listener` | `--listener-arn` | `--dry-run` | Listener must exist | Gateway validates ListenerArn (required) → NATS `elbv2.DeleteListener` → daemon verifies listener exists and belongs to account → deletes from KV → fetches parent LB → calls `pushConfig(lb)` to regenerate HAProxy config without this listener → returns success | 1. Delete listener<br>2. Not found (ListenerNotFound) | **DONE** |
|
||||
| `describe-listeners` | `--load-balancer-arn`, `--listener-arns` | `--page-size`, `--marker`, `--dry-run` | None | NATS `elbv2.DescribeListeners` → daemon lists listeners → if LoadBalancerArn specified, filters to that LB → applies ARN filter → account isolation → returns Listener list | 1. Filter by LB ARN<br>2. Account isolation | **DONE** |
|
||||
|
||||
#### ELBv2 - Not Implemented
|
||||
|
||||
| Feature | Description | Priority | Status |
|
||||
|---------|-------------|----------|--------|
|
||||
| Listener rules | Path-based and hostname-based routing (CreateRule, DeleteRule, DescribeRules, ModifyRule) | High | **NOT STARTED** |
|
||||
| HTTPS/TLS termination | SSL certificates, SSL policies, ALPN | High | **NOT STARTED** |
|
||||
| Modify operations | ModifyLoadBalancerAttributes, ModifyTargetGroup, ModifyTargetGroupAttributes, ModifyListener | Medium | **NOT STARTED** |
|
||||
| Connection draining | Deregistration delay for graceful target removal | Medium | **NOT STARTED** |
|
||||
| Stickiness | Session affinity / sticky sessions on target groups | Medium | **NOT STARTED** |
|
||||
| Active health checking | API-driven health state updates (currently HAProxy-only) | Medium | **NOT STARTED** |
|
||||
| Target types | IP and Lambda target types (only instance supported) | Low | **NOT STARTED** |
|
||||
| Access logs | S3 access log delivery | Low | **NOT STARTED** |
|
||||
| WAF integration | AWS WAF association for web application firewall | Low | **NOT STARTED** |
|
||||
|
||||
### CloudWatch (Basic Monitoring)
|
||||
|
||||
Basic CloudWatch metrics support is required for `monitor-instances`/`unmonitor-instances` to be meaningful. Daemon nodes should publish CPU, disk, and network metrics for running instances. Metrics stored in a time-series-friendly structure in JetStream KV or dedicated NATS subjects.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `put-metric-data` | — | `--namespace`, `--metric-data` (MetricName, Value, Unit, Timestamp, Dimensions) | None | Gateway validates Namespace + MetricData (required) → NATS `cloudwatch.PutMetricData` → daemon stores metric data points in time-series KV (key: `{namespace}.{metricName}.{dimensionHash}`) → return success | 1. Put single metric<br>2. Put with dimensions (InstanceId)<br>3. Missing namespace (MissingParameter) | **NOT STARTED** |
|
||||
| `get-metric-statistics` | — | `--namespace`, `--metric-name`, `--start-time`, `--end-time`, `--period`, `--statistics` (Average, Sum, Min, Max, SampleCount), `--dimensions` | None | Gateway validates required fields → NATS `cloudwatch.GetMetricStatistics` → daemon queries time-series data → aggregates by period → returns Datapoints with requested statistics | 1. Get CPU utilization over 1 hour<br>2. Get with dimensions (InstanceId)<br>3. No data returns empty | **NOT STARTED** |
|
||||
| `list-metrics` | — | `--namespace`, `--metric-name`, `--dimensions`, `--recently-active` | None | NATS `cloudwatch.ListMetrics` → daemon scans metric namespaces → returns Metrics list with Namespace, MetricName, Dimensions | 1. List all metrics<br>2. Filter by namespace<br>3. Filter by metric name | **NOT STARTED** |
|
||||
| `describe-alarms` | — | `--alarm-names`, `--alarm-name-prefix`, `--state-value`, `--action-prefix` | None | NATS `cloudwatch.DescribeAlarms` → daemon lists alarms from KV → return MetricAlarms with State, Threshold, ComparisonOperator | 1. List all alarms<br>2. Filter by state (OK, ALARM, INSUFFICIENT_DATA) | **NOT STARTED** |
|
||||
| `put-metric-alarm` | — | `--alarm-name`, `--namespace`, `--metric-name`, `--statistic`, `--period`, `--evaluation-periods`, `--threshold`, `--comparison-operator`, `--alarm-actions`, `--dimensions` | None | NATS `cloudwatch.PutMetricAlarm` → daemon stores alarm config in KV → alarm evaluation runs on metric publish → state transitions trigger alarm actions | 1. Create alarm<br>2. Update existing alarm<br>3. Missing required fields | **NOT STARTED** |
|
||||
| `delete-alarms` | — | `--alarm-names` | Alarms must exist | NATS `cloudwatch.DeleteAlarms` → daemon deletes alarm records from KV → return success | 1. Delete existing alarm<br>2. Non-existent alarm (ResourceNotFound) | **NOT STARTED** |
|
||||
|
||||
### Auto Scaling
|
||||
|
||||
Auto Scaling manages instance fleets based on scaling policies. Depends on Launch Templates (EC2) and optionally ELBv2 target groups for health-based scaling. Stored in `spinifex-autoscaling` KV bucket.
|
||||
|
||||
| Command | Implemented Flags | Missing Flags | Prerequisites | Basic Logic | Test Cases | Status |
|
||||
|---------|-------------------|---------------|---------------|-------------|------------|--------|
|
||||
| `create-auto-scaling-group` | — | `--auto-scaling-group-name`, `--launch-template` (Id/Name, Version), `--min-size`, `--max-size`, `--desired-capacity`, `--vpc-zone-identifier` (subnet IDs), `--target-group-arns`, `--health-check-type` (EC2/ELB), `--health-check-grace-period`, `--tags` | Launch template must exist | NATS `autoscaling.CreateAutoScalingGroup` → daemon validates launch template → stores ASG record in KV → launches DesiredCapacity instances using template → registers with target groups if specified → starts health check loop → return success | 1. Create ASG with launch template<br>2. Scale to desired capacity<br>3. Missing launch template (ValidationError)<br>4. Min > Max (ValidationError) | **NOT STARTED** |
|
||||
| `update-auto-scaling-group` | — | `--auto-scaling-group-name`, `--min-size`, `--max-size`, `--desired-capacity`, `--launch-template`, `--health-check-type`, `--health-check-grace-period` | ASG must exist | NATS `autoscaling.UpdateAutoScalingGroup` → daemon updates ASG config in KV → if DesiredCapacity changed, scale up/down accordingly → return success | 1. Update desired capacity<br>2. Update launch template<br>3. Non-existent ASG (ValidationError) | **NOT STARTED** |
|
||||
| `delete-auto-scaling-group` | — | `--auto-scaling-group-name`, `--force-delete` | ASG must exist | NATS `autoscaling.DeleteAutoScalingGroup` → if force: terminate all instances → deregister from target groups → delete from KV → return success. Without force: reject if instances > 0 | 1. Delete empty ASG<br>2. Force delete with instances<br>3. Delete with instances (ResourceInUse) | **NOT STARTED** |
|
||||
| `describe-auto-scaling-groups` | — | `--auto-scaling-group-names`, `--filters`, `--max-records` | None | NATS `autoscaling.DescribeAutoScalingGroups` → daemon lists ASG records → return list with Instances, DesiredCapacity, MinSize, MaxSize | 1. List all ASGs<br>2. Filter by name | **NOT STARTED** |
|
||||
| `set-desired-capacity` | — | `--auto-scaling-group-name`, `--desired-capacity`, `--honor-cooldown` | ASG must exist | NATS `autoscaling.SetDesiredCapacity` → daemon updates desired count → scale up (launch) or scale down (terminate oldest) → return success | 1. Scale up<br>2. Scale down<br>3. Exceeds max (ValidationError) | **NOT STARTED** |
|
||||
| `describe-auto-scaling-instances` | — | `--instance-ids`, `--max-records` | None | NATS `autoscaling.DescribeAutoScalingInstances` → daemon lists all ASG-managed instances → return list with ASG name, lifecycle state, health status | 1. List all ASG instances<br>2. Filter by instance ID | **NOT STARTED** |
|
||||
| `put-scaling-policy` | — | `--auto-scaling-group-name`, `--policy-name`, `--policy-type` (TargetTrackingScaling, StepScaling, SimpleScaling), `--target-tracking-configuration`, `--scaling-adjustment`, `--cooldown` | ASG must exist | NATS `autoscaling.PutScalingPolicy` → daemon stores policy in KV → links to CloudWatch alarms for metric-based triggers → return PolicyARN | 1. Create target tracking policy<br>2. Create step scaling policy<br>3. Non-existent ASG (ValidationError) | **NOT STARTED** |
|
||||
| `delete-scaling-policy` | — | `--auto-scaling-group-name`, `--policy-name` | Policy must exist | NATS `autoscaling.DeleteScalingPolicy` → daemon deletes policy → remove CloudWatch alarm links → return success | 1. Delete existing policy<br>2. Non-existent policy | **NOT STARTED** |
|
||||
|
||||
+26
-26
@@ -1,6 +1,6 @@
|
||||
# Hive Architecture
|
||||
# Spinifex Architecture
|
||||
|
||||
Hive is an AWS-compatible infrastructure platform for bare-metal, edge, and on-premise deployments. It provides EC2-compatible VM orchestration backed by QEMU/KVM, with storage provided by companion projects Viperblock (EBS) and Predastore (S3).
|
||||
Spinifex is an AWS-compatible infrastructure platform for bare-metal, edge, and on-premise deployments. It provides EC2-compatible VM orchestration backed by QEMU/KVM, with storage provided by companion projects Viperblock (EBS) and Predastore (S3).
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
@@ -19,7 +19,7 @@ Hive is an AWS-compatible infrastructure platform for bare-metal, edge, and on-p
|
||||
│ • EC2 Query Protocol parsing • Routes to service handlers │
|
||||
│ • XML response generation • NATS client connection │
|
||||
│ │
|
||||
│ File: hive/services/awsgw/awsgw.go │
|
||||
│ File: spinifex/services/awsgw/awsgw.go │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ JSON over NATS
|
||||
@@ -28,7 +28,7 @@ Hive is an AWS-compatible infrastructure platform for bare-metal, edge, and on-p
|
||||
│ NATS Message Broker │
|
||||
│ │
|
||||
│ Topics: Queue Groups: │
|
||||
│ • ec2.RunInstances • hive-workers (load balanced) │
|
||||
│ • ec2.RunInstances • spinifex-workers (load balanced) │
|
||||
│ • ec2.DescribeInstances • No queue (fan-out to all nodes) │
|
||||
│ • ec2.cmd.{instanceID} (per-instance start/stop/terminate) │
|
||||
│ • ebs.mount / ebs.unmount │
|
||||
@@ -37,7 +37,7 @@ Hive is an AWS-compatible infrastructure platform for bare-metal, edge, and on-p
|
||||
┌─────────────────┼─────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ Hive Daemon │ │ Hive Daemon │ │ Hive Daemon │
|
||||
│ Spinifex Daemon │ │ Spinifex Daemon │ │ Spinifex Daemon │
|
||||
│ (Node 1) │ │ (Node 2) │ │ (Node N) │
|
||||
│ │ │ │ │ │
|
||||
│ • NATS subscriber │ │ • NATS subscriber │ │ • NATS subscriber │
|
||||
@@ -58,7 +58,7 @@ Hive is an AWS-compatible infrastructure platform for bare-metal, edge, and on-p
|
||||
|
||||
### 1. AWS SDK Request
|
||||
|
||||
Users interact with Hive using standard AWS SDKs or the AWS CLI by pointing to a custom endpoint:
|
||||
Users interact with Spinifex using standard AWS SDKs or the AWS CLI by pointing to a custom endpoint:
|
||||
|
||||
```bash
|
||||
aws --endpoint-url https://localhost:9999 --no-verify-ssl ec2 run-instances \
|
||||
@@ -73,7 +73,7 @@ The AWS SDK formats this as an HTTP POST with:
|
||||
|
||||
### 2. AWS Gateway
|
||||
|
||||
The gateway (`hive/services/awsgw/awsgw.go:57-107`) is the entry point:
|
||||
The gateway (`spinifex/services/awsgw/awsgw.go:57-107`) is the entry point:
|
||||
|
||||
```go
|
||||
// awsgw.go:74 - Connect to NATS
|
||||
@@ -89,7 +89,7 @@ gw := gateway.GatewayConfig{
|
||||
log.Fatal(app.ListenTLS(nodeConfig.AWSGW.Host, nodeConfig.AWSGW.TLSCert, nodeConfig.AWSGW.TLSKey))
|
||||
```
|
||||
|
||||
Request routing (`hive/gateway/gateway.go:125-156`):
|
||||
Request routing (`spinifex/gateway/gateway.go:125-156`):
|
||||
|
||||
1. **Authentication**: SigV4 middleware validates AWS credentials (`gateway.go:104`)
|
||||
2. **Service Detection**: Extracts service name (ec2, iam, account) from Authorization header (`gateway.go:158-186`)
|
||||
@@ -109,7 +109,7 @@ case "iam":
|
||||
|
||||
### 3. EC2 Handler
|
||||
|
||||
The EC2 handler (`hive/gateway/ec2.go`) parses the Action parameter and delegates to specific handlers:
|
||||
The EC2 handler (`spinifex/gateway/ec2.go`) parses the Action parameter and delegates to specific handlers:
|
||||
|
||||
```go
|
||||
// ec2.go - Action routing
|
||||
@@ -124,7 +124,7 @@ case "DescribeInstances":
|
||||
|
||||
### 4. NATS Messaging
|
||||
|
||||
The gateway communicates with daemons via NATS request/response (`hive/handlers/ec2/instance/service_nats.go:24-57`):
|
||||
The gateway communicates with daemons via NATS request/response (`spinifex/handlers/ec2/instance/service_nats.go:24-57`):
|
||||
|
||||
```go
|
||||
// service_nats.go:24-57
|
||||
@@ -145,13 +145,13 @@ func (s *NATSInstanceService) RunInstances(input *ec2.RunInstancesInput) (*ec2.R
|
||||
|
||||
### 5. Daemon Processing
|
||||
|
||||
Daemons (`hive/daemon/daemon.go`) subscribe to NATS topics and handle requests:
|
||||
Daemons (`spinifex/daemon/daemon.go`) subscribe to NATS topics and handle requests:
|
||||
|
||||
```go
|
||||
// daemon.go:319 - Subscribe with queue group for load balancing
|
||||
d.natsSubscriptions["ec2.RunInstances"], err = d.natsConn.QueueSubscribe(
|
||||
"ec2.RunInstances",
|
||||
"hive-workers", // Queue group - only one daemon handles each request
|
||||
"spinifex-workers", // Queue group - only one daemon handles each request
|
||||
d.handleEC2RunInstances,
|
||||
)
|
||||
|
||||
@@ -162,7 +162,7 @@ d.natsSubscriptions["ec2.DescribeInstances"], err = d.natsConn.Subscribe(
|
||||
)
|
||||
```
|
||||
|
||||
**Queue Groups**: Topics subscribed with a queue group (`hive-workers`) are load-balanced - only one daemon handles each request. Topics without a queue group fan out to all daemons.
|
||||
**Queue Groups**: Topics subscribed with a queue group (`spinifex-workers`) are load-balanced - only one daemon handles each request. Topics without a queue group fan out to all daemons.
|
||||
|
||||
### 6. VM Launch
|
||||
|
||||
@@ -225,19 +225,19 @@ type ResourceManager struct {
|
||||
|
||||
| Topic | Queue Group | Purpose |
|
||||
|-------|-------------|---------|
|
||||
| `ec2.RunInstances` | `hive-workers` | Launch new instances |
|
||||
| `ec2.RunInstances` | `spinifex-workers` | Launch new instances |
|
||||
| `ec2.DescribeInstances` | None (fan-out) | Query all nodes for instances |
|
||||
| `ec2.cmd.{instanceID}` | None (owner only) | Per-instance commands (start/stop/terminate, attach/detach volume) |
|
||||
| `ec2.CreateKeyPair` | `hive-workers` | Generate SSH keypair |
|
||||
| `ec2.DescribeKeyPairs` | `hive-workers` | List SSH keypairs |
|
||||
| `ec2.DescribeImages` | `hive-workers` | List AMIs |
|
||||
| `ec2.CreateKeyPair` | `spinifex-workers` | Generate SSH keypair |
|
||||
| `ec2.DescribeKeyPairs` | `spinifex-workers` | List SSH keypairs |
|
||||
| `ec2.DescribeImages` | `spinifex-workers` | List AMIs |
|
||||
| `ebs.mount` | - | Mount volume via Viperblock |
|
||||
| `ebs.unmount` | - | Unmount volume |
|
||||
| `hive.admin.{node}.health` | None | Node health checks |
|
||||
| `spinifex.admin.{node}.health` | None | Node health checks |
|
||||
|
||||
### Multi-Node Aggregation
|
||||
|
||||
For operations that need data from all nodes (like `DescribeInstances`), the gateway uses inbox-based fan-out (`hive/gateway/ec2/instance/DescribeInstances.go`):
|
||||
For operations that need data from all nodes (like `DescribeInstances`), the gateway uses inbox-based fan-out (`spinifex/gateway/ec2/instance/DescribeInstances.go`):
|
||||
|
||||
```go
|
||||
// DescribeInstances.go:16-111
|
||||
@@ -288,13 +288,13 @@ Object storage used for:
|
||||
|
||||
## Configuration
|
||||
|
||||
Cluster configuration (`hive/config/config.go`):
|
||||
Cluster configuration (`spinifex/config/config.go`):
|
||||
|
||||
```go
|
||||
type ClusterConfig struct {
|
||||
Epoch uint64 // Incremented on config changes
|
||||
Node string // This node's identifier
|
||||
Version string // Hive version
|
||||
Version string // Spinifex version
|
||||
Nodes map[string]Config // Configuration for all nodes
|
||||
}
|
||||
|
||||
@@ -312,8 +312,8 @@ type Config struct {
|
||||
## File Structure
|
||||
|
||||
```
|
||||
hive/
|
||||
├── hive/
|
||||
spinifex/
|
||||
├── spinifex/
|
||||
│ ├── services/
|
||||
│ │ └── awsgw/
|
||||
│ │ └── awsgw.go # AWS Gateway service entry point
|
||||
@@ -340,7 +340,7 @@ hive/
|
||||
│ └── qmp/
|
||||
│ └── qmp.go # QEMU Machine Protocol client
|
||||
└── cmd/
|
||||
└── hive/
|
||||
└── spinifex/
|
||||
└── main.go # CLI entry point
|
||||
```
|
||||
|
||||
@@ -351,8 +351,8 @@ hive/
|
||||
1. Start NATS broker
|
||||
2. Start Predastore (S3)
|
||||
3. Start Viperblock (EBS)
|
||||
4. Start Hive Gateway (`hive awsgw`)
|
||||
5. Start Hive Daemon(s) (`hive daemon`)
|
||||
4. Start Spinifex Gateway (`spx awsgw`)
|
||||
5. Start Spinifex Daemon(s) (`spx daemon`)
|
||||
|
||||
### Testing with AWS CLI
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
# Spinifex Documentation
|
||||
|
||||
This directory contains the Spinifex documentation. This is available in individual markdown files, and a generated website to aid navigation and browing documentation at: [https://docs.mulgadc.com](https://docs.mulgadc.com/)
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: "Creating Accounts"
|
||||
description: "Create and manage isolated user accounts with their own resources."
|
||||
category: "Administration"
|
||||
tags:
|
||||
- accounts
|
||||
- iam
|
||||
- admin
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
---
|
||||
|
||||
# Creating Accounts
|
||||
|
||||
> Create and manage isolated user accounts with their own resources.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Spinifex supports multi-tenant account isolation. Each account gets its own IAM credentials, AWS CLI profile, and isolated resource namespace.
|
||||
|
||||
## Instructions
|
||||
|
||||
## Create Account
|
||||
|
||||
```bash
|
||||
spx admin account create --name myteam
|
||||
export AWS_PROFILE=spinifex-myteam
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
## Credentials Not Working
|
||||
|
||||
Verify the AWS CLI configuration files exist and contain the correct profile:
|
||||
|
||||
```bash
|
||||
cat ~/.aws/config
|
||||
cat ~/.aws/credentials
|
||||
```
|
||||
|
||||
Ensure the `AWS_PROFILE` environment variable matches the profile name:
|
||||
|
||||
```bash
|
||||
echo $AWS_PROFILE
|
||||
export AWS_PROFILE=spinifex-myteam
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: "Hybrid Sync"
|
||||
description: "Synchronize data between Spinifex and AWS when network connectivity is available."
|
||||
category: "Migration"
|
||||
tags:
|
||||
- hybrid
|
||||
- sync
|
||||
- s3
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
---
|
||||
|
||||
# Hybrid Sync
|
||||
|
||||
> Synchronize data between Spinifex and AWS when network connectivity is available.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Spinifex's hybrid mode enables bidirectional data synchronization between local infrastructure and AWS cloud services. Ideal for intermittent connectivity environments.
|
||||
|
||||
## Instructions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure the AWS profile is set:
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
```
|
||||
|
||||
## Push Local to Cloud
|
||||
|
||||
```bash
|
||||
aws s3 sync s3://local-bucket/ s3://cloud-bucket/ \
|
||||
--source-region spinifex --region us-east-1
|
||||
```
|
||||
|
||||
## Pull Cloud to Local
|
||||
|
||||
```bash
|
||||
aws s3 sync s3://cloud-bucket/ s3://local-bucket/ \
|
||||
--source-region us-east-1 --region spinifex
|
||||
```
|
||||
|
||||
## EBS Volume Backup
|
||||
|
||||
```bash
|
||||
rsync -avz /data/ user@cloud-server:/backup/spinifex-data/
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
## Sync Fails Mid Transfer
|
||||
|
||||
S3 sync is idempotent — re-run the same command to resume where it left off:
|
||||
|
||||
```bash
|
||||
aws s3 sync s3://local-bucket/ s3://cloud-bucket/ \
|
||||
--source-region spinifex --region us-east-1
|
||||
```
|
||||
|
||||
Only changed or missing files will be transferred on subsequent runs.
|
||||
@@ -0,0 +1,538 @@
|
||||
---
|
||||
title: "IAM Users and Policies"
|
||||
description: "Create IAM users, manage access keys, and control permissions with policies."
|
||||
category: "Administration"
|
||||
tags:
|
||||
- iam
|
||||
- users
|
||||
- policies
|
||||
- access keys
|
||||
- security
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "AWS IAM CLI Reference"
|
||||
url: "https://docs.aws.amazon.com/cli/latest/reference/iam/"
|
||||
- title: "IAM Policy Reference"
|
||||
url: "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html"
|
||||
---
|
||||
|
||||
# IAM Users and Policies
|
||||
|
||||
> Create IAM users, manage access keys, and control permissions with policies.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Users](#users)
|
||||
- [Access Keys](#access-keys)
|
||||
- [Policies](#policies)
|
||||
- [Attaching Policies](#attaching-policies)
|
||||
- [Putting It All Together](#putting-it-all-together)
|
||||
- [Command Reference](#command-reference)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Spinifex implements AWS-compatible IAM with 16 operations covering user management, access key lifecycle, policy CRUD, and policy attachment. All IAM resources are scoped to the account that creates them — users in one account cannot see or modify resources in another.
|
||||
|
||||
When you create an account with `spx admin account create`, Spinifex bootstraps a root user with an `AdministratorAccess` policy and writes the credentials to `~/.aws/credentials`. From there, you use the standard AWS CLI to manage additional users and permissions.
|
||||
|
||||
**How authentication works:** Every AWS CLI request is signed with SigV4 using an access key pair. The gateway verifies the signature, resolves the caller's account, and evaluates attached policies before routing the request. The root user (account `000000000000`) bypasses policy evaluation entirely.
|
||||
|
||||
## Instructions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running Spinifex cluster with the AWS gateway started
|
||||
- An account created via `spx admin account create` (see [Creating Accounts](/docs/creating-accounts))
|
||||
- AWS CLI configured with the account's profile:
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex-myteam
|
||||
```
|
||||
|
||||
## Users
|
||||
|
||||
### Create a User
|
||||
|
||||
```bash
|
||||
aws iam create-user --user-name alice
|
||||
```
|
||||
|
||||
The response includes the user's ARN, unique ID, and creation date:
|
||||
|
||||
```json
|
||||
{
|
||||
"User": {
|
||||
"UserName": "alice",
|
||||
"UserId": "AIDA1A2B3C4D5E6F7890",
|
||||
"Arn": "arn:aws:iam::000000000001:user/alice",
|
||||
"Path": "/",
|
||||
"CreateDate": "2026-03-24T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `--path` to organise users into hierarchical groups:
|
||||
|
||||
```bash
|
||||
aws iam create-user --user-name bob --path /developers/
|
||||
```
|
||||
|
||||
### Get a User
|
||||
|
||||
```bash
|
||||
aws iam get-user --user-name alice
|
||||
```
|
||||
|
||||
### List Users
|
||||
|
||||
```bash
|
||||
aws iam list-users
|
||||
```
|
||||
|
||||
Filter by path prefix:
|
||||
|
||||
```bash
|
||||
aws iam list-users --path-prefix /developers/
|
||||
```
|
||||
|
||||
### Delete a User
|
||||
|
||||
Before deleting a user, remove all access keys and detach all policies:
|
||||
|
||||
```bash
|
||||
# Remove access keys
|
||||
aws iam list-access-keys --user-name alice
|
||||
aws iam delete-access-key --user-name alice --access-key-id AKIA...
|
||||
|
||||
# Detach policies
|
||||
aws iam list-attached-user-policies --user-name alice
|
||||
aws iam detach-user-policy --user-name alice \
|
||||
--policy-arn arn:aws:iam::000000000001:policy/MyPolicy
|
||||
|
||||
# Now delete
|
||||
aws iam delete-user --user-name alice
|
||||
```
|
||||
|
||||
## Access Keys
|
||||
|
||||
Access keys are how users authenticate with the AWS CLI. Each user can have up to **2 access keys** at a time, allowing key rotation without downtime.
|
||||
|
||||
### Create an Access Key
|
||||
|
||||
```bash
|
||||
aws iam create-access-key --user-name alice
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"AccessKey": {
|
||||
"UserName": "alice",
|
||||
"AccessKeyId": "AKIA1A2B3C4D5E6F7890ABCD",
|
||||
"Status": "Active",
|
||||
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
"CreateDate": "2026-03-24T10:05:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **The secret access key is only shown once.** Save it immediately. If lost, delete the key and create a new one.
|
||||
|
||||
Add the credentials to `~/.aws/credentials` under a new profile and set `AWS_PROFILE` to use them.
|
||||
|
||||
### List Access Keys
|
||||
|
||||
```bash
|
||||
aws iam list-access-keys --user-name alice
|
||||
```
|
||||
|
||||
### Rotate Access Keys
|
||||
|
||||
Create a second key, update your configuration, then delete the old one:
|
||||
|
||||
```bash
|
||||
# 1. Create new key (while old key still works)
|
||||
aws iam create-access-key --user-name alice
|
||||
|
||||
# 2. Update ~/.aws/credentials with the new key
|
||||
|
||||
# 3. Verify the new key works
|
||||
AWS_PROFILE=spinifex-alice aws sts get-caller-identity
|
||||
|
||||
# 4. Delete the old key
|
||||
aws iam delete-access-key --user-name alice --access-key-id AKIA_OLD_KEY_ID
|
||||
```
|
||||
|
||||
### Deactivate an Access Key
|
||||
|
||||
Temporarily disable a key without deleting it:
|
||||
|
||||
```bash
|
||||
aws iam update-access-key --user-name alice \
|
||||
--access-key-id AKIA1A2B3C4D5E6F7890ABCD \
|
||||
--status Inactive
|
||||
```
|
||||
|
||||
Reactivate it later:
|
||||
|
||||
```bash
|
||||
aws iam update-access-key --user-name alice \
|
||||
--access-key-id AKIA1A2B3C4D5E6F7890ABCD \
|
||||
--status Active
|
||||
```
|
||||
|
||||
## Policies
|
||||
|
||||
Policies are JSON documents that define what actions a user can perform on which resources.
|
||||
|
||||
### Policy Document Format
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["ec2:RunInstances", "ec2:DescribeInstances"],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each statement has:
|
||||
|
||||
- **Effect** — `Allow` or `Deny`
|
||||
- **Action** — Service actions (e.g. `ec2:RunInstances`, `s3:GetObject`). Supports wildcards: `ec2:*`, `s3:Get*`, or `*` for all actions.
|
||||
- **Resource** — Target resources. Use `*` for all resources.
|
||||
|
||||
**Evaluation order:** An explicit `Deny` always wins. If no statement matches, access is denied by default.
|
||||
|
||||
### Create a Policy
|
||||
|
||||
Save the policy document to a file, then create the policy:
|
||||
|
||||
```bash
|
||||
cat > /tmp/ec2-readonly.json << 'EOF'
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:DescribeInstances",
|
||||
"ec2:DescribeImages",
|
||||
"ec2:DescribeKeyPairs",
|
||||
"ec2:DescribeSecurityGroups",
|
||||
"ec2:DescribeSubnets",
|
||||
"ec2:DescribeVpcs"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
aws iam create-policy \
|
||||
--policy-name EC2ReadOnly \
|
||||
--policy-document file:///tmp/ec2-readonly.json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Policy": {
|
||||
"PolicyName": "EC2ReadOnly",
|
||||
"PolicyId": "ANPA1A2B3C4D5E6F7890",
|
||||
"Arn": "arn:aws:iam::000000000001:policy/EC2ReadOnly",
|
||||
"Path": "/",
|
||||
"DefaultVersionId": "v1",
|
||||
"CreateDate": "2026-03-24T10:10:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Common Policy Examples
|
||||
|
||||
**Full administrator access:**
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "*",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**S3 read/write (for hybrid sync, backups):**
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:PutObject",
|
||||
"s3:ListBucket",
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**EC2 operator (launch and manage instances, no VPC changes):**
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:RunInstances",
|
||||
"ec2:StartInstances",
|
||||
"ec2:StopInstances",
|
||||
"ec2:TerminateInstances",
|
||||
"ec2:DescribeInstances",
|
||||
"ec2:DescribeImages"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Deny terminate (attach alongside a broader Allow to prevent accidental termination):**
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Deny",
|
||||
"Action": "ec2:TerminateInstances",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Get a Policy
|
||||
|
||||
```bash
|
||||
aws iam get-policy \
|
||||
--policy-arn arn:aws:iam::000000000001:policy/EC2ReadOnly
|
||||
```
|
||||
|
||||
### Get the Policy Document
|
||||
|
||||
Use `get-policy-version` with version `v1` to retrieve the actual JSON document:
|
||||
|
||||
```bash
|
||||
aws iam get-policy-version \
|
||||
--policy-arn arn:aws:iam::000000000001:policy/EC2ReadOnly \
|
||||
--version-id v1
|
||||
```
|
||||
|
||||
### List Policies
|
||||
|
||||
```bash
|
||||
aws iam list-policies
|
||||
```
|
||||
|
||||
### Delete a Policy
|
||||
|
||||
A policy must be detached from all users before it can be deleted:
|
||||
|
||||
```bash
|
||||
aws iam delete-policy \
|
||||
--policy-arn arn:aws:iam::000000000001:policy/EC2ReadOnly
|
||||
```
|
||||
|
||||
## Attaching Policies
|
||||
|
||||
Policies have no effect until attached to a user.
|
||||
|
||||
### Attach a Policy to a User
|
||||
|
||||
```bash
|
||||
aws iam attach-user-policy --user-name alice \
|
||||
--policy-arn arn:aws:iam::000000000001:policy/EC2ReadOnly
|
||||
```
|
||||
|
||||
### List a User's Policies
|
||||
|
||||
```bash
|
||||
aws iam list-attached-user-policies --user-name alice
|
||||
```
|
||||
|
||||
### Detach a Policy
|
||||
|
||||
```bash
|
||||
aws iam detach-user-policy --user-name alice \
|
||||
--policy-arn arn:aws:iam::000000000001:policy/EC2ReadOnly
|
||||
```
|
||||
|
||||
## Putting It All Together
|
||||
|
||||
A complete workflow for onboarding a developer with scoped EC2 and S3 access:
|
||||
|
||||
```bash
|
||||
# 1. Create the user
|
||||
aws iam create-user --user-name dev-carol --path /developers/
|
||||
|
||||
# 2. Create an access key
|
||||
aws iam create-access-key --user-name dev-carol
|
||||
# Save the AccessKeyId and SecretAccessKey from the output
|
||||
|
||||
# 3. Create a scoped policy
|
||||
cat > /tmp/dev-policy.json << 'EOF'
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "EC2Access",
|
||||
"Effect": "Allow",
|
||||
"Action": ["ec2:*"],
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Sid": "S3ReadOnly",
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:GetObject", "s3:ListBucket"],
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Sid": "NoTerminate",
|
||||
"Effect": "Deny",
|
||||
"Action": "ec2:TerminateInstances",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
aws iam create-policy \
|
||||
--policy-name DeveloperAccess \
|
||||
--policy-document file:///tmp/dev-policy.json
|
||||
|
||||
# 4. Attach the policy
|
||||
aws iam attach-user-policy --user-name dev-carol \
|
||||
--policy-arn arn:aws:iam::000000000001:policy/DeveloperAccess
|
||||
|
||||
# 5. Configure the developer's AWS CLI
|
||||
aws configure --profile spinifex-carol
|
||||
# Enter the access key and secret from step 2
|
||||
|
||||
# 6. Verify
|
||||
AWS_PROFILE=spinifex-carol aws ec2 describe-instances
|
||||
AWS_PROFILE=spinifex-carol aws ec2 terminate-instances --instance-ids i-123
|
||||
# ^ This will be denied by the NoTerminate statement
|
||||
```
|
||||
|
||||
## Command Reference
|
||||
|
||||
| Command | Key Parameters | Notes |
|
||||
| ----------------------------- | -------------------------------------------- | --------------------------------- |
|
||||
| `create-user` | `--user-name`, `--path` | Path default: `/` |
|
||||
| `get-user` | `--user-name` | |
|
||||
| `list-users` | `--path-prefix` | |
|
||||
| `delete-user` | `--user-name` | Must have no keys or policies |
|
||||
| `create-access-key` | `--user-name` | Max 2 per user; secret shown once |
|
||||
| `list-access-keys` | `--user-name` | |
|
||||
| `delete-access-key` | `--user-name`, `--access-key-id` | |
|
||||
| `update-access-key` | `--user-name`, `--access-key-id`, `--status` | `Active` or `Inactive` |
|
||||
| `create-policy` | `--policy-name`, `--policy-document` | Max 6144 bytes |
|
||||
| `get-policy` | `--policy-arn` | Metadata only |
|
||||
| `get-policy-version` | `--policy-arn`, `--version-id` | Use `v1`; includes document |
|
||||
| `list-policies` | | |
|
||||
| `delete-policy` | `--policy-arn` | Must not be attached |
|
||||
| `attach-user-policy` | `--user-name`, `--policy-arn` | Idempotent |
|
||||
| `detach-user-policy` | `--user-name`, `--policy-arn` | |
|
||||
| `list-attached-user-policies` | `--user-name` | |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### AccessDenied on IAM Commands
|
||||
|
||||
The calling user must have IAM permissions. Attach a policy with `iam:*` actions, or use the root account profile:
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex-myteam
|
||||
aws iam list-users
|
||||
```
|
||||
|
||||
### InvalidClientTokenId
|
||||
|
||||
The access key is either inactive or does not exist. Check the key status:
|
||||
|
||||
```bash
|
||||
aws iam list-access-keys --user-name alice
|
||||
```
|
||||
|
||||
If the key shows `Inactive`, reactivate it:
|
||||
|
||||
```bash
|
||||
aws iam update-access-key --user-name alice \
|
||||
--access-key-id AKIA... --status Active
|
||||
```
|
||||
|
||||
If the key was deleted, create a new one.
|
||||
|
||||
### DeleteConflict When Deleting a User
|
||||
|
||||
The user still has access keys or attached policies. Remove them first:
|
||||
|
||||
```bash
|
||||
# Check for access keys
|
||||
aws iam list-access-keys --user-name alice
|
||||
|
||||
# Check for attached policies
|
||||
aws iam list-attached-user-policies --user-name alice
|
||||
```
|
||||
|
||||
### DeleteConflict When Deleting a Policy
|
||||
|
||||
The policy is still attached to one or more users. Detach it from all users before deleting:
|
||||
|
||||
```bash
|
||||
# Find who has it attached, then detach
|
||||
aws iam detach-user-policy --user-name alice \
|
||||
--policy-arn arn:aws:iam::000000000001:policy/MyPolicy
|
||||
|
||||
aws iam delete-policy \
|
||||
--policy-arn arn:aws:iam::000000000001:policy/MyPolicy
|
||||
```
|
||||
|
||||
### LimitExceeded When Creating Access Keys
|
||||
|
||||
Each user can have at most 2 access keys. Delete an existing key before creating a new one:
|
||||
|
||||
```bash
|
||||
aws iam list-access-keys --user-name alice
|
||||
aws iam delete-access-key --user-name alice --access-key-id AKIA_OLD
|
||||
aws iam create-access-key --user-name alice
|
||||
```
|
||||
|
||||
### MalformedPolicyDocument
|
||||
|
||||
The policy JSON is invalid. Check that:
|
||||
|
||||
- `Version` is exactly `"2012-10-17"`
|
||||
- At least one `Statement` exists
|
||||
- Each statement has `Effect` (`Allow`/`Deny`), `Action`, and `Resource`
|
||||
- The document is under 6144 bytes
|
||||
- The JSON is well-formed (no trailing commas, correct quoting)
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
title: "Air-Gapped Install"
|
||||
description: "Deploy Spinifex in environments without internet connectivity. Covers offline package caching, USB deployment, and package verification."
|
||||
category: "Getting Started"
|
||||
tags:
|
||||
- install
|
||||
- air-gapped
|
||||
- offline
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "apt-cacher-ng"
|
||||
url: "https://wiki.debian.org/AptCacherNg"
|
||||
---
|
||||
|
||||
# Air-Gapped Installation
|
||||
|
||||
> Deploy Spinifex in environments without internet connectivity.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
In air-gapped or disconnected environments, Spinifex can be deployed without internet access. This guide covers preparing offline packages on a connected machine, creating USB deployment media, and installing on the target server with package verification.
|
||||
|
||||
**Deployment methods:**
|
||||
- **apt-cacher-ng** — Local APT cache for Debian/Ubuntu packages
|
||||
- **Go module cache** — Pre-downloaded Go dependencies
|
||||
- **USB deployment** — Portable installation media with all dependencies
|
||||
- **GPG verification** — Cryptographic package integrity checks
|
||||
|
||||
## Instructions
|
||||
|
||||
## Step 1. Prepare Packages (on a connected machine)
|
||||
|
||||
Set up a local APT cache to collect all required packages:
|
||||
|
||||
```bash
|
||||
sudo apt install apt-cacher-ng
|
||||
sudo systemctl enable --now apt-cacher-ng
|
||||
```
|
||||
|
||||
Pre-download all Spinifex dependencies:
|
||||
|
||||
```bash
|
||||
sudo apt install --download-only 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 \
|
||||
ovn-central ovn-host openvswitch-switch
|
||||
```
|
||||
|
||||
## Step 2. Create USB Deployment Media
|
||||
|
||||
```bash
|
||||
mkdir -p /media/spinifex-deploy/{apt-packages,go-cache,spinifex-source}
|
||||
cp /var/cache/apt/archives/*.deb /media/spinifex-deploy/apt-packages/
|
||||
cp -r ~/go/pkg/mod/cache/ /media/spinifex-deploy/go-cache/
|
||||
cp -r ~/Development/mulga/spinifex /media/spinifex-deploy/spinifex-source/
|
||||
```
|
||||
|
||||
## Step 3. Verify Package Integrity
|
||||
|
||||
Sign and verify packages before transferring to the target:
|
||||
|
||||
```bash
|
||||
gpg --import /media/spinifex-deploy/mulga-signing-key.asc
|
||||
gpg --verify spinifex-v1.0.tar.gz.sig spinifex-v1.0.tar.gz
|
||||
sha256sum -c /media/spinifex-deploy/checksums.sha256
|
||||
```
|
||||
|
||||
## Step 4. Install on the Target Server
|
||||
|
||||
Mount the USB and install:
|
||||
|
||||
```bash
|
||||
sudo mount /dev/sdb1 /mnt/usb
|
||||
sudo dpkg -i /mnt/usb/apt-packages/*.deb
|
||||
cp -r /mnt/usb/spinifex-source ~/Development/mulga/spinifex
|
||||
cd ~/Development/mulga/spinifex && make build
|
||||
```
|
||||
|
||||
Then follow the [Source Install](/docs/install-source) guide from Step 3 (Setup OVN) onwards.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Missing Dependencies After dpkg Install
|
||||
|
||||
Some packages may have unresolved dependencies. Fix with:
|
||||
|
||||
```bash
|
||||
sudo dpkg -i /mnt/usb/apt-packages/*.deb
|
||||
sudo apt-get install -f
|
||||
```
|
||||
|
||||
The `-f` flag tells apt to fix broken dependencies using what's available locally.
|
||||
|
||||
### GPG Verification Fails
|
||||
|
||||
The signing key may not match or the download was corrupted. Re-import the key and verify:
|
||||
|
||||
```bash
|
||||
gpg --import /media/spinifex-deploy/mulga-signing-key.asc
|
||||
gpg --verify spinifex-v1.0.tar.gz.sig spinifex-v1.0.tar.gz
|
||||
```
|
||||
|
||||
If verification still fails, re-download the package on the connected machine and compare checksums:
|
||||
|
||||
```bash
|
||||
sha256sum spinifex-v1.0.tar.gz
|
||||
```
|
||||
|
||||
### Go Module Cache Errors
|
||||
|
||||
The Go module cache may have been incompletely copied. On the connected machine, re-export the full cache:
|
||||
|
||||
```bash
|
||||
cp -r ~/go/pkg/mod/cache/ /media/spinifex-deploy/go-cache/
|
||||
```
|
||||
|
||||
On the target, restore it:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/go/pkg/mod/
|
||||
cp -r /mnt/usb/go-cache ~/go/pkg/mod/cache
|
||||
```
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
title: "Multi-Node Install"
|
||||
description: "Deploy Spinifex across multiple servers to create an availability zone with high availability, data durability, and fault tolerance."
|
||||
category: "Getting Started"
|
||||
tags:
|
||||
- install
|
||||
- multi node
|
||||
- cluster
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "Predastore (S3)"
|
||||
url: "https://github.com/mulgadc/predastore"
|
||||
- title: "Viperblock (EBS)"
|
||||
url: "https://github.com/mulgadc/viperblock"
|
||||
---
|
||||
|
||||
# Multi-Node Installation
|
||||
|
||||
> Deploy Spinifex across multiple servers to create an availability zone.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
A Spinifex cluster distributes services across multiple servers for high availability, data durability, and fault tolerance. Cluster formation is automatic — the init node waits for peers to join, then distributes credentials, CA certificates, and configuration.
|
||||
|
||||
**Network requirements:**
|
||||
|
||||
- Minimum 1 NIC per server (2 recommended for production)
|
||||
- UDP port 6081 open between hosts (Geneve tunnels)
|
||||
- TCP ports 4222, 4248, 6641, 6642 open between hosts (NATS, OVN)
|
||||
|
||||
## Instructions
|
||||
|
||||
## Step 1. Install Spinifex on Each Server
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.mulgadc.com | bash
|
||||
```
|
||||
|
||||
## Step 2. Set Node IP Variables
|
||||
|
||||
On **each server**, export the management IPs for all nodes:
|
||||
|
||||
```bash
|
||||
export SPINIFEX_NODE1=192.168.1.10
|
||||
export SPINIFEX_NODE2=192.168.1.11
|
||||
export SPINIFEX_NODE3=192.168.1.12
|
||||
export AWS_REGION=us-east-1
|
||||
export AWS_AZ=us-east-1a
|
||||
```
|
||||
|
||||
## Step 3. Setup OVN Networking
|
||||
|
||||
Server 1 runs OVN central and must be set up first. If your WAN interface is already a bridge (e.g. `br-wan`), setup-ovn.sh auto-detects it. Otherwise use `--wan-bridge=br-wan --wan-iface=eth1` (dedicated WAN NIC) or `--macvlan --wan-iface=eth0` (single-NIC).
|
||||
|
||||
**Server 1:**
|
||||
|
||||
```bash
|
||||
sudo /usr/local/share/spinifex/setup-ovn.sh \
|
||||
--management \
|
||||
--encap-ip=$SPINIFEX_NODE1
|
||||
```
|
||||
|
||||
**Server 2** (after server 1 is ready):
|
||||
|
||||
```bash
|
||||
sudo /usr/local/share/spinifex/setup-ovn.sh \
|
||||
--ovn-remote=tcp:$SPINIFEX_NODE1:6642 \
|
||||
--encap-ip=$SPINIFEX_NODE2
|
||||
```
|
||||
|
||||
**Server 3** (after server 1 is ready):
|
||||
|
||||
```bash
|
||||
sudo /usr/local/share/spinifex/setup-ovn.sh \
|
||||
--ovn-remote=tcp:$SPINIFEX_NODE1:6642 \
|
||||
--encap-ip=$SPINIFEX_NODE3
|
||||
```
|
||||
|
||||
Verify all 3 chassis registered:
|
||||
|
||||
```bash
|
||||
sudo ovn-sbctl show
|
||||
```
|
||||
|
||||
## Step 4. Form the Cluster
|
||||
|
||||
Run init and join concurrently — init blocks until all nodes join.
|
||||
|
||||
**Server 1 — Initialize:**
|
||||
|
||||
```bash
|
||||
sudo spx admin init \
|
||||
--node node1 --nodes 3 \
|
||||
--bind $SPINIFEX_NODE1 --cluster-bind $SPINIFEX_NODE1 \
|
||||
--port 4432 --region $AWS_REGION --az $AWS_AZ
|
||||
```
|
||||
|
||||
Save the admin credentials — they will not be shown again.
|
||||
|
||||
**Server 2 — Join** (while init is running):
|
||||
|
||||
```bash
|
||||
sudo spx admin join \
|
||||
--node node2 --bind $SPINIFEX_NODE2 --cluster-bind $SPINIFEX_NODE2 \
|
||||
--host $SPINIFEX_NODE1:4432 --region $AWS_REGION --az $AWS_AZ
|
||||
```
|
||||
|
||||
**Server 3 — Join** (while init is running):
|
||||
|
||||
```bash
|
||||
sudo spx admin join \
|
||||
--node node3 --bind $SPINIFEX_NODE3 --cluster-bind $SPINIFEX_NODE3 \
|
||||
--host $SPINIFEX_NODE1:4432 --region $AWS_REGION --az $AWS_AZ
|
||||
```
|
||||
|
||||
## Step 5. Start Services
|
||||
|
||||
On **all servers**:
|
||||
|
||||
```bash
|
||||
sudo systemctl start spinifex.target
|
||||
```
|
||||
|
||||
## Step 6. Verify
|
||||
|
||||
From any node:
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
aws ec2 describe-instance-types
|
||||
```
|
||||
|
||||
If this returns a list of available instance types, your cluster is working.
|
||||
|
||||
**Congratulations! Your Spinifex cluster is installed.**
|
||||
|
||||
Continue to [Setting Up Your Cluster](/docs/setting-up-your-cluster) to import an AMI, create a VPC, and launch your first instance.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Nodes Not Joining
|
||||
|
||||
The init command must still be running when join executes. If init exited, re-run with `--force`.
|
||||
|
||||
```bash
|
||||
curl -s http://$SPINIFEX_NODE1:4432/health
|
||||
```
|
||||
|
||||
### OVN Chassis Not Registering
|
||||
|
||||
```bash
|
||||
sudo ovn-sbctl show
|
||||
sudo ss -tlnp | grep 6642
|
||||
```
|
||||
|
||||
### CA Certificate Not Trusted
|
||||
|
||||
```bash
|
||||
sudo cp ~/spinifex/config/ca.pem /usr/local/share/ca-certificates/spinifex-ca.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
|
||||
### Cross-Host VMs Cannot Communicate
|
||||
|
||||
```bash
|
||||
sudo ovs-vsctl show | grep -i geneve
|
||||
sudo ss -ulnp | grep 6081
|
||||
```
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: "Source Install"
|
||||
description: "Build Spinifex from source for development, custom builds, or contributing."
|
||||
category: "Getting Started"
|
||||
tags:
|
||||
- install
|
||||
- source
|
||||
- development
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "Go Downloads"
|
||||
url: "https://go.dev/dl/"
|
||||
---
|
||||
|
||||
# Source Installation
|
||||
|
||||
> Build Spinifex from source for development, custom builds, or contributing.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This guide builds Spinifex from source. For production deployments, the [binary installer](/docs/install) is recommended.
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Ubuntu 22.04+ / Debian 12+
|
||||
- Go 1.26.1+
|
||||
- GCC, make, pkg-config
|
||||
- QEMU/KVM, OVN/Open vSwitch, AWS CLI v2, dhcpcd-base
|
||||
|
||||
## Instructions
|
||||
|
||||
## Step 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
mkdir -p ~/Development/mulga && cd ~/Development/mulga
|
||||
git clone https://github.com/mulgadc/spinifex.git
|
||||
sudo make -C spinifex quickinstall
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
```
|
||||
|
||||
## Step 2. Clone and Build
|
||||
|
||||
```bash
|
||||
cd spinifex
|
||||
./scripts/clone-deps.sh
|
||||
./scripts/dev-setup.sh
|
||||
```
|
||||
|
||||
Confirm `./bin/spx` exists.
|
||||
|
||||
## Step 3. Setup OVN
|
||||
|
||||
If your WAN interface is already a bridge (e.g. `br-wan`), setup-ovn.sh auto-detects it:
|
||||
|
||||
```bash
|
||||
./scripts/setup-ovn.sh --management
|
||||
```
|
||||
|
||||
If your WAN is a physical NIC, choose one:
|
||||
|
||||
```bash
|
||||
# Dedicated WAN NIC (not your SSH connection):
|
||||
./scripts/setup-ovn.sh --management --wan-bridge=br-wan --wan-iface=eth1
|
||||
|
||||
# Single-NIC host (SSH-safe macvlan):
|
||||
./scripts/setup-ovn.sh --management --macvlan --wan-iface=enp0s3
|
||||
```
|
||||
|
||||
## Step 4. Initialize
|
||||
|
||||
```bash
|
||||
sudo ./bin/spx admin init --node node1 --nodes 1
|
||||
```
|
||||
|
||||
Save the admin credentials printed during init.
|
||||
|
||||
## Step 5. Start Services
|
||||
|
||||
```bash
|
||||
./scripts/start-dev.sh
|
||||
```
|
||||
|
||||
## Step 6. Verify
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
aws ec2 describe-instance-types
|
||||
```
|
||||
|
||||
If this returns a list of available instance types, your installation is working.
|
||||
|
||||
**Congratulations! Spinifex is installed from source.**
|
||||
|
||||
Continue to [Setting Up Your Cluster](/docs/setting-up-your-cluster) to import an AMI, create a VPC, and launch your first instance.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Go Not Found in PATH
|
||||
|
||||
```bash
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
```
|
||||
|
||||
### ./bin/spx Missing After Build
|
||||
|
||||
```bash
|
||||
./scripts/dev-setup.sh
|
||||
```
|
||||
|
||||
### CA Certificate Not Trusted
|
||||
|
||||
```bash
|
||||
sudo cp ~/spinifex/config/ca.pem /usr/local/share/ca-certificates/spinifex-ca.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: "Single-Node Install"
|
||||
description: "Install Spinifex on a single server using the binary installer."
|
||||
category: "Getting Started"
|
||||
tags:
|
||||
- install
|
||||
- single node
|
||||
- quickstart
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "Predastore (S3)"
|
||||
url: "https://github.com/mulgadc/predastore"
|
||||
- title: "Viperblock (EBS)"
|
||||
url: "https://github.com/mulgadc/viperblock"
|
||||
---
|
||||
|
||||
# Installing Spinifex
|
||||
|
||||
> Install Spinifex on a single server using the binary installer.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Spinifex is an open-source infrastructure platform that brings core AWS services — EC2, VPC, EBS, and S3 — to bare-metal, edge, and on-prem environments.
|
||||
|
||||
This guide installs Spinifex on a single server using the binary installer. For multi-server clusters, see [Multi-Node Install](/docs/install-multi-node). To build from source, see [Source Install](/docs/install-source).
|
||||
|
||||
**Supported Operating Systems:**
|
||||
|
||||
- Ubuntu 22.04 / 24.04 / 25.10
|
||||
- Debian 12 / 13
|
||||
|
||||
**What Gets Installed:**
|
||||
|
||||
- Spinifex daemon and CLI
|
||||
- QEMU/KVM for virtual machine management
|
||||
- OVN/Open vSwitch for VPC networking
|
||||
- Predastore (S3-compatible object storage)
|
||||
- Viperblock (EBS-compatible block storage)
|
||||
- AWS CLI v2
|
||||
|
||||
## Instructions
|
||||
|
||||
## Step 1. Install Spinifex
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.mulgadc.com | bash
|
||||
```
|
||||
|
||||
The installer downloads the Spinifex binary and bootstraps all dependencies (QEMU, OVN/OVS, AWS CLI).
|
||||
|
||||
## Step 2. Setup OVN Networking
|
||||
|
||||
If your WAN interface is already a bridge (e.g. `br-wan`), setup-ovn.sh auto-detects it:
|
||||
|
||||
```bash
|
||||
sudo /usr/local/share/spinifex/setup-ovn.sh --management
|
||||
```
|
||||
|
||||
If your WAN is a physical NIC, choose one:
|
||||
|
||||
```bash
|
||||
# Dedicated WAN NIC (not your SSH connection):
|
||||
sudo /usr/local/share/spinifex/setup-ovn.sh --management --wan-bridge=br-wan --wan-iface=eth1
|
||||
```
|
||||
|
||||
**Separating VPC tunnel traffic from WAN:** If you want Geneve tunnel traffic (inter-VM east-west for VPC traffic) to use a dedicated interface instead of the WAN IP, add `--encap-ip=<IP>` to specify the tunnel endpoint address:
|
||||
|
||||
```bash
|
||||
sudo /usr/local/share/spinifex/setup-ovn.sh --management --encap-ip=10.0.0.1
|
||||
```
|
||||
|
||||
This is recommended for production and required in multi-node deployments. See [Multi-Node Install](/docs/install-multi-node) for details.
|
||||
|
||||
## Step 3. Initialize
|
||||
|
||||
```bash
|
||||
sudo spx admin init --node node1 --nodes 1
|
||||
```
|
||||
|
||||
This auto-detects your network topology, generates configuration and TLS certificates, installs the CA into the system trust store, and configures AWS CLI credentials. Save the admin credentials printed during init — they will not be shown again.
|
||||
|
||||
## Step 4. Start Services
|
||||
|
||||
```bash
|
||||
sudo systemctl start spinifex.target
|
||||
```
|
||||
|
||||
## Step 5. Verify
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
aws ec2 describe-instance-types
|
||||
```
|
||||
|
||||
If this returns a list of available instance types, your installation is working.
|
||||
|
||||
**Congratulations! Spinifex is installed.**
|
||||
|
||||
Continue to [Setting Up Your Cluster](/docs/setting-up-your-cluster) to import an AMI, create a VPC, and launch your first instance.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### spx Command Not Found
|
||||
|
||||
```bash
|
||||
export PATH=$PATH:/usr/local/bin
|
||||
```
|
||||
|
||||
### CA Certificate Not Trusted
|
||||
|
||||
`sudo spx admin init` installs the CA automatically. If you need to re-install it manually:
|
||||
|
||||
```bash
|
||||
sudo cp /etc/spinifex/ca.pem /usr/local/share/ca-certificates/spinifex-ca.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
|
||||
### OVN Services Not Starting
|
||||
|
||||
```bash
|
||||
sudo systemctl is-active ovn-controller
|
||||
journalctl -u ovn-controller --no-pager -n 20
|
||||
```
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
title: "Launching Instances"
|
||||
description: "Launch, manage, and connect to EC2-compatible virtual machines on Spinifex."
|
||||
category: "Compute"
|
||||
tags:
|
||||
- ec2
|
||||
- instances
|
||||
- vm
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "AWS EC2 CLI"
|
||||
url: "https://docs.aws.amazon.com/cli/latest/reference/ec2/"
|
||||
---
|
||||
|
||||
# Launching Instances
|
||||
|
||||
> Launch, manage, and connect to EC2-compatible virtual machines on Spinifex.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Launch](#launch)
|
||||
- [Manage](#manage)
|
||||
- [Reboot](#reboot)
|
||||
- [Modify Instance Attributes](#modify-instance-attributes)
|
||||
- [Console Output](#console-output)
|
||||
- [Instance Types](#instance-types)
|
||||
- [SSH (Development)](#ssh-development)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Instance Fails to Boot](#instance-fails-to-boot)
|
||||
- [Cannot SSH Into Instance](#cannot-ssh-into-instance)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Spinifex provides EC2-compatible VM management built on QEMU/KVM. Instances support cloud-init, SSH key injection, VPC networking, and standard AWS lifecycle operations.
|
||||
|
||||
**Supported operations:**
|
||||
|
||||
- `run-instances` — Launch new VMs
|
||||
- `describe-instances` — Query state
|
||||
- `stop-instances` / `start-instances` — Lifecycle
|
||||
- `reboot-instances` — In-place restart (QMP reset)
|
||||
- `terminate-instances` — Permanent removal
|
||||
- `modify-instance-attribute` — Change type, user data, or source/dest check
|
||||
- `get-console-output` — Retrieve serial console log
|
||||
- `describe-instance-types` — List available instance types
|
||||
|
||||
## Instructions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure the AWS profile is set:
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
```
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
aws ec2 run-instances \
|
||||
--image-id $SPINIFEX_AMI \
|
||||
--instance-type t3.small \
|
||||
--key-name spinifex-key
|
||||
|
||||
export INSTANCE_ID="i-XXX"
|
||||
```
|
||||
|
||||
## Manage
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instances --instance-ids $INSTANCE_ID
|
||||
aws ec2 stop-instances --instance-ids $INSTANCE_ID
|
||||
aws ec2 start-instances --instance-ids $INSTANCE_ID
|
||||
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
|
||||
aws ec2 reboot-instances --instance-ids $INSTANCE_ID
|
||||
```
|
||||
|
||||
## Modify Instance Attributes
|
||||
|
||||
Change instance type, user data, or source/dest check. Instance type and user data require the instance to be **stopped** first.
|
||||
|
||||
### Change Instance Type
|
||||
|
||||
```bash
|
||||
aws ec2 stop-instances --instance-ids $INSTANCE_ID
|
||||
|
||||
aws ec2 modify-instance-attribute \
|
||||
--instance-id $INSTANCE_ID \
|
||||
--instance-type t3.medium
|
||||
|
||||
aws ec2 start-instances --instance-ids $INSTANCE_ID
|
||||
```
|
||||
|
||||
## Console Output
|
||||
|
||||
Retrieve the serial console log for a running instance. Output is base64-encoded.
|
||||
|
||||
```bash
|
||||
aws ec2 get-console-output --instance-id $INSTANCE_ID
|
||||
```
|
||||
|
||||
Decode the output:
|
||||
|
||||
```bash
|
||||
aws ec2 get-console-output --instance-id $INSTANCE_ID \
|
||||
--query 'Output' --output text | base64 -d
|
||||
```
|
||||
|
||||
## Instance Types
|
||||
|
||||
List instance types available on the current host. The catalog is generated from the host CPU (Intel, AMD, or ARM) and includes burstable (t-family), general purpose (m-family), compute optimised (c-family), and memory optimised (r-family) types.
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instance-types
|
||||
```
|
||||
|
||||
Filter to a specific type:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instance-types --instance-types t3.micro t3.small
|
||||
```
|
||||
|
||||
Show capacity (how many of each type can still be launched):
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instance-types \
|
||||
--filters Name=capacity,Values=true
|
||||
```
|
||||
|
||||
## SSH (Development)
|
||||
|
||||
In development mode, find the QEMU port forward and connect via localhost:
|
||||
|
||||
```bash
|
||||
ps auxw | grep $INSTANCE_ID
|
||||
ssh -i ~/.ssh/spinifex-key ec2-user@127.0.0.1 -p <port>
|
||||
```
|
||||
|
||||
For production SSH via public IPs, see [Setting Up Your Cluster](/docs/setting-up-your-cluster).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Instance Fails to Boot
|
||||
|
||||
Check QEMU logs for the instance and verify the AMI architecture matches your host:
|
||||
|
||||
```bash
|
||||
cat ~/spinifex/logs/spinifex.log
|
||||
aws ec2 describe-images --image-ids $SPINIFEX_AMI
|
||||
```
|
||||
|
||||
If the AMI is for a different architecture (e.g. arm64 on an x86_64 host), import the correct image:
|
||||
|
||||
```bash
|
||||
spx admin images list
|
||||
spx admin images import --name debian-12-x86_64
|
||||
```
|
||||
|
||||
### Cannot SSH Into Instance
|
||||
|
||||
cloud-init needs time to configure the instance after boot. Wait 30-60 seconds and retry.
|
||||
|
||||
Verify the SSH key was specified correctly when launching:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instances --instance-ids $INSTANCE_ID
|
||||
```
|
||||
|
||||
Check the `KeyName` field matches the key you're using to connect.
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: "Moving an AWS Workload to Mulga"
|
||||
description: "Migrate existing AWS workloads to Spinifex using compatible APIs, SDKs, and Terraform."
|
||||
category: "Migration"
|
||||
tags:
|
||||
- migration
|
||||
- aws
|
||||
- terraform
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "AWS CLI Reference"
|
||||
url: "https://docs.aws.amazon.com/cli/latest/reference/"
|
||||
- title: "Terraform AWS Provider"
|
||||
url: "https://registry.terraform.io/providers/hashicorp/aws/latest"
|
||||
---
|
||||
|
||||
# Moving an AWS Workload to Mulga
|
||||
|
||||
> Migrate existing AWS workloads to Spinifex using compatible APIs, SDKs, and Terraform.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Spinifex provides drop-in compatibility with AWS APIs, making it possible to migrate existing workloads with minimal changes.
|
||||
|
||||
**Supported Services:** VPC, EC2, EBS, S3, IAM
|
||||
|
||||
**Compatible Tools:** AWS CLI, AWS SDKs, Terraform, any S3-compatible client
|
||||
|
||||
## Instructions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure the AWS profile is set:
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
```
|
||||
|
||||
## Configure AWS CLI
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instances
|
||||
aws s3 ls
|
||||
```
|
||||
|
||||
## Terraform
|
||||
|
||||
```hcl
|
||||
provider "aws" {
|
||||
region = "ap-southeast-2"
|
||||
access_key = "your-spinifex-access-key"
|
||||
secret_key = "your-spinifex-secret-key"
|
||||
|
||||
endpoints {
|
||||
ec2 = "https://localhost:9999"
|
||||
s3 = "https://localhost:8443"
|
||||
}
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
}
|
||||
```
|
||||
|
||||
## Hybrid S3 Sync
|
||||
|
||||
```bash
|
||||
aws s3 sync s3://local-bucket/ s3://cloud-bucket/ --source-region spinifex --region us-east-1
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
## Terraform Provider Errors
|
||||
|
||||
Ensure all three skip flags are set in your provider configuration:
|
||||
|
||||
```hcl
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
```
|
||||
|
||||
Without these, Terraform will try to validate credentials and metadata against real AWS endpoints.
|
||||
|
||||
## S3 Signature Errors
|
||||
|
||||
Spinifex uses AWS Signature V4. Ensure your AWS CLI is version 2.0 or higher:
|
||||
|
||||
```bash
|
||||
aws --version
|
||||
```
|
||||
|
||||
If using an older version, upgrade:
|
||||
|
||||
```bash
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
||||
unzip awscliv2.zip
|
||||
sudo ./aws/install --update
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: "Placement Groups"
|
||||
description: "Create and manage spread and cluster placement groups for hardware-level instance placement control."
|
||||
category: "Compute"
|
||||
tags:
|
||||
- ec2
|
||||
- placement
|
||||
- spread
|
||||
- cluster
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "AWS Placement Groups"
|
||||
url: "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html"
|
||||
---
|
||||
|
||||
# Placement Groups
|
||||
|
||||
> Create and manage spread and cluster placement groups for hardware-level instance placement control.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Create](#create)
|
||||
- [Launch Into a Placement Group](#launch-into-a-placement-group)
|
||||
- [Describe](#describe)
|
||||
- [Delete](#delete)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [InsufficientInstanceCapacity Error](#insufficientinstancecapacity-error)
|
||||
- [Cannot Delete Placement Group](#cannot-delete-placement-group)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Placement groups control how Spinifex distributes instances across physical hosts. Two strategies are supported:
|
||||
|
||||
- **Spread** — One instance per physical host, maximising fault isolation
|
||||
- **Cluster** — All instances on the same host, minimising latency
|
||||
|
||||
The `partition` strategy is not supported.
|
||||
|
||||
**Supported operations:**
|
||||
|
||||
- `create-placement-group` — Create a new group
|
||||
- `describe-placement-groups` — Query groups with optional filters
|
||||
- `delete-placement-group` — Remove an empty group
|
||||
|
||||
## Instructions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure the AWS profile is set:
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
```
|
||||
|
||||
## Create
|
||||
|
||||
```bash
|
||||
aws ec2 create-placement-group \
|
||||
--group-name my-spread-group \
|
||||
--strategy spread
|
||||
```
|
||||
|
||||
```bash
|
||||
aws ec2 create-placement-group \
|
||||
--group-name my-cluster-group \
|
||||
--strategy cluster
|
||||
```
|
||||
|
||||
## Launch Into a Placement Group
|
||||
|
||||
```bash
|
||||
aws ec2 run-instances \
|
||||
--image-id $SPINIFEX_AMI \
|
||||
--instance-type t3.small \
|
||||
--key-name spinifex-key \
|
||||
--placement GroupName=my-spread-group \
|
||||
--count 3
|
||||
```
|
||||
|
||||
## Describe
|
||||
|
||||
```bash
|
||||
aws ec2 describe-placement-groups
|
||||
aws ec2 describe-placement-groups --group-names my-spread-group
|
||||
aws ec2 describe-placement-groups --group-ids pg-abc123
|
||||
aws ec2 describe-placement-groups \
|
||||
--filters Name=strategy,Values=spread
|
||||
```
|
||||
|
||||
## Delete
|
||||
|
||||
The group must have no running instances before it can be deleted:
|
||||
|
||||
```bash
|
||||
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
|
||||
aws ec2 delete-placement-group --group-name my-spread-group
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### InsufficientInstanceCapacity Error
|
||||
|
||||
Not enough distinct physical hosts for a spread launch. Reduce `--count` or terminate existing instances to free host slots:
|
||||
|
||||
```bash
|
||||
spx admin nodes list
|
||||
```
|
||||
|
||||
### Cannot Delete Placement Group
|
||||
|
||||
The group still has running instances. Terminate them first:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instances \
|
||||
--filters Name=placement-group-name,Values=my-spread-group
|
||||
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
|
||||
```
|
||||
@@ -0,0 +1,336 @@
|
||||
---
|
||||
title: "Setting Up Your Cluster"
|
||||
description: "Import an AMI, create a VPC with a public subnet, and launch your first EC2 instance on Spinifex."
|
||||
category: "Getting Started"
|
||||
tags:
|
||||
- setup
|
||||
- vpc
|
||||
- ec2
|
||||
- quickstart
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "AWS EC2 CLI Reference"
|
||||
url: "https://docs.aws.amazon.com/cli/latest/reference/ec2/"
|
||||
---
|
||||
|
||||
# Setting Up Your Cluster
|
||||
|
||||
> Import an AMI, create a VPC with a public subnet, and launch your first instance.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [1. Import an AMI](#1-import-an-ami)
|
||||
- [2. Create an SSH Key](#2-create-an-ssh-key)
|
||||
- [3. Create a VPC and Public Subnet](#3-create-a-vpc-and-public-subnet)
|
||||
- [4. Launch an Instance](#4-launch-an-instance)
|
||||
- [5. Connect via SSH](#5-connect-via-ssh)
|
||||
- [6. Managing Instances](#6-managing-instances)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This guide assumes Spinifex is already installed and running. If not, follow one of the installation guides first:
|
||||
- [Binary Install (Single Node)](/docs/install)
|
||||
- [Multi-Node Install](/docs/install-multi-node)
|
||||
- [Source Install](/docs/install-source)
|
||||
|
||||
## Instructions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure the AWS profile is set:
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
```
|
||||
|
||||
## 1. Import an AMI
|
||||
|
||||
List available images and import one matching your architecture:
|
||||
|
||||
```bash
|
||||
spx admin images list
|
||||
```
|
||||
|
||||
```
|
||||
NAME | DISTRO | VERSION | ARCH | BOOT
|
||||
debian-12-arm64 | debian | 12 | arm64 | bios
|
||||
debian-12-x86_64 | debian | 12 | x86_64 | bios
|
||||
ubuntu-24.04-arm64 | ubuntu | 24.04 | arm64 | bios
|
||||
ubuntu-24.04-x86_64 | ubuntu | 24.04 | x86_64 | bios
|
||||
```
|
||||
|
||||
Import an image:
|
||||
|
||||
```bash
|
||||
spx admin images import --name ubuntu-24.04-x86_64
|
||||
```
|
||||
|
||||
Or import a local image file:
|
||||
|
||||
```bash
|
||||
spx admin images import --file ~/images/ubuntu-24.04.img --distro ubuntu --version 24.04 --arch x86_64
|
||||
```
|
||||
|
||||
Verify the import and note the AMI ID:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-images
|
||||
```
|
||||
|
||||
## 2. Create an SSH Key
|
||||
|
||||
### Option A: Import an existing key
|
||||
|
||||
```bash
|
||||
aws ec2 import-key-pair \
|
||||
--key-name "spinifex-key" \
|
||||
--public-key-material fileb://~/.ssh/id_rsa.pub
|
||||
```
|
||||
|
||||
### Option B: Create a new key pair
|
||||
|
||||
```bash
|
||||
aws ec2 create-key-pair --key-name spinifex-key \
|
||||
| jq -r '.KeyMaterial | rtrimstr("\n")' > ~/.ssh/spinifex-key
|
||||
|
||||
chmod 600 ~/.ssh/spinifex-key
|
||||
ssh-keygen -y -f ~/.ssh/spinifex-key > ~/.ssh/spinifex-key.pub
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-key-pairs
|
||||
```
|
||||
|
||||
## 3. Create a VPC and Public Subnet
|
||||
|
||||
### Create a VPC
|
||||
|
||||
```bash
|
||||
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.200.0.0/16 \
|
||||
--query 'Vpc.VpcId' --output text)
|
||||
|
||||
echo "VPC: $VPC_ID"
|
||||
```
|
||||
|
||||
### Create an Internet Gateway
|
||||
|
||||
An Internet Gateway enables instances in public subnets to reach the internet and be reachable from the LAN/WAN.
|
||||
|
||||
```bash
|
||||
IGW_ID=$(aws ec2 create-internet-gateway \
|
||||
--query 'InternetGateway.InternetGatewayId' --output text)
|
||||
|
||||
aws ec2 attach-internet-gateway \
|
||||
--internet-gateway-id $IGW_ID \
|
||||
--vpc-id $VPC_ID
|
||||
|
||||
echo "IGW: $IGW_ID (attached to $VPC_ID)"
|
||||
```
|
||||
|
||||
### Create a Public Subnet
|
||||
|
||||
A public subnet auto-assigns a routable IP to each instance, making it directly reachable from your network.
|
||||
|
||||
```bash
|
||||
SUBNET_ID=$(aws ec2 create-subnet \
|
||||
--vpc-id $VPC_ID \
|
||||
--cidr-block 10.200.1.0/24 \
|
||||
--query 'Subnet.SubnetId' --output text)
|
||||
|
||||
# Enable auto-assign public IP
|
||||
aws ec2 modify-subnet-attribute \
|
||||
--subnet-id $SUBNET_ID \
|
||||
--map-public-ip-on-launch
|
||||
|
||||
echo "Subnet: $SUBNET_ID (public)"
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
aws ec2 describe-vpcs --vpc-ids $VPC_ID
|
||||
aws ec2 describe-subnets --subnet-ids $SUBNET_ID
|
||||
```
|
||||
|
||||
## 4. Launch an Instance
|
||||
|
||||
Query available instance types for your hardware:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instance-types
|
||||
```
|
||||
|
||||
Launch an instance in the public subnet:
|
||||
|
||||
```bash
|
||||
AMI_ID=$(aws ec2 describe-images --query 'Images[0].ImageId' --output text)
|
||||
|
||||
INSTANCE_ID=$(aws ec2 run-instances \
|
||||
--image-id $AMI_ID \
|
||||
--instance-type t3.small \
|
||||
--key-name spinifex-key \
|
||||
--subnet-id $SUBNET_ID \
|
||||
--count 1 \
|
||||
--query 'Instances[0].InstanceId' --output text)
|
||||
|
||||
echo "Instance: $INSTANCE_ID"
|
||||
```
|
||||
|
||||
Wait for the instance to reach `running` state:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instances --instance-ids $INSTANCE_ID \
|
||||
--query 'Reservations[0].Instances[0].[State.Name, PrivateIpAddress, PublicIpAddress]' \
|
||||
--output text
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```
|
||||
running 10.200.1.4 192.168.1.155
|
||||
```
|
||||
|
||||
The instance has both a private IP (VPC overlay) and a public IP (from your external pool, routable on your network).
|
||||
|
||||
## 5. Connect via SSH
|
||||
|
||||
SSH directly to the instance's public IP:
|
||||
|
||||
```bash
|
||||
PUBLIC_IP=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID \
|
||||
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
|
||||
|
||||
ssh -i ~/.ssh/spinifex-key ec2-user@$PUBLIC_IP
|
||||
```
|
||||
|
||||
> **Note:** cloud-init takes 30-60 seconds to configure the instance. If SSH is refused, wait and retry.
|
||||
|
||||
Once connected, verify the instance has internet access:
|
||||
|
||||
```bash
|
||||
curl -s http://ifconfig.me
|
||||
```
|
||||
|
||||
This should return the instance's public IP or the gateway's SNAT address.
|
||||
|
||||
## 6. Managing Instances
|
||||
|
||||
### Stop
|
||||
|
||||
```bash
|
||||
aws ec2 stop-instances --instance-ids $INSTANCE_ID
|
||||
```
|
||||
|
||||
### Start
|
||||
|
||||
```bash
|
||||
aws ec2 start-instances --instance-ids $INSTANCE_ID
|
||||
```
|
||||
|
||||
### Terminate
|
||||
|
||||
```bash
|
||||
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
|
||||
```
|
||||
|
||||
### Console Output
|
||||
|
||||
View the instance's serial console log (useful for debugging boot issues):
|
||||
|
||||
```bash
|
||||
aws ec2 get-console-output --instance-id $INSTANCE_ID \
|
||||
--query 'Output' --output text
|
||||
```
|
||||
|
||||
### Multi-Node: Check Instance Placement
|
||||
|
||||
On a multi-node cluster, instances are distributed across nodes:
|
||||
|
||||
```bash
|
||||
spx get vms
|
||||
```
|
||||
|
||||
## Additional Options
|
||||
|
||||
### Private Subnets (No Public IP)
|
||||
|
||||
Create a subnet without `--map-public-ip-on-launch`:
|
||||
|
||||
```bash
|
||||
PRIVATE_SUBNET=$(aws ec2 create-subnet \
|
||||
--vpc-id $VPC_ID \
|
||||
--cidr-block 10.200.2.0/24 \
|
||||
--query 'Subnet.SubnetId' --output text)
|
||||
```
|
||||
|
||||
Instances in private subnets get a private IP only. They can still reach the internet via SNAT through the VPC router, but are not directly reachable from your network.
|
||||
|
||||
### Multiple Accounts
|
||||
|
||||
Create isolated accounts with their own resources:
|
||||
|
||||
```bash
|
||||
spx admin account create --name myteam
|
||||
export AWS_PROFILE=spinifex-myteam
|
||||
```
|
||||
|
||||
### Web UI
|
||||
|
||||
The Spinifex web interface is available at `https://localhost:3000` when services are running.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Instance Stuck in Pending
|
||||
|
||||
```bash
|
||||
cat ~/spinifex/logs/spinifex.log
|
||||
aws ec2 describe-images
|
||||
```
|
||||
|
||||
### SSH Connection Refused
|
||||
|
||||
cloud-init needs 30-60 seconds after boot. Check instance state:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instances --instance-ids $INSTANCE_ID
|
||||
```
|
||||
|
||||
### No Public IP Assigned
|
||||
|
||||
Verify the subnet has `MapPublicIpOnLaunch` enabled:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-subnets --subnet-ids $SUBNET_ID
|
||||
```
|
||||
|
||||
If `MapPublicIpOnLaunch` is false:
|
||||
|
||||
```bash
|
||||
aws ec2 modify-subnet-attribute --subnet-id $SUBNET_ID --map-public-ip-on-launch
|
||||
```
|
||||
|
||||
Also verify an Internet Gateway is attached to the VPC:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-internet-gateways
|
||||
```
|
||||
|
||||
### Instance Has No Internet Access
|
||||
|
||||
Check the VPC router's NAT rules (from the host):
|
||||
|
||||
```bash
|
||||
sudo ovn-nbctl lr-nat-list $(sudo ovn-nbctl lr-list | awk '{print $2}' | head -1)
|
||||
```
|
||||
|
||||
Verify the default route exists:
|
||||
|
||||
```bash
|
||||
sudo ovn-nbctl lr-route-list $(sudo ovn-nbctl lr-list | awk '{print $2}' | head -1)
|
||||
```
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: "Spinifex Admin CLI"
|
||||
description: "Complete reference for the Spinifex administration CLI. Manage accounts, nodes, VMs, and services."
|
||||
category: "Administration"
|
||||
tags:
|
||||
- cli
|
||||
- admin
|
||||
- reference
|
||||
resources:
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
---
|
||||
|
||||
# Spinifex Admin CLI
|
||||
|
||||
> Complete reference for the Spinifex administration CLI.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The `spx` binary is the central administration tool for managing your Spinifex infrastructure. It provides commands for cluster initialization, account management, node operations, VM lifecycle, and service control.
|
||||
|
||||
**Binary location:** `/usr/local/bin/spx` (binary install) or `~/Development/mulga/spinifex/bin/spx` (source install)
|
||||
|
||||
All services in the Spinifex platform are managed through this single binary.
|
||||
|
||||
## Instructions
|
||||
|
||||
## Account Management
|
||||
|
||||
```bash
|
||||
spx admin account create --name myteam
|
||||
export AWS_PROFILE=spinifex-myteam
|
||||
```
|
||||
|
||||
## Node Management
|
||||
|
||||
```bash
|
||||
spx get nodes
|
||||
```
|
||||
|
||||
```
|
||||
NAME STATUS IP REGION AZ UPTIME VMs
|
||||
node1 Ready 127.0.0.1 ap-southeast-2 ap-southeast-2a 2m 0
|
||||
node2 Ready 127.0.0.2 ap-southeast-2 ap-southeast-2a 2m 0
|
||||
node3 Ready 127.0.0.3 ap-southeast-2 ap-southeast-2a 2m 0
|
||||
```
|
||||
|
||||
## Monitor Resources
|
||||
|
||||
```bash
|
||||
spx top nodes
|
||||
```
|
||||
|
||||
## Image Management
|
||||
|
||||
```bash
|
||||
spx admin images list
|
||||
spx admin images import --name debian-12-arm64
|
||||
```
|
||||
|
||||
## Cluster Shutdown
|
||||
|
||||
```bash
|
||||
spx admin cluster shutdown
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Permission Denied Running Spinifex
|
||||
|
||||
The binary may not be executable. Fix permissions:
|
||||
|
||||
```bash
|
||||
chmod +x /usr/local/bin/spx
|
||||
```
|
||||
|
||||
If you get permission errors during operations, ensure you're running with appropriate privileges. Some OVN and networking commands require `sudo`.
|
||||
|
||||
### Services Fail to Start
|
||||
|
||||
Check the daemon logs for specific errors:
|
||||
|
||||
```bash
|
||||
ls ~/spinifex/logs/
|
||||
cat ~/spinifex/logs/spinifex.log
|
||||
```
|
||||
|
||||
Common causes include port conflicts, missing OVN configuration, or untrusted CA certificates.
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: "Bastion with Private Subnet"
|
||||
description: "Deploy a bastion host to access an isolated private compute instance with no internet connectivity."
|
||||
category: "Terraform Workbooks"
|
||||
tags:
|
||||
- terraform
|
||||
- bastion
|
||||
- vpc
|
||||
- security
|
||||
- private subnet
|
||||
- workbook
|
||||
resources:
|
||||
- title: "Terraform AWS Provider"
|
||||
url: "https://registry.terraform.io/providers/hashicorp/aws/latest"
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "OpenTofu"
|
||||
url: "https://opentofu.org/"
|
||||
---
|
||||
|
||||
# Terraform: Bastion with Private Subnet
|
||||
|
||||
> Deploy a bastion host to access an isolated private compute instance with no internet connectivity.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Deploy a VPC with public and private subnets where the private subnet has no internet access. A bastion host in the public subnet is the only way to reach instances in the private subnet. This pattern is used for sensitive workloads that must remain isolated from the internet — the private instance cannot make or receive any connections outside the VPC.
|
||||
|
||||
**Architecture:**
|
||||
|
||||
```
|
||||
WAN ──SSH──▶ Bastion (public subnet)
|
||||
│
|
||||
▼ SSH (private IP)
|
||||
App Server (private subnet, no internet)
|
||||
```
|
||||
|
||||
**What you'll learn:**
|
||||
|
||||
- Creating public and private subnets in a VPC
|
||||
- Isolating compute instances from the internet using route tables and security groups
|
||||
- Using a bastion host as the sole access point to private resources
|
||||
- SSH hopping through a bastion to reach private instances
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- Sensitive data processing that must not have internet egress
|
||||
- Internal services that should only be reachable within the VPC
|
||||
- Compliance workloads requiring network isolation
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Spinifex installed and running (see [Installing Spinifex](/docs/install))
|
||||
- A Debian 12 AMI imported (see [Setting Up Your Cluster](/docs/setting-up-your-cluster))
|
||||
- OpenTofu or Terraform installed
|
||||
- `AWS_PROFILE=spinifex` configured
|
||||
|
||||
## Instructions
|
||||
|
||||
### Step 1. Get the Template
|
||||
|
||||
Clone the Terraform examples from the Spinifex repository:
|
||||
|
||||
```bash
|
||||
git clone --depth 1 --filter=blob:none --sparse https://github.com/mulgadc/spinifex.git spinifex-tf
|
||||
cd spinifex-tf
|
||||
git sparse-checkout set docs/terraform
|
||||
cd docs/terraform/bastion-private-subnet
|
||||
```
|
||||
|
||||
Or create a `main.tf` file and paste the full configuration below.
|
||||
|
||||
<!-- INCLUDE: main.tf lang:hcl -->
|
||||
|
||||
### Step 2. Deploy
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
tofu init
|
||||
tofu apply
|
||||
```
|
||||
|
||||
### Step 3. Connect
|
||||
|
||||
> **Note:** EC2 instances can take 30+ seconds to boot after apply. If SSH is unreachable, wait and retry.
|
||||
|
||||
SSH into the bastion:
|
||||
|
||||
```bash
|
||||
ssh -i bastion-demo.pem ec2-user@<bastion_public_ip>
|
||||
```
|
||||
|
||||
From the bastion, SSH to the private instance. The key is pre-installed at `~/.ssh/bastion-demo.pem` via cloud-init:
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/bastion-demo.pem ec2-user@<private_ip>
|
||||
```
|
||||
|
||||
### Step 4. Verify Isolation
|
||||
|
||||
From the private instance, confirm there is no internet connectivity:
|
||||
|
||||
```bash
|
||||
# This should time out — the private instance has no route to the internet
|
||||
curl --connect-timeout 5 https://deb.debian.org || echo "No internet access (expected)"
|
||||
```
|
||||
|
||||
The private instance can only communicate within the VPC (`10.20.0.0/16`). Its security group restricts egress to VPC-internal traffic only.
|
||||
|
||||
### Clean Up
|
||||
|
||||
```bash
|
||||
tofu destroy
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot SSH to Private Instance
|
||||
|
||||
The private instance has no public IP — it is only reachable from the bastion. SSH to the bastion first, then use the pre-installed key to hop:
|
||||
|
||||
```bash
|
||||
ssh -i bastion-demo.pem ec2-user@<bastion_ip>
|
||||
ssh -i ~/.ssh/bastion-demo.pem ec2-user@<private_ip>
|
||||
```
|
||||
|
||||
If the key is missing on the bastion, check that cloud-init completed successfully:
|
||||
|
||||
```bash
|
||||
sudo journalctl -u cloud-init --no-pager
|
||||
ls -la ~/.ssh/bastion-demo.pem
|
||||
```
|
||||
|
||||
### Private Instance Can Reach the Internet
|
||||
|
||||
If the private instance unexpectedly has internet access, check that its route table has no default route:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-route-tables --profile spinifex
|
||||
```
|
||||
|
||||
The private route table should have no `0.0.0.0/0` route. Also verify the private security group egress is restricted to the VPC CIDR (`10.20.0.0/16`).
|
||||
|
||||
### AMI Not Found
|
||||
|
||||
Ensure you have imported a Debian 12 image:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-images --owners 000000000000 --profile spinifex
|
||||
```
|
||||
@@ -0,0 +1,346 @@
|
||||
# Example 2: Bastion Host with Private Subnet
|
||||
#
|
||||
# Deploys a VPC with both public and private subnets. A bastion host in the
|
||||
# public subnet provides SSH access to an isolated instance in the private
|
||||
# subnet. The private instance has no internet connectivity — ideal for
|
||||
# sensitive workloads that must remain air-gapped from the internet.
|
||||
#
|
||||
# Architecture:
|
||||
#
|
||||
# WAN ──SSH──▶ Bastion (public subnet)
|
||||
# │
|
||||
# ▼ SSH (private IP)
|
||||
# App Server (private subnet, no internet)
|
||||
#
|
||||
# Usage:
|
||||
# export AWS_PROFILE=spinifex
|
||||
# tofu init && tofu apply
|
||||
#
|
||||
# After apply:
|
||||
# # SSH to the bastion
|
||||
# ssh -i bastion-demo.pem ec2-user@<bastion_ip>
|
||||
#
|
||||
# # From the bastion, SSH to the private instance
|
||||
# # (the key is pre-installed at ~/.ssh/bastion-demo.pem via cloud-init)
|
||||
# ssh -i ~/.ssh/bastion-demo.pem ec2-user@<private_ip>
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.6.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
tls = {
|
||||
source = "hashicorp/tls"
|
||||
version = ">= 4.0"
|
||||
}
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
version = ">= 2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Variables
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "ap-southeast-2"
|
||||
}
|
||||
|
||||
variable "spinifex_endpoint" {
|
||||
type = string
|
||||
default = "https://localhost:9999"
|
||||
description = "Spinifex AWS gateway endpoint"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provider
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
|
||||
endpoints {
|
||||
ec2 = var.spinifex_endpoint
|
||||
iam = var.spinifex_endpoint
|
||||
sts = var.spinifex_endpoint
|
||||
}
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
skip_region_validation = true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data sources
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
data "aws_availability_zones" "available" {
|
||||
state = "available"
|
||||
}
|
||||
|
||||
data "aws_ami" "debian12" {
|
||||
most_recent = true
|
||||
owners = ["000000000000"]
|
||||
|
||||
filter {
|
||||
name = "name"
|
||||
values = ["*debian-12*"]
|
||||
}
|
||||
|
||||
filter {
|
||||
name = "virtualization-type"
|
||||
values = ["hvm"]
|
||||
}
|
||||
|
||||
filter {
|
||||
name = "root-device-type"
|
||||
values = ["ebs"]
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSH Key Pair (shared by bastion and private instances)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "tls_private_key" "bastion" {
|
||||
algorithm = "ED25519"
|
||||
}
|
||||
|
||||
resource "aws_key_pair" "bastion" {
|
||||
key_name = "bastion-demo"
|
||||
public_key = tls_private_key.bastion.public_key_openssh
|
||||
}
|
||||
|
||||
resource "local_file" "bastion_pem" {
|
||||
filename = "${path.module}/bastion-demo.pem"
|
||||
content = tls_private_key.bastion.private_key_openssh
|
||||
file_permission = "0600"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VPC
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_vpc" "main" {
|
||||
cidr_block = "10.20.0.0/16"
|
||||
enable_dns_hostnames = true
|
||||
enable_dns_support = true
|
||||
|
||||
tags = {
|
||||
Name = "bastion-demo-vpc"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internet Gateway — only the public subnet routes through this
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_internet_gateway" "igw" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
tags = {
|
||||
Name = "bastion-demo-igw"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public Subnet — bastion host lives here
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_subnet" "public" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
cidr_block = "10.20.1.0/24"
|
||||
availability_zone = data.aws_availability_zones.available.names[0]
|
||||
map_public_ip_on_launch = true
|
||||
|
||||
tags = {
|
||||
Name = "bastion-demo-public"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table" "public" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
route {
|
||||
cidr_block = "0.0.0.0/0"
|
||||
gateway_id = aws_internet_gateway.igw.id
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "bastion-demo-public-rt"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "public" {
|
||||
subnet_id = aws_subnet.public.id
|
||||
route_table_id = aws_route_table.public.id
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Private Subnet — isolated instances live here (no public IPs, no internet)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_subnet" "private" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
cidr_block = "10.20.2.0/24"
|
||||
availability_zone = data.aws_availability_zones.available.names[0]
|
||||
map_public_ip_on_launch = false
|
||||
|
||||
tags = {
|
||||
Name = "bastion-demo-private"
|
||||
}
|
||||
}
|
||||
|
||||
# Private route table — no default route, no internet access
|
||||
resource "aws_route_table" "private" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
tags = {
|
||||
Name = "bastion-demo-private-rt"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "private" {
|
||||
subnet_id = aws_subnet.private.id
|
||||
route_table_id = aws_route_table.private.id
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Security Groups
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Bastion: SSH from anywhere
|
||||
resource "aws_security_group" "bastion" {
|
||||
name = "bastion-demo-bastion-sg"
|
||||
description = "Bastion: SSH from WAN"
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
ingress {
|
||||
description = "SSH"
|
||||
from_port = 22
|
||||
to_port = 22
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
egress {
|
||||
description = "All outbound"
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "bastion-demo-bastion-sg"
|
||||
}
|
||||
}
|
||||
|
||||
# Private instances: SSH only from the bastion security group
|
||||
resource "aws_security_group" "private" {
|
||||
name = "bastion-demo-private-sg"
|
||||
description = "Private: SSH from bastion only"
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
ingress {
|
||||
description = "SSH from bastion"
|
||||
from_port = 22
|
||||
to_port = 22
|
||||
protocol = "tcp"
|
||||
security_groups = [aws_security_group.bastion.id]
|
||||
}
|
||||
|
||||
egress {
|
||||
description = "VPC internal only"
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["10.20.0.0/16"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "bastion-demo-private-sg"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bastion Host (public subnet)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_instance" "bastion" {
|
||||
ami = data.aws_ami.debian12.id
|
||||
instance_type = "t3.small"
|
||||
|
||||
subnet_id = aws_subnet.public.id
|
||||
vpc_security_group_ids = [aws_security_group.bastion.id]
|
||||
key_name = aws_key_pair.bastion.key_name
|
||||
|
||||
associate_public_ip_address = true
|
||||
|
||||
# Copy the SSH private key onto the bastion so you can hop to private instances
|
||||
user_data_base64 = base64encode(<<-USERDATA
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
mkdir -p /home/ec2-user/.ssh
|
||||
cat > /home/ec2-user/.ssh/bastion-demo.pem <<'KEY'
|
||||
${tls_private_key.bastion.private_key_openssh}
|
||||
KEY
|
||||
chmod 600 /home/ec2-user/.ssh/bastion-demo.pem
|
||||
chown -R ec2-user:ec2-user /home/ec2-user/.ssh
|
||||
USERDATA
|
||||
)
|
||||
|
||||
tags = {
|
||||
Name = "bastion-demo-bastion"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Private Instance (private subnet — no public IP, no internet)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_instance" "private" {
|
||||
ami = data.aws_ami.debian12.id
|
||||
instance_type = "t3.small"
|
||||
|
||||
subnet_id = aws_subnet.private.id
|
||||
vpc_security_group_ids = [aws_security_group.private.id]
|
||||
key_name = aws_key_pair.bastion.key_name
|
||||
|
||||
tags = {
|
||||
Name = "bastion-demo-private-app"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Outputs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
output "note" {
|
||||
value = "EC2 instances can take 30+ seconds to boot after apply. If SSH is unreachable, wait and retry."
|
||||
}
|
||||
|
||||
output "bastion_public_ip" {
|
||||
value = aws_instance.bastion.public_ip
|
||||
}
|
||||
|
||||
output "private_instance_ip" {
|
||||
value = aws_instance.private.private_ip
|
||||
}
|
||||
|
||||
output "ssh_to_bastion" {
|
||||
description = "SSH to the bastion host"
|
||||
value = "ssh -i bastion-demo.pem ec2-user@${aws_instance.bastion.public_ip}"
|
||||
}
|
||||
|
||||
output "ssh_to_private_from_bastion" {
|
||||
description = "From the bastion, SSH to the private instance (key is pre-installed via cloud-init)"
|
||||
value = "ssh -i ~/.ssh/bastion-demo.pem ec2-user@${aws_instance.private.private_ip}"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
title: "Nginx Web Server"
|
||||
description: "Deploy a VPC with a public subnet and an EC2 instance running Nginx using Terraform on Spinifex."
|
||||
category: "Terraform Workbooks"
|
||||
tags:
|
||||
- terraform
|
||||
- nginx
|
||||
- ec2
|
||||
- vpc
|
||||
- workbook
|
||||
resources:
|
||||
- title: "Terraform AWS Provider"
|
||||
url: "https://registry.terraform.io/providers/hashicorp/aws/latest"
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "OpenTofu"
|
||||
url: "https://opentofu.org/"
|
||||
---
|
||||
|
||||
# Terraform: Nginx Web Server
|
||||
|
||||
> Deploy a VPC with a public subnet and an EC2 instance running Nginx using Terraform on Spinifex.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Deploy a complete Nginx web server on Spinifex using Terraform/OpenTofu. This workbook provisions a VPC, public subnet, internet gateway, route table, security group, SSH key pair, and an EC2 instance with cloud-init user-data that installs and starts Nginx.
|
||||
|
||||
**What you'll learn:**
|
||||
|
||||
- Configuring the AWS Terraform provider to target Spinifex
|
||||
- Creating a VPC with public internet access
|
||||
- Provisioning an EC2 instance with cloud-init user-data
|
||||
- Generating SSH key pairs with the TLS provider
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Spinifex installed and running (see [Installing Spinifex](/docs/install))
|
||||
- A Debian 12 AMI imported (see [Setting Up Your Cluster](/docs/setting-up-your-cluster))
|
||||
- OpenTofu or Terraform installed
|
||||
- `AWS_PROFILE=spinifex` configured
|
||||
|
||||
## Instructions
|
||||
|
||||
### Step 1. Get the Template
|
||||
|
||||
Clone the Terraform examples from the Spinifex repository:
|
||||
|
||||
```bash
|
||||
git clone --depth 1 --filter=blob:none --sparse https://github.com/mulgadc/spinifex.git spinifex-tf
|
||||
cd spinifex-tf
|
||||
git sparse-checkout set docs/terraform
|
||||
cd docs/terraform/nginx-webserver
|
||||
```
|
||||
|
||||
Or create a `main.tf` file and paste the full configuration below.
|
||||
|
||||
<!-- INCLUDE: main.tf lang:hcl -->
|
||||
|
||||
### Step 2. Deploy
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
tofu init
|
||||
tofu apply
|
||||
```
|
||||
|
||||
### Step 3. Verify
|
||||
|
||||
> **Note:** EC2 instances can take 30+ seconds to boot after apply. If SSH or HTTP is unreachable, wait and retry.
|
||||
|
||||
After apply completes, use the outputs to test:
|
||||
|
||||
```bash
|
||||
curl http://<public_ip>
|
||||
ssh -i nginx-demo.pem ec2-user@<public_ip>
|
||||
```
|
||||
|
||||
Open the `web_url` output in your browser to see the Nginx welcome page.
|
||||
|
||||
### Clean Up
|
||||
|
||||
```bash
|
||||
tofu destroy
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### AMI Not Found
|
||||
|
||||
Ensure you have imported a Debian 12 image. Check available AMIs:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-images --owners 000000000000 --profile spinifex
|
||||
```
|
||||
|
||||
### Provider Connection Refused
|
||||
|
||||
Verify Spinifex services are running:
|
||||
|
||||
```bash
|
||||
sudo systemctl status spinifex.target
|
||||
curl -k https://localhost:9999/
|
||||
```
|
||||
|
||||
### SSH Connection Timeout
|
||||
|
||||
Check that the security group allows inbound SSH (port 22) and that the instance has a public IP assigned. Verify the instance is running:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-instances --profile spinifex
|
||||
```
|
||||
|
||||
### Nginx Not Responding
|
||||
|
||||
SSH into the instance and check cloud-init logs:
|
||||
|
||||
```bash
|
||||
ssh -i nginx-demo.pem ec2-user@<public_ip>
|
||||
sudo journalctl -u cloud-init --no-pager
|
||||
sudo systemctl status nginx
|
||||
```
|
||||
@@ -0,0 +1,286 @@
|
||||
# Example 1: Nginx Web Server on Spinifex
|
||||
#
|
||||
# Deploys a VPC with a public subnet and an EC2 instance running Nginx.
|
||||
# Demonstrates: VPC, subnet, internet gateway, route table, security group,
|
||||
# key pair, cloud-init user-data, and EC2 instance provisioning.
|
||||
#
|
||||
# Usage:
|
||||
# cd spinifex/scripts/iac/aws/examples/01-nginx-webserver
|
||||
# export AWS_PROFILE=spinifex
|
||||
# tofu init && tofu apply
|
||||
#
|
||||
# After apply:
|
||||
# curl http://<public_ip> # Nginx welcome page
|
||||
# ssh -i nginx-demo.pem ec2-user@<public_ip>
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.6.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
tls = {
|
||||
source = "hashicorp/tls"
|
||||
version = ">= 4.0"
|
||||
}
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
version = ">= 2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Variables
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "ap-southeast-2"
|
||||
}
|
||||
|
||||
variable "spinifex_endpoint" {
|
||||
type = string
|
||||
default = "https://localhost:9999"
|
||||
description = "Spinifex AWS gateway endpoint"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provider — point the AWS provider at Spinifex
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
|
||||
endpoints {
|
||||
ec2 = var.spinifex_endpoint
|
||||
iam = var.spinifex_endpoint
|
||||
sts = var.spinifex_endpoint
|
||||
}
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
skip_region_validation = true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data sources
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
data "aws_availability_zones" "available" {
|
||||
state = "available"
|
||||
}
|
||||
|
||||
data "aws_ami" "debian12" {
|
||||
most_recent = true
|
||||
owners = ["000000000000"] # Spinifex system images
|
||||
|
||||
filter {
|
||||
name = "name"
|
||||
values = ["*debian-12*"]
|
||||
}
|
||||
|
||||
filter {
|
||||
name = "virtualization-type"
|
||||
values = ["hvm"]
|
||||
}
|
||||
|
||||
filter {
|
||||
name = "root-device-type"
|
||||
values = ["ebs"]
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSH Key Pair
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "tls_private_key" "nginx" {
|
||||
algorithm = "ED25519"
|
||||
}
|
||||
|
||||
resource "aws_key_pair" "nginx" {
|
||||
key_name = "nginx-demo"
|
||||
public_key = tls_private_key.nginx.public_key_openssh
|
||||
}
|
||||
|
||||
resource "local_file" "nginx_pem" {
|
||||
filename = "${path.module}/nginx-demo.pem"
|
||||
content = tls_private_key.nginx.private_key_openssh
|
||||
file_permission = "0600"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VPC
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_vpc" "main" {
|
||||
cidr_block = "10.10.0.0/16"
|
||||
enable_dns_hostnames = true
|
||||
enable_dns_support = true
|
||||
|
||||
tags = {
|
||||
Name = "nginx-demo-vpc"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internet Gateway — gives the public subnet a route to the internet
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_internet_gateway" "igw" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
tags = {
|
||||
Name = "nginx-demo-igw"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public Subnet
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_subnet" "public" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
cidr_block = "10.10.1.0/24"
|
||||
availability_zone = data.aws_availability_zones.available.names[0]
|
||||
map_public_ip_on_launch = true
|
||||
|
||||
tags = {
|
||||
Name = "nginx-demo-public"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Route Table — send 0.0.0.0/0 through the internet gateway
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_route_table" "public" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
route {
|
||||
cidr_block = "0.0.0.0/0"
|
||||
gateway_id = aws_internet_gateway.igw.id
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "nginx-demo-public-rt"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "public" {
|
||||
subnet_id = aws_subnet.public.id
|
||||
route_table_id = aws_route_table.public.id
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Security Group — SSH + HTTP inbound, all outbound
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_security_group" "web" {
|
||||
name = "nginx-demo-sg"
|
||||
description = "Allow SSH and HTTP inbound"
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
ingress {
|
||||
description = "SSH"
|
||||
from_port = 22
|
||||
to_port = 22
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
ingress {
|
||||
description = "HTTP"
|
||||
from_port = 80
|
||||
to_port = 80
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
egress {
|
||||
description = "All outbound"
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "nginx-demo-sg"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EC2 Instance — Debian 12 with Nginx installed via cloud-init
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_instance" "nginx" {
|
||||
ami = data.aws_ami.debian12.id
|
||||
instance_type = "t3.small"
|
||||
|
||||
subnet_id = aws_subnet.public.id
|
||||
vpc_security_group_ids = [aws_security_group.web.id]
|
||||
key_name = aws_key_pair.nginx.key_name
|
||||
|
||||
associate_public_ip_address = true
|
||||
|
||||
user_data_base64 = base64encode(<<-USERDATA
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install Nginx
|
||||
apt-get update -y
|
||||
apt-get install -y nginx
|
||||
|
||||
# Write a custom landing page
|
||||
cat > /var/www/html/index.html <<'HTML'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Spinifex Demo</title></head>
|
||||
<body style="font-family: sans-serif; max-width: 600px; margin: 80px auto;">
|
||||
<h1>Hello from Spinifex!</h1>
|
||||
<p>This Nginx server was deployed with Terraform on Spinifex infrastructure.</p>
|
||||
<hr>
|
||||
<p><small>Instance provisioned via cloud-init user-data.</small></p>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
# Ensure Nginx is running
|
||||
systemctl enable nginx
|
||||
systemctl restart nginx
|
||||
USERDATA
|
||||
)
|
||||
|
||||
tags = {
|
||||
Name = "nginx-demo"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Outputs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
output "note" {
|
||||
value = "EC2 instances can take 30+ seconds to boot after apply. If SSH or HTTP is unreachable, wait and retry."
|
||||
}
|
||||
|
||||
output "instance_id" {
|
||||
value = aws_instance.nginx.id
|
||||
}
|
||||
|
||||
output "public_ip" {
|
||||
value = aws_instance.nginx.public_ip
|
||||
}
|
||||
|
||||
output "ssh_command" {
|
||||
value = "ssh -i nginx-demo.pem ec2-user@${aws_instance.nginx.public_ip}"
|
||||
}
|
||||
|
||||
output "web_url" {
|
||||
value = "http://${aws_instance.nginx.public_ip}"
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: "S3-Backed Web App"
|
||||
description: "Deploy a Flask file-sharing web application backed by S3 (Predastore) using Terraform on Spinifex."
|
||||
category: "Terraform Workbooks"
|
||||
tags:
|
||||
- terraform
|
||||
- s3
|
||||
- predastore
|
||||
- flask
|
||||
- webapp
|
||||
- workbook
|
||||
resources:
|
||||
- title: "Terraform AWS Provider"
|
||||
url: "https://registry.terraform.io/providers/hashicorp/aws/latest"
|
||||
- title: "Spinifex Repository"
|
||||
url: "https://github.com/mulgadc/spinifex"
|
||||
- title: "Predastore (S3)"
|
||||
url: "https://github.com/mulgadc/predastore"
|
||||
- title: "OpenTofu"
|
||||
url: "https://opentofu.org/"
|
||||
---
|
||||
|
||||
# Terraform: S3-Backed Web App
|
||||
|
||||
> Deploy a Flask file-sharing web application backed by S3 (Predastore) using Terraform on Spinifex.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Instructions](#instructions)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Deploy an EC2 instance running a Flask file-sharing web application backed by S3 (Predastore). Users can upload files through a web form and browse uploaded content — demonstrating Terraform managing both compute and object storage together.
|
||||
|
||||
**Architecture:**
|
||||
|
||||
```
|
||||
Browser ──HTTP──▶ EC2 Instance (Flask webapp, port 80)
|
||||
│
|
||||
▼ S3 API (boto3)
|
||||
Predastore (port 8443)
|
||||
```
|
||||
|
||||
**What you'll learn:**
|
||||
|
||||
- Configuring the AWS provider with both Spinifex and Predastore endpoints
|
||||
- Creating S3 buckets on Predastore via Terraform
|
||||
- Deploying a Python webapp with cloud-init that talks to S3
|
||||
- Passing credentials and configuration to instances via user-data
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Spinifex installed and running (see [Installing Spinifex](/docs/install))
|
||||
- Predastore running (S3 API on port 8443)
|
||||
- A Debian 12 AMI imported (see [Setting Up Your Cluster](/docs/setting-up-your-cluster))
|
||||
- OpenTofu or Terraform installed
|
||||
- `AWS_PROFILE=spinifex` configured
|
||||
- The EC2 instance must be able to reach Predastore — use the host's br-wan IP, not localhost
|
||||
|
||||
## Instructions
|
||||
|
||||
### Step 1. Get the Template
|
||||
|
||||
Clone the Terraform examples from the Spinifex repository:
|
||||
|
||||
```bash
|
||||
git clone --depth 1 --filter=blob:none --sparse https://github.com/mulgadc/spinifex.git spinifex-tf
|
||||
cd spinifex-tf
|
||||
git sparse-checkout set docs/terraform
|
||||
cd docs/terraform/s3-webapp
|
||||
```
|
||||
|
||||
Or create the files manually and paste the full configuration below.
|
||||
|
||||
### Step 2. Create terraform.tfvars
|
||||
|
||||
Before deploying, create a `terraform.tfvars` with your Predastore credentials. The `predastore_host` must be reachable from inside the VPC — use the host's br-wan or LAN IP, not localhost.
|
||||
|
||||
<!-- INCLUDE: terraform.tfvars.example lang:hcl -->
|
||||
|
||||
### Step 3. Create main.tf
|
||||
|
||||
<!-- INCLUDE: main.tf lang:hcl -->
|
||||
|
||||
### Step 4. Deploy
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=spinifex
|
||||
tofu init
|
||||
tofu apply
|
||||
```
|
||||
|
||||
### Step 5. Test the Application
|
||||
|
||||
> **Note:** EC2 instances can take 30+ seconds to boot after apply. If SSH or HTTP is unreachable, wait and retry.
|
||||
|
||||
Open the `web_url` output in your browser. You should see the file browser UI. Upload a file and verify it appears in the list.
|
||||
|
||||
```bash
|
||||
# Verify via CLI
|
||||
curl http://<public_ip>
|
||||
|
||||
# Check the S3 bucket directly
|
||||
aws s3 ls s3://webapp-uploads/ --profile spinifex --endpoint-url https://localhost:8443
|
||||
```
|
||||
|
||||
### Clean Up
|
||||
|
||||
```bash
|
||||
tofu destroy
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Predastore Connection Refused from Instance
|
||||
|
||||
The EC2 instance cannot reach `localhost` on the host. Set `predastore_host` to the host's br-wan or LAN IP address:
|
||||
|
||||
```hcl
|
||||
predastore_host = "192.168.1.10:8443"
|
||||
```
|
||||
|
||||
### S3 Bucket Creation Fails
|
||||
|
||||
Verify Predastore is running and accessible:
|
||||
|
||||
```bash
|
||||
curl -k https://localhost:8443/
|
||||
aws s3 ls --profile spinifex --endpoint-url https://localhost:8443
|
||||
```
|
||||
|
||||
### Flask App Not Starting
|
||||
|
||||
SSH into the instance and check the service:
|
||||
|
||||
```bash
|
||||
ssh -i s3-webapp-demo.pem ec2-user@<public_ip>
|
||||
sudo systemctl status s3-webapp
|
||||
sudo journalctl -u s3-webapp --no-pager -n 50
|
||||
```
|
||||
|
||||
### Upload Fails or Files Not Appearing
|
||||
|
||||
Check that the S3 credentials in `/opt/webapp/.env` are correct and that the bucket exists:
|
||||
|
||||
```bash
|
||||
ssh -i s3-webapp-demo.pem ec2-user@<public_ip>
|
||||
cat /opt/webapp/.env
|
||||
/opt/webapp/venv/bin/python -c "import boto3; print('boto3 OK')"
|
||||
```
|
||||
|
||||
### AMI Not Found
|
||||
|
||||
Ensure you have imported a Debian 12 image:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-images --owners 000000000000 --profile spinifex
|
||||
```
|
||||
@@ -0,0 +1,445 @@
|
||||
# Example 3: S3-Backed Web Application
|
||||
#
|
||||
# Deploys an EC2 instance running a simple file-sharing webapp backed by S3
|
||||
# (Predastore). Users can upload files through a web form and browse uploaded
|
||||
# content — demonstrating Terraform managing both compute (Spinifex) and
|
||||
# object storage (Predastore) resources together.
|
||||
#
|
||||
# Architecture:
|
||||
#
|
||||
# Browser ──HTTP──▶ EC2 Instance (Flask webapp, port 80)
|
||||
# │
|
||||
# ▼ S3 API (boto3)
|
||||
# Predastore (port 8443)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Spinifex services running (gateway on port 9999)
|
||||
# - Predastore running (S3 API on port 8443)
|
||||
# - The EC2 instance must be able to reach the Predastore endpoint.
|
||||
# Set `predastore_host` to the IP reachable from inside the VPC
|
||||
# (e.g. the host's br-wan IP, NOT localhost).
|
||||
#
|
||||
# Usage:
|
||||
# cd spinifex/scripts/iac/aws/examples/03-s3-webapp
|
||||
# export AWS_PROFILE=spinifex
|
||||
# tofu init && tofu apply
|
||||
#
|
||||
# After apply:
|
||||
# curl http://<public_ip> # File browser UI
|
||||
# ssh -i s3-webapp-demo.pem ec2-user@<public_ip>
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.6.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
tls = {
|
||||
source = "hashicorp/tls"
|
||||
version = ">= 4.0"
|
||||
}
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
version = ">= 2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Variables
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "ap-southeast-2"
|
||||
}
|
||||
|
||||
variable "spinifex_endpoint" {
|
||||
type = string
|
||||
default = "https://localhost:9999"
|
||||
description = "Spinifex AWS gateway endpoint (EC2/IAM)"
|
||||
}
|
||||
|
||||
variable "predastore_endpoint" {
|
||||
type = string
|
||||
default = "https://localhost:8443"
|
||||
description = "Predastore S3 endpoint (for Terraform to create buckets)"
|
||||
}
|
||||
|
||||
variable "predastore_host" {
|
||||
type = string
|
||||
description = "Predastore host:port reachable from inside the VPC (e.g. 192.168.1.10:8443)"
|
||||
}
|
||||
|
||||
variable "s3_access_key" {
|
||||
type = string
|
||||
description = "S3 access key for Predastore"
|
||||
}
|
||||
|
||||
variable "s3_secret_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
description = "S3 secret key for Predastore"
|
||||
}
|
||||
|
||||
variable "bucket_name" {
|
||||
type = string
|
||||
default = "webapp-uploads"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provider — EC2 via Spinifex gateway, S3 via Predastore
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
access_key = var.s3_access_key
|
||||
secret_key = var.s3_secret_key
|
||||
|
||||
endpoints {
|
||||
ec2 = var.spinifex_endpoint
|
||||
s3 = var.predastore_endpoint
|
||||
iam = var.spinifex_endpoint
|
||||
sts = var.spinifex_endpoint
|
||||
}
|
||||
|
||||
s3_use_path_style = true
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
skip_region_validation = true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data sources
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
data "aws_availability_zones" "available" {
|
||||
state = "available"
|
||||
}
|
||||
|
||||
data "aws_ami" "debian12" {
|
||||
most_recent = true
|
||||
owners = ["000000000000"]
|
||||
|
||||
filter {
|
||||
name = "name"
|
||||
values = ["*debian-12*"]
|
||||
}
|
||||
|
||||
filter {
|
||||
name = "virtualization-type"
|
||||
values = ["hvm"]
|
||||
}
|
||||
|
||||
filter {
|
||||
name = "root-device-type"
|
||||
values = ["ebs"]
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# S3 Bucket (created on Predastore)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_s3_bucket" "uploads" {
|
||||
bucket = var.bucket_name
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSH Key Pair
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "tls_private_key" "webapp" {
|
||||
algorithm = "ED25519"
|
||||
}
|
||||
|
||||
resource "aws_key_pair" "webapp" {
|
||||
key_name = "s3-webapp-demo"
|
||||
public_key = tls_private_key.webapp.public_key_openssh
|
||||
}
|
||||
|
||||
resource "local_file" "webapp_pem" {
|
||||
filename = "${path.module}/s3-webapp-demo.pem"
|
||||
content = tls_private_key.webapp.private_key_openssh
|
||||
file_permission = "0600"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VPC + Public Subnet
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_vpc" "main" {
|
||||
cidr_block = "10.30.0.0/16"
|
||||
enable_dns_hostnames = true
|
||||
enable_dns_support = true
|
||||
|
||||
tags = {
|
||||
Name = "s3-webapp-demo-vpc"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_internet_gateway" "igw" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
tags = {
|
||||
Name = "s3-webapp-demo-igw"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_subnet" "public" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
cidr_block = "10.30.1.0/24"
|
||||
availability_zone = data.aws_availability_zones.available.names[0]
|
||||
map_public_ip_on_launch = true
|
||||
|
||||
tags = {
|
||||
Name = "s3-webapp-demo-public"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table" "public" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
route {
|
||||
cidr_block = "0.0.0.0/0"
|
||||
gateway_id = aws_internet_gateway.igw.id
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "s3-webapp-demo-public-rt"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "public" {
|
||||
subnet_id = aws_subnet.public.id
|
||||
route_table_id = aws_route_table.public.id
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Security Group — SSH + HTTP inbound, all outbound
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_security_group" "webapp" {
|
||||
name = "s3-webapp-demo-sg"
|
||||
description = "Allow SSH and HTTP inbound"
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
ingress {
|
||||
description = "SSH"
|
||||
from_port = 22
|
||||
to_port = 22
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
ingress {
|
||||
description = "HTTP"
|
||||
from_port = 80
|
||||
to_port = 80
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
egress {
|
||||
description = "All outbound"
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "s3-webapp-demo-sg"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EC2 Instance — Flask webapp that talks to Predastore S3
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_instance" "webapp" {
|
||||
ami = data.aws_ami.debian12.id
|
||||
instance_type = "t3.small"
|
||||
|
||||
subnet_id = aws_subnet.public.id
|
||||
vpc_security_group_ids = [aws_security_group.webapp.id]
|
||||
key_name = aws_key_pair.webapp.key_name
|
||||
|
||||
associate_public_ip_address = true
|
||||
|
||||
user_data_base64 = base64encode(<<-USERDATA
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install dependencies
|
||||
apt-get update -y
|
||||
apt-get install -y python3-pip python3-venv
|
||||
|
||||
# Create app directory and virtualenv
|
||||
mkdir -p /opt/webapp
|
||||
python3 -m venv /opt/webapp/venv
|
||||
/opt/webapp/venv/bin/pip install flask boto3
|
||||
|
||||
# Write S3 credentials config
|
||||
cat > /opt/webapp/.env <<'ENVFILE'
|
||||
S3_ENDPOINT=https://${var.predastore_host}
|
||||
S3_BUCKET=${var.bucket_name}
|
||||
S3_ACCESS_KEY=${var.s3_access_key}
|
||||
S3_SECRET_KEY=${var.s3_secret_key}
|
||||
S3_REGION=${var.region}
|
||||
ENVFILE
|
||||
|
||||
# Write the Flask application
|
||||
cat > /opt/webapp/app.py <<'PYEOF'
|
||||
import os, io, urllib3
|
||||
from flask import Flask, request, redirect, url_for, Response
|
||||
|
||||
# Suppress TLS warnings for self-signed certs
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# Load env
|
||||
env = {}
|
||||
with open("/opt/webapp/.env") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.split("=", 1)
|
||||
env[k] = v
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=env["S3_ENDPOINT"],
|
||||
aws_access_key_id=env["S3_ACCESS_KEY"],
|
||||
aws_secret_access_key=env["S3_SECRET_KEY"],
|
||||
region_name=env["S3_REGION"],
|
||||
verify=False,
|
||||
config=Config(s3={"addressing_style": "path"}),
|
||||
)
|
||||
|
||||
BUCKET = env["S3_BUCKET"]
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
# List objects in the bucket
|
||||
try:
|
||||
resp = s3.list_objects_v2(Bucket=BUCKET)
|
||||
objects = resp.get("Contents", [])
|
||||
except Exception as e:
|
||||
objects = []
|
||||
|
||||
rows = ""
|
||||
for obj in objects:
|
||||
key = obj["Key"]
|
||||
size = obj["Size"]
|
||||
rows += f'<tr><td><a href="/files/{key}">{key}</a></td><td>{size} bytes</td></tr>\n'
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Spinifex S3 File Browser</title></head>
|
||||
<body style="font-family: sans-serif; max-width: 700px; margin: 40px auto;">
|
||||
<h1>Spinifex S3 File Browser</h1>
|
||||
<p>Bucket: <code>{BUCKET}</code></p>
|
||||
|
||||
<h2>Upload a File</h2>
|
||||
<form method="POST" action="/upload" enctype="multipart/form-data">
|
||||
<input type="file" name="file" required>
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
|
||||
<h2>Files</h2>
|
||||
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse: collapse;">
|
||||
<tr><th>Key</th><th>Size</th></tr>
|
||||
{rows if rows else '<tr><td colspan="2">No files yet</td></tr>'}
|
||||
</table>
|
||||
|
||||
<hr>
|
||||
<p><small>Powered by Spinifex + Predastore</small></p>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
@app.route("/upload", methods=["POST"])
|
||||
def upload():
|
||||
f = request.files.get("file")
|
||||
if not f or not f.filename:
|
||||
return redirect("/")
|
||||
s3.put_object(Bucket=BUCKET, Key=f.filename, Body=f.read())
|
||||
return redirect("/")
|
||||
|
||||
@app.route("/files/<path:key>")
|
||||
def download(key):
|
||||
try:
|
||||
obj = s3.get_object(Bucket=BUCKET, Key=key)
|
||||
return Response(
|
||||
obj["Body"].read(),
|
||||
headers={"Content-Disposition": f'inline; filename="{key}"'},
|
||||
)
|
||||
except Exception:
|
||||
return "Not found", 404
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=80)
|
||||
PYEOF
|
||||
|
||||
# Create a systemd service so the webapp starts on boot
|
||||
cat > /etc/systemd/system/s3-webapp.service <<'SVCEOF'
|
||||
[Unit]
|
||||
Description=S3 File Browser Webapp
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/opt/webapp/venv/bin/python /opt/webapp/app.py
|
||||
WorkingDirectory=/opt/webapp
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SVCEOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable s3-webapp
|
||||
systemctl start s3-webapp
|
||||
USERDATA
|
||||
)
|
||||
|
||||
tags = {
|
||||
Name = "s3-webapp-demo"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Outputs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
output "note" {
|
||||
value = "EC2 instances can take 30+ seconds to boot after apply. If SSH or HTTP is unreachable, wait and retry."
|
||||
}
|
||||
|
||||
output "instance_id" {
|
||||
value = aws_instance.webapp.id
|
||||
}
|
||||
|
||||
output "public_ip" {
|
||||
value = aws_instance.webapp.public_ip
|
||||
}
|
||||
|
||||
output "bucket_name" {
|
||||
value = aws_s3_bucket.uploads.id
|
||||
}
|
||||
|
||||
output "ssh_command" {
|
||||
value = "ssh -i s3-webapp-demo.pem ec2-user@${aws_instance.webapp.public_ip}"
|
||||
}
|
||||
|
||||
output "web_url" {
|
||||
value = "http://${aws_instance.webapp.public_ip}"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copy this to terraform.tfvars and fill in your values.
|
||||
#
|
||||
# The predastore_host must be reachable from INSIDE the VPC guest — not
|
||||
# localhost. Use the host's br-wan or LAN IP, e.g. "192.168.1.10:8443".
|
||||
|
||||
predastore_host = "192.168.1.10:8443"
|
||||
s3_access_key = "AKIAIOSFODNN7EXAMPLE"
|
||||
s3_secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
|
||||
# Optional overrides:
|
||||
# spinifex_endpoint = "https://localhost:9999"
|
||||
# predastore_endpoint = "https://localhost:8443"
|
||||
# bucket_name = "webapp-uploads"
|
||||
# region = "ap-southeast-2"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
module github.com/mulgadc/hive
|
||||
module github.com/mulgadc/spinifex
|
||||
|
||||
go 1.26.1
|
||||
|
||||
@@ -8,18 +8,16 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/kdomanski/iso9660 v0.4.0
|
||||
github.com/klauspost/cpuid/v2 v2.3.0
|
||||
github.com/mulgadc/predastore v1.0.3
|
||||
github.com/mulgadc/viperblock v1.0.17
|
||||
github.com/nats-io/nats-server/v2 v2.12.4
|
||||
github.com/nats-io/nats.go v1.49.0
|
||||
github.com/mulgadc/predastore v1.1.4
|
||||
github.com/mulgadc/viperblock v1.1.5
|
||||
github.com/nats-io/nats-server/v2 v2.12.6
|
||||
github.com/nats-io/nats.go v1.50.0
|
||||
github.com/ovn-kubernetes/libovsdb v0.8.1
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/pelletier/go-toml/v2 v2.3.0
|
||||
github.com/pterm/pterm v0.12.83
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
golang.org/x/net v0.51.0
|
||||
gopkg.in/ini.v1 v1.67.1
|
||||
)
|
||||
|
||||
@@ -27,21 +25,14 @@ 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.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect
|
||||
github.com/antithesishq/antithesis-sdk-go v0.6.0 // indirect
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/buraksezer/consistent v0.10.0 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/hub v1.0.2 // indirect
|
||||
github.com/cenkalti/rpc2 v1.0.5 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
@@ -49,8 +40,7 @@ require (
|
||||
github.com/dgraph-io/badger/v4 v4.9.1 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
@@ -60,13 +50,8 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.13 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/gookit/color v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-metrics v0.5.4 // indirect
|
||||
@@ -77,20 +62,19 @@ require (
|
||||
github.com/hashicorp/raft-boltdb/v2 v2.3.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/klauspost/reedsolomon v1.13.2 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/reedsolomon v1.13.3 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
|
||||
github.com/minio/highwayhash v1.0.4 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.0 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.1 // indirect
|
||||
github.com/nats-io/nkeys v0.4.15 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/openai/openai-go/v3 v3.24.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
@@ -98,45 +82,32 @@ require (
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.24.7 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.opentelemetry.io/otel v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260304144227-18da59047661 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.org/x/vuln v1.1.4 // indirect
|
||||
google.golang.org/genai v1.49.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/grpc v1.79.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
honnef.co/go/tools v0.7.0 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/securego/gosec/v2/cmd/gosec
|
||||
golang.org/x/vuln/cmd/govulncheck
|
||||
honnef.co/go/tools/cmd/staticcheck
|
||||
)
|
||||
tool golang.org/x/vuln/cmd/govulncheck
|
||||
|
||||
@@ -7,14 +7,6 @@ atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtE
|
||||
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
||||
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
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=
|
||||
@@ -25,19 +17,13 @@ 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/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.6.0 h1:v/YViLhFYkZOEEof4AXjD5AgGnGM84YHF4RqEwp6I2g=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.6.0/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0 h1:uWDG8BqLD1lI2ps38WDz2vXflrTX2+vLX0SvZtztJtE=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
@@ -89,16 +75,12 @@ github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/buraksezer/consistent v0.10.0 h1:hqBgz1PvNLC5rkWcEBVAL9dFMBWz6I0VgUCW25rrZlU=
|
||||
github.com/buraksezer/consistent v0.10.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw=
|
||||
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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/hub v1.0.2 h1:Nqv9TNaA9boeO2wQFW8o87BY3zKthtnzXmWGmJqhAV8=
|
||||
github.com/cenkalti/hub v1.0.2/go.mod h1:8LAFAZcCasb83vfxatMUnZHRoQcffho2ELpHb+kaTJU=
|
||||
github.com/cenkalti/rpc2 v1.0.5 h1:T6l4SS3ja3eaJfRyZrn7Oco/PSx/pr3YK5cjCgLVLTk=
|
||||
github.com/cenkalti/rpc2 v1.0.5/go.mod h1:2yfU5b86vOr16+iY1jN3MvT6Kxc9Nf8j5iZWwUf7iaw=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -119,15 +101,11 @@ github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuM
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
@@ -156,8 +134,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
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.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -171,8 +147,6 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
|
||||
@@ -187,26 +161,16 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
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.13 h1:hSPAhW3NX+7HNlTsmrvU0jL75cIzxFktheceg95Nq14=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.13/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
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.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/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
@@ -248,15 +212,15 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kdomanski/iso9660 v0.4.0 h1:BPKKdcINz3m0MdjIMwS0wx1nofsOjxOq8TOr45WGHFg=
|
||||
github.com/kdomanski/iso9660 v0.4.0/go.mod h1:OxUSupHsO9ceI8lBLPJKWBTphLemjrCQY8LPXM7qSzU=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/reedsolomon v1.13.2 h1:9qtQy2tKEVpVB8Pfq87ZljHZb60/LbeTQ1OxV8EGzdE=
|
||||
github.com/klauspost/reedsolomon v1.13.2/go.mod h1:ggJT9lc71Vu+cSOPBlxGvBN6TfAS77qB4fp8vJ05NSA=
|
||||
github.com/klauspost/reedsolomon v1.13.3 h1:01GwnO2xoCSaM0ShP4qwl+FsHg3csFShC6Tu/RS1ji0=
|
||||
github.com/klauspost/reedsolomon v1.13.3/go.mod h1:yjqqjgMTQkBUHSG97/rm4zipffCNbCiZcB3kTqr++sQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
@@ -280,54 +244,46 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk=
|
||||
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
|
||||
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mulgadc/predastore v1.0.3 h1:qkuOEUH9B98QzVByn5osKeXm7SSPgk+ardQ3MibPSok=
|
||||
github.com/mulgadc/predastore v1.0.3/go.mod h1:x3Z6Ts/PONpmVx/R6t/hkHDJ3+0xRieYS3BuuVaNgyA=
|
||||
github.com/mulgadc/viperblock v1.0.17 h1:bGRVlXgcqnmH+Xxon03W/ZsbXqu/EE5+/VG3J4E3EBU=
|
||||
github.com/mulgadc/viperblock v1.0.17/go.mod h1:w9V0DjUH5hMXbXsiLbDr9lpqzXBGhEiYB1BeV6TYBkw=
|
||||
github.com/mulgadc/predastore v1.1.4 h1:czeGQWWqmmZuGnUuvyrORavySN/SGPTwjUxBd/9swuU=
|
||||
github.com/mulgadc/predastore v1.1.4/go.mod h1:YP4TaathWczm9weoI8YomZ1/IgnFB9WJowkHPidh6rw=
|
||||
github.com/mulgadc/viperblock v1.1.5 h1:ojmoJQrY7VkDGzdQj07w2F7ICRJX9RlGAKyn87Wa55o=
|
||||
github.com/mulgadc/viperblock v1.1.5/go.mod h1:utoIQLwCmPujOBaT0mL5NhW3m2fwjQPQiQRJH6+Wtk8=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
|
||||
github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
|
||||
github.com/nats-io/nats-server/v2 v2.12.4 h1:ZnT10v2LU2Xcoiy8ek9X6Se4YG8EuMfIfvAEuFVx1Ts=
|
||||
github.com/nats-io/nats-server/v2 v2.12.4/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg=
|
||||
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
|
||||
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
|
||||
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
|
||||
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
|
||||
github.com/nats-io/nats-server/v2 v2.12.6 h1:Egbx9Vl7Ch8wTtpXPGqbehkZ+IncKqShUxvrt1+Enc8=
|
||||
github.com/nats-io/nats-server/v2 v2.12.6/go.mod h1:4HPlrvtmSO3yd7KcElDNMx9kv5EBJBnJJzQPptXlheo=
|
||||
github.com/nats-io/nats.go v1.50.0 h1:5zAeQrTvyrKrWLJ0fu02W3br8ym57qf7csDzgLOpcds=
|
||||
github.com/nats-io/nats.go v1.50.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
|
||||
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
||||
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
||||
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.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
|
||||
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
|
||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU=
|
||||
github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
|
||||
github.com/ovn-kubernetes/libovsdb v0.8.1 h1:M2J8bcJt5mXCom0HqzfEtuHkT80CTSQRcYG7acT8gf4=
|
||||
github.com/ovn-kubernetes/libovsdb v0.8.1/go.mod h1:ZlnHLzagmLOSvyd9qfxBIZp6wOSOw0IsRsc+6lNUGbU=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
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/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
@@ -370,17 +326,11 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
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.24.7 h1:3k5yJnrhT1TTdsG0ZsnenlfCcT+7Y/+zeCPHbL7QAn8=
|
||||
github.com/securego/gosec/v2 v2.24.7/go.mod h1:AdDJbjcG/XxFgVv7pW19vMNYlFM6+Q6Qy3t6lWAUcEY=
|
||||
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/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
@@ -396,7 +346,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
@@ -411,17 +360,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
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/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/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/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
@@ -431,38 +369,28 @@ go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=
|
||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
|
||||
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
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.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
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-20260218203240-3dfff04db8fa h1:6Wi43P0isP8Nl8D4qJmA3VC4FswVH0RJkr5cauo67SQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
|
||||
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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
@@ -477,8 +405,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
|
||||
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.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -487,8 +415,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -517,8 +445,8 @@ 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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20260304144227-18da59047661 h1:AjMsnhVxCTd9HssPcdXPhNTQ0YazDt5+52EqmcWQ0PA=
|
||||
golang.org/x/telemetry v0.0.0-20260304144227-18da59047661/go.mod h1:NuITXsA9cTiqnXtVk+/wrBT2Ja4X5hsfGOYRJ6kgYjs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -526,18 +454,18 @@ golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9sn
|
||||
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.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
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=
|
||||
@@ -552,15 +480,7 @@ 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=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0=
|
||||
google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -589,5 +509,3 @@ 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.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
|
||||
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
func (d *Daemon) handleEC2CreateVpc(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.vpcService.CreateVpc)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEC2DeleteVpc(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.vpcService.DeleteVpc)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEC2DescribeVpcs(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.vpcService.DescribeVpcs)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEC2CreateSubnet(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.vpcService.CreateSubnet)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEC2DeleteSubnet(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.vpcService.DeleteSubnet)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEC2DescribeSubnets(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.vpcService.DescribeSubnets)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEC2CreateNetworkInterface(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.vpcService.CreateNetworkInterface)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEC2DeleteNetworkInterface(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.vpcService.DeleteNetworkInterface)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEC2DescribeNetworkInterfaces(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.vpcService.DescribeNetworkInterfaces)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package gateway_ec2_instance
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mulgadc/hive/hive/awserrors"
|
||||
"github.com/mulgadc/hive/hive/utils"
|
||||
|
||||
handlers_ec2_instance "github.com/mulgadc/hive/hive/handlers/ec2/instance"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
type RunInstancesResponse struct {
|
||||
Reservation *ec2.Reservation `locationName:"RunInstancesResponse"`
|
||||
}
|
||||
|
||||
func ValidateRunInstancesInput(input *ec2.RunInstancesInput) (err error) {
|
||||
if input == nil {
|
||||
return errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
if input.MinCount == nil {
|
||||
return errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
if *input.MinCount == 0 {
|
||||
return errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
if *input.MaxCount == 0 {
|
||||
return errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
// Additional validation from EC2 spec
|
||||
if *input.MinCount > *input.MaxCount {
|
||||
return errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
if input.ImageId == nil || *input.ImageId == "" {
|
||||
return errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
if input.InstanceType == nil || *input.InstanceType == "" {
|
||||
return errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(*input.ImageId, "ami-") {
|
||||
return errors.New(awserrors.ErrorInvalidAMIIDMalformed)
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func RunInstances(input *ec2.RunInstancesInput, natsConn *nats.Conn, accountID string) (reservation ec2.Reservation, err error) {
|
||||
|
||||
// Validate input
|
||||
err = ValidateRunInstancesInput(input)
|
||||
|
||||
if err != nil {
|
||||
return reservation, err
|
||||
}
|
||||
|
||||
// Create NATS-based instance service
|
||||
service := handlers_ec2_instance.NewNATSInstanceService(natsConn)
|
||||
|
||||
// Call the service directly (no need for JSON marshaling/unmarshaling in same process)
|
||||
reservationPtr, err := service.RunInstances(input, accountID)
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrNoResponders) {
|
||||
// ErrNoResponders means no daemon subscribes to ec2.RunInstances.{type}.
|
||||
// This happens when either: (a) the type is unknown, or (b) all nodes
|
||||
// are at capacity. Query DescribeInstanceTypes to differentiate.
|
||||
if !isKnownInstanceType(natsConn, *input.InstanceType) {
|
||||
return reservation, errors.New(awserrors.ErrorInvalidInstanceType)
|
||||
}
|
||||
return reservation, errors.New(awserrors.ErrorInsufficientInstanceCapacity)
|
||||
}
|
||||
return reservation, err
|
||||
}
|
||||
|
||||
// Dereference pointer to return value
|
||||
return *reservationPtr, nil
|
||||
}
|
||||
|
||||
// isKnownInstanceType checks whether any daemon recognizes the given instance type.
|
||||
func isKnownInstanceType(natsConn *nats.Conn, instanceType string) bool {
|
||||
result, err := utils.NATSRequest[ec2.DescribeInstanceTypesOutput](
|
||||
natsConn, "ec2.DescribeInstanceTypes", &ec2.DescribeInstanceTypesInput{}, 3*time.Second, utils.GlobalAccountID)
|
||||
if err != nil || result == nil {
|
||||
return false
|
||||
}
|
||||
for _, t := range result.InstanceTypes {
|
||||
if t.InstanceType != nil && *t.InstanceType == instanceType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package gateway_ec2_instance
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// startTestNATSServer starts an embedded NATS server for testing
|
||||
func startTestNATSServer(t *testing.T) (*server.Server, *nats.Conn) {
|
||||
t.Helper()
|
||||
|
||||
opts := &server.Options{
|
||||
Host: "127.0.0.1",
|
||||
Port: -1,
|
||||
JetStream: false,
|
||||
NoLog: true,
|
||||
NoSigs: true,
|
||||
}
|
||||
|
||||
ns, err := server.NewServer(opts)
|
||||
require.NoError(t, err, "Failed to create NATS server")
|
||||
|
||||
go ns.Start()
|
||||
|
||||
if !ns.ReadyForConnections(5 * time.Second) {
|
||||
t.Fatal("NATS server failed to start")
|
||||
}
|
||||
|
||||
nc, err := nats.Connect(ns.ClientURL())
|
||||
require.NoError(t, err, "Failed to connect to NATS")
|
||||
|
||||
t.Cleanup(func() {
|
||||
nc.Close()
|
||||
ns.Shutdown()
|
||||
})
|
||||
|
||||
return ns, nc
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package gateway_ec2_vpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mulgadc/hive/hive/awserrors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const testAccountID = "123456789012"
|
||||
|
||||
func TestCreateVpc_NilInput(t *testing.T) {
|
||||
_, err := CreateVpc(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestCreateVpc_NilCidrBlock(t *testing.T) {
|
||||
_, err := CreateVpc(&ec2.CreateVpcInput{}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateVpc_EmptyCidrBlock(t *testing.T) {
|
||||
_, err := CreateVpc(&ec2.CreateVpcInput{CidrBlock: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteVpc_NilInput(t *testing.T) {
|
||||
_, err := DeleteVpc(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDeleteVpc_NilVpcId(t *testing.T) {
|
||||
_, err := DeleteVpc(&ec2.DeleteVpcInput{}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteVpc_EmptyVpcId(t *testing.T) {
|
||||
_, err := DeleteVpc(&ec2.DeleteVpcInput{VpcId: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_NilInput(t *testing.T) {
|
||||
_, err := CreateSubnet(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_NilVpcId(t *testing.T) {
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{CidrBlock: aws.String("10.0.0.0/24")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_EmptyVpcId(t *testing.T) {
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{VpcId: aws.String(""), CidrBlock: aws.String("10.0.0.0/24")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_NilCidrBlock(t *testing.T) {
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{VpcId: aws.String("vpc-123")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_EmptyCidrBlock(t *testing.T) {
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{VpcId: aws.String("vpc-123"), CidrBlock: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteSubnet_NilInput(t *testing.T) {
|
||||
_, err := DeleteSubnet(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDeleteSubnet_NilSubnetId(t *testing.T) {
|
||||
_, err := DeleteSubnet(&ec2.DeleteSubnetInput{}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteSubnet_EmptySubnetId(t *testing.T) {
|
||||
_, err := DeleteSubnet(&ec2.DeleteSubnetInput{SubnetId: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateNetworkInterface_NilInput(t *testing.T) {
|
||||
_, err := CreateNetworkInterface(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestCreateNetworkInterface_NilSubnetId(t *testing.T) {
|
||||
_, err := CreateNetworkInterface(&ec2.CreateNetworkInterfaceInput{}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateNetworkInterface_EmptySubnetId(t *testing.T) {
|
||||
_, err := CreateNetworkInterface(&ec2.CreateNetworkInterfaceInput{SubnetId: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteNetworkInterface_NilInput(t *testing.T) {
|
||||
_, err := DeleteNetworkInterface(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDeleteNetworkInterface_NilNetworkInterfaceId(t *testing.T) {
|
||||
_, err := DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteNetworkInterface_EmptyNetworkInterfaceId(t *testing.T) {
|
||||
_, err := DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{NetworkInterfaceId: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
// Handler tests with valid input + nil NATS — covers NATS error paths
|
||||
|
||||
func TestCreateVpc_NilNATS(t *testing.T) {
|
||||
_, err := CreateVpc(&ec2.CreateVpcInput{CidrBlock: aws.String("10.0.0.0/16")}, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteVpc_NilNATS(t *testing.T) {
|
||||
_, err := DeleteVpc(&ec2.DeleteVpcInput{VpcId: aws.String("vpc-123")}, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_NilNATS(t *testing.T) {
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{VpcId: aws.String("vpc-123"), CidrBlock: aws.String("10.0.1.0/24")}, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteSubnet_NilNATS(t *testing.T) {
|
||||
_, err := DeleteSubnet(&ec2.DeleteSubnetInput{SubnetId: aws.String("subnet-123")}, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCreateNetworkInterface_NilNATS(t *testing.T) {
|
||||
_, err := CreateNetworkInterface(&ec2.CreateNetworkInterfaceInput{SubnetId: aws.String("subnet-123")}, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteNetworkInterface_NilNATS(t *testing.T) {
|
||||
_, err := DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{NetworkInterfaceId: aws.String("eni-123")}, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDescribeVpcs_NilNATS(t *testing.T) {
|
||||
_, err := DescribeVpcs(nil, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = DescribeVpcs(&ec2.DescribeVpcsInput{}, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDescribeSubnets_NilNATS(t *testing.T) {
|
||||
_, err := DescribeSubnets(nil, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = DescribeSubnets(&ec2.DescribeSubnetsInput{}, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDescribeNetworkInterfaces_NilNATS(t *testing.T) {
|
||||
_, err := DescribeNetworkInterfaces(nil, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{}, nil, testAccountID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package handlers_ec2_vpc
|
||||
|
||||
import "github.com/aws/aws-sdk-go/service/ec2"
|
||||
|
||||
// VPCService defines the interface for VPC, Subnet, and ENI operations
|
||||
type VPCService interface {
|
||||
CreateVpc(input *ec2.CreateVpcInput, accountID string) (*ec2.CreateVpcOutput, error)
|
||||
DeleteVpc(input *ec2.DeleteVpcInput, accountID string) (*ec2.DeleteVpcOutput, error)
|
||||
DescribeVpcs(input *ec2.DescribeVpcsInput, accountID string) (*ec2.DescribeVpcsOutput, error)
|
||||
CreateSubnet(input *ec2.CreateSubnetInput, accountID string) (*ec2.CreateSubnetOutput, error)
|
||||
DeleteSubnet(input *ec2.DeleteSubnetInput, accountID string) (*ec2.DeleteSubnetOutput, error)
|
||||
DescribeSubnets(input *ec2.DescribeSubnetsInput, accountID string) (*ec2.DescribeSubnetsOutput, error)
|
||||
CreateNetworkInterface(input *ec2.CreateNetworkInterfaceInput, accountID string) (*ec2.CreateNetworkInterfaceOutput, error)
|
||||
DeleteNetworkInterface(input *ec2.DeleteNetworkInterfaceInput, accountID string) (*ec2.DeleteNetworkInterfaceOutput, error)
|
||||
DescribeNetworkInterfaces(input *ec2.DescribeNetworkInterfacesInput, accountID string) (*ec2.DescribeNetworkInterfacesOutput, error)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package handlers_ec2_vpc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mulgadc/hive/hive/utils"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// NATSVPCService handles VPC and Subnet operations via NATS messaging
|
||||
type NATSVPCService struct {
|
||||
natsConn *nats.Conn
|
||||
}
|
||||
|
||||
// NewNATSVPCService creates a new NATS-based VPC service
|
||||
func NewNATSVPCService(conn *nats.Conn) VPCService {
|
||||
return &NATSVPCService{natsConn: conn}
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) CreateVpc(input *ec2.CreateVpcInput, accountID string) (*ec2.CreateVpcOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateVpcOutput](s.natsConn, "ec2.CreateVpc", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) DeleteVpc(input *ec2.DeleteVpcInput, accountID string) (*ec2.DeleteVpcOutput, error) {
|
||||
return utils.NATSRequest[ec2.DeleteVpcOutput](s.natsConn, "ec2.DeleteVpc", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) DescribeVpcs(input *ec2.DescribeVpcsInput, accountID string) (*ec2.DescribeVpcsOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeVpcsOutput](s.natsConn, "ec2.DescribeVpcs", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) CreateSubnet(input *ec2.CreateSubnetInput, accountID string) (*ec2.CreateSubnetOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateSubnetOutput](s.natsConn, "ec2.CreateSubnet", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) DeleteSubnet(input *ec2.DeleteSubnetInput, accountID string) (*ec2.DeleteSubnetOutput, error) {
|
||||
return utils.NATSRequest[ec2.DeleteSubnetOutput](s.natsConn, "ec2.DeleteSubnet", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) DescribeSubnets(input *ec2.DescribeSubnetsInput, accountID string) (*ec2.DescribeSubnetsOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeSubnetsOutput](s.natsConn, "ec2.DescribeSubnets", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) CreateNetworkInterface(input *ec2.CreateNetworkInterfaceInput, accountID string) (*ec2.CreateNetworkInterfaceOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateNetworkInterfaceOutput](s.natsConn, "ec2.CreateNetworkInterface", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) DeleteNetworkInterface(input *ec2.DeleteNetworkInterfaceInput, accountID string) (*ec2.DeleteNetworkInterfaceOutput, error) {
|
||||
return utils.NATSRequest[ec2.DeleteNetworkInterfaceOutput](s.natsConn, "ec2.DeleteNetworkInterface", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) DescribeNetworkInterfaces(input *ec2.DescribeNetworkInterfacesInput, accountID string) (*ec2.DescribeNetworkInterfacesOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeNetworkInterfacesOutput](s.natsConn, "ec2.DescribeNetworkInterfaces", input, 30*time.Second, accountID)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
24.12.0
|
||||
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"extends": ["ultracite/core", "ultracite/remix", "ultracite/react"],
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"useSortedProperties": "off",
|
||||
"useSortedInterfaceMembers": "off",
|
||||
"organizeImports": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"groups": [
|
||||
":URL:",
|
||||
":BLANK_LINE:",
|
||||
[":BUN:", ":NODE:"],
|
||||
":BLANK_LINE:",
|
||||
[":PACKAGE:", ":PACKAGE_WITH_PROTOCOL:"],
|
||||
":BLANK_LINE:",
|
||||
[":ALIAS:", ":PATH:"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"semicolons": "asNeeded",
|
||||
"trailingCommas": "all"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["**/*.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useFilenamingConvention": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"filenameCases": ["camelCase"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/components/ui/*.tsx"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"performance": {
|
||||
"noNamespaceImport": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
import{c as B,F as $,G as F,y as C,j as s,x as K,H as b,I as k,L}from"./index.js";import{u as E}from"./useSuspenseQuery.js";import{B as O}from"./back-link.js";import{E as S}from"./error-banner.js";import{a as U}from"./s3.js";import{U as A,F as I,O as R}from"./upload-button.js";import"./delete-confirmation-dialog.js";import"./alert-dialog.js";import"./download.js";import"./trash-2.js";function V(){const e=B.c(24),{bucket:n,_splat:v}=$.useParams(),r=F(v||""),{data:m}=E(C(n,r)),a=U();let d;e[0]!==m.Contents?(d=m.Contents||[],e[0]=m.Contents,e[1]=d):d=e[1];const o=d;let p;e[2]!==m.CommonPrefixes?(p=m.CommonPrefixes||[],e[2]=m.CommonPrefixes,e[3]=p):p=e[3];const i=p,u=r.split("/").filter(Boolean),y=[{name:n,path:`/s3/ls/${n}`},...u.map((t,l)=>{const c=`${u.slice(0,l+1).join("/")}/`;return{name:t,path:`/s3/ls/${n}/${c}`}})],P=u.length>1?`/s3/ls/${n}/${u.slice(0,-1).join("/")}/`:`/s3/ls/${n}`;let x;e[4]!==P?(x=s.jsx(O,{to:P,children:"Back"}),e[4]=P,e[5]=x):x=e[5];let f;e[6]!==n||e[7]!==r||e[8]!==a.isPending||e[9]!==a.mutateAsync?(f=s.jsx(A,{bucket:n,isPending:a.isPending,onUpload:a.mutateAsync,prefix:r}),e[6]=n,e[7]=r,e[8]=a.isPending,e[9]=a.mutateAsync,e[10]=f):f=e[10];let h;e[11]!==a.error?(h=a.error&&s.jsx(S,{error:a.error,msg:"Failed to upload object"}),e[11]=a.error,e[12]=h):h=e[12];let g;e[13]!==n||e[14]!==i||e[15]!==r?(g=i.length>0&&s.jsxs("div",{className:"mb-6",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Folders"}),s.jsx("div",{className:"space-y-2",children:i.map(t=>{if(!t.Prefix)return null;const l=K(b(t.Prefix,r)),c=k(t.Prefix,r);return s.jsx(I,{bucketName:n,folderName:l,fullPrefix:c},t.Prefix)})})]}),e[13]=n,e[14]=i,e[15]=r,e[16]=g):g=e[16];let j;e[17]!==n||e[18]!==o||e[19]!==r?(j=o.length>0&&s.jsxs("div",{className:"space-y-2",children:[s.jsx("h2",{className:"mb-3 font-semibold text-lg",children:"Objects"}),o.map(t=>{if(!t.Key)return null;const l=b(t.Key,r),c=k(t.Key,r);return s.jsx(R,{bucketName:n,displayName:l,fullKey:c,object:t},t.Key)})]}),e[17]=n,e[18]=o,e[19]=r,e[20]=j):j=e[20];let N;return e[21]!==i.length||e[22]!==o.length?(N=o.length===0&&i.length===0&&s.jsx("p",{className:"text-muted-foreground",children:"No objects found in this folder."}),e[21]=i.length,e[22]=o.length,e[23]=N):N=e[23],s.jsxs(s.Fragment,{children:[x,s.jsx("div",{className:"mb-6",children:s.jsxs("div",{className:"mb-2 flex items-center justify-between",children:[s.jsx("div",{className:"flex items-center gap-2 text-muted-foreground text-sm",children:y.map((t,l)=>s.jsxs("div",{className:"flex items-center gap-2",children:[l>0&&s.jsx("span",{children:"/"}),l===y.length-1?s.jsx("span",{className:"text-foreground",children:t.name}):s.jsx(L,{className:"hover:text-foreground",to:t.path,children:t.name})]},t.path))}),f]})}),h,g,j,N]})}export{V as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{aJ as D,u as f,aK as v,v as b,r as I,j as e,B as C,L as g}from"./index.js";import{u as d}from"./useSuspenseQuery.js";import{B as u}from"./back-link.js";import{D as V}from"./delete-confirmation-dialog.js";import{D as n,a}from"./detail-row.js";import{E as S}from"./error-banner.js";import{P as y}from"./page-heading.js";import{S as P}from"./state-badge.js";import{g as B}from"./ec2.js";import{T as k}from"./trash-2.js";import"./alert-dialog.js";function Q(){const{id:l}=D.useParams(),m=f(),{data:p}=d(v(l)),{data:x}=d(b),s=p.Vpcs?.[0],r=B(),[j,i]=I.useState(!1),o=x.Subnets?.filter(t=>t.VpcId===l)??[],h=async()=>{try{await r.mutateAsync(l),m({to:"/ec2/describe-vpcs"})}finally{i(!1)}};if(!s?.VpcId)return e.jsxs(e.Fragment,{children:[e.jsx(u,{to:"/ec2/describe-vpcs",children:"Back to VPCs"}),e.jsx("p",{className:"text-muted-foreground",children:"VPC not found."})]});const c=s.Tags?.find(t=>t.Key==="Name")?.Value;return e.jsxs(e.Fragment,{children:[e.jsx(u,{to:"/ec2/describe-vpcs",children:"Back to VPCs"}),r.error&&e.jsx(S,{error:r.error,msg:"Failed to delete VPC"}),e.jsxs("div",{className:"space-y-6",children:[e.jsx(y,{actions:e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsxs(C,{onClick:()=>i(!0),size:"sm",variant:"destructive",children:[e.jsx(k,{className:"size-4"}),"Delete"]}),e.jsx(P,{state:s.State})]}),subtitle:"VPC Details",title:c?`${s.VpcId} (${c})`:s.VpcId}),e.jsxs(n,{children:[e.jsx(n.Header,{children:"VPC Information"}),e.jsxs(n.Content,{children:[e.jsx(a,{label:"VPC ID",value:s.VpcId}),e.jsx(a,{label:"State",value:s.State}),e.jsx(a,{label:"CIDR Block",value:s.CidrBlock}),e.jsx(a,{label:"Is Default",value:s.IsDefault?"Yes":"No"}),e.jsx(a,{label:"DHCP Options ID",value:s.DhcpOptionsId}),e.jsx(a,{label:"Instance Tenancy",value:s.InstanceTenancy}),e.jsx(a,{label:"Owner ID",value:s.OwnerId})]})]}),o.length>0&&e.jsxs(n,{children:[e.jsxs(n.Header,{children:["Subnets (",o.length,")"]}),o.map(t=>t.SubnetId?e.jsxs(n.Content,{children:[e.jsx(a,{label:"Subnet ID",value:e.jsx(g,{className:"text-primary hover:underline",params:{id:t.SubnetId},to:"/ec2/describe-subnets/$id",children:t.SubnetId})}),e.jsx(a,{label:"CIDR Block",value:t.CidrBlock}),e.jsx(a,{label:"Availability Zone",value:t.AvailabilityZone}),e.jsx(a,{label:"State",value:t.State})]},t.SubnetId):null)]})]}),e.jsx(V,{description:`Are you sure you want to delete the VPC "${s.VpcId}"? This action cannot be undone. The VPC must have no associated subnets or internet gateways.`,isPending:r.isPending,onConfirm:h,onOpenChange:i,open:j,title:"Delete VPC"})]})}export{Q as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{c as A,aL as G,u as C,j as s,aM as Q}from"./index.js";import{a as H,u as O}from"./zod.js";import{u as T}from"./useSuspenseQuery.js";import{B as L}from"./back-link.js";import{E as q}from"./error-banner.js";import{F as D}from"./form-actions.js";import{P as J}from"./page-heading.js";import{F as K,a as U,b as W}from"./field.js";import{I as X}from"./input.js";import{h as Y}from"./ec2.js";import{m as Z}from"./ec22.js";function ue(){const e=A.c(53),{id:N}=G.useParams(),l=C();let v;e[0]!==N?(v=Q(N),e[0]=N,e[1]=v):v=e[1];const{data:M}=T(v),I=M.Volumes?.[0],t=I?.VolumeId,r=Y(),i=I?.Size??1;let m;e[2]!==i?(m=H(Z.refine(o=>o.size>i,{message:`New size must be greater than current size (${i} GiB)`,path:["size"]})),e[2]=i,e[3]=m):m=e[3];let a;e[4]!==i?(a={size:i},e[4]=i,e[5]=a):a=e[5];let y;e[6]!==m||e[7]!==a?(y={resolver:m,defaultValues:a},e[6]=m,e[7]=a,e[8]=y):y=e[8];const{handleSubmit:P,register:R,formState:_}=O(y),{errors:F,isSubmitting:E}=_;if(!t){let o;return e[9]===Symbol.for("react.memo_cache_sentinel")?(o=s.jsxs(s.Fragment,{children:[s.jsx(L,{to:"/ec2/describe-volumes",children:"Back to volumes"}),s.jsx("p",{className:"text-muted-foreground",children:"Volume not found."})]}),e[9]=o):o=e[9],o}let S;e[10]!==r||e[11]!==l||e[12]!==t?(S=async o=>{await r.mutateAsync({...o,volumeId:t}),l({to:"/ec2/describe-volumes/$id",params:{id:t}})},e[10]=r,e[11]=l,e[12]=t,e[13]=S):S=e[13];const $=S;let n,c;e[14]!==t?(n=s.jsx(L,{params:{id:t},to:"/ec2/describe-volumes/$id",children:"Back to volume"}),c=s.jsx(J,{subtitle:t,title:"Resize Volume"}),e[14]=t,e[15]=n,e[16]=c):(n=e[15],c=e[16]);let u;e[17]!==r.error?(u=r.error&&s.jsx(q,{error:r.error,msg:"Failed to resize volume"}),e[17]=r.error,e[18]=u):u=e[18];let d;e[19]!==P||e[20]!==$?(d=P($),e[19]=P,e[20]=$,e[21]=d):d=e[21];let f;e[22]!==i?(f=s.jsx("div",{className:"space-y-2",children:s.jsxs("p",{className:"text-muted-foreground text-sm",children:["Current size: ",i," GiB"]})}),e[22]=i,e[23]=f):f=e[23];let V;e[24]===Symbol.for("react.memo_cache_sentinel")?(V=s.jsx(U,{children:s.jsx("label",{htmlFor:"size",children:"New size (GiB)"})}),e[24]=V):V=e[24];const k=!!F.size,w=i+1;let p;e[25]!==R?(p=R("size",{valueAsNumber:!0}),e[25]=R,e[26]=p):p=e[26];let x;e[27]!==k||e[28]!==w||e[29]!==p?(x=s.jsx(X,{"aria-invalid":k,id:"size",min:w,type:"number",...p}),e[27]=k,e[28]=w,e[29]=p,e[30]=x):x=e[30];let z;e[31]!==F.size?(z=s.jsx(K,{errors:[F.size]}),e[31]=F.size,e[32]=z):z=e[32];let b;e[33]!==x||e[34]!==z?(b=s.jsxs(W,{children:[V,x,z]}),e[33]=x,e[34]=z,e[35]=b):b=e[35];let g;e[36]!==l||e[37]!==t?(g=()=>l({to:"/ec2/describe-volumes/$id",params:{id:t}}),e[36]=l,e[37]=t,e[38]=g):g=e[38];let h;e[39]!==E||e[40]!==r.isPending||e[41]!==g?(h=s.jsx(D,{isPending:r.isPending,isSubmitting:E,onCancel:g,pendingLabel:"Resizing\\u2026",submitLabel:"Resize Volume"}),e[39]=E,e[40]=r.isPending,e[41]=g,e[42]=h):h=e[42];let j;e[43]!==f||e[44]!==b||e[45]!==h||e[46]!==d?(j=s.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:d,children:[f,b,h]}),e[43]=f,e[44]=b,e[45]=h,e[46]=d,e[47]=j):j=e[47];let B;return e[48]!==j||e[49]!==n||e[50]!==c||e[51]!==u?(B=s.jsxs(s.Fragment,{children:[n,c,u,j]}),e[48]=j,e[49]=n,e[50]=c,e[51]=u,e[52]=B):B=e[52],B}export{ue as component};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{cX as p,u as x,cY as j,r as f,j as e,B as S,L as h}from"./index.js";import{u as D}from"./useSuspenseQuery.js";import{B as l}from"./back-link.js";import{D as v}from"./delete-confirmation-dialog.js";import{D as o,a as s}from"./detail-row.js";import{E as g}from"./error-banner.js";import{P as I}from"./page-heading.js";import{S as y}from"./state-badge.js";import{l as C}from"./ec2.js";import{T as B}from"./trash-2.js";import"./alert-dialog.js";function $(){const{id:r}=p.useParams(),u=x(),{data:c}=D(j(r)),t=c.Subnets?.[0],a=C(),[d,n]=f.useState(!1),m=async()=>{try{await a.mutateAsync(r),u({to:"/ec2/describe-subnets"})}finally{n(!1)}};if(!t?.SubnetId)return e.jsxs(e.Fragment,{children:[e.jsx(l,{to:"/ec2/describe-subnets",children:"Back to Subnets"}),e.jsx("p",{className:"text-muted-foreground",children:"Subnet not found."})]});const i=t.Tags?.find(b=>b.Key==="Name")?.Value;return e.jsxs(e.Fragment,{children:[e.jsx(l,{to:"/ec2/describe-subnets",children:"Back to Subnets"}),a.error&&e.jsx(g,{error:a.error,msg:"Failed to delete subnet"}),e.jsxs("div",{className:"space-y-6",children:[e.jsx(I,{actions:e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsxs(S,{onClick:()=>n(!0),size:"sm",variant:"destructive",children:[e.jsx(B,{className:"size-4"}),"Delete"]}),e.jsx(y,{state:t.State})]}),subtitle:"Subnet Details",title:i?`${t.SubnetId} (${i})`:t.SubnetId}),e.jsxs(o,{children:[e.jsx(o.Header,{children:"Subnet Information"}),e.jsxs(o.Content,{children:[e.jsx(s,{label:"Subnet ID",value:t.SubnetId}),e.jsx(s,{label:"VPC ID",value:t.VpcId?e.jsx(h,{className:"text-primary hover:underline",params:{id:t.VpcId},to:"/ec2/describe-vpcs/$id",children:t.VpcId}):"—"}),e.jsx(s,{label:"State",value:t.State}),e.jsx(s,{label:"CIDR Block",value:t.CidrBlock}),e.jsx(s,{label:"Availability Zone",value:t.AvailabilityZone}),e.jsx(s,{label:"Available IP Count",value:t.AvailableIpAddressCount?.toString()}),e.jsx(s,{label:"Map Public IP on Launch",value:t.MapPublicIpOnLaunch?"Yes":"No"}),e.jsx(s,{label:"Default for AZ",value:t.DefaultForAz?"Yes":"No"})]})]})]}),e.jsx(v,{description:`Are you sure you want to delete the subnet "${t.SubnetId}"? This action cannot be undone.`,isPending:a.isPending,onConfirm:m,onOpenChange:n,open:d,title:"Delete Subnet"})]})}export{$ as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{J as w,cZ as k,u as N,c_ as B,f as P,r as l,j as e,i as T,B as m,L as F}from"./index.js";import{u}from"./useSuspenseQuery.js";import{B as x}from"./back-link.js";import{D as R}from"./delete-confirmation-dialog.js";import{D as c,a as t}from"./detail-row.js";import{E as g}from"./error-banner.js";import{P as z}from"./page-heading.js";import{S as E}from"./state-badge.js";import{g as V,f as O,b as L,a as H,c as M,d as Q,e as _,h as $}from"./alert-dialog.js";import{b as G,a as J}from"./field.js";import{I as Y}from"./input.js";import{m as Z,n as q}from"./ec2.js";import{T as K}from"./trash-2.js";const U=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]],W=w("copy",U);function he(){const{id:n}=k.useParams(),j=N(),{data:D}=u(B(n)),{data:y}=u(P),s=D.Snapshots?.[0],a=Z(),o=q(),[S,i]=l.useState(!1),[f,r]=l.useState(!1),[p,d]=l.useState(""),C=y.Regions?.[0]?.RegionName??"ap-southeast-2",h=s?.State==="completed",v=async()=>{try{await a.mutateAsync(n),j({to:"/ec2/describe-snapshots"})}finally{i(!1)}},b=async()=>{try{await o.mutateAsync({sourceSnapshotId:n,sourceRegion:C,description:p||void 0}),r(!1),d("")}catch{}};if(!s?.SnapshotId)return e.jsxs(e.Fragment,{children:[e.jsx(x,{to:"/ec2/describe-snapshots",children:"Back to snapshots"}),e.jsx("p",{className:"text-muted-foreground",children:"Snapshot not found."})]});const I=T(s.StartTime);return e.jsxs(e.Fragment,{children:[e.jsx(x,{to:"/ec2/describe-snapshots",children:"Back to snapshots"}),a.error&&e.jsx(g,{error:a.error,msg:"Failed to delete snapshot"}),o.error&&e.jsx(g,{error:o.error,msg:"Failed to copy snapshot"}),e.jsxs("div",{className:"space-y-6",children:[e.jsx(z,{actions:e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsxs(m,{disabled:!h,onClick:()=>i(!0),size:"sm",variant:"destructive",children:[e.jsx(K,{className:"size-4"}),"Delete"]}),e.jsxs(m,{disabled:!h,onClick:()=>r(!0),size:"sm",variant:"outline",children:[e.jsx(W,{className:"size-4"}),"Copy Snapshot"]}),e.jsx(E,{state:s.State})]}),subtitle:"EC2 Snapshot Details",title:s.SnapshotId}),e.jsxs(c,{children:[e.jsx(c.Header,{children:"Snapshot Information"}),e.jsxs(c.Content,{children:[e.jsx(t,{label:"Snapshot ID",value:s.SnapshotId}),e.jsx(t,{label:"Volume ID",value:s.VolumeId?e.jsx(F,{className:"text-primary hover:underline",params:{id:s.VolumeId},to:"/ec2/describe-volumes/$id",children:s.VolumeId}):void 0}),e.jsx(t,{label:"Volume Size",value:`${s.VolumeSize} GiB`}),e.jsx(t,{label:"State",value:s.State}),e.jsx(t,{label:"Description",value:s.Description}),e.jsx(t,{label:"Progress",value:s.Progress}),e.jsx(t,{label:"Start Time",value:I}),e.jsx(t,{label:"Owner ID",value:s.OwnerId}),e.jsx(t,{label:"Encrypted",value:s.Encrypted?"Yes":"No"})]})]})]}),e.jsx(R,{description:`Are you sure you want to delete the snapshot "${s.SnapshotId}"? This action cannot be undone.`,isPending:a.isPending,onConfirm:v,onOpenChange:i,open:S,title:"Delete Snapshot"}),e.jsx(V,{onOpenChange:r,open:f,children:e.jsxs(O,{children:[e.jsxs(L,{children:[e.jsx(H,{children:"Copy Snapshot"}),e.jsx(M,{children:"Create a copy of this snapshot in the current region."})]}),e.jsxs(G,{children:[e.jsx(J,{children:e.jsx("label",{htmlFor:"copyDescription",children:"Description (optional)"})}),e.jsx(Y,{id:"copyDescription",onChange:A=>d(A.target.value),placeholder:"Description for the snapshot copy",value:p})]}),e.jsxs(Q,{children:[e.jsx(_,{children:"Cancel"}),e.jsx($,{disabled:o.isPending,onClick:b,children:o.isPending?"Copying…":"Copy Snapshot"})]})]})})]})}export{he as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{c$ as p,u as x,d0 as j,r as h,j as e,i as g,B as K}from"./index.js";import{u as f}from"./useSuspenseQuery.js";import{B as l}from"./back-link.js";import{D}from"./delete-confirmation-dialog.js";import{D as s,a as r}from"./detail-row.js";import{E as P}from"./error-banner.js";import{P as b}from"./page-heading.js";import{o as k}from"./ec2.js";import{T}from"./trash-2.js";import"./alert-dialog.js";function R(){const{id:o}=p.useParams(),c=x(),{data:m}=f(j(o)),a=m.KeyPairs?.[0],t=k(),[y,i]=h.useState(!1),d=async()=>{try{await t.mutateAsync(o),c({to:"/ec2/describe-key-pairs"})}finally{i(!1)}};if(!a?.KeyPairId)return e.jsxs(e.Fragment,{children:[e.jsx(l,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),e.jsx("p",{className:"text-muted-foreground",children:"Key pair not found."})]});const u=g(a.CreateTime);return e.jsxs(e.Fragment,{children:[e.jsx(l,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),t.error&&e.jsx(P,{error:t.error,msg:"Failed to delete key pair"}),e.jsxs("div",{className:"space-y-6",children:[e.jsx(b,{actions:e.jsxs(K,{onClick:()=>i(!0),size:"sm",variant:"destructive",children:[e.jsx(T,{className:"size-4"}),"Delete"]}),subtitle:"EC2 Key Pair Details",title:a.KeyName??""}),e.jsxs(s,{children:[e.jsx(s.Header,{children:"Key Pair Information"}),e.jsxs(s.Content,{children:[e.jsx(r,{label:"Key Name",value:a.KeyName}),e.jsx(r,{label:"Key Pair ID",value:a.KeyPairId}),e.jsx(r,{label:"Key Type",value:a.KeyType}),e.jsx(r,{label:"Key Fingerprint",value:a.KeyFingerprint}),e.jsx(r,{label:"Creation Date",value:u})]})]}),a.Tags&&a.Tags.length>0&&e.jsxs(s,{children:[e.jsx(s.Header,{children:"Tags"}),e.jsx(s.Content,{children:a.Tags.map(n=>e.jsx(r,{label:n.Key||"",value:n.Value},n.Key))})]})]}),e.jsx(D,{description:`Are you sure you want to delete the key pair "${a.KeyName}"? This action cannot be undone. Any instances using this key pair will no longer be able to be accessed with it.`,isPending:t.isPending,onConfirm:d,onOpenChange:i,open:y,title:"Delete Key Pair"})]})}export{R as component};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{c as f,d4 as u,j as t,d3 as p}from"./index.js";import{u as x}from"./useSuspenseQuery.js";import{B as d}from"./back-link.js";import{P as I}from"./page-heading.js";import{S as j}from"./state-badge.js";import{A as h}from"./ami-details.js";import"./detail-row.js";function _(){const e=f.c(14),{id:n}=u.useParams();let o;e[0]!==n?(o=p(n),e[0]=n,e[1]=o):o=e[1];const{data:g}=x(o),s=g?.Images?.[0];if(!s?.ImageId){let l;return e[2]===Symbol.for("react.memo_cache_sentinel")?(l=t.jsxs(t.Fragment,{children:[t.jsx(d,{to:"/ec2/describe-images",children:"Back to images"}),t.jsx("p",{className:"text-muted-foreground",children:"Image not found."})]}),e[2]=l):l=e[2],l}let r;e[3]===Symbol.for("react.memo_cache_sentinel")?(r=t.jsx(d,{to:"/ec2/describe-images",children:"Back to images"}),e[3]=r):r=e[3];let a;e[4]!==s.State?(a=t.jsx(j,{state:s.State}),e[4]=s.State,e[5]=a):a=e[5];let i;e[6]!==s.ImageId||e[7]!==a?(i=t.jsx(I,{actions:a,subtitle:"EC2 Image Details",title:s.ImageId}),e[6]=s.ImageId,e[7]=a,e[8]=i):i=e[8];let m;e[9]!==s?(m=t.jsx(h,{image:s,showExtendedDetails:!0}),e[9]=s,e[10]=m):m=e[10];let c;return e[11]!==i||e[12]!==m?(c=t.jsxs(t.Fragment,{children:[r,t.jsxs("div",{className:"space-y-6",children:[i,m]})]}),e[11]=i,e[12]=m,e[13]=c):c=e[13],c}export{_ as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{c as f,j as e,av as j,u as P,aw as h,r as v,ax as g,B as N,i as b}from"./index.js";import{u}from"./useSuspenseQuery.js";import{B as p}from"./back-link.js";import{D as C}from"./delete-confirmation-dialog.js";import{D as l,a as n}from"./detail-row.js";import{E as w}from"./error-banner.js";import{P as A}from"./page-heading.js";import{h as I}from"./iam.js";import{T as B}from"./trash-2.js";import"./alert-dialog.js";function R(r){const t=f.c(4),{document:a}=r;let s;try{let i;t[0]!==a?(i=JSON.stringify(JSON.parse(a),null,2),t[0]=a,t[1]=i):i=t[1],s=i}catch{s=a}let o;return t[2]!==s?(o=e.jsx("pre",{className:"overflow-auto rounded-md border bg-muted p-4 font-mono text-sm",children:s}),t[2]=s,t[3]=o):o=t[3],o}function J(){const{policyArn:r}=j.useParams(),t=decodeURIComponent(r),a=P(),{data:s}=u(h(t)),o=s.Policy,i=I(),[y,c]=v.useState(!1),x=o?.DefaultVersionId??"v1",{data:m}=u(g(t,x)),d=m?.PolicyVersion?.Document?decodeURIComponent(m.PolicyVersion.Document):null,D=async()=>{try{await i.mutateAsync(t),a({to:"/iam/list-policies"})}finally{c(!1)}};return o?e.jsxs(e.Fragment,{children:[e.jsx(p,{to:"/iam/list-policies",children:"Back to policies"}),i.error&&e.jsx(w,{error:i.error,msg:"Failed to delete policy"}),e.jsxs("div",{className:"space-y-6",children:[e.jsx(A,{actions:e.jsxs(N,{onClick:()=>c(!0),size:"sm",variant:"destructive",children:[e.jsx(B,{className:"size-4"}),"Delete"]}),subtitle:"IAM Policy Details",title:o.PolicyName??""}),e.jsxs(l,{children:[e.jsx(l.Header,{children:"Policy Information"}),e.jsxs(l.Content,{children:[e.jsx(n,{label:"Policy Name",value:o.PolicyName}),e.jsx(n,{label:"Policy ID",value:o.PolicyId}),e.jsx(n,{label:"ARN",value:o.Arn}),e.jsx(n,{label:"Path",value:o.Path}),e.jsx(n,{label:"Description",value:o.Description||"-"}),e.jsx(n,{label:"Created",value:b(o.CreateDate)})]})]}),d&&e.jsxs(l,{children:[e.jsx(l.Header,{children:"Policy Document"}),e.jsx("div",{className:"p-4",children:e.jsx(R,{document:d})})]})]}),e.jsx(C,{description:`Are you sure you want to delete the policy "${o.PolicyName}"? This action cannot be undone.`,isPending:i.isPending,onConfirm:D,onOpenChange:c,open:y,title:"Delete Policy"})]}):e.jsxs(e.Fragment,{children:[e.jsx(p,{to:"/iam/list-policies",children:"Back to policies"}),e.jsx("p",{className:"text-muted-foreground",children:"Policy not found."})]})}export{J as component};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{c as g,i as k,j as t,L as M}from"./index.js";import{D as T,a}from"./detail-row.js";function C(s){const e=g.c(58),{image:l,showExtendedDetails:P}=s,i=P===void 0?!1:P;let E;e[0]!==l.CreationDate?(E=k(l.CreationDate),e[0]=l.CreationDate,e[1]=E):E=e[1];const R=E,S=i?"AMI Information":"AMI Details";let o;e[2]!==S?(o=t.jsx(T.Header,{children:S}),e[2]=S,e[3]=o):o=e[3];let r;e[4]!==l.ImageId||e[5]!==i?(r=l.ImageId&&!i?t.jsx(M,{className:"text-primary hover:underline",params:{id:l.ImageId},to:"/ec2/describe-images/$id",children:l.ImageId}):l.ImageId,e[4]=l.ImageId,e[5]=i,e[6]=r):r=e[6];let c;e[7]!==r?(c=t.jsx(a,{label:"AMI ID",value:r}),e[7]=r,e[8]=c):c=e[8];let n;e[9]!==l.Name?(n=t.jsx(a,{label:"AMI Name",value:l.Name}),e[9]=l.Name,e[10]=n):n=e[10];let u;e[11]!==l.Description?(u=t.jsx(a,{label:"Description",value:l.Description}),e[11]=l.Description,e[12]=u):u=e[12];let m;e[13]!==l.Architecture?(m=t.jsx(a,{label:"Architecture",value:l.Architecture}),e[13]=l.Architecture,e[14]=m):m=e[14];let v;e[15]!==l.VirtualizationType?(v=t.jsx(a,{label:"Virtualization Type",value:l.VirtualizationType}),e[15]=l.VirtualizationType,e[16]=v):v=e[16];const A=l.Platform||"Linux";let D;e[17]!==A?(D=t.jsx(a,{label:"Platform",value:A}),e[17]=A,e[18]=D):D=e[18];let d;e[19]!==l.PlatformDetails?(d=t.jsx(a,{label:"Platform Details",value:l.PlatformDetails}),e[19]=l.PlatformDetails,e[20]=d):d=e[20];let x;e[21]!==l.ImageType?(x=t.jsx(a,{label:"Image Type",value:l.ImageType}),e[21]=l.ImageType,e[22]=x):x=e[22];let b;e[23]!==l.RootDeviceType?(b=t.jsx(a,{label:"Root Device Type",value:l.RootDeviceType}),e[23]=l.RootDeviceType,e[24]=b):b=e[24];let j;e[25]!==l.RootDeviceName?(j=t.jsx(a,{label:"Root Device Name",value:l.RootDeviceName}),e[25]=l.RootDeviceName,e[26]=j):j=e[26];let f;e[27]!==l.OwnerId?(f=t.jsx(a,{label:"Owner",value:l.OwnerId}),e[27]=l.OwnerId,e[28]=f):f=e[28];let I;e[29]!==l.State?(I=t.jsx(a,{label:"State",value:l.State}),e[29]=l.State,e[30]=I):I=e[30];let h;e[31]!==R||e[32]!==l.Public||e[33]!==i?(h=i&&t.jsxs(t.Fragment,{children:[t.jsx(a,{label:"Creation Date",value:R}),t.jsx(a,{label:"Public",value:l.Public?"Yes":"No"})]}),e[31]=R,e[32]=l.Public,e[33]=i,e[34]=h):h=e[34];let p;e[35]!==v||e[36]!==D||e[37]!==d||e[38]!==x||e[39]!==b||e[40]!==j||e[41]!==f||e[42]!==I||e[43]!==h||e[44]!==c||e[45]!==n||e[46]!==u||e[47]!==m?(p=t.jsxs(T.Content,{children:[c,n,u,m,v,D,d,x,b,j,f,I,h]}),e[35]=v,e[36]=D,e[37]=d,e[38]=x,e[39]=b,e[40]=j,e[41]=f,e[42]=I,e[43]=h,e[44]=c,e[45]=n,e[46]=u,e[47]=m,e[48]=p):p=e[48];let N;e[49]!==p||e[50]!==o?(N=t.jsxs(T,{children:[o,p]}),e[49]=p,e[50]=o,e[51]=N):N=e[51];let y;e[52]!==l.BlockDeviceMappings||e[53]!==i?(y=i&&l.BlockDeviceMappings&&l.BlockDeviceMappings.length>0&&l.BlockDeviceMappings.map(w),e[52]=l.BlockDeviceMappings,e[53]=i,e[54]=y):y=e[54];let V;return e[55]!==N||e[56]!==y?(V=t.jsxs(t.Fragment,{children:[N,y]}),e[55]=N,e[56]=y,e[57]=V):V=e[57],V}function w(s){return t.jsxs(T,{children:[t.jsxs(T.Header,{children:["Block Device: ",s.DeviceName]}),t.jsxs(T.Content,{children:[t.jsx(a,{label:"Device Name",value:s.DeviceName}),s.Ebs&&t.jsxs(t.Fragment,{children:[t.jsx(a,{label:"Volume Size",value:s.Ebs.VolumeSize?`${s.Ebs.VolumeSize} GB`:void 0}),t.jsx(a,{label:"Volume Type",value:s.Ebs.VolumeType}),t.jsx(a,{label:"Delete On Termination",value:s.Ebs.DeleteOnTermination?"Yes":"No"}),t.jsx(a,{label:"Encrypted",value:s.Ebs.Encrypted?"Yes":"No"}),s.Ebs.SnapshotId&&t.jsx(a,{label:"Snapshot ID",value:t.jsx(M,{className:"text-primary hover:underline",params:{id:s.Ebs.SnapshotId},to:"/ec2/describe-snapshots/$id",children:s.Ebs.SnapshotId})})]}),s.VirtualName&&t.jsx(a,{label:"Virtual Name",value:s.VirtualName})]})]},s.DeviceName)}export{C as A};
|
||||
@@ -1 +0,0 @@
|
||||
import{J as o,c as r,j as l,L as a}from"./index.js";const m=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],x=o("arrow-left",m);function p(i){const e=r.c(7);let s,t;e[0]!==i?({children:s,...t}=i,e[0]=i,e[1]=s,e[2]=t):(s=e[1],t=e[2]);let n;e[3]===Symbol.for("react.memo_cache_sentinel")?(n=l.jsx(x,{className:"size-4"}),e[3]=n):n=e[3];let c;return e[4]!==s||e[5]!==t?(c=l.jsxs(a,{className:"mb-6 inline-flex items-center gap-2 text-sm",...t,children:[n,s]}),e[4]=s,e[5]=t,e[6]=c):c=e[6],c}export{p as B};
|
||||
@@ -1 +0,0 @@
|
||||
import{J as c}from"./index.js";const e=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],t=c("check",e);export{t as C};
|
||||
@@ -1 +0,0 @@
|
||||
import{o as C,k as P,c as _,u as v,j as t}from"./index.js";import{a as E,u as z}from"./zod.js";import{B as M}from"./back-link.js";import{E as L}from"./error-banner.js";import{F as $}from"./form-actions.js";import{P as A}from"./page-heading.js";import{F as I,a as R,b as H}from"./field.js";import{I as O}from"./input.js";import{u as T}from"./s3.js";const q=C({bucketName:P().min(3,"Bucket name must be at least 3 characters").max(63,"Bucket name must be at most 63 characters").regex(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/,"Must start and end with a lowercase letter or number").regex(/^[a-z0-9.-]+$/,"Only lowercase letters, numbers, hyphens, and periods allowed").refine(e=>!e.includes(".."),"Must not contain consecutive periods").refine(e=>!(e.includes(".-")||e.includes("-.")),"Must not contain adjacent period and hyphen")});function Y(){const e=_.c(35),r=v(),s=T();let f;e[0]===Symbol.for("react.memo_cache_sentinel")?(f={resolver:E(q)},e[0]=f):f=e[0];const{handleSubmit:j,register:B,formState:S}=z(f),{errors:b,isSubmitting:y}=S;let p;e[1]!==s||e[2]!==r?(p=async w=>{await s.mutateAsync(w),r({to:"/s3/ls"})},e[1]=s,e[2]=r,e[3]=p):p=e[3];const N=p;let k,x;e[4]===Symbol.for("react.memo_cache_sentinel")?(k=t.jsx(M,{to:"/s3/ls",children:"Back to buckets"}),x=t.jsx(A,{title:"Create Bucket"}),e[4]=k,e[5]=x):(k=e[4],x=e[5]);let a;e[6]!==s.error?(a=s.error&&t.jsx(L,{error:s.error,msg:"Failed to create bucket"}),e[6]=s.error,e[7]=a):a=e[7];let i;e[8]!==j||e[9]!==N?(i=j(N),e[8]=j,e[9]=N,e[10]=i):i=e[10];let h;e[11]===Symbol.for("react.memo_cache_sentinel")?(h=t.jsx(R,{children:t.jsx("label",{htmlFor:"bucketName",children:"Bucket Name"})}),e[11]=h):h=e[11];const F=!!b.bucketName;let o;e[12]!==B?(o=B("bucketName"),e[12]=B,e[13]=o):o=e[13];let n;e[14]!==F||e[15]!==o?(n=t.jsx(O,{"aria-invalid":F,id:"bucketName",placeholder:"my-bucket",type:"text",...o}),e[14]=F,e[15]=o,e[16]=n):n=e[16];let c;e[17]!==b.bucketName?(c=t.jsx(I,{errors:[b.bucketName]}),e[17]=b.bucketName,e[18]=c):c=e[18];let l;e[19]!==n||e[20]!==c?(l=t.jsxs(H,{children:[h,n,c]}),e[19]=n,e[20]=c,e[21]=l):l=e[21];let m;e[22]!==r?(m=()=>r({to:"/s3/ls"}),e[22]=r,e[23]=m):m=e[23];let u;e[24]!==s.isPending||e[25]!==y||e[26]!==m?(u=t.jsx($,{isPending:s.isPending,isSubmitting:y,onCancel:m,pendingLabel:"Creating\\u2026",submitLabel:"Create Bucket"}),e[24]=s.isPending,e[25]=y,e[26]=m,e[27]=u):u=e[27];let d;e[28]!==l||e[29]!==u||e[30]!==i?(d=t.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:i,children:[l,u]}),e[28]=l,e[29]=u,e[30]=i,e[31]=d):d=e[31];let g;return e[32]!==d||e[33]!==a?(g=t.jsxs(t.Fragment,{children:[k,x,a,d]}),e[32]=d,e[33]=a,e[34]=g):g=e[34],g}export{Y as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{c as _,u as M,j as a,B as K,r as T}from"./index.js";import{a as B,u as E}from"./zod.js";import{B as L}from"./back-link.js";import{E as R}from"./error-banner.js";import{F as U}from"./form-actions.js";import{P as O}from"./page-heading.js";import{F as $,a as z,b as H}from"./field.js";import{I}from"./input.js";import{e as Y}from"./ec2.js";import{e as q}from"./ec22.js";import{A as G,a as J,b as Q,c as V,d as W,e as X,f as Z,g as ee}from"./alert-dialog.js";import{T as te}from"./textarea.js";import{u as ae,T as se}from"./use-copy-to-clipboard.js";import{C as le}from"./check.js";import{D as re}from"./download.js";function ie(t){const e=_.c(37),{open:s,keyName:l,keyMaterial:d}=t,k=M(),{copied:P,copy:D}=ae();let S;e[0]!==d||e[1]!==l?(S=()=>{const v=new Blob([d],{type:"text/plain"}),F=URL.createObjectURL(v),w=document.createElement("a");w.href=F,w.download=`${l}.pem`,w.click(),URL.revokeObjectURL(F)},e[0]=d,e[1]=l,e[2]=S):S=e[2];const b=S;let g;e[3]!==k?(g=()=>{k({to:"/ec2/describe-key-pairs"})},e[3]=k,e[4]=g):g=e[4];const N=g;let C,j;e[5]===Symbol.for("react.memo_cache_sentinel")?(C=a.jsx(G,{children:a.jsx(se,{className:"text-destructive"})}),j=a.jsx(J,{children:"Save Your Private Key"}),e[5]=C,e[6]=j):(C=e[5],j=e[6]);let y;e[7]!==l?(y=a.jsxs(Q,{children:[C,j,a.jsxs(V,{children:["This is your only chance to save the private key file for"," ",a.jsx("strong",{children:l}),". Please download or copy it now. You won't be able to retrieve it again."]})]}),e[7]=l,e[8]=y):y=e[8];let r;e[9]!==d?(r=a.jsx(te,{className:"font-mono text-xs",readOnly:!0,rows:12,value:d}),e[9]=d,e[10]=r):r=e[10];let i;e[11]!==D||e[12]!==d?(i=()=>D(d),e[11]=D,e[12]=d,e[13]=i):i=e[13];let p;e[14]!==P?(p=P?a.jsxs(a.Fragment,{children:[a.jsx(le,{className:"mr-2 size-4"}),"Copied!"]}):"Copy to Clipboard",e[14]=P,e[15]=p):p=e[15];let h;e[16]!==i||e[17]!==p?(h=a.jsx(K,{className:"flex-1",onClick:i,type:"button",variant:"outline",children:p}),e[16]=i,e[17]=p,e[18]=h):h=e[18];let x;e[19]===Symbol.for("react.memo_cache_sentinel")?(x=a.jsx(re,{className:"mr-2 size-4"}),e[19]=x):x=e[19];let o;e[20]!==b?(o=a.jsxs(K,{className:"flex-1",onClick:b,type:"button",variant:"outline",children:[x,"Download .pem File"]}),e[20]=b,e[21]=o):o=e[21];let n;e[22]!==o||e[23]!==h?(n=a.jsxs("div",{className:"flex gap-2",children:[h,o]}),e[22]=o,e[23]=h,e[24]=n):n=e[24];let c;e[25]!==n||e[26]!==r?(c=a.jsxs("div",{className:"space-y-3",children:[r,n]}),e[25]=n,e[26]=r,e[27]=c):c=e[27];let m;e[28]!==N?(m=a.jsx(W,{children:a.jsx(X,{onClick:N,children:"Close and Continue"})}),e[28]=N,e[29]=m):m=e[29];let f;e[30]!==c||e[31]!==m||e[32]!==y?(f=a.jsxs(Z,{className:"max-w-2xl",children:[y,c,m]}),e[30]=c,e[31]=m,e[32]=y,e[33]=f):f=e[33];let u;return e[34]!==s||e[35]!==f?(u=a.jsx(ee,{open:s,children:f}),e[34]=s,e[35]=f,e[36]=u):u=e[36],u}function Ne(){const t=_.c(37),e=M(),s=Y(),[l,d]=T.useState(null);let k;t[0]===Symbol.for("react.memo_cache_sentinel")?(k={resolver:B(q)},t[0]=k):k=t[0];const{handleSubmit:P,register:D,formState:S}=E(k),{errors:b,isSubmitting:g}=S;let N;t[1]!==s?(N=async w=>{const A=await s.mutateAsync(w);A.KeyMaterial&&d({keyName:w.keyName,material:A.KeyMaterial})},t[1]=s,t[2]=N):N=t[2];const C=N;let j,y;t[3]===Symbol.for("react.memo_cache_sentinel")?(j=a.jsx(L,{to:"/ec2/describe-key-pairs",children:"Back to key pairs"}),y=a.jsx(O,{title:"Create Key Pair"}),t[3]=j,t[4]=y):(j=t[3],y=t[4]);let r;t[5]!==s.error?(r=s.error&&a.jsx(R,{error:s.error,msg:"Failed to create key pair"}),t[5]=s.error,t[6]=r):r=t[6];let i;t[7]!==P||t[8]!==C?(i=P(C),t[7]=P,t[8]=C,t[9]=i):i=t[9];let p;t[10]===Symbol.for("react.memo_cache_sentinel")?(p=a.jsx(z,{children:a.jsx("label",{htmlFor:"keyName",children:"Key Pair Name"})}),t[10]=p):p=t[10];const h=!!b.keyName;let x;t[11]!==D?(x=D("keyName"),t[11]=D,t[12]=x):x=t[12];let o;t[13]!==h||t[14]!==x?(o=a.jsx(I,{"aria-invalid":h,id:"keyName",placeholder:"my-key-pair…",...x}),t[13]=h,t[14]=x,t[15]=o):o=t[15];let n;t[16]!==b.keyName?(n=a.jsx($,{errors:[b.keyName]}),t[16]=b.keyName,t[17]=n):n=t[17];let c;t[18]!==o||t[19]!==n?(c=a.jsxs(H,{children:[p,o,n]}),t[18]=o,t[19]=n,t[20]=c):c=t[20];let m;t[21]!==e?(m=()=>e({to:"/ec2/describe-key-pairs"}),t[21]=e,t[22]=m):m=t[22];let f;t[23]!==s.isPending||t[24]!==g||t[25]!==m?(f=a.jsx(U,{isPending:s.isPending,isSubmitting:g,onCancel:m,pendingLabel:"Creating…",submitLabel:"Create Key Pair"}),t[23]=s.isPending,t[24]=g,t[25]=m,t[26]=f):f=t[26];let u;t[27]!==c||t[28]!==f||t[29]!==i?(u=a.jsxs("form",{className:"max-w-4xl space-y-6",onSubmit:i,children:[c,f]}),t[27]=c,t[28]=f,t[29]=i,t[30]=u):u=t[30];let v;t[31]!==l?(v=l&&a.jsx(ie,{keyMaterial:l.material,keyName:l.keyName,open:!!l}),t[31]=l,t[32]=v):v=t[32];let F;return t[33]!==u||t[34]!==v||t[35]!==r?(F=a.jsxs(a.Fragment,{children:[j,y,r,u,v]}),t[33]=u,t[34]=v,t[35]=r,t[36]=F):F=t[36],F}export{Ne as component};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user