feat: Docker部署 + 云主机导入 + 终端优化
Docker 部署: - 新增根目录 docker-compose.yml(MySQL+Redis+前后端四容器) - 新增 frontend/Dockerfile(多阶段构建)和 nginx.conf(SPA+API反代) - 修复 backend/Dockerfile:GOPROXY国内加速、编译整个cmd包 云主机导入功能: - 新增 backend/internal/cloud 包:Provider接口 + 阿里云API客户端 - 新增 cloud_sync.go:区域同步/主机发现/导入 API - 重写 CloudServerImport.tsx:4步流程(选云→选账号→选区域→发现导入) - 修复导入外键约束失败(region_id/cloud_id 设为 NULL) 终端优化: - 修复双X关闭按钮重影(showCloseButton=false) - 修复最大化→最小化不恢复(ResizeObserver + 禁用CSS过渡) 其他: - GORM AutoMigrate 逐表迁移,避免索引冲突阻塞启动 - Makefile 更新为 AIOps + 新增 docker 命令组 - .env.example 移至 backend 目录
This commit is contained in:
1 parent
2e175daf80
commit
d0344c2f52
14 files changed
+1265
-200
No files matched your search
@@ -0,0 +1,277 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/cloud"
|
||||
"ezops/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CloudSyncHandler 云平台同步处理器
|
||||
type CloudSyncHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCloudSyncHandler(db *gorm.DB) *CloudSyncHandler {
|
||||
return &CloudSyncHandler{db: db}
|
||||
}
|
||||
|
||||
// SyncRegions 同步云账号的区域列表到本地 regions 表
|
||||
// POST /cloud-accounts/:id/sync-regions
|
||||
func (h *CloudSyncHandler) SyncRegions(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var account models.CloudAccount
|
||||
if err := h.db.First(&account, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云账号不存在"})
|
||||
return
|
||||
}
|
||||
if account.Status != "active" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "云账号未激活,无法同步"})
|
||||
return
|
||||
}
|
||||
|
||||
provider := cloud.NewProvider(account.Provider, account.AccessKey, account.SecretKey)
|
||||
if provider == nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "暂不支持的云平台: " + account.Provider})
|
||||
return
|
||||
}
|
||||
|
||||
cloudRegions, err := provider.ListRegions()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "同步区域失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert: 不存在则创建,存在则更新
|
||||
synced := 0
|
||||
for _, cr := range cloudRegions {
|
||||
var region models.Region
|
||||
result := h.db.Where("code = ? AND provider = ?", cr.RegionID, account.Provider).First(®ion)
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
// 新区域 → 创建
|
||||
h.db.Create(&models.Region{
|
||||
Name: cr.LocalName,
|
||||
Code: cr.RegionID,
|
||||
Provider: account.Provider,
|
||||
Endpoint: cr.Endpoint,
|
||||
Status: "active",
|
||||
})
|
||||
synced++
|
||||
} else if result.Error == nil {
|
||||
// 已存在 → 更新 endpoint 和名称
|
||||
region.Name = cr.LocalName
|
||||
region.Endpoint = cr.Endpoint
|
||||
h.db.Save(®ion)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新账号的 region_count 和 last_sync_at
|
||||
h.db.Model(&account).Updates(map[string]interface{}{
|
||||
"region_count": len(cloudRegions),
|
||||
"last_sync_at": time.Now(),
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "同步成功",
|
||||
Data: gin.H{
|
||||
"synced": synced,
|
||||
"total": len(cloudRegions),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetAccountRegions 获取指定云账号的区域列表(按 provider 过滤)
|
||||
// GET /cloud-accounts/:id/regions
|
||||
func (h *CloudSyncHandler) GetAccountRegions(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var account models.CloudAccount
|
||||
if err := h.db.First(&account, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云账号不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var regions []models.Region
|
||||
h.db.Where("provider = ?", account.Provider).Order("name ASC").Find(®ions)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
List: regions,
|
||||
Total: int64(len(regions)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// discoverRequest 发现主机请求
|
||||
type discoverRequest struct {
|
||||
Regions []string `json:"regions"`
|
||||
}
|
||||
|
||||
// DiscoverInstances 发现指定区域的云主机实例
|
||||
// POST /cloud-accounts/:id/discover-instances
|
||||
func (h *CloudSyncHandler) DiscoverInstances(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var account models.CloudAccount
|
||||
if err := h.db.First(&account, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云账号不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req discoverRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误"})
|
||||
return
|
||||
}
|
||||
if len(req.Regions) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请至少选择一个区域"})
|
||||
return
|
||||
}
|
||||
|
||||
provider := cloud.NewProvider(account.Provider, account.AccessKey, account.SecretKey)
|
||||
if provider == nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "暂不支持的云平台"})
|
||||
return
|
||||
}
|
||||
|
||||
var allInstances []cloud.CloudInstance
|
||||
var errors []string
|
||||
|
||||
for _, regionID := range req.Regions {
|
||||
instances, err := provider.ListInstances(regionID)
|
||||
if err != nil {
|
||||
errors = append(errors, regionID+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
allInstances = append(allInstances, instances...)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: gin.H{
|
||||
"list": allInstances,
|
||||
"total": len(allInstances),
|
||||
"errors": errors,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// importRequest 导入主机请求
|
||||
type importRequest struct {
|
||||
Instances []cloud.CloudInstance `json:"instances"`
|
||||
}
|
||||
|
||||
// ImportInstances 将发现的云主机实例导入为本地服务器资产
|
||||
// POST /cloud-accounts/:id/import-instances
|
||||
func (h *CloudSyncHandler) ImportInstances(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var account models.CloudAccount
|
||||
if err := h.db.First(&account, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云账号不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req importRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
imported := 0
|
||||
skipped := 0
|
||||
|
||||
for _, inst := range req.Instances {
|
||||
// 以 IP 去重(没有 IP 则用 InstanceID)
|
||||
ip := inst.PrivateIP
|
||||
if ip == "" {
|
||||
ip = inst.PublicIP
|
||||
}
|
||||
if ip == "" {
|
||||
ip = inst.InstanceID
|
||||
}
|
||||
|
||||
// 检查是否已存在(按 IP 或 hostname)
|
||||
var count int64
|
||||
h.db.Model(&models.Server{}).Where("ip = ? OR hostname = ?", ip, inst.Hostname).Count(&count)
|
||||
if count > 0 {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
// 查找 Region 关联
|
||||
var region models.Region
|
||||
var regionID *uint
|
||||
if h.db.Where("code = ? AND provider = ?", inst.RegionID, account.Provider).First(®ion).Error == nil {
|
||||
regionID = ®ion.ID
|
||||
}
|
||||
|
||||
osName := "linux"
|
||||
if inst.OSType != "" {
|
||||
osName = inst.OSType
|
||||
}
|
||||
status := "stopped"
|
||||
if inst.Status == "Running" {
|
||||
status = "running"
|
||||
}
|
||||
ipAddr := inst.PrivateIP
|
||||
if ipAddr == "" {
|
||||
ipAddr = inst.PublicIP
|
||||
}
|
||||
if ipAddr == "" {
|
||||
ipAddr = inst.InstanceID
|
||||
}
|
||||
hostname := inst.Hostname
|
||||
if hostname == "" {
|
||||
hostname = inst.InstanceName
|
||||
}
|
||||
if hostname == "" {
|
||||
hostname = inst.InstanceID
|
||||
}
|
||||
|
||||
// 注意:servers 表的 cloud_id 外键引用的是 clouds 表(非 cloud_accounts),
|
||||
// region_id 外键引用 regions 表。未找到关联时设为 NULL 避免外键约束失败。
|
||||
server := models.Server{
|
||||
Hostname: hostname,
|
||||
IP: ipAddr,
|
||||
PublicIP: inst.PublicIP,
|
||||
Port: 22,
|
||||
Username: "root",
|
||||
OS: osName,
|
||||
CPU: inst.CPU,
|
||||
Memory: int64(inst.Memory),
|
||||
Status: status,
|
||||
RegionID: regionID, // nil = NULL
|
||||
Description: "从 " + account.Provider + " 导入 (" + inst.InstanceID + ")",
|
||||
AgentStatus: "offline",
|
||||
}
|
||||
|
||||
if err := h.db.Create(&server).Error; err == nil {
|
||||
imported++
|
||||
} else {
|
||||
skipped++
|
||||
}
|
||||
}
|
||||
|
||||
// 更新账号的 server_count
|
||||
h.db.Model(&account).UpdateColumn("server_count", imported)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "导入完成",
|
||||
Data: gin.H{
|
||||
"imported": imported,
|
||||
"skipped": skipped,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -132,6 +132,13 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
||||
cloudAccounts.POST("", cloudAccountHandler.CreateCloudAccount)
|
||||
cloudAccounts.PUT("/:id", cloudAccountHandler.UpdateCloudAccount)
|
||||
cloudAccounts.DELETE("/:id", cloudAccountHandler.DeleteCloudAccount)
|
||||
|
||||
// 云同步路由 — 区域同步 / 主机发现 / 导入主机 // Logan.Li
|
||||
syncHandler := handlers.NewCloudSyncHandler(db)
|
||||
cloudAccounts.POST("/:id/sync-regions", syncHandler.SyncRegions)
|
||||
cloudAccounts.GET("/:id/regions", syncHandler.GetAccountRegions)
|
||||
cloudAccounts.POST("/:id/discover-instances", syncHandler.DiscoverInstances)
|
||||
cloudAccounts.POST("/:id/import-instances", syncHandler.ImportInstances)
|
||||
}
|
||||
|
||||
// 域名路由
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AliyunProvider 阿里云 ECS Provider
|
||||
type AliyunProvider struct {
|
||||
accessKey string
|
||||
secretKey string
|
||||
version string // API 版本
|
||||
}
|
||||
|
||||
func NewAliyunProvider(accessKey, secretKey string) *AliyunProvider {
|
||||
return &AliyunProvider{
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
version: "2014-05-26",
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AliyunProvider) Name() string { return "aliyun" }
|
||||
|
||||
// percentEncode 阿里云 RPC 签名专用 URL 编码
|
||||
func percentEncode(s string) string {
|
||||
s = url.QueryEscape(s)
|
||||
s = strings.ReplaceAll(s, "+", "%20")
|
||||
s = strings.ReplaceAll(s, "*", "%2A")
|
||||
s = strings.ReplaceAll(s, "%7E", "~")
|
||||
return s
|
||||
}
|
||||
|
||||
// sign 计算阿里云 RPC v1 HMAC-SHA1 签名
|
||||
func (a *AliyunProvider) sign(params map[string]string) string {
|
||||
// 1. 排序参数 key
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// 2. 拼接规范化查询字符串
|
||||
var pairs []string
|
||||
for _, k := range keys {
|
||||
pairs = append(pairs, percentEncode(k)+"="+percentEncode(params[k]))
|
||||
}
|
||||
canonicalized := strings.Join(pairs, "&")
|
||||
|
||||
// 3. 构造待签名字符串
|
||||
stringToSign := "GET&" + percentEncode("/") + "&" + percentEncode(canonicalized)
|
||||
|
||||
// 4. HMAC-SHA1
|
||||
mac := hmac.New(sha1.New, []byte(a.secretKey+"&"))
|
||||
mac.Write([]byte(stringToSign))
|
||||
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// call 调用阿里云 RPC API
|
||||
func (a *AliyunProvider) call(action string, extra map[string]string, endpoint string) ([]byte, error) {
|
||||
if endpoint == "" {
|
||||
endpoint = "https://ecs.aliyuncs.com/"
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"Format": "JSON",
|
||||
"Version": a.version,
|
||||
"AccessKeyId": a.accessKey,
|
||||
"SignatureMethod": "HMAC-SHA1",
|
||||
"Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
|
||||
"SignatureVersion": "1.0",
|
||||
"SignatureNonce": fmt.Sprintf("%d", rand.Int63()),
|
||||
"Action": action,
|
||||
}
|
||||
for k, v := range extra {
|
||||
params[k] = v
|
||||
}
|
||||
params["Signature"] = a.sign(params)
|
||||
|
||||
// 构造 GET URL
|
||||
var pairs []string
|
||||
for k, v := range params {
|
||||
pairs = append(pairs, url.QueryEscape(k)+"="+url.QueryEscape(v))
|
||||
}
|
||||
reqURL := endpoint + "?" + strings.Join(pairs, "&")
|
||||
|
||||
resp, err := http.Get(reqURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求阿里云 API 失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("阿里云 API 返回错误 (HTTP %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// aliyunRegionsResponse DescribeRegions 响应
|
||||
type aliyunRegionsResponse struct {
|
||||
Regions struct {
|
||||
Region []struct {
|
||||
RegionID string `json:"RegionId"`
|
||||
LocalName string `json:"LocalName"`
|
||||
RegionEndpoint string `json:"RegionEndpoint"`
|
||||
} `json:"Region"`
|
||||
} `json:"Regions"`
|
||||
}
|
||||
|
||||
// ListRegions 获取阿里云所有区域
|
||||
func (a *AliyunProvider) ListRegions() ([]CloudRegion, error) {
|
||||
body, err := a.call("DescribeRegions", nil, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp aliyunRegionsResponse
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return nil, fmt.Errorf("解析区域响应失败: %w", err)
|
||||
}
|
||||
|
||||
regions := make([]CloudRegion, 0, len(resp.Regions.Region))
|
||||
for _, r := range resp.Regions.Region {
|
||||
regions = append(regions, CloudRegion{
|
||||
RegionID: r.RegionID,
|
||||
LocalName: r.LocalName,
|
||||
Endpoint: r.RegionEndpoint,
|
||||
Status: "active",
|
||||
})
|
||||
}
|
||||
return regions, nil
|
||||
}
|
||||
|
||||
// aliyunInstancesResponse DescribeInstances 响应
|
||||
type aliyunInstancesResponse struct {
|
||||
TotalCount int `json:"TotalCount"`
|
||||
Instances struct {
|
||||
Instance []struct {
|
||||
InstanceID string `json:"InstanceId"`
|
||||
InstanceName string `json:"InstanceName"`
|
||||
HostName string `json:"HostName"`
|
||||
Status string `json:"Status"`
|
||||
RegionID string `json:"RegionId"`
|
||||
ZoneID string `json:"ZoneId"`
|
||||
InstanceType string `json:"InstanceType"`
|
||||
CPU int `json:"Cpu"`
|
||||
Memory int `json:"Memory"`
|
||||
OSType string `json:"OSType"`
|
||||
CreationTime string `json:"CreationTime"`
|
||||
|
||||
VPCAttributes struct {
|
||||
PrivateIPAddress struct {
|
||||
IPAddress []string `json:"IpAddress"`
|
||||
} `json:"PrivateIpAddress"`
|
||||
} `json:"VpcAttributes"`
|
||||
|
||||
PublicIPAddress struct {
|
||||
IPAddress []string `json:"IpAddress"`
|
||||
} `json:"PublicIpAddress"`
|
||||
|
||||
InnerIPAddress struct {
|
||||
IPAddress []string `json:"IpAddress"`
|
||||
} `json:"InnerIpAddress"`
|
||||
} `json:"Instance"`
|
||||
} `json:"Instances"`
|
||||
}
|
||||
|
||||
// ListInstances 获取指定区域的云主机实例
|
||||
func (a *AliyunProvider) ListInstances(regionID string) ([]CloudInstance, error) {
|
||||
endpoint := fmt.Sprintf("https://ecs.%s.aliyuncs.com/", regionID)
|
||||
|
||||
body, err := a.call("DescribeInstances", map[string]string{
|
||||
"RegionId": regionID,
|
||||
"PageSize": "100",
|
||||
}, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp aliyunInstancesResponse
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return nil, fmt.Errorf("解析实例响应失败: %w", err)
|
||||
}
|
||||
|
||||
instances := make([]CloudInstance, 0, len(resp.Instances.Instance))
|
||||
for _, inst := range resp.Instances.Instance {
|
||||
privateIP := ""
|
||||
if len(inst.VPCAttributes.PrivateIPAddress.IPAddress) > 0 {
|
||||
privateIP = inst.VPCAttributes.PrivateIPAddress.IPAddress[0]
|
||||
}
|
||||
publicIP := ""
|
||||
if len(inst.PublicIPAddress.IPAddress) > 0 {
|
||||
publicIP = inst.PublicIPAddress.IPAddress[0]
|
||||
}
|
||||
osType := inst.OSType
|
||||
if osType == "" {
|
||||
osType = "linux"
|
||||
}
|
||||
|
||||
instances = append(instances, CloudInstance{
|
||||
InstanceID: inst.InstanceID,
|
||||
InstanceName: inst.InstanceName,
|
||||
Hostname: inst.HostName,
|
||||
OSType: osType,
|
||||
Status: inst.Status,
|
||||
RegionID: inst.RegionID,
|
||||
ZoneID: inst.ZoneID,
|
||||
PrivateIP: privateIP,
|
||||
PublicIP: publicIP,
|
||||
InstanceType: inst.InstanceType,
|
||||
CPU: inst.CPU,
|
||||
Memory: inst.Memory,
|
||||
CreationTime: inst.CreationTime,
|
||||
})
|
||||
}
|
||||
return instances, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cloud
|
||||
|
||||
// CloudInstance 云主机实例(跨平台统一结构)
|
||||
type CloudInstance struct {
|
||||
InstanceID string `json:"instance_id"`
|
||||
InstanceName string `json:"instance_name"`
|
||||
Hostname string `json:"hostname"`
|
||||
OSType string `json:"os_type"`
|
||||
Status string `json:"status"`
|
||||
RegionID string `json:"region_id"`
|
||||
ZoneID string `json:"zone_id"`
|
||||
PrivateIP string `json:"private_ip"`
|
||||
PublicIP string `json:"public_ip"`
|
||||
InstanceType string `json:"instance_type"` // 规格如 ecs.g6.large
|
||||
CPU int `json:"cpu"`
|
||||
Memory int `json:"memory"` // MB
|
||||
CreationTime string `json:"creation_time"`
|
||||
}
|
||||
|
||||
// CloudRegion 云平台区域(跨平台统一结构)
|
||||
type CloudRegion struct {
|
||||
RegionID string `json:"region_id"`
|
||||
LocalName string `json:"local_name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// Provider 云平台 Provider 接口
|
||||
type Provider interface {
|
||||
// ListRegions 获取该云平台所有区域列表
|
||||
ListRegions() ([]CloudRegion, error)
|
||||
|
||||
// ListInstances 获取指定区域的云主机实例
|
||||
ListInstances(regionID string) ([]CloudInstance, error)
|
||||
|
||||
// Name 返回 provider 标识
|
||||
Name() string
|
||||
}
|
||||
|
||||
// NewProvider 根据 provider 字符串创建对应实例
|
||||
func NewProvider(provider, accessKey, secretKey string) Provider {
|
||||
switch provider {
|
||||
case "aliyun":
|
||||
return NewAliyunProvider(accessKey, secretKey)
|
||||
// case "tencent":
|
||||
// return NewTencentProvider(accessKey, secretKey)
|
||||
// case "huawei":
|
||||
// return NewHuaweiProvider(accessKey, secretKey)
|
||||
// case "aws":
|
||||
// return NewAWSProvider(accessKey, secretKey)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user