- Add runtime-reconnect-config-refresh milestone and SDD - Archive m-runtime-reconnect-config-refresh task with full logs - Update edge/node domain rules, bootstrap, transport sessions - Update config package and node.yaml configuration - Update documentation and ROADMAP/PHASE files
182 lines
4.9 KiB
Go
182 lines
4.9 KiB
Go
package edgecmd
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
// envCmd prints the resolved environment for the bundled edge binary so the
|
|
// user can verify config discovery, advertise host, log path, transport
|
|
// address, artifact/bootstrap URL, and OpenAI-compatible URL without starting
|
|
// edge. It does not bind any port or connect to nodes.
|
|
func envCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "env",
|
|
Short: "Print resolved edge environment without starting edge",
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
cfg, path, err := LoadEdgeConfig(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logPath, err := ResolveEdgeLogPath(cfg.Logging.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.Bootstrap.ArtifactDir = ResolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir)
|
|
renderEnv(cmd.OutOrStdout(), envReport{
|
|
ConfigPath: path,
|
|
BinaryDir: binaryDir(),
|
|
LogPath: logPath,
|
|
Advertise: resolveAdvertiseHost(cfg.Server.AdvertiseHost),
|
|
Server: cfg.Server,
|
|
Bootstrap: cfg.Bootstrap,
|
|
OpenAI: cfg.OpenAI,
|
|
A2A: cfg.A2A,
|
|
Nodes: cfg.Nodes,
|
|
})
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
type advertise struct {
|
|
Host string
|
|
Source string
|
|
}
|
|
|
|
func resolveAdvertiseHost(explicit string) advertise {
|
|
if explicit != "" {
|
|
return advertise{Host: explicit, Source: "explicit"}
|
|
}
|
|
if ip := nonLoopbackIPv4(); ip != "" {
|
|
return advertise{Host: ip, Source: "auto"}
|
|
}
|
|
if hn, err := os.Hostname(); err == nil && hn != "" {
|
|
return advertise{Host: hn, Source: "auto"}
|
|
}
|
|
return advertise{Host: "127.0.0.1", Source: "auto"}
|
|
}
|
|
|
|
func nonLoopbackIPv4() string {
|
|
addrs, err := net.InterfaceAddrs()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, a := range addrs {
|
|
ipnet, ok := a.(*net.IPNet)
|
|
if !ok || ipnet.IP.IsLoopback() {
|
|
continue
|
|
}
|
|
ip4 := ipnet.IP.To4()
|
|
if ip4 == nil {
|
|
continue
|
|
}
|
|
return ip4.String()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// addressFor combines an advertise host with the port from a listen string
|
|
// (e.g. "0.0.0.0:9090" -> "<host>:9090"). If the listen string is invalid it
|
|
// is returned unchanged.
|
|
func addressFor(host, listen string) string {
|
|
_, port, err := net.SplitHostPort(listen)
|
|
if err != nil || port == "" {
|
|
return listen
|
|
}
|
|
return net.JoinHostPort(host, port)
|
|
}
|
|
|
|
func defaultArtifactBaseURL(host string, listen ...string) string {
|
|
bootstrapListen := "0.0.0.0:18080"
|
|
if len(listen) > 0 && listen[0] != "" {
|
|
bootstrapListen = listen[0]
|
|
}
|
|
return "http://" + addressFor(host, bootstrapListen)
|
|
}
|
|
|
|
func bootstrapScriptURL(baseURL, target string) string {
|
|
return strings.TrimSuffix(baseURL, "/") + "/bootstrap/node-" + target + ".sh"
|
|
}
|
|
|
|
func bootstrapPowerShellScriptURL(baseURL, target string) string {
|
|
return strings.TrimSuffix(baseURL, "/") + "/bootstrap/node-" + target + ".ps1"
|
|
}
|
|
|
|
func universalBootstrapScriptURL(baseURL string) string {
|
|
return strings.TrimSuffix(baseURL, "/") + "/bootstrap/node.sh"
|
|
}
|
|
|
|
type envReport struct {
|
|
ConfigPath string
|
|
BinaryDir string
|
|
LogPath string
|
|
Advertise advertise
|
|
Server config.EdgeServerConf
|
|
Bootstrap config.EdgeBootstrapConf
|
|
OpenAI config.EdgeOpenAIConf
|
|
A2A config.EdgeA2AConf
|
|
Nodes []config.NodeDefinition
|
|
}
|
|
|
|
func renderEnv(w io.Writer, r envReport) {
|
|
artifactURL := strings.TrimSuffix(r.Bootstrap.ArtifactBaseURL, "/")
|
|
artifactSource := "configured"
|
|
if artifactURL == "" {
|
|
artifactURL = defaultArtifactBaseURL(r.Advertise.Host, r.Bootstrap.Listen)
|
|
artifactSource = "derived"
|
|
}
|
|
|
|
fmt.Fprintf(w, "config: %s\n", r.ConfigPath)
|
|
fmt.Fprintf(w, "binary_dir: %s\n", r.BinaryDir)
|
|
fmt.Fprintf(w, "log_path: %s\n", r.LogPath)
|
|
fmt.Fprintf(w, "advertise_host: %s (%s)\n", r.Advertise.Host, r.Advertise.Source)
|
|
fmt.Fprintf(w, "node_transport: %s\n", addressFor(r.Advertise.Host, r.Server.Listen))
|
|
fmt.Fprintf(w, "artifact_dir: %s\n", r.Bootstrap.ArtifactDir)
|
|
fmt.Fprintf(w, "artifact_listen: %s\n", r.Bootstrap.Listen)
|
|
fmt.Fprintf(w, "artifact_base_url: %s (%s)\n", artifactURL, artifactSource)
|
|
fmt.Fprintf(w, "node_bootstrap: %s\n", universalBootstrapScriptURL(artifactURL))
|
|
if r.OpenAI.Enabled {
|
|
fmt.Fprintf(w, "openai_url: http://%s/v1\n", addressFor(r.Advertise.Host, r.OpenAI.Listen))
|
|
} else {
|
|
fmt.Fprintln(w, "openai_url: (disabled)")
|
|
}
|
|
if r.A2A.Enabled {
|
|
path := r.A2A.Path
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
fmt.Fprintf(w, "a2a_url: http://%s%s\n", addressFor(r.Advertise.Host, r.A2A.Listen), path)
|
|
} else {
|
|
fmt.Fprintln(w, "a2a_url: (disabled)")
|
|
}
|
|
fmt.Fprintf(w, "configured_nodes: %s\n", summarizeNodes(r.Nodes))
|
|
fmt.Fprintln(w, "connected_nodes: offline (env does not start edge)")
|
|
}
|
|
|
|
func summarizeNodes(nodes []config.NodeDefinition) string {
|
|
if len(nodes) == 0 {
|
|
return "0 (none)"
|
|
}
|
|
labels := make([]string, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
label := n.ID
|
|
if label == "" {
|
|
label = n.Alias
|
|
}
|
|
if label == "" {
|
|
label = "(unnamed)"
|
|
}
|
|
labels = append(labels, label)
|
|
}
|
|
sort.Strings(labels)
|
|
return fmt.Sprintf("%d (%s)", len(nodes), strings.Join(labels, ", "))
|
|
}
|