mirror of
https://github.com/hanzoai/compute.git
synced 2026-10-30 14:50:41 +00:00
Cloud's side of this rename landed some time ago — the app is apps/compute, the env is COMPUTE_URL — and left a note saying the other half moves with the manifest that names it. This is the half that does not touch the cluster: the Go module is github.com/hanzoai/compute, the binary it builds is compute, and the image is ghcr.io/hanzoai/compute. GitHub already carries the new name. What is deliberately NOT here is the cutover. The Service universe declares is still visor, and so are visor-config, visor-kms-sync and visor-db-bucket. Pointing anything at compute.hanzo.svc before those move resolves to nothing, so they move together, in universe, not here.
234 lines
5.9 KiB
Go
234 lines
5.9 KiB
Go
// Copyright 2025 Hanzo Industries Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// volume.go serves a volume's lifecycle, and every handler here takes its tenant
|
|
// from ONE place: resolveComputeOrg, the same principal rule the node-pool,
|
|
// machine and cluster surfaces run.
|
|
//
|
|
// It used to take it from two. The authorization filter derives the object's
|
|
// owner from `?id=` or the request BODY, while these handlers read `?owner=` — so
|
|
// `POST /v1/create-volume?owner=victim` with a body naming the caller cleared
|
|
// authorization against the caller and then provisioned against the victim's
|
|
// cloud credentials, onto the victim's invoice. The reads had the same split by a
|
|
// different field: authorization keys a GET on `?id=`, which a caller could point
|
|
// at its own org while `?owner=` pointed the handler at another's.
|
|
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/hanzoai/compute/object"
|
|
"github.com/hanzoai/compute/service"
|
|
)
|
|
|
|
// GetVolumes
|
|
// @Title GetVolumes
|
|
// @Tag Volume API
|
|
// @Description get all volumes for an owner
|
|
// @Success 200 {array} object.Volume
|
|
// @router /get-volumes [get]
|
|
func (c *ApiController) GetVolumes() {
|
|
owner := c.resolveComputeOrg()
|
|
if owner == "" {
|
|
c.ResponseError(refuseNoOrg)
|
|
return
|
|
}
|
|
volumes, err := object.GetVolumes(owner)
|
|
if err != nil {
|
|
c.ResponseError(err.Error())
|
|
return
|
|
}
|
|
c.ResponseOk(volumes)
|
|
}
|
|
|
|
// GetVolume
|
|
// @Title GetVolume
|
|
// @Tag Volume API
|
|
// @Description get a single volume
|
|
// @Param name query string true "The volume name"
|
|
// @Success 200 {object} object.Volume
|
|
// @router /get-volume [get]
|
|
func (c *ApiController) GetVolume() {
|
|
owner := c.resolveComputeOrg()
|
|
if owner == "" {
|
|
c.ResponseError(refuseNoOrg)
|
|
return
|
|
}
|
|
// The query names WHICH volume; it never names WHOSE.
|
|
name := c.Ctx.Query("name")
|
|
|
|
volume, err := object.GetVolume(owner, name)
|
|
if err != nil {
|
|
c.ResponseError(err.Error())
|
|
return
|
|
}
|
|
c.ResponseOk(volume)
|
|
}
|
|
|
|
// CreateVolume
|
|
// @Title CreateVolume
|
|
// @Tag Volume API
|
|
// @Description create a block storage volume via a provider
|
|
// @Param provider query string true "The provider name"
|
|
// @Param body body service.CreateVolumeSpec true "The volume spec"
|
|
// @Success 200 {object} object.Volume
|
|
// @router /create-volume [post]
|
|
func (c *ApiController) CreateVolume() {
|
|
owner := c.resolveComputeOrg()
|
|
if owner == "" {
|
|
c.ResponseError(refuseNoOrg)
|
|
return
|
|
}
|
|
providerName := c.Ctx.Query("provider")
|
|
if providerName == "" {
|
|
c.ResponseError(refuseNoProviderName)
|
|
return
|
|
}
|
|
|
|
var spec service.CreateVolumeSpec
|
|
err := json.Unmarshal(c.Ctx.Body(), &spec)
|
|
if err != nil {
|
|
c.ResponseError(err.Error())
|
|
return
|
|
}
|
|
|
|
volume, err := object.CreateVolumeCloud(owner, providerName, &spec)
|
|
if err != nil {
|
|
c.ResponseError(err.Error())
|
|
return
|
|
}
|
|
|
|
c.ResponseOk(volume)
|
|
}
|
|
|
|
// DeleteVolume
|
|
// @Title DeleteVolume
|
|
// @Tag Volume API
|
|
// @Description delete a volume
|
|
// @Param provider query string true "The provider name"
|
|
// @Param name query string true "The volume cloud ID"
|
|
// @Success 200 {object} controllers.Response
|
|
// @router /delete-volume [post]
|
|
func (c *ApiController) DeleteVolume() {
|
|
owner := c.resolveComputeOrg()
|
|
if owner == "" {
|
|
c.ResponseError(refuseNoOrg)
|
|
return
|
|
}
|
|
providerName := c.Ctx.Query("provider")
|
|
name := c.Ctx.Query("name")
|
|
|
|
err := object.DeleteVolumeCloud(owner, providerName, name)
|
|
if err != nil {
|
|
c.ResponseError(err.Error())
|
|
return
|
|
}
|
|
|
|
c.Data["json"] = wrapActionResponse(true, nil)
|
|
c.ServeJSON()
|
|
}
|
|
|
|
// AttachVolume
|
|
// @Title AttachVolume
|
|
// @Tag Volume API
|
|
// @Description attach a volume to a machine
|
|
// @router /attach-volume [post]
|
|
func (c *ApiController) AttachVolume() {
|
|
owner := c.resolveComputeOrg()
|
|
if owner == "" {
|
|
c.ResponseError(refuseNoOrg)
|
|
return
|
|
}
|
|
providerName := c.Ctx.Query("provider")
|
|
volumeName := c.Ctx.Query("volume")
|
|
machineName := c.Ctx.Query("machine")
|
|
|
|
err := object.AttachVolumeCloud(owner, providerName, volumeName, machineName)
|
|
if err != nil {
|
|
c.ResponseError(err.Error())
|
|
return
|
|
}
|
|
|
|
c.Data["json"] = wrapActionResponse(true, nil)
|
|
c.ServeJSON()
|
|
}
|
|
|
|
// DetachVolume
|
|
// @Title DetachVolume
|
|
// @Tag Volume API
|
|
// @Description detach a volume from its machine
|
|
// @router /detach-volume [post]
|
|
func (c *ApiController) DetachVolume() {
|
|
owner := c.resolveComputeOrg()
|
|
if owner == "" {
|
|
c.ResponseError(refuseNoOrg)
|
|
return
|
|
}
|
|
providerName := c.Ctx.Query("provider")
|
|
volumeName := c.Ctx.Query("volume")
|
|
|
|
err := object.DetachVolumeCloud(owner, providerName, volumeName)
|
|
if err != nil {
|
|
c.ResponseError(err.Error())
|
|
return
|
|
}
|
|
|
|
c.Data["json"] = wrapActionResponse(true, nil)
|
|
c.ServeJSON()
|
|
}
|
|
|
|
// ResizeVolume
|
|
// @Title ResizeVolume
|
|
// @Tag Volume API
|
|
// @Description resize a volume
|
|
// @router /resize-volume [post]
|
|
func (c *ApiController) ResizeVolume() {
|
|
owner := c.resolveComputeOrg()
|
|
if owner == "" {
|
|
c.ResponseError(refuseNoOrg)
|
|
return
|
|
}
|
|
providerName := c.Ctx.Query("provider")
|
|
volumeName := c.Ctx.Query("volume")
|
|
sizeStr := c.Ctx.Query("size")
|
|
|
|
size := 0
|
|
if sizeStr != "" {
|
|
size = parseInt(sizeStr)
|
|
}
|
|
if size <= 0 {
|
|
c.ResponseError("size parameter must be a positive integer (GB)")
|
|
return
|
|
}
|
|
|
|
err := object.ResizeVolumeCloud(owner, providerName, volumeName, size)
|
|
if err != nil {
|
|
c.ResponseError(err.Error())
|
|
return
|
|
}
|
|
|
|
c.Data["json"] = wrapActionResponse(true, nil)
|
|
c.ServeJSON()
|
|
}
|
|
|
|
func parseInt(s string) int {
|
|
n := 0
|
|
for _, c := range s {
|
|
if c >= '0' && c <= '9' {
|
|
n = n*10 + int(c-'0')
|
|
}
|
|
}
|
|
return n
|
|
}
|