Files
2026-07-30 12:23:05 +08:00

374 lines
12 KiB
Go

package api
import (
"net/http"
"sync"
"github.com/gin-gonic/gin"
"oci-portal/internal/oci"
)
// @Summary 可用域列表
// @Tags 租户配置
// @Param id path int true "配置 ID"
// @Success 200 {array} string
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/availability-domains [get]
func (h *ociConfigHandler) availabilityDomains(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
ads, err := h.svc.AvailabilityDomains(c.Request.Context(), id, c.Query("region"))
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, ads)
}
// ---- 实例 ----
type createInstanceRequest struct {
Region string `json:"region"`
CompartmentID string `json:"compartmentId"` // 为空时建在租户根
AvailabilityDomain string `json:"availabilityDomain"` // 为空时默认使用第一个可用域
DisplayName string `json:"displayName" binding:"required"`
Shape string `json:"shape" binding:"required"`
Count int `json:"count"` // 批量创建数量,缺省 1,上限 10
Ocpus float32 `json:"ocpus"`
MemoryInGBs float32 `json:"memoryInGBs"`
ImageID string `json:"imageId"`
BootVolumeID string `json:"bootVolumeId"`
BootVolumeSizeGBs int64 `json:"bootVolumeSizeGBs"`
BootVolumeVpusPerGB int64 `json:"bootVolumeVpusPerGB"`
SubnetID string `json:"subnetId"` // 为空时自动创建 VCN 与子网
AssignPublicIP bool `json:"assignPublicIp"`
ReservedPublicIPID string `json:"reservedPublicIpId"` // 非空时不分配临时 IP,实例就绪后自动绑定该保留 IP
AssignIpv6 bool `json:"assignIpv6"`
SSHPublicKey string `json:"sshPublicKey"`
RootPassword string `json:"rootPassword"`
GenerateRootPassword bool `json:"generateRootPassword"` // 服务端生成随机 root 密码并写入 TAG RootPassword
UserData string `json:"userData"`
}
type updateInstanceRequest struct {
Region string `json:"region"`
DisplayName string `json:"displayName"`
Shape string `json:"shape"` // 更换 shape,运行中实例会自动重启
Ocpus float32 `json:"ocpus"`
MemoryInGBs float32 `json:"memoryInGBs"`
}
type instanceActionRequest struct {
Region string `json:"region"`
Action string `json:"action" binding:"required"`
}
// @Summary 实例列表
// @Tags 计算
// @Param id path int true "配置 ID"
// @Success 200 {array} oci.Instance
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/instances [get]
func (h *ociConfigHandler) listInstances(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
instances, err := h.svc.Instances(c.Request.Context(), id, c.Query("region"), c.Query("compartmentId"))
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, instances)
}
// @Summary 创建实例
// @Tags 计算
// @Param id path int true "配置 ID"
// @Param body body createInstanceRequest true "请求体"
// @Success 201 {object} createInstancesResponse "部分成功仍 201,errors 为逐台失败信息"
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/instances [post]
func (h *ociConfigHandler) createInstance(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
var req createInstanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
count := req.Count
if count == 0 {
count = 1
}
instances, failures, err := h.svc.CreateInstances(c.Request.Context(), id, oci.CreateInstanceInput{
Region: req.Region,
CompartmentID: req.CompartmentID,
AvailabilityDomain: req.AvailabilityDomain,
DisplayName: req.DisplayName,
Shape: req.Shape,
Ocpus: req.Ocpus,
MemoryInGBs: req.MemoryInGBs,
ImageID: req.ImageID,
BootVolumeID: req.BootVolumeID,
BootVolumeSizeGBs: req.BootVolumeSizeGBs,
BootVolumeVpusPerGB: req.BootVolumeVpusPerGB,
SubnetID: req.SubnetID,
AssignPublicIP: req.AssignPublicIP,
ReservedPublicIPID: req.ReservedPublicIPID,
AssignIpv6: req.AssignIpv6,
SSHPublicKey: req.SSHPublicKey,
RootPassword: req.RootPassword,
GenerateRootPassword: req.GenerateRootPassword,
UserData: req.UserData,
}, count)
if err != nil {
respondError(c, err)
return
}
// 全部失败视为执行失败,部分成功仍返回 201 并附带逐台失败信息
status := http.StatusCreated
if len(instances) == 0 && len(failures) > 0 {
status = http.StatusInternalServerError
}
// nil 切片会序列化为 null,显式空数组免去前端判空
if instances == nil {
instances = []oci.Instance{}
}
if failures == nil {
failures = []string{}
}
c.JSON(status, gin.H{"instances": instances, "errors": failures})
}
// @Summary 实例详情
// @Tags 计算
// @Param id path int true "配置 ID"
// @Param instanceId path string true "instanceId"
// @Success 200 {object} oci.Instance
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/instances/{instanceId} [get]
func (h *ociConfigHandler) getInstance(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
instance, err := h.svc.Instance(c.Request.Context(), id, c.Query("region"), c.Param("instanceId"))
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, instance)
}
// @Summary 更新实例
// @Tags 计算
// @Param id path int true "配置 ID"
// @Param instanceId path string true "instanceId"
// @Param body body updateInstanceRequest true "请求体"
// @Success 200 {object} oci.Instance
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/instances/{instanceId} [put]
func (h *ociConfigHandler) updateInstance(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
var req updateInstanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
instance, err := h.svc.UpdateInstance(c.Request.Context(), id, c.Param("instanceId"), oci.UpdateInstanceInput{
Region: req.Region,
DisplayName: req.DisplayName,
Shape: req.Shape,
Ocpus: req.Ocpus,
MemoryInGBs: req.MemoryInGBs,
})
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, instance)
}
// @Summary 终止实例
// @Tags 计算
// @Param id path int true "配置 ID"
// @Param instanceId path string true "instanceId"
// @Success 204 "无内容"
// @Failure 409 {object} errorResponse "该实例的生命周期操作正在处理中"
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/instances/{instanceId} [delete]
func (h *ociConfigHandler) terminateInstance(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
instanceID := c.Param("instanceId")
if !beginInstanceLifecycleOperation(instanceID) {
respondInstanceLifecycleBusy(c)
return
}
defer endInstanceLifecycleOperation(instanceID)
preserve := c.Query("preserveBootVolume") == "true"
if err := h.svc.TerminateInstance(c.Request.Context(), id, c.Query("region"), instanceID, preserve); err != nil {
respondError(c, err)
return
}
c.Status(http.StatusNoContent)
}
// instanceLifecycleGuard 按实例 OCID 互斥 HTTP 请求飞行期内的终止与电源操作。
var instanceLifecycleGuard sync.Map
func beginInstanceLifecycleOperation(instanceID string) bool {
_, busy := instanceLifecycleGuard.LoadOrStore(instanceID, struct{}{})
return !busy
}
func endInstanceLifecycleOperation(instanceID string) {
instanceLifecycleGuard.Delete(instanceID)
}
func respondInstanceLifecycleBusy(c *gin.Context) {
c.JSON(http.StatusConflict, gin.H{"error": "该实例的生命周期操作正在处理中"})
}
// @Summary 实例电源操作(启停/重启)
// @Tags 计算
// @Param id path int true "配置 ID"
// @Param instanceId path string true "instanceId"
// @Param body body instanceActionRequest true "请求体"
// @Success 200 {object} oci.Instance
// @Failure 409 {object} errorResponse "该实例的生命周期操作正在处理中"
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/action [post]
func (h *ociConfigHandler) instanceAction(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
var req instanceActionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
instanceID := c.Param("instanceId")
if !beginInstanceLifecycleOperation(instanceID) {
respondInstanceLifecycleBusy(c)
return
}
defer endInstanceLifecycleOperation(instanceID)
instance, err := h.svc.InstanceAction(c.Request.Context(), id, req.Region, instanceID, req.Action)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, instance)
}
// ---- 引导卷 ----
type updateBootVolumeRequest struct {
Region string `json:"region"`
DisplayName string `json:"displayName"`
SizeInGBs int64 `json:"sizeInGBs"`
VpusPerGB int64 `json:"vpusPerGB"`
}
// @Summary 引导卷列表
// @Tags 存储
// @Param id path int true "配置 ID"
// @Success 200 {array} oci.BootVolume
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/boot-volumes [get]
func (h *ociConfigHandler) listBootVolumes(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
volumes, err := h.svc.BootVolumes(c.Request.Context(), id, c.Query("region"), c.Query("availabilityDomain"), c.Query("compartmentId"))
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, volumes)
}
// @Summary 引导卷详情
// @Tags 存储
// @Param id path int true "配置 ID"
// @Param bootVolumeId path string true "bootVolumeId"
// @Success 200 {object} oci.BootVolume
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/boot-volumes/{bootVolumeId} [get]
func (h *ociConfigHandler) getBootVolume(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
volume, err := h.svc.BootVolume(c.Request.Context(), id, c.Query("region"), c.Param("bootVolumeId"))
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, volume)
}
// @Summary 更新引导卷
// @Tags 存储
// @Param id path int true "配置 ID"
// @Param bootVolumeId path string true "bootVolumeId"
// @Param body body updateBootVolumeRequest true "请求体"
// @Success 200 {object} oci.BootVolume
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/boot-volumes/{bootVolumeId} [put]
func (h *ociConfigHandler) updateBootVolume(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
var req updateBootVolumeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
volume, err := h.svc.UpdateBootVolume(c.Request.Context(), id, c.Param("bootVolumeId"), oci.UpdateBootVolumeInput{
Region: req.Region,
DisplayName: req.DisplayName,
SizeInGBs: req.SizeInGBs,
VpusPerGB: req.VpusPerGB,
})
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, volume)
}
// @Summary 删除引导卷
// @Tags 存储
// @Param id path int true "配置 ID"
// @Param bootVolumeId path string true "bootVolumeId"
// @Success 204 "无内容"
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/boot-volumes/{bootVolumeId} [delete]
func (h *ociConfigHandler) deleteBootVolume(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
if err := h.svc.DeleteBootVolume(c.Request.Context(), id, c.Query("region"), c.Param("bootVolumeId")); err != nil {
respondError(c, err)
return
}
c.Status(http.StatusNoContent)
}