service

package
v1.108.3 Latest Latest
Warning

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

Go to latest
Published: Jul 2, 2026 License: Apache-2.0 Imports: 35 Imported by: 0

Documentation

Overview

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.)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

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 DeleteOrgMachine

func DeleteOrgMachine(org, id string) error

DeleteOrgMachine deletes a machine only after confirming it belongs to org.

func HanzoPrice

func HanzoPrice(doPrice float64, isGPU bool) float64

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 MeterRunningMachines

func MeterRunningMachines(ctx context.Context)

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

func NewMeteringClient(org string) *metering.Client

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 PriceToCents

func PriceToCents(price float64) int64

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.

Types

type Cpu

type Cpu struct {
	Processors int `json:"processors"`
}

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 CreateNodePoolSpec struct {
	Name      string            `json:"name"`
	Size      string            `json:"size"`
	Count     int               `json:"count"`
	MinNodes  int               `json:"minNodes"`
	MaxNodes  int               `json:"maxNodes"`
	AutoScale bool              `json:"autoScale"`
	Tags      []string          `json:"tags,omitempty"`
	Labels    map[string]string `json:"labels,omitempty"`
}

type CreateVolumeSpec

type CreateVolumeSpec struct {
	Name        string `json:"name"`
	DisplayName string `json:"displayName"`
	Size        int    `json:"size"` // GB
	Region      string `json:"region"`
	Format      string `json:"format"`    // "ext4", "xfs"
	MachineID   string `json:"machineId"` // optional: attach on create
}

type DOKSClient

type DOKSClient struct {
	Client    *godo.Client
	ClusterID string
}

func NewDOKSClient

func NewDOKSClient(token, clusterID string) (*DOKSClient, error)

func (*DOKSClient) CreateNodePool

func (c *DOKSClient) CreateNodePool(spec *CreateNodePoolSpec) (*NodePool, error)

func (*DOKSClient) DeleteNodePool

func (c *DOKSClient) DeleteNodePool(poolID string) error

func (*DOKSClient) GetNodePool

func (c *DOKSClient) GetNodePool(poolID string) (*NodePool, error)

func (*DOKSClient) ListNodePools

func (c *DOKSClient) ListNodePools() ([]*NodePool, error)

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

func CreateOrgImage(org, name, url, region, distribution string) (*ImageInfo, error)

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

func ListImages(org string) ([]ImageInfo, error)

ListImages returns what an org may launch: shared distributions + 1-click applications, plus the org's OWN custom images.

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

func GetOrgMachine(org, id string) (*Machine, error)

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 string, spec *CreateMachineSpec) (*Machine, error)

LaunchOrgMachine provisions a droplet in Hanzo's house account, tagged so it is owned by org. The org tag is injected here (never trusted from the client body) so the machine is always attributable to the right tenant.

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.

func ListOrgMachines

func ListOrgMachines(org string) ([]*Machine, error)

ListOrgMachines returns only the droplets tagged for org — per-org isolation enforced at the DigitalOcean layer via an exact tag query.

func ListRunningHouseMachines

func ListRunningHouseMachines() ([]*Machine, error)

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

type MachineAliyunClient struct {
	Client *ecs.Client
}

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

func (client MachineAliyunClient) UpdateMachineState(name string, state string) (bool, string, error)

type MachineAwsClient

type MachineAwsClient struct {
	Client *ec2.Client
	// contains filtered or unexported fields
}

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

func (client MachineAwsClient) UpdateMachineState(name string, state string) (bool, string, error)

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

func (client MachineAzureClient) UpdateMachineState(name string, state string) (bool, string, error)

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

func NewMachineClient(providerType string, accessKeyId string, accessKeySecret string, region string) (MachineClientInterface, error)

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

func (client MachineDigitalOceanClient) UpdateMachineState(name string, state string) (bool, string, error)

type MachineGcpClient

type MachineGcpClient struct {
	Client    *computepb.InstancesClient
	ProjectID string
	Zone      string
}

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

func (client MachineGcpClient) UpdateMachineState(name string, state string) (bool, string, error)

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

func (client MachineHetznerClient) UpdateMachineState(name string, state string) (bool, string, error)

type MachineKvmClient

type MachineKvmClient struct {
	L *libvirt.Libvirt
}

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

func (client MachineKvmClient) UpdateMachineState(name string, state string) (bool, string, error)

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

func (client MachineLightsailClient) UpdateMachineState(name string, state string) (bool, string, error)

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

func (client MachinePveClient) UpdateMachineState(name string, state string) (bool, string, error)

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

func (client MachineVmwareClient) UpdateMachineState(name string, state string) (bool, string, error)

type NodeInfo

type NodeInfo struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	Status    string `json:"status"`
	DropletID string `json:"dropletId"`
	CreatedAt string `json:"createdAt"`
	UpdatedAt string `json:"updatedAt"`
}

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

func ListGPUSizes() ([]SizeInfo, error)

ListGPUSizes returns only the GPU-backed sizes from the catalog.

func ListSizes

func ListSizes() ([]SizeInfo, error)

ListSizes returns the cached, resale-priced sizes catalog.

func SizeBySlug

func SizeBySlug(slug string) (*SizeInfo, error)

SizeBySlug returns the resale size for a slug, or nil if unknown. Used to price launch quotes.

type VirtualMachine

type VirtualMachine struct {
	ID     string `json:"id"`
	Cpu    Cpu    `json:"cpu"`
	Memory int    `json:"memory"`
}

type VirtualMachinePath

type VirtualMachinePath struct {
	ID   string `json:"id"`
	Path string `json:"path"`
}

type Volume

type Volume struct {
	Name        string
	Id          string
	DisplayName string
	Region      string
	Size        int    // GB
	State       string // "Available", "Attached", "Creating"
	Format      string
	MachineName string // attached server name/ID, empty if detached
}

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

func NewVolumeClient(providerType string, accessKeyId string, accessKeySecret string, region string) (VolumeClientInterface, error)

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

type VolumeHetznerClient struct {
	Client *hcloud.Client
	// contains filtered or unexported fields
}

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

Jump to

Keyboard shortcuts

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