mirror of
https://github.com/harivansh-afk/betterNAS.git
synced 2026-04-15 14:03:48 +00:00
Keep the NAS-side runtime bounded to the configured export path, make WebDAV and registration behavior env-driven, and add runtime coverage so the first storage loop can be verified locally. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
357 lines
8.6 KiB
Go
357 lines
8.6 KiB
Go
package nodeagent
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultPort = "8090"
|
|
defaultAgentVersion = "0.1.0-dev"
|
|
defaultHeartbeatInterval = 30 * time.Second
|
|
defaultListenHost = "127.0.0.1"
|
|
exportPathEnvKey = "BETTERNAS_EXPORT_PATH"
|
|
listenAddressEnvKey = "BETTERNAS_NODE_LISTEN_ADDRESS"
|
|
)
|
|
|
|
type Config struct {
|
|
Port string
|
|
ListenAddress string
|
|
ExportPath string
|
|
MachineID string
|
|
DisplayName string
|
|
AgentVersion string
|
|
DirectAddress string
|
|
RelayAddress string
|
|
ExportLabel string
|
|
ExportTags []string
|
|
ControlPlaneURL string
|
|
ControlPlaneToken string
|
|
RegisterEnabled bool
|
|
HeartbeatEnabled bool
|
|
HeartbeatInterval time.Duration
|
|
}
|
|
|
|
type envLookup func(string) (string, bool)
|
|
|
|
func LoadConfigFromEnv() (Config, error) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("get working directory: %w", err)
|
|
}
|
|
|
|
hostname, err := os.Hostname()
|
|
if err != nil || hostname == "" {
|
|
hostname = "betternas-node"
|
|
}
|
|
|
|
return loadConfig(os.LookupEnv, cwd, hostname)
|
|
}
|
|
|
|
func loadConfig(lookup envLookup, cwd, hostname string) (Config, error) {
|
|
port := envOrDefault(lookup, "PORT", defaultPort)
|
|
|
|
rawExportPath, err := envRequired(lookup, exportPathEnvKey)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
exportPath, err := resolveExportPath(rawExportPath, cwd)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
listenAddress := envOrDefault(lookup, listenAddressEnvKey, defaultListenAddress(port))
|
|
|
|
registerEnabled, err := envBool(lookup, "BETTERNAS_NODE_REGISTER_ENABLED", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
heartbeatEnabled, err := envBool(lookup, "BETTERNAS_NODE_HEARTBEAT_ENABLED", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
heartbeatInterval, err := envDuration(lookup, "BETTERNAS_NODE_HEARTBEAT_INTERVAL", defaultHeartbeatInterval)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
machineID, machineIDProvided := envOptional(lookup, "BETTERNAS_NODE_MACHINE_ID")
|
|
if !machineIDProvided {
|
|
machineID = hostname
|
|
}
|
|
|
|
if registerEnabled && !machineIDProvided {
|
|
return Config{}, fmt.Errorf("BETTERNAS_NODE_MACHINE_ID is required when BETTERNAS_NODE_REGISTER_ENABLED=true")
|
|
}
|
|
|
|
displayName := envOrDefault(lookup, "BETTERNAS_NODE_DISPLAY_NAME", machineID)
|
|
agentVersion := envOrDefault(lookup, "BETTERNAS_VERSION", defaultAgentVersion)
|
|
directAddress := envOrDefault(lookup, "BETTERNAS_NODE_DIRECT_ADDRESS", defaultDirectAddress(listenAddress, port))
|
|
relayAddress := envOrDefault(lookup, "BETTERNAS_NODE_RELAY_ADDRESS", "")
|
|
exportLabel := envOrDefault(lookup, "BETTERNAS_EXPORT_LABEL", defaultExportLabel(exportPath))
|
|
exportTags := parseCSVList(envOrDefault(lookup, "BETTERNAS_EXPORT_TAGS", ""))
|
|
controlPlaneURL := strings.TrimRight(envOrDefault(lookup, "BETTERNAS_CONTROL_PLANE_URL", ""), "/")
|
|
controlPlaneToken := envOrDefault(lookup, "BETTERNAS_CONTROL_PLANE_AUTH_TOKEN", "")
|
|
|
|
cfg := Config{
|
|
Port: port,
|
|
ListenAddress: listenAddress,
|
|
ExportPath: exportPath,
|
|
MachineID: machineID,
|
|
DisplayName: displayName,
|
|
AgentVersion: agentVersion,
|
|
DirectAddress: directAddress,
|
|
RelayAddress: relayAddress,
|
|
ExportLabel: exportLabel,
|
|
ExportTags: exportTags,
|
|
ControlPlaneURL: controlPlaneURL,
|
|
ControlPlaneToken: controlPlaneToken,
|
|
RegisterEnabled: registerEnabled,
|
|
HeartbeatEnabled: heartbeatEnabled,
|
|
HeartbeatInterval: heartbeatInterval,
|
|
}
|
|
|
|
if err := validateRuntimeConfig(cfg); err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func resolveExportPath(rawPath, cwd string) (string, error) {
|
|
exportPath := strings.TrimSpace(rawPath)
|
|
if exportPath == "" {
|
|
return "", fmt.Errorf("export path is required")
|
|
}
|
|
|
|
if !filepath.IsAbs(exportPath) {
|
|
basePath := cwd
|
|
if workspaceRoot, ok := findWorkspaceRoot(cwd); ok {
|
|
basePath = workspaceRoot
|
|
}
|
|
|
|
exportPath = filepath.Join(basePath, exportPath)
|
|
}
|
|
|
|
absolutePath, err := filepath.Abs(exportPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve export path %q: %w", rawPath, err)
|
|
}
|
|
|
|
return filepath.Clean(absolutePath), nil
|
|
}
|
|
|
|
func envRequired(lookup envLookup, key string) (string, error) {
|
|
value, ok := lookup(key)
|
|
if !ok || strings.TrimSpace(value) == "" {
|
|
return "", fmt.Errorf("%s is required", key)
|
|
}
|
|
|
|
return strings.TrimSpace(value), nil
|
|
}
|
|
|
|
func envOptional(lookup envLookup, key string) (string, bool) {
|
|
value, ok := lookup(key)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
return "", false
|
|
}
|
|
|
|
return trimmed, true
|
|
}
|
|
|
|
func defaultListenAddress(port string) string {
|
|
return net.JoinHostPort(defaultListenHost, port)
|
|
}
|
|
|
|
func defaultDirectAddress(listenAddress, fallbackPort string) string {
|
|
if strings.TrimSpace(listenAddress) == defaultListenAddress(fallbackPort) {
|
|
return httpURL("localhost", fallbackPort)
|
|
}
|
|
|
|
host, port, err := net.SplitHostPort(strings.TrimSpace(listenAddress))
|
|
if err != nil || strings.TrimSpace(port) == "" {
|
|
return ""
|
|
}
|
|
|
|
host = strings.TrimSpace(host)
|
|
if isWildcardListenHost(host) {
|
|
return ""
|
|
}
|
|
|
|
return httpURL(host, port)
|
|
}
|
|
|
|
func isWildcardListenHost(host string) bool {
|
|
trimmed := strings.TrimSpace(host)
|
|
if trimmed == "" {
|
|
return true
|
|
}
|
|
|
|
ip := net.ParseIP(trimmed)
|
|
return ip != nil && ip.IsUnspecified()
|
|
}
|
|
|
|
func httpURL(host, port string) string {
|
|
return (&url.URL{
|
|
Scheme: "http",
|
|
Host: net.JoinHostPort(host, port),
|
|
}).String()
|
|
}
|
|
|
|
func findWorkspaceRoot(start string) (string, bool) {
|
|
current := filepath.Clean(start)
|
|
|
|
for {
|
|
if hasPath(filepath.Join(current, "pnpm-workspace.yaml")) || hasPath(filepath.Join(current, "go.work")) || hasPath(filepath.Join(current, ".git")) {
|
|
return current, true
|
|
}
|
|
|
|
parent := filepath.Dir(current)
|
|
if parent == current {
|
|
return "", false
|
|
}
|
|
|
|
current = parent
|
|
}
|
|
}
|
|
|
|
func hasPath(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
func defaultExportLabel(exportPath string) string {
|
|
label := filepath.Base(exportPath)
|
|
if label == "." || label == string(filepath.Separator) || label == "" {
|
|
return "export"
|
|
}
|
|
|
|
return label
|
|
}
|
|
|
|
func parseCSVList(raw string) []string {
|
|
if strings.TrimSpace(raw) == "" {
|
|
return []string{}
|
|
}
|
|
|
|
values := make([]string, 0)
|
|
seen := make(map[string]struct{})
|
|
|
|
for _, part := range strings.Split(raw, ",") {
|
|
value := strings.TrimSpace(part)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
|
|
seen[value] = struct{}{}
|
|
values = append(values, value)
|
|
}
|
|
|
|
return values
|
|
}
|
|
|
|
func envOrDefault(lookup envLookup, key, fallback string) string {
|
|
value, ok := lookup(key)
|
|
if !ok {
|
|
return fallback
|
|
}
|
|
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
return fallback
|
|
}
|
|
|
|
return trimmed
|
|
}
|
|
|
|
func validateRuntimeConfig(cfg Config) error {
|
|
if err := validateListenAddress(cfg.ListenAddress); err != nil {
|
|
return err
|
|
}
|
|
|
|
if cfg.RegisterEnabled && strings.TrimSpace(cfg.ControlPlaneURL) == "" {
|
|
return fmt.Errorf("BETTERNAS_CONTROL_PLANE_URL is required when BETTERNAS_NODE_REGISTER_ENABLED=true")
|
|
}
|
|
|
|
if cfg.RegisterEnabled && strings.TrimSpace(cfg.MachineID) == "" {
|
|
return fmt.Errorf("BETTERNAS_NODE_MACHINE_ID is required when BETTERNAS_NODE_REGISTER_ENABLED=true")
|
|
}
|
|
|
|
if cfg.HeartbeatEnabled && !cfg.RegisterEnabled {
|
|
return fmt.Errorf("BETTERNAS_NODE_HEARTBEAT_ENABLED requires BETTERNAS_NODE_REGISTER_ENABLED=true")
|
|
}
|
|
|
|
if cfg.HeartbeatEnabled && cfg.HeartbeatInterval <= 0 {
|
|
return fmt.Errorf("BETTERNAS_NODE_HEARTBEAT_INTERVAL must be greater than zero")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateListenAddress(address string) error {
|
|
trimmed := strings.TrimSpace(address)
|
|
if trimmed == "" {
|
|
return fmt.Errorf("%s is required", listenAddressEnvKey)
|
|
}
|
|
|
|
_, port, err := net.SplitHostPort(trimmed)
|
|
if err != nil {
|
|
return fmt.Errorf("parse %s: %w", listenAddressEnvKey, err)
|
|
}
|
|
|
|
if strings.TrimSpace(port) == "" {
|
|
return fmt.Errorf("%s must include a port", listenAddressEnvKey)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func envBool(lookup envLookup, key string, fallback bool) (bool, error) {
|
|
value, ok := lookup(key)
|
|
if !ok || strings.TrimSpace(value) == "" {
|
|
return fallback, nil
|
|
}
|
|
|
|
parsed, err := strconv.ParseBool(strings.TrimSpace(value))
|
|
if err != nil {
|
|
return false, fmt.Errorf("parse %s: %w", key, err)
|
|
}
|
|
|
|
return parsed, nil
|
|
}
|
|
|
|
func envDuration(lookup envLookup, key string, fallback time.Duration) (time.Duration, error) {
|
|
value, ok := lookup(key)
|
|
if !ok || strings.TrimSpace(value) == "" {
|
|
return fallback, nil
|
|
}
|
|
|
|
parsed, err := time.ParseDuration(strings.TrimSpace(value))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("parse %s: %w", key, err)
|
|
}
|
|
|
|
if parsed <= 0 {
|
|
return 0, fmt.Errorf("%s must be greater than zero", key)
|
|
}
|
|
|
|
return parsed, nil
|
|
}
|