package edgecmd import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "os" "path/filepath" "time" "github.com/spf13/cobra" "gopkg.in/yaml.v3" "iop/apps/edge/internal/configrefresh" "iop/apps/edge/internal/edgevalidate" "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(), configRefreshCmd()) 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 configRefreshCmd() *cobra.Command { var mode string var addr string var cfgPath string c := &cobra.Command{ Use: "refresh", Short: "Send a config refresh request to a running Edge process", Long: `Send a dry-run or apply config refresh request to the Edge-local admin API. The running Edge process must have refresh.enabled=true in its config.`, Example: ` iop-edge config refresh --mode dry-run iop-edge config refresh --mode apply --config /path/to/edge.yaml`, RunE: func(cmd *cobra.Command, _ []string) error { var m configrefresh.Mode switch mode { case "dry-run", string(configrefresh.ModeDryRun): m = configrefresh.ModeDryRun case string(configrefresh.ModeApply): m = configrefresh.ModeApply default: return fmt.Errorf("invalid --mode %q: must be dry-run or apply", mode) } // Resolve config path from flag or parent --config flag. if cfgPath == "" { resolved, err := ResolveConfigPath(cmd) if err != nil { return fmt.Errorf("resolve config path: %w", err) } cfgPath = resolved } req := configrefresh.Request{ Mode: m, ConfigPath: cfgPath, } body, err := json.Marshal(req) if err != nil { return fmt.Errorf("marshal request: %w", err) } client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Post("http://"+addr+"/refresh", "application/json", bytes.NewReader(body)) if err != nil { return fmt.Errorf("POST http://%s/refresh: %w", addr, err) } defer resp.Body.Close() var result configrefresh.Result if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return fmt.Errorf("decode response: %w", err) } enc := json.NewEncoder(cmd.OutOrStdout()) enc.SetIndent("", " ") return enc.Encode(result) }, } c.Flags().StringVar(&mode, "mode", "dry-run", "refresh mode: dry-run or apply") c.Flags().StringVar(&addr, "addr", "127.0.0.1:19093", "refresh admin server address") c.Flags().StringVar(&cfgPath, "config-path", "", "candidate config path (defaults to the resolved --config path)") return c } func validateEdgeConfig(cfg *config.EdgeConfig) error { return edgevalidate.ValidateEdgeConfig(cfg) }