Files
egress-proxies-operator/internal/provider/provider.go
Jan Novak a4a483acbc 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>
2026-08-07 22:44:22 +02:00

110 lines
4.1 KiB
Go

// 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)
}