Documentation
¶
Overview ¶
Package provision creates and tears down cloud VMs at a provider (Hetzner, DigitalOcean, Azure, GCP) so a fresh node can join the fleet through the existing join-token enrollment flow, without an operator manually clicking through a cloud console first.
Index ¶
- func RenderCloudInit(p CloudInitParams) (string, error)
- type AWS
- func (a *AWS) CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
- func (a *AWS) DeleteServer(ctx context.Context, id string) error
- func (a *AWS) GetServer(ctx context.Context, id string) (ServerStatus, string, error)
- func (a *AWS) ListRegions(ctx context.Context) ([]Region, error)
- func (a *AWS) ListSizes(_ context.Context, _ string) ([]Size, error)
- type AWSCredentials
- type Azure
- func (a *Azure) CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
- func (a *Azure) DeleteServer(ctx context.Context, id string) error
- func (a *Azure) GetServer(ctx context.Context, id string) (ServerStatus, string, error)
- func (a *Azure) ListRegions(ctx context.Context) ([]Region, error)
- func (a *Azure) ListSizes(ctx context.Context, region string) ([]Size, error)
- type CloudInitParams
- type CreateOpts
- type DigitalOcean
- func (d *DigitalOcean) CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
- func (d *DigitalOcean) DeleteServer(ctx context.Context, id string) error
- func (d *DigitalOcean) GetServer(ctx context.Context, id string) (ServerStatus, string, error)
- func (d *DigitalOcean) ListRegions(ctx context.Context) ([]Region, error)
- func (d *DigitalOcean) ListSizes(ctx context.Context, region string) ([]Size, error)
- type GCP
- func (g *GCP) CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
- func (g *GCP) DeleteServer(ctx context.Context, id string) error
- func (g *GCP) GetServer(ctx context.Context, id string) (ServerStatus, string, error)
- func (g *GCP) ListRegions(ctx context.Context) ([]Region, error)
- func (g *GCP) ListSizes(ctx context.Context, zone string) ([]Size, error)
- type Hetzner
- func (h *Hetzner) CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
- func (h *Hetzner) DeleteServer(ctx context.Context, id string) error
- func (h *Hetzner) GetServer(ctx context.Context, id string) (ServerStatus, string, error)
- func (h *Hetzner) ListRegions(ctx context.Context) ([]Region, error)
- func (h *Hetzner) ListSizes(ctx context.Context, region string) ([]Size, error)
- type ProviderError
- type Provisioner
- type Region
- type ServerStatus
- type Size
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RenderCloudInit ¶
func RenderCloudInit(p CloudInitParams) (string, error)
RenderCloudInit renders the user-data cloud-init runs on first boot: it installs Docker if missing (install.sh's own get.docker.com convention), then runs the agent as a systemd-managed container that joins the control plane with a one-time token. The join token and CA fingerprint go in a root-only env file, not container run args, so they don't show up in `ps`; they remain visible via `docker inspect`, a known limitation documented in docs/node-provisioning.md. The agent runs as root (--user 0:0, it needs docker.sock and NET_ADMIN), so the bind-mounted identity directory is created root-owned with mode 0700.
Types ¶
type AWS ¶
type AWS struct {
// contains filtered or unexported fields
}
AWS implements Provisioner against the EC2 API (ec2API, aws_network.go), the same aws-sdk-go-v2 dependency internal/objectstore (S3) and internal/email (SES) already use. Unlike Hetzner/DigitalOcean's single bearer token and location-agnostic server id, an EC2 instance id is only valid in the region it was created in, so client builds a region-scoped client lazily per call rather than storing one fixed *ec2.Client.
func NewAWS ¶
func NewAWS(creds AWSCredentials, shortName string) *AWS
NewAWS returns an AWS provisioner authenticating with creds. shortName namespaces the security group it creates (brand.Brand.ShortName; the product name never appears in source, so this is passed in, not looked up, the convention internal/network.WithShortName already establishes).
func NewAWSFromToken ¶
NewAWSFromToken builds an AWS provisioner from the JSON-encoded AWSCredentials a NodeProviderSecrets lookup returns.
func (*AWS) CreateServer ¶
func (a *AWS) CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
CreateServer launches an EC2 instance: resolves the current Ubuntu AMI, the account's default VPC and subnet, ensures the shared no-inbound security group exists, and runs one instance with opts as its user-data.
func (*AWS) DeleteServer ¶
DeleteServer terminates the EC2 instance. The shared security group ensureSecurityGroup creates or reuses is never deleted here: it's shared across every AWS-provisioned node, not created fresh per instance, so a single DeleteServer call is never its sole owner and cannot safely tell whether other nodes still depend on it. A group ensureDedicatedSSHSecurityGroup created for this one instance (CreateOpts.AllowSSHInbound) is a different story: it's tagged as dedicated at creation, so cleanupDedicatedSecurityGroups can safely delete it once confirmed no other instance references it. No key pair is created either way (there is no SSH key input yet), so there is nothing to clean up there.
func (*AWS) ListRegions ¶
ListRegions calls EC2 DescribeRegions.
func (*AWS) ListSizes ¶
ListSizes returns awsCuratedSizes: a static list, not a live API call (unlike Hetzner/DigitalOcean, EC2's own catalog is too large to be a useful picker, see awsCuratedSizes). region is accepted to satisfy Provisioner but unused: every listed type is available in essentially every commercial AWS region.
type AWSCredentials ¶
type AWSCredentials struct {
AccessKeyID string `json:"access_key_id,omitempty"`
SecretAccessKey string `json:"secret_access_key,omitempty"`
SessionToken string `json:"session_token,omitempty"`
Region string `json:"region,omitempty"`
// RoleARN, when set, is assumed via STS AssumeRole on top of
// whichever base credentials resolve below: the base credentials
// then only need sts:AssumeRole on this one ARN, not direct EC2
// permissions, matching least-privilege IAM practice.
RoleARN string `json:"role_arn,omitempty"`
// UseAmbientCredentials, with AccessKeyID/SecretAccessKey both
// empty, resolves this control plane's own AWS identity (env vars,
// shared config, or an EC2 instance profile) instead of a stored
// static key: only meaningful when the control plane itself runs on
// AWS. Full AssumeRoleWithWebIdentity (OIDC federation, the way
// GitHub Actions authenticates to AWS) is not implemented: it needs
// this control plane to be its own trusted OIDC token issuer, which
// does not exist. RoleARN plus a static key, or plus this ambient
// chain, are the two supported ways to avoid a long-lived key with
// direct EC2 permissions.
UseAmbientCredentials bool `json:"use_ambient_credentials,omitempty"`
}
AWSCredentials is the JSON shape node-provider/aws's stored credential value holds under NodeProviderTokenEnvKey (store.NodeProviderSecretsKey): EC2 has no single bearer token the way Hetzner and DigitalOcean do, so that envKey carries this struct JSON-encoded instead of a raw string.
type Azure ¶
type Azure struct {
// contains filtered or unexported fields
}
Azure implements Provisioner against the Azure Resource Manager REST API (https://learn.microsoft.com/en-us/rest/api/compute/), OAuth2 client-credentials auth, no SDK. Unlike Hetzner/DigitalOcean's static bearer token, every request needs a fresh access token: httpClient's tokenFunc seam (httpclient.go) resolves one from ts on each call.
func NewAzure ¶
NewAzure returns an Azure provisioner authenticating with credentialJSON (see azureCredential): a client secret by default, or workload identity federation when credentialJSON sets federated_token_file instead.
func (*Azure) CreateServer ¶
func (a *Azure) CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
CreateServer provisions a resource group (idempotent, never deleted), a shared per-region VNet/subnet (idempotent, never deleted), then a public IP, NIC and virtual machine unique to this server. No network security group is created: a Standard SKU public IP is closed to inbound traffic by default unless an NSG explicitly allows it, so this matches the "no inbound ports" default the other providers get from their own platform defaults.
func (*Azure) DeleteServer ¶
DeleteServer deletes only the virtual machine. Its NIC, public IP and OS disk were all created with deleteOption "Delete" (createVM, createNIC, createPublicIP), which Azure documents as cascading their deletion from this one call; the shared VNet/subnet is left in place for other servers in the same region. This cascade was not verified against a real Azure subscription, only against fakes (see azure_test.go).
func (*Azure) GetServer ¶
GetServer calls GET .../virtualMachines/{id} plus a second call for the public IP's address, id being the VM name (resourceGroup and subscriptionID are fixed per Azure instance, not part of id).
func (*Azure) ListRegions ¶
ListRegions calls GET /subscriptions/{sub}/locations, keeping only physical regions (Azure also lists "Logical" entries like paired DR regions that aren't valid CreateServer targets).
type CloudInitParams ¶
type CloudInitParams struct {
ControlPlaneAddr string
JoinToken string
CAFingerprint string
NodeName string
// AgentImage overrides defaultAgentImageRepo:tag, for an operator who
// mirrors the image elsewhere. Empty uses the default repo at
// AgentVersion.
AgentImage string
AgentVersion string
// MeshEnabled gives the agent container the TUN device and NET_ADMIN
// its WireGuard device needs.
MeshEnabled bool
}
CloudInitParams is everything RenderCloudInit needs to produce a first-boot script for a freshly created server.
type CreateOpts ¶
type CreateOpts struct {
Region string
Size string
Name string
// UserData is the cloud-init script run on first boot.
UserData string
// AllowSSHInbound opts into an inbound rule for TCP 22, off by
// default to match this platform's "no inbound ports needed" node
// architecture (CLAUDE.md 4.3): the agent always dials out. Only AWS
// currently acts on this; other providers ignore it, the same way
// AWS.ListSizes ignores the region parameter other providers use.
AllowSSHInbound bool
}
CreateOpts describes the server CreateServer should provision.
type DigitalOcean ¶
type DigitalOcean struct {
// contains filtered or unexported fields
}
DigitalOcean implements Provisioner against the DigitalOcean API v2 (https://docs.digitalocean.com/reference/api/), token auth, no SDK.
func NewDigitalOcean ¶
func NewDigitalOcean(token string) *DigitalOcean
NewDigitalOcean returns a DigitalOcean provisioner authenticating with token.
func (*DigitalOcean) CreateServer ¶
func (d *DigitalOcean) CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
CreateServer calls POST /v2/droplets.
func (*DigitalOcean) DeleteServer ¶
func (d *DigitalOcean) DeleteServer(ctx context.Context, id string) error
DeleteServer calls DELETE /v2/droplets/{id}.
func (*DigitalOcean) GetServer ¶
func (d *DigitalOcean) GetServer(ctx context.Context, id string) (ServerStatus, string, error)
GetServer calls GET /v2/droplets/{id}.
func (*DigitalOcean) ListRegions ¶
func (d *DigitalOcean) ListRegions(ctx context.Context) ([]Region, error)
ListRegions calls GET /v2/regions, following every page.
type GCP ¶
type GCP struct {
// contains filtered or unexported fields
}
GCP implements Provisioner against the Compute Engine REST API (https://cloud.google.com/compute/docs/reference/rest/v1), a service account JSON key exchanged for an OAuth2 access token via JWT bearer auth, no SDK (golang.org/x/oauth2/google, not google.golang.org/api).
func NewGCP ¶
NewGCP returns a GCP provisioner authenticating with credentialJSON, a service account key's raw JSON content. The project ID is read from the key itself, not a separate field.
func (*GCP) CreateServer ¶
func (g *GCP) CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
CreateServer calls POST .../zones/{zone}/instances with an ephemeral external IP (no reserved address to clean up later) and an auto-deleting boot disk, so DeleteServer needs only the one instance delete call. The insert call is asynchronous at the operation level, but the instance resource itself exists as soon as this call returns, in PROVISIONING state; its IP isn't assigned yet, so ipAddr is always empty here, matching CreateOpts' own doc comment for a provider that assigns one later. serverID is "{zone}/{name}": unlike Azure's fixed per-account resource group, GCP's zone varies per server and every later call needs it, so it has to travel in the id.
func (*GCP) DeleteServer ¶
DeleteServer calls DELETE .../zones/{zone}/instances/{name}. The boot disk (autoDelete: true) and the ephemeral external IP go with it; no separate cleanup call is needed.
func (*GCP) GetServer ¶
GetServer calls GET .../zones/{zone}/instances/{name}, id being the "{zone}/{name}" CreateServer returned.
func (*GCP) ListRegions ¶
ListRegions returns GCP zones (e.g. us-central1-a), not regions: Compute Engine places an instance in a zone, and CreateServer/ GetServer/DeleteServer all need that zone, so this interface's "region" maps to a GCE zone for this provider.
func (*GCP) ListSizes ¶
ListSizes calls GET .../zones/{zone}/machineTypes. Disk size is a separate resource in GCE, not part of a machine type, so every returned Size has Disk 0; CreateServer picks gcpBootDiskGB itself. GCP's own billing catalog is a separate API this package doesn't call, so PriceMonthly is always empty here too.
type Hetzner ¶
type Hetzner struct {
// contains filtered or unexported fields
}
Hetzner implements Provisioner against the Hetzner Cloud API (https://docs.hetzner.cloud), token auth, no SDK.
func NewHetzner ¶
NewHetzner returns a Hetzner provisioner authenticating with token.
func (*Hetzner) CreateServer ¶
func (h *Hetzner) CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
CreateServer calls POST /v1/servers.
func (*Hetzner) DeleteServer ¶
DeleteServer calls DELETE /v1/servers/{id}.
func (*Hetzner) ListRegions ¶
ListRegions calls GET /v1/locations, following every page.
type ProviderError ¶
ProviderError is a non-2xx response from a provider's API, carrying the HTTP status so callers (and tests) can distinguish "not found" from a generic failure without parsing each provider's own error body shape.
func (*ProviderError) Error ¶
func (e *ProviderError) Error() string
type Provisioner ¶
type Provisioner interface {
ListRegions(ctx context.Context) ([]Region, error)
ListSizes(ctx context.Context, region string) ([]Size, error)
// CreateServer returns the provider's own server id and, when the
// provider assigns one immediately, its public IPv4 address (empty
// until GetServer reports one, for providers that assign it later).
CreateServer(ctx context.Context, opts CreateOpts) (serverID, ipAddr string, err error)
GetServer(ctx context.Context, id string) (status ServerStatus, ipAddr string, err error)
DeleteServer(ctx context.Context, id string) error
}
Provisioner creates and manages servers at a cloud provider. hetzner.go, digitalocean.go, azure.go and gcp.go are the real implementations; all four talk to their provider's REST API directly over net/http, no SDK.
type ServerStatus ¶
type ServerStatus string
ServerStatus is a provider-reported server lifecycle state, normalized across providers into a small common vocabulary.
const ( ServerStatusPending ServerStatus = "pending" ServerStatusRunning ServerStatus = "running" ServerStatusError ServerStatus = "error" )
The three server states GetServer normalizes every provider's own status string into.
type Size ¶
type Size struct {
ID string
Name string
VCPUs int
Memory int // MB
Disk int // GB
// PriceMonthly is the provider's own listed monthly price, in its
// native currency string. Empty when the provider does not return a
// price for this size.
PriceMonthly string
Currency string
}
Size is one server size/plan a provider offers.