iop/apps/edge/internal/edgecmd/env.go
toki ff161b9b12 feat: runtime reconnect config refresh milestone completion
- 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
2026-06-21 16:14:16 +09:00

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, ", "))
}