// Package noderesolver provides unified node discovery for the orama CLI. // // It resolves operator-owned nodes by querying the network's gateway API // (primary) or falling back to the legacy nodes.conf file. package noderesolver import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "time" "github.com/DeBrosOfficial/network/pkg/auth" "github.com/DeBrosOfficial/network/pkg/cli" "github.com/DeBrosOfficial/network/pkg/cli/remotessh" "github.com/DeBrosOfficial/network/pkg/inspector" ) // httpClient is the shared HTTP client for API calls. var httpClient = &http.Client{Timeout: 10 * time.Second} // ResolveNodes returns the operator's nodes for a given environment. // It first tries the network API (GET /v1/operator/nodes), then falls // back to nodes.conf if the API is unreachable or returns no results. func ResolveNodes(env string) ([]inspector.Node, error) { nodes, err := resolveFromNetwork(env) if err == nil && len(nodes) > 0 { return nodes, nil } // Fallback to nodes.conf confNodes, confErr := remotessh.LoadEnvNodes(env) if confErr != nil { if err != nil { return nil, fmt.Errorf("network API: %w; nodes.conf: %v", err, confErr) } return nil, confErr } return confNodes, nil } // ResolveNodesNetworkOnly queries only the network API without nodes.conf fallback. func ResolveNodesNetworkOnly(env string) ([]inspector.Node, error) { return resolveFromNetwork(env) } // resolveFromNetwork queries the gateway API for operator-owned nodes. func resolveFromNetwork(env string) ([]inspector.Node, error) { // 1. Get gateway URL for the environment gatewayURL, err := gatewayURLForEnv(env) if err != nil { return nil, fmt.Errorf("failed to resolve gateway URL: %w", err) } // 2. Load stored credentials for this gateway apiKey, err := loadAPIKey(gatewayURL) if err != nil { return nil, fmt.Errorf("no credentials for %s: %w (run 'orama auth login' first)", gatewayURL, err) } return resolveFromNetworkWithURL(gatewayURL, apiKey, env) } // resolveFromNetworkWithURL queries a specific gateway URL with an API key. // Exported for testing. func resolveFromNetworkWithURL(gatewayURL, apiKey, env string) ([]inspector.Node, error) { endpoint := fmt.Sprintf("%s/v1/operator/nodes?env=%s", gatewayURL, url.QueryEscape(env)) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("X-API-Key", apiKey) resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to reach gateway: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("gateway returned HTTP %d: %s", resp.StatusCode, string(body)) } var result struct { Nodes []struct { ID string `json:"id"` IPAddress string `json:"ip_address"` InternalIP string `json:"internal_ip"` Environment string `json:"environment"` Role string `json:"role"` SSHUser string `json:"ssh_user"` Status string `json:"status"` } `json:"nodes"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } nodes := make([]inspector.Node, 0, len(result.Nodes)) for _, n := range result.Nodes { user := n.SSHUser if user == "" { user = "root" } // Sandbox nodes share a single SSH key; production nodes use per-host keys. vaultTarget := fmt.Sprintf("%s/%s", n.IPAddress, user) if n.Environment == "sandbox" { vaultTarget = "sandbox/root" } nodes = append(nodes, inspector.Node{ Environment: n.Environment, User: user, Host: n.IPAddress, Role: n.Role, VaultTarget: vaultTarget, }) } return nodes, nil } // gatewayURLForEnv returns the gateway URL for a given environment name. // If env is empty, uses the active environment. func gatewayURLForEnv(env string) (string, error) { if env == "" { e, err := cli.GetActiveEnvironment() if err != nil { return "", err } return e.GatewayURL, nil } e, err := cli.GetEnvironmentByName(env) if err != nil { return "", err } return e.GatewayURL, nil } // loadAPIKey loads the stored API key for a gateway URL. func loadAPIKey(gatewayURL string) (string, error) { store, err := auth.LoadEnhancedCredentials() if err != nil { return "", fmt.Errorf("failed to load credentials: %w", err) } creds := store.GetDefaultCredential(gatewayURL) if creds == nil || creds.APIKey == "" { return "", fmt.Errorf("no credentials found for %s", gatewayURL) } return creds.APIKey, nil }