orama/pkg/cli/functions/helpers.go
anonpenguin23 106c2df4d2 feat: implement wallet-based SSH authentication using Ed25519 keys
- Added documentation for wallet-based SSH authentication in WALLET_SSH_AUTH.md.
- Introduced SSH key derivation and management in rootwallet core and CLI.
- Created commands for generating, loading, and unloading SSH keys in the CLI.
- Updated Orama network to support SSH key authentication.
- Added migration steps for nodes to transition from password-based to key-based authentication.

feat: add serverless function management commands

- Implemented function command structure in CLI for managing serverless functions.
- Added commands for initializing, building, deploying, invoking, deleting, and listing functions.
- Created helper functions for handling function configuration and API requests.
- Integrated TinyGo for building functions to WASM.
- Added logging and version management for deployed functions.
2026-02-19 10:51:03 +02:00

261 lines
7.2 KiB
Go

package functions
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"github.com/DeBrosOfficial/network/pkg/cli/shared"
"gopkg.in/yaml.v3"
)
// FunctionConfig represents the function.yaml configuration.
type FunctionConfig struct {
Name string `yaml:"name"`
Public bool `yaml:"public"`
Memory int `yaml:"memory"`
Timeout int `yaml:"timeout"`
Retry RetryConfig `yaml:"retry"`
Env map[string]string `yaml:"env"`
}
// RetryConfig holds retry settings.
type RetryConfig struct {
Count int `yaml:"count"`
Delay int `yaml:"delay"`
}
// wasmMagicBytes is the WASM binary magic number: \0asm
var wasmMagicBytes = []byte{0x00, 0x61, 0x73, 0x6d}
// validNameRegex validates function names (alphanumeric, hyphens, underscores).
var validNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*$`)
// LoadConfig reads and parses a function.yaml from the given directory.
func LoadConfig(dir string) (*FunctionConfig, error) {
path := filepath.Join(dir, "function.yaml")
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read function.yaml: %w", err)
}
var cfg FunctionConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse function.yaml: %w", err)
}
// Apply defaults
if cfg.Memory == 0 {
cfg.Memory = 64
}
if cfg.Timeout == 0 {
cfg.Timeout = 30
}
if cfg.Retry.Delay == 0 {
cfg.Retry.Delay = 5
}
// Validate
if cfg.Name == "" {
return nil, fmt.Errorf("function.yaml: 'name' is required")
}
if !validNameRegex.MatchString(cfg.Name) {
return nil, fmt.Errorf("function.yaml: 'name' must start with a letter and contain only letters, digits, hyphens, or underscores")
}
if cfg.Memory < 1 || cfg.Memory > 256 {
return nil, fmt.Errorf("function.yaml: 'memory' must be between 1 and 256 MB (got %d)", cfg.Memory)
}
if cfg.Timeout < 1 || cfg.Timeout > 300 {
return nil, fmt.Errorf("function.yaml: 'timeout' must be between 1 and 300 seconds (got %d)", cfg.Timeout)
}
return &cfg, nil
}
// ValidateWASM checks that the given bytes are a valid WASM binary (magic number check).
func ValidateWASM(data []byte) error {
if len(data) < 8 {
return fmt.Errorf("file too small to be a valid WASM binary (%d bytes)", len(data))
}
if !bytes.HasPrefix(data, wasmMagicBytes) {
return fmt.Errorf("file is not a valid WASM binary (bad magic bytes)")
}
return nil
}
// ValidateWASMFile checks that the file at the given path is a valid WASM binary.
func ValidateWASMFile(path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to open WASM file: %w", err)
}
defer f.Close()
header := make([]byte, 8)
n, err := f.Read(header)
if err != nil {
return fmt.Errorf("failed to read WASM file: %w", err)
}
return ValidateWASM(header[:n])
}
// apiRequest performs an authenticated HTTP request to the gateway API.
func apiRequest(method, endpoint string, body io.Reader, contentType string) (*http.Response, error) {
apiURL := shared.GetAPIURL()
url := apiURL + endpoint
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
token, err := shared.GetAuthToken()
if err != nil {
return nil, fmt.Errorf("authentication required: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
return http.DefaultClient.Do(req)
}
// apiGet performs an authenticated GET request and returns the parsed JSON response.
func apiGet(endpoint string) (map[string]interface{}, error) {
resp, err := apiRequest("GET", endpoint, nil, "")
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return result, nil
}
// apiDelete performs an authenticated DELETE request and returns the parsed JSON response.
func apiDelete(endpoint string) (map[string]interface{}, error) {
resp, err := apiRequest("DELETE", endpoint, nil, "")
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return result, nil
}
// uploadWASMFunction uploads a WASM file to the deploy endpoint via multipart/form-data.
func uploadWASMFunction(wasmPath string, cfg *FunctionConfig) (map[string]interface{}, error) {
wasmFile, err := os.Open(wasmPath)
if err != nil {
return nil, fmt.Errorf("failed to open WASM file: %w", err)
}
defer wasmFile.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Add form fields
writer.WriteField("name", cfg.Name)
writer.WriteField("is_public", strconv.FormatBool(cfg.Public))
writer.WriteField("memory_limit_mb", strconv.Itoa(cfg.Memory))
writer.WriteField("timeout_seconds", strconv.Itoa(cfg.Timeout))
writer.WriteField("retry_count", strconv.Itoa(cfg.Retry.Count))
writer.WriteField("retry_delay_seconds", strconv.Itoa(cfg.Retry.Delay))
// Add env vars as metadata JSON
if len(cfg.Env) > 0 {
metadata, _ := json.Marshal(map[string]interface{}{
"env_vars": cfg.Env,
})
writer.WriteField("metadata", string(metadata))
}
// Add WASM file
part, err := writer.CreateFormFile("wasm", filepath.Base(wasmPath))
if err != nil {
return nil, fmt.Errorf("failed to create form file: %w", err)
}
if _, err := io.Copy(part, wasmFile); err != nil {
return nil, fmt.Errorf("failed to write WASM data: %w", err)
}
writer.Close()
resp, err := apiRequest("POST", "/v1/functions", body, writer.FormDataContentType())
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("deploy failed (%d): %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return result, nil
}
// ResolveFunctionDir resolves and validates a function directory.
// If dir is empty, uses the current working directory.
func ResolveFunctionDir(dir string) (string, error) {
if dir == "" {
dir = "."
}
absDir, err := filepath.Abs(dir)
if err != nil {
return "", fmt.Errorf("failed to resolve path: %w", err)
}
info, err := os.Stat(absDir)
if err != nil {
return "", fmt.Errorf("directory does not exist: %w", err)
}
if !info.IsDir() {
return "", fmt.Errorf("%s is not a directory", absDir)
}
return absDir, nil
}