- Add configrefresh package for config classification, request handling - Add bootstrap refresh_admin.go for admin-level config refresh - Add edgevalidate package for config validation - Update edge bootstrap runtime with refresh capabilities - Update edge service layer with config refresh integration - Update openai layer routes/chat_handler/server for config refresh - Update edgecmd config handling with refresh support - Update input manager, transport server, status provider - Add edge.yaml config updates and go/config package changes - Add hostsetup templates and test updates - Update roadmap and phase documentation for runtime-reconnect-config-refresh
193 lines
5.7 KiB
Go
193 lines
5.7 KiB
Go
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 <node-id>
|
|
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)
|
|
}
|