The Provider interface (Create/Get/Delete/ListByTag), Instance, and CreateRequest that every cloud backend implements — kept independent of api/v1alpha1 so this package has no CRD-type coupling. Error taxonomy (ErrNotFound/ErrQuotaExceeded/ErrTransient/ErrPermanent) wrapped via a multi-error Unwrap() []error, so errors.Is and errors.As both work off the same value: the reconciler branches on classification, logs keep the underlying SDK error. Unclassified errors default to ErrTransient — retrying is always safer than latching Failed. Deterministic instance naming (SHA-256 -> base32 -> 16 chars, 22 total with the "proxy-" prefix) satisfying GCP's RFC1035 name rules with headroom, and idempotency-tested across 10k UIDs with zero collisions. --providers-config YAML parsing (config.go) with fail-fast validation: unknown type, duplicate name, missing gcp.project, mismatched type/config-block, and strict-mode rejection of unknown keys. internal/provider/registry/registry.go takes its type->constructor map as a parameter rather than hardcoding it, so the package has zero import on internal/provider/mock or internal/provider/gcp (neither exists yet — mock is Step 3, gcp is Step 8) and compiles today. Explicit wiring moves to the composition root in cmd/main.go (Step 10). Deferred internal/provider/metrics.go (the WithMetrics decorator) to Step 9, where the Prometheus vectors it needs actually get built — nothing in this step depends on it. internal/provider at 96.2% coverage, internal/provider/registry at 100%. make test green. Co-Authored-By: Claude <noreply@anthropic.com>
134 lines
4.4 KiB
Go
134 lines
4.4 KiB
Go
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
|
|
}
|