317 lines
19 KiB
Go
317 lines
19 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const MaxMeshProductionObservationSinkCapacity = 10000
|
|
|
|
type Config struct {
|
|
ClusterID string
|
|
ClusterAuthorityPublicKey string
|
|
ClusterAuthorityFingerprint string
|
|
JoinToken string
|
|
NodeName string
|
|
StateDir string
|
|
WorkloadSupervisionEnabled bool
|
|
WebIngressRuntimeEnabled bool
|
|
WebIngressSigningPrivateKey string
|
|
WebIngressSigningKeyID string
|
|
WebIngressTrustedKeysJSON string
|
|
WebIngressRuntimeServiceClasses string
|
|
HeartbeatInterval time.Duration
|
|
EnrollmentPollInterval time.Duration
|
|
EnrollmentPollTimeout time.Duration
|
|
FabricRuntimeEnabled bool
|
|
MeshProductionForwardingEnabled bool
|
|
VPNFabricSessionTransportEnabled bool
|
|
MeshQUICFabricEnabled bool
|
|
MeshQUICFabricListenAddr string
|
|
VPNFabricSessionStreamShards int
|
|
VPNFabricQUICMaxStreamsPerConn int
|
|
VPNFabricQUICIdleTTL time.Duration
|
|
MeshProductionObservationSinkCapacity int
|
|
FabricListenAddr string
|
|
FabricListenPortMode string
|
|
FabricListenAutoPortStart int
|
|
FabricListenAutoPortEnd int
|
|
MeshAdvertiseEndpoint string
|
|
MeshAdvertiseEndpointsJSON string
|
|
FabricRegistryRecordsJSON string
|
|
MeshAdvertiseTransport string
|
|
MeshConnectivityMode string
|
|
MeshNATType string
|
|
MeshSiteID string
|
|
MeshLocalityGroupID string
|
|
MeshNATGroupID string
|
|
MeshSTUNReflexiveEndpoint string
|
|
MeshSTUNServer string
|
|
MeshRelayNodeID string
|
|
MeshRelayEndpoint string
|
|
MeshRegion string
|
|
MeshSyntheticConfigPath string
|
|
MeshPeerEndpointsJSON string
|
|
MeshSyntheticRoutesJSON string
|
|
RemoteWorkspaceRealAdapterEnabled bool
|
|
RemoteWorkspaceRealAdapterCommand string
|
|
RemoteWorkspaceRealAdapterArgsJSON string
|
|
RemoteWorkspaceRealAdapterWorkDir string
|
|
}
|
|
|
|
func Load(args []string, env map[string]string) (Config, error) {
|
|
if env == nil {
|
|
env = readEnv()
|
|
}
|
|
defaultStateDir := filepath.Join(".", ".rap-node-agent")
|
|
fs := flag.NewFlagSet("rap-node-agent", flag.ContinueOnError)
|
|
cfg := Config{}
|
|
fs.StringVar(&cfg.ClusterID, "cluster-id", getEnv(env, "RAP_CLUSTER_ID", ""), "Cluster ID.")
|
|
fs.StringVar(&cfg.ClusterAuthorityPublicKey, "cluster-authority-public-key", getEnv(env, "RAP_CLUSTER_AUTHORITY_PUBLIC_KEY", ""), "Pinned cluster authority Ed25519 public key.")
|
|
fs.StringVar(&cfg.ClusterAuthorityFingerprint, "cluster-authority-fingerprint", getEnv(env, "RAP_CLUSTER_AUTHORITY_FINGERPRINT", ""), "Pinned cluster authority key fingerprint.")
|
|
fs.StringVar(&cfg.JoinToken, "join-token", getEnv(env, "RAP_JOIN_TOKEN", ""), "Short-lived node join token.")
|
|
fs.StringVar(&cfg.NodeName, "node-name", getEnv(env, "RAP_NODE_NAME", hostnameOrDefault()), "Node display name.")
|
|
fs.StringVar(&cfg.StateDir, "state-dir", getEnv(env, "RAP_NODE_STATE_DIR", defaultStateDir), "Local node-agent state directory.")
|
|
fs.BoolVar(&cfg.WorkloadSupervisionEnabled, "workload-supervision-enabled", getEnvBool(env, "RAP_WORKLOAD_SUPERVISION_ENABLED", false), "Enable desired workload polling and status reporting. Disabled by default while service runtime is not implemented.")
|
|
fs.BoolVar(&cfg.WebIngressRuntimeEnabled, "web-ingress-runtime-enabled", getEnvBool(env, "RAP_WEB_INGRESS_RUNTIME_ENABLED", false), "Enable the future real 80/443 web ingress listener runtime. Disabled by default; contract probe remains safe without it.")
|
|
fs.StringVar(&cfg.WebIngressSigningPrivateKey, "web-ingress-signing-private-key", getEnv(env, "RAP_WEB_INGRESS_SIGNING_PRIVATE_KEY", ""), "Base64 Ed25519 private key used to sign web ingress fabric envelopes. Empty keeps signing disabled.")
|
|
fs.StringVar(&cfg.WebIngressSigningKeyID, "web-ingress-signing-key-id", getEnv(env, "RAP_WEB_INGRESS_SIGNING_KEY_ID", ""), "Optional key id for web ingress envelope signatures.")
|
|
fs.StringVar(&cfg.WebIngressTrustedKeysJSON, "web-ingress-trusted-keys-json", getEnv(env, "RAP_WEB_INGRESS_TRUSTED_KEYS_JSON", ""), "JSON map or array of trusted Ed25519 public keys for web ingress runtime receiver.")
|
|
fs.StringVar(&cfg.WebIngressRuntimeServiceClasses, "web-ingress-runtime-service-classes", getEnv(env, "RAP_WEB_INGRESS_RUNTIME_SERVICE_CLASSES", ""), "Optional comma-separated allow-list of web ingress runtime service classes accepted by this node.")
|
|
fs.BoolVar(&cfg.FabricRuntimeEnabled, "fabric-runtime-enabled", getEnvBool(env, "RAP_FABRIC_RUNTIME_ENABLED", false), "Enable C17A synthetic fabric probe runtime. Disabled by default.")
|
|
fs.BoolVar(&cfg.MeshProductionForwardingEnabled, "mesh-production-forwarding-enabled", getEnvBool(env, "RAP_MESH_PRODUCTION_FORWARDING_ENABLED", false), "Enable production fabric-control direct next-hop forwarding gate. Disabled by default.")
|
|
fs.BoolVar(&cfg.VPNFabricSessionTransportEnabled, "vpn-fabric-session-transport-enabled", getEnvBool(env, "RAP_VPN_FABRIC_SESSION_TRANSPORT_ENABLED", false), "Route VPN packet transport over persistent fabric session when explicitly enabled. Disabled by default.")
|
|
fs.BoolVar(&cfg.MeshQUICFabricEnabled, "mesh-quic-fabric-enabled", getEnvBool(env, "RAP_MESH_QUIC_FABRIC_ENABLED", false), "Enable QUIC/UDP fabric listener. Disabled by default.")
|
|
fs.StringVar(&cfg.MeshQUICFabricListenAddr, "mesh-quic-fabric-listen-addr", getEnv(env, "RAP_MESH_QUIC_FABRIC_LISTEN_ADDR", ""), "Listen address for QUIC/UDP fabric endpoint, for example :19443.")
|
|
fs.IntVar(&cfg.VPNFabricSessionStreamShards, "vpn-fabric-session-stream-shards", getEnvInt(env, "RAP_VPN_FABRIC_SESSION_STREAM_SHARDS", 8), "VPN fabric-session stream shards per traffic class.")
|
|
fs.IntVar(&cfg.VPNFabricQUICMaxStreamsPerConn, "vpn-fabric-quic-max-streams-per-conn", getEnvInt(env, "RAP_VPN_FABRIC_QUIC_MAX_STREAMS_PER_CONN", 64), "Maximum logical fabric-session streams per cached VPN QUIC carrier connection.")
|
|
fs.DurationVar(&cfg.VPNFabricQUICIdleTTL, "vpn-fabric-quic-idle-ttl", time.Duration(getEnvInt(env, "RAP_VPN_FABRIC_QUIC_IDLE_TTL_SECONDS", 300))*time.Second, "Idle TTL for cached VPN QUIC carrier connections.")
|
|
fs.IntVar(&cfg.MeshProductionObservationSinkCapacity, "mesh-production-observation-sink-capacity", getEnvSignedInt(env, "RAP_MESH_PRODUCTION_OBSERVATION_SINK_CAPACITY", 0), "Bounded local metadata-only production envelope observation sink capacity. Disabled when 0.")
|
|
fs.StringVar(&cfg.FabricListenAddr, "fabric-listen-addr", getEnv(env, "RAP_FABRIC_LISTEN_ADDR", ""), "Optional node listener address used by the QUIC fabric runtime contract.")
|
|
fs.StringVar(&cfg.FabricListenPortMode, "fabric-listen-port-mode", getEnv(env, "RAP_FABRIC_LISTEN_PORT_MODE", "manual"), "Fabric listen port behavior: manual, auto, or disabled.")
|
|
fs.IntVar(&cfg.FabricListenAutoPortStart, "fabric-listen-auto-port-start", getEnvInt(env, "RAP_FABRIC_LISTEN_AUTO_PORT_START", 19131), "First port used when fabric listen port mode is auto.")
|
|
fs.IntVar(&cfg.FabricListenAutoPortEnd, "fabric-listen-auto-port-end", getEnvInt(env, "RAP_FABRIC_LISTEN_AUTO_PORT_END", 19231), "Last port used when fabric listen port mode is auto.")
|
|
fs.StringVar(&cfg.MeshAdvertiseEndpoint, "mesh-advertise-endpoint", getEnv(env, "RAP_MESH_ADVERTISE_ENDPOINT", ""), "Advertised mesh endpoint reported to the Control Plane. Empty disables endpoint reporting.")
|
|
fs.StringVar(&cfg.MeshAdvertiseEndpointsJSON, "mesh-advertise-endpoints-json", getEnv(env, "RAP_MESH_ADVERTISE_ENDPOINTS_JSON", ""), "JSON array of advertised mesh endpoint candidates, including private/corporate endpoints.")
|
|
fs.StringVar(&cfg.FabricRegistryRecordsJSON, "fabric-registry-records-json", getEnv(env, "RAP_FABRIC_REGISTRY_RECORDS_JSON", ""), "JSON array of signed QUIC-only fabric registry gossip records used as bootstrap discovery seeds.")
|
|
fs.StringVar(&cfg.MeshAdvertiseTransport, "mesh-advertise-transport", getEnv(env, "RAP_MESH_ADVERTISE_TRANSPORT", "quic"), "Transport label for the advertised mesh endpoint.")
|
|
fs.StringVar(&cfg.MeshConnectivityMode, "mesh-connectivity-mode", getEnv(env, "RAP_MESH_CONNECTIVITY_MODE", "direct"), "Connectivity mode reported with the advertised mesh endpoint.")
|
|
fs.StringVar(&cfg.MeshNATType, "mesh-nat-type", getEnv(env, "RAP_MESH_NAT_TYPE", "unknown"), "NAT type hint reported with the advertised mesh endpoint.")
|
|
fs.StringVar(&cfg.MeshSiteID, "mesh-site-id", getEnv(env, "RAP_MESH_SITE_ID", ""), "Optional physical or logical site identifier advertised with QUIC endpoint candidates.")
|
|
fs.StringVar(&cfg.MeshLocalityGroupID, "mesh-locality-group-id", getEnv(env, "RAP_MESH_LOCALITY_GROUP_ID", ""), "Optional locality group identifier used to decide whether private QUIC endpoints are actually local.")
|
|
fs.StringVar(&cfg.MeshNATGroupID, "mesh-nat-group-id", getEnv(env, "RAP_MESH_NAT_GROUP_ID", ""), "Optional NAT group ID advertised with QUIC endpoint candidates.")
|
|
fs.StringVar(&cfg.MeshSTUNReflexiveEndpoint, "mesh-stun-reflexive-endpoint", getEnv(env, "RAP_MESH_STUN_REFLEXIVE_ENDPOINT", ""), "Optional STUN-discovered reflexive QUIC endpoint, for example quic://203.0.113.10:19443.")
|
|
fs.StringVar(&cfg.MeshSTUNServer, "mesh-stun-server", getEnv(env, "RAP_MESH_STUN_SERVER", ""), "Optional STUN server name used to discover the reflexive endpoint.")
|
|
fs.StringVar(&cfg.MeshRelayNodeID, "mesh-relay-node-id", getEnv(env, "RAP_MESH_RELAY_NODE_ID", ""), "Optional relay node ID for relay-required QUIC fallback candidates.")
|
|
fs.StringVar(&cfg.MeshRelayEndpoint, "mesh-relay-endpoint", getEnv(env, "RAP_MESH_RELAY_ENDPOINT", ""), "Optional relay QUIC endpoint for relay-required fallback candidates.")
|
|
fs.StringVar(&cfg.MeshRegion, "mesh-region", getEnv(env, "RAP_MESH_REGION", ""), "Optional region/site hint for the advertised mesh endpoint.")
|
|
fs.StringVar(&cfg.MeshSyntheticConfigPath, "mesh-synthetic-config", getEnv(env, "RAP_MESH_SYNTHETIC_CONFIG", ""), "Path to scoped synthetic mesh config snapshot. Preferred over debug JSON env.")
|
|
fs.StringVar(&cfg.MeshPeerEndpointsJSON, "mesh-peer-endpoints-json", getEnv(env, "RAP_MESH_PEER_ENDPOINTS_JSON", ""), "JSON object mapping peer node_id to synthetic mesh endpoint URL.")
|
|
fs.StringVar(&cfg.MeshSyntheticRoutesJSON, "mesh-synthetic-routes-json", getEnv(env, "RAP_MESH_SYNTHETIC_ROUTES_JSON", ""), "JSON array of synthetic mesh routes for test-only runtime.")
|
|
fs.BoolVar(&cfg.RemoteWorkspaceRealAdapterEnabled, "remote-workspace-real-adapter-enabled", getEnvBool(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_ENABLED", false), "Request future real remote workspace adapter supervision. Disabled until the real runtime stage is implemented.")
|
|
fs.StringVar(&cfg.RemoteWorkspaceRealAdapterCommand, "remote-workspace-real-adapter-command", getEnv(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_COMMAND", ""), "Future real remote workspace adapter command path. Redacted from status payloads.")
|
|
fs.StringVar(&cfg.RemoteWorkspaceRealAdapterArgsJSON, "remote-workspace-real-adapter-args-json", getEnv(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_ARGS_JSON", ""), "Future real remote workspace adapter args JSON. Redacted from status payloads.")
|
|
fs.StringVar(&cfg.RemoteWorkspaceRealAdapterWorkDir, "remote-workspace-real-adapter-workdir", getEnv(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_WORKDIR", ""), "Future real remote workspace adapter working directory. Redacted from status payloads.")
|
|
heartbeatSeconds := getEnvInt(env, "RAP_HEARTBEAT_INTERVAL_SECONDS", 15)
|
|
fs.DurationVar(&cfg.HeartbeatInterval, "heartbeat-interval", time.Duration(heartbeatSeconds)*time.Second, "Heartbeat interval.")
|
|
enrollmentPollIntervalSeconds := getEnvInt(env, "RAP_ENROLLMENT_POLL_INTERVAL_SECONDS", 5)
|
|
enrollmentPollTimeoutSeconds := getEnvSignedInt(env, "RAP_ENROLLMENT_POLL_TIMEOUT_SECONDS", 0)
|
|
fs.DurationVar(&cfg.EnrollmentPollInterval, "enrollment-poll-interval", time.Duration(enrollmentPollIntervalSeconds)*time.Second, "Enrollment approval polling interval.")
|
|
fs.DurationVar(&cfg.EnrollmentPollTimeout, "enrollment-poll-timeout", time.Duration(enrollmentPollTimeoutSeconds)*time.Second, "Enrollment approval polling timeout.")
|
|
if err := fs.Parse(args); err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.ClusterID = strings.TrimSpace(cfg.ClusterID)
|
|
cfg.ClusterAuthorityPublicKey = strings.TrimSpace(cfg.ClusterAuthorityPublicKey)
|
|
cfg.ClusterAuthorityFingerprint = strings.TrimSpace(cfg.ClusterAuthorityFingerprint)
|
|
cfg.JoinToken = strings.TrimSpace(cfg.JoinToken)
|
|
cfg.NodeName = strings.TrimSpace(cfg.NodeName)
|
|
cfg.StateDir = strings.TrimSpace(cfg.StateDir)
|
|
cfg.FabricListenAddr = strings.TrimSpace(cfg.FabricListenAddr)
|
|
cfg.MeshQUICFabricListenAddr = strings.TrimSpace(cfg.MeshQUICFabricListenAddr)
|
|
cfg.FabricListenPortMode = strings.ToLower(strings.TrimSpace(cfg.FabricListenPortMode))
|
|
if cfg.VPNFabricSessionStreamShards <= 0 {
|
|
cfg.VPNFabricSessionStreamShards = 8
|
|
}
|
|
if cfg.VPNFabricSessionStreamShards > 128 {
|
|
cfg.VPNFabricSessionStreamShards = 128
|
|
}
|
|
if cfg.VPNFabricQUICMaxStreamsPerConn <= 0 {
|
|
cfg.VPNFabricQUICMaxStreamsPerConn = 64
|
|
}
|
|
if cfg.VPNFabricQUICIdleTTL <= 0 {
|
|
cfg.VPNFabricQUICIdleTTL = 5 * time.Minute
|
|
}
|
|
cfg.MeshAdvertiseEndpoint = strings.TrimRight(strings.TrimSpace(cfg.MeshAdvertiseEndpoint), "/")
|
|
cfg.MeshAdvertiseEndpointsJSON = strings.TrimSpace(cfg.MeshAdvertiseEndpointsJSON)
|
|
cfg.FabricRegistryRecordsJSON = strings.TrimSpace(cfg.FabricRegistryRecordsJSON)
|
|
cfg.MeshAdvertiseTransport = strings.TrimSpace(cfg.MeshAdvertiseTransport)
|
|
if cfg.MeshAdvertiseTransport == "" {
|
|
cfg.MeshAdvertiseTransport = "quic"
|
|
}
|
|
cfg.MeshConnectivityMode = strings.TrimSpace(cfg.MeshConnectivityMode)
|
|
cfg.MeshNATType = strings.TrimSpace(cfg.MeshNATType)
|
|
cfg.MeshSiteID = strings.TrimSpace(cfg.MeshSiteID)
|
|
cfg.MeshLocalityGroupID = strings.TrimSpace(cfg.MeshLocalityGroupID)
|
|
cfg.MeshNATGroupID = strings.TrimSpace(cfg.MeshNATGroupID)
|
|
cfg.MeshSTUNReflexiveEndpoint = strings.TrimRight(strings.TrimSpace(cfg.MeshSTUNReflexiveEndpoint), "/")
|
|
cfg.MeshSTUNServer = strings.TrimSpace(cfg.MeshSTUNServer)
|
|
cfg.MeshRelayNodeID = strings.TrimSpace(cfg.MeshRelayNodeID)
|
|
cfg.MeshRelayEndpoint = strings.TrimRight(strings.TrimSpace(cfg.MeshRelayEndpoint), "/")
|
|
cfg.MeshRegion = strings.TrimSpace(cfg.MeshRegion)
|
|
cfg.MeshSyntheticConfigPath = strings.TrimSpace(cfg.MeshSyntheticConfigPath)
|
|
cfg.MeshPeerEndpointsJSON = strings.TrimSpace(cfg.MeshPeerEndpointsJSON)
|
|
cfg.MeshSyntheticRoutesJSON = strings.TrimSpace(cfg.MeshSyntheticRoutesJSON)
|
|
cfg.WebIngressSigningPrivateKey = strings.TrimSpace(cfg.WebIngressSigningPrivateKey)
|
|
cfg.WebIngressSigningKeyID = strings.TrimSpace(cfg.WebIngressSigningKeyID)
|
|
cfg.WebIngressTrustedKeysJSON = strings.TrimSpace(cfg.WebIngressTrustedKeysJSON)
|
|
cfg.WebIngressRuntimeServiceClasses = strings.TrimSpace(cfg.WebIngressRuntimeServiceClasses)
|
|
cfg.RemoteWorkspaceRealAdapterCommand = strings.TrimSpace(cfg.RemoteWorkspaceRealAdapterCommand)
|
|
cfg.RemoteWorkspaceRealAdapterArgsJSON = strings.TrimSpace(cfg.RemoteWorkspaceRealAdapterArgsJSON)
|
|
cfg.RemoteWorkspaceRealAdapterWorkDir = strings.TrimSpace(cfg.RemoteWorkspaceRealAdapterWorkDir)
|
|
if cfg.FabricRegistryRecordsJSON == "" {
|
|
return Config{}, errors.New("fabric registry records are required")
|
|
}
|
|
if cfg.NodeName == "" {
|
|
return Config{}, errors.New("node name is required")
|
|
}
|
|
if cfg.StateDir == "" {
|
|
return Config{}, errors.New("state dir is required")
|
|
}
|
|
if cfg.HeartbeatInterval <= 0 {
|
|
return Config{}, errors.New("heartbeat interval must be positive")
|
|
}
|
|
if cfg.EnrollmentPollInterval <= 0 {
|
|
return Config{}, errors.New("enrollment poll interval must be positive")
|
|
}
|
|
if cfg.EnrollmentPollTimeout < 0 {
|
|
return Config{}, errors.New("enrollment poll timeout must not be negative")
|
|
}
|
|
if cfg.MeshProductionObservationSinkCapacity < 0 {
|
|
return Config{}, errors.New("mesh production observation sink capacity must not be negative")
|
|
}
|
|
if cfg.MeshProductionObservationSinkCapacity > MaxMeshProductionObservationSinkCapacity {
|
|
return Config{}, errors.New("mesh production observation sink capacity exceeds maximum")
|
|
}
|
|
if cfg.FabricRegistryRecordsJSON != "" && !isJSONArray(cfg.FabricRegistryRecordsJSON) {
|
|
return Config{}, errors.New("fabric registry records must be a JSON array")
|
|
}
|
|
switch cfg.FabricListenPortMode {
|
|
case "", "manual", "auto", "disabled":
|
|
if cfg.FabricListenPortMode == "" {
|
|
cfg.FabricListenPortMode = "manual"
|
|
}
|
|
default:
|
|
return Config{}, errors.New("fabric listen port mode must be manual, auto, or disabled")
|
|
}
|
|
if cfg.FabricListenAutoPortStart <= 0 || cfg.FabricListenAutoPortEnd <= 0 {
|
|
return Config{}, errors.New("fabric listen auto port range must be positive")
|
|
}
|
|
if cfg.FabricListenAutoPortStart > cfg.FabricListenAutoPortEnd {
|
|
return Config{}, errors.New("fabric listen auto port start must be less than or equal to end")
|
|
}
|
|
if !isQUICAdvertiseTransport(cfg.MeshAdvertiseTransport) {
|
|
return Config{}, errors.New("mesh advertise transport must be a QUIC transport label")
|
|
}
|
|
if hasUnsupportedEndpointScheme(cfg.MeshAdvertiseEndpoint) {
|
|
return Config{}, errors.New("mesh advertise endpoint must be a QUIC endpoint")
|
|
}
|
|
if cfg.MeshSTUNReflexiveEndpoint != "" && hasUnsupportedEndpointScheme(cfg.MeshSTUNReflexiveEndpoint) {
|
|
return Config{}, errors.New("mesh STUN reflexive endpoint must be a QUIC endpoint")
|
|
}
|
|
if cfg.MeshRelayEndpoint != "" && hasUnsupportedEndpointScheme(cfg.MeshRelayEndpoint) {
|
|
return Config{}, errors.New("mesh relay endpoint must be a QUIC endpoint")
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func isQUICAdvertiseTransport(label string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(label)) {
|
|
case "quic", "direct_quic", "udp_quic", "quic_udp", "lan_quic", "reverse_quic", "relay_quic", "ice_quic":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func hasUnsupportedEndpointScheme(endpoint string) bool {
|
|
endpoint = strings.ToLower(strings.TrimSpace(endpoint))
|
|
if endpoint == "" || !strings.Contains(endpoint, "://") {
|
|
return false
|
|
}
|
|
return !strings.HasPrefix(endpoint, "quic://")
|
|
}
|
|
|
|
func isJSONArray(value string) bool {
|
|
var items []json.RawMessage
|
|
return json.Unmarshal([]byte(strings.TrimSpace(value)), &items) == nil
|
|
}
|
|
|
|
func readEnv() map[string]string {
|
|
out := map[string]string{}
|
|
for _, pair := range os.Environ() {
|
|
key, value, ok := strings.Cut(pair, "=")
|
|
if ok {
|
|
out[key] = value
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func getEnv(env map[string]string, key, fallback string) string {
|
|
if value := strings.TrimSpace(env[key]); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func getEnvInt(env map[string]string, key string, fallback int) int {
|
|
value := strings.TrimSpace(env[key])
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil || parsed <= 0 {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func getEnvSignedInt(env map[string]string, key string, fallback int) int {
|
|
value := strings.TrimSpace(env[key])
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func getEnvBool(env map[string]string, key string, fallback bool) bool {
|
|
value := strings.ToLower(strings.TrimSpace(env[key]))
|
|
switch value {
|
|
case "1", "true", "yes", "y", "on":
|
|
return true
|
|
case "0", "false", "no", "n", "off":
|
|
return false
|
|
default:
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
func hostnameOrDefault() string {
|
|
host, err := os.Hostname()
|
|
if err != nil || strings.TrimSpace(host) == "" {
|
|
return "rap-node"
|
|
}
|
|
return host
|
|
}
|