digitalocean

package
v1.801.472 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package digitalocean reads DigitalOcean's billing and infrastructure APIs. DO is our PRIMARY venue (a large promotional credit); this client turns the customer balance + billing history into money.Cents the finance aggregator folds into gross margin and runway, and exposes the account's physical inventory — droplets, block-storage volumes, DOKS clusters, load balancers — that the /v1/admin/infra board reads.

This is the ONE DigitalOcean client the admin plane uses. A new DO read is a method here calling the shared get/send primitive, never a second client.

Auth is a single personal-access token, DO_API_TOKEN, sourced from a KMSSecret on the cloud env — NEVER hard-coded. When the token is unset the client is not Ready and every read reports the honest not-configured state.

SIGN CONVENTION (from DO's public OpenAPI spec): GET /v2/customers/my/balance returns decimal-dollar strings; account_balance carries the accounts-receivable sign — POSITIVE = we OWE DO, NEGATIVE = we hold CREDIT. Our promo credit shows as a negative account balance, so credit-remaining = -Account. Dollars are converted to cents once at this edge; everything downstream is money.Cents.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Action

type Action struct {
	ID     int
	Status string
}

Action is a queued DO droplet action. DO performs a resize ASYNCHRONOUSLY, so a successful call means "accepted", not "done" — the id is what an operator polls.

type Balance

type Balance struct {
	Account     money.Cents
	MonthToDate money.Cents
	Usage       money.Cents
	At          string
}

Balance is the decoded /v2/customers/my/balance, in cents. Account carries DO's accounts-receivable sign (positive = owed to DO, negative = credit we hold).

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client reads DigitalOcean billing with a personal-access token.

func New

func New(token string) *Client

New builds a DO client against the public API.

func NewWithBase

func NewWithBase(base, token string) *Client

NewWithBase builds a DO client against base (a test may point it at a stub).

func (*Client) Balance

func (c *Client) Balance(ctx context.Context) (Balance, error)

Balance fetches the customer balance, converting every dollar string to cents.

func (*Client) Clusters

func (c *Client) Clusters(ctx context.Context) ([]Cluster, error)

Clusters lists ALL DOKS clusters. This is the authoritative denominator for the orphan analysis: a volume may only be called unreferenced once EVERY cluster here has been searched for a PV that claims it.

func (*Client) CreditIssued

func (c *Client) CreditIssued(ctx context.Context) (money.Cents, error)

CreditIssued reports the TOTAL promotional credit DigitalOcean has ever applied to this account, in cents, discovered from DO's own invoices.

WHY THIS IS DISCOVERED AND NOT DECLARED. The grant used to be a constant in finance/providers.go — `"do-ai": 2_600_000`. It was wrong, and being wrong is the normal state of a hand-entered number: nobody re-types it when the vendor applies another tranche or lets one expire. On 2026-07-28 the constant said $26,000, the operator believed $50,000, and DO's ledger showed $21,263.65 ever applied. Three numbers, no two agreeing, and the one nobody could check was the one the dashboard rendered.

DO knows this exactly, so ask DO. Every invoice carries the credit it consumed as a line item with product == "Credits" (ours read `Hatch Credit for: Techstars`), NEGATIVE because it offsets usage. Their absolute sum is the credit that has actually flowed. If DO applies the missing tranche tomorrow, this number moves on its own and no one has to remember.

NOT the wallet. Cash top-ups (DO "Payment" rows — ours were two Apple Pay entries totalling $4.00) are NOT credit and are deliberately excluded: mixing them in is what makes an exhausted grant look alive. Account balance already counts them; this counts only the promotional grant.

Cost: one invoice-list call plus one detail call per invoice. Callers should cache — the value changes at most once a month.

func (*Client) DeleteDroplet

func (c *Client) DeleteDroplet(ctx context.Context, dropletID int) error

DeleteDroplet destroys a droplet. Irreversible, and there is no snapshot-first undo for a droplet the way there is for a volume: callers MUST have proven the droplet is not a DOKS node first (see clients/admin/infra).

func (*Client) DeleteLoadBalancer

func (c *Client) DeleteLoadBalancer(ctx context.Context, lbID string) error

DeleteLoadBalancer destroys a load balancer. Irreversible, and it takes the public IP with it: callers MUST have proven no Kubernetes Service still targets it.

func (*Client) DeleteVolume

func (c *Client) DeleteVolume(ctx context.Context, volumeID string) error

DeleteVolume destroys a block-storage volume. Irreversible: callers MUST have proven the volume is referenced by no PV in any cluster first.

func (*Client) Droplets

func (c *Client) Droplets(ctx context.Context) ([]Droplet, error)

Droplets lists ALL droplets across the account.

func (*Client) History

func (c *Client) History(ctx context.Context, perPage int) ([]Entry, error)

History fetches recent billing history. Used only for the burn-down series; a failure is non-fatal to the caller (it renders the balance tiles with no series).

func (*Client) Kubeconfig

func (c *Client) Kubeconfig(ctx context.Context, clusterID string) ([]byte, error)

Kubeconfig fetches a cluster's admin kubeconfig. DO returns a token-based config against the cluster's public https endpoint (never an exec plugin), which the caller must still funnel through fleet.SafeRESTConfig before dialing.

func (*Client) LoadBalancers

func (c *Client) LoadBalancers(ctx context.Context) ([]LoadBalancer, error)

LoadBalancers lists ALL load balancers across the account.

func (*Client) Ready

func (c *Client) Ready() bool

Ready reports whether a DO token is present.

func (*Client) ResizeDroplet

func (c *Client) ResizeDroplet(ctx context.Context, dropletID int, size string, disk bool) (Action, error)

ResizeDroplet changes a droplet's plan.

disk=true makes the change PERMANENT AND IRREVERSIBLE: the disk grows and the droplet can never be resized down again. disk=false resizes CPU/RAM only and is reversible.

DO requires the droplet to be powered off; if it is not, DO refuses and its message is surfaced verbatim rather than being retried or worked around.

func (*Client) ResizeVolume

func (c *Client) ResizeVolume(ctx context.Context, volumeID, region string, gib int) (Action, error)

ResizeVolume grows a block-storage volume.

GROW ONLY, and not by choice: DigitalOcean has no shrink. It resizes the DEVICE and does nothing to the filesystem on it, so a volume Kubernetes manages must be grown through its PersistentVolumeClaim instead — that path does both, and leaves nothing declaring a stale capacity. See infra.ExpandPVC. This call is for the volumes Kubernetes does not manage, where the DigitalOcean API is the only thing that holds the size.

func (*Client) ScaleNodePool

func (c *Client) ScaleNodePool(ctx context.Context, clusterID, poolID, name string, count int) error

ScaleNodePool sets a node pool's node count. DO's update endpoint requires the pool's name alongside the count — omitting it clears the name, so it is always sent back.

func (*Client) SnapshotVolume

func (c *Client) SnapshotVolume(ctx context.Context, volumeID, name string) (Snapshot, error)

SnapshotVolume takes a point-in-time snapshot of a volume. This is the "undo" that makes a delete recoverable, so the delete path takes one FIRST by default.

func (*Client) Volumes

func (c *Client) Volumes(ctx context.Context) ([]Volume, error)

Volumes lists ALL block-storage volumes across the account. Capacity and attachment are real; per-volume fill is NOT exposed by DO and stays absent (honest) until a filesystem source reports it.

type Cluster

type Cluster struct {
	ID        string
	Name      string
	Region    string
	Version   string
	Status    string
	Pools     []NodePool
	CreatedAt string
}

Cluster is one DOKS cluster.

type Droplet

type Droplet struct {
	ID           int
	Name         string
	Region       string
	Status       string
	SizeSlug     string
	VCPUs        int
	MemoryMiB    int
	LocalDiskGiB int
	MonthlyCents money.Cents
	CreatedAt    string
	PrivateIP    string
	PublicIP     string
	Tags         []string
	VolumeIDs    []string
}

Droplet is one DO droplet. LocalDiskGiB is the droplet's own disk, which is INCLUDED in MonthlyCents — it is NOT separately billed, and conflating it with block storage is how a fleet appears to hold terabytes it never pays for.

type Entry

type Entry struct {
	Description string
	Amount      money.Cents
	Date        string
	Kind        string
	InvoiceID   string
}

Entry is one billing-history row (used to build the credit burn-down series).

type LoadBalancer

type LoadBalancer struct {
	ID           string
	Name         string
	Region       string
	Status       string
	IP           string
	SizeUnit     int
	MonthlyCents money.Cents
	DropletIDs   []int
}

LoadBalancer is one DO load balancer. DO does not price LBs in the API, so cost is derived from the billed unit count (see lbUnitCents).

type NodePool

type NodePool struct {
	ID    string
	Name  string
	Size  string
	Count int
}

NodePool is one DOKS node pool — the ONLY correct way to change a cluster's node count. DOKS owns the droplets in a pool: deleting or resizing one directly is undone by the pool controller, which recreates it.

type Snapshot

type Snapshot struct {
	ID      string
	Name    string
	SizeGiB int
}

Snapshot is a created block-storage snapshot.

type Volume

type Volume struct {
	ID         string
	Name       string
	Region     string
	SizeGiB    int
	DropletIDs []int
	// Tags carries DO's resource tags. DOKS stamps `k8s:<cluster-uuid>` on the volumes
	// it provisions, but that tag is ADVISORY ONLY — it survives cluster deletion and
	// is wrong often enough that it must never decide whether a volume is garbage. The
	// only sound liveness test is a PV cross-reference (see clients/admin/infra).
	Tags      []string
	CreatedAt string
}

Volume is one DO block-storage volume: capacity + attachment + region. DO's API gives capacity and which droplets a volume is attached to, but NOT fill % — the caller enriches fill only where a filesystem source (the datastore's own system.disks) reports it, and renders an honest "—" everywhere else.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL