Create/Get/Delete/ListByTag against real corev1.Pod objects in the same cluster the operator runs in, running an ubuntu/squid container -- picked by actually checking Docker Hub metadata (Canonical-published, rebuilt the same day this was decided, 50M+ pulls) rather than guessing an image reference. It's a public image, so kind nodes pull it directly with no build/load step. providerID is "<namespace>/<podName>", parsed via cache.SplitMetaNamespaceKey -- the same self-contained-providerID reasoning the plan already calls for on the GCP provider's zone-qualified IDs. Pod state maps to InstanceState with Succeeded/Failed/Unknown all collapsing to Terminated, since the reconciler already treats Stopped and Terminated identically; Running-without-PodIP maps to Provisioning so an empty IP is never published. The client is built internally via ctrl.GetConfig() (in-cluster or local kubeconfig, whichever applies), not threaded through the registry Constructor signature -- this is what lets `make run` against a local kind cluster and running in-cluster share the exact same code path with no provider-specific wiring in cmd/main.go. New() is deliberately untested (0% coverage): it's the one function that must never run under `go test`, since it would happily connect to whatever cluster the developer's kubeconfig points at. Tests construct Provider via an unexported newWithClient(client, cfg) instead. ListByTag lists Pods across every namespace (orphan GC needs to find every tagged Pod regardless of where it landed), which means this provider's RBAC has to be a ClusterRole rather than namespace-scoped -- flagged now, wired in Step 10. provider.Config gains KubernetesConfig (replacing MockConfig) and drops the FailWith*/fault-injection surface entirely, since that need is now served by a small in-test stub Provider for reconciler tests (Step 4), not a config-driven mechanism on a real provider package. Tests use sigs.k8s.io/controller-runtime/pkg/client/fake -- real Pod objects, the real client.Client interface -- at 77.6% coverage. make test green across the whole repo. Co-Authored-By: Claude <noreply@anthropic.com>
104 lines
3.3 KiB
Go
104 lines
3.3 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"`
|
|
}
|
|
|
|
// 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
|
|
|
|
switch p.Type {
|
|
case "kubernetes":
|
|
if p.GCP != nil {
|
|
return fmt.Errorf("providers[%d] %q: type is kubernetes but a gcp block is set", i, p.Name)
|
|
}
|
|
case "gcp":
|
|
if p.Kubernetes != nil {
|
|
return fmt.Errorf("providers[%d] %q: type is gcp but a kubernetes 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
|
|
}
|