Documentation
¶
Overview ¶
analytics.go is the ONE path by which visor rolls compute fleet/spend events into hanzoai/datastore (hanzo.compute_usage, ClickHouse) — the ANALYTICAL plane that mirrors, but is orthogonal to, the OPERATIONAL commerce ledger (tenant-data-hierarchy HIP). Commerce metering DEBITS an org's balance; this only RECORDS a fleet event for unified, cross-tenant rollups that admin.hanzo.ai reads by org / app / project — and by kind across visor's compute spectrum (machine, bot, cluster, nodepool, container, function).
Every emit is best-effort and fire-and-forget: the write runs in its own goroutine on a short-lived context and swallows all errors, so an unreachable or slow datastore NEVER blocks or fails a launch, hourly sweep, or destroy. Ingest is the datastore's ClickHouse HTTP interface (INSERT ... FORMAT JSONEachRow on the same DATASTORE_URL surface the other emitters use), so there is no new client dependency — net/http only.
cloud_cost.go is the ONE seam for reading a BYOC cloud account's spend — the input to fleet-billing tier (a), where connected cloud accounts are billed 1% of their cloud spend. Each cloud implements CloudCostReader with that cloud's real cost API (AWS Cost Explorer, Azure Cost Management, GCP BigQuery billing export, DigitalOcean billing). Readers are STATELESS pure API reads (this package must not import object — object imports service); the incremental "bill only new spend" watermark + idempotency live in the billing orchestrator, which owns the persistent cursor.
HONESTY CONTRACT: a reader returns ErrCostUnavailable when the stored credentials lack the cost-read scope that cloud needs (documented per cloud). The collector then SKIPS that account — no fee — rather than inventing a number. No spend is ever fabricated.
Package service — compute.go is Hanzo's resell compute surface over a single HOUSE DigitalOcean account. It is distinct from the per-owner "bring your own cloud" Provider path (machine_cloud.go): here ONE Hanzo DO token (from KMS) backs every tenant, and droplets are namespaced by an org tag so list/get/ delete are scoped to the caller's org at the DigitalOcean layer — never the whole account. The catalog (regions/sizes/GPUs) is fetched once and cached so the dashboard is fast and DO is not hammered.
metering.go is the ONE commerce metering path for resell compute. Both the launch debit (controllers/compute.go) and the recurring hourly debit (MeterRunningMachines, driven by task/ticker) build their client with NewMeteringClient and price with PriceToCents — there is no second metering path for /v1 machines. (The legacy billing/reporter.go meters DOKS NODE POOLS on a different, node-pool-specific event API; it is orthogonal and untouched.)
tenant.go is the ONE home for the org+project tenancy attribution shared by the resell compute surface (compute.go), the metering path (metering.go), and fleet billing (fleet_billing.go): how an org and a project are recovered from a machine's tags, encoded into a metering line, and validated so neither can ever corrupt the comma/colon-joined tag read-back or the commerce billing key.
The org is the tenant boundary and the debit destination; the project is a second attribution dimension WITHIN the org. The gateway mints X-Project-Id (like X-Org-Id); the empty project is the org's DEFAULT project and preserves today's behavior exactly — no project tag is written and the metering actor stays the bare org, so every keyed surface is backward-compatible.
Index ¶
- Constants
- Variables
- func AnalyticsConfigured() bool
- func CanonicalKind(kind string) string
- func ComputeConfigured() bool
- func DeleteOrgKubernetesCluster(org, id string) error
- func DeleteOrgMachine(org, id string) error
- func EmitCompute(org, event string, m *Machine, priceCents int64)
- func EmitComputeEvent(ev ComputeEvent)
- func HanzoPrice(doPrice float64, isGPU bool) float64
- func IsNotFound(err error) bool
- func MachineApp(m *Machine) string
- func MachineKind(m *Machine) string
- func MachineProject(m *Machine) string
- func MeterActor(org, project string) string
- func MeterRunningMachines(ctx context.Context)
- func MeteringConfigured() bool
- func NewMeteringClient(org string) *metering.Client
- func NormalizeProject(project string) string
- func PriceToCents(price float64) int64
- func SetKind(spec *CreateMachineSpec, kind string)
- func SetScope(spec *CreateMachineSpec, app, project string)
- type CloudCostReader
- type ComputeEvent
- type Cpu
- type CreateClusterNodePool
- type CreateClusterSpec
- type CreateMachineSpec
- type CreateNodePoolSpec
- type CreateVolumeSpec
- type DOKSClient
- func (c *DOKSClient) CreateCluster(ctx context.Context, spec *CreateClusterSpec, tags []string) (*KubernetesCluster, error)
- func (c *DOKSClient) CreateNodePool(spec *CreateNodePoolSpec) (*NodePool, error)
- func (c *DOKSClient) DeleteCluster(ctx context.Context, id string) error
- func (c *DOKSClient) DeleteNodePool(poolID string) error
- func (c *DOKSClient) GetCluster(ctx context.Context, id string) (*KubernetesClusterDetail, error)
- func (c *DOKSClient) GetNodePool(poolID string) (*NodePool, error)
- func (c *DOKSClient) ListClusters(ctx context.Context) ([]*KubernetesCluster, error)
- func (c *DOKSClient) ListNodePools() ([]*NodePool, error)
- func (c *DOKSClient) NodeMachines(ctx context.Context) ([]*Machine, error)
- func (c *DOKSClient) RecycleNodePoolNodes(poolID string, nodeIDs []string) error
- func (c *DOKSClient) UpdateNodePool(poolID string, spec *CreateNodePoolSpec) (*NodePool, error)
- type GPUSpec
- type ImageInfo
- type KubernetesCluster
- type KubernetesClusterDetail
- type Machine
- func GetOrgMachine(org, id string) (*Machine, error)
- func LaunchOrgMachine(org, project string, spec *CreateMachineSpec) (*Machine, error)
- func ListOrgKubernetesNodes(org string) ([]*Machine, error)
- func ListOrgMachines(org, project string) ([]*Machine, error)
- func ListRunningHouseMachines() ([]*Machine, error)
- type MachineAliyunClient
- func (client MachineAliyunClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
- func (client MachineAliyunClient) GetMachine(name string) (*Machine, error)
- func (client MachineAliyunClient) GetMachines() ([]*Machine, error)
- func (client MachineAliyunClient) UpdateMachineState(name string, state string) (bool, string, error)
- type MachineAwsClient
- func (client MachineAwsClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
- func (client MachineAwsClient) GetMachine(name string) (*Machine, error)
- func (client MachineAwsClient) GetMachines() ([]*Machine, error)
- func (client MachineAwsClient) UpdateMachineState(name string, state string) (bool, string, error)
- type MachineAzureClient
- func (client MachineAzureClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
- func (client MachineAzureClient) GetMachine(name string) (*Machine, error)
- func (client MachineAzureClient) GetMachines() ([]*Machine, error)
- func (client MachineAzureClient) UpdateMachineState(name string, state string) (bool, string, error)
- type MachineClientInterface
- type MachineDigitalOceanClient
- func (client MachineDigitalOceanClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
- func (client MachineDigitalOceanClient) DeleteMachine(name string) error
- func (client MachineDigitalOceanClient) GetMachine(name string) (*Machine, error)
- func (client MachineDigitalOceanClient) GetMachines() ([]*Machine, error)
- func (client MachineDigitalOceanClient) UpdateMachineState(name string, state string) (bool, string, error)
- type MachineGcpClient
- func (client MachineGcpClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
- func (client MachineGcpClient) GetMachine(name string) (*Machine, error)
- func (client MachineGcpClient) GetMachines() ([]*Machine, error)
- func (client MachineGcpClient) UpdateMachineState(name string, state string) (bool, string, error)
- type MachineHetznerClient
- func (client MachineHetznerClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
- func (client MachineHetznerClient) GetMachine(name string) (*Machine, error)
- func (client MachineHetznerClient) GetMachines() ([]*Machine, error)
- func (client MachineHetznerClient) UpdateMachineState(name string, state string) (bool, string, error)
- type MachineKvmClient
- func (client MachineKvmClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
- func (client MachineKvmClient) GetMachine(name string) (*Machine, error)
- func (client MachineKvmClient) GetMachines() ([]*Machine, error)
- func (client MachineKvmClient) UpdateMachineState(name string, state string) (bool, string, error)
- type MachineLightsailClient
- func (client MachineLightsailClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
- func (client MachineLightsailClient) GetMachine(name string) (*Machine, error)
- func (client MachineLightsailClient) GetMachines() ([]*Machine, error)
- func (client MachineLightsailClient) UpdateMachineState(name string, state string) (bool, string, error)
- type MachinePveClient
- func (client MachinePveClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
- func (client MachinePveClient) GetMachine(name string) (*Machine, error)
- func (client MachinePveClient) GetMachines() ([]*Machine, error)
- func (client MachinePveClient) UpdateMachineState(name string, state string) (bool, string, error)
- type MachineVmwareClient
- func (client MachineVmwareClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
- func (client MachineVmwareClient) GetMachine(name string) (*Machine, error)
- func (client MachineVmwareClient) GetMachines() ([]*Machine, error)
- func (client MachineVmwareClient) UpdateMachineState(name string, state string) (bool, string, error)
- type NodeInfo
- type NodePool
- type RegionInfo
- type SizeInfo
- type VirtualMachine
- type VirtualMachinePath
- type Volume
- type VolumeClientInterface
- type VolumeDigitalOceanClient
- func (c *VolumeDigitalOceanClient) AttachVolume(volumeName string, machineName string) error
- func (c *VolumeDigitalOceanClient) CreateVolume(spec *CreateVolumeSpec) (*Volume, error)
- func (c *VolumeDigitalOceanClient) DeleteVolume(name string) error
- func (c *VolumeDigitalOceanClient) DetachVolume(volumeName string) error
- func (c *VolumeDigitalOceanClient) GetVolume(name string) (*Volume, error)
- func (c *VolumeDigitalOceanClient) GetVolumes() ([]*Volume, error)
- func (c *VolumeDigitalOceanClient) ResizeVolume(volumeName string, sizeGB int) error
- type VolumeHetznerClient
- func (c *VolumeHetznerClient) AttachVolume(volumeName string, machineName string) error
- func (c *VolumeHetznerClient) CreateVolume(spec *CreateVolumeSpec) (*Volume, error)
- func (c *VolumeHetznerClient) DeleteVolume(name string) error
- func (c *VolumeHetznerClient) DetachVolume(volumeName string) error
- func (c *VolumeHetznerClient) GetVolume(name string) (*Volume, error)
- func (c *VolumeHetznerClient) GetVolumes() ([]*Volume, error)
- func (c *VolumeHetznerClient) ResizeVolume(volumeName string, sizeGB int) error
Constants ¶
const ( ComputeLaunched = "launched" ComputeRunning = "running" ComputeDestroyed = "destroyed" )
Compute event kinds — the values of the `event` column. A launched row is written at provision, a running row each hour a machine stays up (alongside the recurring meter), and a destroyed row at teardown.
const ( KindMachine = "machine" KindBot = "bot" KindCluster = "cluster" KindNodePool = "nodepool" KindContainer = "container" KindFunction = "function" )
Compute kinds — the values of the `kind` LowCardinality(String) lens in hanzo.compute_usage, spanning visor's compute spectrum:
machine — a raw droplet/VM (no agent) bot — a machine running the @hanzo/bot agent (gw.hanzo.bot) cluster — a K8s cluster (service/doks.go) nodepool — a K8s node pool (object/node_pool.go) container — a container workload function — a FaaS function (hanzoai/functions)
Every bot is a machine with the agent role; not every machine is a bot. admin.hanzo.ai renders one lens per kind over the one table. Only machine and bot emit today; cluster/nodepool/container/function land later on this SAME table + kind — no schema migration needed (the column is open-ended).
Variables ¶
ErrCostUnavailable means this provider's spend cannot be read with the configured credentials/scope. The collector skips the provider (no fee); it is NOT an error condition to alert on, just "cost-read not wired for this account".
Functions ¶
func AnalyticsConfigured ¶
func AnalyticsConfigured() bool
AnalyticsConfigured reports whether compute events will actually be emitted — true exactly when DATASTORE_URL is set. Absent ⇒ EmitCompute is a no-op.
func CanonicalKind ¶
CanonicalKind normalizes an arbitrary kind string to the known compute spectrum, falling back to machine for anything unrecognized (including empty). Applied on every WRITE (SetKind) and READ (EmitCompute), so a missing or garbage tag safely resolves to machine and the LowCardinality column only ever sees a known value. Fleet launches set bot explicitly; a raw single launch carries no kind and so resolves to machine.
func ComputeConfigured ¶
func ComputeConfigured() bool
ComputeConfigured reports whether the house DO token is present, so callers can return a clean 503 instead of a cryptic client error.
func DeleteOrgKubernetesCluster ¶
DeleteOrgKubernetesCluster destroys a house cluster by id, but ONLY if it carries the caller org's hanzo-org tag — the same isolation as GetOrgKubernetesCluster, so a tenant can never delete another tenant's cluster. An already-absent cluster is a no-op success (idempotent delete).
func DeleteOrgMachine ¶
DeleteOrgMachine deletes a machine only after confirming it belongs to org.
func EmitCompute ¶
EmitCompute records one fleet event for machine m into the datastore. org is the authoritative owner (from IAM on launch; recovered from the machine's own tag on the recurring/destroy paths); app/project/kind are recovered from m's tags; priceCents is the resale price for this event's hour (0 for destroyed). It is fire-and-forget: a no-op when analytics is unconfigured or m is nil, otherwise the write is handed to a goroutine so it NEVER delays or fails the caller.
func EmitComputeEvent ¶
func EmitComputeEvent(ev ComputeEvent)
EmitComputeEvent is the ONE kind-agnostic emit: it records a single compute fleet event of ANY kind (machine, bot, cluster, nodepool, container, function) into the datastore. It canonicalizes the kind and stamps ts when the caller left it empty, then — like every emit — is best-effort and fire-and-forget: a no-op when analytics is unconfigured, otherwise handed to a goroutine so it NEVER delays or fails the caller. EmitCompute (machine) and the cluster / nodepool emitters are all thin adapters over this one path.
func HanzoPrice ¶
HanzoPrice converts a DigitalOcean list price (USD) into Hanzo's resale price (USD). isGPU selects the GPU multiplier. Rounded to 5 decimals so hourly micro-prices (e.g. $0.00744/hr) survive while monthly stays clean.
func IsNotFound ¶
IsNotFound reports whether err wraps a DigitalOcean 404 response, meaning the resource is already gone. Callers treat this as success when deleting.
func MachineApp ¶
MachineApp / MachineProject recover a machine's OPTIONAL app/project scope from its own tags — the read-back counterparts of SetScope, mirroring MachineKind. Empty when the machine carries no such tag (scope is optional). Used by the ?project= list filter and available to any scope-aware read.
func MachineKind ¶
MachineKind recovers a machine's canonical compute kind from its own tags — the read-back counterpart of SetKind (which writes the kind on launch). It is the ONE way to read a live machine's kind (the ?kind= list filter and EmitCompute both use it), so a missing/garbage tag resolves to machine.
func MachineProject ¶
func MeterActor ¶
MeterActor encodes org+project into the commerce metering Actor — the audit-trail identity recorded on every usage transaction. It NEVER changes which balance is gated or debited: that is ALWAYS the org (the Usage.User billing key), so one org credit covers all its projects. Actor only ATTRIBUTES the line, which is what makes spend reportable per project. It is the ONE place org+project is folded into a metering line, shared by the launch debit, the recurring sweep, and every fleet-billing tier.
Empty project == the org's default project == today's behavior (Actor == org), so threading project through the existing launch and sweep debits is a no-op for any caller that does not set X-Project-Id. A named project yields "org/project" (the same org/sub shape commerce already documents for Actor).
func MeterRunningMachines ¶
MeterRunningMachines debits every RUNNING house resell machine one hour of its resale price to its OWNING org — the recurring counterpart to the launch debit. It is the "a running bound machine debits the org" rule: a machine that stays up keeps drawing down the org's credit balance, hour by hour.
Per machine: org is recovered from the machine's own hanzo-org tag (never trusted from a client — it is the tag LaunchOrgMachine injected), the hourly price comes from the resale catalog (SizeBySlug → PriceToCents), and the debit carries RequestID "compute-<machineID>-<YYYYMMDDHH>". Recording is decoupled from gating (the machine already ran that hour, so the cost must be recorded); enforcement/suspend on a depleted balance is a separate control. A per-machine error is logged and does not abort the sweep.
EXACTLY-ONCE PER HOUR is enforced OUTSIDE the RequestID: commerce's RecordUsage does NOT dedup the withdraw on requestId, so the key is only a reconciliation hint. The real once-per-hour guarantees are (1) the ticker's per-hour single-flight lease (object.ClaimMeterHour) so only one replica sweeps, and (2) skipping a machine's LAUNCH hour here (the launch path already billed it).
No-op when metering is unconfigured or when compute is unconfigured (no house token) — nothing to enumerate, nothing to debit.
func MeteringConfigured ¶
func MeteringConfigured() bool
MeteringConfigured reports whether the recurring meter will actually debit. The client's own Enabled() only checks the base URL (which always defaults to the in-cluster commerce), so the real "is billing wired" signal is the operator-provisioned service token (KMS-synced COMMERCE_SERVICE_TOKEN) — the same credential the launch path needs to authorize. Absent ⇒ the sweep is a safe no-op: an unconfigured deployment is never blocked or spammed with failed (401) debits.
func NewMeteringClient ¶
NewMeteringClient builds the commerce metering client for an org. The commerce base URL and the admin-scoped service token both come from the environment (the operator wires the token from KMS as COMMERCE_SERVICE_TOKEN). When the token is absent the client fails closed on Authorize, so real launches are denied while quotes still work, and the recurring meter is a no-op (Record short-circuits on !Enabled()). This is the SAME client construction the launch path uses, so both key the same per-org ledger.
func NormalizeProject ¶
NormalizeProject trims and validates a project scope read from the gateway-minted X-Project-Id header at the edge, returning the canonical project or "" (the org's default project) for an absent or invalid value. It is the ONE normalization the controllers apply, so every keyed surface downstream receives a project that already survives the tag/meter read-back. Built on the same validProjectSlug predicate the write boundaries use, so there is one project rule expressed once.
func PriceToCents ¶
PriceToCents converts a USD price to whole cents for billing. It Ceils (a paid product never under-charges) but subtracts a 1e-9 epsilon first so float64 overshoot on a whole-cent price (0.07*100 = 7.00000000000000089) does not round up to 8. A true sub-cent price still ceils to >= 1; a $0 price yields 0 (free, no charge). This is the ONE price→cents rule shared by launch and recurring metering.
func SetKind ¶
func SetKind(spec *CreateMachineSpec, kind string)
SetKind records the launch's kind on the spec's tags so it flows onto the droplet and is recovered — via the same tag read-back as org/app/project — by the emit, sweep, and destroy paths. It canonicalizes the value and inits the tag map if needed. Both launch surfaces (single-machine compute.go and fleet.go) call it, so kind is set exactly one way.
func SetScope ¶
func SetScope(spec *CreateMachineSpec, app, project string)
SetScope records the launch's OPTIONAL app/project scope on the spec's tags so it flows onto the droplet and is recovered — via the same tag read-back as org/kind (tagValue) — by the emit, sweep, and destroy paths (EmitCompute reads hanzo-app / hanzo-project into ComputeEvent.App/Project). Beneath org in the org > app > project hierarchy, both are optional: an empty value, or one carrying the tag read-back's "," / ":" separators (guarded by safeTagField exactly as org's attribution tag is), is skipped — so a launch that omits them is never broken and no unparseable tag is ever emitted. Both launch surfaces set scope exactly one way through here, mirroring SetKind.
Types ¶
type CloudCostReader ¶
type CloudCostReader interface {
// MonthToDateCents returns the account's spend so far this UTC month, in cents.
// Zero is valid (no spend → no fee). ErrCostUnavailable (or any error) makes the
// collector skip this account for this run — never fabricating a figure.
MonthToDateCents(ctx context.Context, now time.Time) (int64, error)
}
CloudCostReader reads a BYOC cloud account's cumulative spend for the current UTC calendar month, in whole cents of the account's billing currency. Month-to-date is the ONE figure every cloud can report uniformly (a per-day figure is not available from every cloud, e.g. DigitalOcean's balance endpoint), so the billing orchestrator bills 1% of the INCREMENT since it last billed — totaling 1% of the month's spend, idempotently, with month-rollover handled by the cursor key.
func NewCostReader ¶
func NewCostReader(providerType, clientId, clientSecret, region, costScope string) (CloudCostReader, error)
NewCostReader builds the cost reader for a BYOC provider from its stored creds. providerType is the internal provider type; (clientId, clientSecret, region) is the managed-machine credential triple; costScope is Provider.CostReadScope (the cloud-specific extra identifier the cost API needs). Returns ErrCostUnavailable for a provider type with no cost reader, so the collector skips it honestly. The internal provider names never leave this package (brand policy).
type ComputeEvent ¶
type ComputeEvent struct {
Org string `json:"org"`
App string `json:"app"`
Project string `json:"project"`
Kind string `json:"kind"`
Event string `json:"event"`
MachineID string `json:"machine_id"`
Size string `json:"size"`
PriceCents int64 `json:"price_cents"`
Ts string `json:"ts"`
}
ComputeEvent is one row of hanzo.compute_usage. The json tags are the exact ClickHouse column names (JSONEachRow maps by key), making this struct the single Go mirror of the schema in hanzoai/datastore (hanzo/schema.sql). ts is pre-formatted in ClickHouse's default DateTime input format so no per-request parsing setting is needed; kind is a plain LowCardinality(String) value.
type CreateClusterNodePool ¶
type CreateClusterNodePool struct {
Name string `json:"name,omitempty"`
Size string `json:"size"`
Count int `json:"count"`
}
CreateClusterNodePool is the initial worker pool of a new cluster: its instance size and how many nodes. Name is optional (defaults to "<cluster>-pool").
type CreateClusterSpec ¶
type CreateClusterSpec struct {
Name string `json:"name"`
Region string `json:"region"`
Version string `json:"version"`
NodePool CreateClusterNodePool `json:"nodePool"`
}
CreateClusterSpec is the minimal request to provision a DOKS cluster: identity (name), placement (region), the Kubernetes version, and ONE initial node pool. It is deliberately small — the resell surface provisions a single-pool cluster; further pools are added through the node-pool surface. Additional pools can be grown later; this is the create seed, not the full topology.
type CreateMachineSpec ¶
type CreateMachineSpec struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
InstanceType string `json:"instanceType"` // e.g. "t3.medium", "mac2.metal"
ImageID string `json:"imageId"` // AMI ID, image name, etc.
OS string `json:"os"` // "linux", "macos", "windows"
Region string `json:"region"`
Tags map[string]string `json:"tags,omitempty"`
SSHKeyIDs []string `json:"sshKeyIds,omitempty"` // Provider SSH key IDs
}
CreateMachineSpec describes parameters for launching a new cloud instance.
type CreateNodePoolSpec ¶
type CreateVolumeSpec ¶
type DOKSClient ¶
func NewDOKSClient ¶
func NewDOKSClient(token, clusterID string) (*DOKSClient, error)
func (*DOKSClient) CreateCluster ¶
func (c *DOKSClient) CreateCluster(ctx context.Context, spec *CreateClusterSpec, tags []string) (*KubernetesCluster, error)
CreateCluster provisions a DOKS cluster from spec, tagging it with tags so it associates to the owning org (the node/cluster listers scope by that tag).
func (*DOKSClient) CreateNodePool ¶
func (c *DOKSClient) CreateNodePool(spec *CreateNodePoolSpec) (*NodePool, error)
func (*DOKSClient) DeleteCluster ¶
func (c *DOKSClient) DeleteCluster(ctx context.Context, id string) error
DeleteCluster destroys a cluster by id. A DO 404 (already gone) is the caller's success to interpret via IsNotFound — DeleteCluster itself reports the raw error.
func (*DOKSClient) DeleteNodePool ¶
func (c *DOKSClient) DeleteNodePool(poolID string) error
func (*DOKSClient) GetCluster ¶
func (c *DOKSClient) GetCluster(ctx context.Context, id string) (*KubernetesClusterDetail, error)
GetCluster returns one cluster's full detail — identity, node pools and the worker nodes as Machines. The Get is authoritative: it carries the pools (with their nodes) directly, so no separate pool re-list is needed.
func (*DOKSClient) GetNodePool ¶
func (c *DOKSClient) GetNodePool(poolID string) (*NodePool, error)
func (*DOKSClient) ListClusters ¶
func (c *DOKSClient) ListClusters(ctx context.Context) ([]*KubernetesCluster, error)
ListClusters returns every DOKS cluster visible to this client's token, in the clean KubernetesCluster shape. It is clustersByTag with no tag filter — the ONE cluster enumeration, so the tenant-scoped and account-wide lists never drift.
func (*DOKSClient) ListNodePools ¶
func (c *DOKSClient) ListNodePools() ([]*NodePool, error)
func (*DOKSClient) NodeMachines ¶
func (c *DOKSClient) NodeMachines(ctx context.Context) ([]*Machine, error)
NodeMachines returns one Machine per worker node in THIS client's cluster — the BYOC path where a Provider names a single cluster. The cluster Get supplies the region/name; ListNodePools supplies the pools whose nodes become the machines.
func (*DOKSClient) RecycleNodePoolNodes ¶
func (c *DOKSClient) RecycleNodePoolNodes(poolID string, nodeIDs []string) error
func (*DOKSClient) UpdateNodePool ¶
func (c *DOKSClient) UpdateNodePool(poolID string, spec *CreateNodePoolSpec) (*NodePool, error)
type GPUSpec ¶
type GPUSpec struct {
Count int `json:"count"`
Model string `json:"model"`
Vram int `json:"vram"`
VramUnit string `json:"vramUnit"`
}
GPUSpec is the GPU detail for a GPU-backed size.
type ImageInfo ¶
type ImageInfo struct {
ID int `json:"id,omitempty"` // custom/app images select by ID
Slug string `json:"slug,omitempty"` // distributions select by slug
Name string `json:"name"`
Distribution string `json:"distribution,omitempty"`
Kind string `json:"kind"` // "distribution" | "application" | "custom"
Regions []string `json:"regions,omitempty"`
MinDiskGB int `json:"minDiskGb,omitempty"`
SizeGB float64 `json:"sizeGb,omitempty"`
Status string `json:"status,omitempty"` // custom: "pending" -> "available"
}
ImageInfo is one selectable image.
func CreateOrgImage ¶
CreateOrgImage registers a custom image from a URL into the house account, tagged to org so only that org sees it in ListImages. Creation is async (Status "pending" -> "available"); once available the image is launchable by its returned ID (LaunchOrgMachine accepts a numeric ImageID as a custom image).
func ListImages ¶
ListImages returns what an org may launch: shared distributions + 1-click applications, plus the org's OWN custom images.
type KubernetesCluster ¶
type KubernetesCluster struct {
ID string `json:"id"`
Name string `json:"name"`
RegionSlug string `json:"regionSlug"`
Status string `json:"status"`
Tags []string `json:"tags"`
}
KubernetesCluster is a DOKS cluster in the shape visor surfaces: identity, region, status and tags. Tags carry ownership (hanzo-org:<org>) used to scope a house-account cluster to the org that owns it.
func CreateOrgKubernetesCluster ¶
func CreateOrgKubernetesCluster(org string, spec *CreateClusterSpec) (*KubernetesCluster, error)
CreateOrgKubernetesCluster provisions a DOKS cluster in Hanzo's house account for org, stamping it managed-by + hanzo-org:<org> so it associates to the tenant exactly like a droplet — which is what makes it visible to that org's cluster and node listers (and invisible to every other org).
func ListOrgKubernetesClusters ¶
func ListOrgKubernetesClusters(org string) ([]*KubernetesCluster, error)
ListOrgKubernetesClusters returns every DOKS cluster in Hanzo's HOUSE account tagged for org — the house analogue of ListOrgMachines for whole clusters. Per-org isolation is by the cluster's hanzo-org tag: a tenant only ever sees its own clusters, never another org's.
type KubernetesClusterDetail ¶
type KubernetesClusterDetail struct {
KubernetesCluster
NodePools []*NodePool `json:"nodePools"`
Nodes []*Machine `json:"nodes"`
}
KubernetesClusterDetail is a cluster plus its node pools and the worker nodes expanded as fleet Machines — the ONE detail shape for GET .../clusters/:id. It embeds KubernetesCluster so identity/region/status/tags flatten into the same top-level JSON the list emits; NodePools carries the authoritative pool topology and Nodes the per-worker machines (the SAME shape ListOrgMachines emits).
func GetOrgKubernetesCluster ¶
func GetOrgKubernetesCluster(org, id string) (*KubernetesClusterDetail, error)
GetOrgKubernetesCluster returns one house cluster's detail (pools + worker nodes), but ONLY if it carries the caller org's hanzo-org tag. A cluster owned by another org — or a missing cluster — resolves to (nil, nil): the controller renders it as "not found", so a tenant can never read another tenant's cluster by guessing an id.
type Machine ¶
type Machine struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
Id string `xorm:"varchar(100)" json:"id"`
Provider string `xorm:"varchar(100)" json:"provider"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
ExpireTime string `xorm:"varchar(100)" json:"expireTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Region string `xorm:"varchar(100)" json:"region"`
Zone string `xorm:"varchar(100)" json:"zone"`
Category string `xorm:"varchar(100)" json:"category"`
Type string `xorm:"varchar(100)" json:"type"`
Size string `xorm:"varchar(100)" json:"size"`
Tag string `xorm:"varchar(100)" json:"tag"`
State string `xorm:"varchar(100)" json:"state"`
Image string `xorm:"varchar(100)" json:"image"`
Os string `xorm:"varchar(100)" json:"os"`
PublicIp string `xorm:"varchar(100)" json:"publicIp"`
PrivateIp string `xorm:"varchar(100)" json:"privateIp"`
CpuSize string `xorm:"varchar(100)" json:"cpuSize"`
MemSize string `xorm:"varchar(100)" json:"memSize"`
}
func GetOrgMachine ¶
GetOrgMachine returns a single machine only if it belongs to org; otherwise nil (no cross-tenant leak, even to a valid caller of another org).
func LaunchOrgMachine ¶
func LaunchOrgMachine(org, project string, spec *CreateMachineSpec) (*Machine, error)
LaunchOrgMachine provisions a droplet in Hanzo's house account, tagged so it is owned by org and attributed to project. Both attribution tags are injected here (never trusted from the client body) so the machine is always attributable to the right tenant AND project.
org is validated as a clean slug first: it becomes BOTH the hanzo-org attribution tag (read back by the hourly meter) AND the commerce billing key, so a value carrying the meter's "," / ":" separators must never reach the tag. A validated IAM owner claim is already a DNS-label slug, so this only rejects a malformed/forged org — it never breaks a real tenant. project is validated the same way; the EMPTY project is the org's default and writes no hanzo-project tag (backward-compatible with every machine launched before the project dimension).
func ListOrgKubernetesNodes ¶
ListOrgKubernetesNodes returns one Machine per DOKS worker node for every cluster in Hanzo's HOUSE DigitalOcean account tagged for org — the house analogue of ListOrgMachines, but for managed-Kubernetes nodes. DOKS worker droplets carry k8s tags, not a hanzo-org DROPLET tag, so they never surface through ListOrgMachines; this lists them via the managed-Kubernetes API and maps each node to the same Machine shape. Per-org isolation is by the cluster's hanzo-org tag — a tenant can only ever see its own clusters' nodes.
func ListOrgMachines ¶
ListOrgMachines returns the droplets tagged for org — per-org isolation enforced at the DigitalOcean layer via an exact tag query — optionally narrowed to a single project.
project scopes the result WITHIN the org: the empty project is the org's default and returns EVERY org machine (today's behavior — a machine launched before the project dimension carries no hanzo-project tag), while a named project returns only the machines carrying that hanzo-project tag. Project is an attribution and view dimension, not a second isolation boundary — org is the tenant boundary, so get/delete stay org-scoped and only listing narrows by project.
func ListRunningHouseMachines ¶
ListRunningHouseMachines returns every RUNNING droplet in Hanzo's house account that carries a hanzo-org tag — the set the recurring hourly meter debits. It lists across ALL orgs (no per-org tag filter): the org is recovered per machine from its own tag, so ONE sweep meters every tenant's running machines. Untagged/non-resell droplets (no hanzo-org tag) are excluded, so a non-resell house droplet is never billed to a tenant. Only "Running" machines are returned — a stopped droplet consumes no compute-hour.
type MachineAliyunClient ¶
func (MachineAliyunClient) CreateMachine ¶
func (client MachineAliyunClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
func (MachineAliyunClient) GetMachine ¶
func (client MachineAliyunClient) GetMachine(name string) (*Machine, error)
func (MachineAliyunClient) GetMachines ¶
func (client MachineAliyunClient) GetMachines() ([]*Machine, error)
func (MachineAliyunClient) UpdateMachineState ¶
type MachineAwsClient ¶
func (MachineAwsClient) CreateMachine ¶
func (client MachineAwsClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
func (MachineAwsClient) GetMachine ¶
func (client MachineAwsClient) GetMachine(name string) (*Machine, error)
func (MachineAwsClient) GetMachines ¶
func (client MachineAwsClient) GetMachines() ([]*Machine, error)
func (MachineAwsClient) UpdateMachineState ¶
type MachineAzureClient ¶
type MachineAzureClient struct {
Client *armcompute.VirtualMachinesClient
// contains filtered or unexported fields
}
func (MachineAzureClient) CreateMachine ¶
func (client MachineAzureClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
func (MachineAzureClient) GetMachine ¶
func (client MachineAzureClient) GetMachine(name string) (*Machine, error)
func (MachineAzureClient) GetMachines ¶
func (client MachineAzureClient) GetMachines() ([]*Machine, error)
func (MachineAzureClient) UpdateMachineState ¶
type MachineClientInterface ¶
type MachineClientInterface interface {
GetMachines() ([]*Machine, error)
GetMachine(name string) (*Machine, error)
UpdateMachineState(name string, state string) (bool, string, error)
CreateMachine(spec *CreateMachineSpec) (*Machine, error)
}
func NewMachineClient ¶
type MachineDigitalOceanClient ¶
type MachineDigitalOceanClient struct {
Client *godo.Client
// contains filtered or unexported fields
}
func (MachineDigitalOceanClient) CreateMachine ¶
func (client MachineDigitalOceanClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
func (MachineDigitalOceanClient) DeleteMachine ¶
func (client MachineDigitalOceanClient) DeleteMachine(name string) error
func (MachineDigitalOceanClient) GetMachine ¶
func (client MachineDigitalOceanClient) GetMachine(name string) (*Machine, error)
func (MachineDigitalOceanClient) GetMachines ¶
func (client MachineDigitalOceanClient) GetMachines() ([]*Machine, error)
func (MachineDigitalOceanClient) UpdateMachineState ¶
type MachineGcpClient ¶
type MachineGcpClient struct {
// contains filtered or unexported fields
}
func (MachineGcpClient) CreateMachine ¶
func (client MachineGcpClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
func (MachineGcpClient) GetMachine ¶
func (client MachineGcpClient) GetMachine(name string) (*Machine, error)
func (MachineGcpClient) GetMachines ¶
func (client MachineGcpClient) GetMachines() ([]*Machine, error)
func (MachineGcpClient) UpdateMachineState ¶
type MachineHetznerClient ¶
type MachineHetznerClient struct {
Client *hcloud.Client
// contains filtered or unexported fields
}
func (MachineHetznerClient) CreateMachine ¶
func (client MachineHetznerClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
func (MachineHetznerClient) GetMachine ¶
func (client MachineHetznerClient) GetMachine(name string) (*Machine, error)
func (MachineHetznerClient) GetMachines ¶
func (client MachineHetznerClient) GetMachines() ([]*Machine, error)
func (MachineHetznerClient) UpdateMachineState ¶
type MachineKvmClient ¶
func (MachineKvmClient) CreateMachine ¶
func (client MachineKvmClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
func (MachineKvmClient) GetMachine ¶
func (client MachineKvmClient) GetMachine(name string) (*Machine, error)
func (MachineKvmClient) GetMachines ¶
func (client MachineKvmClient) GetMachines() ([]*Machine, error)
func (MachineKvmClient) UpdateMachineState ¶
type MachineLightsailClient ¶
type MachineLightsailClient struct {
Client *lightsail.Client
// contains filtered or unexported fields
}
func (MachineLightsailClient) CreateMachine ¶
func (client MachineLightsailClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
func (MachineLightsailClient) GetMachine ¶
func (client MachineLightsailClient) GetMachine(name string) (*Machine, error)
func (MachineLightsailClient) GetMachines ¶
func (client MachineLightsailClient) GetMachines() ([]*Machine, error)
func (MachineLightsailClient) UpdateMachineState ¶
type MachinePveClient ¶
type MachinePveClient struct {
// contains filtered or unexported fields
}
func (MachinePveClient) CreateMachine ¶
func (client MachinePveClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
func (MachinePveClient) GetMachine ¶
func (client MachinePveClient) GetMachine(name string) (*Machine, error)
func (MachinePveClient) GetMachines ¶
func (client MachinePveClient) GetMachines() ([]*Machine, error)
func (MachinePveClient) UpdateMachineState ¶
type MachineVmwareClient ¶
type MachineVmwareClient struct {
// contains filtered or unexported fields
}
func (MachineVmwareClient) CreateMachine ¶
func (client MachineVmwareClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error)
func (MachineVmwareClient) GetMachine ¶
func (client MachineVmwareClient) GetMachine(name string) (*Machine, error)
func (MachineVmwareClient) GetMachines ¶
func (client MachineVmwareClient) GetMachines() ([]*Machine, error)
func (MachineVmwareClient) UpdateMachineState ¶
type NodePool ¶
type NodePool struct {
ID string `json:"id"`
Name string `json:"name"`
Size string `json:"size"`
Count int `json:"count"`
MinNodes int `json:"minNodes"`
MaxNodes int `json:"maxNodes"`
AutoScale bool `json:"autoScale"`
Nodes []NodeInfo `json:"nodes"`
Tags []string `json:"tags"`
Labels map[string]string `json:"labels"`
}
type RegionInfo ¶
type RegionInfo struct {
Slug string `json:"slug"`
Name string `json:"name"`
Available bool `json:"available"`
Features []string `json:"features"`
Sizes []string `json:"sizes"`
}
RegionInfo is a resellable region.
func ListRegions ¶
func ListRegions() ([]RegionInfo, error)
ListRegions returns the cached DigitalOcean regions catalog.
type SizeInfo ¶
type SizeInfo struct {
Slug string `json:"slug"`
Vcpus int `json:"vcpus"`
MemoryMB int `json:"memoryMb"`
DiskGB int `json:"diskGb"`
Available bool `json:"available"`
Regions []string `json:"regions"`
GPU *GPUSpec `json:"gpu,omitempty"`
Currency string `json:"currency"`
PriceHourly float64 `json:"priceHourly"`
PriceMonthly float64 `json:"priceMonthly"`
}
SizeInfo is a resellable compute size. Only Hanzo's resale price is exposed — the wholesale cost and the upstream provider are never surfaced (brand policy; margin stays private). Markup is applied once in pricing.go.
func ListGPUSizes ¶
ListGPUSizes returns only the GPU-backed sizes from the catalog.
func SizeBySlug ¶
SizeBySlug returns the resale size for a slug, or nil if unknown. Used to price launch quotes.
type VirtualMachine ¶
type VirtualMachinePath ¶
type VolumeClientInterface ¶
type VolumeClientInterface interface {
GetVolumes() ([]*Volume, error)
GetVolume(name string) (*Volume, error)
CreateVolume(spec *CreateVolumeSpec) (*Volume, error)
DeleteVolume(name string) error
AttachVolume(volumeName string, machineName string) error
DetachVolume(volumeName string) error
ResizeVolume(volumeName string, sizeGB int) error
}
func NewVolumeClient ¶
type VolumeDigitalOceanClient ¶
type VolumeDigitalOceanClient struct {
Client *godo.Client
// contains filtered or unexported fields
}
func (*VolumeDigitalOceanClient) AttachVolume ¶
func (c *VolumeDigitalOceanClient) AttachVolume(volumeName string, machineName string) error
func (*VolumeDigitalOceanClient) CreateVolume ¶
func (c *VolumeDigitalOceanClient) CreateVolume(spec *CreateVolumeSpec) (*Volume, error)
func (*VolumeDigitalOceanClient) DeleteVolume ¶
func (c *VolumeDigitalOceanClient) DeleteVolume(name string) error
func (*VolumeDigitalOceanClient) DetachVolume ¶
func (c *VolumeDigitalOceanClient) DetachVolume(volumeName string) error
func (*VolumeDigitalOceanClient) GetVolume ¶
func (c *VolumeDigitalOceanClient) GetVolume(name string) (*Volume, error)
func (*VolumeDigitalOceanClient) GetVolumes ¶
func (c *VolumeDigitalOceanClient) GetVolumes() ([]*Volume, error)
func (*VolumeDigitalOceanClient) ResizeVolume ¶
func (c *VolumeDigitalOceanClient) ResizeVolume(volumeName string, sizeGB int) error
type VolumeHetznerClient ¶
func (*VolumeHetznerClient) AttachVolume ¶
func (c *VolumeHetznerClient) AttachVolume(volumeName string, machineName string) error
func (*VolumeHetznerClient) CreateVolume ¶
func (c *VolumeHetznerClient) CreateVolume(spec *CreateVolumeSpec) (*Volume, error)
func (*VolumeHetznerClient) DeleteVolume ¶
func (c *VolumeHetznerClient) DeleteVolume(name string) error
func (*VolumeHetznerClient) DetachVolume ¶
func (c *VolumeHetznerClient) DetachVolume(volumeName string) error
func (*VolumeHetznerClient) GetVolume ¶
func (c *VolumeHetznerClient) GetVolume(name string) (*Volume, error)
func (*VolumeHetznerClient) GetVolumes ¶
func (c *VolumeHetznerClient) GetVolumes() ([]*Volume, error)
func (*VolumeHetznerClient) ResizeVolume ¶
func (c *VolumeHetznerClient) ResizeVolume(volumeName string, sizeGB int) error
Source Files
¶
- aliyun.go
- analytics.go
- aws.go
- azure.go
- botuser.go
- cloud_cost.go
- compute.go
- cost_aws.go
- cost_azure.go
- cost_do.go
- cost_gcp.go
- digitalocean.go
- doks.go
- gcp.go
- hetzner.go
- images.go
- kvm.go
- lightsail.go
- machine.go
- metering.go
- playground.go
- pricing.go
- pve.go
- tenant.go
- util.go
- vmware.go
- volume.go
- volume_digitalocean.go
- volume_hetzner.go