iop/apps/edge/internal/edgecmd/config.go
toki 445716cc3e feat(edge): runtime config refresh mechanism implementation
- 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
2026-06-21 21:35:50 +09:00

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