mirror of
https://github.com/DeBrosOfficial/network.git
synced 2026-01-30 01:53:02 +00:00
feat: refactor API gateway and CLI utilities for improved functionality
- Updated the API gateway documentation to reflect changes in architecture and functionality, emphasizing its role as a multi-functional entry point for decentralized services. - Refactored CLI commands to utilize utility functions for better code organization and maintainability. - Introduced new utility functions for handling peer normalization, service management, and port validation, enhancing the overall CLI experience. - Added a new production installation script to streamline the setup process for users, including detailed dry-run summaries for better visibility. - Enhanced validation mechanisms for configuration files and swarm keys, ensuring robust error handling and user feedback during setup.
This commit is contained in:
parent
b3b1905fb2
commit
4ee76588ed
7
Makefile
7
Makefile
@ -71,14 +71,9 @@ run-gateway:
|
||||
@echo "Note: Config must be in ~/.orama/data/gateway.yaml"
|
||||
go run ./cmd/orama-gateway
|
||||
|
||||
# Setup local domain names for development
|
||||
setup-domains:
|
||||
@echo "Setting up local domains..."
|
||||
@sudo bash scripts/setup-local-domains.sh
|
||||
|
||||
# Development environment target
|
||||
# Uses orama dev up to start full stack with dependency and port checking
|
||||
dev: build setup-domains
|
||||
dev: build
|
||||
@./bin/orama dev up
|
||||
|
||||
# Graceful shutdown of all dev services
|
||||
|
||||
12
README.md
12
README.md
@ -26,27 +26,25 @@ make stop
|
||||
|
||||
After running `make dev`, test service health using these curl requests:
|
||||
|
||||
> **Note:** Local domains (node-1.local, etc.) require running `sudo make setup-domains` first. Alternatively, use `localhost` with port numbers.
|
||||
|
||||
### Node Unified Gateways
|
||||
|
||||
Each node is accessible via a single unified gateway port:
|
||||
|
||||
```bash
|
||||
# Node-1 (port 6001)
|
||||
curl http://node-1.local:6001/health
|
||||
curl http://localhost:6001/health
|
||||
|
||||
# Node-2 (port 6002)
|
||||
curl http://node-2.local:6002/health
|
||||
curl http://localhost:6002/health
|
||||
|
||||
# Node-3 (port 6003)
|
||||
curl http://node-3.local:6003/health
|
||||
curl http://localhost:6003/health
|
||||
|
||||
# Node-4 (port 6004)
|
||||
curl http://node-4.local:6004/health
|
||||
curl http://localhost:6004/health
|
||||
|
||||
# Node-5 (port 6005)
|
||||
curl http://node-5.local:6005/health
|
||||
curl http://localhost:6005/health
|
||||
```
|
||||
|
||||
## Network Architecture
|
||||
|
||||
123
e2e/serverless_test.go
Normal file
123
e2e/serverless_test.go
Normal file
@ -0,0 +1,123 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestServerless_DeployAndInvoke(t *testing.T) {
|
||||
SkipIfMissingGateway(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
wasmPath := "../examples/functions/bin/hello.wasm"
|
||||
if _, err := os.Stat(wasmPath); os.IsNotExist(err) {
|
||||
t.Skip("hello.wasm not found")
|
||||
}
|
||||
|
||||
wasmBytes, err := os.ReadFile(wasmPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read hello.wasm: %v", err)
|
||||
}
|
||||
|
||||
funcName := "e2e-hello"
|
||||
namespace := "default"
|
||||
|
||||
// 1. Deploy function
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
// Add metadata
|
||||
_ = writer.WriteField("name", funcName)
|
||||
_ = writer.WriteField("namespace", namespace)
|
||||
|
||||
// Add WASM file
|
||||
part, err := writer.CreateFormFile("wasm", funcName+".wasm")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create form file: %v", err)
|
||||
}
|
||||
part.Write(wasmBytes)
|
||||
writer.Close()
|
||||
|
||||
deployReq, _ := http.NewRequestWithContext(ctx, "POST", GetGatewayURL()+"/v1/functions", &buf)
|
||||
deployReq.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
deployReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
client := NewHTTPClient(1 * time.Minute)
|
||||
resp, err := client.Do(deployReq)
|
||||
if err != nil {
|
||||
t.Fatalf("deploy request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("deploy failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 2. Invoke function
|
||||
invokePayload := []byte(`{"name": "E2E Tester"}`)
|
||||
invokeReq, _ := http.NewRequestWithContext(ctx, "POST", GetGatewayURL()+"/v1/functions/"+funcName+"/invoke", bytes.NewReader(invokePayload))
|
||||
invokeReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
invokeReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
resp, err = client.Do(invokeReq)
|
||||
if err != nil {
|
||||
t.Fatalf("invoke request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("invoke failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(resp.Body)
|
||||
expected := "Hello, E2E Tester!"
|
||||
if !bytes.Contains(output, []byte(expected)) {
|
||||
t.Errorf("output %q does not contain %q", string(output), expected)
|
||||
}
|
||||
|
||||
// 3. List functions
|
||||
listReq, _ := http.NewRequestWithContext(ctx, "GET", GetGatewayURL()+"/v1/functions?namespace="+namespace, nil)
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
listReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
resp, err = client.Do(listReq)
|
||||
if err != nil {
|
||||
t.Fatalf("list request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("list failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 4. Delete function
|
||||
deleteReq, _ := http.NewRequestWithContext(ctx, "DELETE", GetGatewayURL()+"/v1/functions/"+funcName+"?namespace="+namespace, nil)
|
||||
if apiKey := GetAPIKey(); apiKey != "" {
|
||||
deleteReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
resp, err = client.Do(deleteReq)
|
||||
if err != nil {
|
||||
t.Fatalf("delete request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("delete failed with status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@ -208,6 +208,11 @@ func (h *ServerlessHandlers) deployFunction(w http.ResponseWriter, r *http.Reque
|
||||
def.Name = r.FormValue("name")
|
||||
}
|
||||
|
||||
// Get namespace from form if not in metadata
|
||||
if def.Namespace == "" {
|
||||
def.Namespace = r.FormValue("namespace")
|
||||
}
|
||||
|
||||
// Get WASM file
|
||||
file, _, err := r.FormFile("wasm")
|
||||
if err != nil {
|
||||
@ -578,7 +583,7 @@ func (h *ServerlessHandlers) getNamespaceFromRequest(r *http.Request) string {
|
||||
return ns
|
||||
}
|
||||
|
||||
return ""
|
||||
return "default"
|
||||
}
|
||||
|
||||
// getWalletFromRequest extracts wallet address from JWT
|
||||
|
||||
84
pkg/gateway/serverless_handlers_test.go
Normal file
84
pkg/gateway/serverless_handlers_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/serverless"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type mockFunctionRegistry struct {
|
||||
functions []*serverless.Function
|
||||
}
|
||||
|
||||
func (m *mockFunctionRegistry) Register(ctx context.Context, fn *serverless.FunctionDefinition, wasmBytes []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockFunctionRegistry) Get(ctx context.Context, namespace, name string, version int) (*serverless.Function, error) {
|
||||
return &serverless.Function{ID: "1", Name: name, Namespace: namespace}, nil
|
||||
}
|
||||
|
||||
func (m *mockFunctionRegistry) List(ctx context.Context, namespace string) ([]*serverless.Function, error) {
|
||||
return m.functions, nil
|
||||
}
|
||||
|
||||
func (m *mockFunctionRegistry) Delete(ctx context.Context, namespace, name string, version int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockFunctionRegistry) GetWASMBytes(ctx context.Context, wasmCID string) ([]byte, error) {
|
||||
return []byte("wasm"), nil
|
||||
}
|
||||
|
||||
func TestServerlessHandlers_ListFunctions(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
registry := &mockFunctionRegistry{
|
||||
functions: []*serverless.Function{
|
||||
{ID: "1", Name: "func1", Namespace: "ns1"},
|
||||
{ID: "2", Name: "func2", Namespace: "ns1"},
|
||||
},
|
||||
}
|
||||
|
||||
h := NewServerlessHandlers(nil, registry, nil, logger)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/v1/functions?namespace=ns1", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
h.handleFunctions(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", rr.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(rr.Body.Bytes(), &resp)
|
||||
|
||||
if resp["count"].(float64) != 2 {
|
||||
t.Errorf("expected 2 functions, got %v", resp["count"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerlessHandlers_DeployFunction(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
registry := &mockFunctionRegistry{}
|
||||
|
||||
h := NewServerlessHandlers(nil, registry, nil, logger)
|
||||
|
||||
// Test JSON deploy (which is partially supported according to code)
|
||||
// Should be 400 because WASM is missing or base64 not supported
|
||||
writer := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/v1/functions", bytes.NewBufferString(`{"name": "test"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h.handleFunctions(writer, req)
|
||||
|
||||
if writer.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d", writer.Code)
|
||||
}
|
||||
}
|
||||
@ -228,7 +228,12 @@ func (g *Gateway) storageStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := g.ipfsClient.PinStatus(ctx, path)
|
||||
if err != nil {
|
||||
g.logger.ComponentError(logging.ComponentGeneral, "failed to get pin status", zap.Error(err), zap.String("cid", path))
|
||||
errStr := strings.ToLower(err.Error())
|
||||
if strings.Contains(errStr, "not found") || strings.Contains(errStr, "404") || strings.Contains(errStr, "invalid") {
|
||||
writeError(w, http.StatusNotFound, fmt.Sprintf("pin not found: %s", path))
|
||||
} else {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get status: %v", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -283,7 +288,8 @@ func (g *Gateway) storageGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
g.logger.ComponentError(logging.ComponentGeneral, "failed to get content from IPFS", zap.Error(err), zap.String("cid", path))
|
||||
// Check if error indicates content not found (404)
|
||||
if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "status 404") {
|
||||
errStr := strings.ToLower(err.Error())
|
||||
if strings.Contains(errStr, "not found") || strings.Contains(errStr, "404") || strings.Contains(errStr, "invalid") {
|
||||
writeError(w, http.StatusNotFound, fmt.Sprintf("content not found: %s", path))
|
||||
} else {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get content: %v", err))
|
||||
|
||||
@ -570,9 +570,13 @@ func (g *HTTPGateway) handleDropTable(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := g.withTimeout(r.Context())
|
||||
defer cancel()
|
||||
|
||||
stmt := "DROP TABLE IF EXISTS " + tbl
|
||||
stmt := "DROP TABLE " + tbl
|
||||
if _, err := g.Client.Exec(ctx, stmt); err != nil {
|
||||
if strings.Contains(err.Error(), "no such table") {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
} else {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
|
||||
|
||||
151
pkg/serverless/engine_test.go
Normal file
151
pkg/serverless/engine_test.go
Normal file
@ -0,0 +1,151 @@
|
||||
package serverless
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestEngine_Execute(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
registry := NewMockRegistry()
|
||||
hostServices := NewMockHostServices()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.ModuleCacheSize = 2
|
||||
|
||||
engine, err := NewEngine(cfg, registry, hostServices, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create engine: %v", err)
|
||||
}
|
||||
defer engine.Close(context.Background())
|
||||
|
||||
// Use a minimal valid WASM module that exports _start (WASI)
|
||||
// This is just 'nop' in WASM
|
||||
wasmBytes := []byte{
|
||||
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
|
||||
0x03, 0x02, 0x01, 0x00,
|
||||
0x07, 0x0a, 0x01, 0x06, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x00, 0x00,
|
||||
0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b,
|
||||
}
|
||||
|
||||
fnDef := &FunctionDefinition{
|
||||
Name: "test-func",
|
||||
Namespace: "test-ns",
|
||||
MemoryLimitMB: 64,
|
||||
TimeoutSeconds: 5,
|
||||
}
|
||||
|
||||
err = registry.Register(context.Background(), fnDef, wasmBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to register function: %v", err)
|
||||
}
|
||||
|
||||
fn, err := registry.Get(context.Background(), "test-ns", "test-func", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get function: %v", err)
|
||||
}
|
||||
|
||||
// Execute function
|
||||
ctx := context.Background()
|
||||
output, err := engine.Execute(ctx, fn, []byte("input"), nil)
|
||||
if err != nil {
|
||||
t.Errorf("failed to execute function: %v", err)
|
||||
}
|
||||
|
||||
// Our minimal WASM doesn't write to stdout, so output should be empty
|
||||
if len(output) != 0 {
|
||||
t.Errorf("expected empty output, got %d bytes", len(output))
|
||||
}
|
||||
|
||||
// Test cache stats
|
||||
size, capacity := engine.GetCacheStats()
|
||||
if size != 1 {
|
||||
t.Errorf("expected cache size 1, got %d", size)
|
||||
}
|
||||
if capacity != 2 {
|
||||
t.Errorf("expected cache capacity 2, got %d", capacity)
|
||||
}
|
||||
|
||||
// Test Invalidate
|
||||
engine.Invalidate(fn.WASMCID)
|
||||
size, _ = engine.GetCacheStats()
|
||||
if size != 0 {
|
||||
t.Errorf("expected cache size 0 after invalidation, got %d", size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_Precompile(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
registry := NewMockRegistry()
|
||||
hostServices := NewMockHostServices()
|
||||
engine, _ := NewEngine(nil, registry, hostServices, logger)
|
||||
defer engine.Close(context.Background())
|
||||
|
||||
wasmBytes := []byte{
|
||||
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
|
||||
0x03, 0x02, 0x01, 0x00,
|
||||
0x07, 0x0a, 0x01, 0x06, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x00, 0x00,
|
||||
0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b,
|
||||
}
|
||||
|
||||
err := engine.Precompile(context.Background(), "test-cid", wasmBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to precompile: %v", err)
|
||||
}
|
||||
|
||||
size, _ := engine.GetCacheStats()
|
||||
if size != 1 {
|
||||
t.Errorf("expected cache size 1, got %d", size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_Timeout(t *testing.T) {
|
||||
// Skip this for now as it might be hard to trigger with a minimal WASM
|
||||
// but we could try a WASM that loops forever.
|
||||
t.Skip("Hard to trigger timeout with minimal WASM")
|
||||
}
|
||||
|
||||
func TestEngine_RealWASM(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
logger := zap.NewNop()
|
||||
registry := NewMockRegistry()
|
||||
hostServices := NewMockHostServices()
|
||||
engine, _ := NewEngine(nil, registry, hostServices, logger)
|
||||
defer engine.Close(context.Background())
|
||||
|
||||
fnDef := &FunctionDefinition{
|
||||
Name: "hello",
|
||||
Namespace: "examples",
|
||||
TimeoutSeconds: 10,
|
||||
}
|
||||
_ = registry.Register(context.Background(), fnDef, wasmBytes)
|
||||
fn, _ := registry.Get(context.Background(), "examples", "hello", 0)
|
||||
|
||||
output, err := engine.Execute(context.Background(), fn, []byte(`{"name": "Tester"}`), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execution failed: %v", err)
|
||||
}
|
||||
|
||||
expected := "Hello, Tester!"
|
||||
if !contains(string(output), expected) {
|
||||
t.Errorf("output %q does not contain %q", string(output), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s[:len(substr)] == substr || contains(s[1:], substr))
|
||||
}
|
||||
45
pkg/serverless/hostfuncs_test.go
Normal file
45
pkg/serverless/hostfuncs_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package serverless
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestHostFunctions_Cache(t *testing.T) {
|
||||
db := NewMockRQLite()
|
||||
ipfs := NewMockIPFSClient()
|
||||
logger := zap.NewNop()
|
||||
|
||||
// MockOlricClient needs to implement olriclib.Client
|
||||
// For now, let's just test other host functions if Olric is hard to mock
|
||||
|
||||
h := NewHostFunctions(db, nil, ipfs, nil, nil, nil, HostFunctionsConfig{}, logger)
|
||||
|
||||
ctx := context.Background()
|
||||
h.SetInvocationContext(&InvocationContext{
|
||||
RequestID: "req-1",
|
||||
Namespace: "ns-1",
|
||||
})
|
||||
|
||||
// Test Logging
|
||||
h.LogInfo(ctx, "hello world")
|
||||
logs := h.GetLogs()
|
||||
if len(logs) != 1 || logs[0].Message != "hello world" {
|
||||
t.Errorf("unexpected logs: %+v", logs)
|
||||
}
|
||||
|
||||
// Test Storage
|
||||
cid, err := h.StoragePut(ctx, []byte("data"))
|
||||
if err != nil {
|
||||
t.Fatalf("StoragePut failed: %v", err)
|
||||
}
|
||||
data, err := h.StorageGet(ctx, cid)
|
||||
if err != nil {
|
||||
t.Fatalf("StorageGet failed: %v", err)
|
||||
}
|
||||
if string(data) != "data" {
|
||||
t.Errorf("expected 'data', got %q", string(data))
|
||||
}
|
||||
}
|
||||
375
pkg/serverless/mocks_test.go
Normal file
375
pkg/serverless/mocks_test.go
Normal file
@ -0,0 +1,375 @@
|
||||
package serverless
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/ipfs"
|
||||
"github.com/DeBrosOfficial/network/pkg/rqlite"
|
||||
)
|
||||
|
||||
// MockRegistry is a mock implementation of FunctionRegistry
|
||||
type MockRegistry struct {
|
||||
mu sync.RWMutex
|
||||
functions map[string]*Function
|
||||
wasm map[string][]byte
|
||||
}
|
||||
|
||||
func NewMockRegistry() *MockRegistry {
|
||||
return &MockRegistry{
|
||||
functions: make(map[string]*Function),
|
||||
wasm: make(map[string][]byte),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockRegistry) Register(ctx context.Context, fn *FunctionDefinition, wasmBytes []byte) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
id := fn.Namespace + "/" + fn.Name
|
||||
wasmCID := "cid-" + id
|
||||
m.functions[id] = &Function{
|
||||
ID: id,
|
||||
Name: fn.Name,
|
||||
Namespace: fn.Namespace,
|
||||
WASMCID: wasmCID,
|
||||
MemoryLimitMB: fn.MemoryLimitMB,
|
||||
TimeoutSeconds: fn.TimeoutSeconds,
|
||||
Status: FunctionStatusActive,
|
||||
}
|
||||
m.wasm[wasmCID] = wasmBytes
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRegistry) Get(ctx context.Context, namespace, name string, version int) (*Function, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
fn, ok := m.functions[namespace+"/"+name]
|
||||
if !ok {
|
||||
return nil, ErrFunctionNotFound
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
func (m *MockRegistry) List(ctx context.Context, namespace string) ([]*Function, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var res []*Function
|
||||
for _, fn := range m.functions {
|
||||
if fn.Namespace == namespace {
|
||||
res = append(res, fn)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *MockRegistry) Delete(ctx context.Context, namespace, name string, version int) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.functions, namespace+"/"+name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRegistry) GetWASMBytes(ctx context.Context, wasmCID string) ([]byte, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
data, ok := m.wasm[wasmCID]
|
||||
if !ok {
|
||||
return nil, ErrFunctionNotFound
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// MockHostServices is a mock implementation of HostServices
|
||||
type MockHostServices struct {
|
||||
mu sync.RWMutex
|
||||
cache map[string][]byte
|
||||
storage map[string][]byte
|
||||
logs []string
|
||||
}
|
||||
|
||||
func NewMockHostServices() *MockHostServices {
|
||||
return &MockHostServices{
|
||||
cache: make(map[string][]byte),
|
||||
storage: make(map[string][]byte),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockHostServices) DBQuery(ctx context.Context, query string, args []interface{}) ([]byte, error) {
|
||||
return []byte("[]"), nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) DBExecute(ctx context.Context, query string, args []interface{}) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) CacheGet(ctx context.Context, key string) ([]byte, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.cache[key], nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) CacheSet(ctx context.Context, key string, value []byte, ttl int64) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.cache[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) CacheDelete(ctx context.Context, key string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.cache, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) StoragePut(ctx context.Context, data []byte) (string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
cid := "cid-" + time.Now().String()
|
||||
m.storage[cid] = data
|
||||
return cid, nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) StorageGet(ctx context.Context, cid string) ([]byte, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.storage[cid], nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) PubSubPublish(ctx context.Context, topic string, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) WSSend(ctx context.Context, clientID string, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) WSBroadcast(ctx context.Context, topic string, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) HTTPFetch(ctx context.Context, method, url string, headers map[string]string, body []byte) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) GetEnv(ctx context.Context, key string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) GetSecret(ctx context.Context, name string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) GetRequestID(ctx context.Context) string {
|
||||
return "req-123"
|
||||
}
|
||||
|
||||
func (m *MockHostServices) GetCallerWallet(ctx context.Context) string {
|
||||
return "wallet-123"
|
||||
}
|
||||
|
||||
func (m *MockHostServices) EnqueueBackground(ctx context.Context, functionName string, payload []byte) (string, error) {
|
||||
return "job-123", nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) ScheduleOnce(ctx context.Context, functionName string, runAt time.Time, payload []byte) (string, error) {
|
||||
return "timer-123", nil
|
||||
}
|
||||
|
||||
func (m *MockHostServices) LogInfo(ctx context.Context, message string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.logs = append(m.logs, "INFO: "+message)
|
||||
}
|
||||
|
||||
func (m *MockHostServices) LogError(ctx context.Context, message string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.logs = append(m.logs, "ERROR: "+message)
|
||||
}
|
||||
|
||||
// MockIPFSClient is a mock for ipfs.IPFSClient
|
||||
type MockIPFSClient struct {
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
func NewMockIPFSClient() *MockIPFSClient {
|
||||
return &MockIPFSClient{data: make(map[string][]byte)}
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) Add(ctx context.Context, reader io.Reader, filename string) (*ipfs.AddResponse, error) {
|
||||
data, _ := io.ReadAll(reader)
|
||||
cid := "cid-" + filename
|
||||
m.data[cid] = data
|
||||
return &ipfs.AddResponse{Cid: cid, Name: filename}, nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) Pin(ctx context.Context, cid string, name string, replicationFactor int) (*ipfs.PinResponse, error) {
|
||||
return &ipfs.PinResponse{Cid: cid, Name: name}, nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) PinStatus(ctx context.Context, cid string) (*ipfs.PinStatus, error) {
|
||||
return &ipfs.PinStatus{Cid: cid, Status: "pinned"}, nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) Get(ctx context.Context, cid, apiURL string) (io.ReadCloser, error) {
|
||||
data, ok := m.data[cid]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
return io.NopCloser(strings.NewReader(string(data))), nil
|
||||
}
|
||||
|
||||
func (m *MockIPFSClient) Unpin(ctx context.Context, cid string) error { return nil }
|
||||
func (m *MockIPFSClient) Health(ctx context.Context) error { return nil }
|
||||
func (m *MockIPFSClient) GetPeerCount(ctx context.Context) (int, error) { return 1, nil }
|
||||
func (m *MockIPFSClient) Close(ctx context.Context) error { return nil }
|
||||
|
||||
// MockRQLite is a mock implementation of rqlite.Client
|
||||
type MockRQLite struct {
|
||||
mu sync.Mutex
|
||||
tables map[string][]map[string]any
|
||||
}
|
||||
|
||||
func NewMockRQLite() *MockRQLite {
|
||||
return &MockRQLite{
|
||||
tables: make(map[string][]map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockRQLite) Query(ctx context.Context, dest any, query string, args ...any) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Very limited mock query logic for scanning into structs
|
||||
if strings.Contains(query, "FROM functions") {
|
||||
rows := m.tables["functions"]
|
||||
filtered := rows
|
||||
if strings.Contains(query, "namespace = ? AND name = ?") {
|
||||
ns := args[0].(string)
|
||||
name := args[1].(string)
|
||||
filtered = nil
|
||||
for _, r := range rows {
|
||||
if r["namespace"] == ns && r["name"] == name {
|
||||
filtered = append(filtered, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destVal := reflect.ValueOf(dest).Elem()
|
||||
if destVal.Kind() == reflect.Slice {
|
||||
elemType := destVal.Type().Elem()
|
||||
for _, r := range filtered {
|
||||
newElem := reflect.New(elemType).Elem()
|
||||
// This is a simplified mapping
|
||||
if f := newElem.FieldByName("ID"); f.IsValid() {
|
||||
f.SetString(r["id"].(string))
|
||||
}
|
||||
if f := newElem.FieldByName("Name"); f.IsValid() {
|
||||
f.SetString(r["name"].(string))
|
||||
}
|
||||
if f := newElem.FieldByName("Namespace"); f.IsValid() {
|
||||
f.SetString(r["namespace"].(string))
|
||||
}
|
||||
destVal.Set(reflect.Append(destVal, newElem))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRQLite) Exec(ctx context.Context, query string, args ...any) (sql.Result, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return &mockResult{}, nil
|
||||
}
|
||||
|
||||
func (m *MockRQLite) FindBy(ctx context.Context, dest any, table string, criteria map[string]any, opts ...rqlite.FindOption) error {
|
||||
return nil
|
||||
}
|
||||
func (m *MockRQLite) FindOneBy(ctx context.Context, dest any, table string, criteria map[string]any, opts ...rqlite.FindOption) error {
|
||||
return nil
|
||||
}
|
||||
func (m *MockRQLite) Save(ctx context.Context, entity any) error { return nil }
|
||||
func (m *MockRQLite) Remove(ctx context.Context, entity any) error { return nil }
|
||||
func (m *MockRQLite) Repository(table string) any { return nil }
|
||||
|
||||
func (m *MockRQLite) CreateQueryBuilder(table string) *rqlite.QueryBuilder {
|
||||
return nil // Should return a valid QueryBuilder if needed by tests
|
||||
}
|
||||
|
||||
func (m *MockRQLite) Tx(ctx context.Context, fn func(tx rqlite.Tx) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockResult struct{}
|
||||
|
||||
func (m *mockResult) LastInsertId() (int64, error) { return 1, nil }
|
||||
func (m *mockResult) RowsAffected() (int64, error) { return 1, nil }
|
||||
|
||||
// MockOlricClient is a mock for olriclib.Client
|
||||
type MockOlricClient struct {
|
||||
dmaps map[string]*MockDMap
|
||||
}
|
||||
|
||||
func NewMockOlricClient() *MockOlricClient {
|
||||
return &MockOlricClient{dmaps: make(map[string]*MockDMap)}
|
||||
}
|
||||
|
||||
func (m *MockOlricClient) NewDMap(name string) (any, error) {
|
||||
if dm, ok := m.dmaps[name]; ok {
|
||||
return dm, nil
|
||||
}
|
||||
dm := &MockDMap{data: make(map[string][]byte)}
|
||||
m.dmaps[name] = dm
|
||||
return dm, nil
|
||||
}
|
||||
|
||||
func (m *MockOlricClient) Close(ctx context.Context) error { return nil }
|
||||
func (m *MockOlricClient) Stats(ctx context.Context, s string) ([]byte, error) { return nil, nil }
|
||||
func (m *MockOlricClient) Ping(ctx context.Context, s string) error { return nil }
|
||||
func (m *MockOlricClient) RoutingTable(ctx context.Context) (map[uint64][]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// MockDMap is a mock for olriclib.DMap
|
||||
type MockDMap struct {
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
func (m *MockDMap) Get(ctx context.Context, key string) (any, error) {
|
||||
val, ok := m.data[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
return &MockGetResponse{val: val}, nil
|
||||
}
|
||||
|
||||
func (m *MockDMap) Put(ctx context.Context, key string, value any) error {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
m.data[key] = v
|
||||
case string:
|
||||
m.data[key] = []byte(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockDMap) Delete(ctx context.Context, key string) (bool, error) {
|
||||
_, ok := m.data[key]
|
||||
delete(m.data, key)
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
type MockGetResponse struct {
|
||||
val []byte
|
||||
}
|
||||
|
||||
func (m *MockGetResponse) Byte() ([]byte, error) { return m.val, nil }
|
||||
func (m *MockGetResponse) String() (string, error) { return string(m.val), nil }
|
||||
41
pkg/serverless/registry_test.go
Normal file
41
pkg/serverless/registry_test.go
Normal file
@ -0,0 +1,41 @@
|
||||
package serverless
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestRegistry_RegisterAndGet(t *testing.T) {
|
||||
db := NewMockRQLite()
|
||||
ipfs := NewMockIPFSClient()
|
||||
logger := zap.NewNop()
|
||||
|
||||
registry := NewRegistry(db, ipfs, RegistryConfig{IPFSAPIURL: "http://localhost:5001"}, logger)
|
||||
|
||||
ctx := context.Background()
|
||||
fnDef := &FunctionDefinition{
|
||||
Name: "test-func",
|
||||
Namespace: "test-ns",
|
||||
IsPublic: true,
|
||||
}
|
||||
wasmBytes := []byte("mock wasm")
|
||||
|
||||
err := registry.Register(ctx, fnDef, wasmBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Register failed: %v", err)
|
||||
}
|
||||
|
||||
// Since MockRQLite doesn't fully implement Query scanning yet,
|
||||
// we won't be able to test Get() effectively without more work.
|
||||
// But we can check if wasm was uploaded.
|
||||
wasm, err := registry.GetWASMBytes(ctx, "cid-test-func.wasm")
|
||||
if err != nil {
|
||||
t.Fatalf("GetWASMBytes failed: %v", err)
|
||||
}
|
||||
if string(wasm) != "mock wasm" {
|
||||
t.Errorf("expected 'mock wasm', got %q", string(wasm))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Setup local domains for DeBros Network development
|
||||
# Adds entries to /etc/hosts for node-1.local through node-5.local
|
||||
# Maps them to 127.0.0.1 for local development
|
||||
|
||||
set -e
|
||||
|
||||
HOSTS_FILE="/etc/hosts"
|
||||
NODES=("node-1" "node-2" "node-3" "node-4" "node-5")
|
||||
|
||||
# Check if we have sudo access
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "This script requires sudo to modify /etc/hosts"
|
||||
echo "Please run: sudo bash scripts/setup-local-domains.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to add or update domain entry
|
||||
add_domain() {
|
||||
local domain=$1
|
||||
local ip="127.0.0.1"
|
||||
|
||||
# Check if domain already exists
|
||||
if grep -q "^[[:space:]]*$ip[[:space:]]\+$domain" "$HOSTS_FILE"; then
|
||||
echo "✓ $domain already configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Add domain to /etc/hosts
|
||||
echo "$ip $domain" >> "$HOSTS_FILE"
|
||||
echo "✓ Added $domain -> $ip"
|
||||
}
|
||||
|
||||
echo "Setting up local domains for DeBros Network..."
|
||||
echo ""
|
||||
|
||||
# Add each node domain
|
||||
for node in "${NODES[@]}"; do
|
||||
add_domain "${node}.local"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✓ Local domains configured successfully!"
|
||||
echo ""
|
||||
echo "You can now access nodes via:"
|
||||
for node in "${NODES[@]}"; do
|
||||
echo " - ${node}.local (HTTP Gateway)"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Example: curl http://node-1.local:8080/rqlite/http/db/status"
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test local domain routing for DeBros Network
|
||||
# Validates that all HTTP gateway routes are working
|
||||
|
||||
set -e
|
||||
|
||||
NODES=("1" "2" "3" "4" "5")
|
||||
GATEWAY_PORTS=(8080 8081 8082 8083 8084)
|
||||
|
||||
# Color codes
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Counters
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
|
||||
# Test a single endpoint
|
||||
test_endpoint() {
|
||||
local node=$1
|
||||
local port=$2
|
||||
local path=$3
|
||||
local description=$4
|
||||
|
||||
local url="http://node-${node}.local:${port}${path}"
|
||||
|
||||
printf "Testing %-50s ... " "$description"
|
||||
|
||||
if curl -s -f "$url" > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ PASS${NC}"
|
||||
((PASSED++))
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗ FAIL${NC}"
|
||||
((FAILED++))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo "DeBros Network Local Domain Tests"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Test each node's HTTP gateway
|
||||
for i in "${!NODES[@]}"; do
|
||||
node=${NODES[$i]}
|
||||
port=${GATEWAY_PORTS[$i]}
|
||||
|
||||
echo "Testing node-${node}.local (port ${port}):"
|
||||
|
||||
# Test health endpoint
|
||||
test_endpoint "$node" "$port" "/health" "Node-${node} health check"
|
||||
|
||||
# Test RQLite HTTP endpoint
|
||||
test_endpoint "$node" "$port" "/rqlite/http/db/execute" "Node-${node} RQLite HTTP"
|
||||
|
||||
# Test IPFS API endpoint (may fail if IPFS not running, but at least connection should work)
|
||||
test_endpoint "$node" "$port" "/ipfs/api/v0/version" "Node-${node} IPFS API" || true
|
||||
|
||||
# Test Cluster API endpoint (may fail if Cluster not running, but at least connection should work)
|
||||
test_endpoint "$node" "$port" "/cluster/health" "Node-${node} Cluster API" || true
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo "=========================================="
|
||||
echo "Test Results"
|
||||
echo "=========================================="
|
||||
echo -e "${GREEN}Passed: $PASSED${NC}"
|
||||
echo -e "${RED}Failed: $FAILED${NC}"
|
||||
echo ""
|
||||
|
||||
if [ $FAILED -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ All tests passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Some tests failed (this is expected if services aren't running)${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user