package provider import ( "fmt" "os" "sigs.k8s.io/yaml" ) // Fail-with classes accepted by MockConfig.FailWith, shared with the mock // provider's fault injection so the two sides never drift on the string // values. const ( FailWithNotFound = "notfound" FailWithQuota = "quota" FailWithTransient = "transient" FailWithPermanent = "permanent" ) var validFailClasses = map[string]bool{ FailWithNotFound: true, FailWithQuota: true, FailWithTransient: true, FailWithPermanent: true, } // 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"` Mock *MockConfig `json:"mock,omitempty"` GCP *GCPConfig `json:"gcp,omitempty"` } // MockConfig configures the in-memory mock provider. type MockConfig struct { // ProvisionDelaySeconds is how long a created instance reports // Provisioning before Running. Default 5. ProvisionDelaySeconds int32 `json:"provisionDelaySeconds,omitempty"` // DeleteDelaySeconds is how long a deleted instance reports Terminated // before Get starts returning ErrNotFound. Default 1. DeleteDelaySeconds int32 `json:"deleteDelaySeconds,omitempty"` // FailNextCreates makes the next N Create calls fail with FailWith, // for exercising the reconciler's error handling in demos. FailNextCreates int `json:"failNextCreates,omitempty"` // FailWith selects the error class injected failures return: one of // FailWithNotFound/FailWithQuota/FailWithTransient/FailWithPermanent. // Default FailWithTransient. FailWith string `json:"failWith,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 switch p.Type { case "mock": if p.GCP != nil { return fmt.Errorf("providers[%d] %q: type is mock but a gcp block is set", i, p.Name) } if p.Mock != nil && p.Mock.FailWith != "" && !validFailClasses[p.Mock.FailWith] { return fmt.Errorf("providers[%d] %q: unknown mock.failWith %q", i, p.Name, p.Mock.FailWith) } case "gcp": if p.Mock != nil { return fmt.Errorf("providers[%d] %q: type is gcp but a mock block is set", i, p.Name) } if p.GCP == nil || p.GCP.Project == "" { return fmt.Errorf("providers[%d] %q: gcp.project is required", i, p.Name) } case "": return fmt.Errorf("providers[%d] %q: type is required", i, p.Name) default: return fmt.Errorf("providers[%d] %q: unknown provider type %q", i, p.Name, p.Type) } } return nil }