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 ¶
- type Action
- type Balance
- type Client
- func (c *Client) Balance(ctx context.Context) (Balance, error)
- func (c *Client) Clusters(ctx context.Context) ([]Cluster, error)
- func (c *Client) CreditIssued(ctx context.Context) (money.Cents, error)
- func (c *Client) DeleteDroplet(ctx context.Context, dropletID int) error
- func (c *Client) DeleteLoadBalancer(ctx context.Context, lbID string) error
- func (c *Client) DeleteVolume(ctx context.Context, volumeID string) error
- func (c *Client) Droplets(ctx context.Context) ([]Droplet, error)
- func (c *Client) History(ctx context.Context, perPage int) ([]Entry, error)
- func (c *Client) Kubeconfig(ctx context.Context, clusterID string) ([]byte, error)
- func (c *Client) LoadBalancers(ctx context.Context) ([]LoadBalancer, error)
- func (c *Client) Ready() bool
- func (c *Client) ResizeDroplet(ctx context.Context, dropletID int, size string, disk bool) (Action, error)
- func (c *Client) ResizeVolume(ctx context.Context, volumeID, region string, gib int) (Action, error)
- func (c *Client) ScaleNodePool(ctx context.Context, clusterID, poolID, name string, count int) error
- func (c *Client) SnapshotVolume(ctx context.Context, volumeID, name string) (Snapshot, error)
- func (c *Client) Volumes(ctx context.Context) ([]Volume, error)
- type Cluster
- type Droplet
- type Entry
- type LoadBalancer
- type NodePool
- type Snapshot
- type Volume
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Action ¶
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 ¶
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 NewWithBase ¶
NewWithBase builds a DO client against base (a test may point it at a stub).
func (*Client) Balance ¶
Balance fetches the customer balance, converting every dollar string to cents.
func (*Client) Clusters ¶
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 ¶
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 ¶
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 ¶
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 ¶
DeleteVolume destroys a block-storage volume. Irreversible: callers MUST have proven the volume is referenced by no PV in any cluster first.
func (*Client) History ¶
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 ¶
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) 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 ¶
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.
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 ¶
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 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.