- 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
214 lines
5.8 KiB
Go
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
|
|
}
|