Add the provider contract, error taxonomy, naming, and config (Step 2)
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>
This commit is contained in:
109
internal/provider/provider.go
Normal file
109
internal/provider/provider.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Package provider defines the contract every cloud provider backend
|
||||
// implements: Create/Get/Delete/ListByTag for a single VM, keyed by an
|
||||
// opaque providerID. Concrete implementations live in subpackages (mock,
|
||||
// gcp); this package has no dependency on any of them, so a type→
|
||||
// constructor registry can be assembled at the composition root (cmd/main.go)
|
||||
// without an import cycle.
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InstanceState is a provider's lifecycle state for a single VM.
|
||||
type InstanceState string
|
||||
|
||||
const (
|
||||
StateProvisioning InstanceState = "Provisioning"
|
||||
StateRunning InstanceState = "Running"
|
||||
StateStopped InstanceState = "Stopped"
|
||||
StateTerminated InstanceState = "Terminated"
|
||||
)
|
||||
|
||||
// GC contract: every cloud resource a provider creates must carry these two
|
||||
// labels/tags. Orphan GC (internal/gc) relies on both — Managed to find
|
||||
// resources it owns at all, UID to decide whether a resource is still
|
||||
// claimed by a live Proxy CR.
|
||||
const (
|
||||
LabelManaged = "proxy-operator-managed"
|
||||
LabelManagedYes = "true"
|
||||
LabelUID = "proxy-operator-uid"
|
||||
)
|
||||
|
||||
// Instance is a provider's view of a single VM.
|
||||
type Instance struct {
|
||||
// ID is the opaque providerID, stable for the life of the VM.
|
||||
ID string
|
||||
// IP is the VM's current address, empty until it's assigned one.
|
||||
IP string
|
||||
// State is the VM's current lifecycle state.
|
||||
State InstanceState
|
||||
// UID is the LabelUID value read back off the resource — the owning
|
||||
// Proxy CR's UID, or "" if the resource predates this label (shouldn't
|
||||
// happen for anything this operator created, but Get/ListByTag callers
|
||||
// must tolerate it rather than panic).
|
||||
UID string
|
||||
// CreatedAt is when the provider created the resource. Orphan GC uses
|
||||
// this to skip young instances that may still be mid-create, avoiding a
|
||||
// race with an in-flight Create whose status write hasn't landed yet.
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Placement is the provider-opaque placement/size configuration a Create
|
||||
// call needs. It deliberately does not import api/v1alpha1 — this package
|
||||
// stays independent of the CRD types; the reconciler maps
|
||||
// v1alpha1.PlacementSpec to this struct when calling Create. Providers may
|
||||
// ignore fields that don't apply to them.
|
||||
type Placement struct {
|
||||
Region string
|
||||
Zone string
|
||||
MachineType string
|
||||
Image string
|
||||
}
|
||||
|
||||
// CreateRequest carries everything a provider needs to create a VM.
|
||||
type CreateRequest struct {
|
||||
// Name is the deterministic instance name, already derived from the
|
||||
// owning CR's UID via NameFromUID. Create must be idempotent keyed on
|
||||
// this name: a repeat call after a crash must find the existing
|
||||
// instance rather than create a duplicate.
|
||||
Name string
|
||||
// UID is the owning Proxy CR's UID. Create must tag/label the created
|
||||
// resource with LabelUID=UID and LabelManaged=LabelManagedYes.
|
||||
UID string
|
||||
|
||||
Namespace string
|
||||
ProxyName string
|
||||
|
||||
Placement Placement
|
||||
// CloudInit is the already-resolved user-data (a Secret reference, if
|
||||
// used, has already been read by the caller).
|
||||
CloudInit string
|
||||
Port int32
|
||||
}
|
||||
|
||||
// Provider is the contract every cloud backend implements. Kept
|
||||
// deliberately minimal: this is the same interface five future providers
|
||||
// must satisfy.
|
||||
type Provider interface {
|
||||
// Create starts VM creation and returns as soon as the request is
|
||||
// submitted — it does not block until the VM is running. Must be
|
||||
// idempotent by req.Name, so a repeat call after a crash finds the
|
||||
// existing VM instead of duplicating it.
|
||||
Create(ctx context.Context, req CreateRequest) (providerID string, err error)
|
||||
|
||||
// Get returns the current state of a previously created instance.
|
||||
// Returning (nil, ErrNotFound) is a normal, expected outcome — it
|
||||
// drives the reconciler's replacement and adoption logic, not an
|
||||
// exceptional condition.
|
||||
Get(ctx context.Context, providerID string) (*Instance, error)
|
||||
|
||||
// Delete is idempotent: deleting an instance that no longer exists is
|
||||
// not an error.
|
||||
Delete(ctx context.Context, providerID string) error
|
||||
|
||||
// ListByTag returns every instance this operator has ever tagged with
|
||||
// LabelManaged=LabelManagedYes, for orphan GC.
|
||||
ListByTag(ctx context.Context) ([]Instance, error)
|
||||
}
|
||||
Reference in New Issue
Block a user