Adds the kvm block to the providers config (SSH access, images map, machine-type profiles, hosts list) with validation and table tests. validate() now rejects mismatched type-specific blocks generically via setBlocks() instead of pairwise checks. Step 1 of docs/plans/2026-09-04-1404-kvm-provider.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
127 lines
3.8 KiB
Go
127 lines
3.8 KiB
Go
package provider
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
// Config is the top-level shape of the --providers-config file.
|
|
type Config struct {
|
|
Providers []ProviderConfig `json:"providers"`
|
|
}
|
|
|
|
// ProviderConfig is one named, typed provider instance — e.g. "gcp-eu" and
|
|
// "gcp-us" can be two ProviderConfigs of Type "gcp" with different GCP
|
|
// blocks. Exactly one of the type-specific blocks below should be set,
|
|
// matching Type.
|
|
type ProviderConfig struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"`
|
|
GCP *GCPConfig `json:"gcp,omitempty"`
|
|
KVM *KVMConfig `json:"kvm,omitempty"`
|
|
}
|
|
|
|
// setBlocks names the type-specific blocks that are non-nil, so validate can
|
|
// reject a block that doesn't match Type without a pairwise check per type.
|
|
func (p ProviderConfig) setBlocks() []string {
|
|
var out []string
|
|
if p.Kubernetes != nil {
|
|
out = append(out, "kubernetes")
|
|
}
|
|
if p.GCP != nil {
|
|
out = append(out, "gcp")
|
|
}
|
|
if p.KVM != nil {
|
|
out = append(out, "kvm")
|
|
}
|
|
return out
|
|
}
|
|
|
|
// KubernetesConfig configures the kubernetes-pod provider, which creates
|
|
// proxy Pods in the same cluster the operator itself runs in.
|
|
type KubernetesConfig struct {
|
|
// Image is the proxy container image. Default "ubuntu/squid:6.6-24.04_edge".
|
|
Image string `json:"image,omitempty"`
|
|
}
|
|
|
|
// GCPConfig configures a named GCP provider instance.
|
|
type GCPConfig struct {
|
|
// Project is the GCP project ID. Required.
|
|
Project string `json:"project"`
|
|
// Network is the VPC network name. Default "default".
|
|
Network string `json:"network,omitempty"`
|
|
// NetworkTag is the firewall/network tag applied to created instances.
|
|
// Default "proxy-operator".
|
|
NetworkTag string `json:"networkTag,omitempty"`
|
|
// DiskSizeGB is the boot disk size in GB. Default 10.
|
|
DiskSizeGB int64 `json:"diskSizeGb,omitempty"`
|
|
}
|
|
|
|
// LoadConfigFile reads and parses a providers-config file from disk.
|
|
func LoadConfigFile(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading providers config %s: %w", path, err)
|
|
}
|
|
return LoadConfig(data)
|
|
}
|
|
|
|
// LoadConfig parses and validates providers-config YAML. It fails fast:
|
|
// unknown provider types, duplicate names, and missing type-specific
|
|
// required fields are all load-time errors here, not runtime surprises
|
|
// discovered only when a provider is actually used.
|
|
func LoadConfig(data []byte) (*Config, error) {
|
|
var cfg Config
|
|
if err := yaml.UnmarshalStrict(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("parsing providers config: %w", err)
|
|
}
|
|
if err := cfg.validate(); err != nil {
|
|
return nil, fmt.Errorf("validating providers config: %w", err)
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
|
|
func (c *Config) validate() error {
|
|
if len(c.Providers) == 0 {
|
|
return fmt.Errorf("providers: at least one provider must be configured")
|
|
}
|
|
seen := make(map[string]bool, len(c.Providers))
|
|
for i, p := range c.Providers {
|
|
if p.Name == "" {
|
|
return fmt.Errorf("providers[%d]: name is required", i)
|
|
}
|
|
if seen[p.Name] {
|
|
return fmt.Errorf("providers[%d]: duplicate provider name %q", i, p.Name)
|
|
}
|
|
seen[p.Name] = true
|
|
|
|
prefix := fmt.Sprintf("providers[%d] %q", i, p.Name)
|
|
switch p.Type {
|
|
case "kubernetes", "gcp", "kvm":
|
|
case "":
|
|
return fmt.Errorf("%s: type is required", prefix)
|
|
default:
|
|
return fmt.Errorf("%s: unknown provider type %q", prefix, p.Type)
|
|
}
|
|
for _, block := range p.setBlocks() {
|
|
if block != p.Type {
|
|
return fmt.Errorf("%s: type is %s but a %s block is set", prefix, p.Type, block)
|
|
}
|
|
}
|
|
switch p.Type {
|
|
case "gcp":
|
|
if p.GCP == nil || p.GCP.Project == "" {
|
|
return fmt.Errorf("%s: gcp.project is required", prefix)
|
|
}
|
|
case "kvm":
|
|
if err := validateKVM(prefix, p.KVM); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|