mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-18 12:36:58 +00:00
Compare commits
No commits in common. "main" and "v0.52.15-nightly" have entirely different histories.
main
...
v0.52.15-n
@ -1,98 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors for output
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
NOCOLOR='\033[0m'
|
||||
|
||||
# Get the directory where this hook is located
|
||||
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# Go up from .git/hooks/ to repo root
|
||||
REPO_ROOT="$(cd "$HOOK_DIR/../.." && pwd)"
|
||||
CHANGELOG_SCRIPT="$REPO_ROOT/scripts/update_changelog.sh"
|
||||
PREVIEW_FILE="$REPO_ROOT/.changelog_preview.tmp"
|
||||
VERSION_FILE="$REPO_ROOT/.changelog_version.tmp"
|
||||
|
||||
# Only run changelog update if there are actual code changes (not just changelog files)
|
||||
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
||||
if [ -z "$STAGED_FILES" ]; then
|
||||
# No staged files, exit
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if only CHANGELOG.md and/or Makefile are being committed
|
||||
OTHER_FILES=$(echo "$STAGED_FILES" | grep -v "^CHANGELOG.md$" | grep -v "^Makefile$")
|
||||
if [ -z "$OTHER_FILES" ]; then
|
||||
# Only changelog files are being committed, skip update
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check for skip flag
|
||||
# To skip changelog generation, set SKIP_CHANGELOG=1 before committing:
|
||||
# SKIP_CHANGELOG=1 git commit -m "your message"
|
||||
# SKIP_CHANGELOG=1 git commit
|
||||
if [ "$SKIP_CHANGELOG" = "1" ] || [ "$SKIP_CHANGELOG" = "true" ]; then
|
||||
echo -e "${YELLOW}Skipping changelog update (SKIP_CHANGELOG is set)${NOCOLOR}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Update changelog before commit
|
||||
if [ -f "$CHANGELOG_SCRIPT" ]; then
|
||||
echo -e "\n${CYAN}Updating changelog...${NOCOLOR}"
|
||||
|
||||
# Set environment variable to indicate we're running from pre-commit
|
||||
export CHANGELOG_CONTEXT=pre-commit
|
||||
|
||||
bash "$CHANGELOG_SCRIPT"
|
||||
changelog_status=$?
|
||||
if [ $changelog_status -ne 0 ]; then
|
||||
echo -e "${RED}Commit aborted: changelog update failed.${NOCOLOR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show preview if changelog was updated
|
||||
if [ -f "$PREVIEW_FILE" ] && [ -f "$VERSION_FILE" ]; then
|
||||
NEW_VERSION=$(cat "$VERSION_FILE")
|
||||
PREVIEW_CONTENT=$(cat "$PREVIEW_FILE")
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||
echo -e "${CYAN} CHANGELOG PREVIEW${NOCOLOR}"
|
||||
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||
echo ""
|
||||
echo -e "${GREEN}New Version: ${YELLOW}$NEW_VERSION${NOCOLOR}"
|
||||
echo ""
|
||||
echo -e "${CYAN}Changelog Entry:${NOCOLOR}"
|
||||
echo -e "${BLUE}────────────────────────────────────────────────────────────────────────${NOCOLOR}"
|
||||
echo -e "$PREVIEW_CONTENT"
|
||||
echo -e "${BLUE}────────────────────────────────────────────────────────────────────────${NOCOLOR}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Do you want to proceed with the commit? (yes/no):${NOCOLOR} "
|
||||
# Read from /dev/tty to ensure we can read from terminal even in git hook context
|
||||
read -r confirmation < /dev/tty
|
||||
|
||||
if [ "$confirmation" != "yes" ]; then
|
||||
echo -e "${RED}Commit aborted by user.${NOCOLOR}"
|
||||
echo -e "${YELLOW}To revert changes, run:${NOCOLOR}"
|
||||
echo -e " git checkout CHANGELOG.md Makefile"
|
||||
# Clean up temp files
|
||||
rm -f "$PREVIEW_FILE" "$VERSION_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Proceeding with commit...${NOCOLOR}"
|
||||
|
||||
# Add the updated CHANGELOG.md and Makefile to the current commit
|
||||
echo -e "${CYAN}Staging CHANGELOG.md and Makefile...${NOCOLOR}"
|
||||
git add CHANGELOG.md Makefile
|
||||
|
||||
# Clean up temp files
|
||||
rm -f "$PREVIEW_FILE" "$VERSION_FILE"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}Warning: changelog update script not found at $CHANGELOG_SCRIPT${NOCOLOR}"
|
||||
fi
|
||||
|
||||
@ -1,18 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors for output
|
||||
CYAN='\033[0;36m'
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NOCOLOR='\033[0m'
|
||||
|
||||
# Run tests before push
|
||||
echo -e "\n${CYAN}Running tests...${NOCOLOR}"
|
||||
echo -e "\nRunning tests:"
|
||||
go test ./... # Runs all tests in your repo
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo -e "${RED}Push aborted: some tests failed.${NOCOLOR}"
|
||||
echo "Push aborted: some tests failed."
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}All tests passed. Proceeding with push.${NOCOLOR}"
|
||||
echo "All tests passed. Proceeding with push."
|
||||
fi
|
||||
|
||||
198
.github/workflows/release-apt.yml
vendored
198
.github/workflows/release-apt.yml
vendored
@ -1,198 +0,0 @@
|
||||
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
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -75,9 +75,3 @@ data/bootstrap/rqlite/
|
||||
configs/
|
||||
|
||||
.dev/
|
||||
|
||||
.gocache/
|
||||
|
||||
.claude/
|
||||
.mcp.json
|
||||
.cursor/
|
||||
@ -1,6 +1,6 @@
|
||||
# GoReleaser Configuration for DeBros Network
|
||||
# Builds and releases the dbn binary for multiple platforms
|
||||
# Other binaries (node, gateway, identity) are installed via: dbn setup
|
||||
# Builds and releases the network-cli binary for multiple platforms
|
||||
# Other binaries (node, gateway, identity) are installed via: network-cli setup
|
||||
|
||||
project_name: debros-network
|
||||
|
||||
@ -8,10 +8,10 @@ env:
|
||||
- GO111MODULE=on
|
||||
|
||||
builds:
|
||||
# dbn binary - only build the CLI
|
||||
- id: dbn
|
||||
# network-cli binary - only build the CLI
|
||||
- id: network-cli
|
||||
main: ./cmd/cli
|
||||
binary: dbn
|
||||
binary: network-cli
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
@ -23,10 +23,10 @@ builds:
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.ShortCommit}}
|
||||
- -X main.date={{.Date}}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
|
||||
archives:
|
||||
# Tar.gz archives for dbn
|
||||
# Tar.gz archives for network-cli
|
||||
- id: binaries
|
||||
format: tar.gz
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
@ -50,10 +50,10 @@ changelog:
|
||||
abbrev: -1
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
- "^chore:"
|
||||
- "^ci:"
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^chore:'
|
||||
- '^ci:'
|
||||
- Merge pull request
|
||||
- Merge branch
|
||||
|
||||
|
||||
68
.zed/debug.json
Normal file
68
.zed/debug.json
Normal file
@ -0,0 +1,68 @@
|
||||
// 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/127.0.0.1/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/127.0.0.1/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"]
|
||||
}
|
||||
]
|
||||
299
CHANGELOG.md
Normal file
299
CHANGELOG.md
Normal file
@ -0,0 +1,299 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semantic Versioning][semver].
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Fixed
|
||||
|
||||
## [0.52.15]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Base64 encoding for the response body in the anonProxyHandler to prevent corruption of binary data when returned in JSON format.
|
||||
|
||||
### Changed
|
||||
|
||||
- **GoReleaser**: Updated to build only `network-cli` binary (v0.52.2+)
|
||||
- Other binaries (node, gateway, identity) now installed via `network-cli setup`
|
||||
- Cleaner, smaller release packages
|
||||
- Resolves archive mismatch errors
|
||||
- **GitHub Actions**: Updated artifact actions from v3 to v4 (deprecated versions)
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed install script to be more clear and bug fixing
|
||||
|
||||
## [0.52.1] - 2025-10-26
|
||||
|
||||
### Added
|
||||
|
||||
- **CLI Refactor**: Modularized monolithic CLI into `pkg/cli/` package structure for better maintainability
|
||||
- New `environment.go`: Multi-environment management system (local, devnet, testnet)
|
||||
- New `env_commands.go`: Environment switching commands (`env list`, `env switch`, `devnet enable`, `testnet enable`)
|
||||
- New `setup.go`: Interactive VPS installation command (`network-cli setup`) that replaces bash install script
|
||||
- New `service.go`: Systemd service management commands (`service start|stop|restart|status|logs`)
|
||||
- New `auth_commands.go`, `config_commands.go`, `basic_commands.go`: Refactored commands into modular pkg/cli
|
||||
- **Release Pipeline**: Complete automated release infrastructure via `.goreleaser.yaml` and GitHub Actions
|
||||
- Multi-platform binary builds (Linux/macOS, amd64/arm64)
|
||||
- Automatic GitHub Release creation with changelog and artifacts
|
||||
- Semantic versioning support with pre-release handling
|
||||
- **Environment Configuration**: Multi-environment switching system
|
||||
- Default environments: local (http://localhost:6001), devnet (https://devnet.debros.network), testnet (https://testnet.debros.network)
|
||||
- Stored in `~/.debros/environments.json`
|
||||
- CLI auto-uses active environment for authentication and operations
|
||||
- **Comprehensive Documentation**
|
||||
- `.cursor/RELEASES.md`: Overview and quick start
|
||||
- `.cursor/goreleaser-guide.md`: Detailed distribution guide
|
||||
- `.cursor/release-checklist.md`: Quick reference
|
||||
|
||||
### Changed
|
||||
|
||||
- **CLI Refactoring**: `cmd/cli/main.go` reduced from 1340 → 180 lines (thin router pattern)
|
||||
- All business logic moved to modular `pkg/cli/` functions
|
||||
- Easier to test, maintain, and extend individual commands
|
||||
- **Installation**: `scripts/install-debros-network.sh` now APT-ready with fallback to source build
|
||||
- **Setup Process**: Consolidated all installation logic into `network-cli setup` command
|
||||
- Single unified installation regardless of installation method
|
||||
- Interactive user experience with clear progress indicators
|
||||
|
||||
### Removed
|
||||
|
||||
## [0.51.9] - 2025-10-25
|
||||
|
||||
### Added
|
||||
|
||||
- One-command `make dev` target to start full development stack (bootstrap + node2 + node3 + gateway in background)
|
||||
- New `network-cli config init` (no --type) generates complete development stack with all configs and identities
|
||||
- Full stack initialization with auto-generated peer identities for bootstrap and all nodes
|
||||
- Explicit control over LibP2P listen addresses for better localhost/development support
|
||||
- Production/development mode detection for NAT services (disabled for localhost, enabled for production)
|
||||
- Process management with .dev/pids directory for background process tracking
|
||||
- Centralized logging to ~/.debros/logs/ for all network services
|
||||
|
||||
### Changed
|
||||
|
||||
- Simplified Makefile: removed legacy dev commands, replaced with unified `make dev` target
|
||||
- Updated README with clearer getting started instructions (single `make dev` command)
|
||||
- Simplified `network-cli config init` behavior: defaults to generating full stack instead of single node
|
||||
- `network-cli config init` now handles bootstrap peer discovery and join addresses automatically
|
||||
- LibP2P configuration: removed always-on NAT services for development environments
|
||||
- Code formatting in pkg/node/node.go (indentation fixes in bootstrapPeerSource)
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed legacy Makefile targets: run-example, show-bootstrap, run-cli, cli-health, cli-peers, cli-status, cli-storage-test, cli-pubsub-test
|
||||
- Removed verbose dev-setup, dev-cluster, and old dev workflow targets
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed indentation in bootstrapPeerSource function for consistency
|
||||
- Fixed gateway.yaml generation with correct YAML indentation for bootstrap_peers
|
||||
- Fixed script for running and added gateway running as well
|
||||
|
||||
### Security
|
||||
|
||||
## [0.51.6] - 2025-10-24
|
||||
|
||||
### Added
|
||||
|
||||
- LibP2P added support over NAT
|
||||
|
||||
### Changed
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
## [0.51.5] - 2025-10-24
|
||||
|
||||
### Added
|
||||
|
||||
- Added validation for yaml files
|
||||
- Added authenticaiton command on cli
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated readme
|
||||
- Where we read .yaml files from and where data is saved to ~/.debros
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
- Regular nodes rqlite not starting
|
||||
|
||||
## [0.51.2] - 2025-09-26
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
- Enhance gateway configuration by adding RQLiteDSN support and updating default connection settings. Updated config parsing to include RQLiteDSN from YAML and environment variables. Changed default RQLite connection URL from port 4001 to 5001.
|
||||
- Update CHANGELOG.md for version 0.51.2, enhance API key extraction to support query parameters, and implement internal auth context in status and storage handlers.
|
||||
|
||||
## [0.51.1] - 2025-09-26
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the configuration file for run-node3 to use node3.yaml.
|
||||
- Modified select_data_dir function to require a hasConfigFile parameter and added error handling for missing configuration.
|
||||
- Updated main function to pass the config path to select_data_dir.
|
||||
- Introduced a peer exchange protocol in the discovery package, allowing nodes to request and exchange peer information.
|
||||
- Refactored peer discovery logic in the node package to utilize the new discovery manager for active peer exchange.
|
||||
- Cleaned up unused code related to previous peer discovery methods.
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
## [0.50.0] - 2025-09-23
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed wrong URL /v1/db to /v1/rqlite
|
||||
|
||||
### Security
|
||||
|
||||
## [0.50.0] - 2025-09-23
|
||||
|
||||
### Added
|
||||
|
||||
- Created new rqlite folder
|
||||
- Created rqlite adapter, client, gateway, migrations and rqlite init
|
||||
- Created namespace_helpers on gateway
|
||||
- Created new rqlite implementation
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated node.go to support new rqlite architecture
|
||||
- Updated readme
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed old storage folder
|
||||
- Removed old pkg/gatway storage and migrated to new rqlite
|
||||
|
||||
### Fixed
|
||||
|
||||
### Security
|
||||
|
||||
## [0.44.0] - 2025-09-22
|
||||
|
||||
### Added
|
||||
|
||||
- Added gateway.yaml file for gateway default configurations
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated readme to include all options for .yaml files
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed unused command setup-production-security.sh
|
||||
- Removed anyone proxy from libp2p proxy
|
||||
|
||||
### Fixed
|
||||
|
||||
### Security
|
||||
|
||||
## [0.43.6] - 2025-09-20
|
||||
|
||||
### Added
|
||||
|
||||
- Added Gateway port on install-debros-network.sh
|
||||
- Added default bootstrap peers on config.go
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated Gateway port from 8080/8005 to 6001
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
### Security
|
||||
|
||||
## [0.43.4] - 2025-09-18
|
||||
|
||||
### Added
|
||||
|
||||
- Added extra comments on main.go
|
||||
- Remove backoff_test.go and associated backoff tests
|
||||
- Created node_test, write tests for CalculateNextBackoff, AddJitter, GetPeerId, LoadOrCreateIdentity, hasBootstrapConnections
|
||||
|
||||
### Changed
|
||||
|
||||
- replaced git.debros.io with github.com
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
### Security
|
||||
|
||||
## [0.43.3] - 2025-09-15
|
||||
|
||||
### Added
|
||||
|
||||
- User authentication module with OAuth2 support.
|
||||
|
||||
### Changed
|
||||
|
||||
- Make file version to 0.43.2
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed cli, network-cli binaries from project
|
||||
- Removed AI_CONTEXT.md
|
||||
- Removed Network.md
|
||||
- Removed unused log from monitoring.go
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resolved race condition when saving settings.
|
||||
|
||||
### Security
|
||||
|
||||
_Initial release._
|
||||
|
||||
[keepachangelog]: https://keepachangelog.com/en/1.1.0/
|
||||
[semver]: https://semver.org/spec/v2.0.0.html
|
||||
@ -22,19 +22,19 @@ make deps
|
||||
- Test: `make test`
|
||||
- Format/Vet: `make fmt vet` (or `make lint`)
|
||||
|
||||
````
|
||||
```
|
||||
|
||||
Useful CLI commands:
|
||||
|
||||
```bash
|
||||
./bin/orama health
|
||||
./bin/orama peers
|
||||
./bin/orama status
|
||||
````
|
||||
./bin/network-cli health
|
||||
./bin/network-cli peers
|
||||
./bin/network-cli status
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
- The CLI reports its version via `orama version`.
|
||||
- The CLI reports its version via `network-cli version`.
|
||||
- Releases are tagged (e.g., `v0.18.0-beta`) and published via GoReleaser.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
195
Makefile
195
Makefile
@ -6,20 +6,22 @@ test:
|
||||
go test -v $(TEST)
|
||||
|
||||
# Gateway-focused E2E tests assume gateway and nodes are already running
|
||||
# Auto-discovers configuration from ~/.orama and queries database for API key
|
||||
# No environment variables required
|
||||
# Configure via env:
|
||||
# GATEWAY_BASE_URL (default http://127.0.0.1:6001)
|
||||
# GATEWAY_API_KEY (required for auth-protected routes)
|
||||
.PHONY: test-e2e
|
||||
test-e2e:
|
||||
@echo "Running comprehensive E2E tests..."
|
||||
@echo "Auto-discovering configuration from ~/.orama..."
|
||||
@echo "Running gateway E2E tests (HTTP/WS only)..."
|
||||
@echo "Base URL: $${GATEWAY_BASE_URL:-http://127.0.0.1:6001}"
|
||||
@test -n "$$GATEWAY_API_KEY" || (echo "GATEWAY_API_KEY must be set" && exit 1)
|
||||
go test -v -tags e2e ./e2e
|
||||
|
||||
# Network - Distributed P2P Database System
|
||||
# Makefile for development and build tasks
|
||||
|
||||
.PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports install-hooks kill
|
||||
.PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports
|
||||
|
||||
VERSION := 0.90.0
|
||||
VERSION := 0.52.15
|
||||
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,17 +31,11 @@ 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/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
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/node ./cmd/node
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/network-cli cmd/cli/main.go
|
||||
# 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/orama version"
|
||||
|
||||
# Install git hooks
|
||||
install-hooks:
|
||||
@echo "Installing git hooks..."
|
||||
@bash scripts/install-hooks.sh
|
||||
@echo "Build complete! Run ./bin/network-cli version"
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@ -50,43 +46,107 @@ clean:
|
||||
|
||||
# Run bootstrap node (auto-selects identity and data dir)
|
||||
run-node:
|
||||
@echo "Starting node..."
|
||||
@echo "Config: ~/.orama/node.yaml"
|
||||
go run ./cmd/orama-node --config node.yaml
|
||||
@echo "Starting bootstrap node..."
|
||||
@echo "Config: ~/.debros/bootstrap.yaml"
|
||||
@echo "Generate it with: network-cli config init --type bootstrap"
|
||||
go run ./cmd/node --config node.yaml
|
||||
|
||||
# Run second node - requires join address
|
||||
# Run second node (regular) - requires join address of bootstrap node
|
||||
# Usage: make run-node2 JOINADDR=/ip4/127.0.0.1/tcp/5001 HTTP=5002 RAFT=7002 P2P=4002
|
||||
run-node2:
|
||||
@echo "Starting second node..."
|
||||
@echo "Config: ~/.orama/node2.yaml"
|
||||
go run ./cmd/orama-node --config node2.yaml
|
||||
@echo "Starting regular node (node.yaml)..."
|
||||
@echo "Config: ~/.debros/node.yaml"
|
||||
@echo "Generate it with: network-cli config init --type node --join localhost:5001 --bootstrap-peers '<peer_multiaddr>'"
|
||||
go run ./cmd/node --config node2.yaml
|
||||
|
||||
# Run third node - requires join address
|
||||
# Run third node (regular) - requires join address of bootstrap node
|
||||
# Usage: make run-node3 JOINADDR=/ip4/127.0.0.1/tcp/5001 HTTP=5003 RAFT=7003 P2P=4003
|
||||
run-node3:
|
||||
@echo "Starting third node..."
|
||||
@echo "Config: ~/.orama/node3.yaml"
|
||||
go run ./cmd/orama-node --config node3.yaml
|
||||
@echo "Starting regular node (node2.yaml)..."
|
||||
@echo "Config: ~/.debros/node2.yaml"
|
||||
@echo "Generate it with: network-cli config init --type node --name node2.yaml --join localhost:5001 --bootstrap-peers '<peer_multiaddr>'"
|
||||
go run ./cmd/node --config node3.yaml
|
||||
|
||||
# Run gateway HTTP server
|
||||
# Usage examples:
|
||||
# make run-gateway # uses ~/.debros/gateway.yaml
|
||||
# Config generated with: network-cli config init --type gateway
|
||||
run-gateway:
|
||||
@echo "Starting gateway HTTP server..."
|
||||
@echo "Note: Config must be in ~/.orama/data/gateway.yaml"
|
||||
go run ./cmd/orama-gateway
|
||||
@echo "Note: Config must be in ~/.debros/gateway.yaml"
|
||||
@echo "Generate it with: network-cli config init --type gateway"
|
||||
go run ./cmd/gateway
|
||||
|
||||
# Development environment target
|
||||
# Uses orama dev up to start full stack with dependency and port checking
|
||||
# One-command dev: Start bootstrap, node2, node3, gateway, and anon in background
|
||||
# Requires: configs already exist in ~/.debros
|
||||
dev: build
|
||||
@./bin/orama dev up
|
||||
|
||||
# Graceful shutdown of all dev services
|
||||
stop:
|
||||
@if [ -f ./bin/orama ]; then \
|
||||
./bin/orama dev down || true; \
|
||||
@echo "🚀 Starting development network stack..."
|
||||
@mkdir -p .dev/pids
|
||||
@mkdir -p $$HOME/.debros/logs
|
||||
@echo "Starting Anyone client (anon proxy)..."
|
||||
@if [ "$$(uname)" = "Darwin" ]; then \
|
||||
echo " Detected macOS - using npx anyone-client"; \
|
||||
if command -v npx >/dev/null 2>&1; then \
|
||||
nohup npx anyone-client > $$HOME/.debros/logs/anon.log 2>&1 & echo $$! > .dev/pids/anon.pid; \
|
||||
echo " Anyone client started (PID: $$(cat .dev/pids/anon.pid))"; \
|
||||
else \
|
||||
echo " ⚠️ npx not found - skipping Anyone client"; \
|
||||
echo " Install with: npm install -g npm"; \
|
||||
fi; \
|
||||
elif [ "$$(uname)" = "Linux" ]; then \
|
||||
echo " Detected Linux - checking systemctl"; \
|
||||
if systemctl is-active --quiet anon 2>/dev/null; then \
|
||||
echo " ✓ Anon service already running"; \
|
||||
elif command -v systemctl >/dev/null 2>&1; then \
|
||||
echo " Starting anon service..."; \
|
||||
sudo systemctl start anon 2>/dev/null || echo " ⚠️ Failed to start anon service"; \
|
||||
else \
|
||||
echo " ⚠️ systemctl not found - skipping Anon"; \
|
||||
fi; \
|
||||
fi
|
||||
@sleep 2
|
||||
@echo "Starting bootstrap node..."
|
||||
@nohup ./bin/node --config bootstrap.yaml > $$HOME/.debros/logs/bootstrap.log 2>&1 & echo $$! > .dev/pids/bootstrap.pid
|
||||
@sleep 2
|
||||
@echo "Starting node2..."
|
||||
@nohup ./bin/node --config node2.yaml > $$HOME/.debros/logs/node2.log 2>&1 & echo $$! > .dev/pids/node2.pid
|
||||
@sleep 1
|
||||
@echo "Starting node3..."
|
||||
@nohup ./bin/node --config node3.yaml > $$HOME/.debros/logs/node3.log 2>&1 & echo $$! > .dev/pids/node3.pid
|
||||
@sleep 1
|
||||
@echo "Starting gateway..."
|
||||
@nohup ./bin/gateway --config gateway.yaml > $$HOME/.debros/logs/gateway.log 2>&1 & echo $$! > .dev/pids/gateway.pid
|
||||
@echo ""
|
||||
@echo "============================================================"
|
||||
@echo "✅ Development stack started!"
|
||||
@echo "============================================================"
|
||||
@echo ""
|
||||
@echo "Processes:"
|
||||
@if [ -f .dev/pids/anon.pid ]; then \
|
||||
echo " Anon: PID=$$(cat .dev/pids/anon.pid) (SOCKS: 9050)"; \
|
||||
fi
|
||||
@echo " Bootstrap: PID=$$(cat .dev/pids/bootstrap.pid)"
|
||||
@echo " Node2: PID=$$(cat .dev/pids/node2.pid)"
|
||||
@echo " Node3: PID=$$(cat .dev/pids/node3.pid)"
|
||||
@echo " Gateway: PID=$$(cat .dev/pids/gateway.pid)"
|
||||
@echo ""
|
||||
@echo "Ports:"
|
||||
@echo " Anon SOCKS: 9050 (proxy endpoint: POST /v1/proxy/anon)"
|
||||
@echo " Bootstrap P2P: 4001, HTTP: 5001, Raft: 7001"
|
||||
@echo " Node2 P2P: 4002, HTTP: 5002, Raft: 7002"
|
||||
@echo " Node3 P2P: 4003, HTTP: 5003, Raft: 7003"
|
||||
@echo " Gateway: 6001"
|
||||
@echo ""
|
||||
@echo "Press Ctrl+C to stop all processes"
|
||||
@echo "============================================================"
|
||||
@echo ""
|
||||
@if [ -f .dev/pids/anon.pid ]; then \
|
||||
trap 'echo "Stopping all processes..."; kill $$(cat .dev/pids/*.pid) 2>/dev/null; rm -f .dev/pids/*.pid; exit 0' INT; \
|
||||
tail -f $$HOME/.debros/logs/anon.log $$HOME/.debros/logs/bootstrap.log $$HOME/.debros/logs/node2.log $$HOME/.debros/logs/node3.log $$HOME/.debros/logs/gateway.log; \
|
||||
else \
|
||||
trap 'echo "Stopping all processes..."; kill $$(cat .dev/pids/*.pid) 2>/dev/null; rm -f .dev/pids/*.pid; exit 0' INT; \
|
||||
tail -f $$HOME/.debros/logs/bootstrap.log $$HOME/.debros/logs/node2.log $$HOME/.debros/logs/node3.log $$HOME/.debros/logs/gateway.log; \
|
||||
fi
|
||||
@bash scripts/dev-kill-all.sh
|
||||
|
||||
# Force kill all processes (immediate termination)
|
||||
kill:
|
||||
@bash scripts/dev-kill-all.sh
|
||||
|
||||
# Help
|
||||
help:
|
||||
@ -95,23 +155,42 @@ help:
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " test - Run tests"
|
||||
@echo ""
|
||||
@echo "Local Development (Recommended):"
|
||||
@echo " make dev - Start full development stack with one command"
|
||||
@echo " - Checks dependencies and available ports"
|
||||
@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 "Development:"
|
||||
@echo " dev - Start full dev stack (bootstrap + 2 nodes + gateway)"
|
||||
@echo " Requires: configs in ~/.debros (run 'network-cli config init' first)"
|
||||
@echo ""
|
||||
@echo "Development Management (via orama):"
|
||||
@echo " ./bin/orama dev status - Show status of all dev services"
|
||||
@echo " ./bin/orama dev logs <component> [--follow]"
|
||||
@echo "Configuration (NEW):"
|
||||
@echo " First, generate config files in ~/.debros with:"
|
||||
@echo " make build # Build CLI first"
|
||||
@echo " ./bin/network-cli config init # Generate full stack"
|
||||
@echo ""
|
||||
@echo "Individual Node Targets (advanced):"
|
||||
@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"
|
||||
@echo "Network Targets (requires config files in ~/.debros):"
|
||||
@echo " run-node - Start bootstrap node"
|
||||
@echo " run-node2 - Start second node"
|
||||
@echo " run-node3 - Start third node"
|
||||
@echo " run-gateway - Start HTTP gateway"
|
||||
@echo " run-example - Run usage example"
|
||||
@echo ""
|
||||
@echo "Running Multiple Nodes:"
|
||||
@echo " Nodes use --config flag to select which YAML file in ~/.debros to load:"
|
||||
@echo " go run ./cmd/node --config bootstrap.yaml"
|
||||
@echo " go run ./cmd/node --config node.yaml"
|
||||
@echo " go run ./cmd/node --config node2.yaml"
|
||||
@echo " Generate configs with: ./bin/network-cli config init --name <filename.yaml>"
|
||||
@echo ""
|
||||
@echo "CLI Commands:"
|
||||
@echo " run-cli - Run network CLI help"
|
||||
@echo " cli-health - Check network health"
|
||||
@echo " cli-peers - List network peers"
|
||||
@echo " cli-status - Get network status"
|
||||
@echo " cli-storage-test - Test storage operations"
|
||||
@echo " cli-pubsub-test - Test pub/sub operations"
|
||||
@echo ""
|
||||
@echo "Development:"
|
||||
@echo " test-multinode - Full multi-node test with 1 bootstrap + 2 nodes"
|
||||
@echo " test-peer-discovery - Test peer discovery (requires running nodes)"
|
||||
@echo " test-replication - Test data replication (requires running nodes)"
|
||||
@echo " test-consensus - Test database consensus (requires running nodes)"
|
||||
@echo ""
|
||||
@echo "Maintenance:"
|
||||
@echo " deps - Download dependencies"
|
||||
@ -119,4 +198,8 @@ help:
|
||||
@echo " fmt - Format code"
|
||||
@echo " vet - Vet code"
|
||||
@echo " lint - Lint code (fmt + vet)"
|
||||
@echo " clear-ports - Clear common dev ports"
|
||||
@echo " dev-setup - Setup development environment"
|
||||
@echo " dev-cluster - Show cluster startup commands"
|
||||
@echo " dev - Full development workflow"
|
||||
@echo " help - Show this help"
|
||||
|
||||
164
cmd/cli/main.go
164
cmd/cli/main.go
@ -34,7 +34,7 @@ func main() {
|
||||
|
||||
switch command {
|
||||
case "version":
|
||||
fmt.Printf("orama %s", version)
|
||||
fmt.Printf("network-cli %s", version)
|
||||
if commit != "" {
|
||||
fmt.Printf(" (commit %s)", commit)
|
||||
}
|
||||
@ -44,38 +44,70 @@ func main() {
|
||||
fmt.Println()
|
||||
return
|
||||
|
||||
// Development environment commands
|
||||
case "dev":
|
||||
cli.HandleDevCommand(args)
|
||||
// Environment commands
|
||||
case "env":
|
||||
cli.HandleEnvCommand(args)
|
||||
case "devnet", "testnet", "local":
|
||||
// Shorthand for switching environments
|
||||
if len(args) > 0 && (args[0] == "enable" || args[0] == "switch") {
|
||||
if err := cli.SwitchEnvironment(command); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to switch environment: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
env, _ := cli.GetActiveEnvironment()
|
||||
fmt.Printf("✅ Switched to %s environment\n", command)
|
||||
if env != nil {
|
||||
fmt.Printf(" Gateway URL: %s\n", env.GatewayURL)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli %s enable\n", command)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 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...))
|
||||
// Setup and service commands
|
||||
case "setup":
|
||||
cli.HandleSetupCommand(args)
|
||||
case "service":
|
||||
cli.HandleServiceCommand(args)
|
||||
|
||||
// Authentication commands
|
||||
case "auth":
|
||||
cli.HandleAuthCommand(args)
|
||||
|
||||
// Config commands
|
||||
case "config":
|
||||
cli.HandleConfigCommand(args)
|
||||
|
||||
// Basic network commands
|
||||
case "health":
|
||||
cli.HandleHealthCommand(format, timeout)
|
||||
case "peers":
|
||||
cli.HandlePeersCommand(format, timeout)
|
||||
case "status":
|
||||
cli.HandleStatusCommand(format, timeout)
|
||||
case "peer-id":
|
||||
cli.HandlePeerIDCommand(format, timeout)
|
||||
|
||||
// Query command
|
||||
case "query":
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli query <sql>\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
cli.HandleQueryCommand(args[0], format, timeout)
|
||||
|
||||
// PubSub commands
|
||||
case "pubsub":
|
||||
cli.HandlePubSubCommand(args, format, timeout)
|
||||
|
||||
// Connect command
|
||||
case "connect":
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli connect <peer_address>\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
cli.HandleConnectCommand(args[0], timeout)
|
||||
|
||||
// Help
|
||||
case "help", "--help", "-h":
|
||||
showHelp()
|
||||
@ -105,47 +137,67 @@ func parseGlobalFlags(args []string) {
|
||||
}
|
||||
|
||||
func showHelp() {
|
||||
fmt.Printf("Orama CLI - Distributed P2P Network Management Tool\n\n")
|
||||
fmt.Printf("Usage: orama <command> [args...]\n\n")
|
||||
fmt.Printf("Network CLI - Distributed P2P Network Management Tool\n\n")
|
||||
fmt.Printf("Usage: network-cli <command> [args...]\n\n")
|
||||
|
||||
fmt.Printf("💻 Local Development:\n")
|
||||
fmt.Printf(" dev up - Start full local dev environment\n")
|
||||
fmt.Printf(" dev down - Stop all dev services\n")
|
||||
fmt.Printf(" dev status - Show status of dev services\n")
|
||||
fmt.Printf(" dev logs <component> - View dev component logs\n")
|
||||
fmt.Printf(" dev help - Show dev command help\n\n")
|
||||
fmt.Printf("🌍 Environment Management:\n")
|
||||
fmt.Printf(" env list - List available environments\n")
|
||||
fmt.Printf(" env current - Show current environment\n")
|
||||
fmt.Printf(" env switch <env> - Switch to environment (local, devnet, testnet)\n")
|
||||
fmt.Printf(" devnet enable - Shorthand for switching to devnet\n")
|
||||
fmt.Printf(" testnet enable - Shorthand for switching to testnet\n\n")
|
||||
|
||||
fmt.Printf("🚀 Production Deployment:\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("🚀 Setup & Services:\n")
|
||||
fmt.Printf(" setup [--force] - Interactive VPS setup (Linux only, requires root)\n")
|
||||
fmt.Printf(" service start <target> - Start service (node, gateway, all)\n")
|
||||
fmt.Printf(" service stop <target> - Stop service\n")
|
||||
fmt.Printf(" service restart <target> - Restart service\n")
|
||||
fmt.Printf(" service status [target] - Show service status\n")
|
||||
fmt.Printf(" service logs <target> [opts] - View service logs (--follow, --since=1h)\n\n")
|
||||
|
||||
fmt.Printf("🔐 Authentication:\n")
|
||||
fmt.Printf(" auth login - Authenticate with wallet\n")
|
||||
fmt.Printf(" auth logout - Clear stored credentials\n")
|
||||
fmt.Printf(" auth whoami - Show current authentication\n")
|
||||
fmt.Printf(" auth status - Show detailed auth info\n")
|
||||
fmt.Printf(" auth help - Show auth command help\n\n")
|
||||
fmt.Printf(" auth status - Show detailed auth info\n\n")
|
||||
|
||||
fmt.Printf("⚙️ Configuration:\n")
|
||||
fmt.Printf(" config init [--type <type>] - Generate configs (full stack or single)\n")
|
||||
fmt.Printf(" config validate --name <file> - Validate config file\n\n")
|
||||
|
||||
fmt.Printf("🌐 Network Commands:\n")
|
||||
fmt.Printf(" health - Check network health\n")
|
||||
fmt.Printf(" peers - List connected peers\n")
|
||||
fmt.Printf(" status - Show network status\n")
|
||||
fmt.Printf(" peer-id - Show this node's peer ID\n")
|
||||
fmt.Printf(" connect <peer_address> - Connect to peer\n\n")
|
||||
|
||||
fmt.Printf("🗄️ Database:\n")
|
||||
fmt.Printf(" query <sql> 🔐 Execute database query\n\n")
|
||||
|
||||
fmt.Printf("📡 PubSub:\n")
|
||||
fmt.Printf(" pubsub publish <topic> <msg> 🔐 Publish message\n")
|
||||
fmt.Printf(" pubsub subscribe <topic> 🔐 Subscribe to topic\n")
|
||||
fmt.Printf(" pubsub topics 🔐 List topics\n\n")
|
||||
|
||||
fmt.Printf("Global Flags:\n")
|
||||
fmt.Printf(" -f, --format <format> - Output format: table, json (default: table)\n")
|
||||
fmt.Printf(" -t, --timeout <duration> - Operation timeout (default: 30s)\n")
|
||||
fmt.Printf(" --help, -h - Show this help message\n\n")
|
||||
fmt.Printf(" -t, --timeout <duration> - Operation timeout (default: 30s)\n\n")
|
||||
|
||||
fmt.Printf("🔐 = Requires authentication (auto-prompts if needed)\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(" # Switch to devnet\n")
|
||||
fmt.Printf(" network-cli devnet enable\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(" # Authenticate and query\n")
|
||||
fmt.Printf(" network-cli auth login\n")
|
||||
fmt.Printf(" network-cli query \"SELECT * FROM users LIMIT 10\"\n\n")
|
||||
|
||||
fmt.Printf(" # Service management\n")
|
||||
fmt.Printf(" orama status\n")
|
||||
fmt.Printf(" orama logs node --follow\n")
|
||||
fmt.Printf(" # Setup VPS (Linux only)\n")
|
||||
fmt.Printf(" sudo network-cli setup\n\n")
|
||||
|
||||
fmt.Printf(" # Manage services\n")
|
||||
fmt.Printf(" sudo network-cli service status all\n")
|
||||
fmt.Printf(" sudo network-cli service logs node --follow\n")
|
||||
}
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/config"
|
||||
"github.com/DeBrosOfficial/network/pkg/gateway"
|
||||
@ -40,53 +37,22 @@ func getEnvBoolDefault(key string, def bool) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// parseGatewayConfig loads gateway.yaml from ~/.orama exclusively.
|
||||
// It accepts an optional --config flag for absolute paths (used by systemd services).
|
||||
// parseGatewayConfig loads gateway.yaml from ~/.debros exclusively.
|
||||
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 ~/.orama)")
|
||||
flag.Parse()
|
||||
|
||||
// Determine config path
|
||||
var configPath string
|
||||
var err error
|
||||
if *configFlag != "" {
|
||||
// If --config flag is provided, use it (handles both absolute and relative paths)
|
||||
if filepath.IsAbs(*configFlag) {
|
||||
configPath = *configFlag
|
||||
} else {
|
||||
configPath, err = config.DefaultPath(*configFlag)
|
||||
configPath, err := config.DefaultPath("gateway.yaml")
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "Failed to determine config path", zap.Error(err))
|
||||
fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 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))
|
||||
fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Load YAML
|
||||
type yamlCfg struct {
|
||||
ListenAddr string `yaml:"listen_addr"`
|
||||
ClientNamespace string `yaml:"client_namespace"`
|
||||
RQLiteDSN string `yaml:"rqlite_dsn"`
|
||||
Peers []string `yaml:"bootstrap_peers"`
|
||||
EnableHTTPS bool `yaml:"enable_https"`
|
||||
DomainName string `yaml:"domain_name"`
|
||||
TLSCacheDir string `yaml:"tls_cache_dir"`
|
||||
OlricServers []string `yaml:"olric_servers"`
|
||||
OlricTimeout string `yaml:"olric_timeout"`
|
||||
IPFSClusterAPIURL string `yaml:"ipfs_cluster_api_url"`
|
||||
IPFSAPIURL string `yaml:"ipfs_api_url"`
|
||||
IPFSTimeout string `yaml:"ipfs_timeout"`
|
||||
IPFSReplicationFactor int `yaml:"ipfs_replication_factor"`
|
||||
BootstrapPeers []string `yaml:"bootstrap_peers"`
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
@ -95,7 +61,7 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
zap.String("path", configPath),
|
||||
zap.Error(err))
|
||||
fmt.Fprintf(os.Stderr, "\nConfig file not found at %s\n", configPath)
|
||||
fmt.Fprintf(os.Stderr, "Generate it using: dbn config init --type gateway\n")
|
||||
fmt.Fprintf(os.Stderr, "Generate it using: network-cli config init --type gateway\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -113,15 +79,6 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
ClientNamespace: "default",
|
||||
BootstrapPeers: nil,
|
||||
RQLiteDSN: "",
|
||||
EnableHTTPS: false,
|
||||
DomainName: "",
|
||||
TLSCacheDir: "",
|
||||
OlricServers: nil,
|
||||
OlricTimeout: 0,
|
||||
IPFSClusterAPIURL: "",
|
||||
IPFSAPIURL: "",
|
||||
IPFSTimeout: 0,
|
||||
IPFSReplicationFactor: 0,
|
||||
}
|
||||
|
||||
if v := strings.TrimSpace(y.ListenAddr); v != "" {
|
||||
@ -133,64 +90,19 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
if v := strings.TrimSpace(y.RQLiteDSN); v != "" {
|
||||
cfg.RQLiteDSN = v
|
||||
}
|
||||
if len(y.Peers) > 0 {
|
||||
var peers []string
|
||||
for _, p := range y.Peers {
|
||||
if len(y.BootstrapPeers) > 0 {
|
||||
var bp []string
|
||||
for _, p := range y.BootstrapPeers {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
peers = append(peers, p)
|
||||
bp = append(bp, p)
|
||||
}
|
||||
}
|
||||
if len(peers) > 0 {
|
||||
cfg.BootstrapPeers = peers
|
||||
if len(bp) > 0 {
|
||||
cfg.BootstrapPeers = bp
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPS configuration
|
||||
cfg.EnableHTTPS = y.EnableHTTPS
|
||||
if v := strings.TrimSpace(y.DomainName); v != "" {
|
||||
cfg.DomainName = v
|
||||
}
|
||||
if v := strings.TrimSpace(y.TLSCacheDir); v != "" {
|
||||
cfg.TLSCacheDir = v
|
||||
} else if cfg.EnableHTTPS {
|
||||
// Default TLS cache directory if HTTPS is enabled but not specified
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
cfg.TLSCacheDir = filepath.Join(homeDir, ".orama", "tls-cache")
|
||||
}
|
||||
}
|
||||
|
||||
// Olric configuration
|
||||
if len(y.OlricServers) > 0 {
|
||||
cfg.OlricServers = y.OlricServers
|
||||
}
|
||||
if v := strings.TrimSpace(y.OlricTimeout); v != "" {
|
||||
if parsed, err := time.ParseDuration(v); err == nil {
|
||||
cfg.OlricTimeout = parsed
|
||||
} else {
|
||||
logger.ComponentWarn(logging.ComponentGeneral, "invalid olric_timeout, using default", zap.String("value", v), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// IPFS configuration
|
||||
if v := strings.TrimSpace(y.IPFSClusterAPIURL); v != "" {
|
||||
cfg.IPFSClusterAPIURL = v
|
||||
}
|
||||
if v := strings.TrimSpace(y.IPFSAPIURL); v != "" {
|
||||
cfg.IPFSAPIURL = v
|
||||
}
|
||||
if v := strings.TrimSpace(y.IPFSTimeout); v != "" {
|
||||
if parsed, err := time.ParseDuration(v); err == nil {
|
||||
cfg.IPFSTimeout = parsed
|
||||
} else {
|
||||
logger.ComponentWarn(logging.ComponentGeneral, "invalid ipfs_timeout, using default", zap.String("value", v), zap.Error(err))
|
||||
}
|
||||
}
|
||||
if y.IPFSReplicationFactor > 0 {
|
||||
cfg.IPFSReplicationFactor = y.IPFSReplicationFactor
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if errs := cfg.ValidateConfig(); len(errs) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\nGateway configuration errors (%d):\n", len(errs))
|
||||
@ -205,7 +117,7 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
zap.String("path", configPath),
|
||||
zap.String("addr", cfg.ListenAddr),
|
||||
zap.String("namespace", cfg.ClientNamespace),
|
||||
zap.Int("peer_count", len(cfg.BootstrapPeers)),
|
||||
zap.Int("bootstrap_peer_count", len(cfg.BootstrapPeers)),
|
||||
)
|
||||
|
||||
return cfg
|
||||
|
||||
@ -12,7 +12,6 @@ import (
|
||||
"github.com/DeBrosOfficial/network/pkg/gateway"
|
||||
"github.com/DeBrosOfficial/network/pkg/logging"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
func setupLogger() *logging.ColoredLogger {
|
||||
@ -43,123 +42,6 @@ func main() {
|
||||
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Creating HTTP server and routes...")
|
||||
|
||||
// Check if HTTPS is enabled
|
||||
if cfg.EnableHTTPS && cfg.DomainName != "" {
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS enabled with ACME",
|
||||
zap.String("domain", cfg.DomainName),
|
||||
zap.String("tls_cache_dir", cfg.TLSCacheDir),
|
||||
)
|
||||
|
||||
// Set up ACME manager
|
||||
manager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(cfg.DomainName),
|
||||
}
|
||||
|
||||
// Set cache directory if specified
|
||||
if cfg.TLSCacheDir != "" {
|
||||
manager.Cache = autocert.DirCache(cfg.TLSCacheDir)
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Using TLS certificate cache",
|
||||
zap.String("cache_dir", cfg.TLSCacheDir),
|
||||
)
|
||||
}
|
||||
|
||||
// Create HTTP server for ACME challenge (port 80)
|
||||
httpServer := &http.Server{
|
||||
Addr: ":80",
|
||||
Handler: manager.HTTPHandler(nil), // Redirects all HTTP traffic to HTTPS except ACME challenge
|
||||
}
|
||||
|
||||
// Create HTTPS server (port 443)
|
||||
httpsServer := &http.Server{
|
||||
Addr: ":443",
|
||||
Handler: gw.Routes(),
|
||||
TLSConfig: manager.TLSConfig(),
|
||||
}
|
||||
|
||||
// Start HTTP server for ACME challenge
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Starting HTTP server for ACME challenge on port 80...")
|
||||
httpLn, err := net.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTP listen address (port 80)", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTP listener bound", zap.String("listen_addr", httpLn.Addr().String()))
|
||||
|
||||
// Start HTTPS server
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Starting HTTPS server on port 443...")
|
||||
httpsLn, err := net.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTPS listen address (port 443)", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS listener bound", zap.String("listen_addr", httpsLn.Addr().String()))
|
||||
|
||||
// Serve HTTP in a goroutine
|
||||
httpServeErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
if err := httpServer.Serve(httpLn); err != nil && err != http.ErrServerClosed {
|
||||
httpServeErrCh <- err
|
||||
return
|
||||
}
|
||||
httpServeErrCh <- nil
|
||||
}()
|
||||
|
||||
// Serve HTTPS in a goroutine
|
||||
httpsServeErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
if err := httpsServer.ServeTLS(httpsLn, "", ""); err != nil && err != http.ErrServerClosed {
|
||||
httpsServeErrCh <- err
|
||||
return
|
||||
}
|
||||
httpsServeErrCh <- nil
|
||||
}()
|
||||
|
||||
// Wait for termination signal or server error
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case sig := <-quit:
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "shutdown signal received", zap.String("signal", sig.String()))
|
||||
case err := <-httpServeErrCh:
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "HTTP server error", zap.Error(err))
|
||||
} else {
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTP server exited normally")
|
||||
}
|
||||
case err := <-httpsServeErrCh:
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "HTTPS server error", zap.Error(err))
|
||||
} else {
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS server exited normally")
|
||||
}
|
||||
}
|
||||
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Shutting down gateway servers...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Shutdown HTTPS server
|
||||
if err := httpsServer.Shutdown(ctx); err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "HTTPS server shutdown error", zap.Error(err))
|
||||
} else {
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS server shutdown complete")
|
||||
}
|
||||
|
||||
// Shutdown HTTP server
|
||||
if err := httpServer.Shutdown(ctx); err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "HTTP server shutdown error", zap.Error(err))
|
||||
} else {
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTP server shutdown complete")
|
||||
}
|
||||
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Gateway shutdown complete")
|
||||
return
|
||||
}
|
||||
|
||||
// Standard HTTP server (no HTTPS)
|
||||
server := &http.Server{
|
||||
Addr: cfg.ListenAddr,
|
||||
Handler: gw.Routes(),
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@ -33,7 +32,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 ~/.orama (default: node.yaml)")
|
||||
configName = flag.String("config", "node.yaml", "Config filename in ~/.debros (default: node.yaml)")
|
||||
help = flag.Bool("help", false, "Show help")
|
||||
flag.Parse()
|
||||
|
||||
@ -63,20 +62,12 @@ func check_if_should_open_help(help *bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// select_data_dir validates that we can load the config from ~/.orama
|
||||
// select_data_dir validates that we can load the config from ~/.debros
|
||||
func select_data_dir_check(configName *string) {
|
||||
logger := setup_logger(logging.ComponentNode)
|
||||
|
||||
var configPath string
|
||||
var err error
|
||||
|
||||
// Check if configName is an absolute path
|
||||
if filepath.IsAbs(*configName) {
|
||||
// Use absolute path directly
|
||||
configPath = *configName
|
||||
} else {
|
||||
// Ensure config directory exists and is writable
|
||||
_, err = config.EnsureConfigDir()
|
||||
_, err := config.EnsureConfigDir()
|
||||
if err != nil {
|
||||
logger.Error("Failed to ensure config directory", zap.Error(err))
|
||||
fmt.Fprintf(os.Stderr, "\n❌ Configuration Error:\n")
|
||||
@ -88,12 +79,11 @@ func select_data_dir_check(configName *string) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
configPath, err = config.DefaultPath(*configName)
|
||||
configPath, err := config.DefaultPath(*configName)
|
||||
if err != nil {
|
||||
logger.Error("Failed to determine config path", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
logger.Error("Config file not found",
|
||||
@ -102,8 +92,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, " orama config init --type node\n")
|
||||
fmt.Fprintf(os.Stderr, " orama config init --type node --peers '<peer_multiaddr>'\n")
|
||||
fmt.Fprintf(os.Stderr, " network-cli config init --type bootstrap\n")
|
||||
fmt.Fprintf(os.Stderr, " network-cli config init --type node --bootstrap-peers '<peer_multiaddr>'\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@ -135,35 +125,16 @@ func startNode(ctx context.Context, cfg *config.Config, port int) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Save the peer ID to a file for CLI access
|
||||
// Save the peer ID to a file for CLI access (especially useful for bootstrap)
|
||||
peerID := n.GetPeerID()
|
||||
peerInfoFile := filepath.Join(dataDir, "peer.info")
|
||||
|
||||
// Extract advertise IP from config (prefer http_adv_address, fallback to raft_adv_address)
|
||||
advertiseIP := "0.0.0.0" // Default fallback
|
||||
if cfg.Discovery.HttpAdvAddress != "" {
|
||||
if host, _, err := net.SplitHostPort(cfg.Discovery.HttpAdvAddress); err == nil && host != "" && host != "localhost" {
|
||||
advertiseIP = host
|
||||
}
|
||||
} else if cfg.Discovery.RaftAdvAddress != "" {
|
||||
if host, _, err := net.SplitHostPort(cfg.Discovery.RaftAdvAddress); err == nil && host != "" && host != "localhost" {
|
||||
advertiseIP = host
|
||||
}
|
||||
}
|
||||
|
||||
// Determine IP protocol (IPv4 or IPv6) for multiaddr
|
||||
ipProtocol := "ip4"
|
||||
if ip := net.ParseIP(advertiseIP); ip != nil && ip.To4() == nil {
|
||||
ipProtocol = "ip6"
|
||||
}
|
||||
|
||||
peerMultiaddr := fmt.Sprintf("/%s/%s/tcp/%d/p2p/%s", ipProtocol, advertiseIP, port, peerID)
|
||||
peerMultiaddr := fmt.Sprintf("/ip4/0.0.0.0/tcp/%d/p2p/%s", port, peerID)
|
||||
|
||||
if err := os.WriteFile(peerInfoFile, []byte(peerMultiaddr), 0644); err != nil {
|
||||
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("Peer multiaddr: %s", zap.String("path", peerMultiaddr))
|
||||
logger.Info("Bootstrap multiaddr: %s", zap.String("path", peerMultiaddr))
|
||||
}
|
||||
|
||||
logger.Info("Node started successfully")
|
||||
@ -261,25 +232,16 @@ func main() {
|
||||
|
||||
check_if_should_open_help(help)
|
||||
|
||||
// Check if config file exists and determine path
|
||||
// Check if config file exists
|
||||
select_data_dir_check(configName)
|
||||
|
||||
// Determine config path (handle both absolute and relative paths)
|
||||
// Note: select_data_dir_check already validated the path exists, so we can safely determine it here
|
||||
var configPath string
|
||||
var err error
|
||||
if filepath.IsAbs(*configName) {
|
||||
// Absolute path passed directly (e.g., from systemd service)
|
||||
configPath = *configName
|
||||
} else {
|
||||
// Relative path - use DefaultPath which checks both ~/.orama/configs/ and ~/.orama/
|
||||
configPath, err = config.DefaultPath(*configName)
|
||||
// Load configuration from ~/.debros/node.yaml
|
||||
configPath, err := config.DefaultPath(*configName)
|
||||
if err != nil {
|
||||
logger.Error("Failed to determine config path", zap.Error(err))
|
||||
fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var cfg *config.Config
|
||||
var cfgErr error
|
||||
@ -293,10 +255,10 @@ func main() {
|
||||
|
||||
// Set default advertised addresses if empty
|
||||
if cfg.Discovery.HttpAdvAddress == "" {
|
||||
cfg.Discovery.HttpAdvAddress = fmt.Sprintf("localhost:%d", cfg.Database.RQLitePort)
|
||||
cfg.Discovery.HttpAdvAddress = fmt.Sprintf("127.0.0.1:%d", cfg.Database.RQLitePort)
|
||||
}
|
||||
if cfg.Discovery.RaftAdvAddress == "" {
|
||||
cfg.Discovery.RaftAdvAddress = fmt.Sprintf("localhost:%d", cfg.Database.RQLiteRaftPort)
|
||||
cfg.Discovery.RaftAdvAddress = fmt.Sprintf("127.0.0.1:%d", cfg.Database.RQLiteRaftPort)
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
@ -316,7 +278,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("peers", cfg.Discovery.BootstrapPeers),
|
||||
zap.Strings("bootstrap_peers", cfg.Discovery.BootstrapPeers),
|
||||
zap.String("rqlite_join_address", cfg.Database.RQLiteJoinAddress),
|
||||
zap.String("data_directory", cfg.Node.DataDir))
|
||||
|
||||
|
||||
@ -1,320 +0,0 @@
|
||||
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
19
debian/control
vendored
@ -1,19 +0,0 @@
|
||||
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
18
debian/postinst
vendored
@ -1,18 +0,0 @@
|
||||
#!/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"
|
||||
|
||||
@ -1,435 +0,0 @@
|
||||
# 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/)
|
||||
@ -1,546 +0,0 @@
|
||||
# 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
|
||||
@ -1,734 +0,0 @@
|
||||
# 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`
|
||||
@ -1,476 +0,0 @@
|
||||
# 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**
|
||||
@ -1,294 +0,0 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func TestAuth_MissingAPIKey(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Request without auth headers
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, GetGatewayURL()+"/v1/network/status", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(30 * time.Second)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should be unauthorized
|
||||
if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden {
|
||||
t.Logf("warning: expected 401/403 for missing auth, got %d (auth may not be enforced on this endpoint)", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_InvalidAPIKey(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Request with invalid API key
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, GetGatewayURL()+"/v1/cache/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer invalid-key-xyz")
|
||||
|
||||
client := NewHTTPClient(30 * time.Second)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should be unauthorized
|
||||
if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden {
|
||||
t.Logf("warning: expected 401/403 for invalid key, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_CacheWithoutAuth(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Request cache endpoint without auth
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/cache/health",
|
||||
SkipAuth: true,
|
||||
}
|
||||
|
||||
_, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
|
||||
// Should fail with 401 or 403
|
||||
if status != http.StatusUnauthorized && status != http.StatusForbidden {
|
||||
t.Logf("warning: expected 401/403 for cache without auth, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_StorageWithoutAuth(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Request storage endpoint without auth
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/storage/status/QmTest",
|
||||
SkipAuth: true,
|
||||
}
|
||||
|
||||
_, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
|
||||
// Should fail with 401 or 403
|
||||
if status != http.StatusUnauthorized && status != http.StatusForbidden {
|
||||
t.Logf("warning: expected 401/403 for storage without auth, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_RQLiteWithoutAuth(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Request rqlite endpoint without auth
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/schema",
|
||||
SkipAuth: true,
|
||||
}
|
||||
|
||||
_, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
|
||||
// Should fail with 401 or 403
|
||||
if status != http.StatusUnauthorized && status != http.StatusForbidden {
|
||||
t.Logf("warning: expected 401/403 for rqlite without auth, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_MalformedBearerToken(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Request with malformed bearer token
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, GetGatewayURL()+"/v1/cache/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
// Missing "Bearer " prefix
|
||||
req.Header.Set("Authorization", "invalid-token-format")
|
||||
|
||||
client := NewHTTPClient(30 * time.Second)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should be unauthorized
|
||||
if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden {
|
||||
t.Logf("warning: expected 401/403 for malformed token, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_ExpiredJWT(t *testing.T) {
|
||||
// Skip if JWT is not being used
|
||||
if GetJWT() == "" && GetAPIKey() == "" {
|
||||
t.Skip("No JWT or API key configured")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// This test would require an expired JWT token
|
||||
// For now, test with a clearly invalid JWT structure
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, GetGatewayURL()+"/v1/cache/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer expired.jwt.token")
|
||||
|
||||
client := NewHTTPClient(30 * time.Second)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should be unauthorized
|
||||
if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden {
|
||||
t.Logf("warning: expected 401/403 for expired JWT, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_EmptyBearerToken(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Request with empty bearer token
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, GetGatewayURL()+"/v1/cache/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer ")
|
||||
|
||||
client := NewHTTPClient(30 * time.Second)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should be unauthorized
|
||||
if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden {
|
||||
t.Logf("warning: expected 401/403 for empty token, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_DuplicateAuthHeaders(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Request with both API key and invalid JWT
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/cache/health",
|
||||
Headers: map[string]string{
|
||||
"Authorization": "Bearer " + GetAPIKey(),
|
||||
"X-API-Key": GetAPIKey(),
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
|
||||
// Should succeed if API key is valid
|
||||
if status != http.StatusOK {
|
||||
t.Logf("request with both headers returned %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_CaseSensitiveAPIKey(t *testing.T) {
|
||||
if GetAPIKey() == "" {
|
||||
t.Skip("No API key configured")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Request with incorrectly cased API key
|
||||
apiKey := GetAPIKey()
|
||||
incorrectKey := ""
|
||||
for i, ch := range apiKey {
|
||||
if i%2 == 0 && unicode.IsLetter(ch) {
|
||||
incorrectKey += string(unicode.ToUpper(ch)) // Convert to uppercase
|
||||
} else {
|
||||
incorrectKey += string(ch)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, GetGatewayURL()+"/v1/cache/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+incorrectKey)
|
||||
|
||||
client := NewHTTPClient(30 * time.Second)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// API keys should be case-sensitive
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
t.Logf("warning: API key check may not be case-sensitive (got 200)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_HealthEndpointNoAuth(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Health endpoint at /health should not require auth
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, GetGatewayURL()+"/v1/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(30 * time.Second)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should succeed without auth
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200 for /health without auth, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@ -1,511 +0,0 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCache_Health(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/cache/health",
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("health check 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["status"] != "ok" {
|
||||
t.Fatalf("expected status 'ok', got %v", resp["status"])
|
||||
}
|
||||
|
||||
if resp["service"] != "olric" {
|
||||
t.Fatalf("expected service 'olric', got %v", resp["service"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_PutGet(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
key := "test-key"
|
||||
value := "test-value"
|
||||
|
||||
// Put value
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": value,
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := putReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("put failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d: %s", status, string(body))
|
||||
}
|
||||
|
||||
// Get value
|
||||
getReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/get",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err = getReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("get failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
var getResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &getResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if getResp["value"] != value {
|
||||
t.Fatalf("expected value %q, got %v", value, getResp["value"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_PutGetJSON(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
key := "json-key"
|
||||
jsonValue := map[string]interface{}{
|
||||
"name": "John",
|
||||
"age": 30,
|
||||
"tags": []string{"developer", "golang"},
|
||||
}
|
||||
|
||||
// Put JSON value
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": jsonValue,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("put failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
// Get JSON value
|
||||
getReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/get",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := getReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("get failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
var getResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &getResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
retrievedValue := getResp["value"].(map[string]interface{})
|
||||
if retrievedValue["name"] != jsonValue["name"] {
|
||||
t.Fatalf("expected name %q, got %v", jsonValue["name"], retrievedValue["name"])
|
||||
}
|
||||
if retrievedValue["age"] != float64(30) {
|
||||
t.Fatalf("expected age 30, got %v", retrievedValue["age"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_Delete(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
key := "delete-key"
|
||||
value := "delete-value"
|
||||
|
||||
// Put value
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": value,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("put failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Delete value
|
||||
deleteReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/delete",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = deleteReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("delete failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
// Verify deletion
|
||||
getReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/get",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = getReq.Do(ctx)
|
||||
// Should get 404 for missing key
|
||||
if status != http.StatusNotFound {
|
||||
t.Fatalf("expected status 404 for deleted key, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_TTL(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
key := "ttl-key"
|
||||
value := "ttl-value"
|
||||
|
||||
// Put value with TTL
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": value,
|
||||
"ttl": "2s",
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("put with TTL failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
// Verify value exists
|
||||
getReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/get",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = getReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("get immediately after put failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Wait for TTL expiry (2 seconds + buffer)
|
||||
Delay(2500)
|
||||
|
||||
// Verify value is expired
|
||||
_, status, err = getReq.Do(ctx)
|
||||
if status != http.StatusNotFound {
|
||||
t.Logf("warning: TTL expiry may not be fully implemented; got status %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_Scan(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
|
||||
// Put multiple keys
|
||||
keys := []string{"user-1", "user-2", "session-1", "session-2"}
|
||||
for _, key := range keys {
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": "value-" + key,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("put failed: status %d, err %v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Scan all keys
|
||||
scanReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/scan",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := scanReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("scan failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
var scanResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &scanResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
keysResp := scanResp["keys"].([]interface{})
|
||||
if len(keysResp) < 4 {
|
||||
t.Fatalf("expected at least 4 keys, got %d", len(keysResp))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_ScanWithRegex(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
|
||||
// Put keys with different patterns
|
||||
keys := []string{"user-1", "user-2", "session-1", "session-2"}
|
||||
for _, key := range keys {
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": "value-" + key,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("put failed: status %d, err %v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Scan with regex pattern
|
||||
scanReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/scan",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"pattern": "^user-",
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := scanReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("scan with regex failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
var scanResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &scanResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
keysResp := scanResp["keys"].([]interface{})
|
||||
if len(keysResp) < 2 {
|
||||
t.Fatalf("expected at least 2 keys matching pattern, got %d", len(keysResp))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_MultiGet(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
keys := []string{"key-1", "key-2", "key-3"}
|
||||
|
||||
// Put values
|
||||
for i, key := range keys {
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": fmt.Sprintf("value-%d", i),
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("put failed: status %d, err %v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-get
|
||||
multiGetReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/mget",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"keys": keys,
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := multiGetReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("mget failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
var mgetResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &mgetResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
results := mgetResp["results"].([]interface{})
|
||||
if len(results) != 3 {
|
||||
t.Fatalf("expected 3 results, got %d", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_MissingDMap(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
getReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/get",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": "",
|
||||
"key": "any-key",
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := getReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400 for missing dmap, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_MissingKey(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
|
||||
getReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/get",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": "non-existent-key",
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := getReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusNotFound {
|
||||
t.Fatalf("expected status 404 for missing key, got %d", status)
|
||||
}
|
||||
}
|
||||
93
e2e/client_e2e_test.go
Normal file
93
e2e/client_e2e_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/client"
|
||||
)
|
||||
|
||||
func getenv(k, def string) string {
|
||||
if v := strings.TrimSpace(os.Getenv(k)); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func requireEnv(t *testing.T, key string) string {
|
||||
t.Helper()
|
||||
v := strings.TrimSpace(os.Getenv(key))
|
||||
if v == "" {
|
||||
t.Skipf("%s not set; skipping", key)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func TestClient_Database_CreateQueryMigrate(t *testing.T) {
|
||||
apiKey := requireEnv(t, "GATEWAY_API_KEY")
|
||||
namespace := getenv("E2E_CLIENT_NAMESPACE", "default")
|
||||
|
||||
cfg := client.DefaultClientConfig(namespace)
|
||||
cfg.APIKey = apiKey
|
||||
cfg.QuietMode = true
|
||||
|
||||
if v := strings.TrimSpace(os.Getenv("E2E_BOOTSTRAP_PEERS")); v != "" {
|
||||
parts := strings.Split(v, ",")
|
||||
var peers []string
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
cfg.BootstrapPeers = peers
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("E2E_RQLITE_NODES")); v != "" {
|
||||
nodes := strings.Fields(strings.ReplaceAll(v, ",", " "))
|
||||
cfg.DatabaseEndpoints = nodes
|
||||
}
|
||||
|
||||
c, err := client.NewClient(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("new client: %v", err)
|
||||
}
|
||||
if err := c.Connect(); err != nil {
|
||||
t.Fatalf("connect: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = c.Disconnect() })
|
||||
|
||||
// Unique table per run
|
||||
table := fmt.Sprintf("e2e_items_client_%d", time.Now().UnixNano())
|
||||
schema := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)", table)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
if err := c.Database().CreateTable(ctx, schema); err != nil {
|
||||
t.Fatalf("create table: %v", err)
|
||||
}
|
||||
// Insert via transaction
|
||||
stmts := []string{
|
||||
fmt.Sprintf("INSERT INTO %s(name) VALUES ('alpha')", table),
|
||||
fmt.Sprintf("INSERT INTO %s(name) VALUES ('beta')", table),
|
||||
}
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel2()
|
||||
if err := c.Database().Transaction(ctx2, stmts); err != nil {
|
||||
t.Fatalf("transaction: %v", err)
|
||||
}
|
||||
// Query rows
|
||||
ctx3, cancel3 := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel3()
|
||||
res, err := c.Database().Query(ctx3, fmt.Sprintf("SELECT name FROM %s ORDER BY id", table))
|
||||
if err != nil {
|
||||
t.Fatalf("query: %v", err)
|
||||
}
|
||||
if res.Count < 2 {
|
||||
t.Fatalf("expected at least 2 rows, got %d", res.Count)
|
||||
}
|
||||
}
|
||||
@ -1,503 +0,0 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestCache_ConcurrentWrites tests concurrent cache writes
|
||||
func TestCache_ConcurrentWrites(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
numGoroutines := 10
|
||||
var wg sync.WaitGroup
|
||||
var errorCount int32
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
|
||||
key := fmt.Sprintf("key-%d", idx)
|
||||
value := fmt.Sprintf("value-%d", idx)
|
||||
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": value,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
atomic.AddInt32(&errorCount, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if errorCount > 0 {
|
||||
t.Fatalf("expected no errors, got %d", errorCount)
|
||||
}
|
||||
|
||||
// Verify all values exist
|
||||
scanReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/scan",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := scanReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("scan failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
var scanResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &scanResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
keys := scanResp["keys"].([]interface{})
|
||||
if len(keys) < numGoroutines {
|
||||
t.Fatalf("expected at least %d keys, got %d", numGoroutines, len(keys))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCache_ConcurrentReads tests concurrent cache reads
|
||||
func TestCache_ConcurrentReads(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
key := "shared-key"
|
||||
value := "shared-value"
|
||||
|
||||
// Put value first
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": value,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("put failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Read concurrently
|
||||
numGoroutines := 10
|
||||
var wg sync.WaitGroup
|
||||
var errorCount int32
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
getReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/get",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := getReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
atomic.AddInt32(&errorCount, 1)
|
||||
return
|
||||
}
|
||||
|
||||
var getResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &getResp); err != nil {
|
||||
atomic.AddInt32(&errorCount, 1)
|
||||
return
|
||||
}
|
||||
|
||||
if getResp["value"] != value {
|
||||
atomic.AddInt32(&errorCount, 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if errorCount > 0 {
|
||||
t.Fatalf("expected no errors, got %d", errorCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCache_ConcurrentDeleteAndWrite tests concurrent delete and write
|
||||
func TestCache_ConcurrentDeleteAndWrite(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
var wg sync.WaitGroup
|
||||
var errorCount int32
|
||||
|
||||
numWrites := 5
|
||||
numDeletes := 3
|
||||
|
||||
// Write keys
|
||||
for i := 0; i < numWrites; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
|
||||
key := fmt.Sprintf("key-%d", idx)
|
||||
value := fmt.Sprintf("value-%d", idx)
|
||||
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": value,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
atomic.AddInt32(&errorCount, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Delete some keys
|
||||
for i := 0; i < numDeletes; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
|
||||
key := fmt.Sprintf("key-%d", idx)
|
||||
|
||||
deleteReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/delete",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := deleteReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
atomic.AddInt32(&errorCount, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if errorCount > 0 {
|
||||
t.Fatalf("expected no errors, got %d", errorCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRQLite_ConcurrentInserts tests concurrent database inserts
|
||||
func TestRQLite_ConcurrentInserts(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
table := GenerateTableName()
|
||||
schema := fmt.Sprintf(
|
||||
"CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER)",
|
||||
table,
|
||||
)
|
||||
|
||||
// Create table
|
||||
createReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/create-table",
|
||||
Body: map[string]interface{}{
|
||||
"schema": schema,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := createReq.Do(ctx)
|
||||
if err != nil || (status != http.StatusCreated && status != http.StatusOK) {
|
||||
t.Fatalf("create table failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Insert concurrently
|
||||
numInserts := 10
|
||||
var wg sync.WaitGroup
|
||||
var errorCount int32
|
||||
|
||||
for i := 0; i < numInserts; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
|
||||
txReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/transaction",
|
||||
Body: map[string]interface{}{
|
||||
"statements": []string{
|
||||
fmt.Sprintf("INSERT INTO %s(value) VALUES (%d)", table, idx),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := txReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
atomic.AddInt32(&errorCount, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if errorCount > 0 {
|
||||
t.Logf("warning: %d concurrent inserts failed", errorCount)
|
||||
}
|
||||
|
||||
// Verify count
|
||||
queryReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/query",
|
||||
Body: map[string]interface{}{
|
||||
"sql": fmt.Sprintf("SELECT COUNT(*) as count FROM %s", table),
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := queryReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("count query failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
var countResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &countResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if rows, ok := countResp["rows"].([]interface{}); ok && len(rows) > 0 {
|
||||
row := rows[0].([]interface{})
|
||||
count := int(row[0].(float64))
|
||||
if count < numInserts {
|
||||
t.Logf("warning: expected %d inserts, got %d", numInserts, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRQLite_LargeBatchTransaction tests a large transaction with many statements
|
||||
func TestRQLite_LargeBatchTransaction(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
table := GenerateTableName()
|
||||
schema := fmt.Sprintf(
|
||||
"CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT)",
|
||||
table,
|
||||
)
|
||||
|
||||
// Create table
|
||||
createReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/create-table",
|
||||
Body: map[string]interface{}{
|
||||
"schema": schema,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := createReq.Do(ctx)
|
||||
if err != nil || (status != http.StatusCreated && status != http.StatusOK) {
|
||||
t.Fatalf("create table failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Create large batch (100 statements)
|
||||
var ops []map[string]interface{}
|
||||
for i := 0; i < 100; i++ {
|
||||
ops = append(ops, map[string]interface{}{
|
||||
"kind": "exec",
|
||||
"sql": fmt.Sprintf("INSERT INTO %s(value) VALUES ('value-%d')", table, i),
|
||||
})
|
||||
}
|
||||
|
||||
txReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/transaction",
|
||||
Body: map[string]interface{}{
|
||||
"ops": ops,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = txReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("large batch transaction failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Verify count
|
||||
queryReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/query",
|
||||
Body: map[string]interface{}{
|
||||
"sql": fmt.Sprintf("SELECT COUNT(*) as count FROM %s", table),
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := queryReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("count query failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
var countResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &countResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if rows, ok := countResp["rows"].([]interface{}); ok && len(rows) > 0 {
|
||||
row := rows[0].([]interface{})
|
||||
if int(row[0].(float64)) != 100 {
|
||||
t.Fatalf("expected 100 rows, got %v", row[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCache_TTLExpiryWithSleep tests TTL expiry with a controlled sleep
|
||||
func TestCache_TTLExpiryWithSleep(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
key := "ttl-expiry-key"
|
||||
value := "ttl-expiry-value"
|
||||
|
||||
// Put value with 2 second TTL
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": value,
|
||||
"ttl": "2s",
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("put with TTL failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Verify exists immediately
|
||||
getReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/get",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = getReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("get immediately after put failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Sleep for TTL duration + buffer
|
||||
Delay(2500)
|
||||
|
||||
// Try to get after TTL expires
|
||||
_, status, err = getReq.Do(ctx)
|
||||
if status == http.StatusOK {
|
||||
t.Logf("warning: TTL expiry may not be fully implemented; key still exists after TTL")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCache_ConcurrentWriteAndDelete tests concurrent writes and deletes on same key
|
||||
func TestCache_ConcurrentWriteAndDelete(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dmap := GenerateDMapName()
|
||||
key := "contested-key"
|
||||
|
||||
// Alternate between writes and deletes
|
||||
numIterations := 5
|
||||
for i := 0; i < numIterations; i++ {
|
||||
// Write
|
||||
putReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/put",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
"value": fmt.Sprintf("value-%d", i),
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := putReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("put failed at iteration %d: status %d, err %v", i, status, err)
|
||||
}
|
||||
|
||||
// Read
|
||||
getReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/get",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = getReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("get failed at iteration %d: status %d, err %v", i, status, err)
|
||||
}
|
||||
|
||||
// Delete
|
||||
deleteReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/cache/delete",
|
||||
Body: map[string]interface{}{
|
||||
"dmap": dmap,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = deleteReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Logf("warning: delete at iteration %d failed: status %d, err %v", i, status, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
968
e2e/env.go
968
e2e/env.go
@ -1,968 +0,0 @@
|
||||
//go:build e2e
|
||||
|
||||
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"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
gatewayURLCache string
|
||||
apiKeyCache string
|
||||
bootstrapCache []string
|
||||
rqliteCache []string
|
||||
ipfsClusterCache string
|
||||
ipfsAPICache string
|
||||
cacheMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// loadGatewayConfig loads gateway configuration from ~/.orama/gateway.yaml
|
||||
func loadGatewayConfig() (map[string]interface{}, error) {
|
||||
configPath, err := config.DefaultPath("gateway.yaml")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get gateway config path: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read gateway config: %w", err)
|
||||
}
|
||||
|
||||
var cfg map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse gateway config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// loadNodeConfig loads node configuration from ~/.orama/node-*.yaml
|
||||
func loadNodeConfig(filename string) (map[string]interface{}, error) {
|
||||
configPath, err := config.DefaultPath(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config path: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||
}
|
||||
|
||||
var cfg map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// GetGatewayURL returns the gateway base URL from config
|
||||
func GetGatewayURL() string {
|
||||
cacheMutex.RLock()
|
||||
if gatewayURLCache != "" {
|
||||
defer cacheMutex.RUnlock()
|
||||
return gatewayURLCache
|
||||
}
|
||||
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 {
|
||||
if server, ok := gwCfg["server"].(map[interface{}]interface{}); ok {
|
||||
if port, ok := server["port"].(int); ok {
|
||||
url := fmt.Sprintf("http://localhost:%d", port)
|
||||
cacheMutex.Lock()
|
||||
gatewayURLCache = url
|
||||
cacheMutex.Unlock()
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return "http://localhost:6001"
|
||||
}
|
||||
|
||||
// GetRQLiteNodes returns rqlite endpoint addresses from config
|
||||
func GetRQLiteNodes() []string {
|
||||
cacheMutex.RLock()
|
||||
if len(rqliteCache) > 0 {
|
||||
defer cacheMutex.RUnlock()
|
||||
return rqliteCache
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if db, ok := nodeCfg["database"].(map[interface{}]interface{}); ok {
|
||||
if rqlitePort, ok := db["rqlite_port"].(int); ok {
|
||||
nodes := []string{fmt.Sprintf("http://localhost:%d", rqlitePort)}
|
||||
cacheMutex.Lock()
|
||||
rqliteCache = nodes
|
||||
cacheMutex.Unlock()
|
||||
return nodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return []string{"http://localhost:5001"}
|
||||
}
|
||||
|
||||
// queryAPIKeyFromRQLite queries the SQLite database directly for an API key
|
||||
func queryAPIKeyFromRQLite() (string, error) {
|
||||
// 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 all node data directories (both production and development paths)
|
||||
dbPaths := []string{
|
||||
// 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 {
|
||||
// Check if database file exists
|
||||
if _, err := os.Stat(dbPath); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Open SQLite database
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Set timeout for connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Query the api_keys table
|
||||
row := db.QueryRowContext(ctx, "SELECT key FROM api_keys ORDER BY id LIMIT 1")
|
||||
var apiKey string
|
||||
if err := row.Scan(&apiKey); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
continue // Try next database
|
||||
}
|
||||
continue // Skip this database on error
|
||||
}
|
||||
|
||||
if apiKey != "" {
|
||||
return apiKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to retrieve API key from any SQLite database")
|
||||
}
|
||||
|
||||
// GetAPIKey returns the gateway API key from rqlite or cache
|
||||
func GetAPIKey() string {
|
||||
cacheMutex.RLock()
|
||||
if apiKeyCache != "" {
|
||||
defer cacheMutex.RUnlock()
|
||||
return apiKeyCache
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// Query rqlite for API key
|
||||
apiKey, err := queryAPIKeyFromRQLite()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
cacheMutex.Lock()
|
||||
apiKeyCache = apiKey
|
||||
cacheMutex.Unlock()
|
||||
|
||||
return apiKey
|
||||
}
|
||||
|
||||
// GetJWT returns the gateway JWT token (currently not auto-discovered)
|
||||
func GetJWT() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetBootstrapPeers returns bootstrap peer addresses from config
|
||||
func GetBootstrapPeers() []string {
|
||||
cacheMutex.RLock()
|
||||
if len(bootstrapCache) > 0 {
|
||||
defer cacheMutex.RUnlock()
|
||||
return bootstrapCache
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
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
|
||||
|
||||
for _, cfgFile := range configFiles {
|
||||
nodeCfg, err := loadNodeConfig(cfgFile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
discovery, ok := nodeCfg["discovery"].(map[interface{}]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rawPeers, ok := discovery["bootstrap_peers"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, v := range rawPeers {
|
||||
peerStr, ok := v.(string)
|
||||
if !ok || peerStr == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[peerStr]; exists {
|
||||
continue
|
||||
}
|
||||
seen[peerStr] = struct{}{}
|
||||
peers = append(peers, peerStr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(peers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cacheMutex.Lock()
|
||||
bootstrapCache = peers
|
||||
cacheMutex.Unlock()
|
||||
|
||||
return peers
|
||||
}
|
||||
|
||||
// GetIPFSClusterURL returns the IPFS cluster API URL from config
|
||||
func GetIPFSClusterURL() string {
|
||||
cacheMutex.RLock()
|
||||
if ipfsClusterCache != "" {
|
||||
defer cacheMutex.RUnlock()
|
||||
return ipfsClusterCache
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// Try to load from node config
|
||||
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
|
||||
}
|
||||
|
||||
if db, ok := nodeCfg["database"].(map[interface{}]interface{}); ok {
|
||||
if ipfs, ok := db["ipfs"].(map[interface{}]interface{}); ok {
|
||||
if url, ok := ipfs["cluster_api_url"].(string); ok && url != "" {
|
||||
cacheMutex.Lock()
|
||||
ipfsClusterCache = url
|
||||
cacheMutex.Unlock()
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return "http://localhost:9094"
|
||||
}
|
||||
|
||||
// GetIPFSAPIURL returns the IPFS API URL from config
|
||||
func GetIPFSAPIURL() string {
|
||||
cacheMutex.RLock()
|
||||
if ipfsAPICache != "" {
|
||||
defer cacheMutex.RUnlock()
|
||||
return ipfsAPICache
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// Try to load from node config
|
||||
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
|
||||
}
|
||||
|
||||
if db, ok := nodeCfg["database"].(map[interface{}]interface{}); ok {
|
||||
if ipfs, ok := db["ipfs"].(map[interface{}]interface{}); ok {
|
||||
if url, ok := ipfs["api_url"].(string); ok && url != "" {
|
||||
cacheMutex.Lock()
|
||||
ipfsAPICache = url
|
||||
cacheMutex.Unlock()
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return "http://localhost:5001"
|
||||
}
|
||||
|
||||
// GetClientNamespace returns the test client namespace from config
|
||||
func GetClientNamespace() string {
|
||||
// Try to load from node config
|
||||
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
|
||||
}
|
||||
|
||||
if discovery, ok := nodeCfg["discovery"].(map[interface{}]interface{}); ok {
|
||||
if ns, ok := discovery["node_namespace"].(string); ok && ns != "" {
|
||||
return ns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "default"
|
||||
}
|
||||
|
||||
// SkipIfMissingGateway skips the test if gateway is not accessible or API key not available
|
||||
func SkipIfMissingGateway(t *testing.T) {
|
||||
t.Helper()
|
||||
apiKey := GetAPIKey()
|
||||
if apiKey == "" {
|
||||
t.Skip("API key not available from rqlite; gateway tests skipped")
|
||||
}
|
||||
|
||||
// Verify gateway is accessible
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, GetGatewayURL()+"/v1/health", nil)
|
||||
if err != nil {
|
||||
t.Skip("Gateway not accessible; tests skipped")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := NewHTTPClient(5 * time.Second).Do(req)
|
||||
if err != nil {
|
||||
t.Skip("Gateway not accessible; tests skipped")
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// IsGatewayReady checks if the gateway is accessible and healthy
|
||||
func IsGatewayReady(ctx context.Context) bool {
|
||||
gatewayURL := GetGatewayURL()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, gatewayURL+"/v1/health", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp, err := NewHTTPClient(5 * time.Second).Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
|
||||
// NewHTTPClient creates an authenticated HTTP client for gateway requests
|
||||
func NewHTTPClient(timeout time.Duration) *http.Client {
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
// 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
|
||||
type HTTPRequest struct {
|
||||
Method string
|
||||
URL string
|
||||
Body interface{}
|
||||
Headers map[string]string
|
||||
Timeout time.Duration
|
||||
SkipAuth bool
|
||||
}
|
||||
|
||||
// Do executes an HTTP request and returns the response body
|
||||
func (hr *HTTPRequest) Do(ctx context.Context) ([]byte, int, error) {
|
||||
if hr.Timeout == 0 {
|
||||
hr.Timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
var reqBody io.Reader
|
||||
if hr.Body != nil {
|
||||
data, err := json.Marshal(hr.Body)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, hr.Method, hr.URL, reqBody)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Add headers
|
||||
if hr.Headers != nil {
|
||||
for k, v := range hr.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Add JSON content type if body is present
|
||||
if hr.Body != nil && req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
// Add auth headers
|
||||
if !hr.SkipAuth {
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("X-API-Key", apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
client := NewHTTPClient(hr.Timeout)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, resp.StatusCode, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
return respBody, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// DecodeJSON unmarshals response body into v
|
||||
func DecodeJSON(data []byte, v interface{}) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// NewNetworkClient creates a network client configured for e2e tests
|
||||
func NewNetworkClient(t *testing.T) client.NetworkClient {
|
||||
t.Helper()
|
||||
|
||||
namespace := GetClientNamespace()
|
||||
cfg := client.DefaultClientConfig(namespace)
|
||||
cfg.APIKey = GetAPIKey()
|
||||
cfg.QuietMode = true // Suppress debug logs in tests
|
||||
|
||||
if jwt := GetJWT(); jwt != "" {
|
||||
cfg.JWT = jwt
|
||||
}
|
||||
|
||||
if peers := GetBootstrapPeers(); len(peers) > 0 {
|
||||
cfg.BootstrapPeers = peers
|
||||
}
|
||||
|
||||
if nodes := GetRQLiteNodes(); len(nodes) > 0 {
|
||||
cfg.DatabaseEndpoints = nodes
|
||||
}
|
||||
|
||||
c, err := client.NewClient(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create network client: %v", err)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// GenerateUniqueID generates a unique identifier for test resources
|
||||
func GenerateUniqueID(prefix string) string {
|
||||
return fmt.Sprintf("%s_%d_%d", prefix, time.Now().UnixNano(), rand.Intn(10000))
|
||||
}
|
||||
|
||||
// GenerateTableName generates a unique table name for database tests
|
||||
func GenerateTableName() string {
|
||||
return GenerateUniqueID("e2e_test")
|
||||
}
|
||||
|
||||
// GenerateDMapName generates a unique dmap name for cache tests
|
||||
func GenerateDMapName() string {
|
||||
return GenerateUniqueID("test_dmap")
|
||||
}
|
||||
|
||||
// GenerateTopic generates a unique topic name for pubsub tests
|
||||
func GenerateTopic() string {
|
||||
return GenerateUniqueID("e2e_topic")
|
||||
}
|
||||
|
||||
// Delay pauses execution for the specified duration
|
||||
func Delay(ms int) {
|
||||
time.Sleep(time.Duration(ms) * time.Millisecond)
|
||||
}
|
||||
|
||||
// WaitForCondition waits for a condition with exponential backoff
|
||||
func WaitForCondition(maxWait time.Duration, check func() bool) error {
|
||||
deadline := time.Now().Add(maxWait)
|
||||
backoff := 100 * time.Millisecond
|
||||
|
||||
for {
|
||||
if check() {
|
||||
return nil
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("condition not met within %v", maxWait)
|
||||
}
|
||||
time.Sleep(backoff)
|
||||
if backoff < 2*time.Second {
|
||||
backoff = backoff * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestLogger creates a test logger for debugging
|
||||
func NewTestLogger(t *testing.T) *zap.Logger {
|
||||
t.Helper()
|
||||
config := zap.NewDevelopmentConfig()
|
||||
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
|
||||
logger, err := config.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create logger: %v", err)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
// CleanupDatabaseTable drops a table from the database after tests
|
||||
func CleanupDatabaseTable(t *testing.T, tableName string) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Query rqlite to drop the table
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Logf("warning: failed to get home directory for cleanup: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
dropSQL := fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName)
|
||||
if _, err := db.ExecContext(ctx, dropSQL); err != nil {
|
||||
t.Logf("warning: failed to drop table %s: %v", tableName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupDMapCache deletes a dmap from the cache after tests
|
||||
func CleanupDMapCache(t *testing.T, dmapName string) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodDelete,
|
||||
URL: GetGatewayURL() + "/v1/cache/dmap/" + dmapName,
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
_, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Logf("warning: failed to delete dmap %s: %v", dmapName, err)
|
||||
return
|
||||
}
|
||||
|
||||
if status != http.StatusOK && status != http.StatusNoContent && status != http.StatusNotFound {
|
||||
t.Logf("warning: delete dmap returned status %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupIPFSFile unpins a file from IPFS after tests
|
||||
func CleanupIPFSFile(t *testing.T, cid string) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := &ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(*cfg, logger)
|
||||
if err != nil {
|
||||
t.Logf("warning: failed to create IPFS client for cleanup: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.Unpin(ctx, cid); err != nil {
|
||||
t.Logf("warning: failed to unpin file %s: %v", cid, err)
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupCacheEntry deletes a cache entry after tests
|
||||
func CleanupCacheEntry(t *testing.T, dmapName, key string) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodDelete,
|
||||
URL: GetGatewayURL() + "/v1/cache/dmap/" + dmapName + "/key/" + key,
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
_, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Logf("warning: failed to delete cache entry: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if status != http.StatusOK && status != http.StatusNoContent && status != http.StatusNotFound {
|
||||
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()
|
||||
}
|
||||
}
|
||||
427
e2e/gateway_e2e_test.go
Normal file
427
e2e/gateway_e2e_test.go
Normal file
@ -0,0 +1,427 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func getEnv(key, def string) string {
|
||||
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func requireAPIKey(t *testing.T) string {
|
||||
t.Helper()
|
||||
key := strings.TrimSpace(os.Getenv("GATEWAY_API_KEY"))
|
||||
if key == "" {
|
||||
t.Skip("GATEWAY_API_KEY not set; skipping gateway auth-required tests")
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func gatewayBaseURL() string {
|
||||
return getEnv("GATEWAY_BASE_URL", "http://127.0.0.1:6001")
|
||||
}
|
||||
|
||||
func httpClient() *http.Client {
|
||||
return &http.Client{Timeout: 10 * time.Second}
|
||||
}
|
||||
|
||||
func authHeader(key string) http.Header {
|
||||
h := http.Header{}
|
||||
h.Set("Authorization", "Bearer "+key)
|
||||
h.Set("Content-Type", "application/json")
|
||||
return h
|
||||
}
|
||||
|
||||
func TestGateway_Health(t *testing.T) {
|
||||
base := gatewayBaseURL()
|
||||
resp, err := httpClient().Get(base + "/v1/health")
|
||||
if err != nil {
|
||||
t.Fatalf("health request error: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status: %d", resp.StatusCode)
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if body["status"] != "ok" {
|
||||
t.Fatalf("status not ok: %+v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGateway_PubSub_WS_Echo(t *testing.T) {
|
||||
key := requireAPIKey(t)
|
||||
base := gatewayBaseURL()
|
||||
|
||||
topic := fmt.Sprintf("e2e-ws-%d", time.Now().UnixNano())
|
||||
wsURL, hdr := toWSURL(base+"/v1/pubsub/ws?topic="+url.QueryEscape(topic)), http.Header{}
|
||||
hdr.Set("Authorization", "Bearer "+key)
|
||||
|
||||
c, _, err := websocket.DefaultDialer.Dial(wsURL, hdr)
|
||||
if err != nil {
|
||||
t.Fatalf("ws dial: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
defer c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
|
||||
msg := []byte("hello-ws")
|
||||
if err := c.WriteMessage(websocket.TextMessage, msg); err != nil {
|
||||
t.Fatalf("ws write: %v", err)
|
||||
}
|
||||
|
||||
_, data, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
t.Fatalf("ws read: %v", err)
|
||||
}
|
||||
if string(data) != string(msg) {
|
||||
t.Fatalf("ws echo mismatch: %q", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGateway_PubSub_RestPublishToWS(t *testing.T) {
|
||||
key := requireAPIKey(t)
|
||||
base := gatewayBaseURL()
|
||||
|
||||
topic := fmt.Sprintf("e2e-rest-%d", time.Now().UnixNano())
|
||||
wsURL, hdr := toWSURL(base+"/v1/pubsub/ws?topic="+url.QueryEscape(topic)), http.Header{}
|
||||
hdr.Set("Authorization", "Bearer "+key)
|
||||
c, _, err := websocket.DefaultDialer.Dial(wsURL, hdr)
|
||||
if err != nil {
|
||||
t.Fatalf("ws dial: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
// Publish via REST
|
||||
payload := randomBytes(24)
|
||||
b64 := base64.StdEncoding.EncodeToString(payload)
|
||||
body := fmt.Sprintf(`{"topic":"%s","data_base64":"%s"}`, topic, b64)
|
||||
req, _ := http.NewRequest(http.MethodPost, base+"/v1/pubsub/publish", strings.NewReader(body))
|
||||
req.Header = authHeader(key)
|
||||
resp, err := httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("publish do: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("publish status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Expect the message via WS
|
||||
_ = c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
_, data, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
t.Fatalf("ws read: %v", err)
|
||||
}
|
||||
if string(data) != string(payload) {
|
||||
t.Fatalf("payload mismatch: %q != %q", string(data), string(payload))
|
||||
}
|
||||
|
||||
// Topics list should include our topic (without namespace prefix)
|
||||
req2, _ := http.NewRequest(http.MethodGet, base+"/v1/pubsub/topics", nil)
|
||||
req2.Header = authHeader(key)
|
||||
resp2, err := httpClient().Do(req2)
|
||||
if err != nil {
|
||||
t.Fatalf("topics do: %v", err)
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
if resp2.StatusCode != http.StatusOK {
|
||||
t.Fatalf("topics status: %d", resp2.StatusCode)
|
||||
}
|
||||
var tlist struct {
|
||||
Topics []string `json:"topics"`
|
||||
}
|
||||
if err := json.NewDecoder(resp2.Body).Decode(&tlist); err != nil {
|
||||
t.Fatalf("topics decode: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, tt := range tlist.Topics {
|
||||
if tt == topic {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("topic %s not found in topics list", topic)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGateway_Database_CreateQueryMigrate(t *testing.T) {
|
||||
key := requireAPIKey(t)
|
||||
base := gatewayBaseURL()
|
||||
|
||||
// Create table
|
||||
schema := `CREATE TABLE IF NOT EXISTS e2e_items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)`
|
||||
body := fmt.Sprintf(`{"schema":%q}`, schema)
|
||||
req, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/create-table", strings.NewReader(body))
|
||||
req.Header = authHeader(key)
|
||||
resp, err := httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("create-table do: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("create-table status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Insert via transaction (simulate migration/data seed)
|
||||
txBody := `{"statements":["INSERT INTO e2e_items(name) VALUES ('one')","INSERT INTO e2e_items(name) VALUES ('two')"]}`
|
||||
req, _ = http.NewRequest(http.MethodPost, base+"/v1/rqlite/transaction", strings.NewReader(txBody))
|
||||
req.Header = authHeader(key)
|
||||
resp, err = httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("tx do: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("tx status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Query rows
|
||||
qBody := `{"sql":"SELECT name FROM e2e_items ORDER BY id ASC"}`
|
||||
req, _ = http.NewRequest(http.MethodPost, base+"/v1/rqlite/query", strings.NewReader(qBody))
|
||||
req.Header = authHeader(key)
|
||||
resp, err = httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("query do: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("query status: %d", resp.StatusCode)
|
||||
}
|
||||
var qr struct {
|
||||
Columns []string `json:"columns"`
|
||||
Rows [][]any `json:"rows"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&qr); err != nil {
|
||||
t.Fatalf("query decode: %v", err)
|
||||
}
|
||||
if qr.Count < 2 {
|
||||
t.Fatalf("expected at least 2 rows, got %d", qr.Count)
|
||||
}
|
||||
|
||||
// Schema endpoint returns tables
|
||||
req, _ = http.NewRequest(http.MethodGet, base+"/v1/rqlite/schema", nil)
|
||||
req.Header = authHeader(key)
|
||||
resp2, err := httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("schema do: %v", err)
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
if resp2.StatusCode != http.StatusOK {
|
||||
t.Fatalf("schema status: %d", resp2.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGateway_Database_DropTable(t *testing.T) {
|
||||
key := requireAPIKey(t)
|
||||
base := gatewayBaseURL()
|
||||
|
||||
table := fmt.Sprintf("e2e_tmp_%d", time.Now().UnixNano())
|
||||
schema := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, note TEXT)", table)
|
||||
// create
|
||||
body := fmt.Sprintf(`{"schema":%q}`, schema)
|
||||
req, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/create-table", strings.NewReader(body))
|
||||
req.Header = authHeader(key)
|
||||
resp, err := httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("create-table do: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("create-table status: %d", resp.StatusCode)
|
||||
}
|
||||
// drop
|
||||
dbody := fmt.Sprintf(`{"table":%q}`, table)
|
||||
req, _ = http.NewRequest(http.MethodPost, base+"/v1/rqlite/drop-table", strings.NewReader(dbody))
|
||||
req.Header = authHeader(key)
|
||||
resp, err = httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("drop-table do: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("drop-table status: %d", resp.StatusCode)
|
||||
}
|
||||
// verify not in schema
|
||||
req, _ = http.NewRequest(http.MethodGet, base+"/v1/rqlite/schema", nil)
|
||||
req.Header = authHeader(key)
|
||||
resp2, err := httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("schema do: %v", err)
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
if resp2.StatusCode != http.StatusOK {
|
||||
t.Fatalf("schema status: %d", resp2.StatusCode)
|
||||
}
|
||||
var schemaResp struct {
|
||||
Tables []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"tables"`
|
||||
}
|
||||
if err := json.NewDecoder(resp2.Body).Decode(&schemaResp); err != nil {
|
||||
t.Fatalf("schema decode: %v", err)
|
||||
}
|
||||
for _, tbl := range schemaResp.Tables {
|
||||
if tbl.Name == table {
|
||||
t.Fatalf("table %s still present after drop", table)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGateway_Database_RecreateWithFK(t *testing.T) {
|
||||
key := requireAPIKey(t)
|
||||
base := gatewayBaseURL()
|
||||
|
||||
// base tables
|
||||
orgs := fmt.Sprintf("e2e_orgs_%d", time.Now().UnixNano())
|
||||
users := fmt.Sprintf("e2e_users_%d", time.Now().UnixNano())
|
||||
createOrgs := fmt.Sprintf(`{"schema":%q}`, fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT)", orgs))
|
||||
createUsers := fmt.Sprintf(`{"schema":%q}`, fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT, org_id INTEGER, age TEXT)", users))
|
||||
|
||||
for _, body := range []string{createOrgs, createUsers} {
|
||||
req, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/create-table", strings.NewReader(body))
|
||||
req.Header = authHeader(key)
|
||||
resp, err := httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("create-table do: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("create-table status: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
// seed data
|
||||
txSeed := fmt.Sprintf(`{"statements":["INSERT INTO %s(id,name) VALUES (1,'org')","INSERT INTO %s(id,name,org_id,age) VALUES (1,'alice',1,'30')"]}`, orgs, users)
|
||||
req, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/transaction", strings.NewReader(txSeed))
|
||||
req.Header = authHeader(key)
|
||||
resp, err := httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("seed tx do: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("seed tx status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// migrate: change users.age TEXT -> INTEGER and add FK to orgs(id)
|
||||
// Note: Some backends may not support connection-scoped BEGIN/COMMIT or PRAGMA via HTTP.
|
||||
// We apply the standard recreate pattern without explicit PRAGMAs/transaction.
|
||||
txMig := fmt.Sprintf(`{"statements":[
|
||||
"CREATE TABLE %s_new (id INTEGER PRIMARY KEY, name TEXT, org_id INTEGER, age INTEGER, FOREIGN KEY(org_id) REFERENCES %s(id) ON DELETE CASCADE)",
|
||||
"INSERT INTO %s_new (id,name,org_id,age) SELECT id,name,org_id, CAST(age AS INTEGER) FROM %s",
|
||||
"DROP TABLE %s",
|
||||
"ALTER TABLE %s_new RENAME TO %s"
|
||||
]}`, users, orgs, users, users, users, users, users)
|
||||
req, _ = http.NewRequest(http.MethodPost, base+"/v1/rqlite/transaction", strings.NewReader(txMig))
|
||||
req.Header = authHeader(key)
|
||||
resp, err = httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("mig tx do: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("mig tx status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// verify schema type change
|
||||
qBody := fmt.Sprintf(`{"sql":"PRAGMA table_info(%s)"}`, users)
|
||||
req, _ = http.NewRequest(http.MethodPost, base+"/v1/rqlite/query", strings.NewReader(qBody))
|
||||
req.Header = authHeader(key)
|
||||
resp, err = httpClient().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("pragma do: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("pragma status: %d", resp.StatusCode)
|
||||
}
|
||||
var qr struct {
|
||||
Columns []string `json:"columns"`
|
||||
Rows [][]any `json:"rows"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&qr); err != nil {
|
||||
t.Fatalf("pragma decode: %v", err)
|
||||
}
|
||||
// column order: cid,name,type,notnull,dflt_value,pk
|
||||
ageIsInt := false
|
||||
for _, row := range qr.Rows {
|
||||
if len(row) >= 3 && fmt.Sprintf("%v", row[1]) == "age" {
|
||||
tstr := strings.ToUpper(fmt.Sprintf("%v", row[2]))
|
||||
if strings.Contains(tstr, "INT") {
|
||||
ageIsInt = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ageIsInt {
|
||||
// Fallback: inspect CREATE TABLE SQL from sqlite_master
|
||||
qBody2 := fmt.Sprintf(`{"sql":"SELECT sql FROM sqlite_master WHERE type='table' AND name='%s'"}`, users)
|
||||
req2, _ := http.NewRequest(http.MethodPost, base+"/v1/rqlite/query", strings.NewReader(qBody2))
|
||||
req2.Header = authHeader(key)
|
||||
resp3, err := httpClient().Do(req2)
|
||||
if err != nil {
|
||||
t.Fatalf("sqlite_master do: %v", err)
|
||||
}
|
||||
defer resp3.Body.Close()
|
||||
if resp3.StatusCode != http.StatusOK {
|
||||
t.Fatalf("sqlite_master status: %d", resp3.StatusCode)
|
||||
}
|
||||
var qr2 struct {
|
||||
Rows [][]any `json:"rows"`
|
||||
}
|
||||
if err := json.NewDecoder(resp3.Body).Decode(&qr2); err != nil {
|
||||
t.Fatalf("sqlite_master decode: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, row := range qr2.Rows {
|
||||
if len(row) > 0 {
|
||||
sql := strings.ToUpper(fmt.Sprintf("%v", row[0]))
|
||||
if strings.Contains(sql, "AGE INT") || strings.Contains(sql, "AGE INTEGER") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("age column type not INTEGER after migration")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toWSURL(httpURL string) string {
|
||||
u, err := url.Parse(httpURL)
|
||||
if err != nil {
|
||||
return httpURL
|
||||
}
|
||||
if u.Scheme == "https" {
|
||||
u.Scheme = "wss"
|
||||
} else {
|
||||
u.Scheme = "ws"
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func randomBytes(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
_, _ = rand.Read(b)
|
||||
return b
|
||||
}
|
||||
@ -1,400 +0,0 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/ipfs"
|
||||
)
|
||||
|
||||
func TestIPFSCluster_Health(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(cfg, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IPFS client: %v", err)
|
||||
}
|
||||
|
||||
err = client.Health(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("health check failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPFSCluster_GetPeerCount(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(cfg, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IPFS client: %v", err)
|
||||
}
|
||||
|
||||
peerCount, err := client.GetPeerCount(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("get peer count failed: %v", err)
|
||||
}
|
||||
|
||||
if peerCount < 0 {
|
||||
t.Fatalf("expected non-negative peer count, got %d", peerCount)
|
||||
}
|
||||
|
||||
t.Logf("IPFS cluster peers: %d", peerCount)
|
||||
}
|
||||
|
||||
func TestIPFSCluster_AddFile(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(cfg, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IPFS client: %v", err)
|
||||
}
|
||||
|
||||
content := []byte("IPFS cluster test content")
|
||||
result, err := client.Add(ctx, bytes.NewReader(content), "test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("add file failed: %v", err)
|
||||
}
|
||||
|
||||
if result.Cid == "" {
|
||||
t.Fatalf("expected non-empty CID")
|
||||
}
|
||||
|
||||
if result.Size != int64(len(content)) {
|
||||
t.Fatalf("expected size %d, got %d", len(content), result.Size)
|
||||
}
|
||||
|
||||
t.Logf("Added file with CID: %s", result.Cid)
|
||||
}
|
||||
|
||||
func TestIPFSCluster_PinFile(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(cfg, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IPFS client: %v", err)
|
||||
}
|
||||
|
||||
// Add file first
|
||||
content := []byte("IPFS pin test content")
|
||||
addResult, err := client.Add(ctx, bytes.NewReader(content), "pin-test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("add file failed: %v", err)
|
||||
}
|
||||
|
||||
cid := addResult.Cid
|
||||
|
||||
// Pin the file
|
||||
pinResult, err := client.Pin(ctx, cid, "pinned-file", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("pin file failed: %v", err)
|
||||
}
|
||||
|
||||
if pinResult.Cid != cid {
|
||||
t.Fatalf("expected cid %s, got %s", cid, pinResult.Cid)
|
||||
}
|
||||
|
||||
t.Logf("Pinned file: %s", cid)
|
||||
}
|
||||
|
||||
func TestIPFSCluster_PinStatus(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(cfg, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IPFS client: %v", err)
|
||||
}
|
||||
|
||||
// Add and pin file
|
||||
content := []byte("IPFS status test content")
|
||||
addResult, err := client.Add(ctx, bytes.NewReader(content), "status-test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("add file failed: %v", err)
|
||||
}
|
||||
|
||||
cid := addResult.Cid
|
||||
|
||||
pinResult, err := client.Pin(ctx, cid, "status-test", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("pin file failed: %v", err)
|
||||
}
|
||||
|
||||
if pinResult.Cid != cid {
|
||||
t.Fatalf("expected cid %s, got %s", cid, pinResult.Cid)
|
||||
}
|
||||
|
||||
// Give pin time to propagate
|
||||
Delay(1000)
|
||||
|
||||
// Get status
|
||||
status, err := client.PinStatus(ctx, cid)
|
||||
if err != nil {
|
||||
t.Fatalf("get pin status failed: %v", err)
|
||||
}
|
||||
|
||||
if status.Cid != cid {
|
||||
t.Fatalf("expected cid %s, got %s", cid, status.Cid)
|
||||
}
|
||||
|
||||
if status.Name != "status-test" {
|
||||
t.Fatalf("expected name 'status-test', got %s", status.Name)
|
||||
}
|
||||
|
||||
if status.ReplicationFactor < 1 {
|
||||
t.Logf("warning: replication factor is %d, expected >= 1", status.ReplicationFactor)
|
||||
}
|
||||
|
||||
t.Logf("Pin status: %s (replication: %d, peers: %d)", status.Status, status.ReplicationFactor, len(status.Peers))
|
||||
}
|
||||
|
||||
func TestIPFSCluster_UnpinFile(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(cfg, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IPFS client: %v", err)
|
||||
}
|
||||
|
||||
// Add and pin file
|
||||
content := []byte("IPFS unpin test content")
|
||||
addResult, err := client.Add(ctx, bytes.NewReader(content), "unpin-test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("add file failed: %v", err)
|
||||
}
|
||||
|
||||
cid := addResult.Cid
|
||||
|
||||
_, err = client.Pin(ctx, cid, "unpin-test", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("pin file failed: %v", err)
|
||||
}
|
||||
|
||||
// Unpin file
|
||||
err = client.Unpin(ctx, cid)
|
||||
if err != nil {
|
||||
t.Fatalf("unpin file failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Unpinned file: %s", cid)
|
||||
}
|
||||
|
||||
func TestIPFSCluster_GetFile(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(cfg, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IPFS client: %v", err)
|
||||
}
|
||||
|
||||
// Add file
|
||||
content := []byte("IPFS get test content")
|
||||
addResult, err := client.Add(ctx, bytes.NewReader(content), "get-test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("add file failed: %v", err)
|
||||
}
|
||||
|
||||
cid := addResult.Cid
|
||||
|
||||
// Give time for propagation
|
||||
Delay(1000)
|
||||
|
||||
// Get file
|
||||
rc, err := client.Get(ctx, cid, GetIPFSAPIURL())
|
||||
if err != nil {
|
||||
t.Fatalf("get file failed: %v", err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
retrievedContent, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read content: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(retrievedContent, content) {
|
||||
t.Fatalf("content mismatch: expected %q, got %q", string(content), string(retrievedContent))
|
||||
}
|
||||
|
||||
t.Logf("Retrieved file: %s (%d bytes)", cid, len(retrievedContent))
|
||||
}
|
||||
|
||||
func TestIPFSCluster_LargeFile(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(cfg, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IPFS client: %v", err)
|
||||
}
|
||||
|
||||
// Create 5MB file
|
||||
content := bytes.Repeat([]byte("x"), 5*1024*1024)
|
||||
result, err := client.Add(ctx, bytes.NewReader(content), "large.bin")
|
||||
if err != nil {
|
||||
t.Fatalf("add large file failed: %v", err)
|
||||
}
|
||||
|
||||
if result.Cid == "" {
|
||||
t.Fatalf("expected non-empty CID")
|
||||
}
|
||||
|
||||
if result.Size != int64(len(content)) {
|
||||
t.Fatalf("expected size %d, got %d", len(content), result.Size)
|
||||
}
|
||||
|
||||
t.Logf("Added large file with CID: %s (%d bytes)", result.Cid, result.Size)
|
||||
}
|
||||
|
||||
func TestIPFSCluster_ReplicationFactor(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(cfg, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IPFS client: %v", err)
|
||||
}
|
||||
|
||||
// Add file
|
||||
content := []byte("IPFS replication test content")
|
||||
addResult, err := client.Add(ctx, bytes.NewReader(content), "replication-test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("add file failed: %v", err)
|
||||
}
|
||||
|
||||
cid := addResult.Cid
|
||||
|
||||
// Pin with specific replication factor
|
||||
replicationFactor := 2
|
||||
pinResult, err := client.Pin(ctx, cid, "replication-test", replicationFactor)
|
||||
if err != nil {
|
||||
t.Fatalf("pin file failed: %v", err)
|
||||
}
|
||||
|
||||
if pinResult.Cid != cid {
|
||||
t.Fatalf("expected cid %s, got %s", cid, pinResult.Cid)
|
||||
}
|
||||
|
||||
// Give time for replication
|
||||
Delay(2000)
|
||||
|
||||
// Check status
|
||||
status, err := client.PinStatus(ctx, cid)
|
||||
if err != nil {
|
||||
t.Fatalf("get pin status failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Replication factor: requested=%d, actual=%d, peers=%d", replicationFactor, status.ReplicationFactor, len(status.Peers))
|
||||
}
|
||||
|
||||
func TestIPFSCluster_MultipleFiles(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := NewTestLogger(t)
|
||||
cfg := ipfs.Config{
|
||||
ClusterAPIURL: GetIPFSClusterURL(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client, err := ipfs.NewClient(cfg, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IPFS client: %v", err)
|
||||
}
|
||||
|
||||
// Add multiple files
|
||||
numFiles := 5
|
||||
var cids []string
|
||||
|
||||
for i := 0; i < numFiles; i++ {
|
||||
content := []byte(fmt.Sprintf("File %d", i))
|
||||
result, err := client.Add(ctx, bytes.NewReader(content), fmt.Sprintf("file%d.txt", i))
|
||||
if err != nil {
|
||||
t.Fatalf("add file %d failed: %v", i, err)
|
||||
}
|
||||
cids = append(cids, result.Cid)
|
||||
}
|
||||
|
||||
if len(cids) != numFiles {
|
||||
t.Fatalf("expected %d files added, got %d", numFiles, len(cids))
|
||||
}
|
||||
|
||||
// Verify all files exist
|
||||
for i, cid := range cids {
|
||||
status, err := client.PinStatus(ctx, cid)
|
||||
if err != nil {
|
||||
t.Logf("warning: failed to get status for file %d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if status.Cid != cid {
|
||||
t.Fatalf("expected cid %s, got %s", cid, status.Cid)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Successfully added and verified %d files", numFiles)
|
||||
}
|
||||
@ -1,294 +0,0 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLibP2P_PeerConnectivity(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create and connect client
|
||||
c := NewNetworkClient(t)
|
||||
if err := c.Connect(); err != nil {
|
||||
t.Fatalf("connect failed: %v", err)
|
||||
}
|
||||
defer c.Disconnect()
|
||||
|
||||
// Verify peer connectivity through the gateway
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/network/peers",
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("peers request 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)
|
||||
}
|
||||
|
||||
peers := resp["peers"].([]interface{})
|
||||
if len(peers) == 0 {
|
||||
t.Logf("warning: no peers connected (cluster may still be initializing)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibP2P_BootstrapPeers(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
bootstrapPeers := GetBootstrapPeers()
|
||||
if len(bootstrapPeers) == 0 {
|
||||
t.Skipf("E2E_BOOTSTRAP_PEERS not set; skipping")
|
||||
}
|
||||
|
||||
// Create client with bootstrap peers explicitly set
|
||||
c := NewNetworkClient(t)
|
||||
if err := c.Connect(); err != nil {
|
||||
t.Fatalf("connect failed: %v", err)
|
||||
}
|
||||
defer c.Disconnect()
|
||||
|
||||
// Give peer discovery time
|
||||
Delay(2000)
|
||||
|
||||
// Verify we're connected (check via gateway status)
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/network/status",
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("status request 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["connected"] != true {
|
||||
t.Logf("warning: client not connected to network (cluster may still be initializing)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibP2P_MultipleClientConnections(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create multiple clients
|
||||
c1 := NewNetworkClient(t)
|
||||
c2 := NewNetworkClient(t)
|
||||
c3 := NewNetworkClient(t)
|
||||
|
||||
if err := c1.Connect(); err != nil {
|
||||
t.Fatalf("c1 connect failed: %v", err)
|
||||
}
|
||||
defer c1.Disconnect()
|
||||
|
||||
if err := c2.Connect(); err != nil {
|
||||
t.Fatalf("c2 connect failed: %v", err)
|
||||
}
|
||||
defer c2.Disconnect()
|
||||
|
||||
if err := c3.Connect(); err != nil {
|
||||
t.Fatalf("c3 connect failed: %v", err)
|
||||
}
|
||||
defer c3.Disconnect()
|
||||
|
||||
// Give peer discovery time
|
||||
Delay(2000)
|
||||
|
||||
// Verify gateway sees multiple peers
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/network/peers",
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("peers request 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)
|
||||
}
|
||||
|
||||
peers := resp["peers"].([]interface{})
|
||||
if len(peers) < 1 {
|
||||
t.Logf("warning: expected at least 1 peer, got %d", len(peers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibP2P_ReconnectAfterDisconnect(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
c := NewNetworkClient(t)
|
||||
|
||||
// Connect
|
||||
if err := c.Connect(); err != nil {
|
||||
t.Fatalf("connect failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify connected via gateway
|
||||
req1 := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/network/status",
|
||||
}
|
||||
|
||||
_, status1, err := req1.Do(ctx)
|
||||
if err != nil || status1 != http.StatusOK {
|
||||
t.Logf("warning: gateway check failed before disconnect: status %d, err %v", status1, err)
|
||||
}
|
||||
|
||||
// Disconnect
|
||||
if err := c.Disconnect(); err != nil {
|
||||
t.Logf("warning: disconnect failed: %v", err)
|
||||
}
|
||||
|
||||
// Give time for disconnect to propagate
|
||||
Delay(500)
|
||||
|
||||
// Reconnect
|
||||
if err := c.Connect(); err != nil {
|
||||
t.Fatalf("reconnect failed: %v", err)
|
||||
}
|
||||
defer c.Disconnect()
|
||||
|
||||
// Verify connected via gateway again
|
||||
req2 := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/network/status",
|
||||
}
|
||||
|
||||
_, status2, err := req2.Do(ctx)
|
||||
if err != nil || status2 != http.StatusOK {
|
||||
t.Logf("warning: gateway check failed after reconnect: status %d, err %v", status2, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibP2P_PeerDiscovery(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create client
|
||||
c := NewNetworkClient(t)
|
||||
if err := c.Connect(); err != nil {
|
||||
t.Fatalf("connect failed: %v", err)
|
||||
}
|
||||
defer c.Disconnect()
|
||||
|
||||
// Give peer discovery time
|
||||
Delay(3000)
|
||||
|
||||
// Get peer list
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/network/peers",
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("peers request 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)
|
||||
}
|
||||
|
||||
peers := resp["peers"].([]interface{})
|
||||
if len(peers) == 0 {
|
||||
t.Logf("warning: no peers discovered (cluster may not have multiple nodes)")
|
||||
} else {
|
||||
// Verify peer format (should be multiaddr strings)
|
||||
for _, p := range peers {
|
||||
peerStr := p.(string)
|
||||
if !strings.Contains(peerStr, "/p2p/") && !strings.Contains(peerStr, "/ipfs/") {
|
||||
t.Logf("warning: unexpected peer format: %s", peerStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibP2P_PeerAddressFormat(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create client
|
||||
c := NewNetworkClient(t)
|
||||
if err := c.Connect(); err != nil {
|
||||
t.Fatalf("connect failed: %v", err)
|
||||
}
|
||||
defer c.Disconnect()
|
||||
|
||||
// Get peer list
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/network/peers",
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("peers request 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)
|
||||
}
|
||||
|
||||
peers := resp["peers"].([]interface{})
|
||||
for _, p := range peers {
|
||||
peerStr := p.(string)
|
||||
// Multiaddrs should start with /
|
||||
if !strings.HasPrefix(peerStr, "/") {
|
||||
t.Fatalf("expected multiaddr format, got %s", peerStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,223 +0,0 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNetwork_Health(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/health",
|
||||
SkipAuth: true,
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("health check 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["status"] != "ok" {
|
||||
t.Fatalf("expected status 'ok', got %v", resp["status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetwork_Status(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/network/status",
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("status check 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 _, ok := resp["connected"]; !ok {
|
||||
t.Fatalf("expected 'connected' field in response")
|
||||
}
|
||||
|
||||
if _, ok := resp["peer_count"]; !ok {
|
||||
t.Fatalf("expected 'peer_count' field in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetwork_Peers(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/network/peers",
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("peers check 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 _, ok := resp["peers"]; !ok {
|
||||
t.Fatalf("expected 'peers' field in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetwork_ProxyAnonSuccess(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/proxy/anon",
|
||||
Body: map[string]interface{}{
|
||||
"url": "https://httpbin.org/get",
|
||||
"method": "GET",
|
||||
"headers": map[string]string{"User-Agent": "DeBros-E2E-Test/1.0"},
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("proxy anon request failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d: %s", status, string(body))
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := DecodeJSON(body, &resp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if resp["status_code"] != float64(200) {
|
||||
t.Fatalf("expected proxy status 200, got %v", resp["status_code"])
|
||||
}
|
||||
|
||||
if _, ok := resp["body"]; !ok {
|
||||
t.Fatalf("expected 'body' field in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetwork_ProxyAnonBadURL(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/proxy/anon",
|
||||
Body: map[string]interface{}{
|
||||
"url": "http://localhost:1/nonexistent",
|
||||
"method": "GET",
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := req.Do(ctx)
|
||||
if err == nil && status == http.StatusOK {
|
||||
t.Fatalf("expected error for bad URL, got status 200")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetwork_ProxyAnonPostRequest(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/proxy/anon",
|
||||
Body: map[string]interface{}{
|
||||
"url": "https://httpbin.org/post",
|
||||
"method": "POST",
|
||||
"headers": map[string]string{"User-Agent": "DeBros-E2E-Test/1.0"},
|
||||
"body": "test_data",
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("proxy anon POST failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d: %s", status, string(body))
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := DecodeJSON(body, &resp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if resp["status_code"] != float64(200) {
|
||||
t.Fatalf("expected proxy status 200, got %v", resp["status_code"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetwork_Unauthorized(t *testing.T) {
|
||||
// Test without API key
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create request without auth
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/network/status",
|
||||
SkipAuth: true,
|
||||
}
|
||||
|
||||
_, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusUnauthorized && status != http.StatusForbidden {
|
||||
t.Logf("warning: expected 401/403, got %d (auth may not be enforced on this endpoint)", status)
|
||||
}
|
||||
}
|
||||
@ -1,461 +0,0 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestPubSub_SubscribePublish tests basic pub/sub functionality via WebSocket
|
||||
func TestPubSub_SubscribePublish(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
topic := GenerateTopic()
|
||||
message := "test-message-from-publisher"
|
||||
|
||||
// Create subscriber first
|
||||
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 message
|
||||
if err := publisher.Publish([]byte(message)); err != nil {
|
||||
t.Fatalf("publish failed: %v", err)
|
||||
}
|
||||
|
||||
// Receive message on subscriber
|
||||
msg, err := subscriber.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("receive failed: %v", err)
|
||||
}
|
||||
|
||||
if string(msg) != message {
|
||||
t.Fatalf("expected message %q, got %q", message, string(msg))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPubSub_MultipleSubscribers tests that multiple subscribers receive the same message
|
||||
func TestPubSub_MultipleSubscribers(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
topic := GenerateTopic()
|
||||
message1 := "message-1"
|
||||
message2 := "message-2"
|
||||
|
||||
// Create two subscribers
|
||||
sub1, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber1: %v", err)
|
||||
}
|
||||
defer sub1.Close()
|
||||
|
||||
sub2, err := NewWSPubSubClient(t, topic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subscriber2: %v", err)
|
||||
}
|
||||
defer sub2.Close()
|
||||
|
||||
// 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 := publisher.Publish([]byte(message1)); err != nil {
|
||||
t.Fatalf("publish1 failed: %v", err)
|
||||
}
|
||||
|
||||
// Both subscribers should receive first message
|
||||
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 := 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 := publisher.Publish([]byte(message2)); err != nil {
|
||||
t.Fatalf("publish2 failed: %v", err)
|
||||
}
|
||||
|
||||
// Both subscribers should receive second message
|
||||
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 := 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)
|
||||
|
||||
topic := GenerateTopic()
|
||||
message := "duplicate-test-message"
|
||||
|
||||
// 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 the same message multiple times
|
||||
for i := 0; i < 3; i++ {
|
||||
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)
|
||||
receivedCount := 0
|
||||
for receivedCount < 3 {
|
||||
_, err := subscriber.ReceiveWithTimeout(5 * time.Second)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
receivedCount++
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
topic := GenerateTopic()
|
||||
numMessages := 10
|
||||
|
||||
// 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 multiple messages concurrently
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < numMessages; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
msg := fmt.Sprintf("concurrent-msg-%d", idx)
|
||||
if err := publisher.Publish([]byte(msg)); err != nil {
|
||||
t.Logf("publish %d failed: %v", idx, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Receive messages
|
||||
receivedCount := 0
|
||||
for receivedCount < numMessages {
|
||||
_, err := subscriber.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
receivedCount++
|
||||
}
|
||||
|
||||
if receivedCount < numMessages {
|
||||
t.Logf("expected %d messages, got %d (some may have been dropped)", numMessages, receivedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPubSub_TopicIsolation tests that messages are isolated to their topics
|
||||
func TestPubSub_TopicIsolation(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
topic1 := GenerateTopic()
|
||||
topic2 := GenerateTopic()
|
||||
msg1 := "message-on-topic1"
|
||||
msg2 := "message-on-topic2"
|
||||
|
||||
// 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
|
||||
if err := pub1.Publish([]byte(msg1)); err != nil {
|
||||
t.Fatalf("publish1 failed: %v", err)
|
||||
}
|
||||
|
||||
// Sub1 should receive msg1 only
|
||||
received1, err := sub1.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("sub1 receive failed: %v", err)
|
||||
}
|
||||
if string(received1) != msg1 {
|
||||
t.Fatalf("sub1: expected %q, got %q", msg1, string(received1))
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
topic := GenerateTopic()
|
||||
|
||||
// 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 empty message
|
||||
if err := publisher.Publish([]byte("")); err != nil {
|
||||
t.Fatalf("publish empty failed: %v", err)
|
||||
}
|
||||
|
||||
// Receive on subscriber - should get empty message
|
||||
msg, err := subscriber.ReceiveWithTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("receive failed: %v", err)
|
||||
}
|
||||
|
||||
if len(msg) != 0 {
|
||||
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))
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
//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"])
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,446 +0,0 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRQLite_CreateTable(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
table := GenerateTableName()
|
||||
schema := fmt.Sprintf(
|
||||
"CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)",
|
||||
table,
|
||||
)
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/create-table",
|
||||
Body: map[string]interface{}{
|
||||
"schema": schema,
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("create table request failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusCreated && status != http.StatusOK {
|
||||
t.Fatalf("expected status 201 or 200, got %d: %s", status, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRQLite_InsertQuery(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
table := GenerateTableName()
|
||||
schema := fmt.Sprintf(
|
||||
"CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
|
||||
table,
|
||||
)
|
||||
|
||||
// Create table
|
||||
createReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/create-table",
|
||||
Body: map[string]interface{}{
|
||||
"schema": schema,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := createReq.Do(ctx)
|
||||
if err != nil || (status != http.StatusCreated && status != http.StatusOK) {
|
||||
t.Fatalf("create table failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Insert rows
|
||||
insertReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/transaction",
|
||||
Body: map[string]interface{}{
|
||||
"statements": []string{
|
||||
fmt.Sprintf("INSERT INTO %s(name) VALUES ('alice')", table),
|
||||
fmt.Sprintf("INSERT INTO %s(name) VALUES ('bob')", table),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = insertReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("insert failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Query rows
|
||||
queryReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/query",
|
||||
Body: map[string]interface{}{
|
||||
"sql": fmt.Sprintf("SELECT name FROM %s ORDER BY id", table),
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := queryReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("query failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
var queryResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &queryResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if queryResp["count"].(float64) < 2 {
|
||||
t.Fatalf("expected at least 2 rows, got %v", queryResp["count"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRQLite_DropTable(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
table := GenerateTableName()
|
||||
schema := fmt.Sprintf(
|
||||
"CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, note TEXT)",
|
||||
table,
|
||||
)
|
||||
|
||||
// Create table
|
||||
createReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/create-table",
|
||||
Body: map[string]interface{}{
|
||||
"schema": schema,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := createReq.Do(ctx)
|
||||
if err != nil || (status != http.StatusCreated && status != http.StatusOK) {
|
||||
t.Fatalf("create table failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Drop table
|
||||
dropReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/drop-table",
|
||||
Body: map[string]interface{}{
|
||||
"table": table,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = dropReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("drop table request failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
// Verify table doesn't exist via schema
|
||||
schemaReq := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/schema",
|
||||
}
|
||||
|
||||
body, status, err := schemaReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Logf("warning: failed to verify schema after drop: status %d, err %v", status, err)
|
||||
return
|
||||
}
|
||||
|
||||
var schemaResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &schemaResp); err != nil {
|
||||
t.Logf("warning: failed to decode schema response: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if tables, ok := schemaResp["tables"].([]interface{}); ok {
|
||||
for _, tbl := range tables {
|
||||
tblMap := tbl.(map[string]interface{})
|
||||
if tblMap["name"] == table {
|
||||
t.Fatalf("table %s still present after drop", table)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRQLite_Schema(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/schema",
|
||||
}
|
||||
|
||||
body, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("schema request 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 _, ok := resp["tables"]; !ok {
|
||||
t.Fatalf("expected 'tables' field in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRQLite_MalformedSQL(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/query",
|
||||
Body: map[string]interface{}{
|
||||
"sql": "SELECT * FROM nonexistent_table WHERE invalid syntax",
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
|
||||
// Should get an error response
|
||||
if status == http.StatusOK {
|
||||
t.Fatalf("expected error for malformed SQL, got status 200")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRQLite_LargeTransaction(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
table := GenerateTableName()
|
||||
schema := fmt.Sprintf(
|
||||
"CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER)",
|
||||
table,
|
||||
)
|
||||
|
||||
// Create table
|
||||
createReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/create-table",
|
||||
Body: map[string]interface{}{
|
||||
"schema": schema,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := createReq.Do(ctx)
|
||||
if err != nil || (status != http.StatusCreated && status != http.StatusOK) {
|
||||
t.Fatalf("create table failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Generate large transaction (50 inserts)
|
||||
var statements []string
|
||||
for i := 0; i < 50; i++ {
|
||||
statements = append(statements, fmt.Sprintf("INSERT INTO %s(value) VALUES (%d)", table, i))
|
||||
}
|
||||
|
||||
txReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/transaction",
|
||||
Body: map[string]interface{}{
|
||||
"statements": statements,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = txReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("large transaction failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Verify all rows were inserted
|
||||
queryReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/query",
|
||||
Body: map[string]interface{}{
|
||||
"sql": fmt.Sprintf("SELECT COUNT(*) as count FROM %s", table),
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := queryReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("count query failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
var countResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &countResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
// Extract count from result
|
||||
if rows, ok := countResp["rows"].([]interface{}); ok && len(rows) > 0 {
|
||||
row := rows[0].([]interface{})
|
||||
if row[0].(float64) != 50 {
|
||||
t.Fatalf("expected 50 rows, got %v", row[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRQLite_ForeignKeyMigration(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
orgsTable := GenerateTableName()
|
||||
usersTable := GenerateTableName()
|
||||
|
||||
// Create base tables
|
||||
createOrgsReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/create-table",
|
||||
Body: map[string]interface{}{
|
||||
"schema": fmt.Sprintf(
|
||||
"CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT)",
|
||||
orgsTable,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := createOrgsReq.Do(ctx)
|
||||
if err != nil || (status != http.StatusCreated && status != http.StatusOK) {
|
||||
t.Fatalf("create orgs table failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
createUsersReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/create-table",
|
||||
Body: map[string]interface{}{
|
||||
"schema": fmt.Sprintf(
|
||||
"CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT, org_id INTEGER, age TEXT)",
|
||||
usersTable,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = createUsersReq.Do(ctx)
|
||||
if err != nil || (status != http.StatusCreated && status != http.StatusOK) {
|
||||
t.Fatalf("create users table failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Seed data
|
||||
seedReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/transaction",
|
||||
Body: map[string]interface{}{
|
||||
"statements": []string{
|
||||
fmt.Sprintf("INSERT INTO %s(id,name) VALUES (1,'org')", orgsTable),
|
||||
fmt.Sprintf("INSERT INTO %s(id,name,org_id,age) VALUES (1,'alice',1,'30')", usersTable),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = seedReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("seed transaction failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Migrate: change age type and add FK
|
||||
migrationReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/transaction",
|
||||
Body: map[string]interface{}{
|
||||
"statements": []string{
|
||||
fmt.Sprintf(
|
||||
"CREATE TABLE %s_new (id INTEGER PRIMARY KEY, name TEXT, org_id INTEGER, age INTEGER, FOREIGN KEY(org_id) REFERENCES %s(id) ON DELETE CASCADE)",
|
||||
usersTable, orgsTable,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"INSERT INTO %s_new (id,name,org_id,age) SELECT id,name,org_id, CAST(age AS INTEGER) FROM %s",
|
||||
usersTable, usersTable,
|
||||
),
|
||||
fmt.Sprintf("DROP TABLE %s", usersTable),
|
||||
fmt.Sprintf("ALTER TABLE %s_new RENAME TO %s", usersTable, usersTable),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err = migrationReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("migration transaction failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
// Verify data is intact
|
||||
queryReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/query",
|
||||
Body: map[string]interface{}{
|
||||
"sql": fmt.Sprintf("SELECT name, org_id, age FROM %s", usersTable),
|
||||
},
|
||||
}
|
||||
|
||||
body, status, err := queryReq.Do(ctx)
|
||||
if err != nil || status != http.StatusOK {
|
||||
t.Fatalf("query after migration failed: status %d, err %v", status, err)
|
||||
}
|
||||
|
||||
var queryResp map[string]interface{}
|
||||
if err := DecodeJSON(body, &queryResp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if queryResp["count"].(float64) != 1 {
|
||||
t.Fatalf("expected 1 row after migration, got %v", queryResp["count"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRQLite_DropNonexistentTable(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dropReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/rqlite/drop-table",
|
||||
Body: map[string]interface{}{
|
||||
"table": "nonexistent_table_xyz_" + fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
},
|
||||
}
|
||||
|
||||
_, status, err := dropReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Logf("warning: drop nonexistent table request failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Should get an error (400 or 404)
|
||||
if status == http.StatusOK {
|
||||
t.Logf("warning: expected error for dropping nonexistent table, got status 200")
|
||||
}
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
//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)
|
||||
}
|
||||
}
|
||||
@ -1,550 +0,0 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// uploadFile is a helper to upload a file to storage
|
||||
func uploadFile(t *testing.T, ctx context.Context, content []byte, filename string) string {
|
||||
t.Helper()
|
||||
|
||||
// Create multipart form
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create form file: %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, bytes.NewReader(content)); err != nil {
|
||||
t.Fatalf("failed to copy data: %v", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GetGatewayURL()+"/v1/storage/upload", &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
// Add auth headers
|
||||
if jwt := GetJWT(); jwt != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+jwt)
|
||||
} else if apiKey := GetAPIKey(); apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(5 * time.Minute)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("upload request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("upload failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
result, err := DecodeJSONFromReader(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode upload response: %v", err)
|
||||
}
|
||||
|
||||
return result["cid"].(string)
|
||||
}
|
||||
|
||||
// DecodeJSON is a helper to decode JSON from io.ReadCloser
|
||||
func DecodeJSONFromReader(rc io.ReadCloser) (map[string]interface{}, error) {
|
||||
defer rc.Close()
|
||||
body, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result map[string]interface{}
|
||||
err = DecodeJSON(body, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func TestStorage_UploadText(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
content := []byte("Hello, IPFS!")
|
||||
filename := "test.txt"
|
||||
|
||||
// Create multipart form
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create form file: %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, bytes.NewReader(content)); err != nil {
|
||||
t.Fatalf("failed to copy data: %v", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GetGatewayURL()+"/v1/storage/upload", &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(5 * time.Minute)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("upload request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("upload failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := DecodeJSON(body, &result); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if result["cid"] == nil {
|
||||
t.Fatalf("expected cid in response")
|
||||
}
|
||||
|
||||
if result["name"] != filename {
|
||||
t.Fatalf("expected name %q, got %v", filename, result["name"])
|
||||
}
|
||||
|
||||
if result["size"] == nil || result["size"].(float64) <= 0 {
|
||||
t.Fatalf("expected positive size")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_UploadBinary(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// PNG header
|
||||
content := []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}
|
||||
filename := "test.png"
|
||||
|
||||
// Create multipart form
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create form file: %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, bytes.NewReader(content)); err != nil {
|
||||
t.Fatalf("failed to copy data: %v", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GetGatewayURL()+"/v1/storage/upload", &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(5 * time.Minute)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("upload request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("upload failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := DecodeJSON(body, &result); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if result["cid"] == nil {
|
||||
t.Fatalf("expected cid in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_UploadLarge(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Create 1MB file
|
||||
content := bytes.Repeat([]byte("x"), 1024*1024)
|
||||
filename := "large.bin"
|
||||
|
||||
// Create multipart form
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create form file: %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, bytes.NewReader(content)); err != nil {
|
||||
t.Fatalf("failed to copy data: %v", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GetGatewayURL()+"/v1/storage/upload", &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(5 * time.Minute)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("upload request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("upload failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := DecodeJSON(body, &result); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if result["size"] != float64(1024*1024) {
|
||||
t.Fatalf("expected size %d, got %v", 1024*1024, result["size"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_PinUnpin(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
content := []byte("test content for pinning")
|
||||
|
||||
// Upload file first
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "pin-test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create form file: %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, bytes.NewReader(content)); err != nil {
|
||||
t.Fatalf("failed to copy data: %v", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
|
||||
// Create upload request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GetGatewayURL()+"/v1/storage/upload", &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(5 * time.Minute)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("upload failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var uploadResult map[string]interface{}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := DecodeJSON(body, &uploadResult); err != nil {
|
||||
t.Fatalf("failed to decode upload response: %v", err)
|
||||
}
|
||||
|
||||
cid := uploadResult["cid"].(string)
|
||||
|
||||
// Pin the file
|
||||
pinReq := &HTTPRequest{
|
||||
Method: http.MethodPost,
|
||||
URL: GetGatewayURL() + "/v1/storage/pin",
|
||||
Body: map[string]interface{}{
|
||||
"cid": cid,
|
||||
"name": "pinned-file",
|
||||
},
|
||||
}
|
||||
|
||||
body2, status, err := pinReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("pin failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d: %s", status, string(body2))
|
||||
}
|
||||
|
||||
var pinResult map[string]interface{}
|
||||
if err := DecodeJSON(body2, &pinResult); err != nil {
|
||||
t.Fatalf("failed to decode pin response: %v", err)
|
||||
}
|
||||
|
||||
if pinResult["cid"] != cid {
|
||||
t.Fatalf("expected cid %s, got %v", cid, pinResult["cid"])
|
||||
}
|
||||
|
||||
// Unpin the file
|
||||
unpinReq := &HTTPRequest{
|
||||
Method: http.MethodDelete,
|
||||
URL: GetGatewayURL() + "/v1/storage/unpin/" + cid,
|
||||
}
|
||||
|
||||
body3, status, err := unpinReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unpin failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d: %s", status, string(body3))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_Status(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
content := []byte("test content for status")
|
||||
|
||||
// Upload file first
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "status-test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create form file: %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, bytes.NewReader(content)); err != nil {
|
||||
t.Fatalf("failed to copy data: %v", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
|
||||
// Create upload request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GetGatewayURL()+"/v1/storage/upload", &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(5 * time.Minute)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("upload failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var uploadResult map[string]interface{}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := DecodeJSON(body, &uploadResult); err != nil {
|
||||
t.Fatalf("failed to decode upload response: %v", err)
|
||||
}
|
||||
|
||||
cid := uploadResult["cid"].(string)
|
||||
|
||||
// Get status
|
||||
statusReq := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/storage/status/" + cid,
|
||||
}
|
||||
|
||||
statusBody, status, err := statusReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("status request failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", status)
|
||||
}
|
||||
|
||||
var statusResult map[string]interface{}
|
||||
if err := DecodeJSON(statusBody, &statusResult); err != nil {
|
||||
t.Fatalf("failed to decode status response: %v", err)
|
||||
}
|
||||
|
||||
if statusResult["cid"] != cid {
|
||||
t.Fatalf("expected cid %s, got %v", cid, statusResult["cid"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_InvalidCID(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
statusReq := &HTTPRequest{
|
||||
Method: http.MethodGet,
|
||||
URL: GetGatewayURL() + "/v1/storage/status/QmInvalidCID123456789",
|
||||
}
|
||||
|
||||
_, status, err := statusReq.Do(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("status request failed: %v", err)
|
||||
}
|
||||
|
||||
if status != http.StatusNotFound {
|
||||
t.Logf("warning: expected status 404 for invalid CID, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_GetByteRange(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
content := []byte("0123456789abcdefghijklmnopqrstuvwxyz")
|
||||
|
||||
// Upload file first
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "range-test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create form file: %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, bytes.NewReader(content)); err != nil {
|
||||
t.Fatalf("failed to copy data: %v", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
|
||||
// Create upload request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GetGatewayURL()+"/v1/storage/upload", &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(5 * time.Minute)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("upload failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var uploadResult map[string]interface{}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := DecodeJSON(body, &uploadResult); err != nil {
|
||||
t.Fatalf("failed to decode upload response: %v", err)
|
||||
}
|
||||
|
||||
cid := uploadResult["cid"].(string)
|
||||
|
||||
// Get full content
|
||||
getReq, err := http.NewRequestWithContext(ctx, http.MethodGet, GetGatewayURL()+"/v1/storage/get/"+cid, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create get request: %v", err)
|
||||
}
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
getReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
resp, err = client.Do(getReq)
|
||||
if err != nil {
|
||||
t.Fatalf("get request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
retrievedContent, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(retrievedContent, content) {
|
||||
t.Fatalf("content mismatch: expected %q, got %q", string(content), string(retrievedContent))
|
||||
}
|
||||
}
|
||||
158
example.http
158
example.http
@ -1,158 +0,0 @@
|
||||
### 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}}
|
||||
151
examples/basic_usage.go
Normal file
151
examples/basic_usage.go
Normal file
@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create client configuration
|
||||
config := client.DefaultClientConfig("example_app")
|
||||
config.BootstrapPeers = []string{
|
||||
"/ip4/127.0.0.1/tcp/4001/p2p/QmBootstrap1",
|
||||
}
|
||||
|
||||
// Create network client
|
||||
networkClient, err := client.NewClient(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create network client: %v", err)
|
||||
}
|
||||
|
||||
// Connect to network
|
||||
if err := networkClient.Connect(); err != nil {
|
||||
log.Fatalf("Failed to connect to network: %v", err)
|
||||
}
|
||||
defer networkClient.Disconnect()
|
||||
|
||||
log.Printf("Connected to network successfully!")
|
||||
|
||||
// Example: Database operations
|
||||
demonstrateDatabase(networkClient)
|
||||
|
||||
// Example: Pub/Sub messaging
|
||||
demonstratePubSub(networkClient)
|
||||
|
||||
// Example: Network information
|
||||
demonstrateNetworkInfo(networkClient)
|
||||
|
||||
log.Printf("Example completed successfully!")
|
||||
}
|
||||
|
||||
func demonstrateDatabase(client client.NetworkClient) {
|
||||
ctx := context.Background()
|
||||
db := client.Database()
|
||||
|
||||
log.Printf("=== Database Operations ===")
|
||||
|
||||
// Create a table
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY,
|
||||
content TEXT NOT NULL,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`
|
||||
if err := db.CreateTable(ctx, schema); err != nil {
|
||||
log.Printf("Error creating table: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Table created successfully")
|
||||
|
||||
// Insert some data
|
||||
insertSQL := "INSERT INTO messages (content) VALUES (?)"
|
||||
result, err := db.Query(ctx, insertSQL, "Hello, distributed world!")
|
||||
if err != nil {
|
||||
log.Printf("Error inserting data: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Data inserted, result: %+v", result)
|
||||
|
||||
// Query data
|
||||
selectSQL := "SELECT * FROM messages"
|
||||
result, err = db.Query(ctx, selectSQL)
|
||||
if err != nil {
|
||||
log.Printf("Error querying data: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Query result: %+v", result)
|
||||
}
|
||||
|
||||
func demonstratePubSub(client client.NetworkClient) {
|
||||
ctx := context.Background()
|
||||
pubsub := client.PubSub()
|
||||
|
||||
log.Printf("=== Pub/Sub Operations ===")
|
||||
|
||||
// Subscribe to a topic
|
||||
topic := "notifications"
|
||||
handler := func(topic string, data []byte) error {
|
||||
log.Printf("Received message on topic '%s': %s", topic, string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := pubsub.Subscribe(ctx, topic, handler); err != nil {
|
||||
log.Printf("Error subscribing: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Subscribed to topic: %s", topic)
|
||||
|
||||
// Publish a message
|
||||
message := []byte("Hello from pub/sub!")
|
||||
if err := pubsub.Publish(ctx, topic, message); err != nil {
|
||||
log.Printf("Error publishing: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Message published")
|
||||
|
||||
// Wait a bit for message delivery
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
// List topics
|
||||
topics, err := pubsub.ListTopics(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error listing topics: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Subscribed topics: %v", topics)
|
||||
}
|
||||
|
||||
func demonstrateNetworkInfo(client client.NetworkClient) {
|
||||
ctx := context.Background()
|
||||
network := client.Network()
|
||||
|
||||
log.Printf("=== Network Information ===")
|
||||
|
||||
// Get network status
|
||||
status, err := network.GetStatus(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error getting status: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Network status: %+v", status)
|
||||
|
||||
// Get peers
|
||||
peers, err := network.GetPeers(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error getting peers: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Connected peers: %+v", peers)
|
||||
|
||||
// Get client health
|
||||
health, err := client.Health()
|
||||
if err != nil {
|
||||
log.Printf("Error getting health: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Client health: %+v", health)
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
#!/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."
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
23
examples/sdk-typescript/README.md
Normal file
23
examples/sdk-typescript/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
# DeBros Gateway TypeScript SDK (Minimal Example)
|
||||
|
||||
Minimal, dependency-light wrapper around the HTTP Gateway.
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
export GATEWAY_BASE_URL=http://127.0.0.1:6001
|
||||
export GATEWAY_API_KEY=your_api_key
|
||||
```
|
||||
|
||||
```ts
|
||||
import { GatewayClient } from './src/client';
|
||||
|
||||
const c = new GatewayClient(process.env.GATEWAY_BASE_URL!, process.env.GATEWAY_API_KEY!);
|
||||
await c.createTable('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await c.transaction([
|
||||
'INSERT INTO users (id,name) VALUES (1,\'Alice\')'
|
||||
]);
|
||||
const res = await c.query('SELECT name FROM users WHERE id = ?', [1]);
|
||||
console.log(res.rows);
|
||||
```
|
||||
17
examples/sdk-typescript/package.json
Normal file
17
examples/sdk-typescript/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "debros-gateway-sdk",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"isomorphic-ws": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
154
examples/sdk-typescript/src/client.ts
Normal file
154
examples/sdk-typescript/src/client.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import WebSocket from "isomorphic-ws";
|
||||
|
||||
export class GatewayClient {
|
||||
constructor(
|
||||
private baseUrl: string,
|
||||
private apiKey: string,
|
||||
private http = fetch
|
||||
) {}
|
||||
|
||||
private headers(json = true): Record<string, string> {
|
||||
const h: Record<string, string> = { "X-API-Key": this.apiKey };
|
||||
if (json) h["Content-Type"] = "application/json";
|
||||
return h;
|
||||
}
|
||||
|
||||
// Database
|
||||
async createTable(schema: string): Promise<void> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/rqlite/create-table`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ schema }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`createTable failed: ${r.status}`);
|
||||
}
|
||||
|
||||
async dropTable(table: string): Promise<void> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/rqlite/drop-table`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ table }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`dropTable failed: ${r.status}`);
|
||||
}
|
||||
|
||||
async query<T = any>(sql: string, args: any[] = []): Promise<{ rows: T[] }> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/rqlite/query`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ sql, args }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`query failed: ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async transaction(statements: string[]): Promise<void> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/rqlite/transaction`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ statements }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`transaction failed: ${r.status}`);
|
||||
}
|
||||
|
||||
async schema(): Promise<any> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/rqlite/schema`, {
|
||||
headers: this.headers(false),
|
||||
});
|
||||
if (!r.ok) throw new Error(`schema failed: ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
// Storage
|
||||
async put(key: string, value: Uint8Array | string): Promise<void> {
|
||||
const body =
|
||||
typeof value === "string" ? new TextEncoder().encode(value) : value;
|
||||
const r = await this.http(
|
||||
`${this.baseUrl}/v1/storage/put?key=${encodeURIComponent(key)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "X-API-Key": this.apiKey },
|
||||
body,
|
||||
}
|
||||
);
|
||||
if (!r.ok) throw new Error(`put failed: ${r.status}`);
|
||||
}
|
||||
|
||||
async get(key: string): Promise<Uint8Array> {
|
||||
const r = await this.http(
|
||||
`${this.baseUrl}/v1/storage/get?key=${encodeURIComponent(key)}`,
|
||||
{
|
||||
headers: { "X-API-Key": this.apiKey },
|
||||
}
|
||||
);
|
||||
if (!r.ok) throw new Error(`get failed: ${r.status}`);
|
||||
const buf = new Uint8Array(await r.arrayBuffer());
|
||||
return buf;
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
const r = await this.http(
|
||||
`${this.baseUrl}/v1/storage/exists?key=${encodeURIComponent(key)}`,
|
||||
{
|
||||
headers: this.headers(false),
|
||||
}
|
||||
);
|
||||
if (!r.ok) throw new Error(`exists failed: ${r.status}`);
|
||||
const j = await r.json();
|
||||
return !!j.exists;
|
||||
}
|
||||
|
||||
async list(prefix = ""): Promise<string[]> {
|
||||
const r = await this.http(
|
||||
`${this.baseUrl}/v1/storage/list?prefix=${encodeURIComponent(prefix)}`,
|
||||
{
|
||||
headers: this.headers(false),
|
||||
}
|
||||
);
|
||||
if (!r.ok) throw new Error(`list failed: ${r.status}`);
|
||||
const j = await r.json();
|
||||
return j.keys || [];
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/storage/delete`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ key }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`delete failed: ${r.status}`);
|
||||
}
|
||||
|
||||
// PubSub (minimal)
|
||||
subscribe(
|
||||
topic: string,
|
||||
onMessage: (data: Uint8Array) => void
|
||||
): { close: () => void } {
|
||||
const url = new URL(`${this.baseUrl.replace(/^http/, "ws")}/v1/pubsub/ws`);
|
||||
url.searchParams.set("topic", topic);
|
||||
const ws = new WebSocket(url.toString(), {
|
||||
headers: { "X-API-Key": this.apiKey },
|
||||
} as any);
|
||||
ws.binaryType = "arraybuffer";
|
||||
ws.onmessage = (ev: any) => {
|
||||
const data =
|
||||
ev.data instanceof ArrayBuffer
|
||||
? new Uint8Array(ev.data)
|
||||
: new TextEncoder().encode(String(ev.data));
|
||||
onMessage(data);
|
||||
};
|
||||
return { close: () => ws.close() };
|
||||
}
|
||||
|
||||
async publish(topic: string, data: Uint8Array | string): Promise<void> {
|
||||
const bytes =
|
||||
typeof data === "string" ? new TextEncoder().encode(data) : data;
|
||||
const b64 = Buffer.from(bytes).toString("base64");
|
||||
const r = await this.http(`${this.baseUrl}/v1/pubsub/publish`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ topic, data_base64: b64 }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`publish failed: ${r.status}`);
|
||||
}
|
||||
}
|
||||
12
examples/sdk-typescript/tsconfig.json
Normal file
12
examples/sdk-typescript/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"moduleResolution": "Node"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
51
go.mod
51
go.mod
@ -1,71 +1,42 @@
|
||||
module github.com/DeBrosOfficial/network
|
||||
|
||||
go 1.24.0
|
||||
go 1.23.8
|
||||
|
||||
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
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
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
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
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/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
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.3 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/logutils v1.0.0 // indirect
|
||||
github.com/hashicorp/memberlist v0.5.3 // indirect
|
||||
github.com/holiman/uint256 v1.2.4 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/ipfs/go-cid v0.5.0 // indirect
|
||||
@ -82,20 +53,13 @@ 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-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
|
||||
@ -137,25 +101,18 @@ require (
|
||||
github.com/quic-go/quic-go v0.50.1 // indirect
|
||||
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
|
||||
github.com/tidwall/btree v1.7.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/redcon v1.6.2 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.uber.org/dig v1.18.0 // indirect
|
||||
go.uber.org/fx v1.23.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
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.38.0 // indirect
|
||||
golang.org/x/sys v0.34.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
|
||||
|
||||
203
go.sum
203
go.sum
@ -8,59 +8,22 @@ dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
|
||||
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/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=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
|
||||
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/buraksezer/consistent v0.10.0 h1:hqBgz1PvNLC5rkWcEBVAL9dFMBWz6I0VgUCW25rrZlU=
|
||||
github.com/buraksezer/consistent v0.10.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw=
|
||||
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=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
@ -80,8 +43,6 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
@ -89,8 +50,6 @@ 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=
|
||||
@ -101,18 +60,9 @@ 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=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
@ -129,29 +79,13 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
@ -167,33 +101,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=
|
||||
github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.3 h1:cB1w4Zrk0O3jQBTcFMKqYQWRFfsSQ/TYKNyUUVyCP2c=
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.3/go.mod h1:SjlwKKFnwBXvxD/I1bEcfJIBbEJ+MCUn39TxymNR5ZU=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/memberlist v0.5.3 h1:tQ1jOCypD0WvMemw/ZhhtH+PWpzcftQvgCorLu0hndk=
|
||||
github.com/hashicorp/memberlist v0.5.3/go.mod h1:h60o12SZn/ua/j0B6iKAZezA4eDaGsIuPO70eOaJ6WE=
|
||||
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
|
||||
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
@ -207,14 +116,8 @@ github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@ -222,11 +125,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk=
|
||||
github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@ -256,8 +156,6 @@ 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=
|
||||
@ -266,12 +164,6 @@ 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=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||
@ -286,21 +178,11 @@ github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8Rv
|
||||
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
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=
|
||||
@ -325,12 +207,8 @@ github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/n
|
||||
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/olric-data/olric v0.7.0 h1:EKN2T6ZTtdu8Un0jV0KOWVxWm9odptJpefmDivfZdjE=
|
||||
github.com/olric-data/olric v0.7.0/go.mod h1:+ZnPpgc8JkNkza8rETCKGn0P/QPF6HhZY0EbCKAOslo=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
@ -339,8 +217,6 @@ github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/
|
||||
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
|
||||
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
@ -385,38 +261,21 @@ github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pion/webrtc/v4 v4.0.10 h1:Hq/JLjhqLxi+NmCtE8lnRPDr8H4LcNvwg8OxVcdv56Q=
|
||||
github.com/pion/webrtc/v4 v4.0.10/go.mod h1:ViHLVaNpiuvaH8pdiuQxuA9awuE6KVzAXx3vVWilOck=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
@ -427,19 +286,12 @@ github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
|
||||
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
||||
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=
|
||||
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
@ -464,22 +316,16 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
@ -487,23 +333,9 @@ 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=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/redcon v1.6.2 h1:5qfvrrybgtO85jnhSravmkZyC0D+7WstbfCs3MmPhow=
|
||||
github.com/tidwall/redcon v1.6.2/go.mod h1:p5Wbsgeyi2VSTBWOcA5vRXrOb9arFTcU2+ZzFjqV75Y=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
@ -525,7 +357,6 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@ -559,15 +390,12 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
@ -591,7 +419,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -599,28 +426,17 @@ golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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=
|
||||
@ -629,8 +445,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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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=
|
||||
@ -640,7 +456,6 @@ golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
@ -687,29 +502,15 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@ -1,243 +0,0 @@
|
||||
-- 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;
|
||||
|
||||
321
openapi/gateway.yaml
Normal file
321
openapi/gateway.yaml
Normal file
@ -0,0 +1,321 @@
|
||||
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" }
|
||||
@ -19,7 +19,7 @@ var disabled bool
|
||||
func SetDisabled(v bool) { disabled = v }
|
||||
|
||||
// Enabled reports whether Anyone proxy routing is active.
|
||||
// Defaults to true, using SOCKS5 at localhost:9050, unless explicitly disabled
|
||||
// Defaults to true, using SOCKS5 at 127.0.0.1:9050, unless explicitly disabled
|
||||
// via SetDisabled(true) or environment variable ANYONE_DISABLE=1.
|
||||
// ANYONE_SOCKS5 may override the proxy address.
|
||||
func Enabled() bool {
|
||||
@ -31,7 +31,7 @@ func Enabled() bool {
|
||||
|
||||
// socksAddr returns the SOCKS5 address to use for proxying (host:port).
|
||||
func socksAddr() string {
|
||||
return "localhost:9050"
|
||||
return "127.0.0.1:9050"
|
||||
}
|
||||
|
||||
// socksContextDialer implements tcp.ContextDialer over a SOCKS5 proxy.
|
||||
@ -57,7 +57,7 @@ func (d *socksContextDialer) DialContext(ctx context.Context, network, address s
|
||||
|
||||
// DialerForAddr returns a tcp.DialerForAddr that routes through the Anyone SOCKS5 proxy.
|
||||
// It automatically BYPASSES the proxy for loopback, private, and link-local addresses
|
||||
// to allow local/dev networking (e.g. localhost, 10.0.0.0/8, 192.168.0.0/16, fc00::/7, fe80::/10).
|
||||
// to allow local/dev networking (e.g. 127.0.0.1, 10.0.0.0/8, 192.168.0.0/16, fc00::/7, fe80::/10).
|
||||
func DialerForAddr() tcp.DialerForAddr {
|
||||
return func(raddr ma.Multiaddr) (tcp.ContextDialer, error) {
|
||||
// Prefer direct dialing for local/private targets
|
||||
|
||||
@ -34,15 +34,15 @@ func GetCredentialsPath() (string, error) {
|
||||
return "", fmt.Errorf("failed to get home 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)
|
||||
debrosDir := filepath.Join(homeDir, ".debros")
|
||||
if err := os.MkdirAll(debrosDir, 0700); err != nil {
|
||||
return "", fmt.Errorf("failed to create .debros directory: %w", err)
|
||||
}
|
||||
|
||||
return filepath.Join(oramaDir, "credentials.json"), nil
|
||||
return filepath.Join(debrosDir, "credentials.json"), nil
|
||||
}
|
||||
|
||||
// LoadCredentials loads credentials from ~/.orama/credentials.json
|
||||
// LoadCredentials loads credentials from ~/.debros/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 ~/.orama/credentials.json
|
||||
// SaveCredentials saves credentials to ~/.debros/credentials.json
|
||||
func (store *CredentialStore) SaveCredentials() error {
|
||||
credPath, err := GetCredentialsPath()
|
||||
if err != nil {
|
||||
|
||||
@ -1,144 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/tlsutil"
|
||||
)
|
||||
|
||||
// PerformSimpleAuthentication performs a simple authentication flow where the user
|
||||
// provides a wallet address and receives an API key without signature verification
|
||||
func PerformSimpleAuthentication(gatewayURL string) (*Credentials, error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Println("\n🔐 Simple Wallet Authentication")
|
||||
fmt.Println("================================")
|
||||
|
||||
// Read wallet address
|
||||
fmt.Print("Enter your wallet address (0x...): ")
|
||||
walletInput, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read wallet address: %w", err)
|
||||
}
|
||||
|
||||
wallet := strings.TrimSpace(walletInput)
|
||||
if wallet == "" {
|
||||
return nil, fmt.Errorf("wallet address cannot be empty")
|
||||
}
|
||||
|
||||
// Validate wallet format (basic check)
|
||||
if !strings.HasPrefix(wallet, "0x") && !strings.HasPrefix(wallet, "0X") {
|
||||
wallet = "0x" + wallet
|
||||
}
|
||||
|
||||
if !ValidateWalletAddress(wallet) {
|
||||
return nil, fmt.Errorf("invalid wallet address format")
|
||||
}
|
||||
|
||||
// Read namespace (optional)
|
||||
fmt.Print("Enter namespace (press Enter for 'default'): ")
|
||||
nsInput, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read namespace: %w", err)
|
||||
}
|
||||
|
||||
namespace := strings.TrimSpace(nsInput)
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
fmt.Printf("\n✅ Wallet: %s\n", wallet)
|
||||
fmt.Printf("✅ Namespace: %s\n", namespace)
|
||||
fmt.Println("⏳ Requesting API key from gateway...")
|
||||
|
||||
// Request API key from gateway
|
||||
apiKey, err := requestAPIKeyFromGateway(gatewayURL, wallet, namespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to request API key: %w", err)
|
||||
}
|
||||
|
||||
// Create credentials
|
||||
creds := &Credentials{
|
||||
APIKey: apiKey,
|
||||
Namespace: namespace,
|
||||
UserID: wallet,
|
||||
Wallet: wallet,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
|
||||
fmt.Printf("\n🎉 Authentication successful!\n")
|
||||
fmt.Printf("📝 API Key: %s\n", creds.APIKey)
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
// requestAPIKeyFromGateway calls the gateway's simple-key endpoint to generate an API key
|
||||
func requestAPIKeyFromGateway(gatewayURL, wallet, namespace string) (string, error) {
|
||||
reqBody := map[string]string{
|
||||
"wallet": wallet,
|
||||
"namespace": namespace,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
endpoint := gatewayURL + "/v1/auth/simple-key"
|
||||
|
||||
// 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)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("gateway returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var respBody map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
|
||||
return "", fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
apiKey, ok := respBody["api_key"].(string)
|
||||
if !ok || apiKey == "" {
|
||||
return "", fmt.Errorf("no api_key in response")
|
||||
}
|
||||
|
||||
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>~/.orama/credentials.json</code></p>
|
||||
<p>Your credentials have been saved securely to <code>~/.debros/credentials.json</code></p>
|
||||
<p><strong>You can now close this browser window and return to your terminal.</strong></p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@ -1,257 +0,0 @@
|
||||
// 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,10 +1,8 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/auth"
|
||||
)
|
||||
@ -35,35 +33,29 @@ func HandleAuthCommand(args []string) {
|
||||
|
||||
func showAuthHelp() {
|
||||
fmt.Printf("🔐 Authentication Commands\n\n")
|
||||
fmt.Printf("Usage: dbn auth <subcommand>\n\n")
|
||||
fmt.Printf("Usage: network-cli auth <subcommand>\n\n")
|
||||
fmt.Printf("Subcommands:\n")
|
||||
fmt.Printf(" login - Authenticate by providing your wallet address\n")
|
||||
fmt.Printf(" login - Authenticate with wallet\n")
|
||||
fmt.Printf(" logout - Clear stored credentials\n")
|
||||
fmt.Printf(" whoami - Show current authentication status\n")
|
||||
fmt.Printf(" status - Show detailed authentication info\n\n")
|
||||
fmt.Printf("Examples:\n")
|
||||
fmt.Printf(" dbn auth login # Enter wallet address interactively\n")
|
||||
fmt.Printf(" dbn auth whoami # Check who you're logged in as\n")
|
||||
fmt.Printf(" dbn auth status # View detailed authentication info\n")
|
||||
fmt.Printf(" dbn auth logout # Clear all stored credentials\n\n")
|
||||
fmt.Printf(" network-cli auth login\n")
|
||||
fmt.Printf(" network-cli auth whoami\n")
|
||||
fmt.Printf(" network-cli auth status\n")
|
||||
fmt.Printf(" network-cli auth logout\n\n")
|
||||
fmt.Printf("Environment Variables:\n")
|
||||
fmt.Printf(" DEBROS_GATEWAY_URL - Gateway URL (overrides environment config)\n\n")
|
||||
fmt.Printf("Authentication Flow:\n")
|
||||
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 ~/.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")
|
||||
fmt.Printf(" Use 'network-cli env current' to see your active environment.\n")
|
||||
}
|
||||
|
||||
func handleAuthLogin() {
|
||||
// Prompt for node selection
|
||||
gatewayURL := promptForGatewayURL()
|
||||
gatewayURL := getGatewayURL()
|
||||
fmt.Printf("🔐 Authenticating with gateway at: %s\n", gatewayURL)
|
||||
|
||||
// Use the simple authentication flow
|
||||
creds, err := auth.PerformSimpleAuthentication(gatewayURL)
|
||||
// Use the wallet authentication flow
|
||||
creds, err := auth.PerformWalletAuthentication(gatewayURL)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Authentication failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
@ -80,7 +72,6 @@ func handleAuthLogin() {
|
||||
fmt.Printf("📁 Credentials saved to: %s\n", credsPath)
|
||||
fmt.Printf("🎯 Wallet: %s\n", creds.Wallet)
|
||||
fmt.Printf("🏢 Namespace: %s\n", creds.Namespace)
|
||||
fmt.Printf("🔑 API Key: %s\n", creds.APIKey)
|
||||
}
|
||||
|
||||
func handleAuthLogout() {
|
||||
@ -102,7 +93,7 @@ func handleAuthWhoami() {
|
||||
creds, exists := store.GetCredentialsForGateway(gatewayURL)
|
||||
|
||||
if !exists || !creds.IsValid() {
|
||||
fmt.Println("❌ Not authenticated - run 'dbn auth login' to authenticate")
|
||||
fmt.Println("❌ Not authenticated - run 'network-cli auth login' to authenticate")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -164,55 +155,7 @@ 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 != "" {
|
||||
@ -225,6 +168,6 @@ func getGatewayURL() string {
|
||||
return env.GatewayURL
|
||||
}
|
||||
|
||||
// Fallback to default (node-1)
|
||||
// Fallback to default
|
||||
return "http://localhost:6001"
|
||||
}
|
||||
|
||||
@ -158,7 +158,7 @@ func HandlePeerIDCommand(format string, timeout time.Duration) {
|
||||
// HandlePubSubCommand handles pubsub commands
|
||||
func HandlePubSubCommand(args []string, format string, timeout time.Duration) {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: dbn pubsub <publish|subscribe|topics> [args...]\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub <publish|subscribe|topics> [args...]\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -179,7 +179,7 @@ func HandlePubSubCommand(args []string, format string, timeout time.Duration) {
|
||||
switch subcommand {
|
||||
case "publish":
|
||||
if len(args) < 3 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: dbn pubsub publish <topic> <message>\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub publish <topic> <message>\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
err := cli.PubSub().Publish(ctx, args[1], []byte(args[2]))
|
||||
@ -191,7 +191,7 @@ func HandlePubSubCommand(args []string, format string, timeout time.Duration) {
|
||||
|
||||
case "subscribe":
|
||||
if len(args) < 2 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: dbn pubsub subscribe <topic> [duration]\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub subscribe <topic> [duration]\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
duration := 30 * time.Second
|
||||
@ -243,23 +243,14 @@ func HandlePubSubCommand(args []string, format string, timeout time.Duration) {
|
||||
// Helper functions
|
||||
|
||||
func createClient() (client.NetworkClient, error) {
|
||||
config := client.DefaultClientConfig("dbn")
|
||||
|
||||
// Use active environment's gateway URL
|
||||
gatewayURL := getGatewayURL()
|
||||
config.GatewayURL = gatewayURL
|
||||
|
||||
// Try to get peer configuration from active environment
|
||||
env, err := GetActiveEnvironment()
|
||||
if err == nil && env != nil {
|
||||
// Environment loaded successfully - gateway URL already set above
|
||||
_ = env // Reserve for future peer configuration
|
||||
}
|
||||
config := client.DefaultClientConfig("network-cli")
|
||||
|
||||
// Check for existing credentials using enhanced authentication
|
||||
creds, err := auth.GetValidEnhancedCredentials()
|
||||
if err != nil {
|
||||
// No valid credentials found, use the enhanced authentication flow
|
||||
gatewayURL := getGatewayURL()
|
||||
|
||||
newCreds, authErr := auth.GetOrPromptForCredentials(gatewayURL)
|
||||
if authErr != nil {
|
||||
return nil, fmt.Errorf("authentication failed: %w", authErr)
|
||||
|
||||
513
pkg/cli/config_commands.go
Normal file
513
pkg/cli/config_commands.go
Normal file
@ -0,0 +1,513 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/config"
|
||||
"github.com/DeBrosOfficial/network/pkg/encryption"
|
||||
)
|
||||
|
||||
// HandleConfigCommand handles config management commands
|
||||
func HandleConfigCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
showConfigHelp()
|
||||
return
|
||||
}
|
||||
|
||||
subcommand := args[0]
|
||||
subargs := args[1:]
|
||||
|
||||
switch subcommand {
|
||||
case "init":
|
||||
handleConfigInit(subargs)
|
||||
case "validate":
|
||||
handleConfigValidate(subargs)
|
||||
case "help":
|
||||
showConfigHelp()
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown config subcommand: %s\n", subcommand)
|
||||
showConfigHelp()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func showConfigHelp() {
|
||||
fmt.Printf("Config Management Commands\n\n")
|
||||
fmt.Printf("Usage: network-cli config <subcommand> [options]\n\n")
|
||||
fmt.Printf("Subcommands:\n")
|
||||
fmt.Printf(" init - Generate full network stack in ~/.debros (bootstrap + 2 nodes + gateway)\n")
|
||||
fmt.Printf(" validate --name <file> - Validate a config file\n\n")
|
||||
fmt.Printf("Init Default Behavior (no --type):\n")
|
||||
fmt.Printf(" Generates bootstrap.yaml, node2.yaml, node3.yaml, gateway.yaml with:\n")
|
||||
fmt.Printf(" - Auto-generated identities for bootstrap, node2, node3\n")
|
||||
fmt.Printf(" - Correct bootstrap_peers and join addresses\n")
|
||||
fmt.Printf(" - Default ports: P2P 4001-4003, HTTP 5001-5003, Raft 7001-7003\n\n")
|
||||
fmt.Printf("Init Options:\n")
|
||||
fmt.Printf(" --type <type> - Single config type: node, bootstrap, gateway (skips stack generation)\n")
|
||||
fmt.Printf(" --name <file> - Output filename (default: depends on --type or 'stack' for full stack)\n")
|
||||
fmt.Printf(" --force - Overwrite existing config/stack files\n\n")
|
||||
fmt.Printf("Single Config Options (with --type):\n")
|
||||
fmt.Printf(" --id <id> - Node ID for bootstrap peers\n")
|
||||
fmt.Printf(" --listen-port <port> - LibP2P listen port (default: 4001)\n")
|
||||
fmt.Printf(" --rqlite-http-port <port> - RQLite HTTP port (default: 5001)\n")
|
||||
fmt.Printf(" --rqlite-raft-port <port> - RQLite Raft port (default: 7001)\n")
|
||||
fmt.Printf(" --join <host:port> - RQLite address to join (required for non-bootstrap)\n")
|
||||
fmt.Printf(" --bootstrap-peers <peers> - Comma-separated bootstrap peer multiaddrs\n\n")
|
||||
fmt.Printf("Examples:\n")
|
||||
fmt.Printf(" network-cli config init # Generate full stack\n")
|
||||
fmt.Printf(" network-cli config init --force # Overwrite existing stack\n")
|
||||
fmt.Printf(" network-cli config init --type bootstrap # Single bootstrap config (legacy)\n")
|
||||
fmt.Printf(" network-cli config validate --name node.yaml\n")
|
||||
}
|
||||
|
||||
func handleConfigInit(args []string) {
|
||||
// Parse flags
|
||||
var (
|
||||
cfgType = ""
|
||||
name = "" // Will be set based on type if not provided
|
||||
id string
|
||||
listenPort = 4001
|
||||
rqliteHTTPPort = 5001
|
||||
rqliteRaftPort = 7001
|
||||
joinAddr string
|
||||
bootstrapPeers string
|
||||
force bool
|
||||
)
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--type":
|
||||
if i+1 < len(args) {
|
||||
cfgType = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--name":
|
||||
if i+1 < len(args) {
|
||||
name = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--id":
|
||||
if i+1 < len(args) {
|
||||
id = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--listen-port":
|
||||
if i+1 < len(args) {
|
||||
if p, err := strconv.Atoi(args[i+1]); err == nil {
|
||||
listenPort = p
|
||||
}
|
||||
i++
|
||||
}
|
||||
case "--rqlite-http-port":
|
||||
if i+1 < len(args) {
|
||||
if p, err := strconv.Atoi(args[i+1]); err == nil {
|
||||
rqliteHTTPPort = p
|
||||
}
|
||||
i++
|
||||
}
|
||||
case "--rqlite-raft-port":
|
||||
if i+1 < len(args) {
|
||||
if p, err := strconv.Atoi(args[i+1]); err == nil {
|
||||
rqliteRaftPort = p
|
||||
}
|
||||
i++
|
||||
}
|
||||
case "--join":
|
||||
if i+1 < len(args) {
|
||||
joinAddr = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--bootstrap-peers":
|
||||
if i+1 < len(args) {
|
||||
bootstrapPeers = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--force":
|
||||
force = true
|
||||
}
|
||||
}
|
||||
|
||||
// If --type is not specified, generate full stack
|
||||
if cfgType == "" {
|
||||
initFullStack(force)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, continue with single-file generation
|
||||
// Validate type
|
||||
if cfgType != "node" && cfgType != "bootstrap" && cfgType != "gateway" {
|
||||
fmt.Fprintf(os.Stderr, "Invalid --type: %s (expected: node, bootstrap, or gateway)\n", cfgType)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Set default name based on type if not provided
|
||||
if name == "" {
|
||||
switch cfgType {
|
||||
case "bootstrap":
|
||||
name = "bootstrap.yaml"
|
||||
case "gateway":
|
||||
name = "gateway.yaml"
|
||||
default:
|
||||
name = "node.yaml"
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure config directory exists
|
||||
configDir, err := config.EnsureConfigDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to ensure config directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(configDir, name)
|
||||
|
||||
// Check if file exists
|
||||
if !force {
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "Config file already exists at %s (use --force to overwrite)\n", configPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate config based on type
|
||||
var configContent string
|
||||
switch cfgType {
|
||||
case "node":
|
||||
configContent = GenerateNodeConfig(name, id, listenPort, rqliteHTTPPort, rqliteRaftPort, joinAddr, bootstrapPeers)
|
||||
case "bootstrap":
|
||||
configContent = GenerateBootstrapConfig(name, id, listenPort, rqliteHTTPPort, rqliteRaftPort)
|
||||
case "gateway":
|
||||
configContent = GenerateGatewayConfig(bootstrapPeers)
|
||||
}
|
||||
|
||||
// Write config file
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write config file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Configuration file created: %s\n", configPath)
|
||||
fmt.Printf(" Type: %s\n", cfgType)
|
||||
fmt.Printf("\nYou can now start the %s using the generated config.\n", cfgType)
|
||||
}
|
||||
|
||||
func handleConfigValidate(args []string) {
|
||||
var name string
|
||||
for i := 0; i < len(args); i++ {
|
||||
if args[i] == "--name" && i+1 < len(args) {
|
||||
name = args[i+1]
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
fmt.Fprintf(os.Stderr, "Missing --name flag\n")
|
||||
showConfigHelp()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
configDir, err := config.ConfigDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to get config directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(configDir, name)
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to open config file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var cfg config.Config
|
||||
if err := config.DecodeStrict(file, &cfg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Run validation
|
||||
errs := cfg.Validate()
|
||||
if len(errs) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\n❌ Configuration errors (%d):\n", len(errs))
|
||||
for _, err := range errs {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Config is valid: %s\n", configPath)
|
||||
}
|
||||
|
||||
func initFullStack(force bool) {
|
||||
fmt.Printf("🚀 Initializing full network stack...\n")
|
||||
|
||||
// Ensure ~/.debros directory exists
|
||||
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")
|
||||
if err := os.MkdirAll(debrosDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to create ~/.debros directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Step 1: Generate bootstrap identity
|
||||
bootstrapIdentityDir := filepath.Join(debrosDir, "bootstrap")
|
||||
bootstrapIdentityPath := filepath.Join(bootstrapIdentityDir, "identity.key")
|
||||
|
||||
if !force {
|
||||
if _, err := os.Stat(bootstrapIdentityPath); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "Bootstrap identity already exists at %s (use --force to overwrite)\n", bootstrapIdentityPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
bootstrapInfo, err := encryption.GenerateIdentity()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to generate bootstrap identity: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.MkdirAll(bootstrapIdentityDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to create bootstrap data directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := encryption.SaveIdentity(bootstrapInfo, bootstrapIdentityPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to save bootstrap identity: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Generated bootstrap identity: %s (Peer ID: %s)\n", bootstrapIdentityPath, bootstrapInfo.PeerID.String())
|
||||
|
||||
// Construct bootstrap multiaddr
|
||||
bootstrapMultiaddr := fmt.Sprintf("/ip4/127.0.0.1/tcp/4001/p2p/%s", bootstrapInfo.PeerID.String())
|
||||
fmt.Printf(" Bootstrap multiaddr: %s\n", bootstrapMultiaddr)
|
||||
|
||||
// Generate configs for all nodes...
|
||||
// (rest of the implementation - similar to what was in main.go)
|
||||
// I'll keep it similar to the original for consistency
|
||||
|
||||
// Step 2: Generate bootstrap.yaml
|
||||
bootstrapName := "bootstrap.yaml"
|
||||
bootstrapPath := filepath.Join(debrosDir, bootstrapName)
|
||||
if !force {
|
||||
if _, err := os.Stat(bootstrapPath); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "Bootstrap config already exists at %s (use --force to overwrite)\n", bootstrapPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
bootstrapContent := GenerateBootstrapConfig(bootstrapName, "", 4001, 5001, 7001)
|
||||
if err := os.WriteFile(bootstrapPath, []byte(bootstrapContent), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write bootstrap config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Generated bootstrap config: %s\n", bootstrapPath)
|
||||
|
||||
// Step 3: Generate node2.yaml
|
||||
node2Name := "node2.yaml"
|
||||
node2Path := filepath.Join(debrosDir, node2Name)
|
||||
if !force {
|
||||
if _, err := os.Stat(node2Path); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "Node2 config already exists at %s (use --force to overwrite)\n", node2Path)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
node2Content := GenerateNodeConfig(node2Name, "", 4002, 5002, 7002, "localhost:7001", bootstrapMultiaddr)
|
||||
if err := os.WriteFile(node2Path, []byte(node2Content), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write node2 config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Generated node2 config: %s\n", node2Path)
|
||||
|
||||
// Step 4: Generate node3.yaml
|
||||
node3Name := "node3.yaml"
|
||||
node3Path := filepath.Join(debrosDir, node3Name)
|
||||
if !force {
|
||||
if _, err := os.Stat(node3Path); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "Node3 config already exists at %s (use --force to overwrite)\n", node3Path)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
node3Content := GenerateNodeConfig(node3Name, "", 4003, 5003, 7003, "localhost:7001", bootstrapMultiaddr)
|
||||
if err := os.WriteFile(node3Path, []byte(node3Content), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write node3 config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Generated node3 config: %s\n", node3Path)
|
||||
|
||||
// Step 5: Generate gateway.yaml
|
||||
gatewayName := "gateway.yaml"
|
||||
gatewayPath := filepath.Join(debrosDir, gatewayName)
|
||||
if !force {
|
||||
if _, err := os.Stat(gatewayPath); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "Gateway config already exists at %s (use --force to overwrite)\n", gatewayPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
gatewayContent := GenerateGatewayConfig(bootstrapMultiaddr)
|
||||
if err := os.WriteFile(gatewayPath, []byte(gatewayContent), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write gateway config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Generated gateway config: %s\n", gatewayPath)
|
||||
|
||||
fmt.Printf("\n" + strings.Repeat("=", 60) + "\n")
|
||||
fmt.Printf("✅ Full network stack initialized successfully!\n")
|
||||
fmt.Printf(strings.Repeat("=", 60) + "\n")
|
||||
fmt.Printf("\nBootstrap Peer ID: %s\n", bootstrapInfo.PeerID.String())
|
||||
fmt.Printf("Bootstrap Multiaddr: %s\n", bootstrapMultiaddr)
|
||||
fmt.Printf("\nGenerated configs:\n")
|
||||
fmt.Printf(" - %s\n", bootstrapPath)
|
||||
fmt.Printf(" - %s\n", node2Path)
|
||||
fmt.Printf(" - %s\n", node3Path)
|
||||
fmt.Printf(" - %s\n", gatewayPath)
|
||||
fmt.Printf("\nStart the network with: make dev\n")
|
||||
}
|
||||
|
||||
// GenerateNodeConfig generates a node configuration
|
||||
func GenerateNodeConfig(name, id string, listenPort, rqliteHTTPPort, rqliteRaftPort int, joinAddr, bootstrapPeers string) string {
|
||||
nodeID := id
|
||||
if nodeID == "" {
|
||||
nodeID = fmt.Sprintf("node-%d", time.Now().Unix())
|
||||
}
|
||||
|
||||
// Parse bootstrap peers
|
||||
var peers []string
|
||||
if bootstrapPeers != "" {
|
||||
for _, p := range strings.Split(bootstrapPeers, ",") {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Construct data_dir from name stem (remove .yaml)
|
||||
dataDir := strings.TrimSuffix(name, ".yaml")
|
||||
dataDir = filepath.Join(os.ExpandEnv("~"), ".debros", dataDir)
|
||||
|
||||
var peersYAML strings.Builder
|
||||
if len(peers) == 0 {
|
||||
peersYAML.WriteString(" bootstrap_peers: []")
|
||||
} else {
|
||||
peersYAML.WriteString(" bootstrap_peers:\n")
|
||||
for _, p := range peers {
|
||||
fmt.Fprintf(&peersYAML, " - \"%s\"\n", p)
|
||||
}
|
||||
}
|
||||
|
||||
if joinAddr == "" {
|
||||
joinAddr = "localhost:5001"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`node:
|
||||
id: "%s"
|
||||
type: "node"
|
||||
listen_addresses:
|
||||
- "/ip4/0.0.0.0/tcp/%d"
|
||||
data_dir: "%s"
|
||||
max_connections: 50
|
||||
|
||||
database:
|
||||
data_dir: "%s/rqlite"
|
||||
replication_factor: 3
|
||||
shard_count: 16
|
||||
max_database_size: 1073741824
|
||||
backup_interval: "24h"
|
||||
rqlite_port: %d
|
||||
rqlite_raft_port: %d
|
||||
rqlite_join_address: "%s"
|
||||
|
||||
discovery:
|
||||
%s
|
||||
discovery_interval: "15s"
|
||||
bootstrap_port: %d
|
||||
http_adv_address: "127.0.0.1:%d"
|
||||
raft_adv_address: "127.0.0.1:%d"
|
||||
node_namespace: "default"
|
||||
|
||||
security:
|
||||
enable_tls: false
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
format: "console"
|
||||
`, nodeID, listenPort, dataDir, dataDir, rqliteHTTPPort, rqliteRaftPort, joinAddr, peersYAML.String(), 4001, rqliteHTTPPort, rqliteRaftPort)
|
||||
}
|
||||
|
||||
// GenerateBootstrapConfig generates a bootstrap configuration
|
||||
func GenerateBootstrapConfig(name, id string, listenPort, rqliteHTTPPort, rqliteRaftPort int) string {
|
||||
nodeID := id
|
||||
if nodeID == "" {
|
||||
nodeID = "bootstrap"
|
||||
}
|
||||
|
||||
dataDir := filepath.Join(os.ExpandEnv("~"), ".debros", "bootstrap")
|
||||
|
||||
return fmt.Sprintf(`node:
|
||||
id: "%s"
|
||||
type: "bootstrap"
|
||||
listen_addresses:
|
||||
- "/ip4/0.0.0.0/tcp/%d"
|
||||
data_dir: "%s"
|
||||
max_connections: 50
|
||||
|
||||
database:
|
||||
data_dir: "%s/rqlite"
|
||||
replication_factor: 3
|
||||
shard_count: 16
|
||||
max_database_size: 1073741824
|
||||
backup_interval: "24h"
|
||||
rqlite_port: %d
|
||||
rqlite_raft_port: %d
|
||||
rqlite_join_address: ""
|
||||
|
||||
discovery:
|
||||
bootstrap_peers: []
|
||||
discovery_interval: "15s"
|
||||
bootstrap_port: %d
|
||||
http_adv_address: "127.0.0.1:%d"
|
||||
raft_adv_address: "127.0.0.1:%d"
|
||||
node_namespace: "default"
|
||||
|
||||
security:
|
||||
enable_tls: false
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
format: "console"
|
||||
`, nodeID, listenPort, dataDir, dataDir, rqliteHTTPPort, rqliteRaftPort, 4001, rqliteHTTPPort, rqliteRaftPort)
|
||||
}
|
||||
|
||||
// GenerateGatewayConfig generates a gateway configuration
|
||||
func GenerateGatewayConfig(bootstrapPeers string) string {
|
||||
var peers []string
|
||||
if bootstrapPeers != "" {
|
||||
for _, p := range strings.Split(bootstrapPeers, ",") {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var peersYAML strings.Builder
|
||||
if len(peers) == 0 {
|
||||
peersYAML.WriteString("bootstrap_peers: []")
|
||||
} else {
|
||||
peersYAML.WriteString("bootstrap_peers:\n")
|
||||
for _, p := range peers {
|
||||
fmt.Fprintf(&peersYAML, " - \"%s\"\n", p)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`listen_addr: ":6001"
|
||||
client_namespace: "default"
|
||||
rqlite_dsn: ""
|
||||
%s
|
||||
`, peersYAML.String())
|
||||
}
|
||||
@ -1,197 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/environments/development"
|
||||
)
|
||||
|
||||
// HandleDevCommand handles the dev command group
|
||||
func HandleDevCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
showDevHelp()
|
||||
return
|
||||
}
|
||||
|
||||
subcommand := args[0]
|
||||
subargs := args[1:]
|
||||
|
||||
switch subcommand {
|
||||
case "up":
|
||||
handleDevUp(subargs)
|
||||
case "down":
|
||||
handleDevDown(subargs)
|
||||
case "status":
|
||||
handleDevStatus(subargs)
|
||||
case "logs":
|
||||
handleDevLogs(subargs)
|
||||
case "help":
|
||||
showDevHelp()
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown dev subcommand: %s\n", subcommand)
|
||||
showDevHelp()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func showDevHelp() {
|
||||
fmt.Printf("🚀 Development Environment Commands\n\n")
|
||||
fmt.Printf("Usage: orama dev <subcommand> [options]\n\n")
|
||||
fmt.Printf("Subcommands:\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(" 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 .orama path
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to get home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
oramaDir := filepath.Join(homeDir, ".orama")
|
||||
|
||||
// Step 1: Check dependencies
|
||||
fmt.Printf("📋 Checking dependencies...\n\n")
|
||||
checker := development.NewDependencyChecker()
|
||||
if _, err := checker.CheckAll(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✓ All required dependencies available\n\n")
|
||||
|
||||
// Step 2: Check ports
|
||||
fmt.Printf("🔌 Checking port availability...\n\n")
|
||||
portChecker := development.NewPortChecker()
|
||||
if _, err := portChecker.CheckAll(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %v\n\n", err)
|
||||
fmt.Fprintf(os.Stderr, "Port mapping:\n")
|
||||
for port, service := range development.PortMap() {
|
||||
fmt.Fprintf(os.Stderr, " %d - %s\n", port, service)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✓ All required ports available\n\n")
|
||||
|
||||
// Step 3: Ensure configs
|
||||
fmt.Printf("⚙️ Preparing configuration files...\n\n")
|
||||
ensurer := development.NewConfigEnsurer(oramaDir)
|
||||
if err := ensurer.EnsureAll(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to prepare configs: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
// Step 4: Start services
|
||||
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)
|
||||
}
|
||||
|
||||
// Step 5: Show summary
|
||||
fmt.Printf("🎉 Development environment is running!\n\n")
|
||||
fmt.Printf("Key endpoints:\n")
|
||||
fmt.Printf(" Gateway: http://localhost:6001\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(" 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) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to get home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
oramaDir := filepath.Join(homeDir, ".orama")
|
||||
|
||||
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) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to get home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
oramaDir := filepath.Join(homeDir, ".orama")
|
||||
|
||||
pm := development.NewProcessManager(oramaDir, os.Stdout)
|
||||
ctx := context.Background()
|
||||
|
||||
pm.Status(ctx)
|
||||
}
|
||||
|
||||
func handleDevLogs(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: dbn dev logs <component> [--follow]\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)
|
||||
}
|
||||
|
||||
component := args[0]
|
||||
follow := len(args) > 1 && args[1] == "--follow"
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to get home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
oramaDir := filepath.Join(homeDir, ".orama")
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if follow {
|
||||
// Run tail -f
|
||||
tailCmd := fmt.Sprintf("tail -f %s", logPath)
|
||||
fmt.Printf("Following %s (press Ctrl+C to stop)...\n\n", logPath)
|
||||
// syscall.Exec doesn't work in all environments, use exec.Command instead
|
||||
cmd := exec.Command("sh", "-c", tailCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Run()
|
||||
} else {
|
||||
// Cat the file
|
||||
data, _ := os.ReadFile(logPath)
|
||||
fmt.Print(string(data))
|
||||
}
|
||||
}
|
||||
@ -35,7 +35,7 @@ func HandleEnvCommand(args []string) {
|
||||
|
||||
func showEnvHelp() {
|
||||
fmt.Printf("🌍 Environment Management Commands\n\n")
|
||||
fmt.Printf("Usage: dbn env <subcommand>\n\n")
|
||||
fmt.Printf("Usage: network-cli env <subcommand>\n\n")
|
||||
fmt.Printf("Subcommands:\n")
|
||||
fmt.Printf(" list - List all available environments\n")
|
||||
fmt.Printf(" current - Show current active environment\n")
|
||||
@ -43,15 +43,15 @@ 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.orama.network)\n")
|
||||
fmt.Printf(" testnet - Test network (https://testnet.orama.network)\n\n")
|
||||
fmt.Printf(" devnet - Development network (https://devnet.debros.network)\n")
|
||||
fmt.Printf(" testnet - Test network (https://testnet.debros.network)\n\n")
|
||||
fmt.Printf("Examples:\n")
|
||||
fmt.Printf(" dbn env list\n")
|
||||
fmt.Printf(" dbn env current\n")
|
||||
fmt.Printf(" dbn env switch devnet\n")
|
||||
fmt.Printf(" dbn env enable testnet\n")
|
||||
fmt.Printf(" dbn devnet enable # Shorthand for switch to devnet\n")
|
||||
fmt.Printf(" dbn testnet enable # Shorthand for switch to testnet\n")
|
||||
fmt.Printf(" network-cli env list\n")
|
||||
fmt.Printf(" network-cli env current\n")
|
||||
fmt.Printf(" network-cli env switch devnet\n")
|
||||
fmt.Printf(" network-cli env enable testnet\n")
|
||||
fmt.Printf(" network-cli devnet enable # Shorthand for switch to devnet\n")
|
||||
fmt.Printf(" network-cli testnet enable # Shorthand for switch to testnet\n")
|
||||
}
|
||||
|
||||
func handleEnvList() {
|
||||
@ -99,7 +99,7 @@ func handleEnvCurrent() {
|
||||
|
||||
func handleEnvSwitch(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: dbn env switch <environment>\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli env switch <environment>\n")
|
||||
fmt.Fprintf(os.Stderr, "Available: local, devnet, testnet\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -28,18 +28,18 @@ var DefaultEnvironments = []Environment{
|
||||
{
|
||||
Name: "local",
|
||||
GatewayURL: "http://localhost:6001",
|
||||
Description: "Local development environment (node-1)",
|
||||
Description: "Local development environment",
|
||||
IsActive: true,
|
||||
},
|
||||
{
|
||||
Name: "devnet",
|
||||
GatewayURL: "https://devnet.orama.network",
|
||||
GatewayURL: "https://devnet.debros.network",
|
||||
Description: "Development network (testnet)",
|
||||
IsActive: false,
|
||||
},
|
||||
{
|
||||
Name: "testnet",
|
||||
GatewayURL: "https://testnet.orama.network",
|
||||
GatewayURL: "https://testnet.debros.network",
|
||||
Description: "Test network (staging)",
|
||||
IsActive: false,
|
||||
},
|
||||
|
||||
@ -1,174 +0,0 @@
|
||||
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
|
||||
expectVPSIP string
|
||||
expectDomain string
|
||||
expectPeers string
|
||||
expectJoin string
|
||||
expectSecret string
|
||||
expectBranch string
|
||||
isFirstNode bool // first node = no peers and no join address
|
||||
}{
|
||||
{
|
||||
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: "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: "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 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
|
||||
var vpsIP, domain, peersStr, joinAddr, clusterSecret, branch string
|
||||
|
||||
for i, arg := range tt.args {
|
||||
switch arg {
|
||||
case "--vps-ip":
|
||||
if i+1 < len(tt.args) {
|
||||
vpsIP = tt.args[i+1]
|
||||
}
|
||||
case "--domain":
|
||||
if i+1 < len(tt.args) {
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,109 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,192 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
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
|
||||
@ -1,156 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
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
|
||||
@ -1,53 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,322 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/production"
|
||||
)
|
||||
|
||||
// HandleProdCommand handles production environment commands
|
||||
func HandleProdCommand(args []string) {
|
||||
production.HandleCommand(args)
|
||||
}
|
||||
243
pkg/cli/service.go
Normal file
243
pkg/cli/service.go
Normal file
@ -0,0 +1,243 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HandleServiceCommand handles systemd service management commands
|
||||
func HandleServiceCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
showServiceHelp()
|
||||
return
|
||||
}
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
fmt.Fprintf(os.Stderr, "❌ Service commands are only supported on Linux with systemd\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
subcommand := args[0]
|
||||
subargs := args[1:]
|
||||
|
||||
switch subcommand {
|
||||
case "start":
|
||||
handleServiceStart(subargs)
|
||||
case "stop":
|
||||
handleServiceStop(subargs)
|
||||
case "restart":
|
||||
handleServiceRestart(subargs)
|
||||
case "status":
|
||||
handleServiceStatus(subargs)
|
||||
case "logs":
|
||||
handleServiceLogs(subargs)
|
||||
case "help":
|
||||
showServiceHelp()
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown service subcommand: %s\n", subcommand)
|
||||
showServiceHelp()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func showServiceHelp() {
|
||||
fmt.Printf("🔧 Service Management Commands\n\n")
|
||||
fmt.Printf("Usage: network-cli service <subcommand> <target> [options]\n\n")
|
||||
fmt.Printf("Subcommands:\n")
|
||||
fmt.Printf(" start <target> - Start services\n")
|
||||
fmt.Printf(" stop <target> - Stop services\n")
|
||||
fmt.Printf(" restart <target> - Restart services\n")
|
||||
fmt.Printf(" status <target> - Show service status\n")
|
||||
fmt.Printf(" logs <target> - View service logs\n\n")
|
||||
fmt.Printf("Targets:\n")
|
||||
fmt.Printf(" node - DeBros node service\n")
|
||||
fmt.Printf(" gateway - DeBros gateway service\n")
|
||||
fmt.Printf(" all - All DeBros services\n\n")
|
||||
fmt.Printf("Logs Options:\n")
|
||||
fmt.Printf(" --follow - Follow logs in real-time (-f)\n")
|
||||
fmt.Printf(" --since=<time> - Show logs since time (e.g., '1h', '30m', '2d')\n")
|
||||
fmt.Printf(" -n <lines> - Show last N lines\n\n")
|
||||
fmt.Printf("Examples:\n")
|
||||
fmt.Printf(" network-cli service start node\n")
|
||||
fmt.Printf(" network-cli service status all\n")
|
||||
fmt.Printf(" network-cli service restart gateway\n")
|
||||
fmt.Printf(" network-cli service logs node --follow\n")
|
||||
fmt.Printf(" network-cli service logs gateway --since=1h\n")
|
||||
fmt.Printf(" network-cli service logs node -n 100\n")
|
||||
}
|
||||
|
||||
func getServices(target string) []string {
|
||||
switch target {
|
||||
case "node":
|
||||
return []string{"debros-node"}
|
||||
case "gateway":
|
||||
return []string{"debros-gateway"}
|
||||
case "all":
|
||||
return []string{"debros-node", "debros-gateway"}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "❌ Invalid target: %s (use: node, gateway, or all)\n", target)
|
||||
os.Exit(1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func requireRoot() {
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Fprintf(os.Stderr, "❌ This command requires root privileges\n")
|
||||
fmt.Fprintf(os.Stderr, " Run with: sudo network-cli service ...\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func handleServiceStart(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli service start <node|gateway|all>\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
requireRoot()
|
||||
|
||||
target := args[0]
|
||||
services := getServices(target)
|
||||
|
||||
fmt.Printf("🚀 Starting services...\n")
|
||||
for _, service := range services {
|
||||
cmd := exec.Command("systemctl", "start", service)
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to start %s: %v\n", service, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" ✓ Started %s\n", service)
|
||||
}
|
||||
}
|
||||
|
||||
func handleServiceStop(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli service stop <node|gateway|all>\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
requireRoot()
|
||||
|
||||
target := args[0]
|
||||
services := getServices(target)
|
||||
|
||||
fmt.Printf("⏹️ Stopping services...\n")
|
||||
for _, service := range services {
|
||||
cmd := exec.Command("systemctl", "stop", service)
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to stop %s: %v\n", service, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" ✓ Stopped %s\n", service)
|
||||
}
|
||||
}
|
||||
|
||||
func handleServiceRestart(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli service restart <node|gateway|all>\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
requireRoot()
|
||||
|
||||
target := args[0]
|
||||
services := getServices(target)
|
||||
|
||||
fmt.Printf("🔄 Restarting services...\n")
|
||||
for _, service := range services {
|
||||
cmd := exec.Command("systemctl", "restart", service)
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to restart %s: %v\n", service, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" ✓ Restarted %s\n", service)
|
||||
}
|
||||
}
|
||||
|
||||
func handleServiceStatus(args []string) {
|
||||
if len(args) == 0 {
|
||||
args = []string{"all"} // Default to all
|
||||
}
|
||||
|
||||
target := args[0]
|
||||
services := getServices(target)
|
||||
|
||||
fmt.Printf("📊 Service Status:\n\n")
|
||||
for _, service := range services {
|
||||
// Use systemctl is-active to get simple status
|
||||
cmd := exec.Command("systemctl", "is-active", service)
|
||||
output, _ := cmd.Output()
|
||||
status := strings.TrimSpace(string(output))
|
||||
|
||||
emoji := "❌"
|
||||
if status == "active" {
|
||||
emoji = "✅"
|
||||
} else if status == "inactive" {
|
||||
emoji = "⚪"
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s: %s\n", emoji, service, status)
|
||||
|
||||
// Show detailed status
|
||||
cmd = exec.Command("systemctl", "status", service, "--no-pager", "-l")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Run()
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func handleServiceLogs(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: network-cli service logs <node|gateway> [--follow] [--since=<time>] [-n <lines>]\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
target := args[0]
|
||||
if target == "all" {
|
||||
fmt.Fprintf(os.Stderr, "❌ Cannot show logs for 'all' - specify 'node' or 'gateway'\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
services := getServices(target)
|
||||
if len(services) == 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
service := services[0]
|
||||
|
||||
// Parse options
|
||||
journalArgs := []string{"-u", service, "--no-pager"}
|
||||
|
||||
for i := 1; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--follow" || arg == "-f":
|
||||
journalArgs = append(journalArgs, "-f")
|
||||
case strings.HasPrefix(arg, "--since="):
|
||||
since := strings.TrimPrefix(arg, "--since=")
|
||||
journalArgs = append(journalArgs, "--since="+since)
|
||||
case arg == "-n":
|
||||
if i+1 < len(args) {
|
||||
journalArgs = append(journalArgs, "-n", args[i+1])
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("📜 Logs for %s:\n\n", service)
|
||||
|
||||
cmd := exec.Command("journalctl", journalArgs...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to show logs: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
819
pkg/cli/setup.go
Normal file
819
pkg/cli/setup.go
Normal file
@ -0,0 +1,819 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HandleSetupCommand handles the interactive 'setup' command for VPS installation
|
||||
func HandleSetupCommand(args []string) {
|
||||
// Parse flags
|
||||
force := false
|
||||
for _, arg := range args {
|
||||
if arg == "--force" {
|
||||
force = true
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("🚀 DeBros Network Setup\n\n")
|
||||
|
||||
// Check if running as root
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Fprintf(os.Stderr, "❌ This command must be run as root (use sudo)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check OS compatibility
|
||||
if runtime.GOOS != "linux" {
|
||||
fmt.Fprintf(os.Stderr, "❌ Setup command is only supported on Linux\n")
|
||||
fmt.Fprintf(os.Stderr, " For other platforms, please install manually\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Detect OS
|
||||
osInfo := detectLinuxDistro()
|
||||
fmt.Printf("📋 Detected OS: %s\n", osInfo)
|
||||
|
||||
if !isSupportedOS(osInfo) {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Unsupported OS: %s\n", osInfo)
|
||||
fmt.Fprintf(os.Stderr, " Supported: Ubuntu 22.04/24.04/25.04, Debian 12\n")
|
||||
fmt.Printf("\nContinue anyway? (yes/no): ")
|
||||
if !promptYesNo() {
|
||||
fmt.Println("Setup cancelled.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Show setup plan
|
||||
fmt.Printf("\n" + strings.Repeat("=", 70) + "\n")
|
||||
fmt.Printf("Setup Plan:\n")
|
||||
fmt.Printf(" 1. Create 'debros' system user (if needed)\n")
|
||||
fmt.Printf(" 2. Install system dependencies (curl, git, make, build tools)\n")
|
||||
fmt.Printf(" 3. Install Go 1.21+ (if needed)\n")
|
||||
fmt.Printf(" 4. Install RQLite database\n")
|
||||
fmt.Printf(" 5. Install Anyone Relay (Anon) for anonymous networking\n")
|
||||
fmt.Printf(" 6. Create directories (/home/debros/bin, /home/debros/src)\n")
|
||||
fmt.Printf(" 7. Clone and build DeBros Network\n")
|
||||
fmt.Printf(" 8. Generate configuration files\n")
|
||||
fmt.Printf(" 9. Create systemd services (debros-node, debros-gateway)\n")
|
||||
fmt.Printf(" 10. Start and enable services\n")
|
||||
fmt.Printf(strings.Repeat("=", 70) + "\n\n")
|
||||
|
||||
fmt.Printf("Ready to begin setup? (yes/no): ")
|
||||
if !promptYesNo() {
|
||||
fmt.Println("Setup cancelled.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
|
||||
// Step 1: Setup debros user
|
||||
setupDebrosUser()
|
||||
|
||||
// Step 2: Install dependencies
|
||||
installSystemDependencies()
|
||||
|
||||
// Step 3: Install Go
|
||||
ensureGo()
|
||||
|
||||
// Step 4: Install RQLite
|
||||
installRQLite()
|
||||
|
||||
// Step 4.5: Install Anon (Anyone relay)
|
||||
installAnon()
|
||||
|
||||
// Step 5: Setup directories
|
||||
setupDirectories()
|
||||
|
||||
// Step 6: Clone and build
|
||||
cloneAndBuild()
|
||||
|
||||
// Step 7: Generate configs (interactive)
|
||||
generateConfigsInteractive(force)
|
||||
|
||||
// Step 8: Create systemd services
|
||||
createSystemdServices()
|
||||
|
||||
// Step 9: Start services
|
||||
startServices()
|
||||
|
||||
// Done!
|
||||
fmt.Printf("\n" + strings.Repeat("=", 70) + "\n")
|
||||
fmt.Printf("✅ Setup Complete!\n")
|
||||
fmt.Printf(strings.Repeat("=", 70) + "\n\n")
|
||||
fmt.Printf("DeBros Network is now running!\n\n")
|
||||
fmt.Printf("Service Management:\n")
|
||||
fmt.Printf(" network-cli service status all\n")
|
||||
fmt.Printf(" network-cli service logs node --follow\n")
|
||||
fmt.Printf(" network-cli service restart gateway\n\n")
|
||||
fmt.Printf("Access DeBros User:\n")
|
||||
fmt.Printf(" sudo -u debros bash\n\n")
|
||||
fmt.Printf("Verify Installation:\n")
|
||||
fmt.Printf(" curl http://localhost:6001/health\n")
|
||||
fmt.Printf(" curl http://localhost:5001/status\n\n")
|
||||
fmt.Printf("Anyone Relay (Anon):\n")
|
||||
fmt.Printf(" sudo systemctl status anon\n")
|
||||
fmt.Printf(" sudo tail -f /home/debros/.debros/logs/anon/notices.log\n")
|
||||
fmt.Printf(" Proxy endpoint: POST http://localhost:6001/v1/proxy/anon\n\n")
|
||||
}
|
||||
|
||||
func detectLinuxDistro() string {
|
||||
if data, err := os.ReadFile("/etc/os-release"); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
var id, version string
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "ID=") {
|
||||
id = strings.Trim(strings.TrimPrefix(line, "ID="), "\"")
|
||||
}
|
||||
if strings.HasPrefix(line, "VERSION_ID=") {
|
||||
version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"")
|
||||
}
|
||||
}
|
||||
if id != "" && version != "" {
|
||||
return fmt.Sprintf("%s %s", id, version)
|
||||
}
|
||||
if id != "" {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func isSupportedOS(osInfo string) bool {
|
||||
supported := []string{
|
||||
"ubuntu 22.04",
|
||||
"ubuntu 24.04",
|
||||
"ubuntu 25.04",
|
||||
"debian 12",
|
||||
}
|
||||
for _, s := range supported {
|
||||
if strings.Contains(strings.ToLower(osInfo), s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func promptYesNo() bool {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, _ := reader.ReadString('\n')
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
return response == "yes" || response == "y"
|
||||
}
|
||||
|
||||
// isValidMultiaddr validates bootstrap peer multiaddr format
|
||||
func isValidMultiaddr(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
if !(strings.HasPrefix(s, "/ip4/") || strings.HasPrefix(s, "/ip6/")) {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(s, "/p2p/")
|
||||
}
|
||||
|
||||
// isValidHostPort validates host:port format
|
||||
func isValidHostPort(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
parts := strings.Split(s, ":")
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
host := strings.TrimSpace(parts[0])
|
||||
port := strings.TrimSpace(parts[1])
|
||||
if host == "" {
|
||||
return false
|
||||
}
|
||||
// Port must be a valid number between 1 and 65535
|
||||
if portNum, err := strconv.Atoi(port); err != nil || portNum < 1 || portNum > 65535 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func setupDebrosUser() {
|
||||
fmt.Printf("👤 Setting up 'debros' user...\n")
|
||||
|
||||
// Check if user exists
|
||||
userExists := false
|
||||
if _, err := exec.Command("id", "debros").CombinedOutput(); err == nil {
|
||||
fmt.Printf(" ✓ User 'debros' already exists\n")
|
||||
userExists = true
|
||||
} else {
|
||||
// Create user
|
||||
cmd := exec.Command("useradd", "-r", "-m", "-s", "/bin/bash", "-d", "/home/debros", "debros")
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to create user 'debros': %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(" ✓ Created user 'debros'\n")
|
||||
}
|
||||
|
||||
// Get the user who invoked sudo (the actual user, not root)
|
||||
sudoUser := os.Getenv("SUDO_USER")
|
||||
if sudoUser == "" {
|
||||
// If not running via sudo, skip sudoers setup
|
||||
return
|
||||
}
|
||||
|
||||
// Create sudoers rule to allow passwordless access to debros user
|
||||
sudoersRule := fmt.Sprintf("%s ALL=(debros) NOPASSWD: ALL\n", sudoUser)
|
||||
sudoersFile := "/etc/sudoers.d/debros-access"
|
||||
|
||||
// Check if sudoers rule already exists
|
||||
if existing, err := os.ReadFile(sudoersFile); err == nil {
|
||||
if strings.Contains(string(existing), sudoUser) {
|
||||
if !userExists {
|
||||
fmt.Printf(" ✓ Sudoers access configured\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Write sudoers rule
|
||||
if err := os.WriteFile(sudoersFile, []byte(sudoersRule), 0440); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to create sudoers rule: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, " You can manually switch to debros using: sudo -u debros bash\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the sudoers file
|
||||
if err := exec.Command("visudo", "-c", "-f", sudoersFile).Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Sudoers rule validation failed, removing file\n")
|
||||
os.Remove(sudoersFile)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ Sudoers access configured\n")
|
||||
fmt.Printf(" You can now run: sudo -u debros bash\n")
|
||||
}
|
||||
|
||||
func installSystemDependencies() {
|
||||
fmt.Printf("📦 Installing system dependencies...\n")
|
||||
|
||||
// Detect package manager
|
||||
var installCmd *exec.Cmd
|
||||
if _, err := exec.LookPath("apt"); err == nil {
|
||||
installCmd = exec.Command("apt", "update")
|
||||
if err := installCmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ apt update failed: %v\n", err)
|
||||
}
|
||||
installCmd = exec.Command("apt", "install", "-y", "curl", "git", "make", "build-essential", "wget")
|
||||
} else if _, err := exec.LookPath("yum"); err == nil {
|
||||
installCmd = exec.Command("yum", "install", "-y", "curl", "git", "make", "gcc", "wget")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "❌ No supported package manager found\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := installCmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to install dependencies: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ Dependencies installed\n")
|
||||
}
|
||||
|
||||
func ensureGo() {
|
||||
fmt.Printf("🔧 Checking Go installation...\n")
|
||||
|
||||
// Check if Go is already installed
|
||||
if _, err := exec.LookPath("go"); err == nil {
|
||||
fmt.Printf(" ✓ Go already installed\n")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Installing Go 1.21.6...\n")
|
||||
|
||||
// Download Go
|
||||
arch := "amd64"
|
||||
if runtime.GOARCH == "arm64" {
|
||||
arch = "arm64"
|
||||
}
|
||||
goTarball := fmt.Sprintf("go1.21.6.linux-%s.tar.gz", arch)
|
||||
goURL := fmt.Sprintf("https://go.dev/dl/%s", goTarball)
|
||||
|
||||
// Download
|
||||
cmd := exec.Command("wget", "-q", goURL, "-O", "/tmp/"+goTarball)
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to download Go: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Extract
|
||||
cmd = exec.Command("tar", "-C", "/usr/local", "-xzf", "/tmp/"+goTarball)
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to extract Go: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Add to PATH for current process
|
||||
os.Setenv("PATH", os.Getenv("PATH")+":/usr/local/go/bin")
|
||||
|
||||
// Also add to debros user's .bashrc for persistent availability
|
||||
debrosHome := "/home/debros"
|
||||
bashrc := debrosHome + "/.bashrc"
|
||||
pathLine := "\nexport PATH=$PATH:/usr/local/go/bin\n"
|
||||
|
||||
// Read existing bashrc
|
||||
existing, _ := os.ReadFile(bashrc)
|
||||
existingStr := string(existing)
|
||||
|
||||
// Add PATH if not already present
|
||||
if !strings.Contains(existingStr, "/usr/local/go/bin") {
|
||||
if err := os.WriteFile(bashrc, []byte(existingStr+pathLine), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to update debros .bashrc: %v\n", err)
|
||||
}
|
||||
// Fix ownership
|
||||
exec.Command("chown", "debros:debros", bashrc).Run()
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ Go installed\n")
|
||||
}
|
||||
|
||||
func installRQLite() {
|
||||
fmt.Printf("🗄️ Installing RQLite...\n")
|
||||
|
||||
// Check if already installed
|
||||
if _, err := exec.LookPath("rqlited"); err == nil {
|
||||
fmt.Printf(" ✓ RQLite already installed\n")
|
||||
return
|
||||
}
|
||||
|
||||
arch := "amd64"
|
||||
switch runtime.GOARCH {
|
||||
case "arm64":
|
||||
arch = "arm64"
|
||||
case "arm":
|
||||
arch = "arm"
|
||||
}
|
||||
|
||||
version := "8.43.0"
|
||||
tarball := fmt.Sprintf("rqlite-v%s-linux-%s.tar.gz", version, arch)
|
||||
url := fmt.Sprintf("https://github.com/rqlite/rqlite/releases/download/v%s/%s", version, tarball)
|
||||
|
||||
// Download
|
||||
cmd := exec.Command("wget", "-q", url, "-O", "/tmp/"+tarball)
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to download RQLite: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Extract
|
||||
cmd = exec.Command("tar", "-C", "/tmp", "-xzf", "/tmp/"+tarball)
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to extract RQLite: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Copy binaries
|
||||
dir := fmt.Sprintf("/tmp/rqlite-v%s-linux-%s", version, arch)
|
||||
exec.Command("cp", dir+"/rqlited", "/usr/local/bin/").Run()
|
||||
exec.Command("cp", dir+"/rqlite", "/usr/local/bin/").Run()
|
||||
exec.Command("chmod", "+x", "/usr/local/bin/rqlited").Run()
|
||||
exec.Command("chmod", "+x", "/usr/local/bin/rqlite").Run()
|
||||
|
||||
fmt.Printf(" ✓ RQLite installed\n")
|
||||
}
|
||||
|
||||
func installAnon() {
|
||||
fmt.Printf("🔐 Installing Anyone Relay (Anon)...\n")
|
||||
|
||||
// Check if already installed
|
||||
if _, err := exec.LookPath("anon"); err == nil {
|
||||
fmt.Printf(" ✓ Anon already installed\n")
|
||||
configureAnonLogs()
|
||||
configureFirewallForAnon()
|
||||
return
|
||||
}
|
||||
|
||||
// Install via APT (official method from docs.anyone.io)
|
||||
fmt.Printf(" Adding Anyone APT repository...\n")
|
||||
|
||||
// Add GPG key
|
||||
cmd := exec.Command("sh", "-c", "curl -fsSL https://deb.anyone.io/gpg.key | gpg --dearmor -o /usr/share/keyrings/anyone-archive-keyring.gpg")
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to add Anyone GPG key: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, " You can manually install with:\n")
|
||||
fmt.Fprintf(os.Stderr, " curl -fsSL https://deb.anyone.io/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/anyone-archive-keyring.gpg\n")
|
||||
fmt.Fprintf(os.Stderr, " echo 'deb [signed-by=/usr/share/keyrings/anyone-archive-keyring.gpg] https://deb.anyone.io/ anyone main' | sudo tee /etc/apt/sources.list.d/anyone.list\n")
|
||||
fmt.Fprintf(os.Stderr, " sudo apt update && sudo apt install -y anon\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Add repository
|
||||
repoLine := "deb [signed-by=/usr/share/keyrings/anyone-archive-keyring.gpg] https://deb.anyone.io/ anyone main"
|
||||
if err := os.WriteFile("/etc/apt/sources.list.d/anyone.list", []byte(repoLine+"\n"), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to add Anyone repository: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update package list
|
||||
fmt.Printf(" Updating package list...\n")
|
||||
exec.Command("apt", "update", "-qq").Run()
|
||||
|
||||
// Install anon
|
||||
fmt.Printf(" Installing Anon package...\n")
|
||||
cmd = exec.Command("apt", "install", "-y", "anon")
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Anon installation failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify installation
|
||||
if _, err := exec.LookPath("anon"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Anon installation may have failed\n")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ Anon installed\n")
|
||||
|
||||
// Configure with sensible defaults
|
||||
configureAnonDefaults()
|
||||
|
||||
// Configure logs
|
||||
configureAnonLogs()
|
||||
|
||||
// Configure firewall
|
||||
configureFirewallForAnon()
|
||||
|
||||
// Enable and start service
|
||||
fmt.Printf(" Enabling Anon service...\n")
|
||||
exec.Command("systemctl", "enable", "anon").Run()
|
||||
exec.Command("systemctl", "start", "anon").Run()
|
||||
|
||||
if exec.Command("systemctl", "is-active", "--quiet", "anon").Run() == nil {
|
||||
fmt.Printf(" ✓ Anon service is running\n")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Anon service may not be running. Check: systemctl status anon\n")
|
||||
}
|
||||
}
|
||||
|
||||
func configureAnonDefaults() {
|
||||
fmt.Printf(" Configuring Anon with default settings...\n")
|
||||
|
||||
hostname := "debros-node"
|
||||
if h, err := os.Hostname(); err == nil && h != "" {
|
||||
hostname = strings.Split(h, ".")[0]
|
||||
}
|
||||
|
||||
anonrcPath := "/etc/anon/anonrc"
|
||||
if _, err := os.Stat(anonrcPath); err == nil {
|
||||
// Backup existing config
|
||||
exec.Command("cp", anonrcPath, anonrcPath+".bak").Run()
|
||||
|
||||
// Read existing config
|
||||
data, err := os.ReadFile(anonrcPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
config := string(data)
|
||||
|
||||
// Add settings if not present
|
||||
if !strings.Contains(config, "Nickname") {
|
||||
config += fmt.Sprintf("\nNickname %s\n", hostname)
|
||||
}
|
||||
if !strings.Contains(config, "ControlPort") {
|
||||
config += "ControlPort 9051\n"
|
||||
}
|
||||
if !strings.Contains(config, "SocksPort") {
|
||||
config += "SocksPort 9050\n"
|
||||
}
|
||||
|
||||
// Write back
|
||||
os.WriteFile(anonrcPath, []byte(config), 0644)
|
||||
|
||||
fmt.Printf(" Nickname: %s\n", hostname)
|
||||
fmt.Printf(" ORPort: 9001 (default)\n")
|
||||
fmt.Printf(" ControlPort: 9051\n")
|
||||
fmt.Printf(" SOCKSPort: 9050\n")
|
||||
}
|
||||
}
|
||||
|
||||
func configureAnonLogs() {
|
||||
fmt.Printf(" Configuring Anon logs...\n")
|
||||
|
||||
// Create log directory
|
||||
logDir := "/home/debros/.debros/logs/anon"
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to create log directory: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Change ownership to debian-anon (the user anon runs as)
|
||||
exec.Command("chown", "-R", "debian-anon:debian-anon", logDir).Run()
|
||||
|
||||
// Update anonrc if it exists
|
||||
anonrcPath := "/etc/anon/anonrc"
|
||||
if _, err := os.Stat(anonrcPath); err == nil {
|
||||
// Read current config
|
||||
data, err := os.ReadFile(anonrcPath)
|
||||
if err == nil {
|
||||
config := string(data)
|
||||
|
||||
// Replace log file path
|
||||
newConfig := strings.ReplaceAll(config,
|
||||
"Log notice file /var/log/anon/notices.log",
|
||||
"Log notice file /home/debros/.debros/logs/anon/notices.log")
|
||||
|
||||
// Write back
|
||||
if err := os.WriteFile(anonrcPath, []byte(newConfig), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to update anonrc: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Anon logs configured to %s\n", logDir)
|
||||
|
||||
// Restart anon service if running
|
||||
if exec.Command("systemctl", "is-active", "--quiet", "anon").Run() == nil {
|
||||
exec.Command("systemctl", "restart", "anon").Run()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func configureFirewallForAnon() {
|
||||
fmt.Printf(" Checking firewall configuration...\n")
|
||||
|
||||
// Check for UFW
|
||||
if _, err := exec.LookPath("ufw"); err == nil {
|
||||
output, _ := exec.Command("ufw", "status").CombinedOutput()
|
||||
if strings.Contains(string(output), "Status: active") {
|
||||
fmt.Printf(" Adding UFW rules for Anon...\n")
|
||||
exec.Command("ufw", "allow", "9001/tcp", "comment", "Anon ORPort").Run()
|
||||
exec.Command("ufw", "allow", "9051/tcp", "comment", "Anon ControlPort").Run()
|
||||
fmt.Printf(" ✓ UFW rules added\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for firewalld
|
||||
if _, err := exec.LookPath("firewall-cmd"); err == nil {
|
||||
output, _ := exec.Command("firewall-cmd", "--state").CombinedOutput()
|
||||
if strings.Contains(string(output), "running") {
|
||||
fmt.Printf(" Adding firewalld rules for Anon...\n")
|
||||
exec.Command("firewall-cmd", "--permanent", "--add-port=9001/tcp").Run()
|
||||
exec.Command("firewall-cmd", "--permanent", "--add-port=9051/tcp").Run()
|
||||
exec.Command("firewall-cmd", "--reload").Run()
|
||||
fmt.Printf(" ✓ firewalld rules added\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for iptables
|
||||
if _, err := exec.LookPath("iptables"); err == nil {
|
||||
output, _ := exec.Command("iptables", "-L", "-n").CombinedOutput()
|
||||
if strings.Contains(string(output), "Chain INPUT") {
|
||||
fmt.Printf(" Adding iptables rules for Anon...\n")
|
||||
exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "--dport", "9001", "-j", "ACCEPT", "-m", "comment", "--comment", "Anon ORPort").Run()
|
||||
exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "--dport", "9051", "-j", "ACCEPT", "-m", "comment", "--comment", "Anon ControlPort").Run()
|
||||
|
||||
// Try to save rules
|
||||
if _, err := exec.LookPath("netfilter-persistent"); err == nil {
|
||||
exec.Command("netfilter-persistent", "save").Run()
|
||||
} else if _, err := exec.LookPath("iptables-save"); err == nil {
|
||||
cmd := exec.Command("sh", "-c", "iptables-save > /etc/iptables/rules.v4")
|
||||
cmd.Run()
|
||||
}
|
||||
fmt.Printf(" ✓ iptables rules added\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf(" No active firewall detected\n")
|
||||
}
|
||||
|
||||
func setupDirectories() {
|
||||
fmt.Printf("📁 Creating directories...\n")
|
||||
|
||||
dirs := []string{
|
||||
"/home/debros/bin",
|
||||
"/home/debros/src",
|
||||
"/home/debros/.debros",
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to create %s: %v\n", dir, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Change ownership to debros
|
||||
cmd := exec.Command("chown", "-R", "debros:debros", dir)
|
||||
cmd.Run()
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ Directories created\n")
|
||||
}
|
||||
|
||||
func cloneAndBuild() {
|
||||
fmt.Printf("🔨 Cloning and building DeBros Network...\n")
|
||||
|
||||
// Check if already cloned
|
||||
if _, err := os.Stat("/home/debros/src/.git"); err == nil {
|
||||
fmt.Printf(" Updating repository...\n")
|
||||
cmd := exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "pull", "origin", "nightly")
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to update repo: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" Cloning repository...\n")
|
||||
cmd := exec.Command("sudo", "-u", "debros", "git", "clone", "--branch", "nightly", "--depth", "1", "https://github.com/DeBrosOfficial/network.git", "/home/debros/src")
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to clone repo: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Build
|
||||
fmt.Printf(" Building binaries...\n")
|
||||
|
||||
// Ensure Go is in PATH for the build
|
||||
os.Setenv("PATH", os.Getenv("PATH")+":/usr/local/go/bin")
|
||||
|
||||
// Use sudo with --preserve-env=PATH to pass Go path to debros user
|
||||
cmd := exec.Command("sudo", "--preserve-env=PATH", "-u", "debros", "make", "build")
|
||||
cmd.Dir = "/home/debros/src"
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to build: %v\n%s\n", err, output)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Copy binaries
|
||||
exec.Command("sh", "-c", "cp -r /home/debros/src/bin/* /home/debros/bin/").Run()
|
||||
exec.Command("chown", "-R", "debros:debros", "/home/debros/bin").Run()
|
||||
exec.Command("chmod", "-R", "755", "/home/debros/bin").Run()
|
||||
|
||||
fmt.Printf(" ✓ Built and installed\n")
|
||||
}
|
||||
|
||||
func generateConfigsInteractive(force bool) {
|
||||
fmt.Printf("⚙️ Generating configurations...\n")
|
||||
|
||||
// For single-node VPS setup, use sensible defaults
|
||||
// This creates a bootstrap node that acts as the cluster leader
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf("Setting up single-node configuration...\n")
|
||||
fmt.Printf(" • Bootstrap node (cluster leader)\n")
|
||||
fmt.Printf(" • No external peers required\n")
|
||||
fmt.Printf(" • Gateway connected to local node\n\n")
|
||||
|
||||
// Generate bootstrap node config with explicit parameters
|
||||
// Pass empty bootstrap-peers and no join address for bootstrap node
|
||||
bootstrapArgs := []string{
|
||||
"-u", "debros",
|
||||
"/home/debros/bin/network-cli", "config", "init",
|
||||
"--type", "bootstrap",
|
||||
"--bootstrap-peers", "",
|
||||
}
|
||||
if force {
|
||||
bootstrapArgs = append(bootstrapArgs, "--force")
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", bootstrapArgs...)
|
||||
cmd.Stdin = nil // Explicitly close stdin to prevent interactive prompts
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to generate bootstrap config: %v\n", err)
|
||||
if len(output) > 0 {
|
||||
fmt.Fprintf(os.Stderr, " Output: %s\n", string(output))
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" ✓ Bootstrap node config created\n")
|
||||
}
|
||||
|
||||
// Rename bootstrap.yaml to node.yaml so the service can find it
|
||||
renameCmd := exec.Command("sudo", "-u", "debros", "mv", "/home/debros/.debros/bootstrap.yaml", "/home/debros/.debros/node.yaml")
|
||||
if err := renameCmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to rename config: %v\n", err)
|
||||
}
|
||||
|
||||
// Generate gateway config with explicit empty bootstrap peers
|
||||
gatewayArgs := []string{
|
||||
"-u", "debros",
|
||||
"/home/debros/bin/network-cli", "config", "init",
|
||||
"--type", "gateway",
|
||||
"--bootstrap-peers", "",
|
||||
}
|
||||
if force {
|
||||
gatewayArgs = append(gatewayArgs, "--force")
|
||||
}
|
||||
|
||||
cmd = exec.Command("sudo", gatewayArgs...)
|
||||
cmd.Stdin = nil // Explicitly close stdin to prevent interactive prompts
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to generate gateway config: %v\n", err)
|
||||
if len(output) > 0 {
|
||||
fmt.Fprintf(os.Stderr, " Output: %s\n", string(output))
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" ✓ Gateway config created\n")
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ Configurations generated\n")
|
||||
}
|
||||
|
||||
func createSystemdServices() {
|
||||
fmt.Printf("🔧 Creating systemd services...\n")
|
||||
|
||||
// Node service
|
||||
nodeService := `[Unit]
|
||||
Description=DeBros Network Node
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=debros
|
||||
Group=debros
|
||||
WorkingDirectory=/home/debros/src
|
||||
ExecStart=/home/debros/bin/node --config node.yaml
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
Environment=HOME=/home/debros
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=debros-node
|
||||
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/home/debros
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
if err := os.WriteFile("/etc/systemd/system/debros-node.service", []byte(nodeService), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to create node service: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Gateway service
|
||||
gatewayService := `[Unit]
|
||||
Description=DeBros Gateway
|
||||
After=debros-node.service
|
||||
Wants=debros-node.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=debros
|
||||
Group=debros
|
||||
WorkingDirectory=/home/debros/src
|
||||
ExecStart=/home/debros/bin/gateway
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
Environment=HOME=/home/debros
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=debros-gateway
|
||||
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/home/debros
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
if err := os.WriteFile("/etc/systemd/system/debros-gateway.service", []byte(gatewayService), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to create gateway service: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Reload systemd
|
||||
exec.Command("systemctl", "daemon-reload").Run()
|
||||
exec.Command("systemctl", "enable", "debros-node").Run()
|
||||
exec.Command("systemctl", "enable", "debros-gateway").Run()
|
||||
|
||||
fmt.Printf(" ✓ Services created and enabled\n")
|
||||
}
|
||||
|
||||
func startServices() {
|
||||
fmt.Printf("🚀 Starting services...\n")
|
||||
|
||||
// Start node
|
||||
if err := exec.Command("systemctl", "start", "debros-node").Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to start node service: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Node service started\n")
|
||||
}
|
||||
|
||||
// Start gateway
|
||||
if err := exec.Command("systemctl", "start", "debros-gateway").Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to start gateway service: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Gateway service started\n")
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@ -1,217 +0,0 @@
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
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
|
||||
}
|
||||
|
||||
@ -35,7 +35,6 @@ type Client struct {
|
||||
database *DatabaseClientImpl
|
||||
network *NetworkInfoImpl
|
||||
pubsub *pubSubBridge
|
||||
storage *StorageClientImpl
|
||||
|
||||
// State
|
||||
connected bool
|
||||
@ -71,7 +70,6 @@ func NewClient(config *ClientConfig) (NetworkClient, error) {
|
||||
// Initialize components (will be configured when connected)
|
||||
client.database = &DatabaseClientImpl{client: client}
|
||||
client.network = &NetworkInfoImpl{client: client}
|
||||
client.storage = &StorageClientImpl{client: client}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@ -91,11 +89,6 @@ func (c *Client) Network() NetworkInfo {
|
||||
return c.network
|
||||
}
|
||||
|
||||
// Storage returns the storage client
|
||||
func (c *Client) Storage() StorageClient {
|
||||
return c.storage
|
||||
}
|
||||
|
||||
// Config returns a snapshot copy of the client's configuration
|
||||
func (c *Client) Config() *ClientConfig {
|
||||
c.mu.RLock()
|
||||
@ -195,49 +188,49 @@ func (c *Client) Connect() error {
|
||||
c.pubsub = &pubSubBridge{client: c, adapter: adapter}
|
||||
c.logger.Info("Pubsub bridge created successfully")
|
||||
|
||||
c.logger.Info("Starting peer connections...")
|
||||
c.logger.Info("Starting bootstrap peer connections...")
|
||||
|
||||
// Connect to peers FIRST
|
||||
// Connect to bootstrap peers FIRST
|
||||
ctx, cancel := context.WithTimeout(context.Background(), c.config.ConnectTimeout)
|
||||
defer cancel()
|
||||
|
||||
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),
|
||||
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),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
peersConnected++
|
||||
c.logger.Info("Successfully connected to peer", zap.String("addr", peerAddr))
|
||||
bootstrapPeersConnected++
|
||||
c.logger.Info("Successfully connected to bootstrap peer", zap.String("addr", bootstrapAddr))
|
||||
}
|
||||
|
||||
if peersConnected == 0 {
|
||||
c.logger.Warn("No peers connected, continuing anyway")
|
||||
if bootstrapPeersConnected == 0 {
|
||||
c.logger.Warn("No bootstrap peers connected, continuing anyway")
|
||||
} else {
|
||||
c.logger.Info("Peer connections completed", zap.Int("connected_count", peersConnected))
|
||||
c.logger.Info("Bootstrap peer connections completed", zap.Int("connected_count", bootstrapPeersConnected))
|
||||
}
|
||||
|
||||
c.logger.Info("Adding peers to peerstore...")
|
||||
c.logger.Info("Adding bootstrap peers to peerstore...")
|
||||
|
||||
// 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 {
|
||||
// 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 {
|
||||
if peerInfo, err := peer.AddrInfoFromP2pAddr(ma); err == nil {
|
||||
c.host.Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, time.Hour*24)
|
||||
c.logger.Debug("Added peer to peerstore",
|
||||
c.logger.Debug("Added bootstrap peer to peerstore",
|
||||
zap.String("peer", peerInfo.ID.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
c.logger.Info("Peers added to peerstore")
|
||||
c.logger.Info("Bootstrap peers added to peerstore")
|
||||
|
||||
c.logger.Info("Starting connection monitoring...")
|
||||
|
||||
// Client is a lightweight P2P participant - no discovery needed
|
||||
// We only connect to known peers and let nodes handle discovery
|
||||
// We only connect to known bootstrap peers and let nodes handle discovery
|
||||
c.logger.Debug("Client configured as lightweight P2P participant (no discovery)")
|
||||
|
||||
// Start minimal connection monitoring
|
||||
@ -329,18 +322,6 @@ 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
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// connectToPeer connects to a peer address
|
||||
func (c *Client) connectToPeer(ctx context.Context, addr string) error {
|
||||
// connectToBootstrap connects to a bootstrap peer
|
||||
func (c *Client) connectToBootstrap(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) connectToPeer(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("Peer address missing peer ID, skipping",
|
||||
c.logger.Warn("Bootstrap address missing peer ID, skipping",
|
||||
zap.String("addr", addr))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Avoid dialing ourselves: if the peer address resolves to our own peer ID, skip.
|
||||
// Avoid dialing ourselves: if the bootstrap address resolves to our own peer ID, skip.
|
||||
if c.host != nil && peerInfo.ID == c.host.ID() {
|
||||
c.logger.Debug("Skipping peer address because it resolves to self",
|
||||
c.logger.Debug("Skipping bootstrap 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) connectToPeer(ctx context.Context, addr string) error {
|
||||
return fmt.Errorf("failed to connect to peer: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Debug("Connected to peer",
|
||||
c.logger.Debug("Connected to bootstrap peer",
|
||||
zap.String("peer_id", peerInfo.ID.String()),
|
||||
zap.String("addr", addr))
|
||||
|
||||
|
||||
@ -9,24 +9,9 @@ import (
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
)
|
||||
|
||||
// DefaultBootstrapPeers returns the default peer multiaddrs.
|
||||
// DefaultBootstrapPeers returns the library's default bootstrap peer multiaddrs.
|
||||
// These can be overridden by environment variables or config.
|
||||
func DefaultBootstrapPeers() []string {
|
||||
// Check environment variable first
|
||||
if envPeers := os.Getenv("DEBROS_BOOTSTRAP_PEERS"); envPeers != "" {
|
||||
peers := splitCSVOrSpace(envPeers)
|
||||
// Filter out empty strings
|
||||
result := make([]string, 0, len(peers))
|
||||
for _, p := range peers {
|
||||
if p != "" {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
if len(result) > 0 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
defaultCfg := config.DefaultConfig()
|
||||
return defaultCfg.Discovery.BootstrapPeers
|
||||
}
|
||||
@ -48,7 +33,7 @@ func DefaultDatabaseEndpoints() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// Try to derive from configured peers if available
|
||||
// Try to derive from bootstrap peers if available
|
||||
peers := DefaultBootstrapPeers()
|
||||
if len(peers) > 0 {
|
||||
endpoints := make([]string, 0, len(peers))
|
||||
|
||||
@ -10,15 +10,10 @@ import (
|
||||
func TestDefaultBootstrapPeersNonEmpty(t *testing.T) {
|
||||
old := os.Getenv("DEBROS_BOOTSTRAP_PEERS")
|
||||
t.Cleanup(func() { os.Setenv("DEBROS_BOOTSTRAP_PEERS", old) })
|
||||
// Set a valid peer
|
||||
validPeer := "/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"
|
||||
_ = os.Setenv("DEBROS_BOOTSTRAP_PEERS", validPeer)
|
||||
_ = os.Setenv("DEBROS_BOOTSTRAP_PEERS", "") // ensure not set
|
||||
peers := DefaultBootstrapPeers()
|
||||
if len(peers) == 0 {
|
||||
t.Fatalf("expected non-empty default peers")
|
||||
}
|
||||
if peers[0] != validPeer {
|
||||
t.Fatalf("expected peer %s, got %s", validPeer, peers[0])
|
||||
t.Fatalf("expected non-empty default bootstrap peers")
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,10 +45,7 @@ func TestNormalizeEndpoints(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEndpointFromMultiaddr(t *testing.T) {
|
||||
ma, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/4001")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create multiaddr: %v", err)
|
||||
}
|
||||
ma, _ := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/4001")
|
||||
if ep := endpointFromMultiaddr(ma, 5001); ep != "http://127.0.0.1:5001" {
|
||||
t.Fatalf("unexpected endpoint: %s", ep)
|
||||
}
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,10 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
"github.com/rqlite/gorqlite"
|
||||
)
|
||||
|
||||
@ -157,31 +160,17 @@ func (d *DatabaseClientImpl) isWriteOperation(sql string) bool {
|
||||
func (d *DatabaseClientImpl) clearConnection() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
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.RLock()
|
||||
conn := d.connection
|
||||
d.mu.RUnlock()
|
||||
|
||||
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
|
||||
defer d.mu.Unlock()
|
||||
|
||||
// Always try to get a fresh connection to handle leadership changes
|
||||
// and node failures gracefully
|
||||
return d.connectToAvailableNode()
|
||||
}
|
||||
|
||||
// getRQLiteNodes returns a list of RQLite node URLs with precedence:
|
||||
@ -198,7 +187,8 @@ func (d *DatabaseClientImpl) getRQLiteNodes() []string {
|
||||
return DefaultDatabaseEndpoints()
|
||||
}
|
||||
|
||||
// hasPort checks if a hostport string has a port suffix
|
||||
// normalizeEndpoints is now imported from defaults.go
|
||||
|
||||
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 {
|
||||
@ -237,6 +227,7 @@ func (d *DatabaseClientImpl) connectToAvailableNode() (*gorqlite.Connection, err
|
||||
continue
|
||||
}
|
||||
|
||||
d.connection = conn
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
@ -400,3 +391,175 @@ 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
|
||||
}
|
||||
@ -2,7 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -17,9 +17,6 @@ type NetworkClient interface {
|
||||
// Network information
|
||||
Network() NetworkInfo
|
||||
|
||||
// Storage operations (IPFS)
|
||||
Storage() StorageClient
|
||||
|
||||
// Lifecycle
|
||||
Connect() error
|
||||
Disconnect() error
|
||||
@ -54,24 +51,6 @@ type NetworkInfo interface {
|
||||
DisconnectFromPeer(ctx context.Context, peerID string) error
|
||||
}
|
||||
|
||||
// StorageClient provides IPFS storage operations
|
||||
type StorageClient interface {
|
||||
// Upload uploads content to IPFS and pins it
|
||||
Upload(ctx context.Context, reader io.Reader, name string) (*StorageUploadResult, error)
|
||||
|
||||
// Pin pins an existing CID
|
||||
Pin(ctx context.Context, cid string, name string) (*StoragePinResult, error)
|
||||
|
||||
// Status gets the pin status for a CID
|
||||
Status(ctx context.Context, cid string) (*StorageStatus, error)
|
||||
|
||||
// Get retrieves content from IPFS by CID
|
||||
Get(ctx context.Context, cid string) (io.ReadCloser, error)
|
||||
|
||||
// Unpin removes a pin from a CID
|
||||
Unpin(ctx context.Context, cid string) error
|
||||
}
|
||||
|
||||
// MessageHandler is called when a pub/sub message is received
|
||||
type MessageHandler func(topic string, data []byte) error
|
||||
|
||||
@ -114,25 +93,10 @@ type PeerInfo struct {
|
||||
// NetworkStatus contains overall network status
|
||||
type NetworkStatus struct {
|
||||
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
|
||||
@ -143,27 +107,36 @@ type HealthStatus struct {
|
||||
ResponseTime time.Duration `json:"response_time"`
|
||||
}
|
||||
|
||||
// StorageUploadResult represents the result of uploading content to IPFS
|
||||
type StorageUploadResult struct {
|
||||
Cid string `json:"cid"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
// 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"`
|
||||
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
|
||||
}
|
||||
|
||||
// StoragePinResult represents the result of pinning a CID
|
||||
type StoragePinResult struct {
|
||||
Cid string `json:"cid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
// DefaultClientConfig returns a default client configuration
|
||||
func DefaultClientConfig(appName string) *ClientConfig {
|
||||
// Base defaults
|
||||
peers := DefaultBootstrapPeers()
|
||||
endpoints := DefaultDatabaseEndpoints()
|
||||
|
||||
// StorageStatus represents the status of a pinned CID
|
||||
type StorageStatus struct {
|
||||
Cid string `json:"cid"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // "pinned", "pinning", "queued", "unpinned", "error"
|
||||
ReplicationMin int `json:"replication_min"`
|
||||
ReplicationMax int `json:"replication_max"`
|
||||
ReplicationFactor int `json:"replication_factor"`
|
||||
Peers []string `json:"peers"`
|
||||
Error string `json:"error,omitempty"`
|
||||
return &ClientConfig{
|
||||
AppName: appName,
|
||||
DatabaseName: fmt.Sprintf("%s_db", appName),
|
||||
BootstrapPeers: peers,
|
||||
DatabaseEndpoints: endpoints,
|
||||
ConnectTimeout: time.Second * 30,
|
||||
RetryAttempts: 3,
|
||||
RetryDelay: time.Second * 5,
|
||||
QuietMode: false,
|
||||
APIKey: "",
|
||||
JWT: "",
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,270 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,225 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StorageClientImpl implements StorageClient using HTTP requests to the gateway
|
||||
type StorageClientImpl struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Upload uploads content to IPFS and pins it
|
||||
func (s *StorageClientImpl) Upload(ctx context.Context, reader io.Reader, name string) (*StorageUploadResult, error) {
|
||||
if err := s.client.requireAccess(ctx); err != nil {
|
||||
return nil, fmt.Errorf("authentication required: %w", err)
|
||||
}
|
||||
|
||||
gatewayURL := s.getGatewayURL()
|
||||
|
||||
// Create multipart form
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
// Add file field
|
||||
part, err := writer.CreateFormFile("file", name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create form file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, reader); err != nil {
|
||||
return nil, fmt.Errorf("failed to copy data: %w", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return nil, fmt.Errorf("failed to close writer: %w", err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", gatewayURL+"/v1/storage/upload", &buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
s.addAuthHeaders(req)
|
||||
|
||||
// Execute request
|
||||
client := &http.Client{Timeout: 5 * time.Minute} // Large timeout for file uploads
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result StorageUploadResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Pin pins an existing CID
|
||||
func (s *StorageClientImpl) Pin(ctx context.Context, cid string, name string) (*StoragePinResult, error) {
|
||||
if err := s.client.requireAccess(ctx); err != nil {
|
||||
return nil, fmt.Errorf("authentication required: %w", err)
|
||||
}
|
||||
|
||||
gatewayURL := s.getGatewayURL()
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"cid": cid,
|
||||
}
|
||||
if name != "" {
|
||||
reqBody["name"] = name
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", gatewayURL+"/v1/storage/pin", bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.addAuthHeaders(req)
|
||||
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("pin failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result StoragePinResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Status gets the pin status for a CID
|
||||
func (s *StorageClientImpl) Status(ctx context.Context, cid string) (*StorageStatus, error) {
|
||||
if err := s.client.requireAccess(ctx); err != nil {
|
||||
return nil, fmt.Errorf("authentication required: %w", err)
|
||||
}
|
||||
|
||||
gatewayURL := s.getGatewayURL()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", gatewayURL+"/v1/storage/status/"+cid, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
s.addAuthHeaders(req)
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("status failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result StorageStatus
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Get retrieves content from IPFS by CID
|
||||
func (s *StorageClientImpl) Get(ctx context.Context, cid string) (io.ReadCloser, error) {
|
||||
if err := s.client.requireAccess(ctx); err != nil {
|
||||
return nil, fmt.Errorf("authentication required: %w", err)
|
||||
}
|
||||
|
||||
gatewayURL := s.getGatewayURL()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", gatewayURL+"/v1/storage/get/"+cid, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
s.addAuthHeaders(req)
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Minute} // Large timeout for file downloads
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("get failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// Unpin removes a pin from a CID
|
||||
func (s *StorageClientImpl) Unpin(ctx context.Context, cid string) error {
|
||||
if err := s.client.requireAccess(ctx); err != nil {
|
||||
return fmt.Errorf("authentication required: %w", err)
|
||||
}
|
||||
|
||||
gatewayURL := s.getGatewayURL()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "DELETE", gatewayURL+"/v1/storage/unpin/"+cid, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
s.addAuthHeaders(req)
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("unpin failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getGatewayURL returns the gateway URL from config
|
||||
func (s *StorageClientImpl) getGatewayURL() string {
|
||||
return getGatewayURL(s.client)
|
||||
}
|
||||
|
||||
// addAuthHeaders adds authentication headers to the request
|
||||
func (s *StorageClientImpl) addAuthHeaders(req *http.Request) {
|
||||
addAuthHeaders(req, s.client)
|
||||
}
|
||||
@ -1,378 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStorageClientImpl_Upload(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
expectedCID := "QmUpload123"
|
||||
expectedName := "test.txt"
|
||||
expectedSize := int64(100)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/storage/upload" {
|
||||
t.Errorf("Expected path '/v1/storage/upload', got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
// Verify multipart form
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
t.Errorf("Failed to parse multipart form: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get file: %v", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if header.Filename != expectedName {
|
||||
t.Errorf("Expected filename %s, got %s", expectedName, header.Filename)
|
||||
}
|
||||
|
||||
response := StorageUploadResult{
|
||||
Cid: expectedCID,
|
||||
Name: expectedName,
|
||||
Size: expectedSize,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := &ClientConfig{
|
||||
GatewayURL: server.URL,
|
||||
AppName: "test-app",
|
||||
APIKey: "ak_test:test-app", // Required for requireAccess check
|
||||
}
|
||||
client := &Client{config: cfg}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
reader := strings.NewReader("test content")
|
||||
result, err := storage.Upload(context.Background(), reader, expectedName)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to upload: %v", err)
|
||||
}
|
||||
|
||||
if result.Cid != expectedCID {
|
||||
t.Errorf("Expected CID %s, got %s", expectedCID, result.Cid)
|
||||
}
|
||||
if result.Name != expectedName {
|
||||
t.Errorf("Expected name %s, got %s", expectedName, result.Name)
|
||||
}
|
||||
if result.Size != expectedSize {
|
||||
t.Errorf("Expected size %d, got %d", expectedSize, result.Size)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("server_error", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("internal error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := &ClientConfig{
|
||||
GatewayURL: server.URL,
|
||||
AppName: "test-app",
|
||||
}
|
||||
client := &Client{config: cfg}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
reader := strings.NewReader("test")
|
||||
_, err := storage.Upload(context.Background(), reader, "test.txt")
|
||||
if err == nil {
|
||||
t.Error("Expected error for server error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing_credentials", func(t *testing.T) {
|
||||
cfg := &ClientConfig{
|
||||
GatewayURL: "http://localhost:6001",
|
||||
// No AppName, JWT, or APIKey
|
||||
}
|
||||
client := &Client{config: cfg}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
reader := strings.NewReader("test")
|
||||
_, err := storage.Upload(context.Background(), reader, "test.txt")
|
||||
if err == nil {
|
||||
t.Error("Expected error for missing credentials")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStorageClientImpl_Pin(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
expectedCID := "QmPin123"
|
||||
expectedName := "pinned-file"
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/storage/pin" {
|
||||
t.Errorf("Expected path '/v1/storage/pin', got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
var reqBody map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||
t.Errorf("Failed to decode request: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody["cid"] != expectedCID {
|
||||
t.Errorf("Expected CID %s, got %v", expectedCID, reqBody["cid"])
|
||||
}
|
||||
|
||||
response := StoragePinResult{
|
||||
Cid: expectedCID,
|
||||
Name: expectedName,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := &ClientConfig{
|
||||
GatewayURL: server.URL,
|
||||
AppName: "test-app",
|
||||
APIKey: "ak_test:test-app", // Required for requireAccess check
|
||||
}
|
||||
client := &Client{config: cfg}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
result, err := storage.Pin(context.Background(), expectedCID, expectedName)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to pin: %v", err)
|
||||
}
|
||||
|
||||
if result.Cid != expectedCID {
|
||||
t.Errorf("Expected CID %s, got %s", expectedCID, result.Cid)
|
||||
}
|
||||
if result.Name != expectedName {
|
||||
t.Errorf("Expected name %s, got %s", expectedName, result.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStorageClientImpl_Status(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
expectedCID := "QmStatus123"
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.URL.Path, "/v1/storage/status/") {
|
||||
t.Errorf("Expected path '/v1/storage/status/', got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
response := StorageStatus{
|
||||
Cid: expectedCID,
|
||||
Name: "test-file",
|
||||
Status: "pinned",
|
||||
ReplicationMin: 3,
|
||||
ReplicationMax: 3,
|
||||
ReplicationFactor: 3,
|
||||
Peers: []string{"peer1", "peer2", "peer3"},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := &ClientConfig{
|
||||
GatewayURL: server.URL,
|
||||
AppName: "test-app",
|
||||
APIKey: "ak_test:test-app", // Required for requireAccess check
|
||||
}
|
||||
client := &Client{config: cfg}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
status, err := storage.Status(context.Background(), expectedCID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get status: %v", err)
|
||||
}
|
||||
|
||||
if status.Cid != expectedCID {
|
||||
t.Errorf("Expected CID %s, got %s", expectedCID, status.Cid)
|
||||
}
|
||||
if status.Status != "pinned" {
|
||||
t.Errorf("Expected status 'pinned', got %s", status.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStorageClientImpl_Get(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
expectedCID := "QmGet123"
|
||||
expectedContent := "test content"
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.URL.Path, "/v1/storage/get/") {
|
||||
t.Errorf("Expected path '/v1/storage/get/', got %s", r.URL.Path)
|
||||
}
|
||||
w.Write([]byte(expectedContent))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := &ClientConfig{
|
||||
GatewayURL: server.URL,
|
||||
AppName: "test-app",
|
||||
APIKey: "ak_test:test-app", // Required for requireAccess check
|
||||
}
|
||||
client := &Client{config: cfg}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
reader, err := storage.Get(context.Background(), expectedCID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get content: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read content: %v", err)
|
||||
}
|
||||
|
||||
if string(data) != expectedContent {
|
||||
t.Errorf("Expected content %s, got %s", expectedContent, string(data))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStorageClientImpl_Unpin(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
expectedCID := "QmUnpin123"
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.URL.Path, "/v1/storage/unpin/") {
|
||||
t.Errorf("Expected path '/v1/storage/unpin/', got %s", r.URL.Path)
|
||||
}
|
||||
if r.Method != "DELETE" {
|
||||
t.Errorf("Expected method DELETE, got %s", r.Method)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := &ClientConfig{
|
||||
GatewayURL: server.URL,
|
||||
AppName: "test-app",
|
||||
APIKey: "ak_test:test-app", // Required for requireAccess check
|
||||
}
|
||||
client := &Client{config: cfg}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
err := storage.Unpin(context.Background(), expectedCID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unpin: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStorageClientImpl_getGatewayURL(t *testing.T) {
|
||||
storage := &StorageClientImpl{}
|
||||
|
||||
t.Run("from_config", func(t *testing.T) {
|
||||
cfg := &ClientConfig{GatewayURL: "http://custom:6001"}
|
||||
client := &Client{config: cfg}
|
||||
storage.client = client
|
||||
|
||||
url := storage.getGatewayURL()
|
||||
if url != "http://custom:6001" {
|
||||
t.Errorf("Expected 'http://custom:6001', got %s", url)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
cfg := &ClientConfig{}
|
||||
client := &Client{config: cfg}
|
||||
storage.client = client
|
||||
|
||||
url := storage.getGatewayURL()
|
||||
if url != "http://localhost:6001" {
|
||||
t.Errorf("Expected 'http://localhost:6001', got %s", url)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil_config", func(t *testing.T) {
|
||||
client := &Client{config: nil}
|
||||
storage.client = client
|
||||
|
||||
url := storage.getGatewayURL()
|
||||
if url != "http://localhost:6001" {
|
||||
t.Errorf("Expected 'http://localhost:6001', got %s", url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStorageClientImpl_addAuthHeaders(t *testing.T) {
|
||||
t.Run("jwt_preferred", func(t *testing.T) {
|
||||
cfg := &ClientConfig{
|
||||
JWT: "test-jwt-token",
|
||||
APIKey: "test-api-key",
|
||||
}
|
||||
client := &Client{config: cfg}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
req := httptest.NewRequest("POST", "/test", nil)
|
||||
storage.addAuthHeaders(req)
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "Bearer test-jwt-token" {
|
||||
t.Errorf("Expected JWT in Authorization header, got %s", auth)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("apikey_fallback", func(t *testing.T) {
|
||||
cfg := &ClientConfig{
|
||||
APIKey: "test-api-key",
|
||||
}
|
||||
client := &Client{config: cfg}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
req := httptest.NewRequest("POST", "/test", nil)
|
||||
storage.addAuthHeaders(req)
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "Bearer test-api-key" {
|
||||
t.Errorf("Expected API key in Authorization header, got %s", auth)
|
||||
}
|
||||
|
||||
apiKey := req.Header.Get("X-API-Key")
|
||||
if apiKey != "test-api-key" {
|
||||
t.Errorf("Expected API key in X-API-Key header, got %s", apiKey)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no_auth", func(t *testing.T) {
|
||||
cfg := &ClientConfig{}
|
||||
client := &Client{config: cfg}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
req := httptest.NewRequest("POST", "/test", nil)
|
||||
storage.addAuthHeaders(req)
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "" {
|
||||
t.Errorf("Expected no Authorization header, got %s", auth)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil_config", func(t *testing.T) {
|
||||
client := &Client{config: nil}
|
||||
storage := &StorageClientImpl{client: client}
|
||||
|
||||
req := httptest.NewRequest("POST", "/test", nil)
|
||||
storage.addAuthHeaders(req)
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "" {
|
||||
t.Errorf("Expected no Authorization header, got %s", auth)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
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,7 +3,6 @@ package config
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/config/validate"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
)
|
||||
|
||||
@ -14,70 +13,62 @@ type Config struct {
|
||||
Discovery DiscoveryConfig `yaml:"discovery"`
|
||||
Security SecurityConfig `yaml:"security"`
|
||||
Logging LoggingConfig `yaml:"logging"`
|
||||
HTTPGateway HTTPGatewayConfig `yaml:"http_gateway"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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 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,
|
||||
})...)
|
||||
// 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 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,
|
||||
})...)
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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,
|
||||
})...)
|
||||
// SecurityConfig contains security-related configuration
|
||||
type SecurityConfig struct {
|
||||
EnableTLS bool `yaml:"enable_tls"`
|
||||
PrivateKeyFile string `yaml:"private_key_file"`
|
||||
CertificateFile string `yaml:"certificate_file"`
|
||||
}
|
||||
|
||||
// Validate security config
|
||||
errs = append(errs, validate.ValidateSecurity(validate.SecurityConfig{
|
||||
EnableTLS: c.Security.EnableTLS,
|
||||
PrivateKeyFile: c.Security.PrivateKeyFile,
|
||||
CertificateFile: c.Security.CertificateFile,
|
||||
})...)
|
||||
// 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
|
||||
}
|
||||
|
||||
// Validate logging config
|
||||
errs = append(errs, validate.ValidateLogging(validate.LoggingConfig{
|
||||
Level: c.Logging.Level,
|
||||
Format: c.Logging.Format,
|
||||
OutputFile: c.Logging.OutputFile,
|
||||
})...)
|
||||
|
||||
return errs
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// ParseMultiaddrs converts string addresses to multiaddr objects
|
||||
@ -97,6 +88,7 @@ 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
|
||||
},
|
||||
@ -113,25 +105,7 @@ func DefaultConfig() *Config {
|
||||
// RQLite-specific configuration
|
||||
RQLitePort: 5001,
|
||||
RQLiteRaftPort: 7001,
|
||||
RQLiteJoinAddress: "", // Empty for first node (creates cluster)
|
||||
|
||||
// Dynamic discovery (always enabled)
|
||||
ClusterSyncInterval: 30 * time.Second,
|
||||
PeerInactivityLimit: 24 * time.Hour,
|
||||
MinClusterSize: 1,
|
||||
|
||||
// Olric cache configuration
|
||||
OlricHTTPPort: 3320,
|
||||
OlricMemberlistPort: 3322,
|
||||
|
||||
// IPFS storage configuration
|
||||
IPFS: IPFSConfig{
|
||||
ClusterAPIURL: "", // Empty = disabled
|
||||
APIURL: "http://localhost:5001",
|
||||
Timeout: 60 * time.Second,
|
||||
ReplicationFactor: 3,
|
||||
EnableEncryption: true,
|
||||
},
|
||||
RQLiteJoinAddress: "", // Empty for bootstrap node
|
||||
},
|
||||
Discovery: DiscoveryConfig{
|
||||
BootstrapPeers: []string{},
|
||||
@ -148,18 +122,5 @@ 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
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