mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 03:13:00 +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"
|
||||
deploycmd "github.com/DeBrosOfficial/network/pkg/cli/cmd/deploy"
|
||||
"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/monitorcmd"
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/namespacecmd"
|
||||
@ -79,6 +80,9 @@ and interacting with the Orama distributed network.`,
|
||||
// Monitor command
|
||||
rootCmd.AddCommand(monitorcmd.Cmd)
|
||||
|
||||
// Serverless function commands
|
||||
rootCmd.AddCommand(functioncmd.Cmd)
|
||||
|
||||
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