package edgecmd import ( "errors" "fmt" "os" "path/filepath" "github.com/spf13/cobra" "gopkg.in/yaml.v3" "iop/apps/edge/internal/node" "iop/packages/go/config" "iop/packages/go/hostsetup" ) func LoadEdgeConfig(cmd *cobra.Command) (*config.EdgeConfig, string, error) { path, err := ResolveConfigPath(cmd) if err != nil { return nil, "", err } cfg, err := config.LoadEdge(path) if err != nil { return nil, "", fmt.Errorf("load config: %w", err) } return cfg, path, nil } func configCmd() *cobra.Command { c := &cobra.Command{ Use: "config", Short: "Create, inspect, and validate edge config", Long: `Create, inspect, and validate the bundle-local edge.yaml configuration. Typical local flow: 1. iop-edge config init 2. iop-edge env 3. iop-edge node register 4. iop-edge config check 5. iop-edge serve`, Example: ` iop-edge config init iop-edge env iop-edge node register node-silicon-ollama --adapter ollama iop-edge config check`, } c.AddCommand(configInitCmd(), configPrintCmd(), configCheckCmd()) return c } func configInitCmd() *cobra.Command { output := "edge.yaml" var force bool var stdout bool c := &cobra.Command{ Use: "init", Short: "Write an edge.yaml template", Long: "Write a deployable edge.yaml template to the current bundle directory or stdout.\n" + "Run this beside a local iop-edge binary, then run `iop-edge config check`.", Example: " iop-edge config init\n" + " iop-edge config init --stdout\n" + " iop-edge config init --output edge.yaml\n" + " iop-edge config check", RunE: func(cmd *cobra.Command, _ []string) error { template := []byte(hostsetup.EdgeBundleConfigTemplate()) if stdout || output == "-" { _, err := cmd.OutOrStdout().Write(template) return err } if !force { if _, err := os.Stat(output); err == nil { return fmt.Errorf("%s already exists (use --force to overwrite)", output) } else if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("stat output: %w", err) } } if dir := filepath.Dir(output); dir != "" && dir != "." { if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("mkdir %s: %w", dir, err) } } if err := os.WriteFile(output, template, 0o600); err != nil { return fmt.Errorf("write %s: %w", output, err) } fmt.Fprintf(cmd.OutOrStdout(), "wrote %s\n", output) return nil }, } c.Flags().StringVarP(&output, "output", "o", output, "output path") c.Flags().BoolVar(&stdout, "stdout", false, "print template to stdout instead of writing a file") c.Flags().BoolVar(&force, "force", false, "overwrite an existing output file") return c } func configPrintCmd() *cobra.Command { return &cobra.Command{ Use: "print", Short: "Print effective edge config as YAML", RunE: func(cmd *cobra.Command, _ []string) error { cfg, _, err := LoadEdgeConfig(cmd) if err != nil { return err } enc := yaml.NewEncoder(cmd.OutOrStdout()) defer enc.Close() return enc.Encode(cfg) }, } } func configCheckCmd() *cobra.Command { return &cobra.Command{ Use: "check", Short: "Validate edge config can be loaded", RunE: func(cmd *cobra.Command, _ []string) error { cfg, path, err := LoadEdgeConfig(cmd) if err != nil { return err } if err := validateEdgeConfig(cfg); err != nil { return fmt.Errorf("validate config: %w", err) } fmt.Fprintf(cmd.OutOrStdout(), "OK %s\n", path) return nil }, } } func anyOllamaEnabled(a config.AdaptersConf) bool { if a.Ollama.Enabled { return true } for _, inst := range a.OllamaInstances { if inst.Enabled { return true } } return false } func anyVllmEnabled(a config.AdaptersConf) bool { if a.Vllm.Enabled { return true } for _, inst := range a.VllmInstances { if inst.Enabled { return true } } return false } func validateEdgeConfig(cfg *config.EdgeConfig) error { _, err := node.LoadFromConfig(cfg.Nodes) if err != nil { return err } for i, n := range cfg.Nodes { name := n.ID if name == "" { name = n.Alias } if name == "" { name = fmt.Sprintf("index %d", i) } hasOllama := anyOllamaEnabled(n.Adapters) hasVllm := anyVllmEnabled(n.Adapters) if !hasOllama && !n.Adapters.CLI.Enabled && !hasVllm { return fmt.Errorf("node %q: at least one adapter must be enabled", name) } if n.Adapters.Ollama.Enabled && n.Adapters.Ollama.BaseURL == "" { return fmt.Errorf("node %q: ollama adapter base_url must not be empty", name) } for j, inst := range n.Adapters.OllamaInstances { if inst.Enabled && inst.BaseURL == "" { return fmt.Errorf("node %q: ollama_instances[%d] %q: base_url must not be empty", name, j, inst.Name) } } if n.Adapters.Vllm.Enabled && n.Adapters.Vllm.Endpoint == "" { return fmt.Errorf("node %q: vllm adapter endpoint must not be empty", name) } for j, inst := range n.Adapters.VllmInstances { if inst.Enabled && inst.Endpoint == "" { return fmt.Errorf("node %q: vllm_instances[%d] %q: endpoint must not be empty", name, j, inst.Name) } } if n.Runtime.Concurrency < 0 { return fmt.Errorf("node %q: runtime concurrency must be non-negative", name) } } return nil }