Files
oci-portal/internal/api/ociconfig.go
T
2026-07-09 19:18:04 +08:00

296 lines
8.8 KiB
Go

package api
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/oracle/oci-go-sdk/v65/common"
"gorm.io/gorm"
"oci-portal/internal/oci"
"oci-portal/internal/service"
)
type ociConfigHandler struct {
svc *service.OciConfigService
}
// importRequest 中 configIni 与显式字段二选一,私钥内容必填。
type importRequest struct {
Alias string `json:"alias" binding:"required"`
Group string `json:"group"` // 面板内自定义分组,可空
ConfigINI string `json:"configIni"`
TenancyOCID string `json:"tenancyOcid"`
UserOCID string `json:"userOcid"`
Region string `json:"region"`
Fingerprint string `json:"fingerprint"`
PrivateKey string `json:"privateKey" binding:"required"`
Passphrase string `json:"passphrase"`
MultiRegion bool `json:"multiRegion"` // 开启后订阅区域入库缓存
MultiCompartment bool `json:"multiCompartment"` // 开启后 compartment 入库缓存
ProxyID *uint `json:"proxyId"` // 关联出站代理,null 直连
}
// @Summary 导入租户配置
// @Tags 租户配置
// @Param body body importRequest true "API Key 配置(私钥密文落库)"
// @Success 201 {object} map[string]any
// @Security BearerAuth
// @Router /api/v1/oci-configs [post]
func (h *ociConfigHandler) create(c *gin.Context) {
var req importRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cfg, err := h.svc.Import(c.Request.Context(), service.ImportInput{
Alias: req.Alias,
Group: req.Group,
ConfigINI: req.ConfigINI,
TenancyOCID: req.TenancyOCID,
UserOCID: req.UserOCID,
Region: req.Region,
Fingerprint: req.Fingerprint,
PrivateKey: req.PrivateKey,
Passphrase: req.Passphrase,
MultiRegion: req.MultiRegion,
MultiCompartment: req.MultiCompartment,
ProxyID: req.ProxyID,
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, cfg)
}
// @Summary 租户配置列表
// @Tags 租户配置
// @Success 200 {object} map[string]any "items"
// @Security BearerAuth
// @Router /api/v1/oci-configs [get]
func (h *ociConfigHandler) list(c *gin.Context) {
items, err := h.svc.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
// @Summary 租户配置详情
// @Tags 租户配置
// @Param id path int true "配置 ID"
// @Success 200 {object} map[string]any
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id} [get]
func (h *ociConfigHandler) get(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
cfg, err := h.svc.Get(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, cfg)
}
// @Summary 验证配置连通性并刷新画像
// @Tags 租户配置
// @Param id path int true "配置 ID"
// @Success 200 {object} map[string]any
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/verify [post]
func (h *ociConfigHandler) verify(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
cfg, changes, err := h.svc.Verify(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"config": cfg, "changes": changes})
}
// updateConfigRequest 空字段不变更;替换私钥须同时给新指纹;
// passphrase / group 一经提供整体覆盖(空串分别表示清除口令 / 取消分组)。
type updateConfigRequest struct {
Alias string `json:"alias"`
Group *string `json:"group"`
Region string `json:"region"`
Fingerprint string `json:"fingerprint"`
PrivateKey string `json:"privateKey"`
Passphrase *string `json:"passphrase"`
MultiRegion *bool `json:"multiRegion"` // 非 null 时切换多区域支持
MultiCompartment *bool `json:"multiCompartment"` // 非 null 时切换多区间支持
ProxyID *uint `json:"proxyId"` // 非 null 切换代理:0 解除,>0 关联
}
// @Summary 更新租户配置
// @Tags 租户配置
// @Param id path int true "配置 ID"
// @Param body body object true "可更新字段(别名/分组/代理/多区域开关等)"
// @Success 200 {object} map[string]any
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id} [put]
func (h *ociConfigHandler) update(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
var req updateConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cfg, changes, err := h.svc.Update(c.Request.Context(), id, service.UpdateInput{
Alias: req.Alias,
Group: req.Group,
Region: req.Region,
Fingerprint: req.Fingerprint,
PrivateKey: req.PrivateKey,
Passphrase: req.Passphrase,
MultiRegion: req.MultiRegion,
MultiCompartment: req.MultiCompartment,
ProxyID: req.ProxyID,
})
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"config": cfg, "changes": changes})
}
// @Summary 删除租户配置
// @Tags 租户配置
// @Param id path int true "配置 ID"
// @Success 204 "无内容"
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id} [delete]
func (h *ociConfigHandler) remove(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
if err := h.svc.Delete(c.Request.Context(), id); err != nil {
respondError(c, err)
return
}
c.Status(http.StatusNoContent)
}
// compartments 列出租户下全部 ACTIVE compartment(不含租户根)。
//
// @Summary 列出租户下全部 ACTIVE compartment
// @Tags 租户配置
// @Param id path int true "配置 ID"
// @Success 200 {object} map[string]any
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/compartments [get]
func (h *ociConfigHandler) compartments(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
items, err := h.svc.Compartments(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, items)
}
// cachedRegions 返回筛选器用区域列表:未开多区域支持只含默认区域;
// 缓存存在非 READY 时实时刷新。
//
// @Summary 返回筛选器用区域列表:未开多区域支持只含默认区域
// @Tags 租户配置
// @Param id path int true "配置 ID"
// @Success 200 {object} map[string]any
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/cached-regions [get]
func (h *ociConfigHandler) cachedRegions(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
items, err := h.svc.CachedRegions(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, items)
}
// cachedCompartments 返回筛选器用区间列表:未开多区间支持返回空数组。
//
// @Summary 返回筛选器用区间列表:未开多区间支持返回空数组
// @Tags 租户配置
// @Param id path int true "配置 ID"
// @Success 200 {object} map[string]any
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/cached-compartments [get]
func (h *ociConfigHandler) cachedCompartments(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
items, err := h.svc.CachedCompartments(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, items)
}
func pathID(c *gin.Context) (uint, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return 0, false
}
return uint(id), true
}
func respondError(c *gin.Context, err error) {
status := http.StatusInternalServerError
if errors.Is(err, gorm.ErrRecordNotFound) {
status = http.StatusNotFound
}
var svcErr common.ServiceError
if errors.As(err, &svcErr) {
respondOCIError(c, err, svcErr)
return
}
c.JSON(status, gin.H{"error": err.Error()})
}
// respondOCIError 提炼 OCI 服务错误:透传 HTTP 状态码,
// error 只保留操作前缀 + 错误码 + 服务端消息(oci.CompactError)。
func respondOCIError(c *gin.Context, err error, svcErr common.ServiceError) {
status := svcErr.GetHTTPStatusCode()
if status < http.StatusBadRequest {
status = http.StatusBadGateway
}
// 面板的 401 专属本地 JWT 失效(前端收到即登出);
// 上游 OCI 的 401 是代理调用被拒,改用 502 透出
if status == http.StatusUnauthorized {
status = http.StatusBadGateway
}
body := gin.H{
"error": oci.CompactError(err),
"ociCode": svcErr.GetCode(),
"opcRequestId": svcErr.GetOpcRequestID(),
}
if hint := oci.ErrorHint(err); hint != "" {
body["hint"] = hint
}
c.JSON(status, body)
}