mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 09:36:56 +00:00
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.
This commit is contained in:
parent
40600c3557
commit
106c2df4d2
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/dbcmd"
|
"github.com/DeBrosOfficial/network/pkg/cli/cmd/dbcmd"
|
||||||
deploycmd "github.com/DeBrosOfficial/network/pkg/cli/cmd/deploy"
|
deploycmd "github.com/DeBrosOfficial/network/pkg/cli/cmd/deploy"
|
||||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/envcmd"
|
"github.com/DeBrosOfficial/network/pkg/cli/cmd/envcmd"
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/cli/cmd/functioncmd"
|
||||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/inspectcmd"
|
"github.com/DeBrosOfficial/network/pkg/cli/cmd/inspectcmd"
|
||||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/monitorcmd"
|
"github.com/DeBrosOfficial/network/pkg/cli/cmd/monitorcmd"
|
||||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/namespacecmd"
|
"github.com/DeBrosOfficial/network/pkg/cli/cmd/namespacecmd"
|
||||||
@ -79,6 +80,9 @@ and interacting with the Orama distributed network.`,
|
|||||||
// Monitor command
|
// Monitor command
|
||||||
rootCmd.AddCommand(monitorcmd.Cmd)
|
rootCmd.AddCommand(monitorcmd.Cmd)
|
||||||
|
|
||||||
|
// Serverless function commands
|
||||||
|
rootCmd.AddCommand(functioncmd.Cmd)
|
||||||
|
|
||||||
return rootCmd
|
return rootCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
pkg/cli/cmd/functioncmd/function.go
Normal file
36
pkg/cli/cmd/functioncmd/function.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package functioncmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/cli/functions"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cmd is the top-level function command.
|
||||||
|
var Cmd = &cobra.Command{
|
||||||
|
Use: "function",
|
||||||
|
Short: "Manage serverless functions",
|
||||||
|
Long: `Deploy, invoke, and manage serverless functions on the Orama Network.
|
||||||
|
|
||||||
|
A function is a folder containing:
|
||||||
|
function.go — your handler code (uses the fn SDK)
|
||||||
|
function.yaml — configuration (name, memory, timeout, etc.)
|
||||||
|
|
||||||
|
Quick start:
|
||||||
|
orama function init my-function
|
||||||
|
cd my-function
|
||||||
|
orama function build
|
||||||
|
orama function deploy
|
||||||
|
orama function invoke my-function --data '{"name": "World"}'`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Cmd.AddCommand(functions.InitCmd)
|
||||||
|
Cmd.AddCommand(functions.BuildCmd)
|
||||||
|
Cmd.AddCommand(functions.DeployCmd)
|
||||||
|
Cmd.AddCommand(functions.InvokeCmd)
|
||||||
|
Cmd.AddCommand(functions.ListCmd)
|
||||||
|
Cmd.AddCommand(functions.GetCmd)
|
||||||
|
Cmd.AddCommand(functions.DeleteCmd)
|
||||||
|
Cmd.AddCommand(functions.LogsCmd)
|
||||||
|
Cmd.AddCommand(functions.VersionsCmd)
|
||||||
|
}
|
||||||
79
pkg/cli/functions/build.go
Normal file
79
pkg/cli/functions/build.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildCmd compiles a function to WASM using TinyGo.
|
||||||
|
var BuildCmd = &cobra.Command{
|
||||||
|
Use: "build [directory]",
|
||||||
|
Short: "Build a function to WASM using TinyGo",
|
||||||
|
Long: `Compiles function.go in the given directory (or current directory) to a WASM binary.
|
||||||
|
Requires TinyGo to be installed (https://tinygo.org/getting-started/install/).`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: runBuild,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBuild(cmd *cobra.Command, args []string) error {
|
||||||
|
dir := ""
|
||||||
|
if len(args) > 0 {
|
||||||
|
dir = args[0]
|
||||||
|
}
|
||||||
|
_, err := buildFunction(dir)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildFunction compiles the function in dir and returns the path to the WASM output.
|
||||||
|
func buildFunction(dir string) (string, error) {
|
||||||
|
absDir, err := ResolveFunctionDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify function.go exists
|
||||||
|
goFile := filepath.Join(absDir, "function.go")
|
||||||
|
if _, err := os.Stat(goFile); os.IsNotExist(err) {
|
||||||
|
return "", fmt.Errorf("function.go not found in %s", absDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify function.yaml exists
|
||||||
|
if _, err := os.Stat(filepath.Join(absDir, "function.yaml")); os.IsNotExist(err) {
|
||||||
|
return "", fmt.Errorf("function.yaml not found in %s", absDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check TinyGo is installed
|
||||||
|
tinygoPath, err := exec.LookPath("tinygo")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("tinygo not found in PATH. Install it: https://tinygo.org/getting-started/install/")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPath := filepath.Join(absDir, "function.wasm")
|
||||||
|
|
||||||
|
fmt.Printf("Building %s...\n", absDir)
|
||||||
|
|
||||||
|
// Run tinygo build
|
||||||
|
buildCmd := exec.Command(tinygoPath, "build", "-o", outputPath, "-target", "wasi", ".")
|
||||||
|
buildCmd.Dir = absDir
|
||||||
|
buildCmd.Stdout = os.Stdout
|
||||||
|
buildCmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := buildCmd.Run(); err != nil {
|
||||||
|
return "", fmt.Errorf("tinygo build failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate output
|
||||||
|
if err := ValidateWASMFile(outputPath); err != nil {
|
||||||
|
os.Remove(outputPath)
|
||||||
|
return "", fmt.Errorf("build produced invalid WASM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, _ := os.Stat(outputPath)
|
||||||
|
fmt.Printf("Built %s (%d bytes)\n", outputPath, info.Size())
|
||||||
|
|
||||||
|
return outputPath, nil
|
||||||
|
}
|
||||||
53
pkg/cli/functions/delete.go
Normal file
53
pkg/cli/functions/delete.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var deleteForce bool
|
||||||
|
|
||||||
|
// DeleteCmd deletes a deployed function.
|
||||||
|
var DeleteCmd = &cobra.Command{
|
||||||
|
Use: "delete <name>",
|
||||||
|
Short: "Delete a deployed function",
|
||||||
|
Long: "Deletes a function from the Orama Network. This action cannot be undone.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runDelete,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DeleteCmd.Flags().BoolVarP(&deleteForce, "force", "f", false, "Skip confirmation prompt")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDelete(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
if !deleteForce {
|
||||||
|
fmt.Printf("Are you sure you want to delete function %q? This cannot be undone. [y/N] ", name)
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
answer, _ := reader.ReadString('\n')
|
||||||
|
answer = strings.TrimSpace(strings.ToLower(answer))
|
||||||
|
if answer != "y" && answer != "yes" {
|
||||||
|
fmt.Println("Cancelled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := apiDelete("/v1/functions/" + name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg, ok := result["message"]; ok {
|
||||||
|
fmt.Println(msg)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Function %q deleted.\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
89
pkg/cli/functions/deploy.go
Normal file
89
pkg/cli/functions/deploy.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeployCmd deploys a function to the Orama Network.
|
||||||
|
var DeployCmd = &cobra.Command{
|
||||||
|
Use: "deploy [directory]",
|
||||||
|
Short: "Deploy a function to the Orama Network",
|
||||||
|
Long: `Deploys the function in the given directory (or current directory).
|
||||||
|
If no .wasm file exists, it will be built automatically using TinyGo.
|
||||||
|
Reads configuration from function.yaml.`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: runDeploy,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDeploy(cmd *cobra.Command, args []string) error {
|
||||||
|
dir := ""
|
||||||
|
if len(args) > 0 {
|
||||||
|
dir = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
absDir, err := ResolveFunctionDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := LoadConfig(absDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wasmPath := filepath.Join(absDir, "function.wasm")
|
||||||
|
|
||||||
|
// Auto-build if no WASM file exists
|
||||||
|
if _, err := os.Stat(wasmPath); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("No function.wasm found, building...\n\n")
|
||||||
|
built, err := buildFunction(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wasmPath = built
|
||||||
|
fmt.Println()
|
||||||
|
} else {
|
||||||
|
// Validate existing WASM
|
||||||
|
if err := ValidateWASMFile(wasmPath); err != nil {
|
||||||
|
return fmt.Errorf("existing function.wasm is invalid: %w\nRun 'orama function build' to rebuild", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Deploying function %q...\n", cfg.Name)
|
||||||
|
|
||||||
|
result, err := uploadWASMFunction(wasmPath, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nFunction deployed successfully!\n\n")
|
||||||
|
|
||||||
|
if msg, ok := result["message"]; ok {
|
||||||
|
fmt.Printf(" %s\n", msg)
|
||||||
|
}
|
||||||
|
if fn, ok := result["function"].(map[string]interface{}); ok {
|
||||||
|
if id, ok := fn["id"]; ok {
|
||||||
|
fmt.Printf(" ID: %s\n", id)
|
||||||
|
}
|
||||||
|
fmt.Printf(" Name: %s\n", cfg.Name)
|
||||||
|
if v, ok := fn["version"]; ok {
|
||||||
|
fmt.Printf(" Version: %v\n", v)
|
||||||
|
}
|
||||||
|
if wc, ok := fn["wasm_cid"]; ok {
|
||||||
|
fmt.Printf(" WASM CID: %s\n", wc)
|
||||||
|
}
|
||||||
|
if st, ok := fn["status"]; ok {
|
||||||
|
fmt.Printf(" Status: %s\n", st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nInvoke with:\n")
|
||||||
|
fmt.Printf(" orama function invoke %s --data '{\"name\": \"World\"}'\n", cfg.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
35
pkg/cli/functions/get.go
Normal file
35
pkg/cli/functions/get.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCmd shows details of a deployed function.
|
||||||
|
var GetCmd = &cobra.Command{
|
||||||
|
Use: "get <name>",
|
||||||
|
Short: "Get details of a deployed function",
|
||||||
|
Long: "Retrieves and displays detailed information about a specific function.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runGet,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGet(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
result, err := apiGet("/v1/functions/" + name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pretty-print the result
|
||||||
|
data, err := json.MarshalIndent(result, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to format response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
260
pkg/cli/functions/helpers.go
Normal file
260
pkg/cli/functions/helpers.go
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
84
pkg/cli/functions/init.go
Normal file
84
pkg/cli/functions/init.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitCmd scaffolds a new function project.
|
||||||
|
var InitCmd = &cobra.Command{
|
||||||
|
Use: "init <name>",
|
||||||
|
Short: "Create a new serverless function project",
|
||||||
|
Long: "Scaffolds a new directory with function.go and function.yaml templates.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runInit,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInit(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
if !validNameRegex.MatchString(name) {
|
||||||
|
return fmt.Errorf("invalid function name %q: must start with a letter and contain only letters, digits, hyphens, or underscores", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Join(".", name)
|
||||||
|
if _, err := os.Stat(dir); err == nil {
|
||||||
|
return fmt.Errorf("directory %q already exists", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write function.yaml
|
||||||
|
yamlContent := fmt.Sprintf(`name: %s
|
||||||
|
public: false
|
||||||
|
memory: 64
|
||||||
|
timeout: 30
|
||||||
|
retry:
|
||||||
|
count: 0
|
||||||
|
delay: 5
|
||||||
|
`, name)
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "function.yaml"), []byte(yamlContent), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write function.yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write function.go
|
||||||
|
goContent := fmt.Sprintf(`package main
|
||||||
|
|
||||||
|
import "github.com/DeBrosOfficial/network/sdk/fn"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fn.Run(func(input []byte) ([]byte, error) {
|
||||||
|
var req struct {
|
||||||
|
Name string `+"`"+`json:"name"`+"`"+`
|
||||||
|
}
|
||||||
|
fn.ParseJSON(input, &req)
|
||||||
|
if req.Name == "" {
|
||||||
|
req.Name = "World"
|
||||||
|
}
|
||||||
|
return fn.JSON(map[string]string{
|
||||||
|
"greeting": "Hello, " + req.Name + "!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "function.go"), []byte(goContent), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write function.go: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Created function project: %s/\n", name)
|
||||||
|
fmt.Printf(" %s/function.yaml — configuration\n", name)
|
||||||
|
fmt.Printf(" %s/function.go — handler code\n\n", name)
|
||||||
|
fmt.Printf("Next steps:\n")
|
||||||
|
fmt.Printf(" cd %s\n", name)
|
||||||
|
fmt.Printf(" orama function build\n")
|
||||||
|
fmt.Printf(" orama function deploy\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
58
pkg/cli/functions/invoke.go
Normal file
58
pkg/cli/functions/invoke.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var invokeData string
|
||||||
|
|
||||||
|
// InvokeCmd invokes a deployed function.
|
||||||
|
var InvokeCmd = &cobra.Command{
|
||||||
|
Use: "invoke <name>",
|
||||||
|
Short: "Invoke a deployed function",
|
||||||
|
Long: "Sends a request to invoke the named function with optional JSON payload.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runInvoke,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
InvokeCmd.Flags().StringVar(&invokeData, "data", "{}", "JSON payload to send to the function")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInvoke(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
fmt.Printf("Invoking function %q...\n\n", name)
|
||||||
|
|
||||||
|
resp, err := apiRequest("POST", "/v1/functions/"+name+"/invoke", bytes.NewBufferString(invokeData), "application/json")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print timing info from headers
|
||||||
|
if reqID := resp.Header.Get("X-Request-ID"); reqID != "" {
|
||||||
|
fmt.Printf("Request ID: %s\n", reqID)
|
||||||
|
}
|
||||||
|
if dur := resp.Header.Get("X-Duration-Ms"); dur != "" {
|
||||||
|
fmt.Printf("Duration: %s ms\n", dur)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("invocation failed (%d): %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nOutput:\n%s\n", string(respBody))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
80
pkg/cli/functions/list.go
Normal file
80
pkg/cli/functions/list.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListCmd lists all deployed functions.
|
||||||
|
var ListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List deployed functions",
|
||||||
|
Long: "Lists all functions deployed in the current namespace.",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: runList,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runList(cmd *cobra.Command, args []string) error {
|
||||||
|
result, err := apiGet("/v1/functions")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
functions, ok := result["functions"].([]interface{})
|
||||||
|
if !ok || len(functions) == 0 {
|
||||||
|
fmt.Println("No functions deployed.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(w, "NAME\tVERSION\tSTATUS\tMEMORY\tTIMEOUT\tPUBLIC")
|
||||||
|
fmt.Fprintln(w, "----\t-------\t------\t------\t-------\t------")
|
||||||
|
|
||||||
|
for _, f := range functions {
|
||||||
|
fn, ok := f.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := valStr(fn, "name")
|
||||||
|
version := valNum(fn, "version")
|
||||||
|
status := valStr(fn, "status")
|
||||||
|
memory := valNum(fn, "memory_limit_mb")
|
||||||
|
timeout := valNum(fn, "timeout_seconds")
|
||||||
|
public := valBool(fn, "is_public")
|
||||||
|
|
||||||
|
publicStr := "no"
|
||||||
|
if public {
|
||||||
|
publicStr = "yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s\t%d\t%s\t%dMB\t%ds\t%s\n", name, version, status, memory, timeout, publicStr)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
fmt.Printf("\nTotal: %d function(s)\n", len(functions))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func valStr(m map[string]interface{}, key string) string {
|
||||||
|
if v, ok := m[key]; ok {
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func valNum(m map[string]interface{}, key string) int {
|
||||||
|
if v, ok := m[key].(float64); ok {
|
||||||
|
return int(v)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func valBool(m map[string]interface{}, key string) bool {
|
||||||
|
if v, ok := m[key].(bool); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
57
pkg/cli/functions/logs.go
Normal file
57
pkg/cli/functions/logs.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logsLimit int
|
||||||
|
|
||||||
|
// LogsCmd retrieves function execution logs.
|
||||||
|
var LogsCmd = &cobra.Command{
|
||||||
|
Use: "logs <name>",
|
||||||
|
Short: "Get execution logs for a function",
|
||||||
|
Long: "Retrieves the most recent execution logs for a deployed function.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runLogs,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
LogsCmd.Flags().IntVar(&logsLimit, "limit", 50, "Maximum number of log entries to retrieve")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogs(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
endpoint := "/v1/functions/" + name + "/logs"
|
||||||
|
if logsLimit > 0 {
|
||||||
|
endpoint += "?limit=" + strconv.Itoa(logsLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := apiGet(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, ok := result["logs"].([]interface{})
|
||||||
|
if !ok || len(logs) == 0 {
|
||||||
|
fmt.Printf("No logs found for function %q.\n", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range logs {
|
||||||
|
log, ok := entry.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ts := valStr(log, "timestamp")
|
||||||
|
level := valStr(log, "level")
|
||||||
|
msg := valStr(log, "message")
|
||||||
|
fmt.Printf("[%s] %s: %s\n", ts, level, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nShowing %d log(s)\n", len(logs))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
54
pkg/cli/functions/versions.go
Normal file
54
pkg/cli/functions/versions.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VersionsCmd lists all versions of a function.
|
||||||
|
var VersionsCmd = &cobra.Command{
|
||||||
|
Use: "versions <name>",
|
||||||
|
Short: "List all versions of a function",
|
||||||
|
Long: "Shows all deployed versions of a specific function.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runVersions,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVersions(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
result, err := apiGet("/v1/functions/" + name + "/versions")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
versions, ok := result["versions"].([]interface{})
|
||||||
|
if !ok || len(versions) == 0 {
|
||||||
|
fmt.Printf("No versions found for function %q.\n", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(w, "VERSION\tWASM CID\tSTATUS\tCREATED")
|
||||||
|
fmt.Fprintln(w, "-------\t--------\t------\t-------")
|
||||||
|
|
||||||
|
for _, v := range versions {
|
||||||
|
ver, ok := v.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
version := valNum(ver, "version")
|
||||||
|
wasmCID := valStr(ver, "wasm_cid")
|
||||||
|
status := valStr(ver, "status")
|
||||||
|
created := valStr(ver, "created_at")
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", version, wasmCID, status, created)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
fmt.Printf("\nTotal: %d version(s)\n", len(versions))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
66
sdk/fn/fn.go
Normal file
66
sdk/fn/fn.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Package fn provides a tiny, TinyGo-compatible SDK for writing Orama serverless functions.
|
||||||
|
//
|
||||||
|
// A function is a Go program that reads JSON input from stdin and writes JSON output to stdout.
|
||||||
|
// This package handles the boilerplate so you only write your handler logic.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// package main
|
||||||
|
//
|
||||||
|
// import "github.com/DeBrosOfficial/network/sdk/fn"
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// fn.Run(func(input []byte) ([]byte, error) {
|
||||||
|
// var req struct{ Name string `json:"name"` }
|
||||||
|
// fn.ParseJSON(input, &req)
|
||||||
|
// if req.Name == "" { req.Name = "World" }
|
||||||
|
// return fn.JSON(map[string]string{"greeting": "Hello, " + req.Name + "!"})
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
package fn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandlerFunc is the signature for a serverless function handler.
|
||||||
|
// It receives the raw JSON input bytes and returns raw JSON output bytes.
|
||||||
|
type HandlerFunc func(input []byte) (output []byte, err error)
|
||||||
|
|
||||||
|
// Run reads input from stdin, calls the handler, and writes the output to stdout.
|
||||||
|
// If the handler returns an error, it writes a JSON error response to stdout and exits with code 1.
|
||||||
|
func Run(handler HandlerFunc) {
|
||||||
|
input, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
writeError(fmt.Sprintf("failed to read input: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := handler(input)
|
||||||
|
if err != nil {
|
||||||
|
writeError(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if output != nil {
|
||||||
|
os.Stdout.Write(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON marshals a value to JSON bytes. Convenience wrapper around json.Marshal.
|
||||||
|
func JSON(v interface{}) ([]byte, error) {
|
||||||
|
return json.Marshal(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJSON unmarshals JSON bytes into a value. Convenience wrapper around json.Unmarshal.
|
||||||
|
func ParseJSON(data []byte, v interface{}) error {
|
||||||
|
return json.Unmarshal(data, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(msg string) {
|
||||||
|
resp, _ := json.Marshal(map[string]string{"error": msg})
|
||||||
|
os.Stdout.Write(resp)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user