iop/apps/edge/internal/edgecmd/config.go
toki 76a8ee5b3b feat: lemonade provider serving validation - config contract and edge node mapping updates
- Migrate config contract files to archive (01_config_contract)
- Update edge config.go for new configuration structure
- Refactor node/mapper.go and mapper_test.go for field mapping
- Update node/store_test.go tests
- Add new fields to configs/edge.yaml
- Extend packages/go/config with new configuration options
- Update protobuf definitions in runtime.proto and generated code
2026-06-15 11:01:21 +09:00

214 lines
5.8 KiB
Go

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 <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())
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 anyOpenAICompatEnabled(a config.AdaptersConf) bool {
if a.OpenAICompat.Enabled {
return true
}
for _, inst := range a.OpenAICompatInstances {
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)
hasOpenAICompat := anyOpenAICompatEnabled(n.Adapters)
if !hasOllama && !n.Adapters.CLI.Enabled && !hasVllm && !hasOpenAICompat {
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.Adapters.OpenAICompat.Enabled && n.Adapters.OpenAICompat.Endpoint == "" {
return fmt.Errorf("node %q: openai_compat adapter endpoint must not be empty", name)
}
for j, inst := range n.Adapters.OpenAICompatInstances {
if inst.Enabled && inst.Endpoint == "" {
return fmt.Errorf("node %q: openai_compat_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
}