374 lines
12 KiB
Go
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)
|
|
}
|