mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 05:32:59 +00:00
Compare commits
57 Commits
v0.69.9-ni
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ade6241357 | ||
|
|
d3d1bb98ba | ||
|
|
ccee66d525 | ||
|
|
acc38d584a | ||
|
|
c20f6e9a25 | ||
|
|
b0bc0a232e | ||
|
|
86f73a1d8e | ||
|
|
8c82124e05 | ||
|
|
6f4f55f669 | ||
|
|
fff665374f | ||
|
|
2b3e6874c8 | ||
|
|
cbbf72092d | ||
|
|
9ddbe945fd | ||
|
|
4f893e08d1 | ||
|
|
df5b11b175 | ||
|
|
a9844a1451 | ||
|
|
4ee76588ed | ||
|
|
b3b1905fb2 | ||
|
|
54aab4841d | ||
|
|
ee80be15d8 | ||
|
|
6740e67d40 | ||
|
|
670c3f99df | ||
|
|
9f43cea907 | ||
| 65286df31e | |||
|
|
b91b7c27ea | ||
|
|
432952ed69 | ||
|
|
9193f088a3 | ||
|
|
3505a6a0eb | ||
|
|
3ca4e1f43b | ||
|
|
2fb1d68fcb | ||
|
|
7126c4068b | ||
|
|
681cef999a | ||
|
|
5c7767b7c8 | ||
|
|
d8994b1e4f | ||
|
|
b983066016 | ||
|
|
660008b0aa | ||
|
|
775289a1a2 | ||
|
|
87059fb9c4 | ||
|
|
90a26295a4 | ||
|
|
4c1f842939 | ||
|
|
33ebf222ff | ||
|
|
2f1ccfa473 | ||
|
|
6f7b7606b0 | ||
|
|
adb180932b | ||
|
|
5d6de3b0b8 | ||
|
|
747be5863b | ||
|
|
358de8a8ad | ||
|
|
47ffe817b4 | ||
|
|
7f77836d73 | ||
|
|
1d060490a8 | ||
|
|
0421155594 | ||
|
|
42131c0e75 | ||
|
|
cc74a8f135 | ||
|
|
685295551c | ||
|
|
ca00561da1 | ||
|
|
a4b4b8f0df | ||
|
|
fe05240362 |
198
.github/workflows/release-apt.yml
vendored
Normal file
198
.github/workflows/release-apt.yml
vendored
Normal file
@ -0,0 +1,198 @@
|
||||
name: Release APT Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to release (e.g., 0.69.20)"
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-deb:
|
||||
name: Build Debian Package
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "release" ]; then
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
VERSION="${VERSION#v}" # Remove 'v' prefix if present
|
||||
else
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU (for arm64)
|
||||
if: matrix.arch == 'arm64'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Build binary
|
||||
env:
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
CGO_ENABLED: 0
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
COMMIT=$(git rev-parse --short HEAD)
|
||||
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
LDFLAGS="-X 'main.version=$VERSION' -X 'main.commit=$COMMIT' -X 'main.date=$DATE'"
|
||||
|
||||
mkdir -p build/usr/local/bin
|
||||
go build -ldflags "$LDFLAGS" -o build/usr/local/bin/orama cmd/cli/main.go
|
||||
go build -ldflags "$LDFLAGS" -o build/usr/local/bin/debros-node cmd/node/main.go
|
||||
# Build the entire gateway package so helper files (e.g., config parsing) are included
|
||||
go build -ldflags "$LDFLAGS" -o build/usr/local/bin/debros-gateway ./cmd/gateway
|
||||
|
||||
- name: Create Debian package structure
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
ARCH="${{ matrix.arch }}"
|
||||
PKG_NAME="orama_${VERSION}_${ARCH}"
|
||||
|
||||
mkdir -p ${PKG_NAME}/DEBIAN
|
||||
mkdir -p ${PKG_NAME}/usr/local/bin
|
||||
|
||||
# Copy binaries
|
||||
cp build/usr/local/bin/* ${PKG_NAME}/usr/local/bin/
|
||||
chmod 755 ${PKG_NAME}/usr/local/bin/*
|
||||
|
||||
# Create control file
|
||||
cat > ${PKG_NAME}/DEBIAN/control << EOF
|
||||
Package: orama
|
||||
Version: ${VERSION}
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: ${ARCH}
|
||||
Depends: libc6
|
||||
Maintainer: DeBros Team <team@debros.network>
|
||||
Description: Orama Network - Distributed P2P Database System
|
||||
Orama is a distributed peer-to-peer network that combines
|
||||
RQLite for distributed SQL, IPFS for content-addressed storage,
|
||||
and LibP2P for peer discovery and communication.
|
||||
EOF
|
||||
|
||||
# Create postinst script
|
||||
cat > ${PKG_NAME}/DEBIAN/postinst << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo ""
|
||||
echo "Orama installed successfully!"
|
||||
echo ""
|
||||
echo "To set up your node, run:"
|
||||
echo " sudo orama install"
|
||||
echo ""
|
||||
EOF
|
||||
chmod 755 ${PKG_NAME}/DEBIAN/postinst
|
||||
|
||||
- name: Build .deb package
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
ARCH="${{ matrix.arch }}"
|
||||
PKG_NAME="orama_${VERSION}_${ARCH}"
|
||||
|
||||
dpkg-deb --build ${PKG_NAME}
|
||||
mv ${PKG_NAME}.deb orama_${VERSION}_${ARCH}.deb
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deb-${{ matrix.arch }}
|
||||
path: "*.deb"
|
||||
|
||||
publish-apt:
|
||||
name: Publish to APT Repository
|
||||
needs: build-deb
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: packages
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "release" ]; then
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
VERSION="${VERSION#v}"
|
||||
else
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up GPG
|
||||
if: env.GPG_PRIVATE_KEY != ''
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --import
|
||||
|
||||
- name: Create APT repository structure
|
||||
run: |
|
||||
mkdir -p apt-repo/pool/main/o/orama
|
||||
mkdir -p apt-repo/dists/stable/main/binary-amd64
|
||||
mkdir -p apt-repo/dists/stable/main/binary-arm64
|
||||
|
||||
# Move packages
|
||||
mv packages/deb-amd64/*.deb apt-repo/pool/main/o/orama/
|
||||
mv packages/deb-arm64/*.deb apt-repo/pool/main/o/orama/
|
||||
|
||||
# Generate Packages files
|
||||
cd apt-repo
|
||||
dpkg-scanpackages --arch amd64 pool/ > dists/stable/main/binary-amd64/Packages
|
||||
dpkg-scanpackages --arch arm64 pool/ > dists/stable/main/binary-arm64/Packages
|
||||
|
||||
gzip -k dists/stable/main/binary-amd64/Packages
|
||||
gzip -k dists/stable/main/binary-arm64/Packages
|
||||
|
||||
# Generate Release file
|
||||
cat > dists/stable/Release << EOF
|
||||
Origin: Orama
|
||||
Label: Orama
|
||||
Suite: stable
|
||||
Codename: stable
|
||||
Architectures: amd64 arm64
|
||||
Components: main
|
||||
Description: Orama Network APT Repository
|
||||
EOF
|
||||
|
||||
cd ..
|
||||
|
||||
- name: Upload to release
|
||||
if: github.event_name == 'release'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
apt-repo/pool/main/o/orama/*.deb
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy APT repository to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./apt-repo
|
||||
destination_dir: apt
|
||||
keep_files: true
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -74,4 +74,10 @@ data/bootstrap/rqlite/
|
||||
|
||||
configs/
|
||||
|
||||
.dev/
|
||||
.dev/
|
||||
|
||||
.gocache/
|
||||
|
||||
.claude/
|
||||
.mcp.json
|
||||
.cursor/
|
||||
@ -1,68 +0,0 @@
|
||||
// Project-local debug tasks
|
||||
//
|
||||
// For more documentation on how to configure debug tasks,
|
||||
// see: https://zed.dev/docs/debugger
|
||||
[
|
||||
{
|
||||
"label": "Gateway Go (Delve)",
|
||||
"adapter": "Delve",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "./cmd/gateway",
|
||||
"env": {
|
||||
"GATEWAY_ADDR": ":6001",
|
||||
"GATEWAY_BOOTSTRAP_PEERS": "/ip4/localhost/tcp/4001/p2p/12D3KooWSHHwEY6cga3ng7tD1rzStAU58ogQXVMX3LZJ6Gqf6dee",
|
||||
"GATEWAY_NAMESPACE": "default",
|
||||
"GATEWAY_API_KEY": "ak_iGustrsFk9H8uXpwczCATe5U:default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "E2E Test Go (Delve)",
|
||||
"adapter": "Delve",
|
||||
"request": "launch",
|
||||
"mode": "test",
|
||||
"buildFlags": "-tags e2e",
|
||||
"program": "./e2e",
|
||||
"env": {
|
||||
"GATEWAY_API_KEY": "ak_iGustrsFk9H8uXpwczCATe5U:default"
|
||||
},
|
||||
"args": ["-test.v"]
|
||||
},
|
||||
{
|
||||
"adapter": "Delve",
|
||||
"label": "Gateway Go 6001 Port (Delve)",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "./cmd/gateway",
|
||||
"env": {
|
||||
"GATEWAY_ADDR": ":6001",
|
||||
"GATEWAY_BOOTSTRAP_PEERS": "/ip4/localhost/tcp/4001/p2p/12D3KooWSHHwEY6cga3ng7tD1rzStAU58ogQXVMX3LZJ6Gqf6dee",
|
||||
"GATEWAY_NAMESPACE": "default",
|
||||
"GATEWAY_API_KEY": "ak_iGustrsFk9H8uXpwczCATe5U:default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"adapter": "Delve",
|
||||
"label": "Network CLI - peers (Delve)",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "./cmd/cli",
|
||||
"args": ["peers"]
|
||||
},
|
||||
{
|
||||
"adapter": "Delve",
|
||||
"label": "Network CLI - PubSub Subscribe (Delve)",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "./cmd/cli",
|
||||
"args": ["pubsub", "subscribe", "monitoring"]
|
||||
},
|
||||
{
|
||||
"adapter": "Delve",
|
||||
"label": "Node Go (Delve)",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "./cmd/node",
|
||||
"args": ["--config", "configs/node.yaml"]
|
||||
}
|
||||
]
|
||||
1117
CHANGELOG.md
1117
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -27,14 +27,14 @@ make deps
|
||||
Useful CLI commands:
|
||||
|
||||
```bash
|
||||
./bin/dbn health
|
||||
./bin/dbn peers
|
||||
./bin/dbn status
|
||||
./bin/orama health
|
||||
./bin/orama peers
|
||||
./bin/orama status
|
||||
````
|
||||
|
||||
## Versioning
|
||||
|
||||
- The CLI reports its version via `dbn version`.
|
||||
- The CLI reports its version via `orama version`.
|
||||
- Releases are tagged (e.g., `v0.18.0-beta`) and published via GoReleaser.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
82
Makefile
82
Makefile
@ -6,12 +6,12 @@ test:
|
||||
go test -v $(TEST)
|
||||
|
||||
# Gateway-focused E2E tests assume gateway and nodes are already running
|
||||
# Auto-discovers configuration from ~/.debros and queries database for API key
|
||||
# Auto-discovers configuration from ~/.orama and queries database for API key
|
||||
# No environment variables required
|
||||
.PHONY: test-e2e
|
||||
test-e2e:
|
||||
@echo "Running comprehensive E2E tests..."
|
||||
@echo "Auto-discovering configuration from ~/.debros..."
|
||||
@echo "Auto-discovering configuration from ~/.orama..."
|
||||
go test -v -tags e2e ./e2e
|
||||
|
||||
# Network - Distributed P2P Database System
|
||||
@ -19,7 +19,7 @@ test-e2e:
|
||||
|
||||
.PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports install-hooks kill
|
||||
|
||||
VERSION := 0.69.6
|
||||
VERSION := 0.90.0
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'
|
||||
@ -29,11 +29,12 @@ build: deps
|
||||
@echo "Building network executables (version=$(VERSION))..."
|
||||
@mkdir -p bin
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/identity ./cmd/identity
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/node ./cmd/node
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/dbn cmd/cli/main.go
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/orama-node ./cmd/node
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/orama cmd/cli/main.go
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/rqlite-mcp ./cmd/rqlite-mcp
|
||||
# Inject gateway build metadata via pkg path variables
|
||||
go build -ldflags "$(LDFLAGS) -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildVersion=$(VERSION)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildCommit=$(COMMIT)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildTime=$(DATE)'" -o bin/gateway ./cmd/gateway
|
||||
@echo "Build complete! Run ./bin/dbn version"
|
||||
@echo "Build complete! Run ./bin/orama version"
|
||||
|
||||
# Install git hooks
|
||||
install-hooks:
|
||||
@ -49,48 +50,43 @@ clean:
|
||||
|
||||
# Run bootstrap node (auto-selects identity and data dir)
|
||||
run-node:
|
||||
@echo "Starting bootstrap node..."
|
||||
@echo "Config: ~/.debros/bootstrap.yaml"
|
||||
@echo "Generate it with: dbn config init --type bootstrap"
|
||||
go run ./cmd/node --config node.yaml
|
||||
@echo "Starting node..."
|
||||
@echo "Config: ~/.orama/node.yaml"
|
||||
go run ./cmd/orama-node --config node.yaml
|
||||
|
||||
# Run second node (regular) - requires join address of bootstrap node
|
||||
# Usage: make run-node2 JOINADDR=/ip4/localhost/tcp/5001 HTTP=5002 RAFT=7002 P2P=4002
|
||||
# Run second node - requires join address
|
||||
run-node2:
|
||||
@echo "Starting regular node (node.yaml)..."
|
||||
@echo "Config: ~/.debros/node.yaml"
|
||||
@echo "Generate it with: dbn config init --type node --join localhost:5001 --bootstrap-peers '<peer_multiaddr>'"
|
||||
go run ./cmd/node --config node2.yaml
|
||||
@echo "Starting second node..."
|
||||
@echo "Config: ~/.orama/node2.yaml"
|
||||
go run ./cmd/orama-node --config node2.yaml
|
||||
|
||||
# Run third node (regular) - requires join address of bootstrap node
|
||||
# Usage: make run-node3 JOINADDR=/ip4/localhost/tcp/5001 HTTP=5003 RAFT=7003 P2P=4003
|
||||
# Run third node - requires join address
|
||||
run-node3:
|
||||
@echo "Starting regular node (node2.yaml)..."
|
||||
@echo "Config: ~/.debros/node2.yaml"
|
||||
@echo "Generate it with: dbn config init --type node --name node2.yaml --join localhost:5001 --bootstrap-peers '<peer_multiaddr>'"
|
||||
go run ./cmd/node --config node3.yaml
|
||||
@echo "Starting third node..."
|
||||
@echo "Config: ~/.orama/node3.yaml"
|
||||
go run ./cmd/orama-node --config node3.yaml
|
||||
|
||||
# Run gateway HTTP server
|
||||
# Usage examples:
|
||||
# make run-gateway # uses ~/.debros/gateway.yaml
|
||||
# Config generated with: dbn config init --type gateway
|
||||
run-gateway:
|
||||
@echo "Starting gateway HTTP server..."
|
||||
@echo "Note: Config must be in ~/.debros/gateway.yaml"
|
||||
@echo "Generate it with: dbn config init --type gateway"
|
||||
go run ./cmd/gateway
|
||||
@echo "Note: Config must be in ~/.orama/data/gateway.yaml"
|
||||
go run ./cmd/orama-gateway
|
||||
|
||||
# Development environment target
|
||||
# Uses dbn dev up to start full stack with dependency and port checking
|
||||
# Uses orama dev up to start full stack with dependency and port checking
|
||||
dev: build
|
||||
@./bin/dbn dev up
|
||||
@./bin/orama dev up
|
||||
|
||||
# Kill all processes (graceful shutdown + force kill stray processes)
|
||||
kill:
|
||||
# Graceful shutdown of all dev services
|
||||
stop:
|
||||
@if [ -f ./bin/orama ]; then \
|
||||
./bin/orama dev down || true; \
|
||||
fi
|
||||
@bash scripts/dev-kill-all.sh
|
||||
|
||||
stop:
|
||||
@./bin/dbn dev down
|
||||
# Force kill all processes (immediate termination)
|
||||
kill:
|
||||
@bash scripts/dev-kill-all.sh
|
||||
|
||||
# Help
|
||||
help:
|
||||
@ -102,19 +98,17 @@ help:
|
||||
@echo "Local Development (Recommended):"
|
||||
@echo " make dev - Start full development stack with one command"
|
||||
@echo " - Checks dependencies and available ports"
|
||||
@echo " - Generates configs (2 bootstraps + 3 nodes + gateway)"
|
||||
@echo " - Starts IPFS, RQLite, Olric, all nodes, and gateway"
|
||||
@echo " - Validates cluster health (IPFS peers, RQLite, LibP2P)"
|
||||
@echo " - Stops all services if health checks fail"
|
||||
@echo " - Includes comprehensive logging"
|
||||
@echo " make kill - Stop all development services"
|
||||
@echo " - Generates configs and starts all services"
|
||||
@echo " - Validates cluster health"
|
||||
@echo " make stop - Gracefully stop all development services"
|
||||
@echo " make kill - Force kill all development services (use if stop fails)"
|
||||
@echo ""
|
||||
@echo "Development Management (via dbn):"
|
||||
@echo " ./bin/dbn dev status - Show status of all dev services"
|
||||
@echo " ./bin/dbn dev logs <component> [--follow]"
|
||||
@echo "Development Management (via orama):"
|
||||
@echo " ./bin/orama dev status - Show status of all dev services"
|
||||
@echo " ./bin/orama dev logs <component> [--follow]"
|
||||
@echo ""
|
||||
@echo "Individual Node Targets (advanced):"
|
||||
@echo " run-node - Start bootstrap node directly"
|
||||
@echo " run-node - Start first node directly"
|
||||
@echo " run-node2 - Start second node directly"
|
||||
@echo " run-node3 - Start third node directly"
|
||||
@echo " run-gateway - Start HTTP gateway directly"
|
||||
|
||||
@ -1,158 +0,0 @@
|
||||
# Production Installation Guide - DeBros Network
|
||||
|
||||
This guide covers production deployment of the DeBros Network using the `dbn prod` command suite.
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **OS**: Ubuntu 20.04 LTS or later, Debian 11+, or other Linux distributions
|
||||
- **Architecture**: x86_64 (amd64) or ARM64 (aarch64)
|
||||
- **RAM**: Minimum 4GB, recommended 8GB+
|
||||
- **Storage**: Minimum 50GB SSD recommended
|
||||
- **Ports**:
|
||||
- 4001 (P2P networking)
|
||||
- 4501 (IPFS HTTP API - bootstrap), 4502/4503 (node2/node3)
|
||||
- 5001-5003 (RQLite HTTP - one per node)
|
||||
- 6001 (Gateway)
|
||||
- 7001-7003 (RQLite Raft - one per node)
|
||||
- 9094 (IPFS Cluster API - bootstrap), 9104/9114 (node2/node3)
|
||||
- 3320/3322 (Olric)
|
||||
- 80, 443 (for HTTPS with Let's Encrypt)
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Root access required**: All production operations require sudo/root privileges
|
||||
2. **Supported distros**: Ubuntu, Debian, Fedora (via package manager)
|
||||
3. **Basic tools**: `curl`, `git`, `make`, `build-essential`, `wget`
|
||||
|
||||
### Single-Node Bootstrap Installation
|
||||
|
||||
Deploy the first node (bootstrap node) on a VPS:
|
||||
|
||||
```bash
|
||||
sudo dbn prod install --bootstrap
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
1. Check system prerequisites (OS, arch, root privileges, basic tools)
|
||||
2. Provision the `debros` system user and filesystem structure at `~/.debros`
|
||||
3. Download and install all required binaries (Go, RQLite, IPFS, IPFS Cluster, Olric, DeBros)
|
||||
4. Generate secrets (cluster secret, swarm key, node identity)
|
||||
5. Initialize repositories (IPFS, IPFS Cluster, RQLite)
|
||||
6. Generate configurations for bootstrap node
|
||||
7. Create and start systemd services
|
||||
|
||||
All files will be under `/home/debros/.debros`:
|
||||
|
||||
```
|
||||
~/.debros/
|
||||
├── bin/ # Compiled binaries
|
||||
├── configs/ # YAML configurations
|
||||
├── data/
|
||||
│ ├── ipfs/ # IPFS repository
|
||||
│ ├── ipfs-cluster/ # IPFS Cluster state
|
||||
│ └── rqlite/ # RQLite database
|
||||
├── logs/ # Service logs
|
||||
└── secrets/ # Keys and certificates
|
||||
```
|
||||
|
||||
## Service Management
|
||||
|
||||
### Check Service Status
|
||||
|
||||
```bash
|
||||
sudo systemctl status debros-node-bootstrap
|
||||
sudo systemctl status debros-gateway
|
||||
sudo systemctl status debros-rqlite-bootstrap
|
||||
```
|
||||
|
||||
### View Service Logs
|
||||
|
||||
```bash
|
||||
# Bootstrap node logs
|
||||
sudo journalctl -u debros-node-bootstrap -f
|
||||
|
||||
# Gateway logs
|
||||
sudo journalctl -u debros-gateway -f
|
||||
|
||||
# All services
|
||||
sudo journalctl -u "debros-*" -f
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
After installation, verify services are running:
|
||||
|
||||
```bash
|
||||
# Check IPFS
|
||||
curl http://localhost:4501/api/v0/id
|
||||
|
||||
# Check RQLite cluster
|
||||
curl http://localhost:5001/status
|
||||
|
||||
# Check Gateway
|
||||
curl http://localhost:6001/health
|
||||
|
||||
# Check Olric
|
||||
curl http://localhost:3320/ping
|
||||
```
|
||||
|
||||
## Port Reference
|
||||
|
||||
### Development Environment (via `make dev`)
|
||||
|
||||
- IPFS API: 4501 (bootstrap), 4502 (node2), 4503 (node3)
|
||||
- RQLite HTTP: 5001, 5002, 5003
|
||||
- RQLite Raft: 7001, 7002, 7003
|
||||
- IPFS Cluster: 9094, 9104, 9114
|
||||
- P2P: 4001, 4002, 4003
|
||||
- Gateway: 6001
|
||||
- Olric: 3320, 3322
|
||||
|
||||
### Production Environment (via `sudo dbn prod install`)
|
||||
|
||||
- Same port assignments as development for consistency
|
||||
|
||||
## Configuration Files
|
||||
|
||||
Key configuration files are located in `~/.debros/configs/`:
|
||||
|
||||
- **bootstrap.yaml**: Bootstrap node configuration
|
||||
- **node.yaml**: Regular node configuration
|
||||
- **gateway.yaml**: HTTP gateway configuration
|
||||
- **olric.yaml**: In-memory cache configuration
|
||||
|
||||
Edit these files directly for advanced configuration, then restart services:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart debros-node-bootstrap
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port already in use
|
||||
|
||||
Check which process is using the port:
|
||||
|
||||
```bash
|
||||
sudo lsof -i :4501
|
||||
sudo lsof -i :5001
|
||||
sudo lsof -i :7001
|
||||
```
|
||||
|
||||
Kill conflicting processes or change ports in config.
|
||||
|
||||
### RQLite cluster not forming
|
||||
|
||||
Ensure:
|
||||
|
||||
1. Bootstrap node is running: `systemctl status debros-rqlite-bootstrap`
|
||||
2. Network connectivity between nodes on ports 5001+ (HTTP) and 7001+ (Raft)
|
||||
3. Check logs: `journalctl -u debros-rqlite-bootstrap -f`
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: November 2024
|
||||
**Compatible with**: Network v1.0.0+
|
||||
894
README.md
894
README.md
@ -1,605 +1,379 @@
|
||||
# DeBros Network - Distributed P2P Database System
|
||||
# Orama Network - Distributed P2P Platform
|
||||
|
||||
DeBros Network is a decentralized peer-to-peer data platform built in Go. It combines distributed SQL (RQLite), pub/sub messaging, and resilient peer discovery so applications can share state without central infrastructure.
|
||||
A high-performance API Gateway and distributed platform built in Go. Provides a unified HTTP/HTTPS API for distributed SQL (RQLite), distributed caching (Olric), decentralized storage (IPFS), pub/sub messaging, and serverless WebAssembly execution.
|
||||
|
||||
## Table of Contents
|
||||
**Architecture:** Modular Gateway / Edge Proxy following SOLID principles
|
||||
|
||||
- [At a Glance](#at-a-glance)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Production Deployment](#production-deployment)
|
||||
- [Components & Ports](#components--ports)
|
||||
- [Configuration Cheatsheet](#configuration-cheatsheet)
|
||||
- [CLI Highlights](#cli-highlights)
|
||||
- [HTTP Gateway](#http-gateway)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Resources](#resources)
|
||||
## Features
|
||||
|
||||
## At a Glance
|
||||
|
||||
- Distributed SQL backed by RQLite and Raft consensus
|
||||
- Topic-based pub/sub with automatic cleanup
|
||||
- Namespace isolation for multi-tenant apps
|
||||
- Secure transport using libp2p plus Noise/TLS
|
||||
- Lightweight Go client and CLI tooling
|
||||
- **🔐 Authentication** - Wallet signatures, API keys, JWT tokens
|
||||
- **💾 Storage** - IPFS-based decentralized file storage with encryption
|
||||
- **⚡ Cache** - Distributed cache with Olric (in-memory key-value)
|
||||
- **🗄️ Database** - RQLite distributed SQL with Raft consensus
|
||||
- **📡 Pub/Sub** - Real-time messaging via LibP2P and WebSocket
|
||||
- **⚙️ Serverless** - WebAssembly function execution with host functions
|
||||
- **🌐 HTTP Gateway** - Unified REST API with automatic HTTPS (Let's Encrypt)
|
||||
- **📦 Client SDK** - Type-safe Go SDK for all services
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Clone and build the project:
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
git clone https://github.com/DeBrosOfficial/network.git
|
||||
cd network
|
||||
make build
|
||||
```
|
||||
```bash
|
||||
# Build the project
|
||||
make build
|
||||
|
||||
2. Generate local configuration (bootstrap, node2, node3, gateway):
|
||||
# Start 5-node development cluster
|
||||
make dev
|
||||
```
|
||||
|
||||
```bash
|
||||
./bin/dbn config init
|
||||
```
|
||||
The cluster automatically performs health checks before declaring success.
|
||||
|
||||
3. Launch the full development stack:
|
||||
### Stop Development Environment
|
||||
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
```bash
|
||||
make stop
|
||||
```
|
||||
|
||||
This starts three nodes and the HTTP gateway. **The command will not complete successfully until all services pass health checks** (IPFS peer connectivity, RQLite cluster formation, and LibP2P connectivity). If health checks fail, all services are stopped automatically. Stop with `Ctrl+C`.
|
||||
## Testing Services
|
||||
|
||||
4. Validate the network from another terminal:
|
||||
After running `make dev`, test service health using these curl requests:
|
||||
|
||||
```bash
|
||||
./bin/dbn health
|
||||
./bin/dbn peers
|
||||
./bin/dbn pubsub publish notifications "Hello World"
|
||||
./bin/dbn pubsub subscribe notifications 10s
|
||||
```
|
||||
### Node Unified Gateways
|
||||
|
||||
Each node is accessible via a single unified gateway port:
|
||||
|
||||
```bash
|
||||
# Node-1 (port 6001)
|
||||
curl http://localhost:6001/health
|
||||
|
||||
# Node-2 (port 6002)
|
||||
curl http://localhost:6002/health
|
||||
|
||||
# Node-3 (port 6003)
|
||||
curl http://localhost:6003/health
|
||||
|
||||
# Node-4 (port 6004)
|
||||
curl http://localhost:6004/health
|
||||
|
||||
# Node-5 (port 6005)
|
||||
curl http://localhost:6005/health
|
||||
```
|
||||
|
||||
## Network Architecture
|
||||
|
||||
### Unified Gateway Ports
|
||||
|
||||
```
|
||||
Node-1: localhost:6001 → /rqlite/http, /rqlite/raft, /cluster, /ipfs/api
|
||||
Node-2: localhost:6002 → Same routes
|
||||
Node-3: localhost:6003 → Same routes
|
||||
Node-4: localhost:6004 → Same routes
|
||||
Node-5: localhost:6005 → Same routes
|
||||
```
|
||||
|
||||
### Direct Service Ports (for debugging)
|
||||
|
||||
```
|
||||
RQLite HTTP: 5001, 5002, 5003, 5004, 5005 (one per node)
|
||||
RQLite Raft: 7001, 7002, 7003, 7004, 7005
|
||||
IPFS API: 4501, 4502, 4503, 4504, 4505
|
||||
IPFS Swarm: 4101, 4102, 4103, 4104, 4105
|
||||
Cluster API: 9094, 9104, 9114, 9124, 9134
|
||||
Internal Gateway: 6000
|
||||
Olric Cache: 3320
|
||||
Anon SOCKS: 9050
|
||||
```
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Start full cluster (5 nodes + gateway)
|
||||
make dev
|
||||
|
||||
# Check service status
|
||||
orama dev status
|
||||
|
||||
# View logs
|
||||
orama dev logs node-1 # Node-1 logs
|
||||
orama dev logs node-1 --follow # Follow logs in real-time
|
||||
orama dev logs gateway --follow # Gateway logs
|
||||
|
||||
# Stop all services
|
||||
orama stop
|
||||
|
||||
# Build binaries
|
||||
make build
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### Network Status
|
||||
|
||||
```bash
|
||||
./bin/orama health # Cluster health check
|
||||
./bin/orama peers # List connected peers
|
||||
./bin/orama status # Network status
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
|
||||
```bash
|
||||
./bin/orama query "SELECT * FROM users"
|
||||
./bin/orama query "CREATE TABLE users (id INTEGER PRIMARY KEY)"
|
||||
./bin/orama transaction --file ops.json
|
||||
```
|
||||
|
||||
### Pub/Sub
|
||||
|
||||
```bash
|
||||
./bin/orama pubsub publish <topic> <message>
|
||||
./bin/orama pubsub subscribe <topic> 30s
|
||||
./bin/orama pubsub topics
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
```bash
|
||||
./bin/orama auth login
|
||||
./bin/orama auth status
|
||||
./bin/orama auth logout
|
||||
```
|
||||
|
||||
## Serverless Functions (WASM)
|
||||
|
||||
Orama supports high-performance serverless function execution using WebAssembly (WASM). Functions are isolated, secure, and can interact with network services like the distributed cache.
|
||||
|
||||
### 1. Build Functions
|
||||
|
||||
Functions must be compiled to WASM. We recommend using [TinyGo](https://tinygo.org/).
|
||||
|
||||
```bash
|
||||
# Build example functions to examples/functions/bin/
|
||||
./examples/functions/build.sh
|
||||
```
|
||||
|
||||
### 2. Deployment
|
||||
|
||||
Deploy your compiled `.wasm` file to the network via the Gateway.
|
||||
|
||||
```bash
|
||||
# Deploy a function
|
||||
curl -X POST http://localhost:6001/v1/functions \
|
||||
-H "Authorization: Bearer <your_api_key>" \
|
||||
-F "name=hello-world" \
|
||||
-F "namespace=default" \
|
||||
-F "wasm=@./examples/functions/bin/hello.wasm"
|
||||
```
|
||||
|
||||
### 3. Invocation
|
||||
|
||||
Trigger your function with a JSON payload. The function receives the payload via `stdin` and returns its response via `stdout`.
|
||||
|
||||
```bash
|
||||
# Invoke via HTTP
|
||||
curl -X POST http://localhost:6001/v1/functions/hello-world/invoke \
|
||||
-H "Authorization: Bearer <your_api_key>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "Developer"}'
|
||||
```
|
||||
|
||||
### 4. Management
|
||||
|
||||
```bash
|
||||
# List all functions in a namespace
|
||||
curl http://localhost:6001/v1/functions?namespace=default
|
||||
|
||||
# Delete a function
|
||||
curl -X DELETE http://localhost:6001/v1/functions/hello-world?namespace=default
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
DeBros Network can be deployed as production systemd services on Linux servers. The production installer handles all dependencies, configuration, and service management automatically.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **OS**: Ubuntu 20.04+, Debian 11+, or compatible Linux distribution
|
||||
- **Architecture**: `amd64` (x86_64) or `arm64` (aarch64)
|
||||
- **Permissions**: Root access (use `sudo`)
|
||||
- **Resources**: Minimum 2GB RAM, 10GB disk space, 2 CPU cores
|
||||
- Ubuntu 22.04+ or Debian 12+
|
||||
- `amd64` or `arm64` architecture
|
||||
- 4GB RAM, 50GB SSD, 2 CPU cores
|
||||
|
||||
### Required Ports
|
||||
|
||||
**External (must be open in firewall):**
|
||||
|
||||
- **80** - HTTP (ACME/Let's Encrypt certificate challenges)
|
||||
- **443** - HTTPS (Main gateway API endpoint)
|
||||
- **4101** - IPFS Swarm (peer connections)
|
||||
- **7001** - RQLite Raft (cluster consensus)
|
||||
|
||||
**Internal (bound to localhost, no firewall needed):**
|
||||
|
||||
- 4501 - IPFS API
|
||||
- 5001 - RQLite HTTP API
|
||||
- 6001 - Unified Gateway
|
||||
- 8080 - IPFS Gateway
|
||||
- 9050 - Anyone Client SOCKS5 proxy
|
||||
- 9094 - IPFS Cluster API
|
||||
- 3320/3322 - Olric Cache
|
||||
|
||||
### Installation
|
||||
|
||||
#### Quick Install
|
||||
|
||||
Install the CLI tool first:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.debros.network | sudo bash
|
||||
# Install via APT
|
||||
echo "deb https://debrosficial.github.io/network/apt stable main" | sudo tee /etc/apt/sources.list.d/debros.list
|
||||
|
||||
sudo apt update && sudo apt install orama
|
||||
|
||||
sudo orama install --interactive
|
||||
```
|
||||
|
||||
Or download manually from [GitHub Releases](https://github.com/DeBrosOfficial/network/releases).
|
||||
|
||||
#### Bootstrap Node (First Node)
|
||||
|
||||
Install the first node in your cluster:
|
||||
### Service Management
|
||||
|
||||
```bash
|
||||
# Main branch (stable releases)
|
||||
sudo dbn prod install --bootstrap
|
||||
# Status
|
||||
orama status
|
||||
|
||||
# Nightly branch (latest development)
|
||||
sudo dbn prod install --bootstrap --branch nightly
|
||||
```
|
||||
# Control services
|
||||
sudo orama start
|
||||
sudo orama stop
|
||||
sudo orama restart
|
||||
|
||||
The bootstrap node initializes the cluster and serves as the primary peer for other nodes to join.
|
||||
|
||||
#### Secondary Node (Join Existing Cluster)
|
||||
|
||||
Join an existing cluster by providing the bootstrap node's IP and peer multiaddr:
|
||||
|
||||
```bash
|
||||
sudo dbn prod install \
|
||||
--vps-ip <your_public_ip> \
|
||||
--peers /ip4/<bootstrap_ip>/tcp/4001/p2p/<peer_id> \
|
||||
--branch nightly
|
||||
```
|
||||
|
||||
**Required flags for secondary nodes:**
|
||||
|
||||
- `--vps-ip`: Your server's public IP address
|
||||
- `--peers`: Comma-separated list of bootstrap peer multiaddrs
|
||||
|
||||
**Optional flags:**
|
||||
|
||||
- `--branch`: Git branch to use (`main` or `nightly`, default: `main`)
|
||||
- `--domain`: Domain name for HTTPS (enables ACME/Let's Encrypt) - see [HTTPS Setup](#https-setup-with-domain) below
|
||||
- `--bootstrap-join`: Raft join address for secondary bootstrap nodes
|
||||
- `--force`: Reconfigure all settings (use with caution)
|
||||
|
||||
#### Secondary Bootstrap Node
|
||||
|
||||
Create a secondary bootstrap node that joins an existing Raft cluster:
|
||||
|
||||
```bash
|
||||
sudo dbn prod install \
|
||||
--bootstrap \
|
||||
--vps-ip <your_public_ip> \
|
||||
--bootstrap-join <primary_bootstrap_ip>:7001 \
|
||||
--branch nightly
|
||||
```
|
||||
|
||||
### Branch Selection
|
||||
|
||||
DeBros Network supports two branches:
|
||||
|
||||
- **`main`**: Stable releases (default). Recommended for production.
|
||||
- **`nightly`**: Latest development builds. Use for testing new features.
|
||||
|
||||
**Branch preference is saved automatically** during installation. Future upgrades will use the same branch unless you override it with `--branch`.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Install with nightly branch
|
||||
sudo dbn prod install --bootstrap --branch nightly
|
||||
|
||||
# Upgrade using saved branch preference
|
||||
sudo dbn prod upgrade --restart
|
||||
|
||||
# Upgrade and switch to main branch
|
||||
sudo dbn prod upgrade --restart --branch main
|
||||
# View logs
|
||||
orama logs node --follow
|
||||
orama logs gateway --follow
|
||||
orama logs ipfs --follow
|
||||
```
|
||||
|
||||
### Upgrade
|
||||
|
||||
Upgrade an existing installation to the latest version:
|
||||
|
||||
```bash
|
||||
# Upgrade using saved branch preference
|
||||
sudo dbn prod upgrade --restart
|
||||
|
||||
# Upgrade and switch branches
|
||||
sudo dbn prod upgrade --restart --branch nightly
|
||||
|
||||
# Upgrade without restarting services
|
||||
sudo dbn prod upgrade
|
||||
# Upgrade to latest version
|
||||
sudo orama upgrade --interactive
|
||||
```
|
||||
|
||||
The upgrade process:
|
||||
## Configuration
|
||||
|
||||
1. ✅ Checks prerequisites
|
||||
2. ✅ Updates binaries (fetches latest from selected branch)
|
||||
3. ✅ Preserves existing configurations and data
|
||||
4. ✅ Updates configurations to latest format
|
||||
5. ✅ Updates systemd service files
|
||||
6. ✅ Optionally restarts services (`--restart` flag)
|
||||
All configuration lives in `~/.orama/`:
|
||||
|
||||
**Note**: The upgrade automatically detects your node type (bootstrap vs. regular node) and preserves all secrets, data, and configurations.
|
||||
|
||||
**Note**: Currently, the `upgrade` command does not support adding a domain via `--domain` flag. To enable HTTPS after installation, see [Adding Domain After Installation](#adding-domain-after-installation) below.
|
||||
|
||||
### HTTPS Setup with Domain
|
||||
|
||||
DeBros Gateway supports automatic HTTPS with Let's Encrypt certificates via ACME. This enables secure connections on ports 80 (HTTP redirect) and 443 (HTTPS).
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- Domain name pointing to your server's public IP address
|
||||
- Ports 80 and 443 open and accessible from the internet
|
||||
- Gateway service running
|
||||
|
||||
#### Adding Domain During Installation
|
||||
|
||||
Specify your domain during installation:
|
||||
|
||||
```bash
|
||||
# Bootstrap node with HTTPS
|
||||
sudo dbn prod install --bootstrap --domain node-kv4la8.debros.network --branch nightly
|
||||
|
||||
# Secondary node with HTTPS
|
||||
sudo dbn prod install \
|
||||
--vps-ip <your_public_ip> \
|
||||
--peers /ip4/<bootstrap_ip>/tcp/4001/p2p/<peer_id> \
|
||||
--domain example.com \
|
||||
--branch nightly
|
||||
```
|
||||
|
||||
The gateway will automatically:
|
||||
|
||||
- Obtain Let's Encrypt certificates via ACME
|
||||
- Serve HTTP on port 80 (redirects to HTTPS)
|
||||
- Serve HTTPS on port 443
|
||||
- Renew certificates automatically
|
||||
|
||||
#### Adding Domain After Installation
|
||||
|
||||
Currently, the `upgrade` command doesn't support `--domain` flag. To enable HTTPS on an existing installation:
|
||||
|
||||
1. **Edit the gateway configuration:**
|
||||
|
||||
```bash
|
||||
sudo nano /home/debros/.debros/data/gateway.yaml
|
||||
```
|
||||
|
||||
2. **Update the configuration:**
|
||||
|
||||
```yaml
|
||||
listen_addr: ":6001"
|
||||
client_namespace: "default"
|
||||
rqlite_dsn: ""
|
||||
bootstrap_peers: []
|
||||
enable_https: true
|
||||
domain_name: "your-domain.com"
|
||||
tls_cache_dir: "/home/debros/.debros/tls-cache"
|
||||
olric_servers:
|
||||
- "127.0.0.1:3320"
|
||||
olric_timeout: "10s"
|
||||
ipfs_cluster_api_url: "http://localhost:9094"
|
||||
ipfs_api_url: "http://localhost:4501"
|
||||
ipfs_timeout: "60s"
|
||||
ipfs_replication_factor: 3
|
||||
```
|
||||
|
||||
3. **Ensure ports 80 and 443 are available:**
|
||||
|
||||
```bash
|
||||
# Check if ports are in use
|
||||
sudo lsof -i :80
|
||||
sudo lsof -i :443
|
||||
|
||||
# If needed, stop conflicting services
|
||||
```
|
||||
|
||||
4. **Restart the gateway:**
|
||||
|
||||
```bash
|
||||
sudo systemctl restart debros-gateway.service
|
||||
```
|
||||
|
||||
5. **Verify HTTPS is working:**
|
||||
|
||||
```bash
|
||||
# Check gateway logs
|
||||
sudo journalctl -u debros-gateway.service -f
|
||||
|
||||
# Test HTTPS endpoint
|
||||
curl https://your-domain.com/health
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
|
||||
- The gateway will automatically obtain Let's Encrypt certificates on first start
|
||||
- Certificates are cached in `/home/debros/.debros/tls-cache`
|
||||
- Certificate renewal happens automatically
|
||||
- Ensure your domain's DNS A record points to the server's public IP before enabling HTTPS
|
||||
|
||||
### Service Management
|
||||
|
||||
All services run as systemd units under the `debros` user.
|
||||
|
||||
#### Check Status
|
||||
|
||||
```bash
|
||||
# View status of all services
|
||||
dbn prod status
|
||||
|
||||
# Or use systemctl directly
|
||||
systemctl status debros-node-bootstrap
|
||||
systemctl status debros-ipfs-bootstrap
|
||||
systemctl status debros-gateway
|
||||
```
|
||||
|
||||
#### View Logs
|
||||
|
||||
```bash
|
||||
# View recent logs (last 50 lines)
|
||||
dbn prod logs node
|
||||
|
||||
# Follow logs in real-time
|
||||
dbn prod logs node --follow
|
||||
|
||||
# View specific service logs
|
||||
dbn prod logs ipfs --follow
|
||||
dbn prod logs ipfs-cluster --follow
|
||||
dbn prod logs rqlite --follow
|
||||
dbn prod logs olric --follow
|
||||
dbn prod logs gateway --follow
|
||||
```
|
||||
|
||||
**Available log service names:**
|
||||
|
||||
- `node` - DeBros Network Node (bootstrap or regular)
|
||||
- `ipfs` - IPFS Daemon
|
||||
- `ipfs-cluster` - IPFS Cluster Service
|
||||
- `rqlite` - RQLite Database
|
||||
- `olric` - Olric Cache Server
|
||||
- `gateway` - DeBros Gateway
|
||||
|
||||
**Note:** The `logs` command uses journalctl and accepts the full systemd service name. Use the short names above for convenience.
|
||||
|
||||
#### Service Control Commands
|
||||
|
||||
Use `dbn prod` commands for convenient service management:
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
sudo dbn prod start
|
||||
|
||||
# Stop all services
|
||||
sudo dbn prod stop
|
||||
|
||||
# Restart all services
|
||||
sudo dbn prod restart
|
||||
```
|
||||
|
||||
Or use `systemctl` directly for more control:
|
||||
|
||||
```bash
|
||||
# Restart all services
|
||||
sudo systemctl restart debros-*
|
||||
|
||||
# Restart specific service
|
||||
sudo systemctl restart debros-node-bootstrap
|
||||
|
||||
# Stop services
|
||||
sudo systemctl stop debros-*
|
||||
|
||||
# Start services
|
||||
sudo systemctl start debros-*
|
||||
|
||||
# Enable services (start on boot)
|
||||
sudo systemctl enable debros-*
|
||||
```
|
||||
|
||||
### Complete Production Commands Reference
|
||||
|
||||
#### Installation & Upgrade
|
||||
|
||||
```bash
|
||||
# Install bootstrap node
|
||||
sudo dbn prod install --bootstrap [--domain DOMAIN] [--branch BRANCH]
|
||||
|
||||
|
||||
sudo dbn prod install --nightly --domain node-gh38V1.debros.network --vps-ip 57.128.223.92 --ignore-resource-checks --bootstrap-join
|
||||
|
||||
# Install secondary node
|
||||
sudo dbn prod install --vps-ip IP --peers ADDRS [--domain DOMAIN] [--branch BRANCH]
|
||||
|
||||
# Install secondary bootstrap
|
||||
sudo dbn prod install --bootstrap --vps-ip IP --bootstrap-join ADDR [--domain DOMAIN] [--branch BRANCH]
|
||||
|
||||
# Upgrade installation
|
||||
sudo dbn prod upgrade [--restart] [--branch BRANCH]
|
||||
```
|
||||
|
||||
#### Service Management
|
||||
|
||||
```bash
|
||||
# Check service status (no sudo required)
|
||||
dbn prod status
|
||||
|
||||
# Start all services
|
||||
sudo dbn prod start
|
||||
|
||||
# Stop all services
|
||||
sudo dbn prod stop
|
||||
|
||||
# Restart all services
|
||||
sudo dbn prod restart
|
||||
```
|
||||
|
||||
#### Logs
|
||||
|
||||
```bash
|
||||
# View recent logs
|
||||
dbn prod logs <service>
|
||||
|
||||
# Follow logs in real-time
|
||||
dbn prod logs <service> --follow
|
||||
|
||||
# Available services: node, ipfs, ipfs-cluster, rqlite, olric, gateway
|
||||
```
|
||||
|
||||
#### Uninstall
|
||||
|
||||
```bash
|
||||
# Remove all services (preserves data and configs)
|
||||
sudo dbn prod uninstall
|
||||
```
|
||||
|
||||
### Directory Structure
|
||||
|
||||
Production installations use `/home/debros/.debros/`:
|
||||
|
||||
```
|
||||
/home/debros/.debros/
|
||||
├── configs/ # Configuration files
|
||||
│ ├── bootstrap.yaml # Bootstrap node config
|
||||
│ ├── node.yaml # Regular node config
|
||||
│ ├── gateway.yaml # Gateway config
|
||||
│ └── olric/ # Olric cache config
|
||||
├── data/ # Runtime data
|
||||
│ ├── bootstrap/ # Bootstrap node data
|
||||
│ │ ├── ipfs/ # IPFS repository
|
||||
│ │ ├── ipfs-cluster/ # IPFS Cluster data
|
||||
│ │ └── rqlite/ # RQLite database
|
||||
│ └── node/ # Regular node data
|
||||
├── secrets/ # Secrets and keys
|
||||
│ ├── cluster-secret # IPFS Cluster secret
|
||||
│ └── swarm.key # IPFS swarm key
|
||||
├── logs/ # Service logs
|
||||
│ ├── node-bootstrap.log
|
||||
│ ├── ipfs-bootstrap.log
|
||||
│ └── gateway.log
|
||||
└── .branch # Saved branch preference
|
||||
```
|
||||
|
||||
### Uninstall
|
||||
|
||||
Remove all production services (preserves data and configs):
|
||||
|
||||
```bash
|
||||
sudo dbn prod uninstall
|
||||
```
|
||||
|
||||
This stops and removes all systemd services but keeps `/home/debros/.debros/` intact. You'll be prompted to confirm before uninstalling.
|
||||
|
||||
**To completely remove everything:**
|
||||
|
||||
```bash
|
||||
sudo dbn prod uninstall
|
||||
sudo rm -rf /home/debros/.debros
|
||||
```
|
||||
|
||||
### Production Troubleshooting
|
||||
|
||||
#### Services Not Starting
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
systemctl status debros-node-bootstrap
|
||||
|
||||
# View detailed logs
|
||||
journalctl -u debros-node-bootstrap -n 100
|
||||
|
||||
# Check log files
|
||||
tail -f /home/debros/.debros/logs/node-bootstrap.log
|
||||
```
|
||||
|
||||
#### Configuration Issues
|
||||
|
||||
```bash
|
||||
# Verify configs exist
|
||||
ls -la /home/debros/.debros/configs/
|
||||
|
||||
# Regenerate configs (preserves secrets)
|
||||
sudo dbn prod upgrade --restart
|
||||
```
|
||||
|
||||
#### IPFS AutoConf Errors
|
||||
|
||||
If you see "AutoConf.Enabled=false but 'auto' placeholder is used" errors, the upgrade process should fix this automatically. If not:
|
||||
|
||||
```bash
|
||||
# Re-run upgrade to fix IPFS config
|
||||
sudo dbn prod upgrade --restart
|
||||
```
|
||||
|
||||
#### Port Conflicts
|
||||
|
||||
```bash
|
||||
# Check what's using ports
|
||||
sudo lsof -i :4001 # P2P port
|
||||
sudo lsof -i :5001 # RQLite HTTP
|
||||
sudo lsof -i :6001 # Gateway
|
||||
```
|
||||
|
||||
#### Reset Installation
|
||||
|
||||
To start fresh (⚠️ **destroys all data**):
|
||||
|
||||
```bash
|
||||
sudo dbn prod uninstall
|
||||
sudo rm -rf /home/debros/.debros
|
||||
sudo dbn prod install --bootstrap --branch nightly
|
||||
```
|
||||
|
||||
## Components & Ports
|
||||
|
||||
- **Bootstrap node**: P2P `4001`, RQLite HTTP `5001`, Raft `7001`
|
||||
- **Additional nodes** (`node2`, `node3`): Incrementing ports (`400{2,3}`, `500{2,3}`, `700{2,3}`)
|
||||
- **Gateway**: HTTP `6001` exposes REST/WebSocket APIs
|
||||
- **Data directory**: `~/.debros/` stores configs, identities, and RQLite data
|
||||
|
||||
Use `make dev` for the complete stack or run binaries individually with `go run ./cmd/node --config <file>` and `go run ./cmd/gateway --config gateway.yaml`.
|
||||
|
||||
## Configuration Cheatsheet
|
||||
|
||||
All runtime configuration lives in `~/.debros/`.
|
||||
|
||||
- `bootstrap.yaml`: `type: bootstrap`, optionally set `database.rqlite_join_address` to join another bootstrap's cluster
|
||||
- `node*.yaml`: `type: node`, set `database.rqlite_join_address` (e.g. `localhost:7001`) and include the bootstrap `discovery.bootstrap_peers`
|
||||
- `gateway.yaml`: configure `gateway.bootstrap_peers`, `gateway.namespace`, and optional auth flags
|
||||
|
||||
Validation reminders:
|
||||
|
||||
- HTTP and Raft ports must differ
|
||||
- Non-bootstrap nodes require a join address and bootstrap peers
|
||||
- Bootstrap nodes can optionally define a join address to synchronize with another bootstrap
|
||||
- Multiaddrs must end with `/p2p/<peerID>`
|
||||
|
||||
Regenerate configs any time with `./bin/dbn config init --force`.
|
||||
|
||||
## CLI Highlights
|
||||
|
||||
All commands accept `--format json`, `--timeout <duration>`, and `--bootstrap <multiaddr>`.
|
||||
|
||||
- **Auth**
|
||||
|
||||
```bash
|
||||
./bin/dbn auth login
|
||||
./bin/dbn auth status
|
||||
./bin/dbn auth logout
|
||||
```
|
||||
|
||||
- **Network**
|
||||
|
||||
```bash
|
||||
./bin/dbn health
|
||||
./bin/dbn status
|
||||
./bin/dbn peers
|
||||
```
|
||||
|
||||
- **Database**
|
||||
|
||||
```bash
|
||||
./bin/dbn query "SELECT * FROM users"
|
||||
./bin/dbn query "CREATE TABLE users (id INTEGER PRIMARY KEY)"
|
||||
./bin/dbn transaction --file ops.json
|
||||
```
|
||||
|
||||
- **Pub/Sub**
|
||||
|
||||
```bash
|
||||
./bin/dbn pubsub publish <topic> <message>
|
||||
./bin/dbn pubsub subscribe <topic> 30s
|
||||
./bin/dbn pubsub topics
|
||||
```
|
||||
|
||||
Credentials live at `~/.debros/credentials.json` with user-only permissions.
|
||||
|
||||
## HTTP Gateway
|
||||
|
||||
Start locally with `make run-gateway` or `go run ./cmd/gateway --config gateway.yaml`.
|
||||
|
||||
Environment overrides:
|
||||
|
||||
```bash
|
||||
export GATEWAY_ADDR="0.0.0.0:6001"
|
||||
export GATEWAY_NAMESPACE="my-app"
|
||||
export GATEWAY_BOOTSTRAP_PEERS="/ip4/localhost/tcp/4001/p2p/<peerID>"
|
||||
export GATEWAY_REQUIRE_AUTH=true
|
||||
export GATEWAY_API_KEYS="key1:namespace1,key2:namespace2"
|
||||
```
|
||||
|
||||
Common endpoints (see `openapi/gateway.yaml` for the full spec):
|
||||
|
||||
- `GET /health`, `GET /v1/status`, `GET /v1/version`
|
||||
- `POST /v1/auth/challenge`, `POST /v1/auth/verify`, `POST /v1/auth/refresh`
|
||||
- `POST /v1/rqlite/exec`, `POST /v1/rqlite/find`, `POST /v1/rqlite/select`, `POST /v1/rqlite/transaction`
|
||||
- `GET /v1/rqlite/schema`
|
||||
- `POST /v1/pubsub/publish`, `GET /v1/pubsub/topics`, `GET /v1/pubsub/ws?topic=<topic>`
|
||||
- `POST /v1/storage/upload`, `POST /v1/storage/pin`, `GET /v1/storage/status/:cid`, `GET /v1/storage/get/:cid`, `DELETE /v1/storage/unpin/:cid`
|
||||
- `configs/node.yaml` - Node configuration
|
||||
- `configs/gateway.yaml` - Gateway configuration
|
||||
- `configs/olric.yaml` - Cache configuration
|
||||
- `secrets/` - Keys and certificates
|
||||
- `data/` - Service data directories
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Config directory errors**: Ensure `~/.debros/` exists, is writable, and has free disk space (`touch ~/.debros/test && rm ~/.debros/test`).
|
||||
- **Port conflicts**: Inspect with `lsof -i :4001` (or other ports) and stop conflicting processes or regenerate configs with new ports.
|
||||
- **Missing configs**: Run `./bin/dbn config init` before starting nodes.
|
||||
- **Cluster join issues**: Confirm the bootstrap node is running, `peer.info` multiaddr matches `bootstrap_peers`, and firewall rules allow the P2P ports.
|
||||
### Services Not Starting
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
systemctl status debros-node
|
||||
|
||||
# View logs
|
||||
journalctl -u debros-node -f
|
||||
|
||||
# Check log files
|
||||
tail -f /home/debros/.orama/logs/node.log
|
||||
```
|
||||
|
||||
### Port Conflicts
|
||||
|
||||
```bash
|
||||
# Check what's using specific ports
|
||||
sudo lsof -i :443 # HTTPS Gateway
|
||||
sudo lsof -i :7001 # TCP/SNI Gateway
|
||||
sudo lsof -i :6001 # Internal Gateway
|
||||
```
|
||||
|
||||
### RQLite Cluster Issues
|
||||
|
||||
```bash
|
||||
# Connect to RQLite CLI
|
||||
rqlite -H localhost -p 5001
|
||||
|
||||
# Check cluster status
|
||||
.nodes
|
||||
.status
|
||||
.ready
|
||||
|
||||
# Check consistency level
|
||||
.consistency
|
||||
```
|
||||
|
||||
### Reset Installation
|
||||
|
||||
```bash
|
||||
# Production reset (⚠️ DESTROYS DATA)
|
||||
sudo orama uninstall
|
||||
sudo rm -rf /home/debros/.orama
|
||||
sudo orama install
|
||||
```
|
||||
|
||||
## HTTP Gateway API
|
||||
|
||||
### Main Gateway Endpoints
|
||||
|
||||
- `GET /health` - Health status
|
||||
- `GET /v1/status` - Full status
|
||||
- `GET /v1/version` - Version info
|
||||
- `POST /v1/rqlite/exec` - Execute SQL
|
||||
- `POST /v1/rqlite/query` - Query database
|
||||
- `GET /v1/rqlite/schema` - Get schema
|
||||
- `POST /v1/pubsub/publish` - Publish message
|
||||
- `GET /v1/pubsub/topics` - List topics
|
||||
- `GET /v1/pubsub/ws?topic=<name>` - WebSocket subscribe
|
||||
- `POST /v1/functions` - Deploy function (multipart/form-data)
|
||||
- `POST /v1/functions/{name}/invoke` - Invoke function
|
||||
- `GET /v1/functions` - List functions
|
||||
- `DELETE /v1/functions/{name}` - Delete function
|
||||
- `GET /v1/functions/{name}/logs` - Get function logs
|
||||
|
||||
See `openapi/gateway.yaml` for complete API specification.
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Architecture Guide](docs/ARCHITECTURE.md)** - System architecture and design patterns
|
||||
- **[Client SDK](docs/CLIENT_SDK.md)** - Go SDK documentation and examples
|
||||
- **[Gateway API](docs/GATEWAY_API.md)** - Complete HTTP API reference
|
||||
- **[Security Deployment](docs/SECURITY_DEPLOYMENT_GUIDE.md)** - Production security hardening
|
||||
|
||||
## Resources
|
||||
|
||||
- Go modules: `go mod tidy`, `go test ./...`
|
||||
- Automation: `make build`, `make dev`, `make run-gateway`, `make lint`
|
||||
- API reference: `openapi/gateway.yaml`
|
||||
- Code of Conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
||||
- [RQLite Documentation](https://rqlite.io/docs/)
|
||||
- [IPFS Documentation](https://docs.ipfs.tech/)
|
||||
- [LibP2P Documentation](https://docs.libp2p.io/)
|
||||
- [WebAssembly](https://webassembly.org/)
|
||||
- [GitHub Repository](https://github.com/DeBrosOfficial/network)
|
||||
- [Issue Tracker](https://github.com/DeBrosOfficial/network/issues)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
network/
|
||||
├── cmd/ # Binary entry points
|
||||
│ ├── cli/ # CLI tool
|
||||
│ ├── gateway/ # HTTP Gateway
|
||||
│ ├── node/ # P2P Node
|
||||
│ └── rqlite-mcp/ # RQLite MCP server
|
||||
├── pkg/ # Core packages
|
||||
│ ├── gateway/ # Gateway implementation
|
||||
│ │ └── handlers/ # HTTP handlers by domain
|
||||
│ ├── client/ # Go SDK
|
||||
│ ├── serverless/ # WASM engine
|
||||
│ ├── rqlite/ # Database ORM
|
||||
│ ├── contracts/ # Interface definitions
|
||||
│ ├── httputil/ # HTTP utilities
|
||||
│ └── errors/ # Error handling
|
||||
├── docs/ # Documentation
|
||||
├── e2e/ # End-to-end tests
|
||||
└── examples/ # Example code
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! This project follows:
|
||||
- **SOLID Principles** - Single responsibility, open/closed, etc.
|
||||
- **DRY Principle** - Don't repeat yourself
|
||||
- **Clean Architecture** - Clear separation of concerns
|
||||
- **Test Coverage** - Unit and E2E tests required
|
||||
|
||||
See our architecture docs for design patterns and guidelines.
|
||||
|
||||
@ -34,7 +34,7 @@ func main() {
|
||||
|
||||
switch command {
|
||||
case "version":
|
||||
fmt.Printf("dbn %s", version)
|
||||
fmt.Printf("orama %s", version)
|
||||
if commit != "" {
|
||||
fmt.Printf(" (commit %s)", commit)
|
||||
}
|
||||
@ -48,10 +48,30 @@ func main() {
|
||||
case "dev":
|
||||
cli.HandleDevCommand(args)
|
||||
|
||||
// Production environment commands
|
||||
// Production environment commands (legacy with 'prod' prefix)
|
||||
case "prod":
|
||||
cli.HandleProdCommand(args)
|
||||
|
||||
// Direct production commands (new simplified interface)
|
||||
case "install":
|
||||
cli.HandleProdCommand(append([]string{"install"}, args...))
|
||||
case "upgrade":
|
||||
cli.HandleProdCommand(append([]string{"upgrade"}, args...))
|
||||
case "migrate":
|
||||
cli.HandleProdCommand(append([]string{"migrate"}, args...))
|
||||
case "status":
|
||||
cli.HandleProdCommand(append([]string{"status"}, args...))
|
||||
case "start":
|
||||
cli.HandleProdCommand(append([]string{"start"}, args...))
|
||||
case "stop":
|
||||
cli.HandleProdCommand(append([]string{"stop"}, args...))
|
||||
case "restart":
|
||||
cli.HandleProdCommand(append([]string{"restart"}, args...))
|
||||
case "logs":
|
||||
cli.HandleProdCommand(append([]string{"logs"}, args...))
|
||||
case "uninstall":
|
||||
cli.HandleProdCommand(append([]string{"uninstall"}, args...))
|
||||
|
||||
// Authentication commands
|
||||
case "auth":
|
||||
cli.HandleAuthCommand(args)
|
||||
@ -85,8 +105,8 @@ func parseGlobalFlags(args []string) {
|
||||
}
|
||||
|
||||
func showHelp() {
|
||||
fmt.Printf("Network CLI - Distributed P2P Network Management Tool\n\n")
|
||||
fmt.Printf("Usage: dbn <command> [args...]\n\n")
|
||||
fmt.Printf("Orama CLI - Distributed P2P Network Management Tool\n\n")
|
||||
fmt.Printf("Usage: orama <command> [args...]\n\n")
|
||||
|
||||
fmt.Printf("💻 Local Development:\n")
|
||||
fmt.Printf(" dev up - Start full local dev environment\n")
|
||||
@ -96,15 +116,14 @@ func showHelp() {
|
||||
fmt.Printf(" dev help - Show dev command help\n\n")
|
||||
|
||||
fmt.Printf("🚀 Production Deployment:\n")
|
||||
fmt.Printf(" prod install [--bootstrap] - Full production bootstrap (requires root/sudo)\n")
|
||||
fmt.Printf(" prod upgrade - Upgrade existing installation\n")
|
||||
fmt.Printf(" prod status - Show production service status\n")
|
||||
fmt.Printf(" prod start - Start all production services (requires root/sudo)\n")
|
||||
fmt.Printf(" prod stop - Stop all production services (requires root/sudo)\n")
|
||||
fmt.Printf(" prod restart - Restart all production services (requires root/sudo)\n")
|
||||
fmt.Printf(" prod logs <service> - View production service logs\n")
|
||||
fmt.Printf(" prod uninstall - Remove production services (requires root/sudo)\n")
|
||||
fmt.Printf(" prod help - Show prod command help\n\n")
|
||||
fmt.Printf(" install - Install production node (requires root/sudo)\n")
|
||||
fmt.Printf(" upgrade - Upgrade existing installation\n")
|
||||
fmt.Printf(" status - Show production service status\n")
|
||||
fmt.Printf(" start - Start all production services (requires root/sudo)\n")
|
||||
fmt.Printf(" stop - Stop all production services (requires root/sudo)\n")
|
||||
fmt.Printf(" restart - Restart all production services (requires root/sudo)\n")
|
||||
fmt.Printf(" logs <service> - View production service logs\n")
|
||||
fmt.Printf(" uninstall - Remove production services (requires root/sudo)\n\n")
|
||||
|
||||
fmt.Printf("🔐 Authentication:\n")
|
||||
fmt.Printf(" auth login - Authenticate with wallet\n")
|
||||
@ -119,16 +138,14 @@ func showHelp() {
|
||||
fmt.Printf(" --help, -h - Show this help message\n\n")
|
||||
|
||||
fmt.Printf("Examples:\n")
|
||||
fmt.Printf(" # Authenticate\n")
|
||||
fmt.Printf(" dbn auth login\n\n")
|
||||
fmt.Printf(" # First node (creates new cluster)\n")
|
||||
fmt.Printf(" sudo orama install --vps-ip 203.0.113.1 --domain node-1.orama.network\n\n")
|
||||
|
||||
fmt.Printf(" # Start local dev environment\n")
|
||||
fmt.Printf(" dbn dev up\n")
|
||||
fmt.Printf(" dbn dev status\n\n")
|
||||
fmt.Printf(" # Join existing cluster\n")
|
||||
fmt.Printf(" sudo orama install --vps-ip 203.0.113.2 --domain node-2.orama.network \\\n")
|
||||
fmt.Printf(" --peers /ip4/203.0.113.1/tcp/4001/p2p/12D3KooW... --cluster-secret <hex>\n\n")
|
||||
|
||||
fmt.Printf(" # Production deployment (requires root/sudo)\n")
|
||||
fmt.Printf(" sudo dbn prod install --bootstrap\n")
|
||||
fmt.Printf(" sudo dbn prod upgrade\n")
|
||||
fmt.Printf(" dbn prod status\n")
|
||||
fmt.Printf(" dbn prod logs node --follow\n")
|
||||
fmt.Printf(" # Service management\n")
|
||||
fmt.Printf(" orama status\n")
|
||||
fmt.Printf(" orama logs node --follow\n")
|
||||
}
|
||||
|
||||
@ -40,11 +40,11 @@ func getEnvBoolDefault(key string, def bool) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// parseGatewayConfig loads gateway.yaml from ~/.debros exclusively.
|
||||
// parseGatewayConfig loads gateway.yaml from ~/.orama exclusively.
|
||||
// It accepts an optional --config flag for absolute paths (used by systemd services).
|
||||
func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
// Parse --config flag (optional, for systemd services that pass absolute paths)
|
||||
configFlag := flag.String("config", "", "Config file path (absolute path or filename in ~/.debros)")
|
||||
configFlag := flag.String("config", "", "Config file path (absolute path or filename in ~/.orama)")
|
||||
flag.Parse()
|
||||
|
||||
// Determine config path
|
||||
@ -63,7 +63,7 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Default behavior: look for gateway.yaml in ~/.debros/data/, ~/.debros/configs/, or ~/.debros/
|
||||
// Default behavior: look for gateway.yaml in ~/.orama/data/, ~/.orama/configs/, or ~/.orama/
|
||||
configPath, err = config.DefaultPath("gateway.yaml")
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "Failed to determine config path", zap.Error(err))
|
||||
@ -77,7 +77,7 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
ListenAddr string `yaml:"listen_addr"`
|
||||
ClientNamespace string `yaml:"client_namespace"`
|
||||
RQLiteDSN string `yaml:"rqlite_dsn"`
|
||||
BootstrapPeers []string `yaml:"bootstrap_peers"`
|
||||
Peers []string `yaml:"bootstrap_peers"`
|
||||
EnableHTTPS bool `yaml:"enable_https"`
|
||||
DomainName string `yaml:"domain_name"`
|
||||
TLSCacheDir string `yaml:"tls_cache_dir"`
|
||||
@ -133,16 +133,16 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
if v := strings.TrimSpace(y.RQLiteDSN); v != "" {
|
||||
cfg.RQLiteDSN = v
|
||||
}
|
||||
if len(y.BootstrapPeers) > 0 {
|
||||
var bp []string
|
||||
for _, p := range y.BootstrapPeers {
|
||||
if len(y.Peers) > 0 {
|
||||
var peers []string
|
||||
for _, p := range y.Peers {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
bp = append(bp, p)
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
if len(bp) > 0 {
|
||||
cfg.BootstrapPeers = bp
|
||||
if len(peers) > 0 {
|
||||
cfg.BootstrapPeers = peers
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,7 +157,7 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
// Default TLS cache directory if HTTPS is enabled but not specified
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
cfg.TLSCacheDir = filepath.Join(homeDir, ".debros", "tls-cache")
|
||||
cfg.TLSCacheDir = filepath.Join(homeDir, ".orama", "tls-cache")
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,7 +205,7 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
zap.String("path", configPath),
|
||||
zap.String("addr", cfg.ListenAddr),
|
||||
zap.String("namespace", cfg.ClientNamespace),
|
||||
zap.Int("bootstrap_peer_count", len(cfg.BootstrapPeers)),
|
||||
zap.Int("peer_count", len(cfg.BootstrapPeers)),
|
||||
)
|
||||
|
||||
return cfg
|
||||
|
||||
@ -33,7 +33,7 @@ func setup_logger(component logging.Component) (logger *logging.ColoredLogger) {
|
||||
|
||||
// parse_flags parses command-line flags and returns them.
|
||||
func parse_flags() (configName *string, help *bool) {
|
||||
configName = flag.String("config", "node.yaml", "Config filename in ~/.debros (default: node.yaml)")
|
||||
configName = flag.String("config", "node.yaml", "Config filename in ~/.orama (default: node.yaml)")
|
||||
help = flag.Bool("help", false, "Show help")
|
||||
flag.Parse()
|
||||
|
||||
@ -63,7 +63,7 @@ func check_if_should_open_help(help *bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// select_data_dir validates that we can load the config from ~/.debros
|
||||
// select_data_dir validates that we can load the config from ~/.orama
|
||||
func select_data_dir_check(configName *string) {
|
||||
logger := setup_logger(logging.ComponentNode)
|
||||
|
||||
@ -102,8 +102,8 @@ func select_data_dir_check(configName *string) {
|
||||
fmt.Fprintf(os.Stderr, "\n❌ Configuration Error:\n")
|
||||
fmt.Fprintf(os.Stderr, "Config file not found at %s\n", configPath)
|
||||
fmt.Fprintf(os.Stderr, "\nGenerate it with one of:\n")
|
||||
fmt.Fprintf(os.Stderr, " dbn config init --type bootstrap\n")
|
||||
fmt.Fprintf(os.Stderr, " dbn config init --type node --bootstrap-peers '<peer_multiaddr>'\n")
|
||||
fmt.Fprintf(os.Stderr, " orama config init --type node\n")
|
||||
fmt.Fprintf(os.Stderr, " orama config init --type node --peers '<peer_multiaddr>'\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@ -135,7 +135,7 @@ func startNode(ctx context.Context, cfg *config.Config, port int) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Save the peer ID to a file for CLI access (especially useful for bootstrap)
|
||||
// Save the peer ID to a file for CLI access
|
||||
peerID := n.GetPeerID()
|
||||
peerInfoFile := filepath.Join(dataDir, "peer.info")
|
||||
|
||||
@ -163,7 +163,7 @@ func startNode(ctx context.Context, cfg *config.Config, port int) error {
|
||||
logger.Error("Failed to save peer info: %v", zap.Error(err))
|
||||
} else {
|
||||
logger.Info("Peer info saved to: %s", zap.String("path", peerInfoFile))
|
||||
logger.Info("Bootstrap multiaddr: %s", zap.String("path", peerMultiaddr))
|
||||
logger.Info("Peer multiaddr: %s", zap.String("path", peerMultiaddr))
|
||||
}
|
||||
|
||||
logger.Info("Node started successfully")
|
||||
@ -272,7 +272,7 @@ func main() {
|
||||
// Absolute path passed directly (e.g., from systemd service)
|
||||
configPath = *configName
|
||||
} else {
|
||||
// Relative path - use DefaultPath which checks both ~/.debros/configs/ and ~/.debros/
|
||||
// Relative path - use DefaultPath which checks both ~/.orama/configs/ and ~/.orama/
|
||||
configPath, err = config.DefaultPath(*configName)
|
||||
if err != nil {
|
||||
logger.Error("Failed to determine config path", zap.Error(err))
|
||||
@ -316,7 +316,7 @@ func main() {
|
||||
zap.Strings("listen_addresses", cfg.Node.ListenAddresses),
|
||||
zap.Int("rqlite_http_port", cfg.Database.RQLitePort),
|
||||
zap.Int("rqlite_raft_port", cfg.Database.RQLiteRaftPort),
|
||||
zap.Strings("bootstrap_peers", cfg.Discovery.BootstrapPeers),
|
||||
zap.Strings("peers", cfg.Discovery.BootstrapPeers),
|
||||
zap.String("rqlite_join_address", cfg.Database.RQLiteJoinAddress),
|
||||
zap.String("data_directory", cfg.Node.DataDir))
|
||||
|
||||
|
||||
320
cmd/rqlite-mcp/main.go
Normal file
320
cmd/rqlite-mcp/main.go
Normal file
@ -0,0 +1,320 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rqlite/gorqlite"
|
||||
)
|
||||
|
||||
// MCP JSON-RPC types
|
||||
type JSONRPCRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID any `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type JSONRPCResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID any `json:"id"`
|
||||
Result any `json:"result,omitempty"`
|
||||
Error *ResponseError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type ResponseError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Tool definition
|
||||
type Tool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema any `json:"inputSchema"`
|
||||
}
|
||||
|
||||
// Tool call types
|
||||
type CallToolRequest struct {
|
||||
Name string `json:"name"`
|
||||
Arguments json.RawMessage `json:"arguments"`
|
||||
}
|
||||
|
||||
type TextContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type CallToolResult struct {
|
||||
Content []TextContent `json:"content"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
type MCPServer struct {
|
||||
conn *gorqlite.Connection
|
||||
}
|
||||
|
||||
func NewMCPServer(rqliteURL string) (*MCPServer, error) {
|
||||
conn, err := gorqlite.Open(rqliteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MCPServer{
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleRequest(req JSONRPCRequest) JSONRPCResponse {
|
||||
var resp JSONRPCResponse
|
||||
resp.JSONRPC = "2.0"
|
||||
resp.ID = req.ID
|
||||
|
||||
// Debug logging disabled to prevent excessive disk writes
|
||||
// log.Printf("Received method: %s", req.Method)
|
||||
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
resp.Result = map[string]any{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": map[string]any{
|
||||
"tools": map[string]any{},
|
||||
},
|
||||
"serverInfo": map[string]any{
|
||||
"name": "rqlite-mcp",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
}
|
||||
|
||||
case "notifications/initialized":
|
||||
// This is a notification, no response needed
|
||||
return JSONRPCResponse{}
|
||||
|
||||
case "tools/list":
|
||||
// Debug logging disabled to prevent excessive disk writes
|
||||
tools := []Tool{
|
||||
{
|
||||
Name: "list_tables",
|
||||
Description: "List all tables in the Rqlite database",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "query",
|
||||
Description: "Run a SELECT query on the Rqlite database",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"sql": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The SQL SELECT query to run",
|
||||
},
|
||||
},
|
||||
"required": []string{"sql"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "execute",
|
||||
Description: "Run an INSERT, UPDATE, or DELETE statement on the Rqlite database",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"sql": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The SQL statement (INSERT, UPDATE, DELETE) to run",
|
||||
},
|
||||
},
|
||||
"required": []string{"sql"},
|
||||
},
|
||||
},
|
||||
}
|
||||
resp.Result = map[string]any{"tools": tools}
|
||||
|
||||
case "tools/call":
|
||||
var callReq CallToolRequest
|
||||
if err := json.Unmarshal(req.Params, &callReq); err != nil {
|
||||
resp.Error = &ResponseError{Code: -32700, Message: "Parse error"}
|
||||
return resp
|
||||
}
|
||||
resp.Result = s.handleToolCall(callReq)
|
||||
|
||||
default:
|
||||
// Debug logging disabled to prevent excessive disk writes
|
||||
resp.Error = &ResponseError{Code: -32601, Message: "Method not found"}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleToolCall(req CallToolRequest) CallToolResult {
|
||||
// Debug logging disabled to prevent excessive disk writes
|
||||
// log.Printf("Tool call: %s", req.Name)
|
||||
|
||||
switch req.Name {
|
||||
case "list_tables":
|
||||
rows, err := s.conn.QueryOne("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
if err != nil {
|
||||
return errorResult(fmt.Sprintf("Error listing tables: %v", err))
|
||||
}
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
slice, err := rows.Slice()
|
||||
if err == nil && len(slice) > 0 {
|
||||
tables = append(tables, fmt.Sprint(slice[0]))
|
||||
}
|
||||
}
|
||||
if len(tables) == 0 {
|
||||
return textResult("No tables found")
|
||||
}
|
||||
return textResult(strings.Join(tables, "\n"))
|
||||
|
||||
case "query":
|
||||
var args struct {
|
||||
SQL string `json:"sql"`
|
||||
}
|
||||
if err := json.Unmarshal(req.Arguments, &args); err != nil {
|
||||
return errorResult(fmt.Sprintf("Invalid arguments: %v", err))
|
||||
}
|
||||
// Debug logging disabled to prevent excessive disk writes
|
||||
rows, err := s.conn.QueryOne(args.SQL)
|
||||
if err != nil {
|
||||
return errorResult(fmt.Sprintf("Query error: %v", err))
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
cols := rows.Columns()
|
||||
result.WriteString(strings.Join(cols, " | ") + "\n")
|
||||
result.WriteString(strings.Repeat("-", len(cols)*10) + "\n")
|
||||
|
||||
rowCount := 0
|
||||
for rows.Next() {
|
||||
vals, err := rows.Slice()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rowCount++
|
||||
for i, v := range vals {
|
||||
if i > 0 {
|
||||
result.WriteString(" | ")
|
||||
}
|
||||
result.WriteString(fmt.Sprint(v))
|
||||
}
|
||||
result.WriteString("\n")
|
||||
}
|
||||
result.WriteString(fmt.Sprintf("\n(%d rows)", rowCount))
|
||||
return textResult(result.String())
|
||||
|
||||
case "execute":
|
||||
var args struct {
|
||||
SQL string `json:"sql"`
|
||||
}
|
||||
if err := json.Unmarshal(req.Arguments, &args); err != nil {
|
||||
return errorResult(fmt.Sprintf("Invalid arguments: %v", err))
|
||||
}
|
||||
// Debug logging disabled to prevent excessive disk writes
|
||||
res, err := s.conn.WriteOne(args.SQL)
|
||||
if err != nil {
|
||||
return errorResult(fmt.Sprintf("Execution error: %v", err))
|
||||
}
|
||||
return textResult(fmt.Sprintf("Rows affected: %d", res.RowsAffected))
|
||||
|
||||
default:
|
||||
return errorResult(fmt.Sprintf("Unknown tool: %s", req.Name))
|
||||
}
|
||||
}
|
||||
|
||||
func textResult(text string) CallToolResult {
|
||||
return CallToolResult{
|
||||
Content: []TextContent{
|
||||
{
|
||||
Type: "text",
|
||||
Text: text,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func errorResult(text string) CallToolResult {
|
||||
return CallToolResult{
|
||||
Content: []TextContent{
|
||||
{
|
||||
Type: "text",
|
||||
Text: text,
|
||||
},
|
||||
},
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Log to stderr so stdout is clean for JSON-RPC
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
rqliteURL := "http://localhost:5001"
|
||||
if u := os.Getenv("RQLITE_URL"); u != "" {
|
||||
rqliteURL = u
|
||||
}
|
||||
|
||||
var server *MCPServer
|
||||
var err error
|
||||
|
||||
// Retry connecting to rqlite
|
||||
maxRetries := 30
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
server, err = NewMCPServer(rqliteURL)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if i%5 == 0 {
|
||||
log.Printf("Waiting for Rqlite at %s... (%d/%d)", rqliteURL, i+1, maxRetries)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to Rqlite after %d retries: %v", maxRetries, err)
|
||||
}
|
||||
|
||||
log.Printf("MCP Rqlite server started (stdio transport)")
|
||||
log.Printf("Connected to Rqlite at %s", rqliteURL)
|
||||
|
||||
// Read JSON-RPC requests from stdin, write responses to stdout
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var req JSONRPCRequest
|
||||
if err := json.Unmarshal([]byte(line), &req); err != nil {
|
||||
// Debug logging disabled to prevent excessive disk writes
|
||||
continue
|
||||
}
|
||||
|
||||
resp := server.handleRequest(req)
|
||||
|
||||
// Don't send response for notifications (no ID)
|
||||
if req.ID == nil && strings.HasPrefix(req.Method, "notifications/") {
|
||||
continue
|
||||
}
|
||||
|
||||
respData, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
// Debug logging disabled to prevent excessive disk writes
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println(string(respData))
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
// Debug logging disabled to prevent excessive disk writes
|
||||
}
|
||||
}
|
||||
19
debian/control
vendored
Normal file
19
debian/control
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
Package: orama
|
||||
Version: 0.69.20
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Depends: libc6
|
||||
Maintainer: DeBros Team <dev@debros.io>
|
||||
Description: Orama Network - Distributed P2P Database System
|
||||
Orama is a distributed peer-to-peer network that combines
|
||||
RQLite for distributed SQL, IPFS for content-addressed storage,
|
||||
and LibP2P for peer discovery and communication.
|
||||
.
|
||||
Features:
|
||||
- Distributed SQLite database with Raft consensus
|
||||
- IPFS-based file storage with encryption
|
||||
- LibP2P peer-to-peer networking
|
||||
- Olric distributed cache
|
||||
- Unified HTTP/HTTPS gateway
|
||||
|
||||
18
debian/postinst
vendored
Normal file
18
debian/postinst
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Post-installation script for orama package
|
||||
|
||||
echo "Orama installed successfully!"
|
||||
echo ""
|
||||
echo "To set up your node, run:"
|
||||
echo " sudo orama install"
|
||||
echo ""
|
||||
echo "This will launch the interactive installer."
|
||||
echo ""
|
||||
echo "For command-line installation:"
|
||||
echo " sudo orama install --vps-ip <your-ip> --domain <your-domain>"
|
||||
echo ""
|
||||
echo "For help:"
|
||||
echo " orama --help"
|
||||
|
||||
435
docs/ARCHITECTURE.md
Normal file
435
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,435 @@
|
||||
# Orama Network Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Orama Network is a high-performance API Gateway and Reverse Proxy designed for a decentralized ecosystem. It serves as a unified entry point that orchestrates traffic between clients and various backend services.
|
||||
|
||||
## Architecture Pattern
|
||||
|
||||
**Modular Gateway / Edge Proxy Architecture**
|
||||
|
||||
The system follows a clean, layered architecture with clear separation of concerns:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Clients │
|
||||
│ (Web, Mobile, CLI, SDKs) │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
│ HTTPS/WSS
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ API Gateway (Port 443) │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ Handlers Layer (HTTP/WebSocket) │ │
|
||||
│ │ - Auth handlers - Storage handlers │ │
|
||||
│ │ - Cache handlers - PubSub handlers │ │
|
||||
│ │ - Serverless - Database handlers │ │
|
||||
│ └──────────────────────┬───────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────▼───────────────────────────────┐ │
|
||||
│ │ Middleware (Security, Auth, Logging) │ │
|
||||
│ └──────────────────────┬───────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────▼───────────────────────────────┐ │
|
||||
│ │ Service Coordination (Gateway Core) │ │
|
||||
│ └──────────────────────┬───────────────────────────────┘ │
|
||||
└─────────────────────────┼────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────┼─────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ RQLite │ │ Olric │ │ IPFS │
|
||||
│ (Database) │ │ (Cache) │ │ (Storage) │
|
||||
│ │ │ │ │ │
|
||||
│ Port 5001 │ │ Port 3320 │ │ Port 4501 │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
|
||||
┌─────────────────┐ ┌──────────────┐
|
||||
│ IPFS Cluster │ │ Serverless │
|
||||
│ (Pinning) │ │ (WASM) │
|
||||
│ │ │ │
|
||||
│ Port 9094 │ │ In-Process │
|
||||
└─────────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. API Gateway (`pkg/gateway/`)
|
||||
|
||||
The gateway is the main entry point for all client requests. It coordinates between various backend services.
|
||||
|
||||
**Key Files:**
|
||||
- `gateway.go` - Core gateway struct and routing
|
||||
- `dependencies.go` - Service initialization and dependency injection
|
||||
- `lifecycle.go` - Start/stop/health lifecycle management
|
||||
- `middleware.go` - Authentication, logging, error handling
|
||||
- `routes.go` - HTTP route registration
|
||||
|
||||
**Handler Packages:**
|
||||
- `handlers/auth/` - Authentication (JWT, API keys, wallet signatures)
|
||||
- `handlers/storage/` - IPFS storage operations
|
||||
- `handlers/cache/` - Distributed cache operations
|
||||
- `handlers/pubsub/` - Pub/sub messaging
|
||||
- `handlers/serverless/` - Serverless function deployment and execution
|
||||
|
||||
### 2. Client SDK (`pkg/client/`)
|
||||
|
||||
Provides a clean Go SDK for interacting with the Orama Network.
|
||||
|
||||
**Architecture:**
|
||||
```go
|
||||
// Main client interface
|
||||
type NetworkClient interface {
|
||||
Storage() StorageClient
|
||||
Cache() CacheClient
|
||||
Database() DatabaseClient
|
||||
PubSub() PubSubClient
|
||||
Serverless() ServerlessClient
|
||||
Auth() AuthClient
|
||||
}
|
||||
```
|
||||
|
||||
**Key Files:**
|
||||
- `client.go` - Main client orchestration
|
||||
- `config.go` - Client configuration
|
||||
- `storage_client.go` - IPFS storage client
|
||||
- `cache_client.go` - Olric cache client
|
||||
- `database_client.go` - RQLite database client
|
||||
- `pubsub_bridge.go` - Pub/sub messaging client
|
||||
- `transport.go` - HTTP transport layer
|
||||
- `errors.go` - Client-specific errors
|
||||
|
||||
**Usage Example:**
|
||||
```go
|
||||
import "github.com/DeBrosOfficial/network/pkg/client"
|
||||
|
||||
// Create client
|
||||
cfg := client.DefaultClientConfig()
|
||||
cfg.GatewayURL = "https://api.orama.network"
|
||||
cfg.APIKey = "your-api-key"
|
||||
|
||||
c := client.NewNetworkClient(cfg)
|
||||
|
||||
// Use storage
|
||||
resp, err := c.Storage().Upload(ctx, data, "file.txt")
|
||||
|
||||
// Use cache
|
||||
err = c.Cache().Set(ctx, "key", value, 0)
|
||||
|
||||
// Query database
|
||||
rows, err := c.Database().Query(ctx, "SELECT * FROM users")
|
||||
|
||||
// Publish message
|
||||
err = c.PubSub().Publish(ctx, "chat", []byte("hello"))
|
||||
|
||||
// Deploy function
|
||||
fn, err := c.Serverless().Deploy(ctx, def, wasmBytes)
|
||||
|
||||
// Invoke function
|
||||
result, err := c.Serverless().Invoke(ctx, "function-name", input)
|
||||
```
|
||||
|
||||
### 3. Database Layer (`pkg/rqlite/`)
|
||||
|
||||
ORM-like interface over RQLite distributed SQL database.
|
||||
|
||||
**Key Files:**
|
||||
- `client.go` - Main ORM client
|
||||
- `orm_types.go` - Interfaces (Client, Tx, Repository[T])
|
||||
- `query_builder.go` - Fluent query builder
|
||||
- `repository.go` - Generic repository pattern
|
||||
- `scanner.go` - Reflection-based row scanning
|
||||
- `transaction.go` - Transaction support
|
||||
|
||||
**Features:**
|
||||
- Fluent query builder
|
||||
- Generic repository pattern with type safety
|
||||
- Automatic struct mapping
|
||||
- Transaction support
|
||||
- Connection pooling with retry
|
||||
|
||||
**Example:**
|
||||
```go
|
||||
// Query builder
|
||||
users, err := client.CreateQueryBuilder("users").
|
||||
Select("id", "name", "email").
|
||||
Where("age > ?", 18).
|
||||
OrderBy("name ASC").
|
||||
Limit(10).
|
||||
GetMany(ctx, &users)
|
||||
|
||||
// Repository pattern
|
||||
type User struct {
|
||||
ID int `db:"id"`
|
||||
Name string `db:"name"`
|
||||
Email string `db:"email"`
|
||||
}
|
||||
|
||||
repo := client.Repository("users")
|
||||
user := &User{Name: "Alice", Email: "alice@example.com"}
|
||||
err := repo.Save(ctx, user)
|
||||
```
|
||||
|
||||
### 4. Serverless Engine (`pkg/serverless/`)
|
||||
|
||||
WebAssembly (WASM) function execution engine with host functions.
|
||||
|
||||
**Architecture:**
|
||||
```
|
||||
pkg/serverless/
|
||||
├── engine.go - Core WASM engine
|
||||
├── execution/ - Function execution
|
||||
│ ├── executor.go
|
||||
│ └── lifecycle.go
|
||||
├── cache/ - Module caching
|
||||
│ └── module_cache.go
|
||||
├── registry/ - Function metadata
|
||||
│ ├── registry.go
|
||||
│ ├── function_store.go
|
||||
│ ├── ipfs_store.go
|
||||
│ └── invocation_logger.go
|
||||
└── hostfunctions/ - Host functions by domain
|
||||
├── cache.go - Cache operations
|
||||
├── storage.go - Storage operations
|
||||
├── database.go - Database queries
|
||||
├── pubsub.go - Messaging
|
||||
├── http.go - HTTP requests
|
||||
└── logging.go - Logging
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Secure WASM execution sandbox
|
||||
- Memory and CPU limits
|
||||
- Host function injection (cache, storage, DB, HTTP)
|
||||
- Function versioning
|
||||
- Invocation logging
|
||||
- Hot module reloading
|
||||
|
||||
### 5. Configuration System (`pkg/config/`)
|
||||
|
||||
Domain-specific configuration with validation.
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
pkg/config/
|
||||
├── config.go - Main config aggregator
|
||||
├── loader.go - YAML loading
|
||||
├── node_config.go - Node settings
|
||||
├── database_config.go - Database settings
|
||||
├── gateway_config.go - Gateway settings
|
||||
└── validate/ - Validation
|
||||
├── validators.go
|
||||
├── node.go
|
||||
├── database.go
|
||||
└── gateway.go
|
||||
```
|
||||
|
||||
### 6. Shared Utilities
|
||||
|
||||
**HTTP Utilities (`pkg/httputil/`):**
|
||||
- Request parsing and validation
|
||||
- JSON response writers
|
||||
- Error handling
|
||||
- Authentication extraction
|
||||
|
||||
**Error Handling (`pkg/errors/`):**
|
||||
- Typed errors (ValidationError, NotFoundError, etc.)
|
||||
- HTTP status code mapping
|
||||
- Error wrapping with context
|
||||
- Stack traces
|
||||
|
||||
**Contracts (`pkg/contracts/`):**
|
||||
- Interface definitions for all services
|
||||
- Enables dependency injection
|
||||
- Clean abstractions
|
||||
|
||||
## Data Flow
|
||||
|
||||
### 1. HTTP Request Flow
|
||||
|
||||
```
|
||||
Client Request
|
||||
↓
|
||||
[HTTPS Termination]
|
||||
↓
|
||||
[Authentication Middleware]
|
||||
↓
|
||||
[Route Handler]
|
||||
↓
|
||||
[Service Layer]
|
||||
↓
|
||||
[Backend Service] (RQLite/Olric/IPFS)
|
||||
↓
|
||||
[Response Formatting]
|
||||
↓
|
||||
Client Response
|
||||
```
|
||||
|
||||
### 2. WebSocket Flow (Pub/Sub)
|
||||
|
||||
```
|
||||
Client WebSocket Connect
|
||||
↓
|
||||
[Upgrade to WebSocket]
|
||||
↓
|
||||
[Authentication]
|
||||
↓
|
||||
[Subscribe to Topic]
|
||||
↓
|
||||
[LibP2P PubSub] ←→ [Local Subscribers]
|
||||
↓
|
||||
[Message Broadcasting]
|
||||
↓
|
||||
Client Receives Messages
|
||||
```
|
||||
|
||||
### 3. Serverless Invocation Flow
|
||||
|
||||
```
|
||||
Function Deployment:
|
||||
Upload WASM → Store in IPFS → Save Metadata (RQLite) → Compile Module
|
||||
|
||||
Function Invocation:
|
||||
Request → Load Metadata → Get WASM from IPFS →
|
||||
Execute in Sandbox → Return Result → Log Invocation
|
||||
```
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
1. **Wallet Signatures** (Ethereum-style)
|
||||
- Challenge/response flow
|
||||
- Nonce-based to prevent replay attacks
|
||||
- Issues JWT tokens after verification
|
||||
|
||||
2. **API Keys**
|
||||
- Long-lived credentials
|
||||
- Stored in RQLite
|
||||
- Namespace-scoped
|
||||
|
||||
3. **JWT Tokens**
|
||||
- Short-lived (15 min default)
|
||||
- Refresh token support
|
||||
- Claims-based authorization
|
||||
|
||||
### TLS/HTTPS
|
||||
|
||||
- Automatic ACME (Let's Encrypt) certificate management
|
||||
- TLS 1.3 support
|
||||
- HTTP/2 enabled
|
||||
- Certificate caching
|
||||
|
||||
### Middleware Stack
|
||||
|
||||
1. **Logger** - Request/response logging
|
||||
2. **CORS** - Cross-origin resource sharing
|
||||
3. **Authentication** - JWT/API key validation
|
||||
4. **Authorization** - Namespace access control
|
||||
5. **Rate Limiting** - Per-client rate limits
|
||||
6. **Error Handling** - Consistent error responses
|
||||
|
||||
## Scalability
|
||||
|
||||
### Horizontal Scaling
|
||||
|
||||
- **Gateway:** Stateless, can run multiple instances behind load balancer
|
||||
- **RQLite:** Multi-node cluster with Raft consensus
|
||||
- **IPFS:** Distributed storage across nodes
|
||||
- **Olric:** Distributed cache with consistent hashing
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
1. **WASM Module Cache** - Compiled modules cached in memory
|
||||
2. **Olric Distributed Cache** - Shared cache across nodes
|
||||
3. **Local Cache** - Per-gateway request caching
|
||||
|
||||
### High Availability
|
||||
|
||||
- **Database:** RQLite cluster with automatic leader election
|
||||
- **Storage:** IPFS replication factor configurable
|
||||
- **Cache:** Olric replication and eventual consistency
|
||||
- **Gateway:** Stateless, multiple replicas supported
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
### Health Checks
|
||||
|
||||
- `/health` - Liveness probe
|
||||
- `/v1/status` - Detailed status with service checks
|
||||
|
||||
### Metrics
|
||||
|
||||
- Prometheus-compatible metrics endpoint
|
||||
- Request counts, latencies, error rates
|
||||
- Service-specific metrics (cache hit ratio, DB query times)
|
||||
|
||||
### Logging
|
||||
|
||||
- Structured logging (JSON format)
|
||||
- Log levels: DEBUG, INFO, WARN, ERROR
|
||||
- Correlation IDs for request tracing
|
||||
|
||||
## Development Patterns
|
||||
|
||||
### SOLID Principles
|
||||
|
||||
- **Single Responsibility:** Each handler/service has one focus
|
||||
- **Open/Closed:** Interface-based design for extensibility
|
||||
- **Liskov Substitution:** All implementations conform to contracts
|
||||
- **Interface Segregation:** Small, focused interfaces
|
||||
- **Dependency Inversion:** Depend on abstractions, not implementations
|
||||
|
||||
### Code Organization
|
||||
|
||||
- **Average file size:** ~150 lines
|
||||
- **Package structure:** Domain-driven, feature-focused
|
||||
- **Testing:** Unit tests for logic, E2E tests for integration
|
||||
- **Documentation:** Godoc comments on all public APIs
|
||||
|
||||
## Deployment
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
make dev # Start 5-node cluster
|
||||
make stop # Stop all services
|
||||
make test # Run unit tests
|
||||
make test-e2e # Run E2E tests
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```bash
|
||||
# First node (creates cluster)
|
||||
sudo orama install --vps-ip <IP> --domain node1.example.com
|
||||
|
||||
# Additional nodes (join cluster)
|
||||
sudo orama install --vps-ip <IP> --domain node2.example.com \
|
||||
--peers /dns4/node1.example.com/tcp/4001/p2p/<PEER_ID> \
|
||||
--join <node1-ip>:7002 \
|
||||
--cluster-secret <secret> \
|
||||
--swarm-key <key>
|
||||
```
|
||||
|
||||
### Docker (Future)
|
||||
|
||||
Planned containerization with Docker Compose and Kubernetes support.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **GraphQL Support** - GraphQL gateway alongside REST
|
||||
2. **gRPC Support** - gRPC protocol support
|
||||
3. **Event Sourcing** - Event-driven architecture
|
||||
4. **Kubernetes Operator** - Native K8s deployment
|
||||
5. **Observability** - OpenTelemetry integration
|
||||
6. **Multi-tenancy** - Enhanced namespace isolation
|
||||
|
||||
## Resources
|
||||
|
||||
- [RQLite Documentation](https://rqlite.io/docs/)
|
||||
- [IPFS Documentation](https://docs.ipfs.tech/)
|
||||
- [LibP2P Documentation](https://docs.libp2p.io/)
|
||||
- [WebAssembly (WASM)](https://webassembly.org/)
|
||||
546
docs/CLIENT_SDK.md
Normal file
546
docs/CLIENT_SDK.md
Normal file
@ -0,0 +1,546 @@
|
||||
# Orama Network Client SDK
|
||||
|
||||
## Overview
|
||||
|
||||
The Orama Network Client SDK provides a clean, type-safe Go interface for interacting with the Orama Network. It abstracts away the complexity of HTTP requests, authentication, and error handling.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/DeBrosOfficial/network/pkg/client
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create client configuration
|
||||
cfg := client.DefaultClientConfig()
|
||||
cfg.GatewayURL = "https://api.orama.network"
|
||||
cfg.APIKey = "your-api-key-here"
|
||||
|
||||
// Create client
|
||||
c := client.NewNetworkClient(cfg)
|
||||
|
||||
// Use the client
|
||||
ctx := context.Background()
|
||||
|
||||
// Upload to storage
|
||||
data := []byte("Hello, Orama!")
|
||||
resp, err := c.Storage().Upload(ctx, data, "hello.txt")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Uploaded: CID=%s\n", resp.CID)
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### ClientConfig
|
||||
|
||||
```go
|
||||
type ClientConfig struct {
|
||||
// Gateway URL (e.g., "https://api.orama.network")
|
||||
GatewayURL string
|
||||
|
||||
// Authentication (choose one)
|
||||
APIKey string // API key authentication
|
||||
JWTToken string // JWT token authentication
|
||||
|
||||
// Client options
|
||||
Timeout time.Duration // Request timeout (default: 30s)
|
||||
UserAgent string // Custom user agent
|
||||
|
||||
// Network client namespace
|
||||
Namespace string // Default namespace for operations
|
||||
}
|
||||
```
|
||||
|
||||
### Creating a Client
|
||||
|
||||
```go
|
||||
// Default configuration
|
||||
cfg := client.DefaultClientConfig()
|
||||
cfg.GatewayURL = "https://api.orama.network"
|
||||
cfg.APIKey = "your-api-key"
|
||||
|
||||
c := client.NewNetworkClient(cfg)
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### API Key Authentication
|
||||
|
||||
```go
|
||||
cfg := client.DefaultClientConfig()
|
||||
cfg.APIKey = "your-api-key-here"
|
||||
c := client.NewNetworkClient(cfg)
|
||||
```
|
||||
|
||||
### JWT Token Authentication
|
||||
|
||||
```go
|
||||
cfg := client.DefaultClientConfig()
|
||||
cfg.JWTToken = "your-jwt-token-here"
|
||||
c := client.NewNetworkClient(cfg)
|
||||
```
|
||||
|
||||
### Obtaining Credentials
|
||||
|
||||
```go
|
||||
// 1. Login with wallet signature (not yet implemented in SDK)
|
||||
// Use the gateway API directly: POST /v1/auth/challenge + /v1/auth/verify
|
||||
|
||||
// 2. Issue API key after authentication
|
||||
// POST /v1/auth/apikey with JWT token
|
||||
```
|
||||
|
||||
## Storage Client
|
||||
|
||||
Upload, download, pin, and unpin files to IPFS.
|
||||
|
||||
### Upload File
|
||||
|
||||
```go
|
||||
data := []byte("Hello, World!")
|
||||
resp, err := c.Storage().Upload(ctx, data, "hello.txt")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("CID: %s\n", resp.CID)
|
||||
```
|
||||
|
||||
### Upload with Options
|
||||
|
||||
```go
|
||||
opts := &client.StorageUploadOptions{
|
||||
Pin: true, // Pin after upload
|
||||
Encrypt: true, // Encrypt before upload
|
||||
ReplicationFactor: 3, // Number of replicas
|
||||
}
|
||||
resp, err := c.Storage().UploadWithOptions(ctx, data, "file.txt", opts)
|
||||
```
|
||||
|
||||
### Get File
|
||||
|
||||
```go
|
||||
cid := "QmXxx..."
|
||||
data, err := c.Storage().Get(ctx, cid)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Downloaded %d bytes\n", len(data))
|
||||
```
|
||||
|
||||
### Pin File
|
||||
|
||||
```go
|
||||
cid := "QmXxx..."
|
||||
resp, err := c.Storage().Pin(ctx, cid)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Pinned: %s\n", resp.CID)
|
||||
```
|
||||
|
||||
### Unpin File
|
||||
|
||||
```go
|
||||
cid := "QmXxx..."
|
||||
err := c.Storage().Unpin(ctx, cid)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Unpinned successfully")
|
||||
```
|
||||
|
||||
### Check Pin Status
|
||||
|
||||
```go
|
||||
cid := "QmXxx..."
|
||||
status, err := c.Storage().Status(ctx, cid)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Status: %s, Replicas: %d\n", status.Status, status.Replicas)
|
||||
```
|
||||
|
||||
## Cache Client
|
||||
|
||||
Distributed key-value cache using Olric.
|
||||
|
||||
### Set Value
|
||||
|
||||
```go
|
||||
key := "user:123"
|
||||
value := map[string]interface{}{
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com",
|
||||
}
|
||||
ttl := 5 * time.Minute
|
||||
|
||||
err := c.Cache().Set(ctx, key, value, ttl)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Get Value
|
||||
|
||||
```go
|
||||
key := "user:123"
|
||||
var user map[string]interface{}
|
||||
err := c.Cache().Get(ctx, key, &user)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("User: %+v\n", user)
|
||||
```
|
||||
|
||||
### Delete Value
|
||||
|
||||
```go
|
||||
key := "user:123"
|
||||
err := c.Cache().Delete(ctx, key)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Get
|
||||
|
||||
```go
|
||||
keys := []string{"user:1", "user:2", "user:3"}
|
||||
results, err := c.Cache().MGet(ctx, keys)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for key, value := range results {
|
||||
fmt.Printf("%s: %v\n", key, value)
|
||||
}
|
||||
```
|
||||
|
||||
## Database Client
|
||||
|
||||
Query RQLite distributed SQL database.
|
||||
|
||||
### Execute Query (Write)
|
||||
|
||||
```go
|
||||
sql := "INSERT INTO users (name, email) VALUES (?, ?)"
|
||||
args := []interface{}{"Alice", "alice@example.com"}
|
||||
|
||||
result, err := c.Database().Execute(ctx, sql, args...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Inserted %d rows\n", result.RowsAffected)
|
||||
```
|
||||
|
||||
### Query (Read)
|
||||
|
||||
```go
|
||||
sql := "SELECT id, name, email FROM users WHERE id = ?"
|
||||
args := []interface{}{123}
|
||||
|
||||
rows, err := c.Database().Query(ctx, sql, args...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
var users []User
|
||||
for _, row := range rows {
|
||||
var user User
|
||||
// Parse row into user struct
|
||||
// (manual parsing required, or use ORM layer)
|
||||
users = append(users, user)
|
||||
}
|
||||
```
|
||||
|
||||
### Create Table
|
||||
|
||||
```go
|
||||
schema := `CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`
|
||||
|
||||
_, err := c.Database().Execute(ctx, schema)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Transaction
|
||||
|
||||
```go
|
||||
tx, err := c.Database().Begin(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = tx.Execute(ctx, "INSERT INTO users (name) VALUES (?)", "Alice")
|
||||
if err != nil {
|
||||
tx.Rollback(ctx)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = tx.Execute(ctx, "INSERT INTO users (name) VALUES (?)", "Bob")
|
||||
if err != nil {
|
||||
tx.Rollback(ctx)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## PubSub Client
|
||||
|
||||
Publish and subscribe to topics.
|
||||
|
||||
### Publish Message
|
||||
|
||||
```go
|
||||
topic := "chat"
|
||||
message := []byte("Hello, everyone!")
|
||||
|
||||
err := c.PubSub().Publish(ctx, topic, message)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Subscribe to Topic
|
||||
|
||||
```go
|
||||
topic := "chat"
|
||||
handler := func(ctx context.Context, msg []byte) error {
|
||||
fmt.Printf("Received: %s\n", string(msg))
|
||||
return nil
|
||||
}
|
||||
|
||||
unsubscribe, err := c.PubSub().Subscribe(ctx, topic, handler)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Later: unsubscribe
|
||||
defer unsubscribe()
|
||||
```
|
||||
|
||||
### List Topics
|
||||
|
||||
```go
|
||||
topics, err := c.PubSub().ListTopics(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Topics: %v\n", topics)
|
||||
```
|
||||
|
||||
## Serverless Client
|
||||
|
||||
Deploy and invoke WebAssembly functions.
|
||||
|
||||
### Deploy Function
|
||||
|
||||
```go
|
||||
// Read WASM file
|
||||
wasmBytes, err := os.ReadFile("function.wasm")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Function definition
|
||||
def := &client.FunctionDefinition{
|
||||
Name: "hello-world",
|
||||
Namespace: "default",
|
||||
Description: "Hello world function",
|
||||
MemoryLimit: 64, // MB
|
||||
Timeout: 30, // seconds
|
||||
}
|
||||
|
||||
// Deploy
|
||||
fn, err := c.Serverless().Deploy(ctx, def, wasmBytes)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Deployed: %s (CID: %s)\n", fn.Name, fn.WASMCID)
|
||||
```
|
||||
|
||||
### Invoke Function
|
||||
|
||||
```go
|
||||
functionName := "hello-world"
|
||||
input := map[string]interface{}{
|
||||
"name": "Alice",
|
||||
}
|
||||
|
||||
output, err := c.Serverless().Invoke(ctx, functionName, input)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Result: %s\n", output)
|
||||
```
|
||||
|
||||
### List Functions
|
||||
|
||||
```go
|
||||
functions, err := c.Serverless().List(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, fn := range functions {
|
||||
fmt.Printf("- %s: %s\n", fn.Name, fn.Description)
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Function
|
||||
|
||||
```go
|
||||
functionName := "hello-world"
|
||||
err := c.Serverless().Delete(ctx, functionName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Get Function Logs
|
||||
|
||||
```go
|
||||
functionName := "hello-world"
|
||||
logs, err := c.Serverless().GetLogs(ctx, functionName, 100)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, log := range logs {
|
||||
fmt.Printf("[%s] %s: %s\n", log.Timestamp, log.Level, log.Message)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
All client methods return typed errors that can be checked:
|
||||
|
||||
```go
|
||||
import "github.com/DeBrosOfficial/network/pkg/errors"
|
||||
|
||||
resp, err := c.Storage().Upload(ctx, data, "file.txt")
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
fmt.Println("Resource not found")
|
||||
} else if errors.IsUnauthorized(err) {
|
||||
fmt.Println("Authentication failed")
|
||||
} else if errors.IsValidation(err) {
|
||||
fmt.Println("Validation error")
|
||||
} else {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Timeout
|
||||
|
||||
```go
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.Storage().Upload(ctx, data, "file.txt")
|
||||
```
|
||||
|
||||
### Retry Logic
|
||||
|
||||
```go
|
||||
import "github.com/DeBrosOfficial/network/pkg/errors"
|
||||
|
||||
maxRetries := 3
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
resp, err := c.Storage().Upload(ctx, data, "file.txt")
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if !errors.ShouldRetry(err) {
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Second * time.Duration(i+1))
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Namespaces
|
||||
|
||||
```go
|
||||
// Default namespace
|
||||
c1 := client.NewNetworkClient(cfg)
|
||||
c1.Storage().Upload(ctx, data, "file.txt") // Uses default namespace
|
||||
|
||||
// Override namespace per request
|
||||
opts := &client.StorageUploadOptions{
|
||||
Namespace: "custom-namespace",
|
||||
}
|
||||
c1.Storage().UploadWithOptions(ctx, data, "file.txt", opts)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Mock Client
|
||||
|
||||
```go
|
||||
// Create a mock client for testing
|
||||
mockClient := &MockNetworkClient{
|
||||
StorageClient: &MockStorageClient{
|
||||
UploadFunc: func(ctx context.Context, data []byte, filename string) (*UploadResponse, error) {
|
||||
return &UploadResponse{CID: "QmMock"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Use in tests
|
||||
resp, err := mockClient.Storage().Upload(ctx, data, "test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "QmMock", resp.CID)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See the `examples/` directory for complete examples:
|
||||
|
||||
- `examples/storage/` - Storage upload/download examples
|
||||
- `examples/cache/` - Cache operations
|
||||
- `examples/database/` - Database queries
|
||||
- `examples/pubsub/` - Pub/sub messaging
|
||||
- `examples/serverless/` - Serverless functions
|
||||
|
||||
## API Reference
|
||||
|
||||
Complete API documentation is available at:
|
||||
- GoDoc: https://pkg.go.dev/github.com/DeBrosOfficial/network/pkg/client
|
||||
- OpenAPI: `openapi/gateway.yaml`
|
||||
|
||||
## Support
|
||||
|
||||
- GitHub Issues: https://github.com/DeBrosOfficial/network/issues
|
||||
- Documentation: https://github.com/DeBrosOfficial/network/tree/main/docs
|
||||
734
docs/GATEWAY_API.md
Normal file
734
docs/GATEWAY_API.md
Normal file
@ -0,0 +1,734 @@
|
||||
# Gateway API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Orama Network Gateway provides a unified HTTP/HTTPS API for all network services. It handles authentication, routing, and service coordination.
|
||||
|
||||
**Base URL:** `https://api.orama.network` (production) or `http://localhost:6001` (development)
|
||||
|
||||
## Authentication
|
||||
|
||||
All API requests (except `/health` and `/v1/auth/*`) require authentication.
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
1. **API Key** (Recommended for server-to-server)
|
||||
2. **JWT Token** (Recommended for user sessions)
|
||||
3. **Wallet Signature** (For blockchain integration)
|
||||
|
||||
### Using API Keys
|
||||
|
||||
Include your API key in the `Authorization` header:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer your-api-key-here" \
|
||||
https://api.orama.network/v1/status
|
||||
```
|
||||
|
||||
Or in the `X-API-Key` header:
|
||||
|
||||
```bash
|
||||
curl -H "X-API-Key: your-api-key-here" \
|
||||
https://api.orama.network/v1/status
|
||||
```
|
||||
|
||||
### Using JWT Tokens
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer your-jwt-token-here" \
|
||||
https://api.orama.network/v1/status
|
||||
```
|
||||
|
||||
## Base Endpoints
|
||||
|
||||
### Health Check
|
||||
|
||||
```http
|
||||
GET /health
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"timestamp": "2024-01-20T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Status
|
||||
|
||||
```http
|
||||
GET /v1/status
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"version": "0.80.0",
|
||||
"uptime": "24h30m15s",
|
||||
"services": {
|
||||
"rqlite": "healthy",
|
||||
"ipfs": "healthy",
|
||||
"olric": "healthy"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Version
|
||||
|
||||
```http
|
||||
GET /v1/version
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"version": "0.80.0",
|
||||
"commit": "abc123...",
|
||||
"built": "2024-01-20T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication API
|
||||
|
||||
### Get Challenge (Wallet Auth)
|
||||
|
||||
Generate a nonce for wallet signature.
|
||||
|
||||
```http
|
||||
POST /v1/auth/challenge
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
|
||||
"purpose": "login",
|
||||
"namespace": "default"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
|
||||
"namespace": "default",
|
||||
"nonce": "a1b2c3d4e5f6...",
|
||||
"purpose": "login",
|
||||
"expires_at": "2024-01-20T10:35:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Verify Signature
|
||||
|
||||
Verify wallet signature and issue JWT + API key.
|
||||
|
||||
```http
|
||||
POST /v1/auth/verify
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
|
||||
"signature": "0x...",
|
||||
"nonce": "a1b2c3d4e5f6...",
|
||||
"namespace": "default"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"jwt_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refresh_token": "refresh_abc123...",
|
||||
"api_key": "api_xyz789...",
|
||||
"expires_in": 900,
|
||||
"namespace": "default"
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh Token
|
||||
|
||||
Refresh an expired JWT token.
|
||||
|
||||
```http
|
||||
POST /v1/auth/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refresh_token": "refresh_abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"jwt_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"expires_in": 900
|
||||
}
|
||||
```
|
||||
|
||||
### Logout
|
||||
|
||||
Revoke refresh tokens.
|
||||
|
||||
```http
|
||||
POST /v1/auth/logout
|
||||
Authorization: Bearer your-jwt-token
|
||||
|
||||
{
|
||||
"all": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "logged out successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Whoami
|
||||
|
||||
Get current authentication info.
|
||||
|
||||
```http
|
||||
GET /v1/auth/whoami
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"authenticated": true,
|
||||
"method": "api_key",
|
||||
"api_key": "api_xyz789...",
|
||||
"namespace": "default"
|
||||
}
|
||||
```
|
||||
|
||||
## Storage API (IPFS)
|
||||
|
||||
### Upload File
|
||||
|
||||
```http
|
||||
POST /v1/storage/upload
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <binary data>
|
||||
```
|
||||
|
||||
Or with JSON:
|
||||
|
||||
```http
|
||||
POST /v1/storage/upload
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": "base64-encoded-data",
|
||||
"filename": "document.pdf",
|
||||
"pin": true,
|
||||
"encrypt": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"cid": "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG",
|
||||
"size": 1024,
|
||||
"filename": "document.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
### Get File
|
||||
|
||||
```http
|
||||
GET /v1/storage/get/:cid
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:** Binary file data or JSON (if `Accept: application/json`)
|
||||
|
||||
### Pin File
|
||||
|
||||
```http
|
||||
POST /v1/storage/pin
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"cid": "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG",
|
||||
"replication_factor": 3
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"cid": "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG",
|
||||
"status": "pinned"
|
||||
}
|
||||
```
|
||||
|
||||
### Unpin File
|
||||
|
||||
```http
|
||||
DELETE /v1/storage/unpin/:cid
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "unpinned successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Pin Status
|
||||
|
||||
```http
|
||||
GET /v1/storage/status/:cid
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"cid": "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG",
|
||||
"status": "pinned",
|
||||
"replicas": 3,
|
||||
"peers": ["12D3KooW...", "12D3KooW..."]
|
||||
}
|
||||
```
|
||||
|
||||
## Cache API (Olric)
|
||||
|
||||
### Set Value
|
||||
|
||||
```http
|
||||
PUT /v1/cache/put
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"key": "user:123",
|
||||
"value": {"name": "Alice", "email": "alice@example.com"},
|
||||
"ttl": 300
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "value set successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Value
|
||||
|
||||
```http
|
||||
GET /v1/cache/get?key=user:123
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"key": "user:123",
|
||||
"value": {"name": "Alice", "email": "alice@example.com"}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Multiple Values
|
||||
|
||||
```http
|
||||
POST /v1/cache/mget
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"keys": ["user:1", "user:2", "user:3"]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"results": {
|
||||
"user:1": {"name": "Alice"},
|
||||
"user:2": {"name": "Bob"},
|
||||
"user:3": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Value
|
||||
|
||||
```http
|
||||
DELETE /v1/cache/delete?key=user:123
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Scan Keys
|
||||
|
||||
```http
|
||||
GET /v1/cache/scan?pattern=user:*&limit=100
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"keys": ["user:1", "user:2", "user:3"],
|
||||
"count": 3
|
||||
}
|
||||
```
|
||||
|
||||
## Database API (RQLite)
|
||||
|
||||
### Execute SQL
|
||||
|
||||
```http
|
||||
POST /v1/rqlite/exec
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"sql": "INSERT INTO users (name, email) VALUES (?, ?)",
|
||||
"args": ["Alice", "alice@example.com"]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"last_insert_id": 123,
|
||||
"rows_affected": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Query SQL
|
||||
|
||||
```http
|
||||
POST /v1/rqlite/query
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"sql": "SELECT * FROM users WHERE id = ?",
|
||||
"args": [123]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"columns": ["id", "name", "email"],
|
||||
"rows": [
|
||||
[123, "Alice", "alice@example.com"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Get Schema
|
||||
|
||||
```http
|
||||
GET /v1/rqlite/schema
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"tables": [
|
||||
{
|
||||
"name": "users",
|
||||
"schema": "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Pub/Sub API
|
||||
|
||||
### Publish Message
|
||||
|
||||
```http
|
||||
POST /v1/pubsub/publish
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"topic": "chat",
|
||||
"data": "SGVsbG8sIFdvcmxkIQ==",
|
||||
"namespace": "default"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "published successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### List Topics
|
||||
|
||||
```http
|
||||
GET /v1/pubsub/topics
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"topics": ["chat", "notifications", "events"]
|
||||
}
|
||||
```
|
||||
|
||||
### Subscribe (WebSocket)
|
||||
|
||||
```http
|
||||
GET /v1/pubsub/ws?topic=chat
|
||||
Authorization: Bearer your-api-key
|
||||
Upgrade: websocket
|
||||
```
|
||||
|
||||
**WebSocket Messages:**
|
||||
|
||||
Incoming (from server):
|
||||
```json
|
||||
{
|
||||
"type": "message",
|
||||
"topic": "chat",
|
||||
"data": "SGVsbG8sIFdvcmxkIQ==",
|
||||
"timestamp": "2024-01-20T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Outgoing (to server):
|
||||
```json
|
||||
{
|
||||
"type": "publish",
|
||||
"topic": "chat",
|
||||
"data": "SGVsbG8sIFdvcmxkIQ=="
|
||||
}
|
||||
```
|
||||
|
||||
### Presence
|
||||
|
||||
```http
|
||||
GET /v1/pubsub/presence?topic=chat
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"topic": "chat",
|
||||
"members": [
|
||||
{"id": "user-123", "joined_at": "2024-01-20T10:00:00Z"},
|
||||
{"id": "user-456", "joined_at": "2024-01-20T10:15:00Z"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Serverless API (WASM)
|
||||
|
||||
### Deploy Function
|
||||
|
||||
```http
|
||||
POST /v1/functions
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
name: hello-world
|
||||
namespace: default
|
||||
description: Hello world function
|
||||
wasm: <binary WASM file>
|
||||
memory_limit: 64
|
||||
timeout: 30
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "fn_abc123",
|
||||
"name": "hello-world",
|
||||
"namespace": "default",
|
||||
"wasm_cid": "QmXxx...",
|
||||
"version": 1,
|
||||
"created_at": "2024-01-20T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Invoke Function
|
||||
|
||||
```http
|
||||
POST /v1/functions/hello-world/invoke
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Alice"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"result": "Hello, Alice!",
|
||||
"execution_time_ms": 15,
|
||||
"memory_used_mb": 2.5
|
||||
}
|
||||
```
|
||||
|
||||
### List Functions
|
||||
|
||||
```http
|
||||
GET /v1/functions?namespace=default
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"functions": [
|
||||
{
|
||||
"name": "hello-world",
|
||||
"description": "Hello world function",
|
||||
"version": 1,
|
||||
"created_at": "2024-01-20T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Function
|
||||
|
||||
```http
|
||||
DELETE /v1/functions/hello-world?namespace=default
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "function deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Function Logs
|
||||
|
||||
```http
|
||||
GET /v1/functions/hello-world/logs?limit=100
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"logs": [
|
||||
{
|
||||
"timestamp": "2024-01-20T10:30:00Z",
|
||||
"level": "info",
|
||||
"message": "Function invoked",
|
||||
"invocation_id": "inv_xyz789"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
All errors follow a consistent format:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "NOT_FOUND",
|
||||
"message": "user with ID '123' not found",
|
||||
"details": {
|
||||
"resource": "user",
|
||||
"id": "123"
|
||||
},
|
||||
"trace_id": "trace-abc123"
|
||||
}
|
||||
```
|
||||
|
||||
### Common Error Codes
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `VALIDATION_ERROR` | 400 | Invalid input |
|
||||
| `UNAUTHORIZED` | 401 | Authentication required |
|
||||
| `FORBIDDEN` | 403 | Permission denied |
|
||||
| `NOT_FOUND` | 404 | Resource not found |
|
||||
| `CONFLICT` | 409 | Resource already exists |
|
||||
| `TIMEOUT` | 408 | Operation timeout |
|
||||
| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests |
|
||||
| `SERVICE_UNAVAILABLE` | 503 | Service unavailable |
|
||||
| `INTERNAL` | 500 | Internal server error |
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
The API implements rate limiting per API key:
|
||||
|
||||
- **Default:** 100 requests per minute
|
||||
- **Burst:** 200 requests
|
||||
|
||||
Rate limit headers:
|
||||
```
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1611144000
|
||||
```
|
||||
|
||||
When rate limited:
|
||||
```json
|
||||
{
|
||||
"code": "RATE_LIMIT_EXCEEDED",
|
||||
"message": "rate limit exceeded",
|
||||
"details": {
|
||||
"limit": 100,
|
||||
"retry_after": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
List endpoints support pagination:
|
||||
|
||||
```http
|
||||
GET /v1/functions?limit=10&offset=20
|
||||
```
|
||||
|
||||
Response includes pagination metadata:
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"pagination": {
|
||||
"total": 100,
|
||||
"limit": 10,
|
||||
"offset": 20,
|
||||
"has_more": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Webhooks (Future)
|
||||
|
||||
Coming soon: webhook support for event notifications.
|
||||
|
||||
## Support
|
||||
|
||||
- API Issues: https://github.com/DeBrosOfficial/network/issues
|
||||
- OpenAPI Spec: `openapi/gateway.yaml`
|
||||
- SDK Documentation: `docs/CLIENT_SDK.md`
|
||||
476
docs/SECURITY_DEPLOYMENT_GUIDE.md
Normal file
476
docs/SECURITY_DEPLOYMENT_GUIDE.md
Normal file
@ -0,0 +1,476 @@
|
||||
# Orama Network - Security Deployment Guide
|
||||
|
||||
**Date:** January 18, 2026
|
||||
**Status:** Production-Ready
|
||||
**Audit Completed By:** Claude Code Security Audit
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines the security hardening measures applied to the 4-node Orama Network production cluster. All critical vulnerabilities identified in the security audit have been addressed.
|
||||
|
||||
**Security Status:** ✅ SECURED FOR PRODUCTION
|
||||
|
||||
---
|
||||
|
||||
## Server Inventory
|
||||
|
||||
| Server ID | IP Address | Domain | OS | Role |
|
||||
|-----------|------------|--------|-----|------|
|
||||
| VPS 1 | 51.83.128.181 | node-kv4la8.debros.network | Ubuntu 22.04 | Gateway + Cluster Node |
|
||||
| VPS 2 | 194.61.28.7 | node-7prvNa.debros.network | Ubuntu 24.04 | Gateway + Cluster Node |
|
||||
| VPS 3 | 83.171.248.66 | node-xn23dq.debros.network | Ubuntu 24.04 | Gateway + Cluster Node |
|
||||
| VPS 4 | 62.72.44.87 | node-nns4n5.debros.network | Ubuntu 24.04 | Gateway + Cluster Node |
|
||||
|
||||
---
|
||||
|
||||
## Services Running on Each Server
|
||||
|
||||
| Service | Port(s) | Purpose | Public Access |
|
||||
|---------|---------|---------|---------------|
|
||||
| **orama-node** | 80, 443, 7001 | API Gateway | Yes (80, 443 only) |
|
||||
| **rqlited** | 5001, 7002 | Distributed SQLite DB | Cluster only |
|
||||
| **ipfs** | 4101, 4501, 8080 | Content-addressed storage | Cluster only |
|
||||
| **ipfs-cluster** | 9094, 9098 | IPFS cluster management | Cluster only |
|
||||
| **olric-server** | 3320, 3322 | Distributed cache | Cluster only |
|
||||
| **anon** (Anyone proxy) | 9001, 9050, 9051 | Anonymity proxy | Cluster only |
|
||||
| **libp2p** | 4001 | P2P networking | Yes (public P2P) |
|
||||
| **SSH** | 22 | Remote access | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Security Measures Implemented
|
||||
|
||||
### 1. Firewall Configuration (UFW)
|
||||
|
||||
**Status:** ✅ Enabled on all 4 servers
|
||||
|
||||
#### Public Ports (Open to Internet)
|
||||
- **22/tcp** - SSH (with hardening)
|
||||
- **80/tcp** - HTTP (redirects to HTTPS)
|
||||
- **443/tcp** - HTTPS (Let's Encrypt production certificates)
|
||||
- **4001/tcp** - libp2p swarm (P2P networking)
|
||||
|
||||
#### Cluster-Only Ports (Restricted to 4 Server IPs)
|
||||
All the following ports are ONLY accessible from the 4 cluster IPs:
|
||||
- **5001/tcp** - rqlite HTTP API
|
||||
- **7001/tcp** - SNI Gateway
|
||||
- **7002/tcp** - rqlite Raft consensus
|
||||
- **9094/tcp** - IPFS Cluster API
|
||||
- **9098/tcp** - IPFS Cluster communication
|
||||
- **3322/tcp** - Olric distributed cache
|
||||
- **4101/tcp** - IPFS swarm (cluster internal)
|
||||
|
||||
#### Firewall Rules Example
|
||||
```bash
|
||||
sudo ufw default deny incoming
|
||||
sudo ufw default allow outgoing
|
||||
sudo ufw allow 22/tcp comment "SSH"
|
||||
sudo ufw allow 80/tcp comment "HTTP"
|
||||
sudo ufw allow 443/tcp comment "HTTPS"
|
||||
sudo ufw allow 4001/tcp comment "libp2p swarm"
|
||||
|
||||
# Cluster-only access for sensitive services
|
||||
sudo ufw allow from 51.83.128.181 to any port 5001 proto tcp
|
||||
sudo ufw allow from 194.61.28.7 to any port 5001 proto tcp
|
||||
sudo ufw allow from 83.171.248.66 to any port 5001 proto tcp
|
||||
sudo ufw allow from 62.72.44.87 to any port 5001 proto tcp
|
||||
# (repeat for ports 7001, 7002, 9094, 9098, 3322, 4101)
|
||||
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
### 2. SSH Hardening
|
||||
|
||||
**Location:** `/etc/ssh/sshd_config.d/99-hardening.conf`
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
PermitRootLogin yes # Root login allowed with SSH keys
|
||||
PasswordAuthentication yes # Password auth enabled (you have keys configured)
|
||||
PubkeyAuthentication yes # SSH key authentication enabled
|
||||
PermitEmptyPasswords no # No empty passwords
|
||||
X11Forwarding no # X11 disabled for security
|
||||
MaxAuthTries 3 # Max 3 login attempts
|
||||
ClientAliveInterval 300 # Keep-alive every 5 minutes
|
||||
ClientAliveCountMax 2 # Disconnect after 2 failed keep-alives
|
||||
```
|
||||
|
||||
**Your SSH Keys Added:**
|
||||
- ✅ `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPcGZPX2iHXWO8tuyyDkHPS5eByPOktkw3+ugcw79yQO`
|
||||
- ✅ `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDgCWmycaBN3aAZJcM2w4+Xi2zrTwN78W8oAiQywvMEkubqNNWHF6I3...`
|
||||
|
||||
Both keys are installed on all 4 servers in:
|
||||
- VPS 1: `/home/ubuntu/.ssh/authorized_keys`
|
||||
- VPS 2, 3, 4: `/root/.ssh/authorized_keys`
|
||||
|
||||
### 3. Fail2ban Protection
|
||||
|
||||
**Status:** ✅ Installed and running on all 4 servers
|
||||
|
||||
**Purpose:** Automatically bans IPs after failed SSH login attempts
|
||||
|
||||
**Check Status:**
|
||||
```bash
|
||||
sudo systemctl status fail2ban
|
||||
```
|
||||
|
||||
### 4. Security Updates
|
||||
|
||||
**Status:** ✅ All security updates applied (as of Jan 18, 2026)
|
||||
|
||||
**Update Command:**
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
```
|
||||
|
||||
### 5. Let's Encrypt TLS Certificates
|
||||
|
||||
**Status:** ✅ Production certificates (NOT staging)
|
||||
|
||||
**Configuration:**
|
||||
- **Provider:** Let's Encrypt (ACME v2 Production)
|
||||
- **Auto-renewal:** Enabled via autocert
|
||||
- **Cache Directory:** `/home/debros/.orama/tls-cache/`
|
||||
- **Domains:**
|
||||
- node-kv4la8.debros.network (VPS 1)
|
||||
- node-7prvNa.debros.network (VPS 2)
|
||||
- node-xn23dq.debros.network (VPS 3)
|
||||
- node-nns4n5.debros.network (VPS 4)
|
||||
|
||||
**Certificate Files:**
|
||||
- Account key: `/home/debros/.orama/tls-cache/acme_account+key`
|
||||
- Certificates auto-managed by autocert
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl -I https://node-kv4la8.debros.network
|
||||
# Should return valid SSL certificate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cluster Configuration
|
||||
|
||||
### RQLite Cluster
|
||||
|
||||
**Nodes:**
|
||||
- 51.83.128.181:7002 (Leader)
|
||||
- 194.61.28.7:7002
|
||||
- 83.171.248.66:7002
|
||||
- 62.72.44.87:7002
|
||||
|
||||
**Test Cluster Health:**
|
||||
```bash
|
||||
ssh ubuntu@51.83.128.181
|
||||
curl -s http://localhost:5001/status | jq '.store.nodes'
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```json
|
||||
[
|
||||
{"id":"194.61.28.7:7002","addr":"194.61.28.7:7002","suffrage":"Voter"},
|
||||
{"id":"51.83.128.181:7002","addr":"51.83.128.181:7002","suffrage":"Voter"},
|
||||
{"id":"62.72.44.87:7002","addr":"62.72.44.87:7002","suffrage":"Voter"},
|
||||
{"id":"83.171.248.66:7002","addr":"83.171.248.66:7002","suffrage":"Voter"}
|
||||
]
|
||||
```
|
||||
|
||||
### IPFS Cluster
|
||||
|
||||
**Test Cluster Health:**
|
||||
```bash
|
||||
ssh ubuntu@51.83.128.181
|
||||
curl -s http://localhost:9094/id | jq '.cluster_peers'
|
||||
```
|
||||
|
||||
**Expected:** All 4 peer IDs listed
|
||||
|
||||
### Olric Cache Cluster
|
||||
|
||||
**Port:** 3320 (localhost), 3322 (cluster communication)
|
||||
|
||||
**Test:**
|
||||
```bash
|
||||
ssh ubuntu@51.83.128.181
|
||||
ss -tulpn | grep olric
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Access Credentials
|
||||
|
||||
### SSH Access
|
||||
|
||||
**VPS 1:**
|
||||
```bash
|
||||
ssh ubuntu@51.83.128.181
|
||||
# OR using your SSH key:
|
||||
ssh -i ~/.ssh/ssh-sotiris/id_ed25519 ubuntu@51.83.128.181
|
||||
```
|
||||
|
||||
**VPS 2, 3, 4:**
|
||||
```bash
|
||||
ssh root@194.61.28.7
|
||||
ssh root@83.171.248.66
|
||||
ssh root@62.72.44.87
|
||||
```
|
||||
|
||||
**Important:** Password authentication is still enabled, but your SSH keys are configured for passwordless access.
|
||||
|
||||
---
|
||||
|
||||
## Testing & Verification
|
||||
|
||||
### 1. Test External Port Access (From Your Machine)
|
||||
|
||||
```bash
|
||||
# These should be BLOCKED (timeout or connection refused):
|
||||
nc -zv 51.83.128.181 5001 # rqlite API - should be blocked
|
||||
nc -zv 51.83.128.181 7002 # rqlite Raft - should be blocked
|
||||
nc -zv 51.83.128.181 9094 # IPFS cluster - should be blocked
|
||||
|
||||
# These should be OPEN:
|
||||
nc -zv 51.83.128.181 22 # SSH - should succeed
|
||||
nc -zv 51.83.128.181 80 # HTTP - should succeed
|
||||
nc -zv 51.83.128.181 443 # HTTPS - should succeed
|
||||
nc -zv 51.83.128.181 4001 # libp2p - should succeed
|
||||
```
|
||||
|
||||
### 2. Test Domain Access
|
||||
|
||||
```bash
|
||||
curl -I https://node-kv4la8.debros.network
|
||||
curl -I https://node-7prvNa.debros.network
|
||||
curl -I https://node-xn23dq.debros.network
|
||||
curl -I https://node-nns4n5.debros.network
|
||||
```
|
||||
|
||||
All should return `HTTP/1.1 200 OK` or similar with valid SSL certificates.
|
||||
|
||||
### 3. Test Cluster Communication (From VPS 1)
|
||||
|
||||
```bash
|
||||
ssh ubuntu@51.83.128.181
|
||||
# Test rqlite cluster
|
||||
curl -s http://localhost:5001/status | jq -r '.store.nodes[].id'
|
||||
|
||||
# Test IPFS cluster
|
||||
curl -s http://localhost:9094/id | jq -r '.cluster_peers[]'
|
||||
|
||||
# Check all services running
|
||||
ps aux | grep -E "(orama-node|rqlited|ipfs|olric)" | grep -v grep
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Maintenance & Operations
|
||||
|
||||
### Firewall Management
|
||||
|
||||
**View current rules:**
|
||||
```bash
|
||||
sudo ufw status numbered
|
||||
```
|
||||
|
||||
**Add a new allowed IP for cluster services:**
|
||||
```bash
|
||||
sudo ufw allow from NEW_IP_ADDRESS to any port 5001 proto tcp
|
||||
sudo ufw allow from NEW_IP_ADDRESS to any port 7002 proto tcp
|
||||
# etc.
|
||||
```
|
||||
|
||||
**Delete a rule:**
|
||||
```bash
|
||||
sudo ufw status numbered # Get rule number
|
||||
sudo ufw delete [NUMBER]
|
||||
```
|
||||
|
||||
### SSH Management
|
||||
|
||||
**Test SSH config without applying:**
|
||||
```bash
|
||||
sudo sshd -t
|
||||
```
|
||||
|
||||
**Reload SSH after config changes:**
|
||||
```bash
|
||||
sudo systemctl reload ssh
|
||||
```
|
||||
|
||||
**View SSH login attempts:**
|
||||
```bash
|
||||
sudo journalctl -u ssh | tail -50
|
||||
```
|
||||
|
||||
### Fail2ban Management
|
||||
|
||||
**Check banned IPs:**
|
||||
```bash
|
||||
sudo fail2ban-client status sshd
|
||||
```
|
||||
|
||||
**Unban an IP:**
|
||||
```bash
|
||||
sudo fail2ban-client set sshd unbanip IP_ADDRESS
|
||||
```
|
||||
|
||||
### Security Updates
|
||||
|
||||
**Check for updates:**
|
||||
```bash
|
||||
apt list --upgradable
|
||||
```
|
||||
|
||||
**Apply updates:**
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
```
|
||||
|
||||
**Reboot if kernel updated:**
|
||||
```bash
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Improvements Completed
|
||||
|
||||
### Before Security Audit:
|
||||
- ❌ No firewall enabled
|
||||
- ❌ rqlite database exposed to internet (port 5001, 7002)
|
||||
- ❌ IPFS cluster management exposed (port 9094, 9098)
|
||||
- ❌ Olric cache exposed (port 3322)
|
||||
- ❌ Root login enabled without restrictions (VPS 2, 3, 4)
|
||||
- ❌ No fail2ban on 3 out of 4 servers
|
||||
- ❌ 19-39 security updates pending
|
||||
|
||||
### After Security Hardening:
|
||||
- ✅ UFW firewall enabled on all servers
|
||||
- ✅ Sensitive ports restricted to cluster IPs only
|
||||
- ✅ SSH hardened with key authentication
|
||||
- ✅ Fail2ban protecting all servers
|
||||
- ✅ All security updates applied
|
||||
- ✅ Let's Encrypt production certificates verified
|
||||
- ✅ Cluster communication tested and working
|
||||
- ✅ External access verified (HTTP/HTTPS only)
|
||||
|
||||
---
|
||||
|
||||
## Recommended Next Steps (Optional)
|
||||
|
||||
These were not implemented per your request but are recommended for future consideration:
|
||||
|
||||
1. **VPN/Private Networking** - Use WireGuard or Tailscale for encrypted cluster communication instead of firewall rules
|
||||
2. **Automated Security Updates** - Enable unattended-upgrades for automatic security patches
|
||||
3. **Monitoring & Alerting** - Set up Prometheus/Grafana for service monitoring
|
||||
4. **Regular Security Audits** - Run `lynis` or `rkhunter` monthly for security checks
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Let's Encrypt Configuration
|
||||
|
||||
The Orama Network gateway uses **autocert** from Go's `golang.org/x/crypto/acme/autocert` package. The configuration is in:
|
||||
|
||||
**File:** `/home/debros/.orama/configs/node.yaml`
|
||||
|
||||
**Relevant settings:**
|
||||
```yaml
|
||||
http_gateway:
|
||||
https:
|
||||
enabled: true
|
||||
domain: "node-kv4la8.debros.network"
|
||||
auto_cert: true
|
||||
cache_dir: "/home/debros/.orama/tls-cache"
|
||||
http_port: 80
|
||||
https_port: 443
|
||||
email: "admin@node-kv4la8.debros.network"
|
||||
```
|
||||
|
||||
**Important:** There is NO `letsencrypt_staging` flag set, which means it defaults to **production Let's Encrypt**. This is correct for production deployment.
|
||||
|
||||
### Firewall Persistence
|
||||
|
||||
UFW rules are persistent across reboots. The firewall will automatically start on boot.
|
||||
|
||||
### SSH Key Access
|
||||
|
||||
Both of your SSH keys are configured on all servers. You can access:
|
||||
- VPS 1: `ssh -i ~/.ssh/ssh-sotiris/id_ed25519 ubuntu@51.83.128.181`
|
||||
- VPS 2-4: `ssh -i ~/.ssh/ssh-sotiris/id_ed25519 root@IP_ADDRESS`
|
||||
|
||||
Password authentication is still enabled as a fallback, but keys are recommended.
|
||||
|
||||
---
|
||||
|
||||
## Emergency Access
|
||||
|
||||
If you get locked out:
|
||||
|
||||
1. **VPS Provider Console:** All major VPS providers offer web-based console access
|
||||
2. **Password Access:** Password auth is still enabled on all servers
|
||||
3. **SSH Keys:** Two keys configured for redundancy
|
||||
|
||||
**Disable firewall temporarily (emergency only):**
|
||||
```bash
|
||||
sudo ufw disable
|
||||
# Fix the issue
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Use this checklist to verify the security hardening:
|
||||
|
||||
- [ ] All 4 servers have UFW firewall enabled
|
||||
- [ ] SSH is hardened (MaxAuthTries 3, X11Forwarding no)
|
||||
- [ ] Your SSH keys work on all servers
|
||||
- [ ] Fail2ban is running on all servers
|
||||
- [ ] Security updates are current
|
||||
- [ ] rqlite port 5001 is NOT accessible from internet
|
||||
- [ ] rqlite port 7002 is NOT accessible from internet
|
||||
- [ ] IPFS cluster ports 9094, 9098 are NOT accessible from internet
|
||||
- [ ] Domains are accessible via HTTPS with valid certificates
|
||||
- [ ] RQLite cluster shows all 4 nodes
|
||||
- [ ] IPFS cluster shows all 4 peers
|
||||
- [ ] All services are running (5 processes per server)
|
||||
|
||||
---
|
||||
|
||||
## Contact & Support
|
||||
|
||||
For issues or questions about this deployment:
|
||||
|
||||
- **Security Audit Date:** January 18, 2026
|
||||
- **Configuration Files:** `/home/debros/.orama/configs/`
|
||||
- **Firewall Rules:** `/etc/ufw/`
|
||||
- **SSH Config:** `/etc/ssh/sshd_config.d/99-hardening.conf`
|
||||
- **TLS Certs:** `/home/debros/.orama/tls-cache/`
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### January 18, 2026 - Production Security Hardening
|
||||
|
||||
**Changes:**
|
||||
1. Added UFW firewall rules on all 4 VPS servers
|
||||
2. Restricted sensitive ports (5001, 7002, 9094, 9098, 3322, 4101) to cluster IPs only
|
||||
3. Hardened SSH configuration
|
||||
4. Added your 2 SSH keys to all servers
|
||||
5. Installed fail2ban on VPS 1, 2, 3 (VPS 4 already had it)
|
||||
6. Applied all pending security updates (23-39 packages per server)
|
||||
7. Verified Let's Encrypt is using production (not staging)
|
||||
8. Tested all services: rqlite, IPFS, libp2p, Olric clusters
|
||||
9. Verified all 4 domains are accessible via HTTPS
|
||||
|
||||
**Result:** Production-ready secure deployment ✅
|
||||
|
||||
---
|
||||
|
||||
**END OF DEPLOYMENT GUIDE**
|
||||
360
e2e/env.go
360
e2e/env.go
@ -5,14 +5,18 @@ package e2e
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@ -20,6 +24,7 @@ import (
|
||||
"github.com/DeBrosOfficial/network/pkg/client"
|
||||
"github.com/DeBrosOfficial/network/pkg/config"
|
||||
"github.com/DeBrosOfficial/network/pkg/ipfs"
|
||||
"github.com/gorilla/websocket"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
@ -35,7 +40,7 @@ var (
|
||||
cacheMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// loadGatewayConfig loads gateway configuration from ~/.debros/gateway.yaml
|
||||
// loadGatewayConfig loads gateway configuration from ~/.orama/gateway.yaml
|
||||
func loadGatewayConfig() (map[string]interface{}, error) {
|
||||
configPath, err := config.DefaultPath("gateway.yaml")
|
||||
if err != nil {
|
||||
@ -55,7 +60,7 @@ func loadGatewayConfig() (map[string]interface{}, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// loadNodeConfig loads node configuration from ~/.debros/node.yaml or bootstrap.yaml
|
||||
// loadNodeConfig loads node configuration from ~/.orama/node-*.yaml
|
||||
func loadNodeConfig(filename string) (map[string]interface{}, error) {
|
||||
configPath, err := config.DefaultPath(filename)
|
||||
if err != nil {
|
||||
@ -84,6 +89,14 @@ func GetGatewayURL() string {
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// Check environment variable first
|
||||
if envURL := os.Getenv("GATEWAY_URL"); envURL != "" {
|
||||
cacheMutex.Lock()
|
||||
gatewayURLCache = envURL
|
||||
cacheMutex.Unlock()
|
||||
return envURL
|
||||
}
|
||||
|
||||
// Try to load from gateway config
|
||||
gwCfg, err := loadGatewayConfig()
|
||||
if err == nil {
|
||||
@ -111,8 +124,8 @@ func GetRQLiteNodes() []string {
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// Try bootstrap.yaml first, then all node variants
|
||||
for _, cfgFile := range []string{"bootstrap.yaml", "bootstrap2.yaml", "node.yaml", "node2.yaml", "node3.yaml", "node4.yaml"} {
|
||||
// Try all node config files
|
||||
for _, cfgFile := range []string{"node-1.yaml", "node-2.yaml", "node-3.yaml", "node-4.yaml", "node-5.yaml"} {
|
||||
nodeCfg, err := loadNodeConfig(cfgFile)
|
||||
if err != nil {
|
||||
continue
|
||||
@ -135,19 +148,31 @@ func GetRQLiteNodes() []string {
|
||||
|
||||
// queryAPIKeyFromRQLite queries the SQLite database directly for an API key
|
||||
func queryAPIKeyFromRQLite() (string, error) {
|
||||
// Build database path from bootstrap/node config
|
||||
// 1. Check environment variable first
|
||||
if envKey := os.Getenv("DEBROS_API_KEY"); envKey != "" {
|
||||
return envKey, nil
|
||||
}
|
||||
|
||||
// 2. Build database path from bootstrap/node config
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
|
||||
// Try bootstrap first, then all nodes
|
||||
// Try all node data directories (both production and development paths)
|
||||
dbPaths := []string{
|
||||
filepath.Join(homeDir, ".debros", "bootstrap", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".debros", "bootstrap2", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".debros", "node2", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".debros", "node3", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".debros", "node4", "rqlite", "db.sqlite"),
|
||||
// Development paths (~/.orama/node-x/...)
|
||||
filepath.Join(homeDir, ".orama", "node-1", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".orama", "node-2", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".orama", "node-3", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".orama", "node-4", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".orama", "node-5", "rqlite", "db.sqlite"),
|
||||
// Production paths (~/.orama/data/node-x/...)
|
||||
filepath.Join(homeDir, ".orama", "data", "node-1", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".orama", "data", "node-2", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".orama", "data", "node-3", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".orama", "data", "node-4", "rqlite", "db.sqlite"),
|
||||
filepath.Join(homeDir, ".orama", "data", "node-5", "rqlite", "db.sqlite"),
|
||||
}
|
||||
|
||||
for _, dbPath := range dbPaths {
|
||||
@ -221,7 +246,7 @@ func GetBootstrapPeers() []string {
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
configFiles := []string{"bootstrap.yaml", "bootstrap2.yaml", "node.yaml", "node2.yaml", "node3.yaml", "node4.yaml"}
|
||||
configFiles := []string{"node-1.yaml", "node-2.yaml", "node-3.yaml", "node-4.yaml", "node-5.yaml"}
|
||||
seen := make(map[string]struct{})
|
||||
var peers []string
|
||||
|
||||
@ -272,7 +297,7 @@ func GetIPFSClusterURL() string {
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// Try to load from node config
|
||||
for _, cfgFile := range []string{"bootstrap.yaml", "bootstrap2.yaml", "node.yaml", "node2.yaml", "node3.yaml", "node4.yaml"} {
|
||||
for _, cfgFile := range []string{"node-1.yaml", "node-2.yaml", "node-3.yaml", "node-4.yaml", "node-5.yaml"} {
|
||||
nodeCfg, err := loadNodeConfig(cfgFile)
|
||||
if err != nil {
|
||||
continue
|
||||
@ -304,7 +329,7 @@ func GetIPFSAPIURL() string {
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// Try to load from node config
|
||||
for _, cfgFile := range []string{"bootstrap.yaml", "bootstrap2.yaml", "node.yaml", "node2.yaml", "node3.yaml", "node4.yaml"} {
|
||||
for _, cfgFile := range []string{"node-1.yaml", "node-2.yaml", "node-3.yaml", "node-4.yaml", "node-5.yaml"} {
|
||||
nodeCfg, err := loadNodeConfig(cfgFile)
|
||||
if err != nil {
|
||||
continue
|
||||
@ -329,7 +354,7 @@ func GetIPFSAPIURL() string {
|
||||
// GetClientNamespace returns the test client namespace from config
|
||||
func GetClientNamespace() string {
|
||||
// Try to load from node config
|
||||
for _, cfgFile := range []string{"bootstrap.yaml", "bootstrap2.yaml", "node.yaml", "node2.yaml", "node3.yaml", "node4.yaml"} {
|
||||
for _, cfgFile := range []string{"node-1.yaml", "node-2.yaml", "node-3.yaml", "node-4.yaml", "node-5.yaml"} {
|
||||
nodeCfg, err := loadNodeConfig(cfgFile)
|
||||
if err != nil {
|
||||
continue
|
||||
@ -363,7 +388,7 @@ func SkipIfMissingGateway(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := NewHTTPClient(5 * time.Second).Do(req)
|
||||
if err != nil {
|
||||
t.Skip("Gateway not accessible; tests skipped")
|
||||
return
|
||||
@ -378,7 +403,7 @@ func IsGatewayReady(ctx context.Context) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := NewHTTPClient(5 * time.Second).Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@ -391,7 +416,11 @@ func NewHTTPClient(timeout time.Duration) *http.Client {
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
return &http.Client{Timeout: timeout}
|
||||
// Skip TLS verification for testing against self-signed certificates
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
return &http.Client{Timeout: timeout, Transport: transport}
|
||||
}
|
||||
|
||||
// HTTPRequest is a helper for making authenticated HTTP requests
|
||||
@ -562,7 +591,7 @@ func CleanupDatabaseTable(t *testing.T, tableName string) {
|
||||
return
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(homeDir, ".debros", "bootstrap", "rqlite", "db.sqlite")
|
||||
dbPath := filepath.Join(homeDir, ".orama", "data", "node-1", "rqlite", "db.sqlite")
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
t.Logf("warning: failed to open database for cleanup: %v", err)
|
||||
@ -644,3 +673,296 @@ func CleanupCacheEntry(t *testing.T, dmapName, key string) {
|
||||
t.Logf("warning: delete cache entry returned status %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WebSocket PubSub Client for E2E Tests
|
||||
// ============================================================================
|
||||
|
||||
// WSPubSubClient is a WebSocket-based PubSub client that connects to the gateway
|
||||
type WSPubSubClient struct {
|
||||
t *testing.T
|
||||
conn *websocket.Conn
|
||||
topic string
|
||||
handlers []func(topic string, data []byte) error
|
||||
msgChan chan []byte
|
||||
doneChan chan struct{}
|
||||
mu sync.RWMutex
|
||||
writeMu sync.Mutex // Protects concurrent writes to WebSocket
|
||||
closed bool
|
||||
}
|
||||
|
||||
// WSPubSubMessage represents a message received from the gateway
|
||||
type WSPubSubMessage struct {
|
||||
Data string `json:"data"` // base64 encoded
|
||||
Timestamp int64 `json:"timestamp"` // unix milliseconds
|
||||
Topic string `json:"topic"`
|
||||
}
|
||||
|
||||
// NewWSPubSubClient creates a new WebSocket PubSub client connected to a topic
|
||||
func NewWSPubSubClient(t *testing.T, topic string) (*WSPubSubClient, error) {
|
||||
t.Helper()
|
||||
|
||||
// Build WebSocket URL
|
||||
gatewayURL := GetGatewayURL()
|
||||
wsURL := strings.Replace(gatewayURL, "http://", "ws://", 1)
|
||||
wsURL = strings.Replace(wsURL, "https://", "wss://", 1)
|
||||
|
||||
u, err := url.Parse(wsURL + "/v1/pubsub/ws")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse WebSocket URL: %w", err)
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("topic", topic)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
// Set up headers with authentication
|
||||
headers := http.Header{}
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
headers.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
// Connect to WebSocket
|
||||
dialer := websocket.Dialer{
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
conn, resp, err := dialer.Dial(u.String(), headers)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("websocket dial failed (status %d): %w - body: %s", resp.StatusCode, err, string(body))
|
||||
}
|
||||
return nil, fmt.Errorf("websocket dial failed: %w", err)
|
||||
}
|
||||
|
||||
client := &WSPubSubClient{
|
||||
t: t,
|
||||
conn: conn,
|
||||
topic: topic,
|
||||
handlers: make([]func(topic string, data []byte) error, 0),
|
||||
msgChan: make(chan []byte, 128),
|
||||
doneChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Start reader goroutine
|
||||
go client.readLoop()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// NewWSPubSubPresenceClient creates a new WebSocket PubSub client with presence parameters
|
||||
func NewWSPubSubPresenceClient(t *testing.T, topic, memberID string, meta map[string]interface{}) (*WSPubSubClient, error) {
|
||||
t.Helper()
|
||||
|
||||
// Build WebSocket URL
|
||||
gatewayURL := GetGatewayURL()
|
||||
wsURL := strings.Replace(gatewayURL, "http://", "ws://", 1)
|
||||
wsURL = strings.Replace(wsURL, "https://", "wss://", 1)
|
||||
|
||||
u, err := url.Parse(wsURL + "/v1/pubsub/ws")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse WebSocket URL: %w", err)
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("topic", topic)
|
||||
q.Set("presence", "true")
|
||||
q.Set("member_id", memberID)
|
||||
if meta != nil {
|
||||
metaJSON, _ := json.Marshal(meta)
|
||||
q.Set("member_meta", string(metaJSON))
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
// Set up headers with authentication
|
||||
headers := http.Header{}
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
headers.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
// Connect to WebSocket
|
||||
dialer := websocket.Dialer{
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
conn, resp, err := dialer.Dial(u.String(), headers)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("websocket dial failed (status %d): %w - body: %s", resp.StatusCode, err, string(body))
|
||||
}
|
||||
return nil, fmt.Errorf("websocket dial failed: %w", err)
|
||||
}
|
||||
|
||||
client := &WSPubSubClient{
|
||||
t: t,
|
||||
conn: conn,
|
||||
topic: topic,
|
||||
handlers: make([]func(topic string, data []byte) error, 0),
|
||||
msgChan: make(chan []byte, 128),
|
||||
doneChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Start reader goroutine
|
||||
go client.readLoop()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// readLoop reads messages from the WebSocket and dispatches to handlers
|
||||
func (c *WSPubSubClient) readLoop() {
|
||||
defer close(c.doneChan)
|
||||
|
||||
for {
|
||||
_, message, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
c.mu.RLock()
|
||||
closed := c.closed
|
||||
c.mu.RUnlock()
|
||||
if !closed {
|
||||
// Only log if not intentionally closed
|
||||
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
|
||||
c.t.Logf("websocket read error: %v", err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the message envelope
|
||||
var msg WSPubSubMessage
|
||||
if err := json.Unmarshal(message, &msg); err != nil {
|
||||
c.t.Logf("failed to unmarshal message: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Decode base64 data
|
||||
data, err := base64.StdEncoding.DecodeString(msg.Data)
|
||||
if err != nil {
|
||||
c.t.Logf("failed to decode base64 data: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Send to message channel
|
||||
select {
|
||||
case c.msgChan <- data:
|
||||
default:
|
||||
c.t.Logf("message channel full, dropping message")
|
||||
}
|
||||
|
||||
// Dispatch to handlers
|
||||
c.mu.RLock()
|
||||
handlers := make([]func(topic string, data []byte) error, len(c.handlers))
|
||||
copy(handlers, c.handlers)
|
||||
c.mu.RUnlock()
|
||||
|
||||
for _, handler := range handlers {
|
||||
if err := handler(msg.Topic, data); err != nil {
|
||||
c.t.Logf("handler error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe adds a message handler
|
||||
func (c *WSPubSubClient) Subscribe(handler func(topic string, data []byte) error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.handlers = append(c.handlers, handler)
|
||||
}
|
||||
|
||||
// Publish sends a message to the topic
|
||||
func (c *WSPubSubClient) Publish(data []byte) error {
|
||||
c.mu.RLock()
|
||||
closed := c.closed
|
||||
c.mu.RUnlock()
|
||||
|
||||
if closed {
|
||||
return fmt.Errorf("client is closed")
|
||||
}
|
||||
|
||||
// Protect concurrent writes to WebSocket
|
||||
c.writeMu.Lock()
|
||||
defer c.writeMu.Unlock()
|
||||
|
||||
return c.conn.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
// ReceiveWithTimeout waits for a message with timeout
|
||||
func (c *WSPubSubClient) ReceiveWithTimeout(timeout time.Duration) ([]byte, error) {
|
||||
select {
|
||||
case msg := <-c.msgChan:
|
||||
return msg, nil
|
||||
case <-time.After(timeout):
|
||||
return nil, fmt.Errorf("timeout waiting for message")
|
||||
case <-c.doneChan:
|
||||
return nil, fmt.Errorf("connection closed")
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the WebSocket connection
|
||||
func (c *WSPubSubClient) Close() error {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
c.closed = true
|
||||
c.mu.Unlock()
|
||||
|
||||
// Send close message
|
||||
_ = c.conn.WriteMessage(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
|
||||
// Close connection
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// Topic returns the topic this client is subscribed to
|
||||
func (c *WSPubSubClient) Topic() string {
|
||||
return c.topic
|
||||
}
|
||||
|
||||
// WSPubSubClientPair represents a publisher and subscriber pair for testing
|
||||
type WSPubSubClientPair struct {
|
||||
Publisher *WSPubSubClient
|
||||
Subscriber *WSPubSubClient
|
||||
Topic string
|
||||
}
|
||||
|
||||
// NewWSPubSubClientPair creates a publisher and subscriber pair for a topic
|
||||
func NewWSPubSubClientPair(t *testing.T, topic string) (*WSPubSubClientPair, error) {
|
||||
t.Helper()
|
||||
|
||||
// Create subscriber first
|
||||
sub, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create subscriber: %w", err)
|
||||
}
|
||||
|
||||
// Small delay to ensure subscriber is registered
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Create publisher
|
||||
pub, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
sub.Close()
|
||||
return nil, fmt.Errorf("failed to create publisher: %w", err)
|
||||
}
|
||||
|
||||
return &WSPubSubClientPair{
|
||||
Publisher: pub,
|
||||
Subscriber: sub,
|
||||
Topic: topic,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes both publisher and subscriber
|
||||
func (p *WSPubSubClientPair) Close() {
|
||||
if p.Publisher != nil {
|
||||
p.Publisher.Close()
|
||||
}
|
||||
if p.Subscriber != nil {
|
||||
p.Subscriber.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,82 +3,46 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newMessageCollector(ctx context.Context, buffer int) (chan []byte, func(string, []byte) error) {
|
||||
if buffer <= 0 {
|
||||
buffer = 1
|
||||
}
|
||||
|
||||
ch := make(chan []byte, buffer)
|
||||
handler := func(_ string, data []byte) error {
|
||||
copied := append([]byte(nil), data...)
|
||||
select {
|
||||
case ch <- copied:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ch, handler
|
||||
}
|
||||
|
||||
func waitForMessage(ctx context.Context, ch <-chan []byte) ([]byte, error) {
|
||||
select {
|
||||
case msg := <-ch:
|
||||
return msg, nil
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("context finished while waiting for pubsub message: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// TestPubSub_SubscribePublish tests basic pub/sub functionality via WebSocket
|
||||
func TestPubSub_SubscribePublish(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create two clients
|
||||
client1 := NewNetworkClient(t)
|
||||
client2 := NewNetworkClient(t)
|
||||
|
||||
if err := client1.Connect(); err != nil {
|
||||
t.Fatalf("client1 connect failed: %v", err)
|
||||
}
|
||||
defer client1.Disconnect()
|
||||
|
||||
if err := client2.Connect(); err != nil {
|
||||
t.Fatalf("client2 connect failed: %v", err)
|
||||
}
|
||||
defer client2.Disconnect()
|
||||
|
||||
topic := GenerateTopic()
|
||||
message := "test-message-from-client1"
|
||||
message := "test-message-from-publisher"
|
||||
|
||||
// Subscribe on client2
|
||||
messageCh, handler := newMessageCollector(ctx, 1)
|
||||
if err := client2.PubSub().Subscribe(ctx, topic, handler); err != nil {
|
||||
t.Fatalf("subscribe failed: %v", err)
|
||||
// Create subscriber first
|
||||
subscriber, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber: %v", err)
|
||||
}
|
||||
defer client2.PubSub().Unsubscribe(ctx, topic)
|
||||
defer subscriber.Close()
|
||||
|
||||
// Give subscription time to propagate and mesh to form
|
||||
Delay(2000)
|
||||
// Give subscriber time to register
|
||||
Delay(200)
|
||||
|
||||
// Publish from client1
|
||||
if err := client1.PubSub().Publish(ctx, topic, []byte(message)); err != nil {
|
||||
// Create publisher
|
||||
publisher, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create publisher: %v", err)
|
||||
}
|
||||
defer publisher.Close()
|
||||
|
||||
// Give connections time to stabilize
|
||||
Delay(200)
|
||||
|
||||
// Publish message
|
||||
if err := publisher.Publish([]byte(message)); err != nil {
|
||||
t.Fatalf("publish failed: %v", err)
|
||||
}
|
||||
|
||||
// Receive message on client2
|
||||
recvCtx, recvCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer recvCancel()
|
||||
|
||||
msg, err := waitForMessage(recvCtx, messageCh)
|
||||
// Receive message on subscriber
|
||||
msg, err := subscriber.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("receive failed: %v", err)
|
||||
}
|
||||
@ -88,154 +52,126 @@ func TestPubSub_SubscribePublish(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestPubSub_MultipleSubscribers tests that multiple subscribers receive the same message
|
||||
func TestPubSub_MultipleSubscribers(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create three clients
|
||||
clientPub := NewNetworkClient(t)
|
||||
clientSub1 := NewNetworkClient(t)
|
||||
clientSub2 := NewNetworkClient(t)
|
||||
|
||||
if err := clientPub.Connect(); err != nil {
|
||||
t.Fatalf("publisher connect failed: %v", err)
|
||||
}
|
||||
defer clientPub.Disconnect()
|
||||
|
||||
if err := clientSub1.Connect(); err != nil {
|
||||
t.Fatalf("subscriber1 connect failed: %v", err)
|
||||
}
|
||||
defer clientSub1.Disconnect()
|
||||
|
||||
if err := clientSub2.Connect(); err != nil {
|
||||
t.Fatalf("subscriber2 connect failed: %v", err)
|
||||
}
|
||||
defer clientSub2.Disconnect()
|
||||
|
||||
topic := GenerateTopic()
|
||||
message1 := "message-for-sub1"
|
||||
message2 := "message-for-sub2"
|
||||
message1 := "message-1"
|
||||
message2 := "message-2"
|
||||
|
||||
// Subscribe on both clients
|
||||
sub1Ch, sub1Handler := newMessageCollector(ctx, 4)
|
||||
if err := clientSub1.PubSub().Subscribe(ctx, topic, sub1Handler); err != nil {
|
||||
t.Fatalf("subscribe1 failed: %v", err)
|
||||
// Create two subscribers
|
||||
sub1, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber1: %v", err)
|
||||
}
|
||||
defer clientSub1.PubSub().Unsubscribe(ctx, topic)
|
||||
defer sub1.Close()
|
||||
|
||||
sub2Ch, sub2Handler := newMessageCollector(ctx, 4)
|
||||
if err := clientSub2.PubSub().Subscribe(ctx, topic, sub2Handler); err != nil {
|
||||
t.Fatalf("subscribe2 failed: %v", err)
|
||||
sub2, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber2: %v", err)
|
||||
}
|
||||
defer clientSub2.PubSub().Unsubscribe(ctx, topic)
|
||||
defer sub2.Close()
|
||||
|
||||
// Give subscriptions time to propagate
|
||||
Delay(500)
|
||||
// Give subscribers time to register
|
||||
Delay(200)
|
||||
|
||||
// Create publisher
|
||||
publisher, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create publisher: %v", err)
|
||||
}
|
||||
defer publisher.Close()
|
||||
|
||||
// Give connections time to stabilize
|
||||
Delay(200)
|
||||
|
||||
// Publish first message
|
||||
if err := clientPub.PubSub().Publish(ctx, topic, []byte(message1)); err != nil {
|
||||
if err := publisher.Publish([]byte(message1)); err != nil {
|
||||
t.Fatalf("publish1 failed: %v", err)
|
||||
}
|
||||
|
||||
// Both subscribers should receive first message
|
||||
recvCtx, recvCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer recvCancel()
|
||||
|
||||
msg1a, err := waitForMessage(recvCtx, sub1Ch)
|
||||
msg1a, err := sub1.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("sub1 receive1 failed: %v", err)
|
||||
}
|
||||
|
||||
if string(msg1a) != message1 {
|
||||
t.Fatalf("sub1: expected %q, got %q", message1, string(msg1a))
|
||||
}
|
||||
|
||||
msg1b, err := waitForMessage(recvCtx, sub2Ch)
|
||||
msg1b, err := sub2.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("sub2 receive1 failed: %v", err)
|
||||
}
|
||||
|
||||
if string(msg1b) != message1 {
|
||||
t.Fatalf("sub2: expected %q, got %q", message1, string(msg1b))
|
||||
}
|
||||
|
||||
// Publish second message
|
||||
if err := clientPub.PubSub().Publish(ctx, topic, []byte(message2)); err != nil {
|
||||
if err := publisher.Publish([]byte(message2)); err != nil {
|
||||
t.Fatalf("publish2 failed: %v", err)
|
||||
}
|
||||
|
||||
// Both subscribers should receive second message
|
||||
recvCtx2, recvCancel2 := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer recvCancel2()
|
||||
|
||||
msg2a, err := waitForMessage(recvCtx2, sub1Ch)
|
||||
msg2a, err := sub1.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("sub1 receive2 failed: %v", err)
|
||||
}
|
||||
|
||||
if string(msg2a) != message2 {
|
||||
t.Fatalf("sub1: expected %q, got %q", message2, string(msg2a))
|
||||
}
|
||||
|
||||
msg2b, err := waitForMessage(recvCtx2, sub2Ch)
|
||||
msg2b, err := sub2.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("sub2 receive2 failed: %v", err)
|
||||
}
|
||||
|
||||
if string(msg2b) != message2 {
|
||||
t.Fatalf("sub2: expected %q, got %q", message2, string(msg2b))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPubSub_Deduplication tests that multiple identical messages are all received
|
||||
func TestPubSub_Deduplication(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create two clients
|
||||
clientPub := NewNetworkClient(t)
|
||||
clientSub := NewNetworkClient(t)
|
||||
|
||||
if err := clientPub.Connect(); err != nil {
|
||||
t.Fatalf("publisher connect failed: %v", err)
|
||||
}
|
||||
defer clientPub.Disconnect()
|
||||
|
||||
if err := clientSub.Connect(); err != nil {
|
||||
t.Fatalf("subscriber connect failed: %v", err)
|
||||
}
|
||||
defer clientSub.Disconnect()
|
||||
|
||||
topic := GenerateTopic()
|
||||
message := "duplicate-test-message"
|
||||
|
||||
// Subscribe on client
|
||||
messageCh, handler := newMessageCollector(ctx, 3)
|
||||
if err := clientSub.PubSub().Subscribe(ctx, topic, handler); err != nil {
|
||||
t.Fatalf("subscribe failed: %v", err)
|
||||
// Create subscriber
|
||||
subscriber, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber: %v", err)
|
||||
}
|
||||
defer clientSub.PubSub().Unsubscribe(ctx, topic)
|
||||
defer subscriber.Close()
|
||||
|
||||
// Give subscription time to propagate and mesh to form
|
||||
Delay(2000)
|
||||
// Give subscriber time to register
|
||||
Delay(200)
|
||||
|
||||
// Create publisher
|
||||
publisher, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create publisher: %v", err)
|
||||
}
|
||||
defer publisher.Close()
|
||||
|
||||
// Give connections time to stabilize
|
||||
Delay(200)
|
||||
|
||||
// Publish the same message multiple times
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := clientPub.PubSub().Publish(ctx, topic, []byte(message)); err != nil {
|
||||
if err := publisher.Publish([]byte(message)); err != nil {
|
||||
t.Fatalf("publish %d failed: %v", i, err)
|
||||
}
|
||||
// Small delay between publishes
|
||||
Delay(50)
|
||||
}
|
||||
|
||||
// Receive messages - should get all (no dedup filter on subscribe)
|
||||
recvCtx, recvCancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer recvCancel()
|
||||
|
||||
// Receive messages - should get all (no dedup filter)
|
||||
receivedCount := 0
|
||||
for receivedCount < 3 {
|
||||
if _, err := waitForMessage(recvCtx, messageCh); err != nil {
|
||||
_, err := subscriber.ReceiveWithTimeout(5 * time.Second)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
receivedCount++
|
||||
@ -244,40 +180,35 @@ func TestPubSub_Deduplication(t *testing.T) {
|
||||
if receivedCount < 1 {
|
||||
t.Fatalf("expected to receive at least 1 message, got %d", receivedCount)
|
||||
}
|
||||
t.Logf("received %d messages", receivedCount)
|
||||
}
|
||||
|
||||
// TestPubSub_ConcurrentPublish tests concurrent message publishing
|
||||
func TestPubSub_ConcurrentPublish(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create clients
|
||||
clientPub := NewNetworkClient(t)
|
||||
clientSub := NewNetworkClient(t)
|
||||
|
||||
if err := clientPub.Connect(); err != nil {
|
||||
t.Fatalf("publisher connect failed: %v", err)
|
||||
}
|
||||
defer clientPub.Disconnect()
|
||||
|
||||
if err := clientSub.Connect(); err != nil {
|
||||
t.Fatalf("subscriber connect failed: %v", err)
|
||||
}
|
||||
defer clientSub.Disconnect()
|
||||
|
||||
topic := GenerateTopic()
|
||||
numMessages := 10
|
||||
|
||||
// Subscribe
|
||||
messageCh, handler := newMessageCollector(ctx, numMessages)
|
||||
if err := clientSub.PubSub().Subscribe(ctx, topic, handler); err != nil {
|
||||
t.Fatalf("subscribe failed: %v", err)
|
||||
// Create subscriber
|
||||
subscriber, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber: %v", err)
|
||||
}
|
||||
defer clientSub.PubSub().Unsubscribe(ctx, topic)
|
||||
defer subscriber.Close()
|
||||
|
||||
// Give subscription time to propagate and mesh to form
|
||||
Delay(2000)
|
||||
// Give subscriber time to register
|
||||
Delay(200)
|
||||
|
||||
// Create publisher
|
||||
publisher, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create publisher: %v", err)
|
||||
}
|
||||
defer publisher.Close()
|
||||
|
||||
// Give connections time to stabilize
|
||||
Delay(200)
|
||||
|
||||
// Publish multiple messages concurrently
|
||||
var wg sync.WaitGroup
|
||||
@ -286,7 +217,7 @@ func TestPubSub_ConcurrentPublish(t *testing.T) {
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
msg := fmt.Sprintf("concurrent-msg-%d", idx)
|
||||
if err := clientPub.PubSub().Publish(ctx, topic, []byte(msg)); err != nil {
|
||||
if err := publisher.Publish([]byte(msg)); err != nil {
|
||||
t.Logf("publish %d failed: %v", idx, err)
|
||||
}
|
||||
}(i)
|
||||
@ -294,12 +225,10 @@ func TestPubSub_ConcurrentPublish(t *testing.T) {
|
||||
wg.Wait()
|
||||
|
||||
// Receive messages
|
||||
recvCtx, recvCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer recvCancel()
|
||||
|
||||
receivedCount := 0
|
||||
for receivedCount < numMessages {
|
||||
if _, err := waitForMessage(recvCtx, messageCh); err != nil {
|
||||
_, err := subscriber.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
receivedCount++
|
||||
@ -310,107 +239,110 @@ func TestPubSub_ConcurrentPublish(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestPubSub_TopicIsolation tests that messages are isolated to their topics
|
||||
func TestPubSub_TopicIsolation(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create clients
|
||||
clientPub := NewNetworkClient(t)
|
||||
clientSub := NewNetworkClient(t)
|
||||
|
||||
if err := clientPub.Connect(); err != nil {
|
||||
t.Fatalf("publisher connect failed: %v", err)
|
||||
}
|
||||
defer clientPub.Disconnect()
|
||||
|
||||
if err := clientSub.Connect(); err != nil {
|
||||
t.Fatalf("subscriber connect failed: %v", err)
|
||||
}
|
||||
defer clientSub.Disconnect()
|
||||
|
||||
topic1 := GenerateTopic()
|
||||
topic2 := GenerateTopic()
|
||||
|
||||
// Subscribe to topic1
|
||||
messageCh, handler := newMessageCollector(ctx, 2)
|
||||
if err := clientSub.PubSub().Subscribe(ctx, topic1, handler); err != nil {
|
||||
t.Fatalf("subscribe1 failed: %v", err)
|
||||
}
|
||||
defer clientSub.PubSub().Unsubscribe(ctx, topic1)
|
||||
|
||||
// Give subscription time to propagate and mesh to form
|
||||
Delay(2000)
|
||||
|
||||
// Publish to topic2
|
||||
msg1 := "message-on-topic1"
|
||||
msg2 := "message-on-topic2"
|
||||
if err := clientPub.PubSub().Publish(ctx, topic2, []byte(msg2)); err != nil {
|
||||
|
||||
// Create subscriber for topic1
|
||||
sub1, err := NewWSPubSubClient(t, topic1)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber1: %v", err)
|
||||
}
|
||||
defer sub1.Close()
|
||||
|
||||
// Create subscriber for topic2
|
||||
sub2, err := NewWSPubSubClient(t, topic2)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber2: %v", err)
|
||||
}
|
||||
defer sub2.Close()
|
||||
|
||||
// Give subscribers time to register
|
||||
Delay(200)
|
||||
|
||||
// Create publishers
|
||||
pub1, err := NewWSPubSubClient(t, topic1)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create publisher1: %v", err)
|
||||
}
|
||||
defer pub1.Close()
|
||||
|
||||
pub2, err := NewWSPubSubClient(t, topic2)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create publisher2: %v", err)
|
||||
}
|
||||
defer pub2.Close()
|
||||
|
||||
// Give connections time to stabilize
|
||||
Delay(200)
|
||||
|
||||
// Publish to topic2 first
|
||||
if err := pub2.Publish([]byte(msg2)); err != nil {
|
||||
t.Fatalf("publish2 failed: %v", err)
|
||||
}
|
||||
|
||||
// Publish to topic1
|
||||
msg1 := "message-on-topic1"
|
||||
if err := clientPub.PubSub().Publish(ctx, topic1, []byte(msg1)); err != nil {
|
||||
if err := pub1.Publish([]byte(msg1)); err != nil {
|
||||
t.Fatalf("publish1 failed: %v", err)
|
||||
}
|
||||
|
||||
// Receive on sub1 - should get msg1 only
|
||||
recvCtx, recvCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer recvCancel()
|
||||
|
||||
msg, err := waitForMessage(recvCtx, messageCh)
|
||||
// Sub1 should receive msg1 only
|
||||
received1, err := sub1.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("receive failed: %v", err)
|
||||
t.Fatalf("sub1 receive failed: %v", err)
|
||||
}
|
||||
if string(received1) != msg1 {
|
||||
t.Fatalf("sub1: expected %q, got %q", msg1, string(received1))
|
||||
}
|
||||
|
||||
if string(msg) != msg1 {
|
||||
t.Fatalf("expected %q, got %q", msg1, string(msg))
|
||||
// Sub2 should receive msg2 only
|
||||
received2, err := sub2.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("sub2 receive failed: %v", err)
|
||||
}
|
||||
if string(received2) != msg2 {
|
||||
t.Fatalf("sub2: expected %q, got %q", msg2, string(received2))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPubSub_EmptyMessage tests sending and receiving empty messages
|
||||
func TestPubSub_EmptyMessage(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create clients
|
||||
clientPub := NewNetworkClient(t)
|
||||
clientSub := NewNetworkClient(t)
|
||||
|
||||
if err := clientPub.Connect(); err != nil {
|
||||
t.Fatalf("publisher connect failed: %v", err)
|
||||
}
|
||||
defer clientPub.Disconnect()
|
||||
|
||||
if err := clientSub.Connect(); err != nil {
|
||||
t.Fatalf("subscriber connect failed: %v", err)
|
||||
}
|
||||
defer clientSub.Disconnect()
|
||||
|
||||
topic := GenerateTopic()
|
||||
|
||||
// Subscribe
|
||||
messageCh, handler := newMessageCollector(ctx, 1)
|
||||
if err := clientSub.PubSub().Subscribe(ctx, topic, handler); err != nil {
|
||||
t.Fatalf("subscribe failed: %v", err)
|
||||
// Create subscriber
|
||||
subscriber, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber: %v", err)
|
||||
}
|
||||
defer clientSub.PubSub().Unsubscribe(ctx, topic)
|
||||
defer subscriber.Close()
|
||||
|
||||
// Give subscription time to propagate and mesh to form
|
||||
Delay(2000)
|
||||
// Give subscriber time to register
|
||||
Delay(200)
|
||||
|
||||
// Create publisher
|
||||
publisher, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create publisher: %v", err)
|
||||
}
|
||||
defer publisher.Close()
|
||||
|
||||
// Give connections time to stabilize
|
||||
Delay(200)
|
||||
|
||||
// Publish empty message
|
||||
if err := clientPub.PubSub().Publish(ctx, topic, []byte("")); err != nil {
|
||||
if err := publisher.Publish([]byte("")); err != nil {
|
||||
t.Fatalf("publish empty failed: %v", err)
|
||||
}
|
||||
|
||||
// Receive on sub - should get empty message
|
||||
recvCtx, recvCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer recvCancel()
|
||||
|
||||
msg, err := waitForMessage(recvCtx, messageCh)
|
||||
// Receive on subscriber - should get empty message
|
||||
msg, err := subscriber.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("receive failed: %v", err)
|
||||
}
|
||||
@ -419,3 +351,111 @@ func TestPubSub_EmptyMessage(t *testing.T) {
|
||||
t.Fatalf("expected empty message, got %q", string(msg))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPubSub_LargeMessage tests sending and receiving large messages
|
||||
func TestPubSub_LargeMessage(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
topic := GenerateTopic()
|
||||
|
||||
// Create a large message (100KB)
|
||||
largeMessage := make([]byte, 100*1024)
|
||||
for i := range largeMessage {
|
||||
largeMessage[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
// Create subscriber
|
||||
subscriber, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber: %v", err)
|
||||
}
|
||||
defer subscriber.Close()
|
||||
|
||||
// Give subscriber time to register
|
||||
Delay(200)
|
||||
|
||||
// Create publisher
|
||||
publisher, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create publisher: %v", err)
|
||||
}
|
||||
defer publisher.Close()
|
||||
|
||||
// Give connections time to stabilize
|
||||
Delay(200)
|
||||
|
||||
// Publish large message
|
||||
if err := publisher.Publish(largeMessage); err != nil {
|
||||
t.Fatalf("publish large message failed: %v", err)
|
||||
}
|
||||
|
||||
// Receive on subscriber
|
||||
msg, err := subscriber.ReceiveWithTimeout(30 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("receive failed: %v", err)
|
||||
}
|
||||
|
||||
if len(msg) != len(largeMessage) {
|
||||
t.Fatalf("expected message of length %d, got %d", len(largeMessage), len(msg))
|
||||
}
|
||||
|
||||
// Verify content
|
||||
for i := range msg {
|
||||
if msg[i] != largeMessage[i] {
|
||||
t.Fatalf("message content mismatch at byte %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPubSub_RapidPublish tests rapid message publishing
|
||||
func TestPubSub_RapidPublish(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
topic := GenerateTopic()
|
||||
numMessages := 50
|
||||
|
||||
// Create subscriber
|
||||
subscriber, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber: %v", err)
|
||||
}
|
||||
defer subscriber.Close()
|
||||
|
||||
// Give subscriber time to register
|
||||
Delay(200)
|
||||
|
||||
// Create publisher
|
||||
publisher, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create publisher: %v", err)
|
||||
}
|
||||
defer publisher.Close()
|
||||
|
||||
// Give connections time to stabilize
|
||||
Delay(200)
|
||||
|
||||
// Publish messages rapidly
|
||||
for i := 0; i < numMessages; i++ {
|
||||
msg := fmt.Sprintf("rapid-msg-%d", i)
|
||||
if err := publisher.Publish([]byte(msg)); err != nil {
|
||||
t.Fatalf("publish %d failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Receive messages
|
||||
receivedCount := 0
|
||||
for receivedCount < numMessages {
|
||||
_, err := subscriber.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
receivedCount++
|
||||
}
|
||||
|
||||
// Allow some message loss due to buffering
|
||||
minExpected := numMessages * 80 / 100 // 80% minimum
|
||||
if receivedCount < minExpected {
|
||||
t.Fatalf("expected at least %d messages, got %d", minExpected, receivedCount)
|
||||
}
|
||||
t.Logf("received %d/%d messages (%.1f%%)", receivedCount, numMessages, float64(receivedCount)*100/float64(numMessages))
|
||||
}
|
||||
|
||||
122
e2e/pubsub_presence_test.go
Normal file
122
e2e/pubsub_presence_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPubSub_Presence(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
topic := GenerateTopic()
|
||||
memberID := "user123"
|
||||
memberMeta := map[string]interface{}{"name": "Alice"}
|
||||
|
||||
// 1. Subscribe with presence
|
||||
client1, err := NewWSPubSubPresenceClient(t, topic, memberID, memberMeta)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create presence client: %v", err)
|
||||
}
|
||||
defer client1.Close()
|
||||
|
||||
// Wait for join event
|
||||
msg, err := client1.ReceiveWithTimeout(5 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("did not receive join event: %v", err)
|
||||
}
|
||||
|
||||
var event map[string]interface{}
|
||||
if err := json.Unmarshal(msg, &event); err != nil {
|
||||
t.Fatalf("failed to unmarshal event: %v", err)
|
||||
}
|
||||
|
||||
if event["type"] != "presence.join" {
|
||||
t.Fatalf("expected presence.join event, got %v", event["type"])
|
||||
}
|
||||
|
||||
if event["member_id"] != memberID {
|
||||
t.Fatalf("expected member_id %s, got %v", memberID, event["member_id"])
|
||||
}
|
||||
|
||||
// 2. Query presence endpoint
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: fmt.Sprintf("%s/v1/pubsub/presence?topic=%s", GetGatewayURL(), topic),
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("presence query failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := DecodeJSON(body, &resp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if resp["count"] != float64(1) {
|
||||
t.Fatalf("expected count 1, got %v", resp["count"])
|
||||
}
|
||||
|
||||
members := resp["members"].([]interface{})
|
||||
if len(members) != 1 {
|
||||
t.Fatalf("expected 1 member, got %d", len(members))
|
||||
}
|
||||
|
||||
member := members[0].(map[string]interface{})
|
||||
if member["member_id"] != memberID {
|
||||
t.Fatalf("expected member_id %s, got %v", memberID, member["member_id"])
|
||||
}
|
||||
|
||||
// 3. Subscribe second member
|
||||
memberID2 := "user456"
|
||||
client2, err := NewWSPubSubPresenceClient(t, topic, memberID2, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create second presence client: %v", err)
|
||||
}
|
||||
// We'll close client2 later to test leave event
|
||||
|
||||
// Client1 should receive join event for Client2
|
||||
msg2, err := client1.ReceiveWithTimeout(5 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("client1 did not receive join event for client2: %v", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(msg2, &event); err != nil {
|
||||
t.Fatalf("failed to unmarshal event: %v", err)
|
||||
}
|
||||
|
||||
if event["type"] != "presence.join" || event["member_id"] != memberID2 {
|
||||
t.Fatalf("expected presence.join for %s, got %v for %v", memberID2, event["type"], event["member_id"])
|
||||
}
|
||||
|
||||
// 4. Disconnect client2 and verify leave event
|
||||
client2.Close()
|
||||
|
||||
msg3, err := client1.ReceiveWithTimeout(5 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("client1 did not receive leave event for client2: %v", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(msg3, &event); err != nil {
|
||||
t.Fatalf("failed to unmarshal event: %v", err)
|
||||
}
|
||||
|
||||
if event["type"] != "presence.leave" || event["member_id"] != memberID2 {
|
||||
t.Fatalf("expected presence.leave for %s, got %v for %v", memberID2, event["type"], event["member_id"])
|
||||
}
|
||||
}
|
||||
|
||||
123
e2e/serverless_test.go
Normal file
123
e2e/serverless_test.go
Normal file
@ -0,0 +1,123 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestServerless_DeployAndInvoke(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
wasmPath := "../examples/functions/bin/hello.wasm"
|
||||
if _, err := os.Stat(wasmPath); os.IsNotExist(err) {
|
||||
t.Skip("hello.wasm not found")
|
||||
}
|
||||
|
||||
wasmBytes, err := os.ReadFile(wasmPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read hello.wasm: %v", err)
|
||||
}
|
||||
|
||||
funcName := "e2e-hello"
|
||||
namespace := "default"
|
||||
|
||||
// 1. Deploy function
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
// Add metadata
|
||||
_ = writer.WriteField("name", funcName)
|
||||
_ = writer.WriteField("namespace", namespace)
|
||||
|
||||
// Add WASM file
|
||||
part, err := writer.CreateFormFile("wasm", funcName+".wasm")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create form file: %v", err)
|
||||
}
|
||||
part.Write(wasmBytes)
|
||||
writer.Close()
|
||||
|
||||
deployReq, _ := http.NewRequestWithContext(ctx, "POST", GetGatewayURL()+"/v1/functions", &buf)
|
||||
deployReq.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
deployReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(1 * time.Minute)
|
||||
resp, err := client.Do(deployReq)
|
||||
if err != nil {
|
||||
t.Fatalf("deploy request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("deploy failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 2. Invoke function
|
||||
invokePayload := []byte(`{"name": "E2E Tester"}`)
|
||||
invokeReq, _ := http.NewRequestWithContext(ctx, "POST", GetGatewayURL()+"/v1/functions/"+funcName+"/invoke", bytes.NewReader(invokePayload))
|
||||
invokeReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
invokeReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
resp, err = client.Do(invokeReq)
|
||||
if err != nil {
|
||||
t.Fatalf("invoke request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("invoke failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(resp.Body)
|
||||
expected := "Hello, E2E Tester!"
|
||||
if !bytes.Contains(output, []byte(expected)) {
|
||||
t.Errorf("output %q does not contain %q", string(output), expected)
|
||||
}
|
||||
|
||||
// 3. List functions
|
||||
listReq, _ := http.NewRequestWithContext(ctx, "GET", GetGatewayURL()+"/v1/functions?namespace="+namespace, nil)
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
listReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
resp, err = client.Do(listReq)
|
||||
if err != nil {
|
||||
t.Fatalf("list request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("list failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 4. Delete function
|
||||
deleteReq, _ := http.NewRequestWithContext(ctx, "DELETE", GetGatewayURL()+"/v1/functions/"+funcName+"?namespace="+namespace, nil)
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
deleteReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
resp, err = client.Do(deleteReq)
|
||||
if err != nil {
|
||||
t.Fatalf("delete request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("delete failed with status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
158
example.http
Normal file
158
example.http
Normal file
@ -0,0 +1,158 @@
|
||||
### Orama Network Gateway API Examples
|
||||
# This file is designed for the VS Code "REST Client" extension.
|
||||
# It demonstrates the core capabilities of the DeBros Network Gateway.
|
||||
|
||||
@baseUrl = http://localhost:6001
|
||||
@apiKey = ak_X32jj2fiin8zzv0hmBKTC5b5:default
|
||||
@contentType = application/json
|
||||
|
||||
############################################################
|
||||
### 1. SYSTEM & HEALTH
|
||||
############################################################
|
||||
|
||||
# @name HealthCheck
|
||||
GET {{baseUrl}}/v1/health
|
||||
X-API-Key: {{apiKey}}
|
||||
|
||||
###
|
||||
|
||||
# @name SystemStatus
|
||||
# Returns the full status of the gateway and connected services
|
||||
GET {{baseUrl}}/v1/status
|
||||
X-API-Key: {{apiKey}}
|
||||
|
||||
###
|
||||
|
||||
# @name NetworkStatus
|
||||
# Returns the P2P network status and PeerID
|
||||
GET {{baseUrl}}/v1/network/status
|
||||
X-API-Key: {{apiKey}}
|
||||
|
||||
|
||||
############################################################
|
||||
### 2. DISTRIBUTED CACHE (OLRIC)
|
||||
############################################################
|
||||
|
||||
# @name CachePut
|
||||
# Stores a value in the distributed cache (DMap)
|
||||
POST {{baseUrl}}/v1/cache/put
|
||||
X-API-Key: {{apiKey}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"dmap": "demo-cache",
|
||||
"key": "video-demo",
|
||||
"value": "Hello from REST Client!"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# @name CacheGet
|
||||
# Retrieves a value from the distributed cache
|
||||
POST {{baseUrl}}/v1/cache/get
|
||||
X-API-Key: {{apiKey}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"dmap": "demo-cache",
|
||||
"key": "video-demo"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# @name CacheScan
|
||||
# Scans for keys in a specific DMap
|
||||
POST {{baseUrl}}/v1/cache/scan
|
||||
X-API-Key: {{apiKey}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"dmap": "demo-cache"
|
||||
}
|
||||
|
||||
|
||||
############################################################
|
||||
### 3. DECENTRALIZED STORAGE (IPFS)
|
||||
############################################################
|
||||
|
||||
# @name StorageUpload
|
||||
# Uploads a file to IPFS (Multipart)
|
||||
POST {{baseUrl}}/v1/storage/upload
|
||||
X-API-Key: {{apiKey}}
|
||||
Content-Type: multipart/form-data; boundary=boundary
|
||||
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="file"; filename="demo.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
This is a demonstration of decentralized storage on the Sonr Network.
|
||||
--boundary--
|
||||
|
||||
###
|
||||
|
||||
# @name StorageStatus
|
||||
# Check the pinning status and replication of a CID
|
||||
# Replace {cid} with the CID returned from the upload above
|
||||
@demoCid = bafkreid76y6x6v2n5o4n6n5o4n6n5o4n6n5o4n6n5o4
|
||||
GET {{baseUrl}}/v1/storage/status/{{demoCid}}
|
||||
X-API-Key: {{apiKey}}
|
||||
|
||||
###
|
||||
|
||||
# @name StorageDownload
|
||||
# Retrieve content directly from IPFS via the gateway
|
||||
GET {{baseUrl}}/v1/storage/get/{{demoCid}}
|
||||
X-API-Key: {{apiKey}}
|
||||
|
||||
|
||||
############################################################
|
||||
### 4. REAL-TIME PUB/SUB
|
||||
############################################################
|
||||
|
||||
# @name ListTopics
|
||||
# Lists all active topics in the current namespace
|
||||
GET {{baseUrl}}/v1/pubsub/topics
|
||||
X-API-Key: {{apiKey}}
|
||||
|
||||
###
|
||||
|
||||
# @name PublishMessage
|
||||
# Publishes a base64 encoded message to a topic
|
||||
POST {{baseUrl}}/v1/pubsub/publish
|
||||
X-API-Key: {{apiKey}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"topic": "network-updates",
|
||||
"data_base64": "U29uciBOZXR3b3JrIGlzIGF3ZXNvbWUh"
|
||||
}
|
||||
|
||||
|
||||
############################################################
|
||||
### 5. SERVERLESS FUNCTIONS
|
||||
############################################################
|
||||
|
||||
# @name ListFunctions
|
||||
# Lists all deployed serverless functions
|
||||
GET {{baseUrl}}/v1/functions
|
||||
X-API-Key: {{apiKey}}
|
||||
|
||||
###
|
||||
|
||||
# @name InvokeFunction
|
||||
# Invokes a deployed function by name
|
||||
# Path: /v1/invoke/{namespace}/{functionName}
|
||||
POST {{baseUrl}}/v1/invoke/default/hello
|
||||
X-API-Key: {{apiKey}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"name": "Developer"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# @name WhoAmI
|
||||
# Validates the API Key and returns caller identity
|
||||
GET {{baseUrl}}/v1/auth/whoami
|
||||
X-API-Key: {{apiKey}}
|
||||
42
examples/functions/build.sh
Executable file
42
examples/functions/build.sh
Executable file
@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Build all example functions to WASM using TinyGo
|
||||
#
|
||||
# Prerequisites:
|
||||
# - TinyGo installed: https://tinygo.org/getting-started/install/
|
||||
# - On macOS: brew install tinygo
|
||||
#
|
||||
# Usage: ./build.sh
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/bin"
|
||||
|
||||
# Check if TinyGo is installed
|
||||
if ! command -v tinygo &> /dev/null; then
|
||||
echo "Error: TinyGo is not installed."
|
||||
echo "Install it with: brew install tinygo (macOS) or see https://tinygo.org/getting-started/install/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "Building example functions to WASM..."
|
||||
echo
|
||||
|
||||
# Build each function
|
||||
for dir in "$SCRIPT_DIR"/*/; do
|
||||
if [ -f "$dir/main.go" ]; then
|
||||
name=$(basename "$dir")
|
||||
echo "Building $name..."
|
||||
cd "$dir"
|
||||
tinygo build -o "$OUTPUT_DIR/$name.wasm" -target wasi main.go
|
||||
echo " -> $OUTPUT_DIR/$name.wasm"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Done! WASM files are in $OUTPUT_DIR/"
|
||||
ls -lh "$OUTPUT_DIR"/*.wasm 2>/dev/null || echo "No WASM files built."
|
||||
|
||||
66
examples/functions/counter/main.go
Normal file
66
examples/functions/counter/main.go
Normal file
@ -0,0 +1,66 @@
|
||||
// Example: Counter function with Olric cache
|
||||
// This function demonstrates using the distributed cache to maintain state.
|
||||
// Compile with: tinygo build -o counter.wasm -target wasi main.go
|
||||
//
|
||||
// Note: This example shows the CONCEPT. Actual host function integration
|
||||
// requires the host function bindings to be exposed to the WASM module.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read input from stdin
|
||||
var input []byte
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := os.Stdin.Read(buf)
|
||||
if n > 0 {
|
||||
input = append(input, buf[:n]...)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Parse input
|
||||
var payload struct {
|
||||
Action string `json:"action"` // "increment", "decrement", "get", "reset"
|
||||
CounterID string `json:"counter_id"`
|
||||
}
|
||||
if err := json.Unmarshal(input, &payload); err != nil {
|
||||
response := map[string]interface{}{
|
||||
"error": "Invalid JSON input",
|
||||
}
|
||||
output, _ := json.Marshal(response)
|
||||
os.Stdout.Write(output)
|
||||
return
|
||||
}
|
||||
|
||||
if payload.CounterID == "" {
|
||||
payload.CounterID = "default"
|
||||
}
|
||||
|
||||
// NOTE: In the real implementation, this would use host functions:
|
||||
// - cache_get(key) to read the counter
|
||||
// - cache_put(key, value, ttl) to write the counter
|
||||
//
|
||||
// For this example, we just simulate the logic:
|
||||
response := map[string]interface{}{
|
||||
"counter_id": payload.CounterID,
|
||||
"action": payload.Action,
|
||||
"message": "Counter operations require cache host functions",
|
||||
"example": map[string]interface{}{
|
||||
"increment": "cache_put('counter:' + counter_id, current + 1)",
|
||||
"decrement": "cache_put('counter:' + counter_id, current - 1)",
|
||||
"get": "cache_get('counter:' + counter_id)",
|
||||
"reset": "cache_put('counter:' + counter_id, 0)",
|
||||
},
|
||||
}
|
||||
|
||||
output, _ := json.Marshal(response)
|
||||
os.Stdout.Write(output)
|
||||
}
|
||||
|
||||
50
examples/functions/echo/main.go
Normal file
50
examples/functions/echo/main.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Example: Echo function
|
||||
// This is a simple serverless function that echoes back the input.
|
||||
// Compile with: tinygo build -o echo.wasm -target wasi main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Input is read from stdin, output is written to stdout.
|
||||
// The Orama serverless engine passes the invocation payload via stdin
|
||||
// and expects the response on stdout.
|
||||
|
||||
func main() {
|
||||
// Read all input from stdin
|
||||
var input []byte
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := os.Stdin.Read(buf)
|
||||
if n > 0 {
|
||||
input = append(input, buf[:n]...)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Parse input as JSON (optional - could also just echo raw bytes)
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(input, &payload); err != nil {
|
||||
// Not JSON, just echo the raw input
|
||||
response := map[string]interface{}{
|
||||
"echo": string(input),
|
||||
}
|
||||
output, _ := json.Marshal(response)
|
||||
os.Stdout.Write(output)
|
||||
return
|
||||
}
|
||||
|
||||
// Create response
|
||||
response := map[string]interface{}{
|
||||
"echo": payload,
|
||||
"message": "Echo function received your input!",
|
||||
}
|
||||
|
||||
output, _ := json.Marshal(response)
|
||||
os.Stdout.Write(output)
|
||||
}
|
||||
|
||||
42
examples/functions/hello/main.go
Normal file
42
examples/functions/hello/main.go
Normal file
@ -0,0 +1,42 @@
|
||||
// Example: Hello function
|
||||
// This is a simple serverless function that returns a greeting.
|
||||
// Compile with: tinygo build -o hello.wasm -target wasi main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read input from stdin
|
||||
var input []byte
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := os.Stdin.Read(buf)
|
||||
if n > 0 {
|
||||
input = append(input, buf[:n]...)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Parse input to get name
|
||||
var payload struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.Unmarshal(input, &payload); err != nil || payload.Name == "" {
|
||||
payload.Name = "World"
|
||||
}
|
||||
|
||||
// Create greeting response
|
||||
response := map[string]interface{}{
|
||||
"greeting": "Hello, " + payload.Name + "!",
|
||||
"message": "This is a serverless function running on Orama Network",
|
||||
}
|
||||
|
||||
output, _ := json.Marshal(response)
|
||||
os.Stdout.Write(output)
|
||||
}
|
||||
|
||||
27
go.mod
27
go.mod
@ -1,33 +1,45 @@
|
||||
module github.com/DeBrosOfficial/network
|
||||
|
||||
go 1.23.8
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.20.0
|
||||
github.com/charmbracelet/bubbletea v1.2.4
|
||||
github.com/charmbracelet/lipgloss v1.0.0
|
||||
github.com/ethereum/go-ethereum v1.13.14
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/libp2p/go-libp2p v0.41.1
|
||||
github.com/libp2p/go-libp2p-pubsub v0.14.2
|
||||
github.com/mackerelio/go-osstat v0.2.6
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/multiformats/go-multiaddr v0.15.0
|
||||
github.com/olric-data/olric v0.7.0
|
||||
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8
|
||||
github.com/tetratelabs/wazero v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/net v0.42.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/RoaringBitmap/roaring v1.9.4 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.22.0 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
|
||||
github.com/buraksezer/consistent v0.10.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.4.5 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
||||
@ -35,6 +47,7 @@ require (
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/elastic/gosigar v0.14.3 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/flynn/noise v1.1.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
@ -43,7 +56,6 @@ require (
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-metrics v0.5.4 // indirect
|
||||
@ -70,15 +82,20 @@ require (
|
||||
github.com/libp2p/go-netroute v0.2.2 // indirect
|
||||
github.com/libp2p/go-reuseport v0.4.0 // indirect
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/miekg/dns v1.1.66 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect
|
||||
@ -121,6 +138,7 @@ require (
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
|
||||
github.com/raulk/go-watchdog v1.3.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.8.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
@ -137,10 +155,9 @@ require (
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
||||
40
go.sum
40
go.sum
@ -19,6 +19,10 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
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/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
@ -44,6 +48,16 @@ github.com/buraksezer/consistent v0.10.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0ma
|
||||
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=
|
||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||
github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE=
|
||||
github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM=
|
||||
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||
github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM=
|
||||
github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
@ -75,6 +89,8 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
|
||||
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
|
||||
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/ethereum/go-ethereum v1.13.14 h1:EwiY3FZP94derMCIam1iW4HFVrSgIcpsu0HwTQtm6CQ=
|
||||
github.com/ethereum/go-ethereum v1.13.14/go.mod h1:TN8ZiHrdJwSe8Cb6x+p0hs5CxhJZPbqB7hHkaUXcmIU=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
@ -85,6 +101,8 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@ -238,6 +256,8 @@ github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQsc
|
||||
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po=
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mackerelio/go-osstat v0.2.6 h1:gs4U8BZeS1tjrL08tt5VUliVvSWP26Ai2Ob8Lr7f2i0=
|
||||
github.com/mackerelio/go-osstat v0.2.6/go.mod h1:lRy8V9ZuHpuRVZh+vyTkODeDPl3/d5MgXHtLSaqG8bA=
|
||||
@ -246,6 +266,10 @@ github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
|
||||
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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
@ -271,6 +295,12 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||
@ -399,6 +429,9 @@ github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtB
|
||||
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
||||
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
|
||||
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8 h1:BoxiqWvhprOB2isgM59s8wkgKwAoyQH66Twfmof41oE=
|
||||
@ -454,6 +487,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
|
||||
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
||||
github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
|
||||
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
|
||||
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
||||
@ -585,6 +620,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -593,8 +629,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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=
|
||||
|
||||
243
migrations/004_serverless_functions.sql
Normal file
243
migrations/004_serverless_functions.sql
Normal file
@ -0,0 +1,243 @@
|
||||
-- Orama Network - Serverless Functions Engine (Phase 4)
|
||||
-- WASM-based serverless function execution with triggers, jobs, and secrets
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- =============================================================================
|
||||
-- FUNCTIONS TABLE
|
||||
-- Core function registry with versioning support
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS functions (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
namespace TEXT NOT NULL,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
wasm_cid TEXT NOT NULL,
|
||||
source_cid TEXT,
|
||||
memory_limit_mb INTEGER NOT NULL DEFAULT 64,
|
||||
timeout_seconds INTEGER NOT NULL DEFAULT 30,
|
||||
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
retry_delay_seconds INTEGER NOT NULL DEFAULT 5,
|
||||
dlq_topic TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by TEXT NOT NULL,
|
||||
UNIQUE(namespace, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_functions_namespace ON functions(namespace);
|
||||
CREATE INDEX IF NOT EXISTS idx_functions_name ON functions(namespace, name);
|
||||
CREATE INDEX IF NOT EXISTS idx_functions_status ON functions(status);
|
||||
|
||||
-- =============================================================================
|
||||
-- FUNCTION ENVIRONMENT VARIABLES
|
||||
-- Non-sensitive configuration per function
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_env_vars (
|
||||
id TEXT PRIMARY KEY,
|
||||
function_id TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(function_id, key),
|
||||
FOREIGN KEY (function_id) REFERENCES functions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_env_vars_function ON function_env_vars(function_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- FUNCTION SECRETS
|
||||
-- Encrypted secrets per namespace (shared across functions in namespace)
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_secrets (
|
||||
id TEXT PRIMARY KEY,
|
||||
namespace TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
encrypted_value BLOB NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(namespace, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_secrets_namespace ON function_secrets(namespace);
|
||||
|
||||
-- =============================================================================
|
||||
-- CRON TRIGGERS
|
||||
-- Scheduled function execution using cron expressions
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_cron_triggers (
|
||||
id TEXT PRIMARY KEY,
|
||||
function_id TEXT NOT NULL,
|
||||
cron_expression TEXT NOT NULL,
|
||||
next_run_at TIMESTAMP,
|
||||
last_run_at TIMESTAMP,
|
||||
last_status TEXT,
|
||||
last_error TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (function_id) REFERENCES functions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_cron_triggers_function ON function_cron_triggers(function_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_cron_triggers_next_run ON function_cron_triggers(next_run_at)
|
||||
WHERE enabled = TRUE;
|
||||
|
||||
-- =============================================================================
|
||||
-- DATABASE TRIGGERS
|
||||
-- Trigger functions on database changes (INSERT/UPDATE/DELETE)
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_db_triggers (
|
||||
id TEXT PRIMARY KEY,
|
||||
function_id TEXT NOT NULL,
|
||||
table_name TEXT NOT NULL,
|
||||
operation TEXT NOT NULL CHECK(operation IN ('INSERT', 'UPDATE', 'DELETE')),
|
||||
condition TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (function_id) REFERENCES functions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_db_triggers_function ON function_db_triggers(function_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_db_triggers_table ON function_db_triggers(table_name, operation)
|
||||
WHERE enabled = TRUE;
|
||||
|
||||
-- =============================================================================
|
||||
-- PUBSUB TRIGGERS
|
||||
-- Trigger functions on pubsub messages
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_pubsub_triggers (
|
||||
id TEXT PRIMARY KEY,
|
||||
function_id TEXT NOT NULL,
|
||||
topic TEXT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (function_id) REFERENCES functions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_pubsub_triggers_function ON function_pubsub_triggers(function_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_pubsub_triggers_topic ON function_pubsub_triggers(topic)
|
||||
WHERE enabled = TRUE;
|
||||
|
||||
-- =============================================================================
|
||||
-- ONE-TIME TIMERS
|
||||
-- Schedule functions to run once at a specific time
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_timers (
|
||||
id TEXT PRIMARY KEY,
|
||||
function_id TEXT NOT NULL,
|
||||
run_at TIMESTAMP NOT NULL,
|
||||
payload TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed')),
|
||||
error TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
FOREIGN KEY (function_id) REFERENCES functions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_timers_function ON function_timers(function_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_timers_pending ON function_timers(run_at)
|
||||
WHERE status = 'pending';
|
||||
|
||||
-- =============================================================================
|
||||
-- BACKGROUND JOBS
|
||||
-- Long-running async function execution
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
function_id TEXT NOT NULL,
|
||||
payload TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed', 'cancelled')),
|
||||
progress INTEGER NOT NULL DEFAULT 0 CHECK(progress >= 0 AND progress <= 100),
|
||||
result TEXT,
|
||||
error TEXT,
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (function_id) REFERENCES functions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_jobs_function ON function_jobs(function_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_jobs_status ON function_jobs(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_jobs_pending ON function_jobs(created_at)
|
||||
WHERE status = 'pending';
|
||||
|
||||
-- =============================================================================
|
||||
-- INVOCATION LOGS
|
||||
-- Record of all function invocations for debugging and metrics
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_invocations (
|
||||
id TEXT PRIMARY KEY,
|
||||
function_id TEXT NOT NULL,
|
||||
request_id TEXT NOT NULL,
|
||||
trigger_type TEXT NOT NULL,
|
||||
caller_wallet TEXT,
|
||||
input_size INTEGER,
|
||||
output_size INTEGER,
|
||||
started_at TIMESTAMP NOT NULL,
|
||||
completed_at TIMESTAMP,
|
||||
duration_ms INTEGER,
|
||||
status TEXT CHECK(status IN ('success', 'error', 'timeout')),
|
||||
error_message TEXT,
|
||||
memory_used_mb REAL,
|
||||
FOREIGN KEY (function_id) REFERENCES functions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_invocations_function ON function_invocations(function_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_invocations_request ON function_invocations(request_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_invocations_time ON function_invocations(started_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_invocations_status ON function_invocations(function_id, status);
|
||||
|
||||
-- =============================================================================
|
||||
-- FUNCTION LOGS
|
||||
-- Captured log output from function execution
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
function_id TEXT NOT NULL,
|
||||
invocation_id TEXT NOT NULL,
|
||||
level TEXT NOT NULL CHECK(level IN ('info', 'warn', 'error', 'debug')),
|
||||
message TEXT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (function_id) REFERENCES functions(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (invocation_id) REFERENCES function_invocations(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_logs_invocation ON function_logs(invocation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_logs_function ON function_logs(function_id, timestamp);
|
||||
|
||||
-- =============================================================================
|
||||
-- DB CHANGE TRACKING
|
||||
-- Track last processed row for database triggers (CDC-like)
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_db_change_tracking (
|
||||
id TEXT PRIMARY KEY,
|
||||
trigger_id TEXT NOT NULL UNIQUE,
|
||||
last_row_id INTEGER,
|
||||
last_updated_at TIMESTAMP,
|
||||
last_check_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (trigger_id) REFERENCES function_db_triggers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- RATE LIMITING
|
||||
-- Track request counts for rate limiting
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS function_rate_limits (
|
||||
id TEXT PRIMARY KEY,
|
||||
window_key TEXT NOT NULL,
|
||||
count INTEGER NOT NULL DEFAULT 0,
|
||||
window_start TIMESTAMP NOT NULL,
|
||||
UNIQUE(window_key, window_start)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_rate_limits_window ON function_rate_limits(window_key, window_start);
|
||||
|
||||
-- =============================================================================
|
||||
-- MIGRATION VERSION TRACKING
|
||||
-- =============================================================================
|
||||
INSERT OR IGNORE INTO schema_migrations(version) VALUES (4);
|
||||
|
||||
COMMIT;
|
||||
|
||||
@ -1,321 +0,0 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: DeBros Gateway API
|
||||
version: 0.40.0
|
||||
description: REST API over the DeBros Network client for storage, database, and pubsub.
|
||||
servers:
|
||||
- url: http://localhost:6001
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- BearerAuth: []
|
||||
components:
|
||||
securitySchemes:
|
||||
ApiKeyAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-Key
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
schemas:
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
QueryRequest:
|
||||
type: object
|
||||
required: [sql]
|
||||
properties:
|
||||
sql:
|
||||
type: string
|
||||
args:
|
||||
type: array
|
||||
items: {}
|
||||
QueryResponse:
|
||||
type: object
|
||||
properties:
|
||||
columns:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
rows:
|
||||
type: array
|
||||
items:
|
||||
type: array
|
||||
items: {}
|
||||
count:
|
||||
type: integer
|
||||
format: int64
|
||||
TransactionRequest:
|
||||
type: object
|
||||
required: [statements]
|
||||
properties:
|
||||
statements:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
CreateTableRequest:
|
||||
type: object
|
||||
required: [schema]
|
||||
properties:
|
||||
schema:
|
||||
type: string
|
||||
DropTableRequest:
|
||||
type: object
|
||||
required: [table]
|
||||
properties:
|
||||
table:
|
||||
type: string
|
||||
TopicsResponse:
|
||||
type: object
|
||||
properties:
|
||||
topics:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
paths:
|
||||
/v1/health:
|
||||
get:
|
||||
summary: Gateway health
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/v1/storage/put:
|
||||
post:
|
||||
summary: Store a value by key
|
||||
parameters:
|
||||
- in: query
|
||||
name: key
|
||||
schema: { type: string }
|
||||
required: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
"201": { description: Created }
|
||||
"400":
|
||||
{
|
||||
description: Bad Request,
|
||||
content:
|
||||
{
|
||||
application/json:
|
||||
{ schema: { $ref: "#/components/schemas/Error" } },
|
||||
},
|
||||
}
|
||||
"401": { description: Unauthorized }
|
||||
"500":
|
||||
{
|
||||
description: Error,
|
||||
content:
|
||||
{
|
||||
application/json:
|
||||
{ schema: { $ref: "#/components/schemas/Error" } },
|
||||
},
|
||||
}
|
||||
/v1/storage/get:
|
||||
get:
|
||||
summary: Get a value by key
|
||||
parameters:
|
||||
- in: query
|
||||
name: key
|
||||
schema: { type: string }
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
"404":
|
||||
{
|
||||
description: Not Found,
|
||||
content:
|
||||
{
|
||||
application/json:
|
||||
{ schema: { $ref: "#/components/schemas/Error" } },
|
||||
},
|
||||
}
|
||||
/v1/storage/exists:
|
||||
get:
|
||||
summary: Check key existence
|
||||
parameters:
|
||||
- in: query
|
||||
name: key
|
||||
schema: { type: string }
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
exists:
|
||||
type: boolean
|
||||
/v1/storage/list:
|
||||
get:
|
||||
summary: List keys by prefix
|
||||
parameters:
|
||||
- in: query
|
||||
name: prefix
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
keys:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
/v1/storage/delete:
|
||||
post:
|
||||
summary: Delete a key
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [key]
|
||||
properties:
|
||||
key: { type: string }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/v1/rqlite/create-table:
|
||||
post:
|
||||
summary: Create tables via SQL DDL
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CreateTableRequest" }
|
||||
responses:
|
||||
"201": { description: Created }
|
||||
"400":
|
||||
{
|
||||
description: Bad Request,
|
||||
content:
|
||||
{
|
||||
application/json:
|
||||
{ schema: { $ref: "#/components/schemas/Error" } },
|
||||
},
|
||||
}
|
||||
"500":
|
||||
{
|
||||
description: Error,
|
||||
content:
|
||||
{
|
||||
application/json:
|
||||
{ schema: { $ref: "#/components/schemas/Error" } },
|
||||
},
|
||||
}
|
||||
/v1/rqlite/drop-table:
|
||||
post:
|
||||
summary: Drop a table
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/DropTableRequest" }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/v1/rqlite/query:
|
||||
post:
|
||||
summary: Execute a single SQL query
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/QueryRequest" }
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/QueryResponse" }
|
||||
"400":
|
||||
{
|
||||
description: Bad Request,
|
||||
content:
|
||||
{
|
||||
application/json:
|
||||
{ schema: { $ref: "#/components/schemas/Error" } },
|
||||
},
|
||||
}
|
||||
"500":
|
||||
{
|
||||
description: Error,
|
||||
content:
|
||||
{
|
||||
application/json:
|
||||
{ schema: { $ref: "#/components/schemas/Error" } },
|
||||
},
|
||||
}
|
||||
/v1/rqlite/transaction:
|
||||
post:
|
||||
summary: Execute multiple SQL statements atomically
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/TransactionRequest" }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
"400":
|
||||
{
|
||||
description: Bad Request,
|
||||
content:
|
||||
{
|
||||
application/json:
|
||||
{ schema: { $ref: "#/components/schemas/Error" } },
|
||||
},
|
||||
}
|
||||
"500":
|
||||
{
|
||||
description: Error,
|
||||
content:
|
||||
{
|
||||
application/json:
|
||||
{ schema: { $ref: "#/components/schemas/Error" } },
|
||||
},
|
||||
}
|
||||
/v1/rqlite/schema:
|
||||
get:
|
||||
summary: Get current database schema
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/v1/pubsub/publish:
|
||||
post:
|
||||
summary: Publish to a topic
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [topic, data_base64]
|
||||
properties:
|
||||
topic: { type: string }
|
||||
data_base64: { type: string }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/v1/pubsub/topics:
|
||||
get:
|
||||
summary: List topics in caller namespace
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/TopicsResponse" }
|
||||
@ -34,15 +34,15 @@ func GetCredentialsPath() (string, error) {
|
||||
return "", fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
|
||||
debrosDir := filepath.Join(homeDir, ".debros")
|
||||
if err := os.MkdirAll(debrosDir, 0700); err != nil {
|
||||
return "", fmt.Errorf("failed to create .debros directory: %w", err)
|
||||
oramaDir := filepath.Join(homeDir, ".orama")
|
||||
if err := os.MkdirAll(oramaDir, 0700); err != nil {
|
||||
return "", fmt.Errorf("failed to create .orama directory: %w", err)
|
||||
}
|
||||
|
||||
return filepath.Join(debrosDir, "credentials.json"), nil
|
||||
return filepath.Join(oramaDir, "credentials.json"), nil
|
||||
}
|
||||
|
||||
// LoadCredentials loads credentials from ~/.debros/credentials.json
|
||||
// LoadCredentials loads credentials from ~/.orama/credentials.json
|
||||
func LoadCredentials() (*CredentialStore, error) {
|
||||
credPath, err := GetCredentialsPath()
|
||||
if err != nil {
|
||||
@ -80,7 +80,7 @@ func LoadCredentials() (*CredentialStore, error) {
|
||||
return &store, nil
|
||||
}
|
||||
|
||||
// SaveCredentials saves credentials to ~/.debros/credentials.json
|
||||
// SaveCredentials saves credentials to ~/.orama/credentials.json
|
||||
func (store *CredentialStore) SaveCredentials() error {
|
||||
credPath, err := GetCredentialsPath()
|
||||
if err != nil {
|
||||
|
||||
@ -10,6 +10,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/tlsutil"
|
||||
)
|
||||
|
||||
// PerformSimpleAuthentication performs a simple authentication flow where the user
|
||||
@ -91,7 +93,13 @@ func requestAPIKeyFromGateway(gatewayURL, wallet, namespace string) (string, err
|
||||
}
|
||||
|
||||
endpoint := gatewayURL + "/v1/auth/simple-key"
|
||||
resp, err := http.Post(endpoint, "application/json", bytes.NewReader(payload))
|
||||
|
||||
// Extract domain from URL for TLS configuration
|
||||
// This uses tlsutil which handles Let's Encrypt staging certificates for *.debros.network
|
||||
domain := extractDomainFromURL(gatewayURL)
|
||||
client := tlsutil.NewHTTPClientForDomain(30*time.Second, domain)
|
||||
|
||||
resp, err := client.Post(endpoint, "application/json", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to call gateway: %w", err)
|
||||
}
|
||||
@ -114,3 +122,23 @@ func requestAPIKeyFromGateway(gatewayURL, wallet, namespace string) (string, err
|
||||
|
||||
return apiKey, nil
|
||||
}
|
||||
|
||||
// extractDomainFromURL extracts the domain from a URL
|
||||
// Removes protocol (https://, http://), path, and port components
|
||||
func extractDomainFromURL(url string) string {
|
||||
// Remove protocol prefixes
|
||||
url = strings.TrimPrefix(url, "https://")
|
||||
url = strings.TrimPrefix(url, "http://")
|
||||
|
||||
// Remove path component
|
||||
if idx := strings.Index(url, "/"); idx != -1 {
|
||||
url = url[:idx]
|
||||
}
|
||||
|
||||
// Remove port component
|
||||
if idx := strings.Index(url, ":"); idx != -1 {
|
||||
url = url[:idx]
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
@ -199,7 +199,7 @@ func (as *AuthServer) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
%s
|
||||
</div>
|
||||
|
||||
<p>Your credentials have been saved securely to <code>~/.debros/credentials.json</code></p>
|
||||
<p>Your credentials have been saved securely to <code>~/.orama/credentials.json</code></p>
|
||||
<p><strong>You can now close this browser window and return to your terminal.</strong></p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
257
pkg/certutil/cert_manager.go
Normal file
257
pkg/certutil/cert_manager.go
Normal file
@ -0,0 +1,257 @@
|
||||
// Package certutil provides utilities for managing self-signed certificates
|
||||
package certutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CertificateManager manages self-signed certificates for the network
|
||||
type CertificateManager struct {
|
||||
baseDir string
|
||||
}
|
||||
|
||||
// NewCertificateManager creates a new certificate manager
|
||||
func NewCertificateManager(baseDir string) *CertificateManager {
|
||||
return &CertificateManager{
|
||||
baseDir: baseDir,
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureCACertificate creates or loads the CA certificate
|
||||
func (cm *CertificateManager) EnsureCACertificate() ([]byte, []byte, error) {
|
||||
caCertPath := filepath.Join(cm.baseDir, "ca.crt")
|
||||
caKeyPath := filepath.Join(cm.baseDir, "ca.key")
|
||||
|
||||
// Check if CA already exists
|
||||
if _, err := os.Stat(caCertPath); err == nil {
|
||||
certPEM, err := os.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read CA certificate: %w", err)
|
||||
}
|
||||
keyPEM, err := os.ReadFile(caKeyPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read CA key: %w", err)
|
||||
}
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// Create new CA certificate
|
||||
certPEM, keyPEM, err := cm.generateCACertificate()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(cm.baseDir, 0700); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create cert directory: %w", err)
|
||||
}
|
||||
|
||||
// Write to files
|
||||
if err := os.WriteFile(caCertPath, certPEM, 0644); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to write CA certificate: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(caKeyPath, keyPEM, 0600); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to write CA key: %w", err)
|
||||
}
|
||||
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// EnsureNodeCertificate creates or loads a node certificate signed by the CA
|
||||
func (cm *CertificateManager) EnsureNodeCertificate(hostname string, caCertPEM, caKeyPEM []byte) ([]byte, []byte, error) {
|
||||
certPath := filepath.Join(cm.baseDir, fmt.Sprintf("%s.crt", hostname))
|
||||
keyPath := filepath.Join(cm.baseDir, fmt.Sprintf("%s.key", hostname))
|
||||
|
||||
// Check if certificate already exists
|
||||
if _, err := os.Stat(certPath); err == nil {
|
||||
certData, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read certificate: %w", err)
|
||||
}
|
||||
keyData, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read key: %w", err)
|
||||
}
|
||||
return certData, keyData, nil
|
||||
}
|
||||
|
||||
// Create new certificate
|
||||
certPEM, keyPEM, err := cm.generateNodeCertificate(hostname, caCertPEM, caKeyPEM)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Write to files
|
||||
if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to write certificate: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to write key: %w", err)
|
||||
}
|
||||
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// generateCACertificate generates a self-signed CA certificate
|
||||
func (cm *CertificateManager) generateCACertificate() ([]byte, []byte, error) {
|
||||
// Generate private key
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
|
||||
}
|
||||
|
||||
// Create certificate template
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "DeBros Network Root CA",
|
||||
Organization: []string{"DeBros"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(10, 0, 0), // 10 year validity
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
// Self-sign the certificate
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
|
||||
}
|
||||
|
||||
// Encode certificate to PEM
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certDER,
|
||||
})
|
||||
|
||||
// Encode private key to PEM
|
||||
keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal private key: %w", err)
|
||||
}
|
||||
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: keyDER,
|
||||
})
|
||||
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// generateNodeCertificate generates a certificate signed by the CA
|
||||
func (cm *CertificateManager) generateNodeCertificate(hostname string, caCertPEM, caKeyPEM []byte) ([]byte, []byte, error) {
|
||||
// Parse CA certificate and key
|
||||
caCert, caKey, err := cm.parseCACertificate(caCertPEM, caKeyPEM)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Generate node private key
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
|
||||
}
|
||||
|
||||
// Create certificate template
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{
|
||||
CommonName: hostname,
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(5, 0, 0), // 5 year validity
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
DNSNames: []string{hostname},
|
||||
}
|
||||
|
||||
// Add wildcard support if hostname contains *.debros.network
|
||||
if hostname == "*.debros.network" {
|
||||
template.DNSNames = []string{"*.debros.network", "debros.network"}
|
||||
} else if hostname == "debros.network" {
|
||||
template.DNSNames = []string{"*.debros.network", "debros.network"}
|
||||
}
|
||||
|
||||
// Try to parse as IP address for IP-based certificates
|
||||
if ip := net.ParseIP(hostname); ip != nil {
|
||||
template.IPAddresses = []net.IP{ip}
|
||||
template.DNSNames = nil
|
||||
}
|
||||
|
||||
// Sign certificate with CA
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, caCert, &privateKey.PublicKey, caKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
|
||||
}
|
||||
|
||||
// Encode certificate to PEM
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certDER,
|
||||
})
|
||||
|
||||
// Encode private key to PEM
|
||||
keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal private key: %w", err)
|
||||
}
|
||||
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: keyDER,
|
||||
})
|
||||
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// parseCACertificate parses CA certificate and key from PEM
|
||||
func (cm *CertificateManager) parseCACertificate(caCertPEM, caKeyPEM []byte) (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
// Parse CA certificate
|
||||
certBlock, _ := pem.Decode(caCertPEM)
|
||||
if certBlock == nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse CA certificate PEM")
|
||||
}
|
||||
|
||||
caCert, err := x509.ParseCertificate(certBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse CA certificate: %w", err)
|
||||
}
|
||||
|
||||
// Parse CA private key
|
||||
keyBlock, _ := pem.Decode(caKeyPEM)
|
||||
if keyBlock == nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse CA key PEM")
|
||||
}
|
||||
|
||||
caKey, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse CA key: %w", err)
|
||||
}
|
||||
|
||||
rsaKey, ok := caKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("CA key is not RSA")
|
||||
}
|
||||
|
||||
return caCert, rsaKey, nil
|
||||
}
|
||||
|
||||
// LoadTLSCertificate loads a TLS certificate from PEM files
|
||||
func LoadTLSCertificate(certPEM, keyPEM []byte) (tls.Certificate, error) {
|
||||
return tls.X509KeyPair(certPEM, keyPEM)
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/auth"
|
||||
)
|
||||
@ -50,13 +52,14 @@ func showAuthHelp() {
|
||||
fmt.Printf(" 1. Run 'dbn auth login'\n")
|
||||
fmt.Printf(" 2. Enter your wallet address when prompted\n")
|
||||
fmt.Printf(" 3. Enter your namespace (or press Enter for 'default')\n")
|
||||
fmt.Printf(" 4. An API key will be generated and saved to ~/.debros/credentials.json\n\n")
|
||||
fmt.Printf(" 4. An API key will be generated and saved to ~/.orama/credentials.json\n\n")
|
||||
fmt.Printf("Note: Authentication uses the currently active environment.\n")
|
||||
fmt.Printf(" Use 'dbn env current' to see your active environment.\n")
|
||||
}
|
||||
|
||||
func handleAuthLogin() {
|
||||
gatewayURL := getGatewayURL()
|
||||
// Prompt for node selection
|
||||
gatewayURL := promptForGatewayURL()
|
||||
fmt.Printf("🔐 Authenticating with gateway at: %s\n", gatewayURL)
|
||||
|
||||
// Use the simple authentication flow
|
||||
@ -161,7 +164,55 @@ func handleAuthStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
// promptForGatewayURL interactively prompts for the gateway URL
|
||||
// Allows user to choose between local node or remote node by domain
|
||||
func promptForGatewayURL() string {
|
||||
// Check environment variable first (allows override without prompting)
|
||||
if url := os.Getenv("DEBROS_GATEWAY_URL"); url != "" {
|
||||
return url
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Println("\n🌐 Node Connection")
|
||||
fmt.Println("==================")
|
||||
fmt.Println("1. Local node (localhost:6001)")
|
||||
fmt.Println("2. Remote node (enter domain)")
|
||||
fmt.Print("\nSelect option [1/2]: ")
|
||||
|
||||
choice, _ := reader.ReadString('\n')
|
||||
choice = strings.TrimSpace(choice)
|
||||
|
||||
if choice == "1" || choice == "" {
|
||||
return "http://localhost:6001"
|
||||
}
|
||||
|
||||
if choice != "2" {
|
||||
fmt.Println("⚠️ Invalid option, using localhost")
|
||||
return "http://localhost:6001"
|
||||
}
|
||||
|
||||
fmt.Print("Enter node domain (e.g., node-hk19de.debros.network): ")
|
||||
domain, _ := reader.ReadString('\n')
|
||||
domain = strings.TrimSpace(domain)
|
||||
|
||||
if domain == "" {
|
||||
fmt.Println("⚠️ No domain entered, using localhost")
|
||||
return "http://localhost:6001"
|
||||
}
|
||||
|
||||
// Remove any protocol prefix if user included it
|
||||
domain = strings.TrimPrefix(domain, "https://")
|
||||
domain = strings.TrimPrefix(domain, "http://")
|
||||
// Remove trailing slash
|
||||
domain = strings.TrimSuffix(domain, "/")
|
||||
|
||||
// Use HTTPS for remote domains
|
||||
return fmt.Sprintf("https://%s", domain)
|
||||
}
|
||||
|
||||
// getGatewayURL returns the gateway URL based on environment or env var
|
||||
// Used by other commands that don't need interactive node selection
|
||||
func getGatewayURL() string {
|
||||
// Check environment variable first (for backwards compatibility)
|
||||
if url := os.Getenv("DEBROS_GATEWAY_URL"); url != "" {
|
||||
@ -174,6 +225,6 @@ func getGatewayURL() string {
|
||||
return env.GatewayURL
|
||||
}
|
||||
|
||||
// Fallback to default
|
||||
// Fallback to default (node-1)
|
||||
return "http://localhost:6001"
|
||||
}
|
||||
|
||||
@ -249,14 +249,11 @@ func createClient() (client.NetworkClient, error) {
|
||||
gatewayURL := getGatewayURL()
|
||||
config.GatewayURL = gatewayURL
|
||||
|
||||
// Try to get bootstrap peers from active environment
|
||||
// For now, we'll use the default bootstrap peers from config
|
||||
// In the future, environments could specify their own bootstrap peers
|
||||
// Try to get peer configuration from active environment
|
||||
env, err := GetActiveEnvironment()
|
||||
if err == nil && env != nil {
|
||||
// Environment loaded successfully - gateway URL already set above
|
||||
// Bootstrap peers could be added to Environment struct in the future
|
||||
_ = env // Use env if we add bootstrap peers to it
|
||||
_ = env // Reserve for future peer configuration
|
||||
}
|
||||
|
||||
// Check for existing credentials using enhanced authentication
|
||||
|
||||
@ -40,30 +40,30 @@ func HandleDevCommand(args []string) {
|
||||
|
||||
func showDevHelp() {
|
||||
fmt.Printf("🚀 Development Environment Commands\n\n")
|
||||
fmt.Printf("Usage: dbn dev <subcommand> [options]\n\n")
|
||||
fmt.Printf("Usage: orama dev <subcommand> [options]\n\n")
|
||||
fmt.Printf("Subcommands:\n")
|
||||
fmt.Printf(" up - Start development environment (2 bootstraps + 3 nodes + gateway)\n")
|
||||
fmt.Printf(" up - Start development environment (5 nodes + gateway)\n")
|
||||
fmt.Printf(" down - Stop all development services\n")
|
||||
fmt.Printf(" status - Show status of running services\n")
|
||||
fmt.Printf(" logs <component> - Tail logs for a component\n")
|
||||
fmt.Printf(" help - Show this help\n\n")
|
||||
fmt.Printf("Examples:\n")
|
||||
fmt.Printf(" dbn dev up\n")
|
||||
fmt.Printf(" dbn dev down\n")
|
||||
fmt.Printf(" dbn dev status\n")
|
||||
fmt.Printf(" dbn dev logs bootstrap --follow\n")
|
||||
fmt.Printf(" orama dev up\n")
|
||||
fmt.Printf(" orama dev down\n")
|
||||
fmt.Printf(" orama dev status\n")
|
||||
fmt.Printf(" orama dev logs node-1 --follow\n")
|
||||
}
|
||||
|
||||
func handleDevUp(args []string) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Get home directory and .debros path
|
||||
// Get home directory and .orama path
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to get home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
debrosDir := filepath.Join(homeDir, ".debros")
|
||||
oramaDir := filepath.Join(homeDir, ".orama")
|
||||
|
||||
// Step 1: Check dependencies
|
||||
fmt.Printf("📋 Checking dependencies...\n\n")
|
||||
@ -90,7 +90,7 @@ func handleDevUp(args []string) {
|
||||
|
||||
// Step 3: Ensure configs
|
||||
fmt.Printf("⚙️ Preparing configuration files...\n\n")
|
||||
ensurer := development.NewConfigEnsurer(debrosDir)
|
||||
ensurer := development.NewConfigEnsurer(oramaDir)
|
||||
if err := ensurer.EnsureAll(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to prepare configs: %v\n", err)
|
||||
os.Exit(1)
|
||||
@ -98,7 +98,7 @@ func handleDevUp(args []string) {
|
||||
fmt.Printf("\n")
|
||||
|
||||
// Step 4: Start services
|
||||
pm := development.NewProcessManager(debrosDir, os.Stdout)
|
||||
pm := development.NewProcessManager(oramaDir, os.Stdout)
|
||||
if err := pm.StartAll(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error starting services: %v\n", err)
|
||||
os.Exit(1)
|
||||
@ -108,19 +108,19 @@ func handleDevUp(args []string) {
|
||||
fmt.Printf("🎉 Development environment is running!\n\n")
|
||||
fmt.Printf("Key endpoints:\n")
|
||||
fmt.Printf(" Gateway: http://localhost:6001\n")
|
||||
fmt.Printf(" Bootstrap IPFS: http://localhost:4501\n")
|
||||
fmt.Printf(" Bootstrap2 IPFS: http://localhost:4511\n")
|
||||
fmt.Printf(" Node2 IPFS: http://localhost:4502\n")
|
||||
fmt.Printf(" Node3 IPFS: http://localhost:4503\n")
|
||||
fmt.Printf(" Node4 IPFS: http://localhost:4504\n")
|
||||
fmt.Printf(" Node-1 IPFS: http://localhost:4501\n")
|
||||
fmt.Printf(" Node-2 IPFS: http://localhost:4502\n")
|
||||
fmt.Printf(" Node-3 IPFS: http://localhost:4503\n")
|
||||
fmt.Printf(" Node-4 IPFS: http://localhost:4504\n")
|
||||
fmt.Printf(" Node-5 IPFS: http://localhost:4505\n")
|
||||
fmt.Printf(" Anon SOCKS: 127.0.0.1:9050\n")
|
||||
fmt.Printf(" Olric Cache: http://localhost:3320\n\n")
|
||||
fmt.Printf("Useful commands:\n")
|
||||
fmt.Printf(" dbn dev status - Show status\n")
|
||||
fmt.Printf(" dbn dev logs bootstrap - Bootstrap logs\n")
|
||||
fmt.Printf(" dbn dev logs bootstrap2 - Bootstrap2 logs\n")
|
||||
fmt.Printf(" dbn dev down - Stop all services\n\n")
|
||||
fmt.Printf("Logs directory: %s/logs\n\n", debrosDir)
|
||||
fmt.Printf(" orama dev status - Show status\n")
|
||||
fmt.Printf(" orama dev logs node-1 - Node-1 logs\n")
|
||||
fmt.Printf(" orama dev logs node-2 - Node-2 logs\n")
|
||||
fmt.Printf(" orama dev down - Stop all services\n\n")
|
||||
fmt.Printf("Logs directory: %s/logs\n\n", oramaDir)
|
||||
}
|
||||
|
||||
func handleDevDown(args []string) {
|
||||
@ -129,14 +129,17 @@ func handleDevDown(args []string) {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to get home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
debrosDir := filepath.Join(homeDir, ".debros")
|
||||
oramaDir := filepath.Join(homeDir, ".orama")
|
||||
|
||||
pm := development.NewProcessManager(debrosDir, os.Stdout)
|
||||
pm := development.NewProcessManager(oramaDir, os.Stdout)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := pm.StopAll(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Error stopping services: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ All services have been stopped\n\n")
|
||||
}
|
||||
|
||||
func handleDevStatus(args []string) {
|
||||
@ -145,9 +148,9 @@ func handleDevStatus(args []string) {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to get home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
debrosDir := filepath.Join(homeDir, ".debros")
|
||||
oramaDir := filepath.Join(homeDir, ".orama")
|
||||
|
||||
pm := development.NewProcessManager(debrosDir, os.Stdout)
|
||||
pm := development.NewProcessManager(oramaDir, os.Stdout)
|
||||
ctx := context.Background()
|
||||
|
||||
pm.Status(ctx)
|
||||
@ -156,7 +159,7 @@ func handleDevStatus(args []string) {
|
||||
func handleDevLogs(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: dbn dev logs <component> [--follow]\n")
|
||||
fmt.Fprintf(os.Stderr, "\nComponents: bootstrap, bootstrap2, node2, node3, node4, gateway, ipfs-bootstrap, ipfs-bootstrap2, ipfs-node2, ipfs-node3, ipfs-node4, olric, anon\n")
|
||||
fmt.Fprintf(os.Stderr, "\nComponents: node-1, node-2, node-3, node-4, node-5, gateway, ipfs-node-1, ipfs-node-2, ipfs-node-3, ipfs-node-4, ipfs-node-5, olric, anon\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -168,9 +171,9 @@ func handleDevLogs(args []string) {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to get home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
debrosDir := filepath.Join(homeDir, ".debros")
|
||||
oramaDir := filepath.Join(homeDir, ".orama")
|
||||
|
||||
logPath := filepath.Join(debrosDir, "logs", fmt.Sprintf("%s.log", component))
|
||||
logPath := filepath.Join(oramaDir, "logs", fmt.Sprintf("%s.log", component))
|
||||
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "❌ Log file not found: %s\n", logPath)
|
||||
os.Exit(1)
|
||||
|
||||
@ -43,8 +43,8 @@ func showEnvHelp() {
|
||||
fmt.Printf(" enable - Alias for 'switch' (e.g., 'devnet enable')\n\n")
|
||||
fmt.Printf("Available Environments:\n")
|
||||
fmt.Printf(" local - Local development (http://localhost:6001)\n")
|
||||
fmt.Printf(" devnet - Development network (https://devnet.debros.network)\n")
|
||||
fmt.Printf(" testnet - Test network (https://testnet.debros.network)\n\n")
|
||||
fmt.Printf(" devnet - Development network (https://devnet.orama.network)\n")
|
||||
fmt.Printf(" testnet - Test network (https://testnet.orama.network)\n\n")
|
||||
fmt.Printf("Examples:\n")
|
||||
fmt.Printf(" dbn env list\n")
|
||||
fmt.Printf(" dbn env current\n")
|
||||
|
||||
@ -28,18 +28,18 @@ var DefaultEnvironments = []Environment{
|
||||
{
|
||||
Name: "local",
|
||||
GatewayURL: "http://localhost:6001",
|
||||
Description: "Local development environment",
|
||||
Description: "Local development environment (node-1)",
|
||||
IsActive: true,
|
||||
},
|
||||
{
|
||||
Name: "devnet",
|
||||
GatewayURL: "https://devnet.debros.network",
|
||||
GatewayURL: "https://devnet.orama.network",
|
||||
Description: "Development network (testnet)",
|
||||
IsActive: false,
|
||||
},
|
||||
{
|
||||
Name: "testnet",
|
||||
GatewayURL: "https://testnet.debros.network",
|
||||
GatewayURL: "https://testnet.orama.network",
|
||||
Description: "Test network (staging)",
|
||||
IsActive: false,
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,79 +2,172 @@ package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/utils"
|
||||
)
|
||||
|
||||
// TestProdCommandFlagParsing verifies that prod command flags are parsed correctly
|
||||
// Note: The installer now uses --vps-ip presence to determine if it's a first node (no --bootstrap flag)
|
||||
// First node: has --vps-ip but no --peers or --join
|
||||
// Joining node: has --vps-ip, --peers, and --cluster-secret
|
||||
func TestProdCommandFlagParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectBootstrap bool
|
||||
expectVPSIP string
|
||||
expectBootstrapJoin string
|
||||
expectPeers string
|
||||
name string
|
||||
args []string
|
||||
expectVPSIP string
|
||||
expectDomain string
|
||||
expectPeers string
|
||||
expectJoin string
|
||||
expectSecret string
|
||||
expectBranch string
|
||||
isFirstNode bool // first node = no peers and no join address
|
||||
}{
|
||||
{
|
||||
name: "bootstrap node",
|
||||
args: []string{"install", "--bootstrap"},
|
||||
expectBootstrap: true,
|
||||
name: "first node (creates new cluster)",
|
||||
args: []string{"install", "--vps-ip", "10.0.0.1", "--domain", "node-1.example.com"},
|
||||
expectVPSIP: "10.0.0.1",
|
||||
expectDomain: "node-1.example.com",
|
||||
isFirstNode: true,
|
||||
},
|
||||
{
|
||||
name: "non-bootstrap with vps-ip",
|
||||
args: []string{"install", "--vps-ip", "10.0.0.2", "--peers", "multiaddr1,multiaddr2"},
|
||||
expectVPSIP: "10.0.0.2",
|
||||
expectPeers: "multiaddr1,multiaddr2",
|
||||
name: "joining node with peers",
|
||||
args: []string{"install", "--vps-ip", "10.0.0.2", "--peers", "/ip4/10.0.0.1/tcp/4001/p2p/Qm123", "--cluster-secret", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},
|
||||
expectVPSIP: "10.0.0.2",
|
||||
expectPeers: "/ip4/10.0.0.1/tcp/4001/p2p/Qm123",
|
||||
expectSecret: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
isFirstNode: false,
|
||||
},
|
||||
{
|
||||
name: "secondary bootstrap",
|
||||
args: []string{"install", "--bootstrap", "--vps-ip", "10.0.0.3", "--bootstrap-join", "10.0.0.1:7001"},
|
||||
expectBootstrap: true,
|
||||
expectVPSIP: "10.0.0.3",
|
||||
expectBootstrapJoin: "10.0.0.1:7001",
|
||||
name: "joining node with join address",
|
||||
args: []string{"install", "--vps-ip", "10.0.0.3", "--join", "10.0.0.1:7001", "--cluster-secret", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},
|
||||
expectVPSIP: "10.0.0.3",
|
||||
expectJoin: "10.0.0.1:7001",
|
||||
expectSecret: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
isFirstNode: false,
|
||||
},
|
||||
{
|
||||
name: "with domain",
|
||||
args: []string{"install", "--bootstrap", "--domain", "example.com"},
|
||||
expectBootstrap: true,
|
||||
name: "with nightly branch",
|
||||
args: []string{"install", "--vps-ip", "10.0.0.4", "--branch", "nightly"},
|
||||
expectVPSIP: "10.0.0.4",
|
||||
expectBranch: "nightly",
|
||||
isFirstNode: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Extract flags manually to verify parsing logic
|
||||
isBootstrap := false
|
||||
var vpsIP, peersStr, bootstrapJoin string
|
||||
var vpsIP, domain, peersStr, joinAddr, clusterSecret, branch string
|
||||
|
||||
for i, arg := range tt.args {
|
||||
switch arg {
|
||||
case "--bootstrap":
|
||||
isBootstrap = true
|
||||
case "--peers":
|
||||
if i+1 < len(tt.args) {
|
||||
peersStr = tt.args[i+1]
|
||||
}
|
||||
case "--vps-ip":
|
||||
if i+1 < len(tt.args) {
|
||||
vpsIP = tt.args[i+1]
|
||||
}
|
||||
case "--bootstrap-join":
|
||||
case "--domain":
|
||||
if i+1 < len(tt.args) {
|
||||
bootstrapJoin = tt.args[i+1]
|
||||
domain = tt.args[i+1]
|
||||
}
|
||||
case "--peers":
|
||||
if i+1 < len(tt.args) {
|
||||
peersStr = tt.args[i+1]
|
||||
}
|
||||
case "--join":
|
||||
if i+1 < len(tt.args) {
|
||||
joinAddr = tt.args[i+1]
|
||||
}
|
||||
case "--cluster-secret":
|
||||
if i+1 < len(tt.args) {
|
||||
clusterSecret = tt.args[i+1]
|
||||
}
|
||||
case "--branch":
|
||||
if i+1 < len(tt.args) {
|
||||
branch = tt.args[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isBootstrap != tt.expectBootstrap {
|
||||
t.Errorf("expected bootstrap=%v, got %v", tt.expectBootstrap, isBootstrap)
|
||||
}
|
||||
// First node detection: no peers and no join address
|
||||
isFirstNode := peersStr == "" && joinAddr == ""
|
||||
|
||||
if vpsIP != tt.expectVPSIP {
|
||||
t.Errorf("expected vpsIP=%q, got %q", tt.expectVPSIP, vpsIP)
|
||||
}
|
||||
if domain != tt.expectDomain {
|
||||
t.Errorf("expected domain=%q, got %q", tt.expectDomain, domain)
|
||||
}
|
||||
if peersStr != tt.expectPeers {
|
||||
t.Errorf("expected peers=%q, got %q", tt.expectPeers, peersStr)
|
||||
}
|
||||
if bootstrapJoin != tt.expectBootstrapJoin {
|
||||
t.Errorf("expected bootstrapJoin=%q, got %q", tt.expectBootstrapJoin, bootstrapJoin)
|
||||
if joinAddr != tt.expectJoin {
|
||||
t.Errorf("expected join=%q, got %q", tt.expectJoin, joinAddr)
|
||||
}
|
||||
if clusterSecret != tt.expectSecret {
|
||||
t.Errorf("expected clusterSecret=%q, got %q", tt.expectSecret, clusterSecret)
|
||||
}
|
||||
if branch != tt.expectBranch {
|
||||
t.Errorf("expected branch=%q, got %q", tt.expectBranch, branch)
|
||||
}
|
||||
if isFirstNode != tt.isFirstNode {
|
||||
t.Errorf("expected isFirstNode=%v, got %v", tt.isFirstNode, isFirstNode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizePeers tests the peer multiaddr normalization
|
||||
func TestNormalizePeers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectCount int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expectCount: 0,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "single peer",
|
||||
input: "/ip4/10.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj",
|
||||
expectCount: 1,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "multiple peers",
|
||||
input: "/ip4/10.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj,/ip4/10.0.0.2/tcp/4001/p2p/12D3KooWJzL4SHW3o7sZpzjfEPJzC6Ky7gKvJxY8vQVDR2jHc8F1",
|
||||
expectCount: 2,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "duplicate peers deduplicated",
|
||||
input: "/ip4/10.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj,/ip4/10.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj",
|
||||
expectCount: 1,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid multiaddr",
|
||||
input: "not-a-multiaddr",
|
||||
expectCount: 0,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
peers, err := utils.NormalizePeers(tt.input)
|
||||
|
||||
if tt.expectError && err == nil {
|
||||
t.Errorf("expected error but got none")
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if len(peers) != tt.expectCount {
|
||||
t.Errorf("expected %d peers, got %d", tt.expectCount, len(peers))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
109
pkg/cli/production/commands.go
Normal file
109
pkg/cli/production/commands.go
Normal file
@ -0,0 +1,109 @@
|
||||
package production
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/production/install"
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/production/lifecycle"
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/production/logs"
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/production/migrate"
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/production/status"
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/production/uninstall"
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/production/upgrade"
|
||||
)
|
||||
|
||||
// HandleCommand handles production environment commands
|
||||
func HandleCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
ShowHelp()
|
||||
return
|
||||
}
|
||||
|
||||
subcommand := args[0]
|
||||
subargs := args[1:]
|
||||
|
||||
switch subcommand {
|
||||
case "install":
|
||||
install.Handle(subargs)
|
||||
case "upgrade":
|
||||
upgrade.Handle(subargs)
|
||||
case "migrate":
|
||||
migrate.Handle(subargs)
|
||||
case "status":
|
||||
status.Handle()
|
||||
case "start":
|
||||
lifecycle.HandleStart()
|
||||
case "stop":
|
||||
lifecycle.HandleStop()
|
||||
case "restart":
|
||||
lifecycle.HandleRestart()
|
||||
case "logs":
|
||||
logs.Handle(subargs)
|
||||
case "uninstall":
|
||||
uninstall.Handle()
|
||||
case "help":
|
||||
ShowHelp()
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown prod subcommand: %s\n", subcommand)
|
||||
ShowHelp()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ShowHelp displays help information for production commands
|
||||
func ShowHelp() {
|
||||
fmt.Printf("Production Environment Commands\n\n")
|
||||
fmt.Printf("Usage: orama <subcommand> [options]\n\n")
|
||||
fmt.Printf("Subcommands:\n")
|
||||
fmt.Printf(" install - Install production node (requires root/sudo)\n")
|
||||
fmt.Printf(" Options:\n")
|
||||
fmt.Printf(" --interactive - Launch interactive TUI wizard\n")
|
||||
fmt.Printf(" --force - Reconfigure all settings\n")
|
||||
fmt.Printf(" --vps-ip IP - VPS public IP address (required)\n")
|
||||
fmt.Printf(" --domain DOMAIN - Domain for this node (e.g., node-1.orama.network)\n")
|
||||
fmt.Printf(" --peers ADDRS - Comma-separated peer multiaddrs (for joining cluster)\n")
|
||||
fmt.Printf(" --join ADDR - RQLite join address IP:port (for joining cluster)\n")
|
||||
fmt.Printf(" --cluster-secret HEX - 64-hex cluster secret (required when joining)\n")
|
||||
fmt.Printf(" --swarm-key HEX - 64-hex IPFS swarm key (required when joining)\n")
|
||||
fmt.Printf(" --ipfs-peer ID - IPFS peer ID to connect to (auto-discovered)\n")
|
||||
fmt.Printf(" --ipfs-addrs ADDRS - IPFS swarm addresses (auto-discovered)\n")
|
||||
fmt.Printf(" --ipfs-cluster-peer ID - IPFS Cluster peer ID (auto-discovered)\n")
|
||||
fmt.Printf(" --ipfs-cluster-addrs ADDRS - IPFS Cluster addresses (auto-discovered)\n")
|
||||
fmt.Printf(" --branch BRANCH - Git branch to use (main or nightly, default: main)\n")
|
||||
fmt.Printf(" --no-pull - Skip git clone/pull, use existing /home/debros/src\n")
|
||||
fmt.Printf(" --ignore-resource-checks - Skip disk/RAM/CPU prerequisite validation\n")
|
||||
fmt.Printf(" --dry-run - Show what would be done without making changes\n")
|
||||
fmt.Printf(" upgrade - Upgrade existing installation (requires root/sudo)\n")
|
||||
fmt.Printf(" Options:\n")
|
||||
fmt.Printf(" --restart - Automatically restart services after upgrade\n")
|
||||
fmt.Printf(" --branch BRANCH - Git branch to use (main or nightly)\n")
|
||||
fmt.Printf(" --no-pull - Skip git clone/pull, use existing source\n")
|
||||
fmt.Printf(" migrate - Migrate from old unified setup (requires root/sudo)\n")
|
||||
fmt.Printf(" Options:\n")
|
||||
fmt.Printf(" --dry-run - Show what would be migrated without making changes\n")
|
||||
fmt.Printf(" status - Show status of production services\n")
|
||||
fmt.Printf(" start - Start all production services (requires root/sudo)\n")
|
||||
fmt.Printf(" stop - Stop all production services (requires root/sudo)\n")
|
||||
fmt.Printf(" restart - Restart all production services (requires root/sudo)\n")
|
||||
fmt.Printf(" logs <service> - View production service logs\n")
|
||||
fmt.Printf(" Service aliases: node, ipfs, cluster, gateway, olric\n")
|
||||
fmt.Printf(" Options:\n")
|
||||
fmt.Printf(" --follow - Follow logs in real-time\n")
|
||||
fmt.Printf(" uninstall - Remove production services (requires root/sudo)\n\n")
|
||||
fmt.Printf("Examples:\n")
|
||||
fmt.Printf(" # First node (creates new cluster)\n")
|
||||
fmt.Printf(" sudo orama install --vps-ip 203.0.113.1 --domain node-1.orama.network\n\n")
|
||||
fmt.Printf(" # Join existing cluster\n")
|
||||
fmt.Printf(" sudo orama install --vps-ip 203.0.113.2 --domain node-2.orama.network \\\n")
|
||||
fmt.Printf(" --peers /ip4/203.0.113.1/tcp/4001/p2p/12D3KooW... \\\n")
|
||||
fmt.Printf(" --cluster-secret <64-hex-secret> --swarm-key <64-hex-swarm-key>\n\n")
|
||||
fmt.Printf(" # Upgrade\n")
|
||||
fmt.Printf(" sudo orama upgrade --restart\n\n")
|
||||
fmt.Printf(" # Service management\n")
|
||||
fmt.Printf(" sudo orama start\n")
|
||||
fmt.Printf(" sudo orama stop\n")
|
||||
fmt.Printf(" sudo orama restart\n\n")
|
||||
fmt.Printf(" orama status\n")
|
||||
fmt.Printf(" orama logs node --follow\n")
|
||||
}
|
||||
47
pkg/cli/production/install/command.go
Normal file
47
pkg/cli/production/install/command.go
Normal file
@ -0,0 +1,47 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Handle executes the install command
|
||||
func Handle(args []string) {
|
||||
// Parse flags
|
||||
flags, err := ParseFlags(args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create orchestrator
|
||||
orchestrator, err := NewOrchestrator(flags)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Validate flags
|
||||
if err := orchestrator.validator.ValidateFlags(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check root privileges
|
||||
if err := orchestrator.validator.ValidateRootPrivileges(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check port availability before proceeding
|
||||
if err := orchestrator.validator.ValidatePorts(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Execute installation
|
||||
if err := orchestrator.Execute(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
65
pkg/cli/production/install/flags.go
Normal file
65
pkg/cli/production/install/flags.go
Normal file
@ -0,0 +1,65 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Flags represents install command flags
|
||||
type Flags struct {
|
||||
VpsIP string
|
||||
Domain string
|
||||
Branch string
|
||||
NoPull bool
|
||||
Force bool
|
||||
DryRun bool
|
||||
SkipChecks bool
|
||||
JoinAddress string
|
||||
ClusterSecret string
|
||||
SwarmKey string
|
||||
PeersStr string
|
||||
|
||||
// IPFS/Cluster specific info for Peering configuration
|
||||
IPFSPeerID string
|
||||
IPFSAddrs string
|
||||
IPFSClusterPeerID string
|
||||
IPFSClusterAddrs string
|
||||
}
|
||||
|
||||
// ParseFlags parses install command flags
|
||||
func ParseFlags(args []string) (*Flags, error) {
|
||||
fs := flag.NewFlagSet("install", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
|
||||
flags := &Flags{}
|
||||
|
||||
fs.StringVar(&flags.VpsIP, "vps-ip", "", "Public IP of this VPS (required)")
|
||||
fs.StringVar(&flags.Domain, "domain", "", "Domain name for HTTPS (optional, e.g. gateway.example.com)")
|
||||
fs.StringVar(&flags.Branch, "branch", "main", "Git branch to use (main or nightly)")
|
||||
fs.BoolVar(&flags.NoPull, "no-pull", false, "Skip git clone/pull, use existing repository in /home/debros/src")
|
||||
fs.BoolVar(&flags.Force, "force", false, "Force reconfiguration even if already installed")
|
||||
fs.BoolVar(&flags.DryRun, "dry-run", false, "Show what would be done without making changes")
|
||||
fs.BoolVar(&flags.SkipChecks, "skip-checks", false, "Skip minimum resource checks (RAM/CPU)")
|
||||
|
||||
// Cluster join flags
|
||||
fs.StringVar(&flags.JoinAddress, "join", "", "Join an existing cluster (e.g. 1.2.3.4:7001)")
|
||||
fs.StringVar(&flags.ClusterSecret, "cluster-secret", "", "Cluster secret for IPFS Cluster (required if joining)")
|
||||
fs.StringVar(&flags.SwarmKey, "swarm-key", "", "IPFS Swarm key (required if joining)")
|
||||
fs.StringVar(&flags.PeersStr, "peers", "", "Comma-separated list of bootstrap peer multiaddrs")
|
||||
|
||||
// IPFS/Cluster specific info for Peering configuration
|
||||
fs.StringVar(&flags.IPFSPeerID, "ipfs-peer", "", "Peer ID of existing IPFS node to peer with")
|
||||
fs.StringVar(&flags.IPFSAddrs, "ipfs-addrs", "", "Comma-separated multiaddrs of existing IPFS node")
|
||||
fs.StringVar(&flags.IPFSClusterPeerID, "ipfs-cluster-peer", "", "Peer ID of existing IPFS Cluster node")
|
||||
fs.StringVar(&flags.IPFSClusterAddrs, "ipfs-cluster-addrs", "", "Comma-separated multiaddrs of existing IPFS Cluster node")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse flags: %w", err)
|
||||
}
|
||||
|
||||
return flags, nil
|
||||
}
|
||||
192
pkg/cli/production/install/orchestrator.go
Normal file
192
pkg/cli/production/install/orchestrator.go
Normal file
@ -0,0 +1,192 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/utils"
|
||||
"github.com/DeBrosOfficial/network/pkg/environments/production"
|
||||
)
|
||||
|
||||
// Orchestrator manages the install process
|
||||
type Orchestrator struct {
|
||||
oramaHome string
|
||||
oramaDir string
|
||||
setup *production.ProductionSetup
|
||||
flags *Flags
|
||||
validator *Validator
|
||||
peers []string
|
||||
}
|
||||
|
||||
// NewOrchestrator creates a new install orchestrator
|
||||
func NewOrchestrator(flags *Flags) (*Orchestrator, error) {
|
||||
oramaHome := "/home/debros"
|
||||
oramaDir := oramaHome + "/.orama"
|
||||
|
||||
// Normalize peers
|
||||
peers, err := utils.NormalizePeers(flags.PeersStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid peers: %w", err)
|
||||
}
|
||||
|
||||
setup := production.NewProductionSetup(oramaHome, os.Stdout, flags.Force, flags.Branch, flags.NoPull, flags.SkipChecks)
|
||||
validator := NewValidator(flags, oramaDir)
|
||||
|
||||
return &Orchestrator{
|
||||
oramaHome: oramaHome,
|
||||
oramaDir: oramaDir,
|
||||
setup: setup,
|
||||
flags: flags,
|
||||
validator: validator,
|
||||
peers: peers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute runs the installation process
|
||||
func (o *Orchestrator) Execute() error {
|
||||
fmt.Printf("🚀 Starting production installation...\n\n")
|
||||
|
||||
// Inform user if skipping git pull
|
||||
if o.flags.NoPull {
|
||||
fmt.Printf(" ⚠️ --no-pull flag enabled: Skipping git clone/pull\n")
|
||||
fmt.Printf(" Using existing repository at /home/debros/src\n")
|
||||
}
|
||||
|
||||
// Validate DNS if domain is provided
|
||||
o.validator.ValidateDNS()
|
||||
|
||||
// Dry-run mode: show what would be done and exit
|
||||
if o.flags.DryRun {
|
||||
utils.ShowDryRunSummary(o.flags.VpsIP, o.flags.Domain, o.flags.Branch, o.peers, o.flags.JoinAddress, o.validator.IsFirstNode(), o.oramaDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save secrets before installation
|
||||
if err := o.validator.SaveSecrets(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save branch preference for future upgrades
|
||||
if err := production.SaveBranchPreference(o.oramaDir, o.flags.Branch); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Warning: Failed to save branch preference: %v\n", err)
|
||||
}
|
||||
|
||||
// Phase 1: Check prerequisites
|
||||
fmt.Printf("\n📋 Phase 1: Checking prerequisites...\n")
|
||||
if err := o.setup.Phase1CheckPrerequisites(); err != nil {
|
||||
return fmt.Errorf("prerequisites check failed: %w", err)
|
||||
}
|
||||
|
||||
// Phase 2: Provision environment
|
||||
fmt.Printf("\n🛠️ Phase 2: Provisioning environment...\n")
|
||||
if err := o.setup.Phase2ProvisionEnvironment(); err != nil {
|
||||
return fmt.Errorf("environment provisioning failed: %w", err)
|
||||
}
|
||||
|
||||
// Phase 2b: Install binaries
|
||||
fmt.Printf("\nPhase 2b: Installing binaries...\n")
|
||||
if err := o.setup.Phase2bInstallBinaries(); err != nil {
|
||||
return fmt.Errorf("binary installation failed: %w", err)
|
||||
}
|
||||
|
||||
// Phase 3: Generate secrets FIRST (before service initialization)
|
||||
fmt.Printf("\n🔐 Phase 3: Generating secrets...\n")
|
||||
if err := o.setup.Phase3GenerateSecrets(); err != nil {
|
||||
return fmt.Errorf("secret generation failed: %w", err)
|
||||
}
|
||||
|
||||
// Phase 4: Generate configs (BEFORE service initialization)
|
||||
fmt.Printf("\n⚙️ Phase 4: Generating configurations...\n")
|
||||
enableHTTPS := o.flags.Domain != ""
|
||||
if err := o.setup.Phase4GenerateConfigs(o.peers, o.flags.VpsIP, enableHTTPS, o.flags.Domain, o.flags.JoinAddress); err != nil {
|
||||
return fmt.Errorf("configuration generation failed: %w", err)
|
||||
}
|
||||
|
||||
// Validate generated configuration
|
||||
if err := o.validator.ValidateGeneratedConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Phase 2c: Initialize services (after config is in place)
|
||||
fmt.Printf("\nPhase 2c: Initializing services...\n")
|
||||
ipfsPeerInfo := o.buildIPFSPeerInfo()
|
||||
ipfsClusterPeerInfo := o.buildIPFSClusterPeerInfo()
|
||||
|
||||
if err := o.setup.Phase2cInitializeServices(o.peers, o.flags.VpsIP, ipfsPeerInfo, ipfsClusterPeerInfo); err != nil {
|
||||
return fmt.Errorf("service initialization failed: %w", err)
|
||||
}
|
||||
|
||||
// Phase 5: Create systemd services
|
||||
fmt.Printf("\n🔧 Phase 5: Creating systemd services...\n")
|
||||
if err := o.setup.Phase5CreateSystemdServices(enableHTTPS); err != nil {
|
||||
return fmt.Errorf("service creation failed: %w", err)
|
||||
}
|
||||
|
||||
// Log completion with actual peer ID
|
||||
o.setup.LogSetupComplete(o.setup.NodePeerID)
|
||||
fmt.Printf("✅ Production installation complete!\n\n")
|
||||
|
||||
// For first node, print important secrets and identifiers
|
||||
if o.validator.IsFirstNode() {
|
||||
o.printFirstNodeSecrets()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Orchestrator) buildIPFSPeerInfo() *production.IPFSPeerInfo {
|
||||
if o.flags.IPFSPeerID != "" {
|
||||
var addrs []string
|
||||
if o.flags.IPFSAddrs != "" {
|
||||
addrs = strings.Split(o.flags.IPFSAddrs, ",")
|
||||
}
|
||||
return &production.IPFSPeerInfo{
|
||||
PeerID: o.flags.IPFSPeerID,
|
||||
Addrs: addrs,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Orchestrator) buildIPFSClusterPeerInfo() *production.IPFSClusterPeerInfo {
|
||||
if o.flags.IPFSClusterPeerID != "" {
|
||||
var addrs []string
|
||||
if o.flags.IPFSClusterAddrs != "" {
|
||||
addrs = strings.Split(o.flags.IPFSClusterAddrs, ",")
|
||||
}
|
||||
return &production.IPFSClusterPeerInfo{
|
||||
PeerID: o.flags.IPFSClusterPeerID,
|
||||
Addrs: addrs,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Orchestrator) printFirstNodeSecrets() {
|
||||
fmt.Printf("📋 Save these for joining future nodes:\n\n")
|
||||
|
||||
// Print cluster secret
|
||||
clusterSecretPath := filepath.Join(o.oramaDir, "secrets", "cluster-secret")
|
||||
if clusterSecretData, err := os.ReadFile(clusterSecretPath); err == nil {
|
||||
fmt.Printf(" Cluster Secret (--cluster-secret):\n")
|
||||
fmt.Printf(" %s\n\n", string(clusterSecretData))
|
||||
}
|
||||
|
||||
// Print swarm key
|
||||
swarmKeyPath := filepath.Join(o.oramaDir, "secrets", "swarm.key")
|
||||
if swarmKeyData, err := os.ReadFile(swarmKeyPath); err == nil {
|
||||
swarmKeyContent := strings.TrimSpace(string(swarmKeyData))
|
||||
lines := strings.Split(swarmKeyContent, "\n")
|
||||
if len(lines) >= 3 {
|
||||
// Extract just the hex part (last line)
|
||||
fmt.Printf(" IPFS Swarm Key (--swarm-key, last line only):\n")
|
||||
fmt.Printf(" %s\n\n", lines[len(lines)-1])
|
||||
}
|
||||
}
|
||||
|
||||
// Print peer ID
|
||||
fmt.Printf(" Node Peer ID:\n")
|
||||
fmt.Printf(" %s\n\n", o.setup.NodePeerID)
|
||||
}
|
||||
106
pkg/cli/production/install/validator.go
Normal file
106
pkg/cli/production/install/validator.go
Normal file
@ -0,0 +1,106 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/utils"
|
||||
)
|
||||
|
||||
// Validator validates install command inputs
|
||||
type Validator struct {
|
||||
flags *Flags
|
||||
oramaDir string
|
||||
isFirstNode bool
|
||||
}
|
||||
|
||||
// NewValidator creates a new validator
|
||||
func NewValidator(flags *Flags, oramaDir string) *Validator {
|
||||
return &Validator{
|
||||
flags: flags,
|
||||
oramaDir: oramaDir,
|
||||
isFirstNode: flags.JoinAddress == "",
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateFlags validates required flags
|
||||
func (v *Validator) ValidateFlags() error {
|
||||
if v.flags.VpsIP == "" && !v.flags.DryRun {
|
||||
return fmt.Errorf("--vps-ip is required for installation\nExample: dbn prod install --vps-ip 1.2.3.4")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRootPrivileges checks if running as root
|
||||
func (v *Validator) ValidateRootPrivileges() error {
|
||||
if os.Geteuid() != 0 && !v.flags.DryRun {
|
||||
return fmt.Errorf("production installation must be run as root (use sudo)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePorts validates port availability
|
||||
func (v *Validator) ValidatePorts() error {
|
||||
if err := utils.EnsurePortsAvailable("install", utils.DefaultPorts()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDNS validates DNS record if domain is provided
|
||||
func (v *Validator) ValidateDNS() {
|
||||
if v.flags.Domain != "" {
|
||||
fmt.Printf("\n🌐 Pre-flight DNS validation...\n")
|
||||
utils.ValidateDNSRecord(v.flags.Domain, v.flags.VpsIP)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGeneratedConfig validates generated configuration files
|
||||
func (v *Validator) ValidateGeneratedConfig() error {
|
||||
fmt.Printf(" Validating generated configuration...\n")
|
||||
if err := utils.ValidateGeneratedConfig(v.oramaDir); err != nil {
|
||||
return fmt.Errorf("configuration validation failed: %w", err)
|
||||
}
|
||||
fmt.Printf(" ✓ Configuration validated\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveSecrets saves cluster secret and swarm key to secrets directory
|
||||
func (v *Validator) SaveSecrets() error {
|
||||
// If cluster secret was provided, save it to secrets directory before setup
|
||||
if v.flags.ClusterSecret != "" {
|
||||
secretsDir := filepath.Join(v.oramaDir, "secrets")
|
||||
if err := os.MkdirAll(secretsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create secrets directory: %w", err)
|
||||
}
|
||||
secretPath := filepath.Join(secretsDir, "cluster-secret")
|
||||
if err := os.WriteFile(secretPath, []byte(v.flags.ClusterSecret), 0600); err != nil {
|
||||
return fmt.Errorf("failed to save cluster secret: %w", err)
|
||||
}
|
||||
fmt.Printf(" ✓ Cluster secret saved\n")
|
||||
}
|
||||
|
||||
// If swarm key was provided, save it to secrets directory in full format
|
||||
if v.flags.SwarmKey != "" {
|
||||
secretsDir := filepath.Join(v.oramaDir, "secrets")
|
||||
if err := os.MkdirAll(secretsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create secrets directory: %w", err)
|
||||
}
|
||||
// Convert 64-hex key to full swarm.key format
|
||||
swarmKeyContent := fmt.Sprintf("/key/swarm/psk/1.0.0/\n/base16/\n%s\n", strings.ToUpper(v.flags.SwarmKey))
|
||||
swarmKeyPath := filepath.Join(secretsDir, "swarm.key")
|
||||
if err := os.WriteFile(swarmKeyPath, []byte(swarmKeyContent), 0600); err != nil {
|
||||
return fmt.Errorf("failed to save swarm key: %w", err)
|
||||
}
|
||||
fmt.Printf(" ✓ Swarm key saved\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsFirstNode returns true if this is the first node in the cluster
|
||||
func (v *Validator) IsFirstNode() bool {
|
||||
return v.isFirstNode
|
||||
}
|
||||
67
pkg/cli/production/lifecycle/restart.go
Normal file
67
pkg/cli/production/lifecycle/restart.go
Normal file
@ -0,0 +1,67 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/utils"
|
||||
)
|
||||
|
||||
// HandleRestart restarts all production services
|
||||
func HandleRestart() {
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Fprintf(os.Stderr, "❌ Production commands must be run as root (use sudo)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Restarting all DeBros production services...\n")
|
||||
|
||||
services := utils.GetProductionServices()
|
||||
if len(services) == 0 {
|
||||
fmt.Printf(" ⚠️ No DeBros services found\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Stop all active services first
|
||||
fmt.Printf(" Stopping services...\n")
|
||||
for _, svc := range services {
|
||||
active, err := utils.IsServiceActive(svc)
|
||||
if err != nil {
|
||||
fmt.Printf(" ⚠️ Unable to check %s: %v\n", svc, err)
|
||||
continue
|
||||
}
|
||||
if !active {
|
||||
fmt.Printf(" ℹ️ %s was already stopped\n", svc)
|
||||
continue
|
||||
}
|
||||
if err := exec.Command("systemctl", "stop", svc).Run(); err != nil {
|
||||
fmt.Printf(" ⚠️ Failed to stop %s: %v\n", svc, err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Stopped %s\n", svc)
|
||||
}
|
||||
}
|
||||
|
||||
// Check port availability before restarting
|
||||
ports, err := utils.CollectPortsForServices(services, false)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := utils.EnsurePortsAvailable("prod restart", ports); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Start all services
|
||||
fmt.Printf(" Starting services...\n")
|
||||
for _, svc := range services {
|
||||
if err := exec.Command("systemctl", "start", svc).Run(); err != nil {
|
||||
fmt.Printf(" ⚠️ Failed to start %s: %v\n", svc, err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Started %s\n", svc)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n✅ All services restarted\n")
|
||||
}
|
||||
111
pkg/cli/production/lifecycle/start.go
Normal file
111
pkg/cli/production/lifecycle/start.go
Normal file
@ -0,0 +1,111 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/utils"
|
||||
)
|
||||
|
||||
// HandleStart starts all production services
|
||||
func HandleStart() {
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Fprintf(os.Stderr, "❌ Production commands must be run as root (use sudo)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Starting all DeBros production services...\n")
|
||||
|
||||
services := utils.GetProductionServices()
|
||||
if len(services) == 0 {
|
||||
fmt.Printf(" ⚠️ No DeBros services found\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Reset failed state for all services before starting
|
||||
// This helps with services that were previously in failed state
|
||||
resetArgs := []string{"reset-failed"}
|
||||
resetArgs = append(resetArgs, services...)
|
||||
exec.Command("systemctl", resetArgs...).Run()
|
||||
|
||||
// Check which services are inactive and need to be started
|
||||
inactive := make([]string, 0, len(services))
|
||||
for _, svc := range services {
|
||||
// Check if service is masked and unmask it
|
||||
masked, err := utils.IsServiceMasked(svc)
|
||||
if err == nil && masked {
|
||||
fmt.Printf(" ⚠️ %s is masked, unmasking...\n", svc)
|
||||
if err := exec.Command("systemctl", "unmask", svc).Run(); err != nil {
|
||||
fmt.Printf(" ⚠️ Failed to unmask %s: %v\n", svc, err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Unmasked %s\n", svc)
|
||||
}
|
||||
}
|
||||
|
||||
active, err := utils.IsServiceActive(svc)
|
||||
if err != nil {
|
||||
fmt.Printf(" ⚠️ Unable to check %s: %v\n", svc, err)
|
||||
continue
|
||||
}
|
||||
if active {
|
||||
fmt.Printf(" ℹ️ %s already running\n", svc)
|
||||
// Re-enable if disabled (in case it was stopped with 'dbn prod stop')
|
||||
enabled, err := utils.IsServiceEnabled(svc)
|
||||
if err == nil && !enabled {
|
||||
if err := exec.Command("systemctl", "enable", svc).Run(); err != nil {
|
||||
fmt.Printf(" ⚠️ Failed to re-enable %s: %v\n", svc, err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Re-enabled %s (will auto-start on boot)\n", svc)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
inactive = append(inactive, svc)
|
||||
}
|
||||
|
||||
if len(inactive) == 0 {
|
||||
fmt.Printf("\n✅ All services already running\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Check port availability for services we're about to start
|
||||
ports, err := utils.CollectPortsForServices(inactive, false)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := utils.EnsurePortsAvailable("prod start", ports); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Enable and start inactive services
|
||||
for _, svc := range inactive {
|
||||
// Re-enable the service first (in case it was disabled by 'dbn prod stop')
|
||||
enabled, err := utils.IsServiceEnabled(svc)
|
||||
if err == nil && !enabled {
|
||||
if err := exec.Command("systemctl", "enable", svc).Run(); err != nil {
|
||||
fmt.Printf(" ⚠️ Failed to enable %s: %v\n", svc, err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Enabled %s (will auto-start on boot)\n", svc)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the service
|
||||
if err := exec.Command("systemctl", "start", svc).Run(); err != nil {
|
||||
fmt.Printf(" ⚠️ Failed to start %s: %v\n", svc, err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Started %s\n", svc)
|
||||
}
|
||||
}
|
||||
|
||||
// Give services more time to fully initialize before verification
|
||||
// Some services may need more time to start up, especially if they're
|
||||
// waiting for dependencies or initializing databases
|
||||
fmt.Printf(" ⏳ Waiting for services to initialize...\n")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
fmt.Printf("\n✅ All services started\n")
|
||||
}
|
||||
112
pkg/cli/production/lifecycle/stop.go
Normal file
112
pkg/cli/production/lifecycle/stop.go
Normal file
@ -0,0 +1,112 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/utils"
|
||||
)
|
||||
|
||||
// HandleStop stops all production services
|
||||
func HandleStop() {
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Fprintf(os.Stderr, "❌ Production commands must be run as root (use sudo)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Stopping all DeBros production services...\n")
|
||||
|
||||
services := utils.GetProductionServices()
|
||||
if len(services) == 0 {
|
||||
fmt.Printf(" ⚠️ No DeBros services found\n")
|
||||
return
|
||||
}
|
||||
|
||||
// First, disable all services to prevent auto-restart
|
||||
disableArgs := []string{"disable"}
|
||||
disableArgs = append(disableArgs, services...)
|
||||
if err := exec.Command("systemctl", disableArgs...).Run(); err != nil {
|
||||
fmt.Printf(" ⚠️ Warning: Failed to disable some services: %v\n", err)
|
||||
}
|
||||
|
||||
// Stop all services at once using a single systemctl command
|
||||
// This is more efficient and ensures they all stop together
|
||||
stopArgs := []string{"stop"}
|
||||
stopArgs = append(stopArgs, services...)
|
||||
if err := exec.Command("systemctl", stopArgs...).Run(); err != nil {
|
||||
fmt.Printf(" ⚠️ Warning: Some services may have failed to stop: %v\n", err)
|
||||
// Continue anyway - we'll verify and handle individually below
|
||||
}
|
||||
|
||||
// Wait a moment for services to fully stop
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Reset failed state for any services that might be in failed state
|
||||
resetArgs := []string{"reset-failed"}
|
||||
resetArgs = append(resetArgs, services...)
|
||||
exec.Command("systemctl", resetArgs...).Run()
|
||||
|
||||
// Wait again after reset-failed
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Stop again to ensure they're stopped
|
||||
exec.Command("systemctl", stopArgs...).Run()
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
hadError := false
|
||||
for _, svc := range services {
|
||||
active, err := utils.IsServiceActive(svc)
|
||||
if err != nil {
|
||||
fmt.Printf(" ⚠️ Unable to check %s: %v\n", svc, err)
|
||||
hadError = true
|
||||
continue
|
||||
}
|
||||
if !active {
|
||||
fmt.Printf(" ✓ Stopped %s\n", svc)
|
||||
} else {
|
||||
// Service is still active, try stopping it individually
|
||||
fmt.Printf(" ⚠️ %s still active, attempting individual stop...\n", svc)
|
||||
if err := exec.Command("systemctl", "stop", svc).Run(); err != nil {
|
||||
fmt.Printf(" ❌ Failed to stop %s: %v\n", svc, err)
|
||||
hadError = true
|
||||
} else {
|
||||
// Wait and verify again
|
||||
time.Sleep(1 * time.Second)
|
||||
if stillActive, _ := utils.IsServiceActive(svc); stillActive {
|
||||
fmt.Printf(" ❌ %s restarted itself (Restart=always)\n", svc)
|
||||
hadError = true
|
||||
} else {
|
||||
fmt.Printf(" ✓ Stopped %s\n", svc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disable the service to prevent it from auto-starting on boot
|
||||
enabled, err := utils.IsServiceEnabled(svc)
|
||||
if err != nil {
|
||||
fmt.Printf(" ⚠️ Unable to check if %s is enabled: %v\n", svc, err)
|
||||
// Continue anyway - try to disable
|
||||
}
|
||||
if enabled {
|
||||
if err := exec.Command("systemctl", "disable", svc).Run(); err != nil {
|
||||
fmt.Printf(" ⚠️ Failed to disable %s: %v\n", svc, err)
|
||||
hadError = true
|
||||
} else {
|
||||
fmt.Printf(" ✓ Disabled %s (will not auto-start on boot)\n", svc)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" ℹ️ %s already disabled\n", svc)
|
||||
}
|
||||
}
|
||||
|
||||
if hadError {
|
||||
fmt.Fprintf(os.Stderr, "\n⚠️ Some services may still be restarting due to Restart=always\n")
|
||||
fmt.Fprintf(os.Stderr, " Check status with: systemctl list-units 'debros-*'\n")
|
||||
fmt.Fprintf(os.Stderr, " If services are still restarting, they may need manual intervention\n")
|
||||
} else {
|
||||
fmt.Printf("\n✅ All services stopped and disabled (will not auto-start on boot)\n")
|
||||
fmt.Printf(" Use 'dbn prod start' to start and re-enable services\n")
|
||||
}
|
||||
}
|
||||
104
pkg/cli/production/logs/command.go
Normal file
104
pkg/cli/production/logs/command.go
Normal file
@ -0,0 +1,104 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/utils"
|
||||
)
|
||||
|
||||
// Handle executes the logs command
|
||||
func Handle(args []string) {
|
||||
if len(args) == 0 {
|
||||
showUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
serviceAlias := args[0]
|
||||
follow := false
|
||||
if len(args) > 1 && (args[1] == "--follow" || args[1] == "-f") {
|
||||
follow = true
|
||||
}
|
||||
|
||||
// Resolve service alias to actual service names
|
||||
serviceNames, err := utils.ResolveServiceName(serviceAlias)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "\nAvailable service aliases: node, ipfs, cluster, gateway, olric\n")
|
||||
fmt.Fprintf(os.Stderr, "Or use full service name like: debros-node\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// If multiple services match, show all of them
|
||||
if len(serviceNames) > 1 {
|
||||
handleMultipleServices(serviceNames, serviceAlias, follow)
|
||||
return
|
||||
}
|
||||
|
||||
// Single service
|
||||
service := serviceNames[0]
|
||||
if follow {
|
||||
followServiceLogs(service)
|
||||
} else {
|
||||
showServiceLogs(service)
|
||||
}
|
||||
}
|
||||
|
||||
func showUsage() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: dbn prod logs <service> [--follow]\n")
|
||||
fmt.Fprintf(os.Stderr, "\nService aliases:\n")
|
||||
fmt.Fprintf(os.Stderr, " node, ipfs, cluster, gateway, olric\n")
|
||||
fmt.Fprintf(os.Stderr, "\nOr use full service name:\n")
|
||||
fmt.Fprintf(os.Stderr, " debros-node, debros-gateway, etc.\n")
|
||||
}
|
||||
|
||||
func handleMultipleServices(serviceNames []string, serviceAlias string, follow bool) {
|
||||
if follow {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Multiple services match alias %q:\n", serviceAlias)
|
||||
for _, svc := range serviceNames {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", svc)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\nShowing logs for all matching services...\n\n")
|
||||
|
||||
// Use journalctl with multiple units (build args correctly)
|
||||
args := []string{}
|
||||
for _, svc := range serviceNames {
|
||||
args = append(args, "-u", svc)
|
||||
}
|
||||
args = append(args, "-f")
|
||||
cmd := exec.Command("journalctl", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Run()
|
||||
} else {
|
||||
for i, svc := range serviceNames {
|
||||
if i > 0 {
|
||||
fmt.Print("\n" + strings.Repeat("=", 70) + "\n\n")
|
||||
}
|
||||
fmt.Printf("📋 Logs for %s:\n\n", svc)
|
||||
cmd := exec.Command("journalctl", "-u", svc, "-n", "50")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func followServiceLogs(service string) {
|
||||
fmt.Printf("Following logs for %s (press Ctrl+C to stop)...\n\n", service)
|
||||
cmd := exec.Command("journalctl", "-u", service, "-f")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Run()
|
||||
}
|
||||
|
||||
func showServiceLogs(service string) {
|
||||
cmd := exec.Command("journalctl", "-u", service, "-n", "50")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Run()
|
||||
}
|
||||
9
pkg/cli/production/logs/tailer.go
Normal file
9
pkg/cli/production/logs/tailer.go
Normal file
@ -0,0 +1,9 @@
|
||||
package logs
|
||||
|
||||
// This file contains log tailing utilities
|
||||
// Currently all tailing is done via journalctl in command.go
|
||||
// Future enhancements could include:
|
||||
// - Custom log parsing and filtering
|
||||
// - Log streaming from remote nodes
|
||||
// - Log aggregation across multiple services
|
||||
// - Advanced filtering and search capabilities
|
||||
156
pkg/cli/production/migrate/command.go
Normal file
156
pkg/cli/production/migrate/command.go
Normal file
@ -0,0 +1,156 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Handle executes the migrate command
|
||||
func Handle(args []string) {
|
||||
// Parse flags
|
||||
fs := flag.NewFlagSet("migrate", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
dryRun := fs.Bool("dry-run", false, "Show what would be migrated without making changes")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to parse flags: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if os.Geteuid() != 0 && !*dryRun {
|
||||
fmt.Fprintf(os.Stderr, "❌ Migration must be run as root (use sudo)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
oramaDir := "/home/debros/.orama"
|
||||
|
||||
fmt.Printf("🔄 Checking for installations to migrate...\n\n")
|
||||
|
||||
// Check for old-style installations
|
||||
validator := NewValidator(oramaDir)
|
||||
needsMigration := validator.CheckNeedsMigration()
|
||||
|
||||
if !needsMigration {
|
||||
fmt.Printf("\n✅ No migration needed - installation already uses unified structure\n")
|
||||
return
|
||||
}
|
||||
|
||||
if *dryRun {
|
||||
fmt.Printf("\n📋 Dry run - no changes made\n")
|
||||
fmt.Printf(" Run without --dry-run to perform migration\n")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n🔄 Starting migration...\n")
|
||||
|
||||
// Stop old services first
|
||||
stopOldServices()
|
||||
|
||||
// Migrate data directories
|
||||
migrateDataDirectories(oramaDir)
|
||||
|
||||
// Migrate config files
|
||||
migrateConfigFiles(oramaDir)
|
||||
|
||||
// Remove old services
|
||||
removeOldServices()
|
||||
|
||||
// Reload systemd
|
||||
exec.Command("systemctl", "daemon-reload").Run()
|
||||
|
||||
fmt.Printf("\n✅ Migration complete!\n")
|
||||
fmt.Printf(" Run 'sudo orama upgrade --restart' to regenerate services with new names\n\n")
|
||||
}
|
||||
|
||||
func stopOldServices() {
|
||||
oldServices := []string{
|
||||
"debros-ipfs",
|
||||
"debros-ipfs-cluster",
|
||||
"debros-node",
|
||||
}
|
||||
|
||||
fmt.Printf("\n Stopping old services...\n")
|
||||
for _, svc := range oldServices {
|
||||
if err := exec.Command("systemctl", "stop", svc).Run(); err == nil {
|
||||
fmt.Printf(" ✓ Stopped %s\n", svc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func migrateDataDirectories(oramaDir string) {
|
||||
oldDataDirs := []string{
|
||||
filepath.Join(oramaDir, "data", "node-1"),
|
||||
filepath.Join(oramaDir, "data", "node"),
|
||||
}
|
||||
newDataDir := filepath.Join(oramaDir, "data")
|
||||
|
||||
fmt.Printf("\n Migrating data directories...\n")
|
||||
|
||||
// Prefer node-1 data if it exists, otherwise use node data
|
||||
sourceDir := ""
|
||||
if _, err := os.Stat(filepath.Join(oramaDir, "data", "node-1")); err == nil {
|
||||
sourceDir = filepath.Join(oramaDir, "data", "node-1")
|
||||
} else if _, err := os.Stat(filepath.Join(oramaDir, "data", "node")); err == nil {
|
||||
sourceDir = filepath.Join(oramaDir, "data", "node")
|
||||
}
|
||||
|
||||
if sourceDir != "" {
|
||||
// Move contents to unified data directory
|
||||
entries, _ := os.ReadDir(sourceDir)
|
||||
for _, entry := range entries {
|
||||
src := filepath.Join(sourceDir, entry.Name())
|
||||
dst := filepath.Join(newDataDir, entry.Name())
|
||||
if _, err := os.Stat(dst); os.IsNotExist(err) {
|
||||
if err := os.Rename(src, dst); err == nil {
|
||||
fmt.Printf(" ✓ Moved %s → %s\n", src, dst)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old data directories
|
||||
for _, dir := range oldDataDirs {
|
||||
if err := os.RemoveAll(dir); err == nil {
|
||||
fmt.Printf(" ✓ Removed %s\n", dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func migrateConfigFiles(oramaDir string) {
|
||||
fmt.Printf("\n Migrating config files...\n")
|
||||
oldNodeConfig := filepath.Join(oramaDir, "configs", "bootstrap.yaml")
|
||||
newNodeConfig := filepath.Join(oramaDir, "configs", "node.yaml")
|
||||
|
||||
if _, err := os.Stat(oldNodeConfig); err == nil {
|
||||
if _, err := os.Stat(newNodeConfig); os.IsNotExist(err) {
|
||||
if err := os.Rename(oldNodeConfig, newNodeConfig); err == nil {
|
||||
fmt.Printf(" ✓ Renamed bootstrap.yaml → node.yaml\n")
|
||||
}
|
||||
} else {
|
||||
os.Remove(oldNodeConfig)
|
||||
fmt.Printf(" ✓ Removed old bootstrap.yaml (node.yaml already exists)\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeOldServices() {
|
||||
oldServices := []string{
|
||||
"debros-ipfs",
|
||||
"debros-ipfs-cluster",
|
||||
"debros-node",
|
||||
}
|
||||
|
||||
fmt.Printf("\n Removing old service files...\n")
|
||||
for _, svc := range oldServices {
|
||||
unitPath := filepath.Join("/etc/systemd/system", svc+".service")
|
||||
if err := os.Remove(unitPath); err == nil {
|
||||
fmt.Printf(" ✓ Removed %s\n", unitPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
64
pkg/cli/production/migrate/validator.go
Normal file
64
pkg/cli/production/migrate/validator.go
Normal file
@ -0,0 +1,64 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Validator checks if migration is needed
|
||||
type Validator struct {
|
||||
oramaDir string
|
||||
}
|
||||
|
||||
// NewValidator creates a new Validator
|
||||
func NewValidator(oramaDir string) *Validator {
|
||||
return &Validator{oramaDir: oramaDir}
|
||||
}
|
||||
|
||||
// CheckNeedsMigration checks if migration is needed
|
||||
func (v *Validator) CheckNeedsMigration() bool {
|
||||
oldDataDirs := []string{
|
||||
filepath.Join(v.oramaDir, "data", "node-1"),
|
||||
filepath.Join(v.oramaDir, "data", "node"),
|
||||
}
|
||||
|
||||
oldServices := []string{
|
||||
"debros-ipfs",
|
||||
"debros-ipfs-cluster",
|
||||
"debros-node",
|
||||
}
|
||||
|
||||
oldConfigs := []string{
|
||||
filepath.Join(v.oramaDir, "configs", "bootstrap.yaml"),
|
||||
}
|
||||
|
||||
var needsMigration bool
|
||||
|
||||
fmt.Printf("Checking data directories:\n")
|
||||
for _, dir := range oldDataDirs {
|
||||
if _, err := os.Stat(dir); err == nil {
|
||||
fmt.Printf(" ⚠️ Found old directory: %s\n", dir)
|
||||
needsMigration = true
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nChecking services:\n")
|
||||
for _, svc := range oldServices {
|
||||
unitPath := filepath.Join("/etc/systemd/system", svc+".service")
|
||||
if _, err := os.Stat(unitPath); err == nil {
|
||||
fmt.Printf(" ⚠️ Found old service: %s\n", svc)
|
||||
needsMigration = true
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nChecking configs:\n")
|
||||
for _, cfg := range oldConfigs {
|
||||
if _, err := os.Stat(cfg); err == nil {
|
||||
fmt.Printf(" ⚠️ Found old config: %s\n", cfg)
|
||||
needsMigration = true
|
||||
}
|
||||
}
|
||||
|
||||
return needsMigration
|
||||
}
|
||||
58
pkg/cli/production/status/command.go
Normal file
58
pkg/cli/production/status/command.go
Normal file
@ -0,0 +1,58 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/utils"
|
||||
)
|
||||
|
||||
// Handle executes the status command
|
||||
func Handle() {
|
||||
fmt.Printf("Production Environment Status\n\n")
|
||||
|
||||
// Unified service names (no bootstrap/node distinction)
|
||||
serviceNames := []string{
|
||||
"debros-ipfs",
|
||||
"debros-ipfs-cluster",
|
||||
// Note: RQLite is managed by node process, not as separate service
|
||||
"debros-olric",
|
||||
"debros-node",
|
||||
"debros-gateway",
|
||||
}
|
||||
|
||||
// Friendly descriptions
|
||||
descriptions := map[string]string{
|
||||
"debros-ipfs": "IPFS Daemon",
|
||||
"debros-ipfs-cluster": "IPFS Cluster",
|
||||
"debros-olric": "Olric Cache Server",
|
||||
"debros-node": "DeBros Node (includes RQLite)",
|
||||
"debros-gateway": "DeBros Gateway",
|
||||
}
|
||||
|
||||
fmt.Printf("Services:\n")
|
||||
found := false
|
||||
for _, svc := range serviceNames {
|
||||
active, _ := utils.IsServiceActive(svc)
|
||||
status := "❌ Inactive"
|
||||
if active {
|
||||
status = "✅ Active"
|
||||
found = true
|
||||
}
|
||||
fmt.Printf(" %s: %s\n", status, descriptions[svc])
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.Printf(" (No services found - installation may be incomplete)\n")
|
||||
}
|
||||
|
||||
fmt.Printf("\nDirectories:\n")
|
||||
oramaDir := "/home/debros/.orama"
|
||||
if _, err := os.Stat(oramaDir); err == nil {
|
||||
fmt.Printf(" ✅ %s exists\n", oramaDir)
|
||||
} else {
|
||||
fmt.Printf(" ❌ %s not found\n", oramaDir)
|
||||
}
|
||||
|
||||
fmt.Printf("\nView logs with: dbn prod logs <service>\n")
|
||||
}
|
||||
9
pkg/cli/production/status/formatter.go
Normal file
9
pkg/cli/production/status/formatter.go
Normal file
@ -0,0 +1,9 @@
|
||||
package status
|
||||
|
||||
// This file contains formatting utilities for status output
|
||||
// Currently all formatting is done inline in command.go
|
||||
// Future enhancements could include:
|
||||
// - JSON output format
|
||||
// - Table-based formatting
|
||||
// - Color-coded output
|
||||
// - More detailed service information
|
||||
53
pkg/cli/production/uninstall/command.go
Normal file
53
pkg/cli/production/uninstall/command.go
Normal file
@ -0,0 +1,53 @@
|
||||
package uninstall
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Handle executes the uninstall command
|
||||
func Handle() {
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Fprintf(os.Stderr, "❌ Production uninstall must be run as root (use sudo)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("⚠️ This will stop and remove all DeBros production services\n")
|
||||
fmt.Printf("⚠️ Configuration and data will be preserved in /home/debros/.orama\n\n")
|
||||
fmt.Printf("Continue? (yes/no): ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, _ := reader.ReadString('\n')
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
|
||||
if response != "yes" && response != "y" {
|
||||
fmt.Printf("Uninstall cancelled\n")
|
||||
return
|
||||
}
|
||||
|
||||
services := []string{
|
||||
"debros-gateway",
|
||||
"debros-node",
|
||||
"debros-olric",
|
||||
"debros-ipfs-cluster",
|
||||
"debros-ipfs",
|
||||
"debros-anyone-client",
|
||||
}
|
||||
|
||||
fmt.Printf("Stopping services...\n")
|
||||
for _, svc := range services {
|
||||
exec.Command("systemctl", "stop", svc).Run()
|
||||
exec.Command("systemctl", "disable", svc).Run()
|
||||
unitPath := filepath.Join("/etc/systemd/system", svc+".service")
|
||||
os.Remove(unitPath)
|
||||
}
|
||||
|
||||
exec.Command("systemctl", "daemon-reload").Run()
|
||||
fmt.Printf("✅ Services uninstalled\n")
|
||||
fmt.Printf(" Configuration and data preserved in /home/debros/.orama\n")
|
||||
fmt.Printf(" To remove all data: rm -rf /home/debros/.orama\n\n")
|
||||
}
|
||||
29
pkg/cli/production/upgrade/command.go
Normal file
29
pkg/cli/production/upgrade/command.go
Normal file
@ -0,0 +1,29 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Handle executes the upgrade command
|
||||
func Handle(args []string) {
|
||||
// Parse flags
|
||||
flags, err := ParseFlags(args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check root privileges
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Fprintf(os.Stderr, "❌ Production upgrade must be run as root (use sudo)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create orchestrator and execute upgrade
|
||||
orchestrator := NewOrchestrator(flags)
|
||||
if err := orchestrator.Execute(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
54
pkg/cli/production/upgrade/flags.go
Normal file
54
pkg/cli/production/upgrade/flags.go
Normal file
@ -0,0 +1,54 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Flags represents upgrade command flags
|
||||
type Flags struct {
|
||||
Force bool
|
||||
RestartServices bool
|
||||
NoPull bool
|
||||
Branch string
|
||||
}
|
||||
|
||||
// ParseFlags parses upgrade command flags
|
||||
func ParseFlags(args []string) (*Flags, error) {
|
||||
fs := flag.NewFlagSet("upgrade", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
|
||||
flags := &Flags{}
|
||||
|
||||
fs.BoolVar(&flags.Force, "force", false, "Reconfigure all settings")
|
||||
fs.BoolVar(&flags.RestartServices, "restart", false, "Automatically restart services after upgrade")
|
||||
fs.BoolVar(&flags.NoPull, "no-pull", false, "Skip git clone/pull, use existing /home/debros/src")
|
||||
fs.StringVar(&flags.Branch, "branch", "", "Git branch to use (main or nightly, uses saved preference if not specified)")
|
||||
|
||||
// Support legacy flags for backwards compatibility
|
||||
nightly := fs.Bool("nightly", false, "Use nightly branch (deprecated, use --branch nightly)")
|
||||
main := fs.Bool("main", false, "Use main branch (deprecated, use --branch main)")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse flags: %w", err)
|
||||
}
|
||||
|
||||
// Handle legacy flags
|
||||
if *nightly {
|
||||
flags.Branch = "nightly"
|
||||
}
|
||||
if *main {
|
||||
flags.Branch = "main"
|
||||
}
|
||||
|
||||
// Validate branch if provided
|
||||
if flags.Branch != "" && flags.Branch != "main" && flags.Branch != "nightly" {
|
||||
return nil, fmt.Errorf("invalid branch: %s (must be 'main' or 'nightly')", flags.Branch)
|
||||
}
|
||||
|
||||
return flags, nil
|
||||
}
|
||||
322
pkg/cli/production/upgrade/orchestrator.go
Normal file
322
pkg/cli/production/upgrade/orchestrator.go
Normal file
@ -0,0 +1,322 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/utils"
|
||||
"github.com/DeBrosOfficial/network/pkg/environments/production"
|
||||
)
|
||||
|
||||
// Orchestrator manages the upgrade process
|
||||
type Orchestrator struct {
|
||||
oramaHome string
|
||||
oramaDir string
|
||||
setup *production.ProductionSetup
|
||||
flags *Flags
|
||||
}
|
||||
|
||||
// NewOrchestrator creates a new upgrade orchestrator
|
||||
func NewOrchestrator(flags *Flags) *Orchestrator {
|
||||
oramaHome := "/home/debros"
|
||||
oramaDir := oramaHome + "/.orama"
|
||||
setup := production.NewProductionSetup(oramaHome, os.Stdout, flags.Force, flags.Branch, flags.NoPull, false)
|
||||
|
||||
return &Orchestrator{
|
||||
oramaHome: oramaHome,
|
||||
oramaDir: oramaDir,
|
||||
setup: setup,
|
||||
flags: flags,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute runs the upgrade process
|
||||
func (o *Orchestrator) Execute() error {
|
||||
fmt.Printf("🔄 Upgrading production installation...\n")
|
||||
fmt.Printf(" This will preserve existing configurations and data\n")
|
||||
fmt.Printf(" Configurations will be updated to latest format\n\n")
|
||||
|
||||
// Log if --no-pull is enabled
|
||||
if o.flags.NoPull {
|
||||
fmt.Printf(" ⚠️ --no-pull flag enabled: Skipping git clone/pull\n")
|
||||
fmt.Printf(" Using existing repository at %s/src\n", o.oramaHome)
|
||||
}
|
||||
|
||||
// Handle branch preferences
|
||||
if err := o.handleBranchPreferences(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Phase 1: Check prerequisites
|
||||
fmt.Printf("\n📋 Phase 1: Checking prerequisites...\n")
|
||||
if err := o.setup.Phase1CheckPrerequisites(); err != nil {
|
||||
return fmt.Errorf("prerequisites check failed: %w", err)
|
||||
}
|
||||
|
||||
// Phase 2: Provision environment
|
||||
fmt.Printf("\n🛠️ Phase 2: Provisioning environment...\n")
|
||||
if err := o.setup.Phase2ProvisionEnvironment(); err != nil {
|
||||
return fmt.Errorf("environment provisioning failed: %w", err)
|
||||
}
|
||||
|
||||
// Stop services before upgrading binaries
|
||||
if o.setup.IsUpdate() {
|
||||
if err := o.stopServices(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Check port availability after stopping services
|
||||
if err := utils.EnsurePortsAvailable("prod upgrade", utils.DefaultPorts()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Phase 2b: Install/update binaries
|
||||
fmt.Printf("\nPhase 2b: Installing/updating binaries...\n")
|
||||
if err := o.setup.Phase2bInstallBinaries(); err != nil {
|
||||
return fmt.Errorf("binary installation failed: %w", err)
|
||||
}
|
||||
|
||||
// Detect existing installation
|
||||
if o.setup.IsUpdate() {
|
||||
fmt.Printf(" Detected existing installation\n")
|
||||
} else {
|
||||
fmt.Printf(" ⚠️ No existing installation detected, treating as fresh install\n")
|
||||
fmt.Printf(" Use 'orama install' for fresh installation\n")
|
||||
}
|
||||
|
||||
// Phase 3: Ensure secrets exist
|
||||
fmt.Printf("\n🔐 Phase 3: Ensuring secrets...\n")
|
||||
if err := o.setup.Phase3GenerateSecrets(); err != nil {
|
||||
return fmt.Errorf("secret generation failed: %w", err)
|
||||
}
|
||||
|
||||
// Phase 4: Regenerate configs
|
||||
if err := o.regenerateConfigs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Phase 2c: Ensure services are properly initialized
|
||||
fmt.Printf("\nPhase 2c: Ensuring services are properly initialized...\n")
|
||||
peers := o.extractPeers()
|
||||
vpsIP, _ := o.extractNetworkConfig()
|
||||
if err := o.setup.Phase2cInitializeServices(peers, vpsIP, nil, nil); err != nil {
|
||||
return fmt.Errorf("service initialization failed: %w", err)
|
||||
}
|
||||
|
||||
// Phase 5: Update systemd services
|
||||
fmt.Printf("\n🔧 Phase 5: Updating systemd services...\n")
|
||||
enableHTTPS, _ := o.extractGatewayConfig()
|
||||
if err := o.setup.Phase5CreateSystemdServices(enableHTTPS); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Service update warning: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n✅ Upgrade complete!\n")
|
||||
|
||||
// Restart services if requested
|
||||
if o.flags.RestartServices {
|
||||
return o.restartServices()
|
||||
}
|
||||
|
||||
fmt.Printf(" To apply changes, restart services:\n")
|
||||
fmt.Printf(" sudo systemctl daemon-reload\n")
|
||||
fmt.Printf(" sudo systemctl restart debros-*\n")
|
||||
fmt.Printf("\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Orchestrator) handleBranchPreferences() error {
|
||||
// If branch was explicitly provided, save it for future upgrades
|
||||
if o.flags.Branch != "" {
|
||||
if err := production.SaveBranchPreference(o.oramaDir, o.flags.Branch); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Warning: Failed to save branch preference: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Using branch: %s (saved for future upgrades)\n", o.flags.Branch)
|
||||
}
|
||||
} else {
|
||||
// Show which branch is being used (read from saved preference)
|
||||
currentBranch := production.ReadBranchPreference(o.oramaDir)
|
||||
fmt.Printf(" Using branch: %s (from saved preference)\n", currentBranch)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Orchestrator) stopServices() error {
|
||||
fmt.Printf("\n⏹️ Stopping services before upgrade...\n")
|
||||
serviceController := production.NewSystemdController()
|
||||
services := []string{
|
||||
"debros-gateway.service",
|
||||
"debros-node.service",
|
||||
"debros-ipfs-cluster.service",
|
||||
"debros-ipfs.service",
|
||||
// Note: RQLite is managed by node process, not as separate service
|
||||
"debros-olric.service",
|
||||
}
|
||||
for _, svc := range services {
|
||||
unitPath := filepath.Join("/etc/systemd/system", svc)
|
||||
if _, err := os.Stat(unitPath); err == nil {
|
||||
if err := serviceController.StopService(svc); err != nil {
|
||||
fmt.Printf(" ⚠️ Warning: Failed to stop %s: %v\n", svc, err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Stopped %s\n", svc)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Give services time to shut down gracefully
|
||||
time.Sleep(2 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Orchestrator) extractPeers() []string {
|
||||
nodeConfigPath := filepath.Join(o.oramaDir, "configs", "node.yaml")
|
||||
var peers []string
|
||||
if data, err := os.ReadFile(nodeConfigPath); err == nil {
|
||||
configStr := string(data)
|
||||
inPeersList := false
|
||||
for _, line := range strings.Split(configStr, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "bootstrap_peers:") || strings.HasPrefix(trimmed, "peers:") {
|
||||
inPeersList = true
|
||||
continue
|
||||
}
|
||||
if inPeersList {
|
||||
if strings.HasPrefix(trimmed, "-") {
|
||||
// Extract multiaddr after the dash
|
||||
parts := strings.SplitN(trimmed, "-", 2)
|
||||
if len(parts) > 1 {
|
||||
peer := strings.TrimSpace(parts[1])
|
||||
peer = strings.Trim(peer, "\"'")
|
||||
if peer != "" && strings.HasPrefix(peer, "/") {
|
||||
peers = append(peers, peer)
|
||||
}
|
||||
}
|
||||
} else if trimmed == "" || !strings.HasPrefix(trimmed, "-") {
|
||||
// End of peers list
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return peers
|
||||
}
|
||||
|
||||
func (o *Orchestrator) extractNetworkConfig() (vpsIP, joinAddress string) {
|
||||
nodeConfigPath := filepath.Join(o.oramaDir, "configs", "node.yaml")
|
||||
if data, err := os.ReadFile(nodeConfigPath); err == nil {
|
||||
configStr := string(data)
|
||||
for _, line := range strings.Split(configStr, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
// Try to extract VPS IP from http_adv_address or raft_adv_address
|
||||
if vpsIP == "" && (strings.HasPrefix(trimmed, "http_adv_address:") || strings.HasPrefix(trimmed, "raft_adv_address:")) {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
addr := strings.TrimSpace(parts[1])
|
||||
addr = strings.Trim(addr, "\"'")
|
||||
if addr != "" && addr != "null" && addr != "localhost:5001" && addr != "localhost:7001" {
|
||||
// Extract IP from address (format: "IP:PORT" or "[IPv6]:PORT")
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil && host != "" && host != "localhost" {
|
||||
vpsIP = host
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Extract join address
|
||||
if strings.HasPrefix(trimmed, "rqlite_join_address:") {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
joinAddress = strings.TrimSpace(parts[1])
|
||||
joinAddress = strings.Trim(joinAddress, "\"'")
|
||||
if joinAddress == "null" || joinAddress == "" {
|
||||
joinAddress = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return vpsIP, joinAddress
|
||||
}
|
||||
|
||||
func (o *Orchestrator) extractGatewayConfig() (enableHTTPS bool, domain string) {
|
||||
gatewayConfigPath := filepath.Join(o.oramaDir, "configs", "gateway.yaml")
|
||||
if data, err := os.ReadFile(gatewayConfigPath); err == nil {
|
||||
configStr := string(data)
|
||||
if strings.Contains(configStr, "domain:") {
|
||||
for _, line := range strings.Split(configStr, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "domain:") {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
domain = strings.TrimSpace(parts[1])
|
||||
if domain != "" && domain != "\"\"" && domain != "''" && domain != "null" {
|
||||
domain = strings.Trim(domain, "\"'")
|
||||
enableHTTPS = true
|
||||
} else {
|
||||
domain = ""
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return enableHTTPS, domain
|
||||
}
|
||||
|
||||
func (o *Orchestrator) regenerateConfigs() error {
|
||||
peers := o.extractPeers()
|
||||
vpsIP, joinAddress := o.extractNetworkConfig()
|
||||
enableHTTPS, domain := o.extractGatewayConfig()
|
||||
|
||||
fmt.Printf(" Preserving existing configuration:\n")
|
||||
if len(peers) > 0 {
|
||||
fmt.Printf(" - Peers: %d peer(s) preserved\n", len(peers))
|
||||
}
|
||||
if vpsIP != "" {
|
||||
fmt.Printf(" - VPS IP: %s\n", vpsIP)
|
||||
}
|
||||
if domain != "" {
|
||||
fmt.Printf(" - Domain: %s\n", domain)
|
||||
}
|
||||
if joinAddress != "" {
|
||||
fmt.Printf(" - Join address: %s\n", joinAddress)
|
||||
}
|
||||
|
||||
// Phase 4: Generate configs
|
||||
if err := o.setup.Phase4GenerateConfigs(peers, vpsIP, enableHTTPS, domain, joinAddress); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Config generation warning: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, " Existing configs preserved\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Orchestrator) restartServices() error {
|
||||
fmt.Printf(" Restarting services...\n")
|
||||
// Reload systemd daemon
|
||||
if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, " ⚠️ Warning: Failed to reload systemd daemon: %v\n", err)
|
||||
}
|
||||
|
||||
// Restart services to apply changes - use getProductionServices to only restart existing services
|
||||
services := utils.GetProductionServices()
|
||||
if len(services) == 0 {
|
||||
fmt.Printf(" ⚠️ No services found to restart\n")
|
||||
} else {
|
||||
for _, svc := range services {
|
||||
if err := exec.Command("systemctl", "restart", svc).Run(); err != nil {
|
||||
fmt.Printf(" ⚠️ Failed to restart %s: %v\n", svc, err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Restarted %s\n", svc)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" ✓ All services restarted\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
10
pkg/cli/production_commands.go
Normal file
10
pkg/cli/production_commands.go
Normal file
@ -0,0 +1,10 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/production"
|
||||
)
|
||||
|
||||
// HandleProdCommand handles production environment commands
|
||||
func HandleProdCommand(args []string) {
|
||||
production.HandleCommand(args)
|
||||
}
|
||||
97
pkg/cli/utils/install.go
Normal file
97
pkg/cli/utils/install.go
Normal file
@ -0,0 +1,97 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IPFSPeerInfo holds IPFS peer information for configuring Peering.Peers
|
||||
type IPFSPeerInfo struct {
|
||||
PeerID string
|
||||
Addrs []string
|
||||
}
|
||||
|
||||
// IPFSClusterPeerInfo contains IPFS Cluster peer information for cluster discovery
|
||||
type IPFSClusterPeerInfo struct {
|
||||
PeerID string
|
||||
Addrs []string
|
||||
}
|
||||
|
||||
// ShowDryRunSummary displays what would be done during installation without making changes
|
||||
func ShowDryRunSummary(vpsIP, domain, branch string, peers []string, joinAddress string, isFirstNode bool, oramaDir string) {
|
||||
fmt.Print("\n" + strings.Repeat("=", 70) + "\n")
|
||||
fmt.Printf("DRY RUN - No changes will be made\n")
|
||||
fmt.Print(strings.Repeat("=", 70) + "\n\n")
|
||||
|
||||
fmt.Printf("📋 Installation Summary:\n")
|
||||
fmt.Printf(" VPS IP: %s\n", vpsIP)
|
||||
fmt.Printf(" Domain: %s\n", domain)
|
||||
fmt.Printf(" Branch: %s\n", branch)
|
||||
if isFirstNode {
|
||||
fmt.Printf(" Node Type: First node (creates new cluster)\n")
|
||||
} else {
|
||||
fmt.Printf(" Node Type: Joining existing cluster\n")
|
||||
if joinAddress != "" {
|
||||
fmt.Printf(" Join Address: %s\n", joinAddress)
|
||||
}
|
||||
if len(peers) > 0 {
|
||||
fmt.Printf(" Peers: %d peer(s)\n", len(peers))
|
||||
for _, peer := range peers {
|
||||
fmt.Printf(" - %s\n", peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n📁 Directories that would be created:\n")
|
||||
fmt.Printf(" %s/configs/\n", oramaDir)
|
||||
fmt.Printf(" %s/secrets/\n", oramaDir)
|
||||
fmt.Printf(" %s/data/ipfs/repo/\n", oramaDir)
|
||||
fmt.Printf(" %s/data/ipfs-cluster/\n", oramaDir)
|
||||
fmt.Printf(" %s/data/rqlite/\n", oramaDir)
|
||||
fmt.Printf(" %s/logs/\n", oramaDir)
|
||||
fmt.Printf(" %s/tls-cache/\n", oramaDir)
|
||||
|
||||
fmt.Printf("\n🔧 Binaries that would be installed:\n")
|
||||
fmt.Printf(" - Go (if not present)\n")
|
||||
fmt.Printf(" - RQLite 8.43.0\n")
|
||||
fmt.Printf(" - IPFS/Kubo 0.38.2\n")
|
||||
fmt.Printf(" - IPFS Cluster (latest)\n")
|
||||
fmt.Printf(" - Olric 0.7.0\n")
|
||||
fmt.Printf(" - anyone-client (npm)\n")
|
||||
fmt.Printf(" - DeBros binaries (built from %s branch)\n", branch)
|
||||
|
||||
fmt.Printf("\n🔐 Secrets that would be generated:\n")
|
||||
fmt.Printf(" - Cluster secret (64-hex)\n")
|
||||
fmt.Printf(" - IPFS swarm key\n")
|
||||
fmt.Printf(" - Node identity (Ed25519 keypair)\n")
|
||||
|
||||
fmt.Printf("\n📝 Configuration files that would be created:\n")
|
||||
fmt.Printf(" - %s/configs/node.yaml\n", oramaDir)
|
||||
fmt.Printf(" - %s/configs/olric/config.yaml\n", oramaDir)
|
||||
|
||||
fmt.Printf("\n⚙️ Systemd services that would be created:\n")
|
||||
fmt.Printf(" - debros-ipfs.service\n")
|
||||
fmt.Printf(" - debros-ipfs-cluster.service\n")
|
||||
fmt.Printf(" - debros-olric.service\n")
|
||||
fmt.Printf(" - debros-node.service (includes embedded gateway + RQLite)\n")
|
||||
fmt.Printf(" - debros-anyone-client.service\n")
|
||||
|
||||
fmt.Printf("\n🌐 Ports that would be used:\n")
|
||||
fmt.Printf(" External (must be open in firewall):\n")
|
||||
fmt.Printf(" - 80 (HTTP for ACME/Let's Encrypt)\n")
|
||||
fmt.Printf(" - 443 (HTTPS gateway)\n")
|
||||
fmt.Printf(" - 4101 (IPFS swarm)\n")
|
||||
fmt.Printf(" - 7001 (RQLite Raft)\n")
|
||||
fmt.Printf(" Internal (localhost only):\n")
|
||||
fmt.Printf(" - 4501 (IPFS API)\n")
|
||||
fmt.Printf(" - 5001 (RQLite HTTP)\n")
|
||||
fmt.Printf(" - 6001 (Unified gateway)\n")
|
||||
fmt.Printf(" - 8080 (IPFS gateway)\n")
|
||||
fmt.Printf(" - 9050 (Anyone SOCKS5)\n")
|
||||
fmt.Printf(" - 9094 (IPFS Cluster API)\n")
|
||||
fmt.Printf(" - 3320/3322 (Olric)\n")
|
||||
|
||||
fmt.Print("\n" + strings.Repeat("=", 70) + "\n")
|
||||
fmt.Printf("To proceed with installation, run without --dry-run\n")
|
||||
fmt.Print(strings.Repeat("=", 70) + "\n\n")
|
||||
}
|
||||
217
pkg/cli/utils/systemd.go
Normal file
217
pkg/cli/utils/systemd.go
Normal file
@ -0,0 +1,217 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var ErrServiceNotFound = errors.New("service not found")
|
||||
|
||||
// PortSpec defines a port and its name for checking availability
|
||||
type PortSpec struct {
|
||||
Name string
|
||||
Port int
|
||||
}
|
||||
|
||||
var ServicePorts = map[string][]PortSpec{
|
||||
"debros-gateway": {
|
||||
{Name: "Gateway API", Port: 6001},
|
||||
},
|
||||
"debros-olric": {
|
||||
{Name: "Olric HTTP", Port: 3320},
|
||||
{Name: "Olric Memberlist", Port: 3322},
|
||||
},
|
||||
"debros-node": {
|
||||
{Name: "RQLite HTTP", Port: 5001},
|
||||
{Name: "RQLite Raft", Port: 7001},
|
||||
},
|
||||
"debros-ipfs": {
|
||||
{Name: "IPFS API", Port: 4501},
|
||||
{Name: "IPFS Gateway", Port: 8080},
|
||||
{Name: "IPFS Swarm", Port: 4101},
|
||||
},
|
||||
"debros-ipfs-cluster": {
|
||||
{Name: "IPFS Cluster API", Port: 9094},
|
||||
},
|
||||
}
|
||||
|
||||
// DefaultPorts is used for fresh installs/upgrades before unit files exist.
|
||||
func DefaultPorts() []PortSpec {
|
||||
return []PortSpec{
|
||||
{Name: "IPFS Swarm", Port: 4001},
|
||||
{Name: "IPFS API", Port: 4501},
|
||||
{Name: "IPFS Gateway", Port: 8080},
|
||||
{Name: "Gateway API", Port: 6001},
|
||||
{Name: "RQLite HTTP", Port: 5001},
|
||||
{Name: "RQLite Raft", Port: 7001},
|
||||
{Name: "IPFS Cluster API", Port: 9094},
|
||||
{Name: "Olric HTTP", Port: 3320},
|
||||
{Name: "Olric Memberlist", Port: 3322},
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveServiceName resolves service aliases to actual systemd service names
|
||||
func ResolveServiceName(alias string) ([]string, error) {
|
||||
// Service alias mapping (unified - no bootstrap/node distinction)
|
||||
aliases := map[string][]string{
|
||||
"node": {"debros-node"},
|
||||
"ipfs": {"debros-ipfs"},
|
||||
"cluster": {"debros-ipfs-cluster"},
|
||||
"ipfs-cluster": {"debros-ipfs-cluster"},
|
||||
"gateway": {"debros-gateway"},
|
||||
"olric": {"debros-olric"},
|
||||
"rqlite": {"debros-node"}, // RQLite logs are in node logs
|
||||
}
|
||||
|
||||
// Check if it's an alias
|
||||
if serviceNames, ok := aliases[strings.ToLower(alias)]; ok {
|
||||
// Filter to only existing services
|
||||
var existing []string
|
||||
for _, svc := range serviceNames {
|
||||
unitPath := filepath.Join("/etc/systemd/system", svc+".service")
|
||||
if _, err := os.Stat(unitPath); err == nil {
|
||||
existing = append(existing, svc)
|
||||
}
|
||||
}
|
||||
if len(existing) == 0 {
|
||||
return nil, fmt.Errorf("no services found for alias %q", alias)
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
// Check if it's already a full service name
|
||||
unitPath := filepath.Join("/etc/systemd/system", alias+".service")
|
||||
if _, err := os.Stat(unitPath); err == nil {
|
||||
return []string{alias}, nil
|
||||
}
|
||||
|
||||
// Try without .service suffix
|
||||
if !strings.HasSuffix(alias, ".service") {
|
||||
unitPath = filepath.Join("/etc/systemd/system", alias+".service")
|
||||
if _, err := os.Stat(unitPath); err == nil {
|
||||
return []string{alias}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("service %q not found. Use: node, ipfs, cluster, gateway, olric, or full service name", alias)
|
||||
}
|
||||
|
||||
// IsServiceActive checks if a systemd service is currently active (running)
|
||||
func IsServiceActive(service string) (bool, error) {
|
||||
cmd := exec.Command("systemctl", "is-active", "--quiet", service)
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
switch exitErr.ExitCode() {
|
||||
case 3:
|
||||
return false, nil
|
||||
case 4:
|
||||
return false, ErrServiceNotFound
|
||||
}
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// IsServiceEnabled checks if a systemd service is enabled to start on boot
|
||||
func IsServiceEnabled(service string) (bool, error) {
|
||||
cmd := exec.Command("systemctl", "is-enabled", "--quiet", service)
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
switch exitErr.ExitCode() {
|
||||
case 1:
|
||||
return false, nil // Service is disabled
|
||||
case 4:
|
||||
return false, ErrServiceNotFound
|
||||
}
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// IsServiceMasked checks if a systemd service is masked
|
||||
func IsServiceMasked(service string) (bool, error) {
|
||||
cmd := exec.Command("systemctl", "is-enabled", service)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
outputStr := string(output)
|
||||
if strings.Contains(outputStr, "masked") {
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// GetProductionServices returns a list of all DeBros production service names that exist
|
||||
func GetProductionServices() []string {
|
||||
// Unified service names (no bootstrap/node distinction)
|
||||
allServices := []string{
|
||||
"debros-gateway",
|
||||
"debros-node",
|
||||
"debros-olric",
|
||||
"debros-ipfs-cluster",
|
||||
"debros-ipfs",
|
||||
"debros-anyone-client",
|
||||
}
|
||||
|
||||
// Filter to only existing services by checking if unit file exists
|
||||
var existing []string
|
||||
for _, svc := range allServices {
|
||||
unitPath := filepath.Join("/etc/systemd/system", svc+".service")
|
||||
if _, err := os.Stat(unitPath); err == nil {
|
||||
existing = append(existing, svc)
|
||||
}
|
||||
}
|
||||
|
||||
return existing
|
||||
}
|
||||
|
||||
// CollectPortsForServices returns a list of ports used by the specified services
|
||||
func CollectPortsForServices(services []string, skipActive bool) ([]PortSpec, error) {
|
||||
seen := make(map[int]PortSpec)
|
||||
for _, svc := range services {
|
||||
if skipActive {
|
||||
active, err := IsServiceActive(svc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to check %s: %w", svc, err)
|
||||
}
|
||||
if active {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, spec := range ServicePorts[svc] {
|
||||
if _, ok := seen[spec.Port]; !ok {
|
||||
seen[spec.Port] = spec
|
||||
}
|
||||
}
|
||||
}
|
||||
ports := make([]PortSpec, 0, len(seen))
|
||||
for _, spec := range seen {
|
||||
ports = append(ports, spec)
|
||||
}
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
// EnsurePortsAvailable checks if the specified ports are available
|
||||
func EnsurePortsAvailable(action string, ports []PortSpec) error {
|
||||
for _, spec := range ports {
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", spec.Port))
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EADDRINUSE) || strings.Contains(err.Error(), "address already in use") {
|
||||
return fmt.Errorf("%s cannot continue: %s (port %d) is already in use", action, spec.Name, spec.Port)
|
||||
}
|
||||
return fmt.Errorf("%s cannot continue: failed to inspect %s (port %d): %w", action, spec.Name, spec.Port, err)
|
||||
}
|
||||
_ = ln.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
113
pkg/cli/utils/validation.go
Normal file
113
pkg/cli/utils/validation.go
Normal file
@ -0,0 +1,113 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/config"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
)
|
||||
|
||||
// ValidateGeneratedConfig loads and validates the generated node configuration
|
||||
func ValidateGeneratedConfig(oramaDir string) error {
|
||||
configPath := filepath.Join(oramaDir, "configs", "node.yaml")
|
||||
|
||||
// Check if config file exists
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("configuration file not found at %s", configPath)
|
||||
}
|
||||
|
||||
// Load the config file
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open config file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var cfg config.Config
|
||||
if err := config.DecodeStrict(file, &cfg); err != nil {
|
||||
return fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
// Validate the configuration
|
||||
if errs := cfg.Validate(); len(errs) > 0 {
|
||||
var errMsgs []string
|
||||
for _, e := range errs {
|
||||
errMsgs = append(errMsgs, e.Error())
|
||||
}
|
||||
return fmt.Errorf("configuration validation errors:\n - %s", strings.Join(errMsgs, "\n - "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDNSRecord validates that the domain points to the expected IP address
|
||||
// Returns nil if DNS is valid, warning message if DNS doesn't match but continues,
|
||||
// or error if DNS lookup fails completely
|
||||
func ValidateDNSRecord(domain, expectedIP string) error {
|
||||
if domain == "" {
|
||||
return nil // No domain provided, skip validation
|
||||
}
|
||||
|
||||
ips, err := net.LookupIP(domain)
|
||||
if err != nil {
|
||||
// DNS lookup failed - this is a warning, not a fatal error
|
||||
// The user might be setting up DNS after installation
|
||||
fmt.Printf(" ⚠️ DNS lookup failed for %s: %v\n", domain, err)
|
||||
fmt.Printf(" Make sure DNS is configured before enabling HTTPS\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if any resolved IP matches the expected IP
|
||||
for _, ip := range ips {
|
||||
if ip.String() == expectedIP {
|
||||
fmt.Printf(" ✓ DNS validated: %s → %s\n", domain, expectedIP)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DNS doesn't point to expected IP - warn but continue
|
||||
resolvedIPs := make([]string, len(ips))
|
||||
for i, ip := range ips {
|
||||
resolvedIPs[i] = ip.String()
|
||||
}
|
||||
fmt.Printf(" ⚠️ DNS mismatch: %s resolves to %v, expected %s\n", domain, resolvedIPs, expectedIP)
|
||||
fmt.Printf(" HTTPS certificate generation may fail until DNS is updated\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// NormalizePeers normalizes and validates peer multiaddrs
|
||||
func NormalizePeers(peersStr string) ([]string, error) {
|
||||
if peersStr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Split by comma and trim whitespace
|
||||
rawPeers := strings.Split(peersStr, ",")
|
||||
peers := make([]string, 0, len(rawPeers))
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, peer := range rawPeers {
|
||||
peer = strings.TrimSpace(peer)
|
||||
if peer == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate multiaddr format
|
||||
if _, err := multiaddr.NewMultiaddr(peer); err != nil {
|
||||
return nil, fmt.Errorf("invalid multiaddr %q: %w", peer, err)
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
if !seen[peer] {
|
||||
peers = append(peers, peer)
|
||||
seen[peer] = true
|
||||
}
|
||||
}
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
@ -195,49 +195,49 @@ func (c *Client) Connect() error {
|
||||
c.pubsub = &pubSubBridge{client: c, adapter: adapter}
|
||||
c.logger.Info("Pubsub bridge created successfully")
|
||||
|
||||
c.logger.Info("Starting bootstrap peer connections...")
|
||||
c.logger.Info("Starting peer connections...")
|
||||
|
||||
// Connect to bootstrap peers FIRST
|
||||
// Connect to peers FIRST
|
||||
ctx, cancel := context.WithTimeout(context.Background(), c.config.ConnectTimeout)
|
||||
defer cancel()
|
||||
|
||||
bootstrapPeersConnected := 0
|
||||
for _, bootstrapAddr := range c.config.BootstrapPeers {
|
||||
c.logger.Info("Attempting to connect to bootstrap peer", zap.String("addr", bootstrapAddr))
|
||||
if err := c.connectToBootstrap(ctx, bootstrapAddr); err != nil {
|
||||
c.logger.Warn("Failed to connect to bootstrap peer",
|
||||
zap.String("addr", bootstrapAddr),
|
||||
peersConnected := 0
|
||||
for _, peerAddr := range c.config.BootstrapPeers {
|
||||
c.logger.Info("Attempting to connect to peer", zap.String("addr", peerAddr))
|
||||
if err := c.connectToPeer(ctx, peerAddr); err != nil {
|
||||
c.logger.Warn("Failed to connect to peer",
|
||||
zap.String("addr", peerAddr),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
bootstrapPeersConnected++
|
||||
c.logger.Info("Successfully connected to bootstrap peer", zap.String("addr", bootstrapAddr))
|
||||
peersConnected++
|
||||
c.logger.Info("Successfully connected to peer", zap.String("addr", peerAddr))
|
||||
}
|
||||
|
||||
if bootstrapPeersConnected == 0 {
|
||||
c.logger.Warn("No bootstrap peers connected, continuing anyway")
|
||||
if peersConnected == 0 {
|
||||
c.logger.Warn("No peers connected, continuing anyway")
|
||||
} else {
|
||||
c.logger.Info("Bootstrap peer connections completed", zap.Int("connected_count", bootstrapPeersConnected))
|
||||
c.logger.Info("Peer connections completed", zap.Int("connected_count", peersConnected))
|
||||
}
|
||||
|
||||
c.logger.Info("Adding bootstrap peers to peerstore...")
|
||||
c.logger.Info("Adding peers to peerstore...")
|
||||
|
||||
// Add bootstrap peers to peerstore so we can connect to them later
|
||||
for _, bootstrapAddr := range c.config.BootstrapPeers {
|
||||
if ma, err := multiaddr.NewMultiaddr(bootstrapAddr); err == nil {
|
||||
// Add peers to peerstore so we can connect to them later
|
||||
for _, peerAddr := range c.config.BootstrapPeers {
|
||||
if ma, err := multiaddr.NewMultiaddr(peerAddr); err == nil {
|
||||
if peerInfo, err := peer.AddrInfoFromP2pAddr(ma); err == nil {
|
||||
c.host.Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, time.Hour*24)
|
||||
c.logger.Debug("Added bootstrap peer to peerstore",
|
||||
c.logger.Debug("Added peer to peerstore",
|
||||
zap.String("peer", peerInfo.ID.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
c.logger.Info("Bootstrap peers added to peerstore")
|
||||
c.logger.Info("Peers added to peerstore")
|
||||
|
||||
c.logger.Info("Starting connection monitoring...")
|
||||
|
||||
// Client is a lightweight P2P participant - no discovery needed
|
||||
// We only connect to known bootstrap peers and let nodes handle discovery
|
||||
// We only connect to known peers and let nodes handle discovery
|
||||
c.logger.Debug("Client configured as lightweight P2P participant (no discovery)")
|
||||
|
||||
// Start minimal connection monitoring
|
||||
@ -329,6 +329,18 @@ func (c *Client) getAppNamespace() string {
|
||||
return c.config.AppName
|
||||
}
|
||||
|
||||
// PubSubAdapter returns the underlying pubsub.ClientAdapter for direct use by serverless functions.
|
||||
// This bypasses the authentication checks used by PubSub() since serverless functions
|
||||
// are already authenticated via the gateway.
|
||||
func (c *Client) PubSubAdapter() *pubsub.ClientAdapter {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if c.pubsub == nil {
|
||||
return nil
|
||||
}
|
||||
return c.pubsub.adapter
|
||||
}
|
||||
|
||||
// requireAccess enforces that credentials are present and that any context-based namespace overrides match
|
||||
func (c *Client) requireAccess(ctx context.Context) error {
|
||||
// Allow internal system operations to bypass authentication
|
||||
|
||||
42
pkg/client/config.go
Normal file
42
pkg/client/config.go
Normal file
@ -0,0 +1,42 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ClientConfig represents configuration for network clients
|
||||
type ClientConfig struct {
|
||||
AppName string `json:"app_name"`
|
||||
DatabaseName string `json:"database_name"`
|
||||
BootstrapPeers []string `json:"peers"`
|
||||
DatabaseEndpoints []string `json:"database_endpoints"`
|
||||
GatewayURL string `json:"gateway_url"` // Gateway URL for HTTP API access (e.g., "http://localhost:6001")
|
||||
ConnectTimeout time.Duration `json:"connect_timeout"`
|
||||
RetryAttempts int `json:"retry_attempts"`
|
||||
RetryDelay time.Duration `json:"retry_delay"`
|
||||
QuietMode bool `json:"quiet_mode"` // Suppress debug/info logs
|
||||
APIKey string `json:"api_key"` // API key for gateway auth
|
||||
JWT string `json:"jwt"` // Optional JWT bearer token
|
||||
}
|
||||
|
||||
// DefaultClientConfig returns a default client configuration
|
||||
func DefaultClientConfig(appName string) *ClientConfig {
|
||||
// Base defaults
|
||||
peers := DefaultBootstrapPeers()
|
||||
endpoints := DefaultDatabaseEndpoints()
|
||||
|
||||
return &ClientConfig{
|
||||
AppName: appName,
|
||||
DatabaseName: fmt.Sprintf("%s_db", appName),
|
||||
BootstrapPeers: peers,
|
||||
DatabaseEndpoints: endpoints,
|
||||
GatewayURL: "http://localhost:6001",
|
||||
ConnectTimeout: time.Second * 30,
|
||||
RetryAttempts: 3,
|
||||
RetryDelay: time.Second * 5,
|
||||
QuietMode: false,
|
||||
APIKey: "",
|
||||
JWT: "",
|
||||
}
|
||||
}
|
||||
@ -9,8 +9,8 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// connectToBootstrap connects to a bootstrap peer
|
||||
func (c *Client) connectToBootstrap(ctx context.Context, addr string) error {
|
||||
// connectToPeer connects to a peer address
|
||||
func (c *Client) connectToPeer(ctx context.Context, addr string) error {
|
||||
ma, err := multiaddr.NewMultiaddr(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid multiaddr: %w", err)
|
||||
@ -20,14 +20,14 @@ func (c *Client) connectToBootstrap(ctx context.Context, addr string) error {
|
||||
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
|
||||
if err != nil {
|
||||
// If there's no peer ID, we can't connect
|
||||
c.logger.Warn("Bootstrap address missing peer ID, skipping",
|
||||
c.logger.Warn("Peer address missing peer ID, skipping",
|
||||
zap.String("addr", addr))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Avoid dialing ourselves: if the bootstrap address resolves to our own peer ID, skip.
|
||||
// Avoid dialing ourselves: if the peer address resolves to our own peer ID, skip.
|
||||
if c.host != nil && peerInfo.ID == c.host.ID() {
|
||||
c.logger.Debug("Skipping bootstrap address because it resolves to self",
|
||||
c.logger.Debug("Skipping peer address because it resolves to self",
|
||||
zap.String("addr", addr),
|
||||
zap.String("peer_id", peerInfo.ID.String()))
|
||||
return nil
|
||||
@ -38,7 +38,7 @@ func (c *Client) connectToBootstrap(ctx context.Context, addr string) error {
|
||||
return fmt.Errorf("failed to connect to peer: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Debug("Connected to bootstrap peer",
|
||||
c.logger.Debug("Connected to peer",
|
||||
zap.String("peer_id", peerInfo.ID.String()),
|
||||
zap.String("addr", addr))
|
||||
|
||||
|
||||
@ -5,10 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
"github.com/rqlite/gorqlite"
|
||||
)
|
||||
|
||||
@ -160,17 +157,31 @@ func (d *DatabaseClientImpl) isWriteOperation(sql string) bool {
|
||||
func (d *DatabaseClientImpl) clearConnection() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.connection = nil
|
||||
if d.connection != nil {
|
||||
d.connection.Close()
|
||||
d.connection = nil
|
||||
}
|
||||
}
|
||||
|
||||
// getRQLiteConnection returns a connection to RQLite, creating one if needed
|
||||
func (d *DatabaseClientImpl) getRQLiteConnection() (*gorqlite.Connection, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.mu.RLock()
|
||||
conn := d.connection
|
||||
d.mu.RUnlock()
|
||||
|
||||
// Always try to get a fresh connection to handle leadership changes
|
||||
// and node failures gracefully
|
||||
return d.connectToAvailableNode()
|
||||
if conn != nil {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
newConn, err := d.connectToAvailableNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
d.connection = newConn
|
||||
d.mu.Unlock()
|
||||
return newConn, nil
|
||||
}
|
||||
|
||||
// getRQLiteNodes returns a list of RQLite node URLs with precedence:
|
||||
@ -187,8 +198,7 @@ func (d *DatabaseClientImpl) getRQLiteNodes() []string {
|
||||
return DefaultDatabaseEndpoints()
|
||||
}
|
||||
|
||||
// normalizeEndpoints is now imported from defaults.go
|
||||
|
||||
// hasPort checks if a hostport string has a port suffix
|
||||
func hasPort(hostport string) bool {
|
||||
// cheap check for :port suffix (IPv6 with brackets handled by url.Parse earlier)
|
||||
if i := strings.LastIndex(hostport, ":"); i > -1 && i < len(hostport)-1 {
|
||||
@ -227,7 +237,6 @@ func (d *DatabaseClientImpl) connectToAvailableNode() (*gorqlite.Connection, err
|
||||
continue
|
||||
}
|
||||
|
||||
d.connection = conn
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
@ -391,175 +400,3 @@ func (d *DatabaseClientImpl) GetSchema(ctx context.Context) (*SchemaInfo, error)
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
// NetworkInfoImpl implements NetworkInfo
|
||||
type NetworkInfoImpl struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// GetPeers returns information about connected peers
|
||||
func (n *NetworkInfoImpl) GetPeers(ctx context.Context) ([]PeerInfo, error) {
|
||||
if !n.client.isConnected() {
|
||||
return nil, fmt.Errorf("client not connected")
|
||||
}
|
||||
|
||||
if err := n.client.requireAccess(ctx); err != nil {
|
||||
return nil, fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
|
||||
}
|
||||
|
||||
// Get peers from LibP2P host
|
||||
host := n.client.host
|
||||
if host == nil {
|
||||
return nil, fmt.Errorf("no host available")
|
||||
}
|
||||
|
||||
// Get connected peers
|
||||
connectedPeers := host.Network().Peers()
|
||||
peers := make([]PeerInfo, 0, len(connectedPeers)+1) // +1 for self
|
||||
|
||||
// Add connected peers
|
||||
for _, peerID := range connectedPeers {
|
||||
// Get peer addresses
|
||||
peerInfo := host.Peerstore().PeerInfo(peerID)
|
||||
|
||||
// Convert multiaddrs to strings
|
||||
addrs := make([]string, len(peerInfo.Addrs))
|
||||
for i, addr := range peerInfo.Addrs {
|
||||
addrs[i] = addr.String()
|
||||
}
|
||||
|
||||
peers = append(peers, PeerInfo{
|
||||
ID: peerID.String(),
|
||||
Addresses: addrs,
|
||||
Connected: true,
|
||||
LastSeen: time.Now(), // LibP2P doesn't track last seen, so use current time
|
||||
})
|
||||
}
|
||||
|
||||
// Add self node
|
||||
selfPeerInfo := host.Peerstore().PeerInfo(host.ID())
|
||||
selfAddrs := make([]string, len(selfPeerInfo.Addrs))
|
||||
for i, addr := range selfPeerInfo.Addrs {
|
||||
selfAddrs[i] = addr.String()
|
||||
}
|
||||
|
||||
// Insert self node at the beginning of the list
|
||||
selfPeer := PeerInfo{
|
||||
ID: host.ID().String(),
|
||||
Addresses: selfAddrs,
|
||||
Connected: true,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
|
||||
// Prepend self to the list
|
||||
peers = append([]PeerInfo{selfPeer}, peers...)
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
// GetStatus returns network status
|
||||
func (n *NetworkInfoImpl) GetStatus(ctx context.Context) (*NetworkStatus, error) {
|
||||
if !n.client.isConnected() {
|
||||
return nil, fmt.Errorf("client not connected")
|
||||
}
|
||||
|
||||
if err := n.client.requireAccess(ctx); err != nil {
|
||||
return nil, fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
|
||||
}
|
||||
|
||||
host := n.client.host
|
||||
if host == nil {
|
||||
return nil, fmt.Errorf("no host available")
|
||||
}
|
||||
|
||||
// Get actual network status
|
||||
connectedPeers := host.Network().Peers()
|
||||
|
||||
// Try to get database size from RQLite (optional - don't fail if unavailable)
|
||||
var dbSize int64 = 0
|
||||
dbClient := n.client.database
|
||||
if conn, err := dbClient.getRQLiteConnection(); err == nil {
|
||||
// Query database size (rough estimate)
|
||||
if result, err := conn.QueryOne("SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"); err == nil {
|
||||
for result.Next() {
|
||||
if row, err := result.Slice(); err == nil && len(row) > 0 {
|
||||
if size, ok := row[0].(int64); ok {
|
||||
dbSize = size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &NetworkStatus{
|
||||
NodeID: host.ID().String(),
|
||||
Connected: true,
|
||||
PeerCount: len(connectedPeers),
|
||||
DatabaseSize: dbSize,
|
||||
Uptime: time.Since(n.client.startTime),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConnectToPeer connects to a specific peer
|
||||
func (n *NetworkInfoImpl) ConnectToPeer(ctx context.Context, peerAddr string) error {
|
||||
if !n.client.isConnected() {
|
||||
return fmt.Errorf("client not connected")
|
||||
}
|
||||
|
||||
if err := n.client.requireAccess(ctx); err != nil {
|
||||
return fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
|
||||
}
|
||||
|
||||
host := n.client.host
|
||||
if host == nil {
|
||||
return fmt.Errorf("no host available")
|
||||
}
|
||||
|
||||
// Parse the multiaddr
|
||||
ma, err := multiaddr.NewMultiaddr(peerAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid multiaddr: %w", err)
|
||||
}
|
||||
|
||||
// Extract peer info
|
||||
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract peer info: %w", err)
|
||||
}
|
||||
|
||||
// Connect to the peer
|
||||
if err := host.Connect(ctx, *peerInfo); err != nil {
|
||||
return fmt.Errorf("failed to connect to peer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisconnectFromPeer disconnects from a specific peer
|
||||
func (n *NetworkInfoImpl) DisconnectFromPeer(ctx context.Context, peerID string) error {
|
||||
if !n.client.isConnected() {
|
||||
return fmt.Errorf("client not connected")
|
||||
}
|
||||
|
||||
if err := n.client.requireAccess(ctx); err != nil {
|
||||
return fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
|
||||
}
|
||||
|
||||
host := n.client.host
|
||||
if host == nil {
|
||||
return fmt.Errorf("no host available")
|
||||
}
|
||||
|
||||
// Parse the peer ID
|
||||
pid, err := peer.Decode(peerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid peer ID: %w", err)
|
||||
}
|
||||
|
||||
// Close the connection to the peer
|
||||
if err := host.Network().ClosePeer(pid); err != nil {
|
||||
return fmt.Errorf("failed to disconnect from peer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
)
|
||||
|
||||
// DefaultBootstrapPeers returns the library's default bootstrap peer multiaddrs.
|
||||
// DefaultBootstrapPeers returns the default peer multiaddrs.
|
||||
// These can be overridden by environment variables or config.
|
||||
func DefaultBootstrapPeers() []string {
|
||||
// Check environment variable first
|
||||
@ -48,7 +48,7 @@ func DefaultDatabaseEndpoints() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// Try to derive from bootstrap peers if available
|
||||
// Try to derive from configured peers if available
|
||||
peers := DefaultBootstrapPeers()
|
||||
if len(peers) > 0 {
|
||||
endpoints := make([]string, 0, len(peers))
|
||||
|
||||
@ -10,15 +10,15 @@ import (
|
||||
func TestDefaultBootstrapPeersNonEmpty(t *testing.T) {
|
||||
old := os.Getenv("DEBROS_BOOTSTRAP_PEERS")
|
||||
t.Cleanup(func() { os.Setenv("DEBROS_BOOTSTRAP_PEERS", old) })
|
||||
// Set a valid bootstrap peer
|
||||
// Set a valid peer
|
||||
validPeer := "/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"
|
||||
_ = os.Setenv("DEBROS_BOOTSTRAP_PEERS", validPeer)
|
||||
peers := DefaultBootstrapPeers()
|
||||
if len(peers) == 0 {
|
||||
t.Fatalf("expected non-empty default bootstrap peers")
|
||||
t.Fatalf("expected non-empty default peers")
|
||||
}
|
||||
if peers[0] != validPeer {
|
||||
t.Fatalf("expected bootstrap peer %s, got %s", validPeer, peers[0])
|
||||
t.Fatalf("expected peer %s, got %s", validPeer, peers[0])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
51
pkg/client/errors.go
Normal file
51
pkg/client/errors.go
Normal file
@ -0,0 +1,51 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Common client errors
|
||||
var (
|
||||
// ErrNotConnected indicates the client is not connected to the network
|
||||
ErrNotConnected = errors.New("client not connected")
|
||||
|
||||
// ErrAuthRequired indicates authentication is required for the operation
|
||||
ErrAuthRequired = errors.New("authentication required")
|
||||
|
||||
// ErrNoHost indicates no LibP2P host is available
|
||||
ErrNoHost = errors.New("no host available")
|
||||
|
||||
// ErrInvalidConfig indicates the client configuration is invalid
|
||||
ErrInvalidConfig = errors.New("invalid configuration")
|
||||
|
||||
// ErrNamespaceMismatch indicates a namespace mismatch
|
||||
ErrNamespaceMismatch = errors.New("namespace mismatch")
|
||||
)
|
||||
|
||||
// ClientError represents a client-specific error with additional context
|
||||
type ClientError struct {
|
||||
Op string // Operation that failed
|
||||
Message string // Error message
|
||||
Err error // Underlying error
|
||||
}
|
||||
|
||||
func (e *ClientError) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("%s: %s: %v", e.Op, e.Message, e.Err)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", e.Op, e.Message)
|
||||
}
|
||||
|
||||
func (e *ClientError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// NewClientError creates a new ClientError
|
||||
func NewClientError(op, message string, err error) *ClientError {
|
||||
return &ClientError{
|
||||
Op: op,
|
||||
Message: message,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
@ -114,11 +113,26 @@ type PeerInfo struct {
|
||||
|
||||
// NetworkStatus contains overall network status
|
||||
type NetworkStatus struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Connected bool `json:"connected"`
|
||||
PeerCount int `json:"peer_count"`
|
||||
DatabaseSize int64 `json:"database_size"`
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
NodeID string `json:"node_id"`
|
||||
PeerID string `json:"peer_id"`
|
||||
Connected bool `json:"connected"`
|
||||
PeerCount int `json:"peer_count"`
|
||||
DatabaseSize int64 `json:"database_size"`
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
IPFS *IPFSPeerInfo `json:"ipfs,omitempty"`
|
||||
IPFSCluster *IPFSClusterPeerInfo `json:"ipfs_cluster,omitempty"`
|
||||
}
|
||||
|
||||
// IPFSPeerInfo contains IPFS peer information for discovery
|
||||
type IPFSPeerInfo struct {
|
||||
PeerID string `json:"peer_id"`
|
||||
SwarmAddresses []string `json:"swarm_addresses"`
|
||||
}
|
||||
|
||||
// IPFSClusterPeerInfo contains IPFS Cluster peer information for cluster discovery
|
||||
type IPFSClusterPeerInfo struct {
|
||||
PeerID string `json:"peer_id"` // Cluster peer ID (different from IPFS peer ID)
|
||||
Addresses []string `json:"addresses"` // Cluster multiaddresses (e.g., /ip4/x.x.x.x/tcp/9098)
|
||||
}
|
||||
|
||||
// HealthStatus contains health check information
|
||||
@ -153,39 +167,3 @@ type StorageStatus struct {
|
||||
Peers []string `json:"peers"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ClientConfig represents configuration for network clients
|
||||
type ClientConfig struct {
|
||||
AppName string `json:"app_name"`
|
||||
DatabaseName string `json:"database_name"`
|
||||
BootstrapPeers []string `json:"bootstrap_peers"`
|
||||
DatabaseEndpoints []string `json:"database_endpoints"`
|
||||
GatewayURL string `json:"gateway_url"` // Gateway URL for HTTP API access (e.g., "http://localhost:6001")
|
||||
ConnectTimeout time.Duration `json:"connect_timeout"`
|
||||
RetryAttempts int `json:"retry_attempts"`
|
||||
RetryDelay time.Duration `json:"retry_delay"`
|
||||
QuietMode bool `json:"quiet_mode"` // Suppress debug/info logs
|
||||
APIKey string `json:"api_key"` // API key for gateway auth
|
||||
JWT string `json:"jwt"` // Optional JWT bearer token
|
||||
}
|
||||
|
||||
// DefaultClientConfig returns a default client configuration
|
||||
func DefaultClientConfig(appName string) *ClientConfig {
|
||||
// Base defaults
|
||||
peers := DefaultBootstrapPeers()
|
||||
endpoints := DefaultDatabaseEndpoints()
|
||||
|
||||
return &ClientConfig{
|
||||
AppName: appName,
|
||||
DatabaseName: fmt.Sprintf("%s_db", appName),
|
||||
BootstrapPeers: peers,
|
||||
DatabaseEndpoints: endpoints,
|
||||
GatewayURL: "http://localhost:6001",
|
||||
ConnectTimeout: time.Second * 30,
|
||||
RetryAttempts: 3,
|
||||
RetryDelay: time.Second * 5,
|
||||
QuietMode: false,
|
||||
APIKey: "",
|
||||
JWT: "",
|
||||
}
|
||||
}
|
||||
|
||||
270
pkg/client/network_client.go
Normal file
270
pkg/client/network_client.go
Normal file
@ -0,0 +1,270 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
)
|
||||
|
||||
// NetworkInfoImpl implements NetworkInfo
|
||||
type NetworkInfoImpl struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// GetPeers returns information about connected peers
|
||||
func (n *NetworkInfoImpl) GetPeers(ctx context.Context) ([]PeerInfo, error) {
|
||||
if !n.client.isConnected() {
|
||||
return nil, fmt.Errorf("client not connected")
|
||||
}
|
||||
|
||||
if err := n.client.requireAccess(ctx); err != nil {
|
||||
return nil, fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
|
||||
}
|
||||
|
||||
// Get peers from LibP2P host
|
||||
host := n.client.host
|
||||
if host == nil {
|
||||
return nil, fmt.Errorf("no host available")
|
||||
}
|
||||
|
||||
// Get connected peers
|
||||
connectedPeers := host.Network().Peers()
|
||||
peers := make([]PeerInfo, 0, len(connectedPeers)+1) // +1 for self
|
||||
|
||||
// Add connected peers
|
||||
for _, peerID := range connectedPeers {
|
||||
// Get peer addresses
|
||||
peerInfo := host.Peerstore().PeerInfo(peerID)
|
||||
|
||||
// Convert multiaddrs to strings
|
||||
addrs := make([]string, len(peerInfo.Addrs))
|
||||
for i, addr := range peerInfo.Addrs {
|
||||
addrs[i] = addr.String()
|
||||
}
|
||||
|
||||
peers = append(peers, PeerInfo{
|
||||
ID: peerID.String(),
|
||||
Addresses: addrs,
|
||||
Connected: true,
|
||||
LastSeen: time.Now(), // LibP2P doesn't track last seen, so use current time
|
||||
})
|
||||
}
|
||||
|
||||
// Add self node
|
||||
selfPeerInfo := host.Peerstore().PeerInfo(host.ID())
|
||||
selfAddrs := make([]string, len(selfPeerInfo.Addrs))
|
||||
for i, addr := range selfPeerInfo.Addrs {
|
||||
selfAddrs[i] = addr.String()
|
||||
}
|
||||
|
||||
// Insert self node at the beginning of the list
|
||||
selfPeer := PeerInfo{
|
||||
ID: host.ID().String(),
|
||||
Addresses: selfAddrs,
|
||||
Connected: true,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
|
||||
// Prepend self to the list
|
||||
peers = append([]PeerInfo{selfPeer}, peers...)
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
// GetStatus returns network status
|
||||
func (n *NetworkInfoImpl) GetStatus(ctx context.Context) (*NetworkStatus, error) {
|
||||
if !n.client.isConnected() {
|
||||
return nil, fmt.Errorf("client not connected")
|
||||
}
|
||||
|
||||
if err := n.client.requireAccess(ctx); err != nil {
|
||||
return nil, fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
|
||||
}
|
||||
|
||||
host := n.client.host
|
||||
if host == nil {
|
||||
return nil, fmt.Errorf("no host available")
|
||||
}
|
||||
|
||||
// Get actual network status
|
||||
connectedPeers := host.Network().Peers()
|
||||
|
||||
// Try to get database size from RQLite (optional - don't fail if unavailable)
|
||||
var dbSize int64 = 0
|
||||
dbClient := n.client.database
|
||||
if conn, err := dbClient.getRQLiteConnection(); err == nil {
|
||||
// Query database size (rough estimate)
|
||||
if result, err := conn.QueryOne("SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"); err == nil {
|
||||
for result.Next() {
|
||||
if row, err := result.Slice(); err == nil && len(row) > 0 {
|
||||
if size, ok := row[0].(int64); ok {
|
||||
dbSize = size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get IPFS peer info (optional - don't fail if unavailable)
|
||||
ipfsInfo := queryIPFSPeerInfo()
|
||||
|
||||
// Try to get IPFS Cluster peer info (optional - don't fail if unavailable)
|
||||
ipfsClusterInfo := queryIPFSClusterPeerInfo()
|
||||
|
||||
return &NetworkStatus{
|
||||
NodeID: host.ID().String(),
|
||||
PeerID: host.ID().String(),
|
||||
Connected: true,
|
||||
PeerCount: len(connectedPeers),
|
||||
DatabaseSize: dbSize,
|
||||
Uptime: time.Since(n.client.startTime),
|
||||
IPFS: ipfsInfo,
|
||||
IPFSCluster: ipfsClusterInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// queryIPFSPeerInfo queries the local IPFS API for peer information
|
||||
// Returns nil if IPFS is not running or unavailable
|
||||
func queryIPFSPeerInfo() *IPFSPeerInfo {
|
||||
// IPFS API typically runs on port 4501 in our setup
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
resp, err := client.Post("http://localhost:4501/api/v0/id", "", nil)
|
||||
if err != nil {
|
||||
return nil // IPFS not available
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result struct {
|
||||
ID string `json:"ID"`
|
||||
Addresses []string `json:"Addresses"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter addresses to only include public/routable ones
|
||||
var swarmAddrs []string
|
||||
for _, addr := range result.Addresses {
|
||||
// Skip loopback and private addresses for external discovery
|
||||
if !strings.Contains(addr, "127.0.0.1") && !strings.Contains(addr, "/ip6/::1") {
|
||||
swarmAddrs = append(swarmAddrs, addr)
|
||||
}
|
||||
}
|
||||
|
||||
return &IPFSPeerInfo{
|
||||
PeerID: result.ID,
|
||||
SwarmAddresses: swarmAddrs,
|
||||
}
|
||||
}
|
||||
|
||||
// queryIPFSClusterPeerInfo queries the local IPFS Cluster API for peer information
|
||||
// Returns nil if IPFS Cluster is not running or unavailable
|
||||
func queryIPFSClusterPeerInfo() *IPFSClusterPeerInfo {
|
||||
// IPFS Cluster API typically runs on port 9094 in our setup
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
resp, err := client.Get("http://localhost:9094/id")
|
||||
if err != nil {
|
||||
return nil // IPFS Cluster not available
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result struct {
|
||||
ID string `json:"id"`
|
||||
Addresses []string `json:"addresses"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter addresses to only include public/routable ones for cluster discovery
|
||||
var clusterAddrs []string
|
||||
for _, addr := range result.Addresses {
|
||||
// Skip loopback addresses - only keep routable addresses
|
||||
if !strings.Contains(addr, "127.0.0.1") && !strings.Contains(addr, "/ip6/::1") {
|
||||
clusterAddrs = append(clusterAddrs, addr)
|
||||
}
|
||||
}
|
||||
|
||||
return &IPFSClusterPeerInfo{
|
||||
PeerID: result.ID,
|
||||
Addresses: clusterAddrs,
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectToPeer connects to a specific peer
|
||||
func (n *NetworkInfoImpl) ConnectToPeer(ctx context.Context, peerAddr string) error {
|
||||
if !n.client.isConnected() {
|
||||
return fmt.Errorf("client not connected")
|
||||
}
|
||||
|
||||
if err := n.client.requireAccess(ctx); err != nil {
|
||||
return fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
|
||||
}
|
||||
|
||||
host := n.client.host
|
||||
if host == nil {
|
||||
return fmt.Errorf("no host available")
|
||||
}
|
||||
|
||||
// Parse the multiaddr
|
||||
ma, err := multiaddr.NewMultiaddr(peerAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid multiaddr: %w", err)
|
||||
}
|
||||
|
||||
// Extract peer info
|
||||
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract peer info: %w", err)
|
||||
}
|
||||
|
||||
// Connect to the peer
|
||||
if err := host.Connect(ctx, *peerInfo); err != nil {
|
||||
return fmt.Errorf("failed to connect to peer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisconnectFromPeer disconnects from a specific peer
|
||||
func (n *NetworkInfoImpl) DisconnectFromPeer(ctx context.Context, peerID string) error {
|
||||
if !n.client.isConnected() {
|
||||
return fmt.Errorf("client not connected")
|
||||
}
|
||||
|
||||
if err := n.client.requireAccess(ctx); err != nil {
|
||||
return fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
|
||||
}
|
||||
|
||||
host := n.client.host
|
||||
if host == nil {
|
||||
return fmt.Errorf("no host available")
|
||||
}
|
||||
|
||||
// Parse the peer ID
|
||||
pid, err := peer.Decode(peerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid peer ID: %w", err)
|
||||
}
|
||||
|
||||
// Close the connection to the peer
|
||||
if err := host.Network().ClosePeer(pid); err != nil {
|
||||
return fmt.Errorf("failed to disconnect from peer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -215,31 +214,12 @@ func (s *StorageClientImpl) Unpin(ctx context.Context, cid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getGatewayURL returns the gateway URL from config, defaulting to localhost:6001
|
||||
// getGatewayURL returns the gateway URL from config
|
||||
func (s *StorageClientImpl) getGatewayURL() string {
|
||||
cfg := s.client.Config()
|
||||
if cfg != nil && cfg.GatewayURL != "" {
|
||||
return strings.TrimSuffix(cfg.GatewayURL, "/")
|
||||
}
|
||||
return "http://localhost:6001"
|
||||
return getGatewayURL(s.client)
|
||||
}
|
||||
|
||||
// addAuthHeaders adds authentication headers to the request
|
||||
func (s *StorageClientImpl) addAuthHeaders(req *http.Request) {
|
||||
cfg := s.client.Config()
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Prefer JWT if available
|
||||
if cfg.JWT != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.JWT)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to API key
|
||||
if cfg.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("X-API-Key", cfg.APIKey)
|
||||
}
|
||||
addAuthHeaders(req, s.client)
|
||||
}
|
||||
|
||||
35
pkg/client/transport.go
Normal file
35
pkg/client/transport.go
Normal file
@ -0,0 +1,35 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// getGatewayURL returns the gateway URL from config, defaulting to localhost:6001
|
||||
func getGatewayURL(c *Client) string {
|
||||
cfg := c.Config()
|
||||
if cfg != nil && cfg.GatewayURL != "" {
|
||||
return strings.TrimSuffix(cfg.GatewayURL, "/")
|
||||
}
|
||||
return "http://localhost:6001"
|
||||
}
|
||||
|
||||
// addAuthHeaders adds authentication headers to the request
|
||||
func addAuthHeaders(req *http.Request, c *Client) {
|
||||
cfg := c.Config()
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Prefer JWT if available
|
||||
if cfg.JWT != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.JWT)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to API key
|
||||
if cfg.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("X-API-Key", cfg.APIKey)
|
||||
}
|
||||
}
|
||||
@ -3,107 +3,81 @@ package config
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/config/validate"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
)
|
||||
|
||||
// Config represents the main configuration for a network node
|
||||
type Config struct {
|
||||
Node NodeConfig `yaml:"node"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Discovery DiscoveryConfig `yaml:"discovery"`
|
||||
Security SecurityConfig `yaml:"security"`
|
||||
Logging LoggingConfig `yaml:"logging"`
|
||||
Node NodeConfig `yaml:"node"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Discovery DiscoveryConfig `yaml:"discovery"`
|
||||
Security SecurityConfig `yaml:"security"`
|
||||
Logging LoggingConfig `yaml:"logging"`
|
||||
HTTPGateway HTTPGatewayConfig `yaml:"http_gateway"`
|
||||
}
|
||||
|
||||
// NodeConfig contains node-specific configuration
|
||||
type NodeConfig struct {
|
||||
ID string `yaml:"id"` // Auto-generated if empty
|
||||
Type string `yaml:"type"` // "bootstrap" or "node"
|
||||
ListenAddresses []string `yaml:"listen_addresses"` // LibP2P listen addresses
|
||||
DataDir string `yaml:"data_dir"` // Data directory
|
||||
MaxConnections int `yaml:"max_connections"` // Maximum peer connections
|
||||
// ValidationError represents a single validation error with context.
|
||||
// This is exported from the validate subpackage for backward compatibility.
|
||||
type ValidationError = validate.ValidationError
|
||||
|
||||
// ValidateSwarmKey validates that a swarm key is 64 hex characters.
|
||||
// This is exported from the validate subpackage for backward compatibility.
|
||||
func ValidateSwarmKey(key string) error {
|
||||
return validate.ValidateSwarmKey(key)
|
||||
}
|
||||
|
||||
// DatabaseConfig contains database-related configuration
|
||||
type DatabaseConfig struct {
|
||||
DataDir string `yaml:"data_dir"`
|
||||
ReplicationFactor int `yaml:"replication_factor"`
|
||||
ShardCount int `yaml:"shard_count"`
|
||||
MaxDatabaseSize int64 `yaml:"max_database_size"` // In bytes
|
||||
BackupInterval time.Duration `yaml:"backup_interval"`
|
||||
// Validate performs comprehensive validation of the entire config.
|
||||
// It aggregates all errors and returns them, allowing the caller to print all issues at once.
|
||||
func (c *Config) Validate() []error {
|
||||
var errs []error
|
||||
|
||||
// RQLite-specific configuration
|
||||
RQLitePort int `yaml:"rqlite_port"` // RQLite HTTP API port
|
||||
RQLiteRaftPort int `yaml:"rqlite_raft_port"` // RQLite Raft consensus port
|
||||
RQLiteJoinAddress string `yaml:"rqlite_join_address"` // Address to join RQLite cluster
|
||||
// Validate node config
|
||||
errs = append(errs, validate.ValidateNode(validate.NodeConfig{
|
||||
ID: c.Node.ID,
|
||||
ListenAddresses: c.Node.ListenAddresses,
|
||||
DataDir: c.Node.DataDir,
|
||||
MaxConnections: c.Node.MaxConnections,
|
||||
})...)
|
||||
|
||||
// Dynamic discovery configuration (always enabled)
|
||||
ClusterSyncInterval time.Duration `yaml:"cluster_sync_interval"` // default: 30s
|
||||
PeerInactivityLimit time.Duration `yaml:"peer_inactivity_limit"` // default: 24h
|
||||
MinClusterSize int `yaml:"min_cluster_size"` // default: 1
|
||||
// Validate database config
|
||||
errs = append(errs, validate.ValidateDatabase(validate.DatabaseConfig{
|
||||
DataDir: c.Database.DataDir,
|
||||
ReplicationFactor: c.Database.ReplicationFactor,
|
||||
ShardCount: c.Database.ShardCount,
|
||||
MaxDatabaseSize: c.Database.MaxDatabaseSize,
|
||||
RQLitePort: c.Database.RQLitePort,
|
||||
RQLiteRaftPort: c.Database.RQLiteRaftPort,
|
||||
RQLiteJoinAddress: c.Database.RQLiteJoinAddress,
|
||||
ClusterSyncInterval: c.Database.ClusterSyncInterval,
|
||||
PeerInactivityLimit: c.Database.PeerInactivityLimit,
|
||||
MinClusterSize: c.Database.MinClusterSize,
|
||||
})...)
|
||||
|
||||
// Olric cache configuration
|
||||
OlricHTTPPort int `yaml:"olric_http_port"` // Olric HTTP API port (default: 3320)
|
||||
OlricMemberlistPort int `yaml:"olric_memberlist_port"` // Olric memberlist port (default: 3322)
|
||||
// Validate discovery config
|
||||
errs = append(errs, validate.ValidateDiscovery(validate.DiscoveryConfig{
|
||||
BootstrapPeers: c.Discovery.BootstrapPeers,
|
||||
DiscoveryInterval: c.Discovery.DiscoveryInterval,
|
||||
BootstrapPort: c.Discovery.BootstrapPort,
|
||||
HttpAdvAddress: c.Discovery.HttpAdvAddress,
|
||||
RaftAdvAddress: c.Discovery.RaftAdvAddress,
|
||||
})...)
|
||||
|
||||
// IPFS storage configuration
|
||||
IPFS IPFSConfig `yaml:"ipfs"`
|
||||
}
|
||||
// Validate security config
|
||||
errs = append(errs, validate.ValidateSecurity(validate.SecurityConfig{
|
||||
EnableTLS: c.Security.EnableTLS,
|
||||
PrivateKeyFile: c.Security.PrivateKeyFile,
|
||||
CertificateFile: c.Security.CertificateFile,
|
||||
})...)
|
||||
|
||||
// IPFSConfig contains IPFS storage configuration
|
||||
type IPFSConfig struct {
|
||||
// ClusterAPIURL is the IPFS Cluster HTTP API URL (e.g., "http://localhost:9094")
|
||||
// If empty, IPFS storage is disabled for this node
|
||||
ClusterAPIURL string `yaml:"cluster_api_url"`
|
||||
// Validate logging config
|
||||
errs = append(errs, validate.ValidateLogging(validate.LoggingConfig{
|
||||
Level: c.Logging.Level,
|
||||
Format: c.Logging.Format,
|
||||
OutputFile: c.Logging.OutputFile,
|
||||
})...)
|
||||
|
||||
// APIURL is the IPFS HTTP API URL for content retrieval (e.g., "http://localhost:5001")
|
||||
// If empty, defaults to "http://localhost:5001"
|
||||
APIURL string `yaml:"api_url"`
|
||||
|
||||
// Timeout for IPFS operations
|
||||
// If zero, defaults to 60 seconds
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
|
||||
// ReplicationFactor is the replication factor for pinned content
|
||||
// If zero, defaults to 3
|
||||
ReplicationFactor int `yaml:"replication_factor"`
|
||||
|
||||
// EnableEncryption enables client-side encryption before upload
|
||||
// Defaults to true
|
||||
EnableEncryption bool `yaml:"enable_encryption"`
|
||||
}
|
||||
|
||||
// DiscoveryConfig contains peer discovery configuration
|
||||
type DiscoveryConfig struct {
|
||||
BootstrapPeers []string `yaml:"bootstrap_peers"` // Bootstrap peer addresses
|
||||
DiscoveryInterval time.Duration `yaml:"discovery_interval"` // Discovery announcement interval
|
||||
BootstrapPort int `yaml:"bootstrap_port"` // Default port for bootstrap nodes
|
||||
HttpAdvAddress string `yaml:"http_adv_address"` // HTTP advertisement address
|
||||
RaftAdvAddress string `yaml:"raft_adv_address"` // Raft advertisement
|
||||
NodeNamespace string `yaml:"node_namespace"` // Namespace for node identifiers
|
||||
}
|
||||
|
||||
// SecurityConfig contains security-related configuration
|
||||
type SecurityConfig struct {
|
||||
EnableTLS bool `yaml:"enable_tls"`
|
||||
PrivateKeyFile string `yaml:"private_key_file"`
|
||||
CertificateFile string `yaml:"certificate_file"`
|
||||
}
|
||||
|
||||
// LoggingConfig contains logging configuration
|
||||
type LoggingConfig struct {
|
||||
Level string `yaml:"level"` // debug, info, warn, error
|
||||
Format string `yaml:"format"` // json, console
|
||||
OutputFile string `yaml:"output_file"` // Empty for stdout
|
||||
}
|
||||
|
||||
// ClientConfig represents configuration for network clients
|
||||
type ClientConfig struct {
|
||||
AppName string `yaml:"app_name"`
|
||||
DatabaseName string `yaml:"database_name"`
|
||||
BootstrapPeers []string `yaml:"bootstrap_peers"`
|
||||
ConnectTimeout time.Duration `yaml:"connect_timeout"`
|
||||
RetryAttempts int `yaml:"retry_attempts"`
|
||||
return errs
|
||||
}
|
||||
|
||||
// ParseMultiaddrs converts string addresses to multiaddr objects
|
||||
@ -123,7 +97,6 @@ func (c *Config) ParseMultiaddrs() ([]multiaddr.Multiaddr, error) {
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Node: NodeConfig{
|
||||
Type: "node",
|
||||
ListenAddresses: []string{
|
||||
"/ip4/0.0.0.0/tcp/4001", // TCP only - compatible with Anyone proxy/SOCKS5
|
||||
},
|
||||
@ -140,7 +113,7 @@ func DefaultConfig() *Config {
|
||||
// RQLite-specific configuration
|
||||
RQLitePort: 5001,
|
||||
RQLiteRaftPort: 7001,
|
||||
RQLiteJoinAddress: "", // Empty for bootstrap node
|
||||
RQLiteJoinAddress: "", // Empty for first node (creates cluster)
|
||||
|
||||
// Dynamic discovery (always enabled)
|
||||
ClusterSyncInterval: 30 * time.Second,
|
||||
@ -175,5 +148,18 @@ func DefaultConfig() *Config {
|
||||
Level: "info",
|
||||
Format: "console",
|
||||
},
|
||||
HTTPGateway: HTTPGatewayConfig{
|
||||
Enabled: true,
|
||||
ListenAddr: ":8080",
|
||||
NodeName: "default",
|
||||
Routes: make(map[string]RouteConfig),
|
||||
ClientNamespace: "default",
|
||||
RQLiteDSN: "http://localhost:5001",
|
||||
OlricServers: []string{"localhost:3320"},
|
||||
OlricTimeout: 10 * time.Second,
|
||||
IPFSClusterAPIURL: "http://localhost:9094",
|
||||
IPFSAPIURL: "http://localhost:5001",
|
||||
IPFSTimeout: 60 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
59
pkg/config/database_config.go
Normal file
59
pkg/config/database_config.go
Normal file
@ -0,0 +1,59 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// DatabaseConfig contains database-related configuration
|
||||
type DatabaseConfig struct {
|
||||
DataDir string `yaml:"data_dir"`
|
||||
ReplicationFactor int `yaml:"replication_factor"`
|
||||
ShardCount int `yaml:"shard_count"`
|
||||
MaxDatabaseSize int64 `yaml:"max_database_size"` // In bytes
|
||||
BackupInterval time.Duration `yaml:"backup_interval"`
|
||||
|
||||
// RQLite-specific configuration
|
||||
RQLitePort int `yaml:"rqlite_port"` // RQLite HTTP API port
|
||||
RQLiteRaftPort int `yaml:"rqlite_raft_port"` // RQLite Raft consensus port
|
||||
RQLiteJoinAddress string `yaml:"rqlite_join_address"` // Address to join RQLite cluster
|
||||
|
||||
// RQLite node-to-node TLS encryption (for inter-node Raft communication)
|
||||
// See: https://rqlite.io/docs/guides/security/#encrypting-node-to-node-communication
|
||||
NodeCert string `yaml:"node_cert"` // Path to X.509 certificate for node-to-node communication
|
||||
NodeKey string `yaml:"node_key"` // Path to X.509 private key for node-to-node communication
|
||||
NodeCACert string `yaml:"node_ca_cert"` // Path to CA certificate (optional, uses system CA if not set)
|
||||
NodeNoVerify bool `yaml:"node_no_verify"` // Skip certificate verification (for testing/self-signed certs)
|
||||
|
||||
// Dynamic discovery configuration (always enabled)
|
||||
ClusterSyncInterval time.Duration `yaml:"cluster_sync_interval"` // default: 30s
|
||||
PeerInactivityLimit time.Duration `yaml:"peer_inactivity_limit"` // default: 24h
|
||||
MinClusterSize int `yaml:"min_cluster_size"` // default: 1
|
||||
|
||||
// Olric cache configuration
|
||||
OlricHTTPPort int `yaml:"olric_http_port"` // Olric HTTP API port (default: 3320)
|
||||
OlricMemberlistPort int `yaml:"olric_memberlist_port"` // Olric memberlist port (default: 3322)
|
||||
|
||||
// IPFS storage configuration
|
||||
IPFS IPFSConfig `yaml:"ipfs"`
|
||||
}
|
||||
|
||||
// IPFSConfig contains IPFS storage configuration
|
||||
type IPFSConfig struct {
|
||||
// ClusterAPIURL is the IPFS Cluster HTTP API URL (e.g., "http://localhost:9094")
|
||||
// If empty, IPFS storage is disabled for this node
|
||||
ClusterAPIURL string `yaml:"cluster_api_url"`
|
||||
|
||||
// APIURL is the IPFS HTTP API URL for content retrieval (e.g., "http://localhost:5001")
|
||||
// If empty, defaults to "http://localhost:5001"
|
||||
APIURL string `yaml:"api_url"`
|
||||
|
||||
// Timeout for IPFS operations
|
||||
// If zero, defaults to 60 seconds
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
|
||||
// ReplicationFactor is the replication factor for pinned content
|
||||
// If zero, defaults to 3
|
||||
ReplicationFactor int `yaml:"replication_factor"`
|
||||
|
||||
// EnableEncryption enables client-side encryption before upload
|
||||
// Defaults to true
|
||||
EnableEncryption bool `yaml:"enable_encryption"`
|
||||
}
|
||||
13
pkg/config/discovery_config.go
Normal file
13
pkg/config/discovery_config.go
Normal file
@ -0,0 +1,13 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// DiscoveryConfig contains peer discovery configuration
|
||||
type DiscoveryConfig struct {
|
||||
BootstrapPeers []string `yaml:"bootstrap_peers"` // Peer addresses to connect to
|
||||
DiscoveryInterval time.Duration `yaml:"discovery_interval"` // Discovery announcement interval
|
||||
BootstrapPort int `yaml:"bootstrap_port"` // Default port for peer discovery
|
||||
HttpAdvAddress string `yaml:"http_adv_address"` // HTTP advertisement address
|
||||
RaftAdvAddress string `yaml:"raft_adv_address"` // Raft advertisement
|
||||
NodeNamespace string `yaml:"node_namespace"` // Namespace for node identifiers
|
||||
}
|
||||
62
pkg/config/gateway_config.go
Normal file
62
pkg/config/gateway_config.go
Normal file
@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// HTTPGatewayConfig contains HTTP reverse proxy gateway configuration
|
||||
type HTTPGatewayConfig struct {
|
||||
Enabled bool `yaml:"enabled"` // Enable HTTP gateway
|
||||
ListenAddr string `yaml:"listen_addr"` // Address to listen on (e.g., ":8080")
|
||||
NodeName string `yaml:"node_name"` // Node name for routing
|
||||
Routes map[string]RouteConfig `yaml:"routes"` // Service routes
|
||||
HTTPS HTTPSConfig `yaml:"https"` // HTTPS/TLS configuration
|
||||
SNI SNIConfig `yaml:"sni"` // SNI-based TCP routing configuration
|
||||
|
||||
// Full gateway configuration (for API, auth, pubsub)
|
||||
ClientNamespace string `yaml:"client_namespace"` // Namespace for network client
|
||||
RQLiteDSN string `yaml:"rqlite_dsn"` // RQLite database DSN
|
||||
OlricServers []string `yaml:"olric_servers"` // List of Olric server addresses
|
||||
OlricTimeout time.Duration `yaml:"olric_timeout"` // Timeout for Olric operations
|
||||
IPFSClusterAPIURL string `yaml:"ipfs_cluster_api_url"` // IPFS Cluster API URL
|
||||
IPFSAPIURL string `yaml:"ipfs_api_url"` // IPFS API URL
|
||||
IPFSTimeout time.Duration `yaml:"ipfs_timeout"` // Timeout for IPFS operations
|
||||
}
|
||||
|
||||
// HTTPSConfig contains HTTPS/TLS configuration for the gateway
|
||||
type HTTPSConfig struct {
|
||||
Enabled bool `yaml:"enabled"` // Enable HTTPS (port 443)
|
||||
Domain string `yaml:"domain"` // Primary domain (e.g., node-123.orama.network)
|
||||
AutoCert bool `yaml:"auto_cert"` // Use Let's Encrypt for automatic certificate
|
||||
UseSelfSigned bool `yaml:"use_self_signed"` // Use self-signed certificates (pre-generated)
|
||||
CertFile string `yaml:"cert_file"` // Path to certificate file (if not using auto_cert)
|
||||
KeyFile string `yaml:"key_file"` // Path to key file (if not using auto_cert)
|
||||
CacheDir string `yaml:"cache_dir"` // Directory for Let's Encrypt certificate cache
|
||||
HTTPPort int `yaml:"http_port"` // HTTP port for ACME challenge (default: 80)
|
||||
HTTPSPort int `yaml:"https_port"` // HTTPS port (default: 443)
|
||||
Email string `yaml:"email"` // Email for Let's Encrypt account
|
||||
}
|
||||
|
||||
// SNIConfig contains SNI-based TCP routing configuration for port 7001
|
||||
type SNIConfig struct {
|
||||
Enabled bool `yaml:"enabled"` // Enable SNI-based TCP routing
|
||||
ListenAddr string `yaml:"listen_addr"` // Address to listen on (e.g., ":7001")
|
||||
Routes map[string]string `yaml:"routes"` // SNI hostname -> backend address mapping
|
||||
CertFile string `yaml:"cert_file"` // Path to certificate file
|
||||
KeyFile string `yaml:"key_file"` // Path to key file
|
||||
}
|
||||
|
||||
// RouteConfig defines a single reverse proxy route
|
||||
type RouteConfig struct {
|
||||
PathPrefix string `yaml:"path_prefix"` // URL path prefix (e.g., "/rqlite/http")
|
||||
BackendURL string `yaml:"backend_url"` // Backend service URL
|
||||
Timeout time.Duration `yaml:"timeout"` // Request timeout
|
||||
WebSocket bool `yaml:"websocket"` // Support WebSocket upgrades
|
||||
}
|
||||
|
||||
// ClientConfig represents configuration for network clients
|
||||
type ClientConfig struct {
|
||||
AppName string `yaml:"app_name"`
|
||||
DatabaseName string `yaml:"database_name"`
|
||||
BootstrapPeers []string `yaml:"bootstrap_peers"`
|
||||
ConnectTimeout time.Duration `yaml:"connect_timeout"`
|
||||
RetryAttempts int `yaml:"retry_attempts"`
|
||||
}
|
||||
8
pkg/config/logging_config.go
Normal file
8
pkg/config/logging_config.go
Normal file
@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
// LoggingConfig contains logging configuration
|
||||
type LoggingConfig struct {
|
||||
Level string `yaml:"level"` // debug, info, warn, error
|
||||
Format string `yaml:"format"` // json, console
|
||||
OutputFile string `yaml:"output_file"` // Empty for stdout
|
||||
}
|
||||
10
pkg/config/node_config.go
Normal file
10
pkg/config/node_config.go
Normal file
@ -0,0 +1,10 @@
|
||||
package config
|
||||
|
||||
// NodeConfig contains node-specific configuration
|
||||
type NodeConfig struct {
|
||||
ID string `yaml:"id"` // Auto-generated if empty
|
||||
ListenAddresses []string `yaml:"listen_addresses"` // LibP2P listen addresses
|
||||
DataDir string `yaml:"data_dir"` // Data directory
|
||||
MaxConnections int `yaml:"max_connections"` // Maximum peer connections
|
||||
Domain string `yaml:"domain"` // Domain for this node (e.g., node-1.orama.network)
|
||||
}
|
||||
@ -6,13 +6,13 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ConfigDir returns the path to the DeBros config directory (~/.debros).
|
||||
// ConfigDir returns the path to the DeBros config directory (~/.orama).
|
||||
func ConfigDir() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to determine home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(home, ".debros"), nil
|
||||
return filepath.Join(home, ".orama"), nil
|
||||
}
|
||||
|
||||
// EnsureConfigDir creates the config directory if it does not exist.
|
||||
@ -28,8 +28,8 @@ func EnsureConfigDir() (string, error) {
|
||||
}
|
||||
|
||||
// DefaultPath returns the path to the config file for the given component name.
|
||||
// component should be e.g., "node.yaml", "bootstrap.yaml", "gateway.yaml"
|
||||
// It checks ~/.debros/data/, ~/.debros/configs/, and ~/.debros/ for backward compatibility.
|
||||
// component should be e.g., "node.yaml", "gateway.yaml"
|
||||
// It checks ~/.orama/data/, ~/.orama/configs/, and ~/.orama/ for backward compatibility.
|
||||
// If component is already an absolute path, it returns it as-is.
|
||||
func DefaultPath(component string) (string, error) {
|
||||
// If component is already an absolute path, return it directly
|
||||
@ -42,28 +42,35 @@ func DefaultPath(component string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var gatewayDefault string
|
||||
// For gateway.yaml, check data/ directory first (production location)
|
||||
if component == "gateway.yaml" {
|
||||
dataPath := filepath.Join(dir, "data", component)
|
||||
if _, err := os.Stat(dataPath); err == nil {
|
||||
return dataPath, nil
|
||||
}
|
||||
// Return data path as default for gateway.yaml (even if it doesn't exist yet)
|
||||
return dataPath, nil
|
||||
// Remember the preferred default so we can still fall back to legacy paths
|
||||
gatewayDefault = dataPath
|
||||
}
|
||||
|
||||
// First check in ~/.debros/configs/ (production installer location)
|
||||
// First check in ~/.orama/configs/ (production installer location)
|
||||
configsPath := filepath.Join(dir, "configs", component)
|
||||
if _, err := os.Stat(configsPath); err == nil {
|
||||
return configsPath, nil
|
||||
}
|
||||
|
||||
// Fallback to ~/.debros/ (legacy/development location)
|
||||
// Fallback to ~/.orama/ (legacy/development location)
|
||||
legacyPath := filepath.Join(dir, component)
|
||||
if _, err := os.Stat(legacyPath); err == nil {
|
||||
return legacyPath, nil
|
||||
}
|
||||
|
||||
if gatewayDefault != "" {
|
||||
// If we preferred the data path (gateway.yaml) but didn't find it anywhere else,
|
||||
// return the data path so error messages point to the production location.
|
||||
return gatewayDefault, nil
|
||||
}
|
||||
|
||||
// Return configs path as default (even if it doesn't exist yet)
|
||||
// This allows the error message to show the expected production location
|
||||
return configsPath, nil
|
||||
|
||||
8
pkg/config/security_config.go
Normal file
8
pkg/config/security_config.go
Normal file
@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
// SecurityConfig contains security-related configuration
|
||||
type SecurityConfig struct {
|
||||
EnableTLS bool `yaml:"enable_tls"`
|
||||
PrivateKeyFile string `yaml:"private_key_file"`
|
||||
CertificateFile string `yaml:"certificate_file"`
|
||||
}
|
||||
@ -1,638 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
manet "github.com/multiformats/go-multiaddr/net"
|
||||
)
|
||||
|
||||
// ValidationError represents a single validation error with context.
|
||||
type ValidationError struct {
|
||||
Path string // e.g., "discovery.bootstrap_peers[0]"
|
||||
Message string // e.g., "invalid multiaddr"
|
||||
Hint string // e.g., "expected /ip{4,6}/.../tcp/<port>/p2p/<peerID>"
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
if e.Hint != "" {
|
||||
return fmt.Sprintf("%s: %s; %s", e.Path, e.Message, e.Hint)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", e.Path, e.Message)
|
||||
}
|
||||
|
||||
// Validate performs comprehensive validation of the entire config.
|
||||
// It aggregates all errors and returns them, allowing the caller to print all issues at once.
|
||||
func (c *Config) Validate() []error {
|
||||
var errs []error
|
||||
|
||||
// Validate node config
|
||||
errs = append(errs, c.validateNode()...)
|
||||
// Validate database config
|
||||
errs = append(errs, c.validateDatabase()...)
|
||||
// Validate discovery config
|
||||
errs = append(errs, c.validateDiscovery()...)
|
||||
// Validate security config
|
||||
errs = append(errs, c.validateSecurity()...)
|
||||
// Validate logging config
|
||||
errs = append(errs, c.validateLogging()...)
|
||||
// Cross-field validations
|
||||
errs = append(errs, c.validateCrossFields()...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *Config) validateNode() []error {
|
||||
var errs []error
|
||||
nc := c.Node
|
||||
|
||||
// Validate node ID (required for RQLite cluster membership)
|
||||
if nc.ID == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.id",
|
||||
Message: "must not be empty (required for cluster membership)",
|
||||
Hint: "will be auto-generated if empty, but explicit ID recommended",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if nc.Type != "bootstrap" && nc.Type != "node" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.type",
|
||||
Message: fmt.Sprintf("must be one of [bootstrap node]; got %q", nc.Type),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate listen_addresses
|
||||
if len(nc.ListenAddresses) == 0 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.listen_addresses",
|
||||
Message: "must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for i, addr := range nc.ListenAddresses {
|
||||
path := fmt.Sprintf("node.listen_addresses[%d]", i)
|
||||
|
||||
// Parse as multiaddr
|
||||
ma, err := multiaddr.NewMultiaddr(addr)
|
||||
if err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("invalid multiaddr: %v", err),
|
||||
Hint: "expected /ip{4,6}/.../ tcp/<port>",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for TCP and valid port
|
||||
tcpAddr, err := manet.ToNetAddr(ma)
|
||||
if err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("cannot convert multiaddr to network address: %v", err),
|
||||
Hint: "ensure multiaddr contains /tcp/<port>",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
tcpPort := tcpAddr.(*net.TCPAddr).Port
|
||||
if tcpPort < 1 || tcpPort > 65535 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("invalid TCP port %d", tcpPort),
|
||||
Hint: "port must be between 1 and 65535",
|
||||
})
|
||||
}
|
||||
|
||||
if seen[addr] {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: "duplicate listen address",
|
||||
})
|
||||
}
|
||||
seen[addr] = true
|
||||
}
|
||||
|
||||
// Validate data_dir
|
||||
if nc.DataDir == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.data_dir",
|
||||
Message: "must not be empty",
|
||||
})
|
||||
} else {
|
||||
if err := validateDataDir(nc.DataDir); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.data_dir",
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate max_connections
|
||||
if nc.MaxConnections <= 0 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.max_connections",
|
||||
Message: fmt.Sprintf("must be > 0; got %d", nc.MaxConnections),
|
||||
})
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *Config) validateDatabase() []error {
|
||||
var errs []error
|
||||
dc := c.Database
|
||||
|
||||
// Validate data_dir
|
||||
if dc.DataDir == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.data_dir",
|
||||
Message: "must not be empty",
|
||||
})
|
||||
} else {
|
||||
if err := validateDataDir(dc.DataDir); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.data_dir",
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate replication_factor
|
||||
if dc.ReplicationFactor < 1 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.replication_factor",
|
||||
Message: fmt.Sprintf("must be >= 1; got %d", dc.ReplicationFactor),
|
||||
})
|
||||
} else if dc.ReplicationFactor%2 == 0 {
|
||||
// Warn about even replication factor (Raft best practice: odd)
|
||||
// For now we log a note but don't error
|
||||
_ = fmt.Sprintf("note: database.replication_factor %d is even; Raft recommends odd numbers for quorum", dc.ReplicationFactor)
|
||||
}
|
||||
|
||||
// Validate shard_count
|
||||
if dc.ShardCount < 1 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.shard_count",
|
||||
Message: fmt.Sprintf("must be >= 1; got %d", dc.ShardCount),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate max_database_size
|
||||
if dc.MaxDatabaseSize < 0 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.max_database_size",
|
||||
Message: fmt.Sprintf("must be >= 0; got %d", dc.MaxDatabaseSize),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate rqlite_port
|
||||
if dc.RQLitePort < 1 || dc.RQLitePort > 65535 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_port",
|
||||
Message: fmt.Sprintf("must be between 1 and 65535; got %d", dc.RQLitePort),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate rqlite_raft_port
|
||||
if dc.RQLiteRaftPort < 1 || dc.RQLiteRaftPort > 65535 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_raft_port",
|
||||
Message: fmt.Sprintf("must be between 1 and 65535; got %d", dc.RQLiteRaftPort),
|
||||
})
|
||||
}
|
||||
|
||||
// Ports must differ
|
||||
if dc.RQLitePort == dc.RQLiteRaftPort {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_raft_port",
|
||||
Message: fmt.Sprintf("must differ from database.rqlite_port (%d)", dc.RQLitePort),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate rqlite_join_address context-dependently
|
||||
if c.Node.Type == "node" {
|
||||
if dc.RQLiteJoinAddress == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_join_address",
|
||||
Message: "required for node type (non-bootstrap)",
|
||||
})
|
||||
} else {
|
||||
if err := validateHostPort(dc.RQLiteJoinAddress); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_join_address",
|
||||
Message: err.Error(),
|
||||
Hint: "expected format: host:port",
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if c.Node.Type == "bootstrap" {
|
||||
// Bootstrap nodes can optionally join another bootstrap's RQLite cluster
|
||||
// This allows secondary bootstraps to synchronize with the primary
|
||||
if dc.RQLiteJoinAddress != "" {
|
||||
if err := validateHostPort(dc.RQLiteJoinAddress); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_join_address",
|
||||
Message: err.Error(),
|
||||
Hint: "expected format: host:port",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate cluster_sync_interval
|
||||
if dc.ClusterSyncInterval != 0 && dc.ClusterSyncInterval < 10*time.Second {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.cluster_sync_interval",
|
||||
Message: fmt.Sprintf("must be >= 10s or 0 (for default); got %v", dc.ClusterSyncInterval),
|
||||
Hint: "recommended: 30s",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate peer_inactivity_limit
|
||||
if dc.PeerInactivityLimit != 0 {
|
||||
if dc.PeerInactivityLimit < time.Hour {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.peer_inactivity_limit",
|
||||
Message: fmt.Sprintf("must be >= 1h or 0 (for default); got %v", dc.PeerInactivityLimit),
|
||||
Hint: "recommended: 24h",
|
||||
})
|
||||
} else if dc.PeerInactivityLimit > 7*24*time.Hour {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.peer_inactivity_limit",
|
||||
Message: fmt.Sprintf("must be <= 7d; got %v", dc.PeerInactivityLimit),
|
||||
Hint: "recommended: 24h",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate min_cluster_size
|
||||
if dc.MinClusterSize < 1 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.min_cluster_size",
|
||||
Message: fmt.Sprintf("must be >= 1; got %d", dc.MinClusterSize),
|
||||
})
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *Config) validateDiscovery() []error {
|
||||
var errs []error
|
||||
disc := c.Discovery
|
||||
|
||||
// Validate discovery_interval
|
||||
if disc.DiscoveryInterval <= 0 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.discovery_interval",
|
||||
Message: fmt.Sprintf("must be > 0; got %v", disc.DiscoveryInterval),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate bootstrap_port
|
||||
if disc.BootstrapPort < 1 || disc.BootstrapPort > 65535 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.bootstrap_port",
|
||||
Message: fmt.Sprintf("must be between 1 and 65535; got %d", disc.BootstrapPort),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate bootstrap_peers context-dependently
|
||||
if c.Node.Type == "node" {
|
||||
if len(disc.BootstrapPeers) == 0 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.bootstrap_peers",
|
||||
Message: "required for node type (must not be empty)",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate each bootstrap peer multiaddr
|
||||
seenPeers := make(map[string]bool)
|
||||
for i, peer := range disc.BootstrapPeers {
|
||||
path := fmt.Sprintf("discovery.bootstrap_peers[%d]", i)
|
||||
|
||||
_, err := multiaddr.NewMultiaddr(peer)
|
||||
if err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("invalid multiaddr: %v", err),
|
||||
Hint: "expected /ip{4,6}/.../tcp/<port>/p2p/<peerID>",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for /p2p/ component
|
||||
if !strings.Contains(peer, "/p2p/") {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: "missing /p2p/<peerID> component",
|
||||
Hint: "expected /ip{4,6}/.../tcp/<port>/p2p/<peerID>",
|
||||
})
|
||||
}
|
||||
|
||||
// Extract TCP port by parsing the multiaddr string directly
|
||||
// Look for /tcp/ in the peer string
|
||||
tcpPortStr := extractTCPPort(peer)
|
||||
if tcpPortStr == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: "missing /tcp/<port> component",
|
||||
Hint: "expected /ip{4,6}/.../tcp/<port>/p2p/<peerID>",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
tcpPort, err := strconv.Atoi(tcpPortStr)
|
||||
if err != nil || tcpPort < 1 || tcpPort > 65535 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("invalid TCP port %s", tcpPortStr),
|
||||
Hint: "port must be between 1 and 65535",
|
||||
})
|
||||
}
|
||||
|
||||
if seenPeers[peer] {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: "duplicate bootstrap peer",
|
||||
})
|
||||
}
|
||||
seenPeers[peer] = true
|
||||
}
|
||||
|
||||
// Validate http_adv_address (required for cluster discovery)
|
||||
if disc.HttpAdvAddress == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.http_adv_address",
|
||||
Message: "required for RQLite cluster discovery",
|
||||
Hint: "set to your public HTTP address (e.g., 51.83.128.181:5001)",
|
||||
})
|
||||
} else {
|
||||
if err := validateHostOrHostPort(disc.HttpAdvAddress); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.http_adv_address",
|
||||
Message: err.Error(),
|
||||
Hint: "expected format: host or host:port",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate raft_adv_address (required for cluster discovery)
|
||||
if disc.RaftAdvAddress == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.raft_adv_address",
|
||||
Message: "required for RQLite cluster discovery",
|
||||
Hint: "set to your public Raft address (e.g., 51.83.128.181:7001)",
|
||||
})
|
||||
} else {
|
||||
if err := validateHostOrHostPort(disc.RaftAdvAddress); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.raft_adv_address",
|
||||
Message: err.Error(),
|
||||
Hint: "expected format: host or host:port",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *Config) validateSecurity() []error {
|
||||
var errs []error
|
||||
sec := c.Security
|
||||
|
||||
// Validate logging level
|
||||
if sec.EnableTLS {
|
||||
if sec.PrivateKeyFile == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "security.private_key_file",
|
||||
Message: "required when enable_tls is true",
|
||||
})
|
||||
} else {
|
||||
if err := validateFileReadable(sec.PrivateKeyFile); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "security.private_key_file",
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if sec.CertificateFile == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "security.certificate_file",
|
||||
Message: "required when enable_tls is true",
|
||||
})
|
||||
} else {
|
||||
if err := validateFileReadable(sec.CertificateFile); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "security.certificate_file",
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *Config) validateLogging() []error {
|
||||
var errs []error
|
||||
log := c.Logging
|
||||
|
||||
// Validate level
|
||||
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
|
||||
if !validLevels[log.Level] {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "logging.level",
|
||||
Message: fmt.Sprintf("invalid value %q", log.Level),
|
||||
Hint: "allowed values: debug, info, warn, error",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate format
|
||||
validFormats := map[string]bool{"json": true, "console": true}
|
||||
if !validFormats[log.Format] {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "logging.format",
|
||||
Message: fmt.Sprintf("invalid value %q", log.Format),
|
||||
Hint: "allowed values: json, console",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate output_file
|
||||
if log.OutputFile != "" {
|
||||
dir := filepath.Dir(log.OutputFile)
|
||||
if dir != "" && dir != "." {
|
||||
if err := validateDirWritable(dir); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "logging.output_file",
|
||||
Message: fmt.Sprintf("parent directory not writable: %v", err),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *Config) validateCrossFields() []error {
|
||||
var errs []error
|
||||
|
||||
// If node.type is invalid, don't run cross-checks
|
||||
if c.Node.Type != "bootstrap" && c.Node.Type != "node" {
|
||||
return errs
|
||||
}
|
||||
|
||||
// Cross-check rqlite_join_address vs node type
|
||||
// Note: Bootstrap nodes can optionally join another bootstrap's cluster
|
||||
|
||||
if c.Node.Type == "node" && c.Database.RQLiteJoinAddress == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_join_address",
|
||||
Message: "required for non-bootstrap node type",
|
||||
})
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// Helper validation functions
|
||||
|
||||
func validateDataDir(path string) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("must not be empty")
|
||||
}
|
||||
|
||||
// Expand ~ to home directory
|
||||
expandedPath := os.ExpandEnv(path)
|
||||
if strings.HasPrefix(expandedPath, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine home directory: %v", err)
|
||||
}
|
||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
||||
}
|
||||
|
||||
if info, err := os.Stat(expandedPath); err == nil {
|
||||
// Directory exists; check if it's a directory and writable
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("path exists but is not a directory")
|
||||
}
|
||||
// Try to write a test file to check permissions
|
||||
testFile := filepath.Join(expandedPath, ".write_test")
|
||||
if err := os.WriteFile(testFile, []byte(""), 0644); err != nil {
|
||||
return fmt.Errorf("directory not writable: %v", err)
|
||||
}
|
||||
os.Remove(testFile)
|
||||
} else if os.IsNotExist(err) {
|
||||
// Directory doesn't exist; check if parent is writable
|
||||
parent := filepath.Dir(expandedPath)
|
||||
if parent == "" || parent == "." {
|
||||
parent = "."
|
||||
}
|
||||
// Allow parent not existing - it will be created at runtime
|
||||
if info, err := os.Stat(parent); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("parent directory not accessible: %v", err)
|
||||
}
|
||||
// Parent doesn't exist either - that's ok, will be created
|
||||
} else if !info.IsDir() {
|
||||
return fmt.Errorf("parent path is not a directory")
|
||||
} else {
|
||||
// Parent exists, check if writable
|
||||
if err := validateDirWritable(parent); err != nil {
|
||||
return fmt.Errorf("parent directory not writable: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("cannot access path: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDirWritable(path string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot access directory: %v", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("path is not a directory")
|
||||
}
|
||||
|
||||
// Try to write a test file
|
||||
testFile := filepath.Join(path, ".write_test")
|
||||
if err := os.WriteFile(testFile, []byte(""), 0644); err != nil {
|
||||
return fmt.Errorf("directory not writable: %v", err)
|
||||
}
|
||||
os.Remove(testFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateFileReadable(path string) error {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read file: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateHostPort(hostPort string) error {
|
||||
parts := strings.Split(hostPort, ":")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("expected format host:port")
|
||||
}
|
||||
|
||||
host := parts[0]
|
||||
port := parts[1]
|
||||
|
||||
if host == "" {
|
||||
return fmt.Errorf("host must not be empty")
|
||||
}
|
||||
|
||||
portNum, err := strconv.Atoi(port)
|
||||
if err != nil || portNum < 1 || portNum > 65535 {
|
||||
return fmt.Errorf("port must be a number between 1 and 65535; got %q", port)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateHostOrHostPort(addr string) error {
|
||||
// Try to parse as host:port first
|
||||
if strings.Contains(addr, ":") {
|
||||
return validateHostPort(addr)
|
||||
}
|
||||
|
||||
// Otherwise just check if it's a valid hostname/IP
|
||||
if addr == "" {
|
||||
return fmt.Errorf("address must not be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractTCPPort(multiaddrStr string) string {
|
||||
// Look for the /tcp/ protocol code
|
||||
parts := strings.Split(multiaddrStr, "/")
|
||||
for i := 0; i < len(parts); i++ {
|
||||
if parts[i] == "tcp" {
|
||||
// The port is the next part
|
||||
if i+1 < len(parts) {
|
||||
return parts[i+1]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
140
pkg/config/validate/database.go
Normal file
140
pkg/config/validate/database.go
Normal file
@ -0,0 +1,140 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DatabaseConfig represents the database configuration for validation purposes.
|
||||
type DatabaseConfig struct {
|
||||
DataDir string
|
||||
ReplicationFactor int
|
||||
ShardCount int
|
||||
MaxDatabaseSize int64
|
||||
RQLitePort int
|
||||
RQLiteRaftPort int
|
||||
RQLiteJoinAddress string
|
||||
ClusterSyncInterval time.Duration
|
||||
PeerInactivityLimit time.Duration
|
||||
MinClusterSize int
|
||||
}
|
||||
|
||||
// ValidateDatabase performs validation of the database configuration.
|
||||
func ValidateDatabase(dc DatabaseConfig) []error {
|
||||
var errs []error
|
||||
|
||||
// Validate data_dir
|
||||
if dc.DataDir == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.data_dir",
|
||||
Message: "must not be empty",
|
||||
})
|
||||
} else {
|
||||
if err := ValidateDataDir(dc.DataDir); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.data_dir",
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate replication_factor
|
||||
if dc.ReplicationFactor < 1 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.replication_factor",
|
||||
Message: fmt.Sprintf("must be >= 1; got %d", dc.ReplicationFactor),
|
||||
})
|
||||
} else if dc.ReplicationFactor%2 == 0 {
|
||||
// Warn about even replication factor (Raft best practice: odd)
|
||||
// For now we log a note but don't error
|
||||
_ = fmt.Sprintf("note: database.replication_factor %d is even; Raft recommends odd numbers for quorum", dc.ReplicationFactor)
|
||||
}
|
||||
|
||||
// Validate shard_count
|
||||
if dc.ShardCount < 1 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.shard_count",
|
||||
Message: fmt.Sprintf("must be >= 1; got %d", dc.ShardCount),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate max_database_size
|
||||
if dc.MaxDatabaseSize < 0 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.max_database_size",
|
||||
Message: fmt.Sprintf("must be >= 0; got %d", dc.MaxDatabaseSize),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate rqlite_port
|
||||
if dc.RQLitePort < 1 || dc.RQLitePort > 65535 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_port",
|
||||
Message: fmt.Sprintf("must be between 1 and 65535; got %d", dc.RQLitePort),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate rqlite_raft_port
|
||||
if dc.RQLiteRaftPort < 1 || dc.RQLiteRaftPort > 65535 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_raft_port",
|
||||
Message: fmt.Sprintf("must be between 1 and 65535; got %d", dc.RQLiteRaftPort),
|
||||
})
|
||||
}
|
||||
|
||||
// Ports must differ
|
||||
if dc.RQLitePort == dc.RQLiteRaftPort {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_raft_port",
|
||||
Message: fmt.Sprintf("must differ from database.rqlite_port (%d)", dc.RQLitePort),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate rqlite_join_address format if provided (optional for all nodes)
|
||||
// The first node in a cluster won't have a join address; subsequent nodes will
|
||||
if dc.RQLiteJoinAddress != "" {
|
||||
if err := ValidateHostPort(dc.RQLiteJoinAddress); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.rqlite_join_address",
|
||||
Message: err.Error(),
|
||||
Hint: "expected format: host:port",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate cluster_sync_interval
|
||||
if dc.ClusterSyncInterval != 0 && dc.ClusterSyncInterval < 10*time.Second {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.cluster_sync_interval",
|
||||
Message: fmt.Sprintf("must be >= 10s or 0 (for default); got %v", dc.ClusterSyncInterval),
|
||||
Hint: "recommended: 30s",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate peer_inactivity_limit
|
||||
if dc.PeerInactivityLimit != 0 {
|
||||
if dc.PeerInactivityLimit < time.Hour {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.peer_inactivity_limit",
|
||||
Message: fmt.Sprintf("must be >= 1h or 0 (for default); got %v", dc.PeerInactivityLimit),
|
||||
Hint: "recommended: 24h",
|
||||
})
|
||||
} else if dc.PeerInactivityLimit > 7*24*time.Hour {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.peer_inactivity_limit",
|
||||
Message: fmt.Sprintf("must be <= 7d; got %v", dc.PeerInactivityLimit),
|
||||
Hint: "recommended: 24h",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate min_cluster_size
|
||||
if dc.MinClusterSize < 1 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "database.min_cluster_size",
|
||||
Message: fmt.Sprintf("must be >= 1; got %d", dc.MinClusterSize),
|
||||
})
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
131
pkg/config/validate/discovery.go
Normal file
131
pkg/config/validate/discovery.go
Normal file
@ -0,0 +1,131 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
)
|
||||
|
||||
// DiscoveryConfig represents the discovery configuration for validation purposes.
|
||||
type DiscoveryConfig struct {
|
||||
BootstrapPeers []string
|
||||
DiscoveryInterval time.Duration
|
||||
BootstrapPort int
|
||||
HttpAdvAddress string
|
||||
RaftAdvAddress string
|
||||
}
|
||||
|
||||
// ValidateDiscovery performs validation of the discovery configuration.
|
||||
func ValidateDiscovery(disc DiscoveryConfig) []error {
|
||||
var errs []error
|
||||
|
||||
// Validate discovery_interval
|
||||
if disc.DiscoveryInterval <= 0 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.discovery_interval",
|
||||
Message: fmt.Sprintf("must be > 0; got %v", disc.DiscoveryInterval),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate peer discovery port
|
||||
if disc.BootstrapPort < 1 || disc.BootstrapPort > 65535 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.bootstrap_port",
|
||||
Message: fmt.Sprintf("must be between 1 and 65535; got %d", disc.BootstrapPort),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate peer addresses (optional - all nodes are unified peers now)
|
||||
// Validate each peer multiaddr
|
||||
seenPeers := make(map[string]bool)
|
||||
for i, peer := range disc.BootstrapPeers {
|
||||
path := fmt.Sprintf("discovery.bootstrap_peers[%d]", i)
|
||||
|
||||
_, err := multiaddr.NewMultiaddr(peer)
|
||||
if err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("invalid multiaddr: %v", err),
|
||||
Hint: "expected /ip{4,6}/.../tcp/<port>/p2p/<peerID>",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for /p2p/ component
|
||||
if !strings.Contains(peer, "/p2p/") {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: "missing /p2p/<peerID> component",
|
||||
Hint: "expected /ip{4,6}/.../tcp/<port>/p2p/<peerID>",
|
||||
})
|
||||
}
|
||||
|
||||
// Extract TCP port by parsing the multiaddr string directly
|
||||
// Look for /tcp/ in the peer string
|
||||
tcpPortStr := ExtractTCPPort(peer)
|
||||
if tcpPortStr == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: "missing /tcp/<port> component",
|
||||
Hint: "expected /ip{4,6}/.../tcp/<port>/p2p/<peerID>",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
tcpPort, err := strconv.Atoi(tcpPortStr)
|
||||
if err != nil || tcpPort < 1 || tcpPort > 65535 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("invalid TCP port %s", tcpPortStr),
|
||||
Hint: "port must be between 1 and 65535",
|
||||
})
|
||||
}
|
||||
|
||||
if seenPeers[peer] {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: "duplicate peer",
|
||||
})
|
||||
}
|
||||
seenPeers[peer] = true
|
||||
}
|
||||
|
||||
// Validate http_adv_address (required for cluster discovery)
|
||||
if disc.HttpAdvAddress == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.http_adv_address",
|
||||
Message: "required for RQLite cluster discovery",
|
||||
Hint: "set to your public HTTP address (e.g., 51.83.128.181:5001)",
|
||||
})
|
||||
} else {
|
||||
if err := ValidateHostOrHostPort(disc.HttpAdvAddress); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.http_adv_address",
|
||||
Message: err.Error(),
|
||||
Hint: "expected format: host or host:port",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate raft_adv_address (required for cluster discovery)
|
||||
if disc.RaftAdvAddress == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.raft_adv_address",
|
||||
Message: "required for RQLite cluster discovery",
|
||||
Hint: "set to your public Raft address (e.g., 51.83.128.181:7001)",
|
||||
})
|
||||
} else {
|
||||
if err := ValidateHostOrHostPort(disc.RaftAdvAddress); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "discovery.raft_adv_address",
|
||||
Message: err.Error(),
|
||||
Hint: "expected format: host or host:port",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
53
pkg/config/validate/logging.go
Normal file
53
pkg/config/validate/logging.go
Normal file
@ -0,0 +1,53 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// LoggingConfig represents the logging configuration for validation purposes.
|
||||
type LoggingConfig struct {
|
||||
Level string
|
||||
Format string
|
||||
OutputFile string
|
||||
}
|
||||
|
||||
// ValidateLogging performs validation of the logging configuration.
|
||||
func ValidateLogging(log LoggingConfig) []error {
|
||||
var errs []error
|
||||
|
||||
// Validate level
|
||||
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
|
||||
if !validLevels[log.Level] {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "logging.level",
|
||||
Message: fmt.Sprintf("invalid value %q", log.Level),
|
||||
Hint: "allowed values: debug, info, warn, error",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate format
|
||||
validFormats := map[string]bool{"json": true, "console": true}
|
||||
if !validFormats[log.Format] {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "logging.format",
|
||||
Message: fmt.Sprintf("invalid value %q", log.Format),
|
||||
Hint: "allowed values: json, console",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate output_file
|
||||
if log.OutputFile != "" {
|
||||
dir := filepath.Dir(log.OutputFile)
|
||||
if dir != "" && dir != "." {
|
||||
if err := ValidateDirWritable(dir); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "logging.output_file",
|
||||
Message: fmt.Sprintf("parent directory not writable: %v", err),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
108
pkg/config/validate/node.go
Normal file
108
pkg/config/validate/node.go
Normal file
@ -0,0 +1,108 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
manet "github.com/multiformats/go-multiaddr/net"
|
||||
)
|
||||
|
||||
// NodeConfig represents the node configuration for validation purposes.
|
||||
type NodeConfig struct {
|
||||
ID string
|
||||
ListenAddresses []string
|
||||
DataDir string
|
||||
MaxConnections int
|
||||
}
|
||||
|
||||
// ValidateNode performs validation of the node configuration.
|
||||
func ValidateNode(nc NodeConfig) []error {
|
||||
var errs []error
|
||||
|
||||
// Validate node ID (required for RQLite cluster membership)
|
||||
if nc.ID == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.id",
|
||||
Message: "must not be empty (required for cluster membership)",
|
||||
Hint: "will be auto-generated if empty, but explicit ID recommended",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate listen_addresses
|
||||
if len(nc.ListenAddresses) == 0 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.listen_addresses",
|
||||
Message: "must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for i, addr := range nc.ListenAddresses {
|
||||
path := fmt.Sprintf("node.listen_addresses[%d]", i)
|
||||
|
||||
// Parse as multiaddr
|
||||
ma, err := multiaddr.NewMultiaddr(addr)
|
||||
if err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("invalid multiaddr: %v", err),
|
||||
Hint: "expected /ip{4,6}/.../tcp/<port>",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for TCP and valid port
|
||||
tcpAddr, err := manet.ToNetAddr(ma)
|
||||
if err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("cannot convert multiaddr to network address: %v", err),
|
||||
Hint: "ensure multiaddr contains /tcp/<port>",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
tcpPort := tcpAddr.(*net.TCPAddr).Port
|
||||
if tcpPort < 1 || tcpPort > 65535 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("invalid TCP port %d", tcpPort),
|
||||
Hint: "port must be between 1 and 65535",
|
||||
})
|
||||
}
|
||||
|
||||
if seen[addr] {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: path,
|
||||
Message: "duplicate listen address",
|
||||
})
|
||||
}
|
||||
seen[addr] = true
|
||||
}
|
||||
|
||||
// Validate data_dir
|
||||
if nc.DataDir == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.data_dir",
|
||||
Message: "must not be empty",
|
||||
})
|
||||
} else {
|
||||
if err := ValidateDataDir(nc.DataDir); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.data_dir",
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate max_connections
|
||||
if nc.MaxConnections <= 0 {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "node.max_connections",
|
||||
Message: fmt.Sprintf("must be > 0; got %d", nc.MaxConnections),
|
||||
})
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
46
pkg/config/validate/security.go
Normal file
46
pkg/config/validate/security.go
Normal file
@ -0,0 +1,46 @@
|
||||
package validate
|
||||
|
||||
// SecurityConfig represents the security configuration for validation purposes.
|
||||
type SecurityConfig struct {
|
||||
EnableTLS bool
|
||||
PrivateKeyFile string
|
||||
CertificateFile string
|
||||
}
|
||||
|
||||
// ValidateSecurity performs validation of the security configuration.
|
||||
func ValidateSecurity(sec SecurityConfig) []error {
|
||||
var errs []error
|
||||
|
||||
// Validate logging level
|
||||
if sec.EnableTLS {
|
||||
if sec.PrivateKeyFile == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "security.private_key_file",
|
||||
Message: "required when enable_tls is true",
|
||||
})
|
||||
} else {
|
||||
if err := ValidateFileReadable(sec.PrivateKeyFile); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "security.private_key_file",
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if sec.CertificateFile == "" {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "security.certificate_file",
|
||||
Message: "required when enable_tls is true",
|
||||
})
|
||||
} else {
|
||||
if err := ValidateFileReadable(sec.CertificateFile); err != nil {
|
||||
errs = append(errs, ValidationError{
|
||||
Path: "security.certificate_file",
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
180
pkg/config/validate/validators.go
Normal file
180
pkg/config/validate/validators.go
Normal file
@ -0,0 +1,180 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidationError represents a single validation error with context.
|
||||
type ValidationError struct {
|
||||
Path string // e.g., "discovery.bootstrap_peers[0]" or "discovery.peers[0]"
|
||||
Message string // e.g., "invalid multiaddr"
|
||||
Hint string // e.g., "expected /ip{4,6}/.../tcp/<port>/p2p/<peerID>"
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
if e.Hint != "" {
|
||||
return fmt.Sprintf("%s: %s; %s", e.Path, e.Message, e.Hint)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", e.Path, e.Message)
|
||||
}
|
||||
|
||||
// ValidateDataDir validates that a data directory exists or can be created.
|
||||
func ValidateDataDir(path string) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("must not be empty")
|
||||
}
|
||||
|
||||
// Expand ~ to home directory
|
||||
expandedPath := os.ExpandEnv(path)
|
||||
if strings.HasPrefix(expandedPath, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine home directory: %v", err)
|
||||
}
|
||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
||||
}
|
||||
|
||||
if info, err := os.Stat(expandedPath); err == nil {
|
||||
// Directory exists; check if it's a directory and writable
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("path exists but is not a directory")
|
||||
}
|
||||
// Try to write a test file to check permissions
|
||||
testFile := filepath.Join(expandedPath, ".write_test")
|
||||
if err := os.WriteFile(testFile, []byte(""), 0644); err != nil {
|
||||
return fmt.Errorf("directory not writable: %v", err)
|
||||
}
|
||||
os.Remove(testFile)
|
||||
} else if os.IsNotExist(err) {
|
||||
// Directory doesn't exist; check if parent is writable
|
||||
parent := filepath.Dir(expandedPath)
|
||||
if parent == "" || parent == "." {
|
||||
parent = "."
|
||||
}
|
||||
// Allow parent not existing - it will be created at runtime
|
||||
if info, err := os.Stat(parent); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("parent directory not accessible: %v", err)
|
||||
}
|
||||
// Parent doesn't exist either - that's ok, will be created
|
||||
} else if !info.IsDir() {
|
||||
return fmt.Errorf("parent path is not a directory")
|
||||
} else {
|
||||
// Parent exists, check if writable
|
||||
if err := ValidateDirWritable(parent); err != nil {
|
||||
return fmt.Errorf("parent directory not writable: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("cannot access path: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDirWritable validates that a directory exists and is writable.
|
||||
func ValidateDirWritable(path string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot access directory: %v", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("path is not a directory")
|
||||
}
|
||||
|
||||
// Try to write a test file
|
||||
testFile := filepath.Join(path, ".write_test")
|
||||
if err := os.WriteFile(testFile, []byte(""), 0644); err != nil {
|
||||
return fmt.Errorf("directory not writable: %v", err)
|
||||
}
|
||||
os.Remove(testFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateFileReadable validates that a file exists and is readable.
|
||||
func ValidateFileReadable(path string) error {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read file: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateHostPort validates a host:port address format.
|
||||
func ValidateHostPort(hostPort string) error {
|
||||
parts := strings.Split(hostPort, ":")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("expected format host:port")
|
||||
}
|
||||
|
||||
host := parts[0]
|
||||
port := parts[1]
|
||||
|
||||
if host == "" {
|
||||
return fmt.Errorf("host must not be empty")
|
||||
}
|
||||
|
||||
portNum, err := strconv.Atoi(port)
|
||||
if err != nil || portNum < 1 || portNum > 65535 {
|
||||
return fmt.Errorf("port must be a number between 1 and 65535; got %q", port)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateHostOrHostPort validates either a hostname or host:port format.
|
||||
func ValidateHostOrHostPort(addr string) error {
|
||||
// Try to parse as host:port first
|
||||
if strings.Contains(addr, ":") {
|
||||
return ValidateHostPort(addr)
|
||||
}
|
||||
|
||||
// Otherwise just check if it's a valid hostname/IP
|
||||
if addr == "" {
|
||||
return fmt.Errorf("address must not be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePort validates that a port number is in the valid range.
|
||||
func ValidatePort(port int) error {
|
||||
if port < 1 || port > 65535 {
|
||||
return fmt.Errorf("port must be between 1 and 65535; got %d", port)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractTCPPort extracts the TCP port from a multiaddr string.
|
||||
func ExtractTCPPort(multiaddrStr string) string {
|
||||
// Look for the /tcp/ protocol code
|
||||
parts := strings.Split(multiaddrStr, "/")
|
||||
for i := 0; i < len(parts); i++ {
|
||||
if parts[i] == "tcp" {
|
||||
// The port is the next part
|
||||
if i+1 < len(parts) {
|
||||
return parts[i+1]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ValidateSwarmKey validates that a swarm key is 64 hex characters.
|
||||
func ValidateSwarmKey(key string) error {
|
||||
key = strings.TrimSpace(key)
|
||||
if len(key) != 64 {
|
||||
return fmt.Errorf("swarm key must be 64 hex characters (32 bytes), got %d", len(key))
|
||||
}
|
||||
if _, err := hex.DecodeString(key); err != nil {
|
||||
return fmt.Errorf("swarm key must be valid hexadecimal: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -5,12 +5,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// validConfigForType returns a valid config for the given node type
|
||||
func validConfigForType(nodeType string) *Config {
|
||||
// validConfigForNode returns a valid config
|
||||
func validConfigForNode() *Config {
|
||||
validPeer := "/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{
|
||||
Type: nodeType,
|
||||
ID: "test-node-id",
|
||||
ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"},
|
||||
DataDir: ".",
|
||||
@ -25,6 +24,7 @@ func validConfigForType(nodeType string) *Config {
|
||||
RQLitePort: 5001,
|
||||
RQLiteRaftPort: 7001,
|
||||
MinClusterSize: 1,
|
||||
RQLiteJoinAddress: "", // Optional - first node creates cluster, others join
|
||||
},
|
||||
Discovery: DiscoveryConfig{
|
||||
BootstrapPeers: []string{validPeer},
|
||||
@ -40,51 +40,9 @@ func validConfigForType(nodeType string) *Config {
|
||||
},
|
||||
}
|
||||
|
||||
// Set rqlite_join_address based on node type
|
||||
if nodeType == "node" {
|
||||
cfg.Database.RQLiteJoinAddress = "localhost:5001"
|
||||
// Node type requires bootstrap peers
|
||||
cfg.Discovery.BootstrapPeers = []string{validPeer}
|
||||
} else {
|
||||
// Bootstrap type: empty join address and peers optional
|
||||
cfg.Database.RQLiteJoinAddress = ""
|
||||
cfg.Discovery.BootstrapPeers = []string{}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func TestValidateNodeType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
nodeType string
|
||||
shouldError bool
|
||||
}{
|
||||
{"bootstrap", "bootstrap", false},
|
||||
{"node", "node", false},
|
||||
{"invalid", "invalid-type", true},
|
||||
{"empty", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType("bootstrap") // Start with valid bootstrap
|
||||
if tt.nodeType == "node" {
|
||||
cfg = validConfigForType("node")
|
||||
} else {
|
||||
cfg.Node.Type = tt.nodeType
|
||||
}
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
}
|
||||
if !tt.shouldError && len(errs) > 0 {
|
||||
t.Errorf("unexpected errors: %v", errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateListenAddresses(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -102,7 +60,7 @@ func TestValidateListenAddresses(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType("node")
|
||||
cfg := validConfigForNode()
|
||||
cfg.Node.ListenAddresses = tt.addresses
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
@ -130,7 +88,7 @@ func TestValidateReplicationFactor(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType("node")
|
||||
cfg := validConfigForNode()
|
||||
cfg.Database.ReplicationFactor = tt.replication
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
@ -160,7 +118,7 @@ func TestValidateRQLitePorts(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType("node")
|
||||
cfg := validConfigForNode()
|
||||
cfg.Database.RQLitePort = tt.httpPort
|
||||
cfg.Database.RQLiteRaftPort = tt.raftPort
|
||||
errs := cfg.Validate()
|
||||
@ -177,21 +135,18 @@ func TestValidateRQLitePorts(t *testing.T) {
|
||||
func TestValidateRQLiteJoinAddress(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
nodeType string
|
||||
joinAddr string
|
||||
shouldError bool
|
||||
}{
|
||||
{"node with join", "node", "localhost:5001", false},
|
||||
{"node without join", "node", "", true},
|
||||
{"bootstrap with join", "bootstrap", "localhost:5001", false},
|
||||
{"bootstrap without join", "bootstrap", "", false},
|
||||
{"invalid join format", "node", "localhost", true},
|
||||
{"invalid join port", "node", "localhost:99999", true},
|
||||
{"node with join", "localhost:5001", false},
|
||||
{"node without join", "", false}, // Join address is optional (first node creates cluster)
|
||||
{"invalid join format", "localhost", true},
|
||||
{"invalid join port", "localhost:99999", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType(tt.nodeType)
|
||||
cfg := validConfigForNode()
|
||||
cfg.Database.RQLiteJoinAddress = tt.joinAddr
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
@ -204,27 +159,24 @@ func TestValidateRQLiteJoinAddress(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBootstrapPeers(t *testing.T) {
|
||||
func TestValidatePeerAddresses(t *testing.T) {
|
||||
validPeer := "/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"
|
||||
tests := []struct {
|
||||
name string
|
||||
nodeType string
|
||||
peers []string
|
||||
shouldError bool
|
||||
}{
|
||||
{"node with peer", "node", []string{validPeer}, false},
|
||||
{"node without peer", "node", []string{}, true},
|
||||
{"bootstrap with peer", "bootstrap", []string{validPeer}, false},
|
||||
{"bootstrap without peer", "bootstrap", []string{}, false},
|
||||
{"invalid multiaddr", "node", []string{"invalid"}, true},
|
||||
{"missing p2p", "node", []string{"/ip4/127.0.0.1/tcp/4001"}, true},
|
||||
{"duplicate peer", "node", []string{validPeer, validPeer}, true},
|
||||
{"invalid port", "node", []string{"/ip4/127.0.0.1/tcp/99999/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, true},
|
||||
{"node with peer", []string{validPeer}, false},
|
||||
{"node without peer", []string{}, false}, // All nodes are unified peers - bootstrap peers optional
|
||||
{"invalid multiaddr", []string{"invalid"}, true},
|
||||
{"missing p2p", []string{"/ip4/127.0.0.1/tcp/4001"}, true},
|
||||
{"duplicate peer", []string{validPeer, validPeer}, true},
|
||||
{"invalid port", []string{"/ip4/127.0.0.1/tcp/99999/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType(tt.nodeType)
|
||||
cfg := validConfigForNode()
|
||||
cfg.Discovery.BootstrapPeers = tt.peers
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
@ -253,7 +205,7 @@ func TestValidateLoggingLevel(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType("node")
|
||||
cfg := validConfigForNode()
|
||||
cfg.Logging.Level = tt.level
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
@ -280,7 +232,7 @@ func TestValidateLoggingFormat(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType("node")
|
||||
cfg := validConfigForNode()
|
||||
cfg.Logging.Format = tt.format
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
@ -307,7 +259,7 @@ func TestValidateMaxConnections(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType("node")
|
||||
cfg := validConfigForNode()
|
||||
cfg.Node.MaxConnections = tt.maxConn
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
@ -334,7 +286,7 @@ func TestValidateDiscoveryInterval(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType("node")
|
||||
cfg := validConfigForNode()
|
||||
cfg.Discovery.DiscoveryInterval = tt.interval
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
@ -347,7 +299,7 @@ func TestValidateDiscoveryInterval(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBootstrapPort(t *testing.T) {
|
||||
func TestValidatePeerDiscoveryPort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
port int
|
||||
@ -361,7 +313,7 @@ func TestValidateBootstrapPort(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := validConfigForType("node")
|
||||
cfg := validConfigForNode()
|
||||
cfg.Discovery.BootstrapPort = tt.port
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
@ -378,7 +330,6 @@ func TestValidateCompleteConfig(t *testing.T) {
|
||||
// Test a complete valid config
|
||||
validCfg := &Config{
|
||||
Node: NodeConfig{
|
||||
Type: "node",
|
||||
ID: "node1",
|
||||
ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4002"},
|
||||
DataDir: ".",
|
||||
|
||||
68
pkg/contracts/auth.go
Normal file
68
pkg/contracts/auth.go
Normal file
@ -0,0 +1,68 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuthService handles wallet-based authentication and authorization.
|
||||
// Provides nonce generation, signature verification, JWT lifecycle management,
|
||||
// and application registration for the gateway.
|
||||
type AuthService interface {
|
||||
// CreateNonce generates a cryptographic nonce for wallet authentication.
|
||||
// The nonce is valid for a limited time and used to prevent replay attacks.
|
||||
// wallet is the wallet address, purpose describes the nonce usage,
|
||||
// and namespace isolates nonces across different contexts.
|
||||
CreateNonce(ctx context.Context, wallet, purpose, namespace string) (string, error)
|
||||
|
||||
// VerifySignature validates a cryptographic signature from a wallet.
|
||||
// Supports multiple blockchain types (ETH, SOL) for signature verification.
|
||||
// Returns true if the signature is valid for the given nonce.
|
||||
VerifySignature(ctx context.Context, wallet, nonce, signature, chainType string) (bool, error)
|
||||
|
||||
// IssueTokens generates a new access token and refresh token pair.
|
||||
// Access tokens are short-lived (typically 15 minutes).
|
||||
// Refresh tokens are long-lived (typically 30 days).
|
||||
// Returns: accessToken, refreshToken, expirationUnix, error.
|
||||
IssueTokens(ctx context.Context, wallet, namespace string) (string, string, int64, error)
|
||||
|
||||
// RefreshToken validates a refresh token and issues a new access token.
|
||||
// Returns: newAccessToken, subject (wallet), expirationUnix, error.
|
||||
RefreshToken(ctx context.Context, refreshToken, namespace string) (string, string, int64, error)
|
||||
|
||||
// RevokeToken invalidates a refresh token or all tokens for a subject.
|
||||
// If token is provided, revokes that specific token.
|
||||
// If all is true and subject is provided, revokes all tokens for that subject.
|
||||
RevokeToken(ctx context.Context, namespace, token string, all bool, subject string) error
|
||||
|
||||
// ParseAndVerifyJWT validates a JWT access token and returns its claims.
|
||||
// Verifies signature, expiration, and issuer.
|
||||
ParseAndVerifyJWT(token string) (*JWTClaims, error)
|
||||
|
||||
// GenerateJWT creates a new signed JWT with the specified claims and TTL.
|
||||
// Returns: token, expirationUnix, error.
|
||||
GenerateJWT(namespace, subject string, ttl time.Duration) (string, int64, error)
|
||||
|
||||
// RegisterApp registers a new client application with the gateway.
|
||||
// Returns an application ID that can be used for OAuth flows.
|
||||
RegisterApp(ctx context.Context, wallet, namespace, name, publicKey string) (string, error)
|
||||
|
||||
// GetOrCreateAPIKey retrieves an existing API key or creates a new one.
|
||||
// API keys provide programmatic access without interactive authentication.
|
||||
GetOrCreateAPIKey(ctx context.Context, wallet, namespace string) (string, error)
|
||||
|
||||
// ResolveNamespaceID ensures a namespace exists and returns its internal ID.
|
||||
// Creates the namespace if it doesn't exist.
|
||||
ResolveNamespaceID(ctx context.Context, namespace string) (interface{}, error)
|
||||
}
|
||||
|
||||
// JWTClaims represents the claims contained in a JWT access token.
|
||||
type JWTClaims struct {
|
||||
Iss string `json:"iss"` // Issuer
|
||||
Sub string `json:"sub"` // Subject (wallet address)
|
||||
Aud string `json:"aud"` // Audience
|
||||
Iat int64 `json:"iat"` // Issued At
|
||||
Nbf int64 `json:"nbf"` // Not Before
|
||||
Exp int64 `json:"exp"` // Expiration
|
||||
Namespace string `json:"namespace"` // Namespace isolation
|
||||
}
|
||||
28
pkg/contracts/cache.go
Normal file
28
pkg/contracts/cache.go
Normal file
@ -0,0 +1,28 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// CacheProvider defines the interface for distributed cache operations.
|
||||
// Implementations provide a distributed key-value store with eventual consistency.
|
||||
type CacheProvider interface {
|
||||
// Health checks if the cache service is operational.
|
||||
// Returns an error if the service is unavailable or cannot be reached.
|
||||
Health(ctx context.Context) error
|
||||
|
||||
// Close gracefully shuts down the cache client and releases resources.
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
||||
// CacheClient provides extended cache operations beyond basic connectivity.
|
||||
// This interface is intentionally kept minimal as cache operations are
|
||||
// typically accessed through the underlying client's DMap API.
|
||||
type CacheClient interface {
|
||||
CacheProvider
|
||||
|
||||
// UnderlyingClient returns the native cache client for advanced operations.
|
||||
// The returned client can be used to access DMap operations like Get, Put, Delete, etc.
|
||||
// Return type is interface{} to avoid leaking concrete implementation details.
|
||||
UnderlyingClient() interface{}
|
||||
}
|
||||
117
pkg/contracts/database.go
Normal file
117
pkg/contracts/database.go
Normal file
@ -0,0 +1,117 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// DatabaseClient defines the interface for ORM-like database operations.
|
||||
// Provides both raw SQL execution and fluent query building capabilities.
|
||||
type DatabaseClient interface {
|
||||
// Query executes a SELECT query and scans results into dest.
|
||||
// dest must be a pointer to a slice of structs or []map[string]any.
|
||||
Query(ctx context.Context, dest any, query string, args ...any) error
|
||||
|
||||
// Exec executes a write statement (INSERT/UPDATE/DELETE) and returns the result.
|
||||
Exec(ctx context.Context, query string, args ...any) (sql.Result, error)
|
||||
|
||||
// FindBy retrieves multiple records matching the criteria.
|
||||
// dest must be a pointer to a slice, table is the table name,
|
||||
// criteria is a map of column->value filters, and opts customize the query.
|
||||
FindBy(ctx context.Context, dest any, table string, criteria map[string]any, opts ...FindOption) error
|
||||
|
||||
// FindOneBy retrieves a single record matching the criteria.
|
||||
// dest must be a pointer to a struct or map.
|
||||
FindOneBy(ctx context.Context, dest any, table string, criteria map[string]any, opts ...FindOption) error
|
||||
|
||||
// Save inserts or updates an entity based on its primary key.
|
||||
// If the primary key is zero, performs an INSERT.
|
||||
// If the primary key is set, performs an UPDATE.
|
||||
Save(ctx context.Context, entity any) error
|
||||
|
||||
// Remove deletes an entity by its primary key.
|
||||
Remove(ctx context.Context, entity any) error
|
||||
|
||||
// Repository returns a generic repository for a table.
|
||||
// Return type is any to avoid exposing generic type parameters in the interface.
|
||||
Repository(table string) any
|
||||
|
||||
// CreateQueryBuilder creates a fluent query builder for advanced queries.
|
||||
// Supports joins, where clauses, ordering, grouping, and pagination.
|
||||
CreateQueryBuilder(table string) QueryBuilder
|
||||
|
||||
// Tx executes a function within a database transaction.
|
||||
// If fn returns an error, the transaction is rolled back.
|
||||
// Otherwise, it is committed.
|
||||
Tx(ctx context.Context, fn func(tx DatabaseTransaction) error) error
|
||||
}
|
||||
|
||||
// DatabaseTransaction provides database operations within a transaction context.
|
||||
type DatabaseTransaction interface {
|
||||
// Query executes a SELECT query within the transaction.
|
||||
Query(ctx context.Context, dest any, query string, args ...any) error
|
||||
|
||||
// Exec executes a write statement within the transaction.
|
||||
Exec(ctx context.Context, query string, args ...any) (sql.Result, error)
|
||||
|
||||
// CreateQueryBuilder creates a query builder that executes within the transaction.
|
||||
CreateQueryBuilder(table string) QueryBuilder
|
||||
|
||||
// Save inserts or updates an entity within the transaction.
|
||||
Save(ctx context.Context, entity any) error
|
||||
|
||||
// Remove deletes an entity within the transaction.
|
||||
Remove(ctx context.Context, entity any) error
|
||||
}
|
||||
|
||||
// QueryBuilder provides a fluent interface for building SQL queries.
|
||||
type QueryBuilder interface {
|
||||
// Select specifies which columns to retrieve (default: *).
|
||||
Select(cols ...string) QueryBuilder
|
||||
|
||||
// Alias sets a table alias for the query.
|
||||
Alias(alias string) QueryBuilder
|
||||
|
||||
// Where adds a WHERE condition (same as AndWhere).
|
||||
Where(expr string, args ...any) QueryBuilder
|
||||
|
||||
// AndWhere adds a WHERE condition with AND conjunction.
|
||||
AndWhere(expr string, args ...any) QueryBuilder
|
||||
|
||||
// OrWhere adds a WHERE condition with OR conjunction.
|
||||
OrWhere(expr string, args ...any) QueryBuilder
|
||||
|
||||
// InnerJoin adds an INNER JOIN clause.
|
||||
InnerJoin(table string, on string) QueryBuilder
|
||||
|
||||
// LeftJoin adds a LEFT JOIN clause.
|
||||
LeftJoin(table string, on string) QueryBuilder
|
||||
|
||||
// Join adds a JOIN clause (default join type).
|
||||
Join(table string, on string) QueryBuilder
|
||||
|
||||
// GroupBy adds a GROUP BY clause.
|
||||
GroupBy(cols ...string) QueryBuilder
|
||||
|
||||
// OrderBy adds an ORDER BY clause.
|
||||
// Supports expressions like "name ASC", "created_at DESC".
|
||||
OrderBy(exprs ...string) QueryBuilder
|
||||
|
||||
// Limit sets the maximum number of rows to return.
|
||||
Limit(n int) QueryBuilder
|
||||
|
||||
// Offset sets the number of rows to skip.
|
||||
Offset(n int) QueryBuilder
|
||||
|
||||
// Build constructs the final SQL query and returns it with positional arguments.
|
||||
Build() (query string, args []any)
|
||||
|
||||
// GetMany executes the query and scans results into dest (pointer to slice).
|
||||
GetMany(ctx context.Context, dest any) error
|
||||
|
||||
// GetOne executes the query with LIMIT 1 and scans into dest (pointer to struct/map).
|
||||
GetOne(ctx context.Context, dest any) error
|
||||
}
|
||||
|
||||
// FindOption is a function that configures a FindBy/FindOneBy query.
|
||||
type FindOption func(q QueryBuilder)
|
||||
36
pkg/contracts/discovery.go
Normal file
36
pkg/contracts/discovery.go
Normal file
@ -0,0 +1,36 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PeerDiscovery handles peer discovery and connection management.
|
||||
// Provides mechanisms for finding and connecting to network peers
|
||||
// without relying on a DHT (Distributed Hash Table).
|
||||
type PeerDiscovery interface {
|
||||
// Start begins periodic peer discovery with the given configuration.
|
||||
// Runs discovery in the background until Stop is called.
|
||||
Start(config DiscoveryConfig) error
|
||||
|
||||
// Stop halts the peer discovery process and cleans up resources.
|
||||
Stop()
|
||||
|
||||
// StartProtocolHandler registers the peer exchange protocol handler.
|
||||
// Must be called to enable incoming peer exchange requests.
|
||||
StartProtocolHandler()
|
||||
|
||||
// TriggerPeerExchange manually triggers peer exchange with all connected peers.
|
||||
// Useful for bootstrapping or refreshing peer metadata.
|
||||
// Returns the number of peers from which metadata was collected.
|
||||
TriggerPeerExchange(ctx context.Context) int
|
||||
}
|
||||
|
||||
// DiscoveryConfig contains configuration for peer discovery.
|
||||
type DiscoveryConfig struct {
|
||||
// DiscoveryInterval is how often to run peer discovery.
|
||||
DiscoveryInterval time.Duration
|
||||
|
||||
// MaxConnections is the maximum number of new connections per discovery round.
|
||||
MaxConnections int
|
||||
}
|
||||
24
pkg/contracts/doc.go
Normal file
24
pkg/contracts/doc.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Package contracts defines clean, focused interface contracts for the Orama Network.
|
||||
//
|
||||
// This package follows the Interface Segregation Principle (ISP) by providing
|
||||
// small, focused interfaces that define clear contracts between components.
|
||||
// Each interface represents a specific capability or service without exposing
|
||||
// implementation details.
|
||||
//
|
||||
// Design Principles:
|
||||
// - Small, focused interfaces (ISP compliance)
|
||||
// - No concrete type leakage in signatures
|
||||
// - Comprehensive documentation for all public methods
|
||||
// - Domain-aligned contracts (storage, cache, database, auth, serverless, etc.)
|
||||
//
|
||||
// Interfaces:
|
||||
// - StorageProvider: Decentralized content storage (IPFS)
|
||||
// - CacheProvider/CacheClient: Distributed caching (Olric)
|
||||
// - DatabaseClient: ORM-like database operations (RQLite)
|
||||
// - AuthService: Wallet-based authentication and JWT management
|
||||
// - FunctionExecutor: WebAssembly function execution
|
||||
// - FunctionRegistry: Function metadata and bytecode storage
|
||||
// - PubSubService: Topic-based messaging
|
||||
// - PeerDiscovery: Peer discovery and connection management
|
||||
// - Logger: Structured logging
|
||||
package contracts
|
||||
48
pkg/contracts/logger.go
Normal file
48
pkg/contracts/logger.go
Normal file
@ -0,0 +1,48 @@
|
||||
package contracts
|
||||
|
||||
// Logger defines a structured logging interface.
|
||||
// Provides leveled logging with contextual fields for debugging and monitoring.
|
||||
type Logger interface {
|
||||
// Debug logs a debug-level message with optional fields.
|
||||
Debug(msg string, fields ...Field)
|
||||
|
||||
// Info logs an info-level message with optional fields.
|
||||
Info(msg string, fields ...Field)
|
||||
|
||||
// Warn logs a warning-level message with optional fields.
|
||||
Warn(msg string, fields ...Field)
|
||||
|
||||
// Error logs an error-level message with optional fields.
|
||||
Error(msg string, fields ...Field)
|
||||
|
||||
// Fatal logs a fatal-level message and terminates the application.
|
||||
Fatal(msg string, fields ...Field)
|
||||
|
||||
// With creates a child logger with additional context fields.
|
||||
// The returned logger includes all parent fields plus the new ones.
|
||||
With(fields ...Field) Logger
|
||||
|
||||
// Sync flushes any buffered log entries.
|
||||
// Should be called before application shutdown.
|
||||
Sync() error
|
||||
}
|
||||
|
||||
// Field represents a structured logging field with a key and value.
|
||||
// Implementations typically use zap.Field or similar structured logging types.
|
||||
type Field interface {
|
||||
// Key returns the field's key name.
|
||||
Key() string
|
||||
|
||||
// Value returns the field's value.
|
||||
Value() interface{}
|
||||
}
|
||||
|
||||
// LoggerFactory creates logger instances with configuration.
|
||||
type LoggerFactory interface {
|
||||
// NewLogger creates a new logger with the given name.
|
||||
// The name is typically used as a component identifier in logs.
|
||||
NewLogger(name string) Logger
|
||||
|
||||
// NewLoggerWithFields creates a new logger with pre-set context fields.
|
||||
NewLoggerWithFields(name string, fields ...Field) Logger
|
||||
}
|
||||
36
pkg/contracts/pubsub.go
Normal file
36
pkg/contracts/pubsub.go
Normal file
@ -0,0 +1,36 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// PubSubService defines the interface for publish-subscribe messaging.
|
||||
// Provides topic-based message broadcasting with support for multiple handlers.
|
||||
type PubSubService interface {
|
||||
// Publish sends a message to all subscribers of a topic.
|
||||
// The message is delivered asynchronously to all registered handlers.
|
||||
Publish(ctx context.Context, topic string, data []byte) error
|
||||
|
||||
// Subscribe registers a handler for messages on a topic.
|
||||
// Multiple handlers can be registered for the same topic.
|
||||
// Returns a HandlerID that can be used to unsubscribe.
|
||||
Subscribe(ctx context.Context, topic string, handler MessageHandler) (HandlerID, error)
|
||||
|
||||
// Unsubscribe removes a specific handler from a topic.
|
||||
// The subscription is reference-counted per topic.
|
||||
Unsubscribe(ctx context.Context, topic string, handlerID HandlerID) error
|
||||
|
||||
// Close gracefully shuts down the pubsub service and releases resources.
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
||||
// MessageHandler processes messages received from a subscribed topic.
|
||||
// Each handler receives the topic name and message data.
|
||||
// Multiple handlers for the same topic each receive a copy of the message.
|
||||
// Handlers should return an error only for critical failures.
|
||||
type MessageHandler func(topic string, data []byte) error
|
||||
|
||||
// HandlerID uniquely identifies a subscription handler.
|
||||
// Each Subscribe call generates a new HandlerID, allowing multiple
|
||||
// independent subscriptions to the same topic.
|
||||
type HandlerID string
|
||||
129
pkg/contracts/serverless.go
Normal file
129
pkg/contracts/serverless.go
Normal file
@ -0,0 +1,129 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FunctionExecutor handles the execution of WebAssembly serverless functions.
|
||||
// Manages compilation, caching, and runtime execution of WASM modules.
|
||||
type FunctionExecutor interface {
|
||||
// Execute runs a function with the given input and returns the output.
|
||||
// fn contains the function metadata, input is the function's input data,
|
||||
// and invCtx provides context about the invocation (caller, trigger type, etc.).
|
||||
Execute(ctx context.Context, fn *Function, input []byte, invCtx *InvocationContext) ([]byte, error)
|
||||
|
||||
// Precompile compiles a WASM module and caches it for faster execution.
|
||||
// wasmCID is the content identifier, wasmBytes is the raw WASM bytecode.
|
||||
// Precompiling reduces cold-start latency for subsequent invocations.
|
||||
Precompile(ctx context.Context, wasmCID string, wasmBytes []byte) error
|
||||
|
||||
// Invalidate removes a compiled module from the cache.
|
||||
// Call this when a function is updated or deleted.
|
||||
Invalidate(wasmCID string)
|
||||
}
|
||||
|
||||
// FunctionRegistry manages function metadata and bytecode storage.
|
||||
// Responsible for CRUD operations on function definitions.
|
||||
type FunctionRegistry interface {
|
||||
// Register deploys a new function or updates an existing one.
|
||||
// fn contains the function definition, wasmBytes is the compiled WASM code.
|
||||
// Returns the old function definition if it was updated, or nil for new registrations.
|
||||
Register(ctx context.Context, fn *FunctionDefinition, wasmBytes []byte) (*Function, error)
|
||||
|
||||
// Get retrieves a function by name and optional version.
|
||||
// If version is 0, returns the latest active version.
|
||||
// Returns an error if the function is not found.
|
||||
Get(ctx context.Context, namespace, name string, version int) (*Function, error)
|
||||
|
||||
// List returns all active functions in a namespace.
|
||||
// Returns only the latest version of each function.
|
||||
List(ctx context.Context, namespace string) ([]*Function, error)
|
||||
|
||||
// Delete marks a function as inactive (soft delete).
|
||||
// If version is 0, marks all versions as inactive.
|
||||
Delete(ctx context.Context, namespace, name string, version int) error
|
||||
|
||||
// GetWASMBytes retrieves the compiled WASM bytecode for a function.
|
||||
// wasmCID is the content identifier returned during registration.
|
||||
GetWASMBytes(ctx context.Context, wasmCID string) ([]byte, error)
|
||||
|
||||
// GetLogs retrieves execution logs for a function.
|
||||
// limit constrains the number of log entries returned.
|
||||
GetLogs(ctx context.Context, namespace, name string, limit int) ([]LogEntry, error)
|
||||
}
|
||||
|
||||
// Function represents a deployed serverless function with its metadata.
|
||||
type Function struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Version int `json:"version"`
|
||||
WASMCID string `json:"wasm_cid"`
|
||||
SourceCID string `json:"source_cid,omitempty"`
|
||||
MemoryLimitMB int `json:"memory_limit_mb"`
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
RetryDelaySeconds int `json:"retry_delay_seconds"`
|
||||
DLQTopic string `json:"dlq_topic,omitempty"`
|
||||
Status FunctionStatus `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
}
|
||||
|
||||
// FunctionDefinition contains the configuration for deploying a function.
|
||||
type FunctionDefinition struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Version int `json:"version,omitempty"`
|
||||
MemoryLimitMB int `json:"memory_limit_mb,omitempty"`
|
||||
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
|
||||
IsPublic bool `json:"is_public,omitempty"`
|
||||
RetryCount int `json:"retry_count,omitempty"`
|
||||
RetryDelaySeconds int `json:"retry_delay_seconds,omitempty"`
|
||||
DLQTopic string `json:"dlq_topic,omitempty"`
|
||||
EnvVars map[string]string `json:"env_vars,omitempty"`
|
||||
}
|
||||
|
||||
// InvocationContext provides context for a function invocation.
|
||||
type InvocationContext struct {
|
||||
RequestID string `json:"request_id"`
|
||||
FunctionID string `json:"function_id"`
|
||||
FunctionName string `json:"function_name"`
|
||||
Namespace string `json:"namespace"`
|
||||
CallerWallet string `json:"caller_wallet,omitempty"`
|
||||
TriggerType TriggerType `json:"trigger_type"`
|
||||
WSClientID string `json:"ws_client_id,omitempty"`
|
||||
EnvVars map[string]string `json:"env_vars,omitempty"`
|
||||
}
|
||||
|
||||
// LogEntry represents a log message from a function execution.
|
||||
type LogEntry struct {
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// FunctionStatus represents the current state of a deployed function.
|
||||
type FunctionStatus string
|
||||
|
||||
const (
|
||||
FunctionStatusActive FunctionStatus = "active"
|
||||
FunctionStatusInactive FunctionStatus = "inactive"
|
||||
FunctionStatusError FunctionStatus = "error"
|
||||
)
|
||||
|
||||
// TriggerType identifies the type of event that triggered a function invocation.
|
||||
type TriggerType string
|
||||
|
||||
const (
|
||||
TriggerTypeHTTP TriggerType = "http"
|
||||
TriggerTypeWebSocket TriggerType = "websocket"
|
||||
TriggerTypeCron TriggerType = "cron"
|
||||
TriggerTypeDatabase TriggerType = "database"
|
||||
TriggerTypePubSub TriggerType = "pubsub"
|
||||
TriggerTypeTimer TriggerType = "timer"
|
||||
TriggerTypeJob TriggerType = "job"
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user