From 0946f732fa522c2d364655b0c439a67e47b3489d Mon Sep 17 00:00:00 2001 From: Bamboo <13664854532@163.com> Date: Wed, 8 Oct 2025 14:01:08 +0800 Subject: [PATCH 1/8] fix: update --- internal/k8s/manager/pod_manager.go | 14 +--- internal/tree/dao/tree_local_dao.go | 5 +- internal/tree/dao/tree_node_dao.go | 32 ++------ internal/tree/service/tree_local_service.go | 74 ++++------------- internal/tree/service/tree_node_service.go | 25 +++--- internal/tree/utils/crypto_util.go | 69 ++++++++++++++++ internal/tree/utils/tree_util.go | 59 +++++++++++++ internal/tree/utils/validation_util.go | 91 +++++++++++++++++++++ 8 files changed, 260 insertions(+), 109 deletions(-) create mode 100644 internal/tree/utils/crypto_util.go create mode 100644 internal/tree/utils/tree_util.go create mode 100644 internal/tree/utils/validation_util.go diff --git a/internal/k8s/manager/pod_manager.go b/internal/k8s/manager/pod_manager.go index fdf035c9..195bf5bb 100644 --- a/internal/k8s/manager/pod_manager.go +++ b/internal/k8s/manager/pod_manager.go @@ -210,7 +210,6 @@ func (m *podManager) DeletePod(ctx context.Context, clusterID int, namespace, na func (m *podManager) GetPodLogs(ctx context.Context, clusterID int, namespace, name string, logOptions *corev1.PodLogOptions) (io.ReadCloser, error) { kubeClient, err := m.getKubeClient(clusterID) - if err != nil { return nil, err } @@ -230,18 +229,14 @@ func (m *podManager) GetPodLogs(ctx context.Context, clusterID int, namespace, n } // BatchDeletePods 批量删除 Pod -// 使用并发+重试机制提高批量操作的效率和可靠性 -// 并发度为3是经过测试的平衡值:既能提高性能,又不会对API Server造成过大压力 func (m *podManager) BatchDeletePods(ctx context.Context, clusterID int, namespace string, podNames []string, deleteOpts metav1.DeleteOptions) error { kubeClient, err := m.getKubeClient(clusterID) - if err != nil { return err } tasks := make([]retry.WrapperTask, 0, len(podNames)) for _, name := range podNames { - tasks = append(tasks, retry.WrapperTask{ Backoff: retry.DefaultBackoff, @@ -279,7 +274,6 @@ func (m *podManager) PodTerminalSession( namespace, pod, container, shell string, conn *websocket.Conn, ) error { - kubeClient, err := m.clientFactory.GetKubeClient(clusterID) if err != nil { return err @@ -301,7 +295,6 @@ func (m *podManager) PodTerminalSession( } func (m *podManager) UploadFileToPod(ctx *gin.Context, clusterID int, namespace, pod, container, filePath string) error { - if namespace == "" { return fmt.Errorf("命名空间不能为空") } @@ -440,7 +433,6 @@ func (m *podManager) UploadFileToPod(ctx *gin.Context, clusterID int, namespace, Stderr: &uploadStderr, TerminalSizeQueue: nil, }) - if err != nil { m.logger.Error("执行上传失败", zap.Error(err), @@ -463,7 +455,6 @@ func (m *podManager) UploadFileToPod(ctx *gin.Context, clusterID int, namespace, } func (m *podManager) PortForward(ctx context.Context, ports []string, dialer httpstream.Dialer) error { - // 创建 PortForwarder stopChan := make(chan struct{}, 1) readyChan := make(chan struct{}) @@ -574,7 +565,6 @@ func (m *podManager) PodPortForward(ctx context.Context, clusterID int, namespac } func (m *podManager) DownloadPodFile(ctx context.Context, clusterID int, namespace, pod, container, filePath string) (*k8sutils.PodFileStreamPipe, error) { - if namespace == "" { return nil, fmt.Errorf("命名空间不能为空") } @@ -631,7 +621,6 @@ func (m *podManager) DownloadPodFile(ctx context.Context, clusterID int, namespa reader, err := k8sutils.NewPodFileStreamPipe( ctx, restConfig, kubeClient, namespace, pod, container, filePath) - if err != nil { m.logger.Error("创建Pod文件流失败", zap.Error(err), @@ -776,7 +765,7 @@ func writeFilesToTar(files []fileWithHeader, w io.Writer) error { hdr := &tar.Header{ Name: filename, - Mode: 0644, + Mode: 0o644, Size: fileInfo.header.Size, } @@ -796,7 +785,6 @@ func writeFilesToTar(files []fileWithHeader, w io.Writer) error { return nil }(f, i) - if err != nil { return err } diff --git a/internal/tree/dao/tree_local_dao.go b/internal/tree/dao/tree_local_dao.go index 01d6cabc..cb9d8f89 100644 --- a/internal/tree/dao/tree_local_dao.go +++ b/internal/tree/dao/tree_local_dao.go @@ -30,6 +30,7 @@ import ( "errors" "github.com/GoSimplicity/AI-CloudOps/internal/model" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" "go.uber.org/zap" "gorm.io/gorm" ) @@ -172,7 +173,7 @@ func (d *treeLocalDAO) BatchGetByIDs(ctx context.Context, ids []int) ([]*model.T // BindTreeNodes 绑定树节点 func (d *treeLocalDAO) BindTreeNodes(ctx context.Context, localID int, treeNodeIds []int) error { - if len(treeNodeIds) == 0 { + if !treeUtils.ValidateTreeNodeIDs(treeNodeIds) { d.logger.Info("没有需要绑定的树节点") return nil } @@ -202,7 +203,7 @@ func (d *treeLocalDAO) BindTreeNodes(ctx context.Context, localID int, treeNodeI } func (d *treeLocalDAO) UnBindTreeNodes(ctx context.Context, localID int, treeNodeIds []int) error { - if len(treeNodeIds) == 0 { + if !treeUtils.ValidateTreeNodeIDs(treeNodeIds) { d.logger.Info("没有需要解绑的树节点") return nil } diff --git a/internal/tree/dao/tree_node_dao.go b/internal/tree/dao/tree_node_dao.go index 6a650ab3..3f0cfed7 100644 --- a/internal/tree/dao/tree_node_dao.go +++ b/internal/tree/dao/tree_node_dao.go @@ -31,6 +31,7 @@ import ( "strings" "github.com/GoSimplicity/AI-CloudOps/internal/model" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" "go.uber.org/zap" "gorm.io/gorm" ) @@ -110,31 +111,7 @@ func (t *treeNodeDAO) GetTreeList(ctx context.Context, req *model.GetTreeNodeLis } // 构建树形结构 - return t.buildTreeStructure(nodes), count, nil -} - -// buildTreeStructure 构建树形结构 -func (t *treeNodeDAO) buildTreeStructure(nodes []*model.TreeNode) []*model.TreeNode { - nodeMap := make(map[int]*model.TreeNode) - var rootNodes []*model.TreeNode - - for _, node := range nodes { - nodeClone := *node - nodeClone.Children = make([]*model.TreeNode, 0) - nodeMap[node.ID] = &nodeClone - } - - for _, node := range nodes { - currentNode := nodeMap[node.ID] - if node.ParentID == 0 || nodeMap[node.ParentID] == nil { - rootNodes = append(rootNodes, currentNode) - } else { - parent := nodeMap[node.ParentID] - parent.Children = append(parent.Children, currentNode) - } - } - - return rootNodes + return treeUtils.BuildTreeStructure(nodes), count, nil } // GetNode 获取节点详情 @@ -464,6 +441,11 @@ func (t *treeNodeDAO) DeleteNode(ctx context.Context, id int) error { // BindResource 绑定资源到节点 func (t *treeNodeDAO) BindResource(ctx context.Context, nodeId int, resourceIds []int) error { + // 验证资源ID列表 + if err := treeUtils.ValidateResourceIDs(resourceIds); err != nil { + return err + } + // 验证节点存在 var count int64 if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("id = ?", nodeId).Count(&count).Error; err != nil { diff --git a/internal/tree/service/tree_local_service.go b/internal/tree/service/tree_local_service.go index 33bd47c6..44570a51 100644 --- a/internal/tree/service/tree_local_service.go +++ b/internal/tree/service/tree_local_service.go @@ -32,9 +32,8 @@ import ( "github.com/GoSimplicity/AI-CloudOps/internal/model" "github.com/GoSimplicity/AI-CloudOps/internal/tree/dao" - "github.com/GoSimplicity/AI-CloudOps/pkg/utils" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" "github.com/imdario/mergo" - "github.com/spf13/viper" "go.uber.org/zap" "gorm.io/gorm" ) @@ -65,12 +64,7 @@ func NewTreeLocalService(logger *zap.Logger, dao dao.TreeLocalDAO) TreeLocalServ // GetTreeLocalList 获取本地主机列表 func (s *treeLocalService) GetTreeLocalList(ctx context.Context, req *model.GetTreeLocalResourceListReq) (model.ListResp[*model.TreeLocalResource], error) { // 兜底分页参数,避免offset为负或size为0 - if req.Page <= 0 { - req.Page = 1 - } - if req.Size <= 0 { - req.Size = 10 - } + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) locals, total, err := s.dao.GetList(ctx, req) if err != nil { s.logger.Error("获取本地主机列表失败", zap.Error(err)) @@ -85,8 +79,8 @@ func (s *treeLocalService) GetTreeLocalList(ctx context.Context, req *model.GetT // GetTreeLocalDetail 获取本地主机详情 func (s *treeLocalService) GetTreeLocalDetail(ctx context.Context, req *model.GetTreeLocalResourceDetailReq) (*model.TreeLocalResource, error) { - if req.ID <= 0 { - return nil, errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return nil, fmt.Errorf("无效的主机ID: %w", err) } local, err := s.dao.GetByID(ctx, req.ID) @@ -103,8 +97,8 @@ func (s *treeLocalService) GetTreeLocalDetail(ctx context.Context, req *model.Ge // GetTreeLocalForConnection 获取用于连接的本地主机详情(包含解密后的密码) func (s *treeLocalService) GetTreeLocalForConnection(ctx context.Context, req *model.GetTreeLocalResourceDetailReq) (*model.TreeLocalResource, error) { - if req.ID <= 0 { - return nil, errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return nil, fmt.Errorf("无效的主机ID: %w", err) } local, err := s.dao.GetByID(ctx, req.ID) @@ -118,7 +112,7 @@ func (s *treeLocalService) GetTreeLocalForConnection(ctx context.Context, req *m // 解密密码以供连接使用 if local.AuthMode == model.AuthModePassword && local.Password != "" { - plainPassword, err := s.decryptPassword(local.Password) + plainPassword, err := treeUtils.DecryptPassword(local.Password) if err != nil { s.logger.Error("密码解密失败", zap.Int("id", req.ID), zap.Error(err)) return nil, fmt.Errorf("密码解密失败: %w", err) @@ -166,7 +160,7 @@ func (s *treeLocalService) CreateTreeLocal(ctx context.Context, req *model.Creat // 加密 if local.AuthMode == model.AuthModePassword && req.Password != "" { - encryptedPassword, err := s.encryptPassword(req.Password) + encryptedPassword, err := treeUtils.EncryptPassword(req.Password) if err != nil { s.logger.Error("密码加密失败", zap.Error(err)) return fmt.Errorf("密码加密失败: %w", err) @@ -183,8 +177,8 @@ func (s *treeLocalService) CreateTreeLocal(ctx context.Context, req *model.Creat } func (s *treeLocalService) UpdateTreeLocal(ctx context.Context, req *model.UpdateTreeLocalResourceReq) error { - if req.ID <= 0 { - return errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的主机ID: %w", err) } // 检查是否存在 @@ -224,7 +218,7 @@ func (s *treeLocalService) UpdateTreeLocal(ctx context.Context, req *model.Updat // 加密密码 if req.AuthMode == model.AuthModePassword && req.Password != "" { - pwd, err := s.encryptPassword(req.Password) + pwd, err := treeUtils.EncryptPassword(req.Password) if err != nil { s.logger.Error("密码加密失败", zap.Error(err)) return fmt.Errorf("密码加密失败: %w", err) @@ -252,8 +246,8 @@ func (s *treeLocalService) UpdateTreeLocal(ctx context.Context, req *model.Updat // DeleteTreeLocal 删除本地主机 func (s *treeLocalService) DeleteTreeLocal(ctx context.Context, req *model.DeleteTreeLocalResourceReq) error { - if req.ID <= 0 { - return errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的主机ID: %w", err) } if err := s.dao.Delete(ctx, req.ID); err != nil { @@ -268,8 +262,8 @@ func (s *treeLocalService) DeleteTreeLocal(ctx context.Context, req *model.Delet } func (s *treeLocalService) BindTreeLocal(ctx context.Context, req *model.BindTreeLocalResourceReq) error { - if req.ID <= 0 { - return errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的主机ID: %w", err) } if err := s.dao.BindTreeNodes(ctx, req.ID, req.TreeNodeIDs); err != nil { @@ -281,8 +275,8 @@ func (s *treeLocalService) BindTreeLocal(ctx context.Context, req *model.BindTre } func (s *treeLocalService) UnBindLocalResource(ctx context.Context, req *model.UnBindTreeLocalResourceReq) error { - if req.ID <= 0 { - return errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的主机ID: %w", err) } if err := s.dao.UnBindTreeNodes(ctx, req.ID, req.TreeNodeIDs); err != nil { @@ -292,37 +286,3 @@ func (s *treeLocalService) UnBindLocalResource(ctx context.Context, req *model.U return nil } - -// encryptPassword 加密密码 -func (s *treeLocalService) encryptPassword(password string) (string, error) { - if password == "" { - return "", nil - } - - encryptionKey := viper.GetString("tree.password_encryption_key") - if encryptionKey == "" { - return "", errors.New("未配置密码加密密钥") - } - if len(encryptionKey) != 32 { - return "", errors.New("密码加密密钥长度必须为32字节") - } - - return utils.EncryptSecretKey(password, []byte(encryptionKey)) -} - -// decryptPassword 解密密码 -func (s *treeLocalService) decryptPassword(encryptedPassword string) (string, error) { - if encryptedPassword == "" { - return "", nil - } - - encryptionKey := viper.GetString("tree.password_encryption_key") - if encryptionKey == "" { - return "", errors.New("未配置密码加密密钥") - } - if len(encryptionKey) != 32 { - return "", errors.New("密码加密密钥长度必须为32字节") - } - - return utils.DecryptSecretKey(encryptedPassword, []byte(encryptionKey)) -} diff --git a/internal/tree/service/tree_node_service.go b/internal/tree/service/tree_node_service.go index 4078a428..b5204035 100644 --- a/internal/tree/service/tree_node_service.go +++ b/internal/tree/service/tree_node_service.go @@ -32,6 +32,7 @@ import ( "github.com/GoSimplicity/AI-CloudOps/internal/model" "github.com/GoSimplicity/AI-CloudOps/internal/tree/dao" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" userDao "github.com/GoSimplicity/AI-CloudOps/internal/user/dao" "go.uber.org/zap" ) @@ -118,8 +119,8 @@ func (s *treeService) GetNodeDetail(ctx context.Context, id int) (*model.TreeNod // GetChildNodes 获取直接子节点 func (s *treeService) GetChildNodes(ctx context.Context, parentID int) ([]*model.TreeNode, error) { - if parentID < 0 { - return nil, errors.New("父节点ID无效") + if err := treeUtils.ValidateParentID(parentID); err != nil { + return nil, err } return s.dao.GetChildNodes(ctx, parentID) } @@ -179,12 +180,12 @@ func (s *treeService) DeleteNode(ctx context.Context, id int) error { // MoveNode 移动节点 func (s *treeService) MoveNode(ctx context.Context, nodeId, newParentId int) error { - if newParentId < 0 { - return errors.New("新父节点ID不能为负数") + if err := treeUtils.ValidateParentID(newParentId); err != nil { + return err } - if nodeId == newParentId { - return errors.New("节点不能移动到自己") + if err := treeUtils.ValidateNodeMove(nodeId, newParentId); err != nil { + return err } s.logger.Info("移动节点", zap.Int("nodeId", nodeId), zap.Int("newParentId", newParentId)) @@ -209,8 +210,8 @@ func (s *treeService) MoveNode(ctx context.Context, nodeId, newParentId int) err // GetNodeMembers 获取节点成员列表 func (s *treeService) GetNodeMembers(ctx context.Context, nodeId int, memberType string) (model.ListResp[*model.User], error) { - if memberType != "" && memberType != NodeAdminRole && memberType != NodeMemberRole && memberType != "all" { - return model.ListResp[*model.User]{}, errors.New("成员类型只能是admin、member或all") + if err := treeUtils.ValidateMemberType(memberType); err != nil { + return model.ListResp[*model.User]{}, err } s.logger.Debug("获取节点成员", zap.Int("nodeId", nodeId), zap.String("memberType", memberType)) @@ -257,8 +258,8 @@ func (s *treeService) RemoveNodeMember(ctx context.Context, req *model.RemoveTre // BindResource 绑定资源到节点 func (s *treeService) BindResource(ctx context.Context, req *model.BindTreeNodeResourceReq) error { - if len(req.ResourceIDs) == 0 { - return errors.New("资源ID列表不能为空") + if err := treeUtils.ValidateResourceIDs(req.ResourceIDs); err != nil { + return err } return s.dao.BindResource(ctx, req.NodeID, req.ResourceIDs) @@ -266,8 +267,8 @@ func (s *treeService) BindResource(ctx context.Context, req *model.BindTreeNodeR // UnbindResource 解绑资源 func (s *treeService) UnbindResource(ctx context.Context, req *model.UnbindTreeNodeResourceReq) error { - if req.ResourceID <= 0 { - return errors.New("资源ID不能为空或小于等于0") + if err := treeUtils.ValidateID(req.ResourceID); err != nil { + return err } s.logger.Info("解绑资源", diff --git a/internal/tree/utils/crypto_util.go b/internal/tree/utils/crypto_util.go new file mode 100644 index 00000000..650bb59e --- /dev/null +++ b/internal/tree/utils/crypto_util.go @@ -0,0 +1,69 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import ( + "errors" + + pkgUtils "github.com/GoSimplicity/AI-CloudOps/pkg/utils" + "github.com/spf13/viper" +) + +// EncryptPassword 加密密码 +// 使用配置文件中的加密密钥对密码进行加密 +func EncryptPassword(password string) (string, error) { + if password == "" { + return "", nil + } + + encryptionKey := viper.GetString("tree.password_encryption_key") + if encryptionKey == "" { + return "", errors.New("未配置密码加密密钥") + } + if len(encryptionKey) != 32 { + return "", errors.New("密码加密密钥长度必须为32字节") + } + + return pkgUtils.EncryptSecretKey(password, []byte(encryptionKey)) +} + +// DecryptPassword 解密密码 +// 使用配置文件中的加密密钥对加密后的密码进行解密 +func DecryptPassword(encryptedPassword string) (string, error) { + if encryptedPassword == "" { + return "", nil + } + + encryptionKey := viper.GetString("tree.password_encryption_key") + if encryptionKey == "" { + return "", errors.New("未配置密码加密密钥") + } + if len(encryptionKey) != 32 { + return "", errors.New("密码加密密钥长度必须为32字节") + } + + return pkgUtils.DecryptSecretKey(encryptedPassword, []byte(encryptionKey)) +} diff --git a/internal/tree/utils/tree_util.go b/internal/tree/utils/tree_util.go new file mode 100644 index 00000000..415f6ab0 --- /dev/null +++ b/internal/tree/utils/tree_util.go @@ -0,0 +1,59 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import "github.com/GoSimplicity/AI-CloudOps/internal/model" + +// BuildTreeStructure 构建树形结构 +// 将扁平化的节点列表转换为具有父子关系的树形结构 +// 返回根节点列表,每个节点的Children字段包含其子节点 +func BuildTreeStructure(nodes []*model.TreeNode) []*model.TreeNode { + // 创建节点映射表,用于快速查找节点 + nodeMap := make(map[int]*model.TreeNode) + var rootNodes []*model.TreeNode + + // 第一遍遍历:复制节点并初始化Children字段 + for _, node := range nodes { + nodeClone := *node + nodeClone.Children = make([]*model.TreeNode, 0) + nodeMap[node.ID] = &nodeClone + } + + // 第二遍遍历:建立父子关系 + for _, node := range nodes { + currentNode := nodeMap[node.ID] + // 如果节点没有父节点或父节点不存在,则为根节点 + if node.ParentID == 0 || nodeMap[node.ParentID] == nil { + rootNodes = append(rootNodes, currentNode) + } else { + // 将当前节点添加到其父节点的Children列表中 + parent := nodeMap[node.ParentID] + parent.Children = append(parent.Children, currentNode) + } + } + + return rootNodes +} diff --git a/internal/tree/utils/validation_util.go b/internal/tree/utils/validation_util.go new file mode 100644 index 00000000..cd4e88c5 --- /dev/null +++ b/internal/tree/utils/validation_util.go @@ -0,0 +1,91 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import "errors" + +// ValidateAndSetPaginationDefaults 验证并设置分页参数的默认值 +// 如果page <= 0,设置为1 +// 如果size <= 0,设置为10 +func ValidateAndSetPaginationDefaults(page, size *int) { + if *page <= 0 { + *page = 1 + } + if *size <= 0 { + *size = 10 + } +} + +// ValidateID 验证ID是否有效 +// ID必须大于0 +func ValidateID(id int) error { + if id <= 0 { + return errors.New("无效的ID") + } + return nil +} + +// ValidateParentID 验证父节点ID是否有效 +// 父节点ID不能为负数 +func ValidateParentID(parentID int) error { + if parentID < 0 { + return errors.New("父节点ID不能为负数") + } + return nil +} + +// ValidateNodeMove 验证节点移动操作 +// 节点不能移动到自己 +func ValidateNodeMove(nodeID, newParentID int) error { + if nodeID == newParentID { + return errors.New("节点不能移动到自己") + } + return nil +} + +// ValidateMemberType 验证成员类型 +// 成员类型只能是 admin、member 或 all +func ValidateMemberType(memberType string) error { + if memberType != "" && memberType != "admin" && memberType != "member" && memberType != "all" { + return errors.New("成员类型只能是admin、member或all") + } + return nil +} + +// ValidateResourceIDs 验证资源ID列表 +// 资源ID列表不能为空 +func ValidateResourceIDs(resourceIDs []int) error { + if len(resourceIDs) == 0 { + return errors.New("资源ID列表不能为空") + } + return nil +} + +// ValidateTreeNodeIDs 验证树节点ID列表 +// 如果列表为空,返回false(表示没有需要处理的节点) +func ValidateTreeNodeIDs(treeNodeIDs []int) bool { + return len(treeNodeIDs) > 0 +} From 1b43bcb3ba9045b01e92d809b4998f14c1d59542 Mon Sep 17 00:00:00 2001 From: Bamboo <13664854532@163.com> Date: Wed, 8 Oct 2025 21:28:21 +0800 Subject: [PATCH 2/8] feat: Implement cloud account and tree cloud resource management with CRUD operations and validation --- internal/model/cloud_account.go | 112 +++++ internal/model/tree_cloud.go | 262 ++++++++++ internal/model/tree_local.go | 80 ++-- internal/model/tree_node.go | 25 +- internal/tree/api/cloud_account_handler.go | 164 +++++++ internal/tree/api/tree_cloud_handler.go | 324 +++++++++++++ internal/tree/api/tree_local_handler.go | 2 +- internal/tree/dao/cloud_account_dao.go | 176 +++++++ internal/tree/dao/tree_cloud_dao.go | 312 ++++++++++++ .../tree/service/cloud_account_service.go | 281 +++++++++++ internal/tree/service/tree_cloud_service.go | 451 ++++++++++++++++++ internal/tree/service/tree_local_service.go | 12 +- internal/tree/utils/cloud_account_util.go | 145 ++++++ .../utils/{crypto_util.go => common_util.go} | 35 +- internal/tree/utils/tree_cloud_util.go | 161 +++++++ .../utils/{tree_util.go => tree_node_util.go} | 40 +- internal/tree/utils/validation_util.go | 91 ---- pkg/di/init.go | 4 +- pkg/di/web.go | 4 + pkg/di/wire.go | 6 + pkg/di/wire_gen.go | 14 +- 21 files changed, 2537 insertions(+), 164 deletions(-) create mode 100644 internal/model/cloud_account.go create mode 100644 internal/model/tree_cloud.go create mode 100644 internal/tree/api/cloud_account_handler.go create mode 100644 internal/tree/api/tree_cloud_handler.go create mode 100644 internal/tree/dao/cloud_account_dao.go create mode 100644 internal/tree/dao/tree_cloud_dao.go create mode 100644 internal/tree/service/cloud_account_service.go create mode 100644 internal/tree/service/tree_cloud_service.go create mode 100644 internal/tree/utils/cloud_account_util.go rename internal/tree/utils/{crypto_util.go => common_util.go} (77%) create mode 100644 internal/tree/utils/tree_cloud_util.go rename internal/tree/utils/{tree_util.go => tree_node_util.go} (69%) delete mode 100644 internal/tree/utils/validation_util.go diff --git a/internal/model/cloud_account.go b/internal/model/cloud_account.go new file mode 100644 index 00000000..995097a6 --- /dev/null +++ b/internal/model/cloud_account.go @@ -0,0 +1,112 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package model + +// CloudAccountStatus 云账户状态 +type CloudAccountStatus int8 + +const ( + CloudAccountEnabled CloudAccountStatus = iota + 1 // 启用 + CloudAccountDisabled // 禁用 +) + +// CloudAccount 云账户管理 +type CloudAccount struct { + Model + Name string `json:"name" gorm:"type:varchar(100);not null;comment:账户名称"` + Provider CloudProvider `json:"provider" gorm:"type:tinyint(1);not null;comment:云厂商类型;default:1"` + Region string `json:"region" gorm:"type:varchar(50);not null;comment:区域,如cn-hangzhou"` + AccessKey string `json:"-" gorm:"type:varchar(500);not null;comment:访问密钥ID,加密存储"` + SecretKey string `json:"-" gorm:"type:varchar(500);not null;comment:访问密钥Secret,加密存储"` + AccountID string `json:"account_id" gorm:"type:varchar(100);comment:云账号ID"` + AccountName string `json:"account_name" gorm:"type:varchar(100);comment:云账号名称"` + AccountAlias string `json:"account_alias" gorm:"type:varchar(100);comment:账号别名"` + Description string `json:"description" gorm:"type:text;comment:账户描述"` + Status CloudAccountStatus `json:"status" gorm:"type:tinyint(1);not null;comment:账户状态,1:启用,2:禁用;default:1"` + CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` + CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` + CloudResources []*TreeCloudResource `json:"cloud_resources,omitempty" gorm:"foreignKey:CloudAccountID"` +} + +func (c *CloudAccount) TableName() string { + return "cl_cloud_account" +} + +// GetCloudAccountListReq 获取云账户列表请求 +type GetCloudAccountListReq struct { + ListReq + Provider CloudProvider `json:"provider" form:"provider" binding:"omitempty,oneof=1 2 3 4 5 6"` + Region string `json:"region" form:"region"` + Status CloudAccountStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2"` +} + +// GetCloudAccountDetailReq 获取云账户详情请求 +type GetCloudAccountDetailReq struct { + ID int `json:"id" form:"id" binding:"required,gt=0"` +} + +// CreateCloudAccountReq 创建云账户请求 +type CreateCloudAccountReq struct { + Name string `json:"name" binding:"required"` + Provider CloudProvider `json:"provider" binding:"required,oneof=1 2 3 4 5 6"` + Region string `json:"region" binding:"required"` + AccessKey string `json:"access_key" binding:"required"` + SecretKey string `json:"secret_key" binding:"required"` + AccountID string `json:"account_id"` + AccountName string `json:"account_name"` + AccountAlias string `json:"account_alias"` + Description string `json:"description"` + CreateUserID int `json:"create_user_id"` + CreateUserName string `json:"create_user_name"` +} + +// UpdateCloudAccountReq 更新云账户请求 +type UpdateCloudAccountReq struct { + ID int `json:"id" binding:"required,gt=0"` + Name string `json:"name"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + AccountID string `json:"account_id"` + AccountName string `json:"account_name"` + AccountAlias string `json:"account_alias"` + Description string `json:"description"` +} + +// DeleteCloudAccountReq 删除云账户请求 +type DeleteCloudAccountReq struct { + ID int `json:"id" binding:"required,gt=0"` +} + +// UpdateCloudAccountStatusReq 更新云账户状态请求 +type UpdateCloudAccountStatusReq struct { + ID int `json:"id" binding:"required,gt=0"` + Status CloudAccountStatus `json:"status" binding:"required,oneof=1 2"` +} + +// VerifyCloudAccountReq 验证云账户凭证请求 +type VerifyCloudAccountReq struct { + ID int `json:"id" binding:"required,gt=0"` +} diff --git a/internal/model/tree_cloud.go b/internal/model/tree_cloud.go new file mode 100644 index 00000000..61af21f0 --- /dev/null +++ b/internal/model/tree_cloud.go @@ -0,0 +1,262 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package model + +import "time" + +// CloudProvider 云厂商类型 +type CloudProvider int8 + +const ( + ProviderAliyun CloudProvider = iota + 1 // 阿里云 + ProviderTencent // 腾讯云 + ProviderAWS // AWS + ProviderHuawei // 华为云 + ProviderAzure // Azure + ProviderGCP // Google Cloud +) + +// CloudResourceType 云资源类型 +type CloudResourceType int8 + +const ( + ResourceTypeECS CloudResourceType = iota + 1 // 云服务器 + ResourceTypeRDS // 云数据库 + ResourceTypeSLB // 负载均衡 + ResourceTypeOSS // 对象存储 + ResourceTypeVPC // 虚拟私有云 + ResourceTypeOther // 其他资源 +) + +// CloudResourceStatus 云资源状态 +type CloudResourceStatus int8 + +const ( + CloudResourceRunning CloudResourceStatus = iota + 1 // 运行中 + CloudResourceStopped // 已停止 + CloudResourceStarting // 启动中 + CloudResourceStopping // 停止中 + CloudResourceDeleted // 已删除 + CloudResourceUnknown // 未知状态 +) + +// Currency 货币单位 +type Currency string + +const ( + CurrencyCNY Currency = "CNY" // 人民币 + CurrencyUSD Currency = "USD" // 美元 +) + +// ChargeType 计费方式 +type ChargeType string + +const ( + ChargeTypePostPaid ChargeType = "PostPaid" // 按量付费 + ChargeTypePrePaid ChargeType = "PrePaid" // 包年包月 +) + +// SyncMode 同步模式 +type SyncMode string + +const ( + SyncModeFull SyncMode = "full" // 全量同步 + SyncModeIncremental SyncMode = "incremental" // 增量同步 +) + +// TreeCloudResource 云资源管理 +type TreeCloudResource struct { + Model + + Name string `json:"name" gorm:"type:varchar(100);not null;comment:资源名称"` + ResourceType CloudResourceType `json:"resource_type" gorm:"type:tinyint(1);not null;comment:资源类型;default:1"` + Status CloudResourceStatus `json:"status" gorm:"type:tinyint(1);not null;comment:资源状态;default:1"` + Environment string `json:"environment" gorm:"type:varchar(50);comment:环境标识(dev/test/prod)"` + Description string `json:"description" gorm:"type:text;comment:资源描述"` + Tags KeyValueList `json:"tags" gorm:"type:text;serializer:json;comment:资源标签集合"` + CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` + CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` + CloudAccountID int `json:"cloud_account_id" gorm:"not null;comment:云账户ID"` + CloudAccount *CloudAccount `json:"cloud_account,omitempty" gorm:"foreignKey:CloudAccountID"` + InstanceID string `json:"instance_id" gorm:"type:varchar(100);comment:云资源实例ID"` + InstanceType string `json:"instance_type" gorm:"type:varchar(100);comment:实例规格(如ecs.g6.large)"` + Cpu int `json:"cpu" gorm:"comment:CPU核数;default:0"` + Memory int `json:"memory" gorm:"comment:内存大小(GiB);default:0"` + Disk int `json:"disk" gorm:"comment:磁盘大小(GiB);default:0"` + PublicIP string `json:"public_ip" gorm:"type:varchar(45);comment:公网IP"` + PrivateIP string `json:"private_ip" gorm:"type:varchar(45);comment:私网IP"` + VpcID string `json:"vpc_id" gorm:"type:varchar(100);comment:VPC ID"` + ZoneID string `json:"zone_id" gorm:"type:varchar(50);comment:可用区ID"` + ChargeType ChargeType `json:"charge_type" gorm:"type:varchar(50);comment:计费方式(PostPaid/PrePaid)"` + ExpireTime *time.Time `json:"expire_time" gorm:"type:datetime;comment:到期时间"` + MonthlyCost float64 `json:"monthly_cost" gorm:"type:decimal(10,2);comment:月度成本;default:0"` + Currency Currency `json:"currency" gorm:"type:varchar(10);not null;comment:货币单位;default:'CNY'"` + OSType string `json:"os_type" gorm:"type:varchar(50);comment:操作系统类型(linux/windows)"` + OSName string `json:"os_name" gorm:"type:varchar(100);comment:操作系统名称"` + ImageID string `json:"image_id" gorm:"type:varchar(100);comment:镜像ID"` + ImageName string `json:"image_name" gorm:"type:varchar(100);comment:镜像名称"` + Port int `json:"port" gorm:"comment:SSH端口号;default:22"` + Username string `json:"username" gorm:"type:varchar(100);comment:SSH用户名"` + Password string `json:"-" gorm:"type:varchar(500);comment:SSH密码(加密存储)"` + Key string `json:"-" gorm:"type:text;comment:SSH密钥"` + AuthMode AuthMode `json:"auth_mode" gorm:"type:tinyint(1);comment:SSH认证方式(1:密码,2:密钥);default:1"` + TreeNodes []*TreeNode `json:"tree_nodes" gorm:"many2many:cl_tree_node_cloud"` +} + +func (t *TreeCloudResource) TableName() string { + return "cl_tree_cloud_resource" +} + +// GetTreeCloudResourceListReq 获取云资源列表请求 +type GetTreeCloudResourceListReq struct { + ListReq + CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` + ResourceType CloudResourceType `json:"resource_type" form:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` + Status CloudResourceStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2 3 4 5 6"` + Environment string `json:"environment" form:"environment"` +} + +// GetTreeCloudResourceDetailReq 获取云资源详情请求 +type GetTreeCloudResourceDetailReq struct { + ID int `json:"id" form:"id" binding:"required,gt=0"` +} + +// CreateTreeCloudResourceReq 创建云资源请求(录入已有云资源) +type CreateTreeCloudResourceReq struct { + Name string `json:"name" binding:"required"` + ResourceType CloudResourceType `json:"resource_type" binding:"required,oneof=1 2 3 4 5 6"` + Environment string `json:"environment"` + Description string `json:"description"` + Tags KeyValueList `json:"tags"` + CreateUserID int `json:"create_user_id"` + CreateUserName string `json:"create_user_name"` + CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` + InstanceID string `json:"instance_id"` + InstanceType string `json:"instance_type"` + Cpu int `json:"cpu" binding:"omitempty,gte=0"` + Memory int `json:"memory" binding:"omitempty,gte=0"` + Disk int `json:"disk" binding:"omitempty,gte=0"` + PublicIP string `json:"public_ip" binding:"omitempty,ip"` + PrivateIP string `json:"private_ip" binding:"omitempty,ip"` + VpcID string `json:"vpc_id"` + ZoneID string `json:"zone_id"` + ChargeType ChargeType `json:"charge_type"` + ExpireTime *time.Time `json:"expire_time"` + MonthlyCost float64 `json:"monthly_cost" binding:"omitempty,gte=0"` + Currency string `json:"currency"` + OSType string `json:"os_type"` + OSName string `json:"os_name"` + ImageID string `json:"image_id"` + ImageName string `json:"image_name"` + Port int `json:"port" binding:"omitempty,gte=1,lte=65535"` + Username string `json:"username"` + Password string `json:"password"` + Key string `json:"key"` + AuthMode AuthMode `json:"auth_mode" binding:"omitempty,oneof=1 2"` +} + +// UpdateTreeCloudResourceReq 更新云资源请求 +type UpdateTreeCloudResourceReq struct { + ID int `json:"id" binding:"required,gt=0"` + Name string `json:"name"` + Environment string `json:"environment"` + Description string `json:"description"` + Tags KeyValueList `json:"tags"` + ResourceType CloudResourceType `json:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` + CloudAccountID int `json:"cloud_account_id" binding:"omitempty,gt=0"` + InstanceType string `json:"instance_type"` + PublicIP string `json:"public_ip" binding:"omitempty,ip"` + PrivateIP string `json:"private_ip" binding:"omitempty,ip"` + ChargeType ChargeType `json:"charge_type"` + ExpireTime *time.Time `json:"expire_time"` + MonthlyCost float64 `json:"monthly_cost" binding:"omitempty,gte=0"` + Currency string `json:"currency"` + Port int `json:"port" binding:"omitempty,gte=1,lte=65535"` + Username string `json:"username"` + Password string `json:"password"` + Key string `json:"key"` + AuthMode AuthMode `json:"auth_mode" binding:"omitempty,oneof=1 2"` +} + +// DeleteTreeCloudResourceReq 删除云资源请求 +type DeleteTreeCloudResourceReq struct { + ID int `json:"id" binding:"required,gt=0"` +} + +// SyncTreeCloudResourceReq 从云厂商同步资源请求 +type SyncTreeCloudResourceReq struct { + CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` + ResourceType CloudResourceType `json:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` // 同步的资源类型,为空则同步所有 + InstanceIDs []string `json:"instance_ids"` // 指定同步的实例ID列表,为空则同步所有 + SyncMode SyncMode `json:"sync_mode" binding:"omitempty,oneof=full incremental"` // 同步模式: full-全量, incremental-增量 +} + +// VerifyCloudCredentialsReq 验证云厂商凭证请求 +// Deprecated: 使用 cloud_account.go 中的 VerifyCloudAccountReq +type VerifyCloudCredentialsReq struct { + Provider CloudProvider `json:"provider" binding:"required,oneof=1 2 3 4 5 6"` + Region string `json:"region" binding:"required"` + AccessKey string `json:"access_key" binding:"required"` + SecretKey string `json:"secret_key" binding:"required"` +} + +// GetTreeNodeCloudResourcesReq 获取树节点下的云资源请求 +type GetTreeNodeCloudResourcesReq struct { + NodeID int `json:"node_id" form:"node_id" binding:"required,gt=0"` + CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` + ResourceType CloudResourceType `json:"resource_type" form:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` + Status CloudResourceStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2 3 4 5 6"` +} + +// BatchImportCloudResourceReq 批量导入云资源请求 +type BatchImportCloudResourceReq struct { + CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` + InstanceIDs []string `json:"instance_ids" binding:"required,min=1"` // 要导入的实例ID列表 +} + +// BindTreeCloudResourceReq 绑定云资源到树节点请求 +type BindTreeCloudResourceReq struct { + ID int `json:"id" binding:"required,gt=0"` + TreeNodeIDs []int `json:"tree_node_ids" binding:"required,min=1,dive,gt=0"` +} + +// UnBindTreeCloudResourceReq 解绑云资源与树节点请求 +type UnBindTreeCloudResourceReq struct { + ID int `json:"id" binding:"required,gt=0"` + TreeNodeIDs []int `json:"tree_node_ids" binding:"required,min=1,dive,gt=0"` +} + +// ConnectTreeCloudResourceTerminalReq 连接云资源终端请求(针对ECS) +type ConnectTreeCloudResourceTerminalReq struct { + ID int `json:"id" form:"id" binding:"required,gt=0"` + UserID int `json:"user_id"` +} + +// UpdateCloudResourceStatusReq 更新云资源状态请求 +type UpdateCloudResourceStatusReq struct { + ID int `json:"id" binding:"required,gt=0"` + Status CloudResourceStatus `json:"status" binding:"required,oneof=1 2 3 4 5 6"` +} diff --git a/internal/model/tree_local.go b/internal/model/tree_local.go index c67108b7..71ce8ad2 100644 --- a/internal/model/tree_local.go +++ b/internal/model/tree_local.go @@ -50,7 +50,7 @@ type TreeLocalResource struct { Status ResourceStatus `json:"status" gorm:"type:tinyint(1);comment:资源状态;default:1"` Environment string `json:"environment" gorm:"type:varchar(50);comment:环境标识,如dev,prod"` Description string `json:"description" gorm:"type:text;comment:资源描述"` - Tags StringList `json:"tags" gorm:"type:varchar(500);comment:资源标签集合"` + Tags KeyValueList `json:"tags" gorm:"type:text;serializer:json;comment:资源标签集合"` Cpu int `json:"cpu" gorm:"comment:CPU核数"` Memory int `json:"memory" gorm:"comment:内存大小,单位GiB"` Disk int `json:"disk" gorm:"comment:系统盘大小,单位GiB"` @@ -62,7 +62,7 @@ type TreeLocalResource struct { CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` Key string `json:"key" gorm:"type:text;comment:密钥"` AuthMode AuthMode `json:"auth_mode" gorm:"type:tinyint(1);comment:认证方式,1:密码,2:密钥;default:1"` - OsType string `json:"os_type" gorm:"type:varchar(50);comment:操作系统类型,如win,linux"` + OSType string `json:"os_type" gorm:"type:varchar(50);comment:操作系统类型,如win,linux"` OSName string `json:"os_name" gorm:"type:varchar(100);comment:操作系统名称"` ImageName string `json:"image_name" gorm:"type:varchar(100);comment:镜像名称"` TreeNodes []*TreeNode `json:"tree_nodes" gorm:"many2many:cl_tree_node_local"` @@ -85,58 +85,58 @@ type GetTreeLocalResourceDetailReq struct { // CreateTreeLocalReq 创建本地树资源请求 type CreateTreeLocalResourceReq struct { - Name string `json:"name" binding:"required"` - Environment string `json:"environment"` - Description string `json:"description"` - Tags StringList `json:"tags"` - IpAddr string `json:"ip_addr" binding:"required"` - Port int `json:"port"` - Username string `json:"username"` - Password string `json:"password"` - CreateUserID int `json:"create_user_id"` - CreateUserName string `json:"create_user_name"` - OsType string `json:"os_type"` - OSName string `json:"os_name"` - ImageName string `json:"image_name"` - Key string `json:"key"` - AuthMode AuthMode `json:"auth_mode"` + Name string `json:"name" binding:"required"` + Environment string `json:"environment"` + Description string `json:"description"` + Tags KeyValueList `json:"tags"` + IpAddr string `json:"ip_addr" binding:"required"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + CreateUserID int `json:"create_user_id"` + CreateUserName string `json:"create_user_name"` + OSType string `json:"os_type"` + OSName string `json:"os_name"` + ImageName string `json:"image_name"` + Key string `json:"key"` + AuthMode AuthMode `json:"auth_mode"` } // UpdateTreeLocalReq 更新本地树资源请求 type UpdateTreeLocalResourceReq struct { - ID int `json:"id" form:"id"` - Name string `json:"name"` - Environment string `json:"environment"` - Description string `json:"description"` - Tags StringList `json:"tags"` - IpAddr string `json:"ip_addr"` - Port int `json:"port"` - OsType string `json:"os_type"` - OSName string `json:"os_name"` - ImageName string `json:"image_name"` - Username string `json:"username"` - Password string `json:"password"` - Key string `json:"key"` - AuthMode AuthMode `json:"auth_mode"` + ID int `json:"id" form:"id"` + Name string `json:"name"` + Environment string `json:"environment"` + Description string `json:"description"` + Tags KeyValueList `json:"tags"` + IpAddr string `json:"ip_addr"` + Port int `json:"port"` + OSType string `json:"os_type"` + OSName string `json:"os_name"` + ImageName string `json:"image_name"` + Username string `json:"username"` + Password string `json:"password"` + Key string `json:"key"` + AuthMode AuthMode `json:"auth_mode"` } -// DeleteTreeLocalReq 删除本地树资源请求 +// DeleteTreeLocalResourceReq 删除本地树资源请求 type DeleteTreeLocalResourceReq struct { - ID int `json:"id" form:"id"` + ID int `json:"id" form:"id" binding:"required"` } -// ConnectTerminalReq 连接终端请求 -type ConnectTerminalResourceReq struct { - ID int `json:"id" form:"id"` +// ConnectTreeLocalResourceTerminalReq 连接本地资源终端请求 +type ConnectTreeLocalResourceTerminalReq struct { + ID int `json:"id" form:"id" binding:"required"` UserID int `json:"user_id"` } type BindTreeLocalResourceReq struct { - ID int `json:"id" form:"id"` - TreeNodeIDs []int `json:"tree_node_ids" form:"tree_node_ids"` + ID int `json:"id" form:"id" binding:"required"` + TreeNodeIDs []int `json:"tree_node_ids" form:"tree_node_ids" binding:"required,min=1"` } type UnBindTreeLocalResourceReq struct { - ID int `json:"id" form:"id"` - TreeNodeIDs []int `json:"tree_node_ids" form:"tree_node_ids"` + ID int `json:"id" form:"id" binding:"required"` + TreeNodeIDs []int `json:"tree_node_ids" form:"tree_node_ids" binding:"required,min=1"` } diff --git a/internal/model/tree_node.go b/internal/model/tree_node.go index cfcd14fa..4e1dfc0e 100644 --- a/internal/model/tree_node.go +++ b/internal/model/tree_node.go @@ -48,18 +48,19 @@ const ( // TreeNode 服务树节点结构 type TreeNode struct { Model - Name string `json:"name" gorm:"type:varchar(50);not null;comment:节点名称"` // 节点名称 - ParentID int `json:"parent_id" gorm:"index;comment:父节点ID;default:0"` // 父节点ID - Level int `json:"level" gorm:"comment:节点层级,默认在第1层;default:1"` // 节点层级 - Description string `json:"description" gorm:"type:text;comment:节点描述"` // 节点描述 - CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` // 创建者ID - CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` // 创建者姓名 - Status TreeNodeStatus `json:"status" gorm:"default:1;comment:节点状态, 1:活跃 2:非活跃"` // 节点状态 - AdminUsers []User `json:"admins" gorm:"many2many:cl_tree_node_admin;"` // 管理员多对多关系 - MemberUsers []User `json:"members" gorm:"many2many:cl_tree_node_member;"` // 成员多对多关系 - IsLeaf int8 `json:"is_leaf" gorm:"comment:是否为叶子节点1:是 2:不是;default:2"` // 是否为叶子节点 - Children []*TreeNode `json:"children" gorm:"-"` // 子节点列表 - TreeLocalResources []*TreeLocalResource `json:"tree_local_resources" gorm:"many2many:cl_tree_node_local;"` + Name string `json:"name" gorm:"type:varchar(50);not null;comment:节点名称"` // 节点名称 + ParentID int `json:"parent_id" gorm:"index;comment:父节点ID;default:0"` // 父节点ID + Level int `json:"level" gorm:"comment:节点层级,默认在第1层;default:1"` // 节点层级 + Description string `json:"description" gorm:"type:text;comment:节点描述"` // 节点描述 + CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` // 创建者ID + CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` // 创建者姓名 + Status TreeNodeStatus `json:"status" gorm:"default:1;comment:节点状态, 1:活跃 2:非活跃"` // 节点状态 + AdminUsers []User `json:"admins" gorm:"many2many:cl_tree_node_admin;"` // 管理员多对多关系 + MemberUsers []User `json:"members" gorm:"many2many:cl_tree_node_member;"` // 成员多对多关系 + IsLeaf int8 `json:"is_leaf" gorm:"comment:是否为叶子节点1:是 2:不是;default:2"` // 是否为叶子节点 + Children []*TreeNode `json:"children" gorm:"-"` // 子节点列表 + TreeLocalResources []*TreeLocalResource `json:"tree_local_resources" gorm:"many2many:cl_tree_node_local;"` // 本地资源关联 + TreeCloudResources []*TreeCloudResource `json:"tree_cloud_resources" gorm:"many2many:cl_tree_node_cloud;"` // 云资源关联 } func (t *TreeNode) TableName() string { diff --git a/internal/tree/api/cloud_account_handler.go b/internal/tree/api/cloud_account_handler.go new file mode 100644 index 00000000..6d59a202 --- /dev/null +++ b/internal/tree/api/cloud_account_handler.go @@ -0,0 +1,164 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package api + +import ( + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "github.com/GoSimplicity/AI-CloudOps/internal/tree/service" + "github.com/GoSimplicity/AI-CloudOps/pkg/utils" + "github.com/gin-gonic/gin" +) + +type CloudAccountHandler struct { + service service.CloudAccountService +} + +func NewCloudAccountHandler(service service.CloudAccountService) *CloudAccountHandler { + return &CloudAccountHandler{ + service: service, + } +} + +func (h *CloudAccountHandler) RegisterRouters(server *gin.Engine) { + accountGroup := server.Group("/api/tree/cloud/account") + { + accountGroup.GET("/list", h.GetCloudAccountList) + accountGroup.GET("/:id/detail", h.GetCloudAccountDetail) + accountGroup.POST("/create", h.CreateCloudAccount) + accountGroup.PUT("/:id/update", h.UpdateCloudAccount) + accountGroup.DELETE("/:id/delete", h.DeleteCloudAccount) + accountGroup.PUT("/:id/status", h.UpdateCloudAccountStatus) + accountGroup.POST("/:id/verify", h.VerifyCloudAccount) + } +} + +// GetCloudAccountList 获取云账户列表 +func (h *CloudAccountHandler) GetCloudAccountList(ctx *gin.Context) { + var req model.GetCloudAccountListReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetCloudAccountList(ctx, &req) + }) +} + +// GetCloudAccountDetail 获取云账户详情 +func (h *CloudAccountHandler) GetCloudAccountDetail(ctx *gin.Context) { + var req model.GetCloudAccountDetailReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的账户ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetCloudAccountDetail(ctx, &req) + }) +} + +// CreateCloudAccount 创建云账户 +func (h *CloudAccountHandler) CreateCloudAccount(ctx *gin.Context) { + var req model.CreateCloudAccountReq + + user := ctx.MustGet("user").(utils.UserClaims) + + req.CreateUserID = user.Uid + req.CreateUserName = user.Username + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.CreateCloudAccount(ctx, &req) + }) +} + +// UpdateCloudAccount 更新云账户 +func (h *CloudAccountHandler) UpdateCloudAccount(ctx *gin.Context) { + var req model.UpdateCloudAccountReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的账户ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.UpdateCloudAccount(ctx, &req) + }) +} + +// DeleteCloudAccount 删除云账户 +func (h *CloudAccountHandler) DeleteCloudAccount(ctx *gin.Context) { + var req model.DeleteCloudAccountReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的账户ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.DeleteCloudAccount(ctx, &req) + }) +} + +// UpdateCloudAccountStatus 更新云账户状态 +func (h *CloudAccountHandler) UpdateCloudAccountStatus(ctx *gin.Context) { + var req model.UpdateCloudAccountStatusReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的账户ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.UpdateCloudAccountStatus(ctx, &req) + }) +} + +// VerifyCloudAccount 验证云账户凭证 +func (h *CloudAccountHandler) VerifyCloudAccount(ctx *gin.Context) { + var req model.VerifyCloudAccountReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的账户ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.VerifyCloudAccount(ctx, &req) + }) +} diff --git a/internal/tree/api/tree_cloud_handler.go b/internal/tree/api/tree_cloud_handler.go new file mode 100644 index 00000000..761e77a5 --- /dev/null +++ b/internal/tree/api/tree_cloud_handler.go @@ -0,0 +1,324 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package api + +import ( + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "github.com/GoSimplicity/AI-CloudOps/internal/tree/service" + "github.com/GoSimplicity/AI-CloudOps/pkg/ssh" + "github.com/GoSimplicity/AI-CloudOps/pkg/utils" + "github.com/gin-gonic/gin" +) + +type TreeCloudHandler struct { + service service.TreeCloudService + sshClient ssh.Client +} + +func NewTreeCloudHandler(service service.TreeCloudService, sshClient ssh.Client) *TreeCloudHandler { + return &TreeCloudHandler{ + service: service, + sshClient: sshClient, + } +} + +func (h *TreeCloudHandler) RegisterRouters(server *gin.Engine) { + cloudGroup := server.Group("/api/tree/cloud") + { + cloudGroup.GET("/list", h.GetTreeCloudResourceList) + cloudGroup.GET("/:id/detail", h.GetTreeCloudResourceDetail) + cloudGroup.POST("/create", h.CreateTreeCloudResource) + cloudGroup.PUT("/:id/update", h.UpdateTreeCloudResource) + cloudGroup.DELETE("/:id/delete", h.DeleteTreeCloudResource) + cloudGroup.POST("/:id/bind", h.BindTreeCloudResource) + cloudGroup.POST("/:id/unbind", h.UnBindTreeCloudResource) + cloudGroup.POST("/verify", h.VerifyCloudCredentials) + cloudGroup.POST("/sync", h.SyncTreeCloudResource) + cloudGroup.POST("/batch_import", h.BatchImportCloudResource) + cloudGroup.GET("/:id/node", h.GetTreeNodeCloudResources) + cloudGroup.GET("/:id/terminal", h.ConnectCloudResourceTerminal) + cloudGroup.PUT("/:id/status", h.UpdateCloudResourceStatus) + } +} + +// GetTreeCloudResourceList 获取云资源列表 +func (h *TreeCloudHandler) GetTreeCloudResourceList(ctx *gin.Context) { + var req model.GetTreeCloudResourceListReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetTreeCloudResourceList(ctx, &req) + }) +} + +// GetTreeCloudResourceDetail 获取云资源详情 +func (h *TreeCloudHandler) GetTreeCloudResourceDetail(ctx *gin.Context) { + var req model.GetTreeCloudResourceDetailReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetTreeCloudResourceDetail(ctx, &req) + }) +} + +// CreateTreeCloudResource 创建云资源 +func (h *TreeCloudHandler) CreateTreeCloudResource(ctx *gin.Context) { + var req model.CreateTreeCloudResourceReq + + user := ctx.MustGet("user").(utils.UserClaims) + + req.CreateUserID = user.Uid + req.CreateUserName = user.Username + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.CreateTreeCloudResource(ctx, &req) + }) +} + +// UpdateTreeCloudResource 更新云资源 +func (h *TreeCloudHandler) UpdateTreeCloudResource(ctx *gin.Context) { + var req model.UpdateTreeCloudResourceReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.UpdateTreeCloudResource(ctx, &req) + }) +} + +// DeleteTreeCloudResource 删除云资源 +func (h *TreeCloudHandler) DeleteTreeCloudResource(ctx *gin.Context) { + var req model.DeleteTreeCloudResourceReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.DeleteTreeCloudResource(ctx, &req) + }) +} + +// BindTreeCloudResource 绑定云资源到树节点 +func (h *TreeCloudHandler) BindTreeCloudResource(ctx *gin.Context) { + var req model.BindTreeCloudResourceReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.BindTreeCloudResource(ctx, &req) + }) +} + +// UnBindTreeCloudResource 解绑云资源与树节点 +func (h *TreeCloudHandler) UnBindTreeCloudResource(ctx *gin.Context) { + var req model.UnBindTreeCloudResourceReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.UnBindTreeCloudResource(ctx, &req) + }) +} + +// VerifyCloudCredentials 验证云厂商凭证 +func (h *TreeCloudHandler) VerifyCloudCredentials(ctx *gin.Context) { + var req model.VerifyCloudCredentialsReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.VerifyCloudCredentials(ctx, &req) + }) +} + +// SyncTreeCloudResource 从云厂商同步资源 +func (h *TreeCloudHandler) SyncTreeCloudResource(ctx *gin.Context) { + var req model.SyncTreeCloudResourceReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.SyncTreeCloudResource(ctx, &req) + }) +} + +// BatchImportCloudResource 批量导入云资源 +func (h *TreeCloudHandler) BatchImportCloudResource(ctx *gin.Context) { + var req model.BatchImportCloudResourceReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.BatchImportCloudResource(ctx, &req) + }) +} + +// GetTreeNodeCloudResources 获取树节点下的云资源 +func (h *TreeCloudHandler) GetTreeNodeCloudResources(ctx *gin.Context) { + var req model.GetTreeNodeCloudResourcesReq + + nodeId, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的节点ID") + return + } + + req.NodeID = nodeId + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetTreeNodeCloudResources(ctx, &req) + }) +} + +// ConnectCloudResourceTerminal 连接云资源终端 +func (h *TreeCloudHandler) ConnectCloudResourceTerminal(ctx *gin.Context) { + var req model.ConnectTreeCloudResourceTerminalReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + uc := ctx.MustGet("user").(utils.UserClaims) + req.ID = id + req.UserID = uc.Uid + + // 获取云资源详情 + detailReq := &model.GetTreeCloudResourceDetailReq{ID: req.ID} + cloud, err := h.service.GetTreeCloudResourceForConnection(ctx, detailReq) + if err != nil { + utils.ErrorWithMessage(ctx, "获取云资源信息失败: "+err.Error()) + return + } + + // 仅支持ECS类型的云资源连接终端 + if cloud.ResourceType != model.ResourceTypeECS { + utils.ErrorWithMessage(ctx, "仅支持ECS类型的云资源连接终端") + return + } + + // 如果没有公网IP,尝试使用私网IP + ipAddr := cloud.PublicIP + if ipAddr == "" { + ipAddr = cloud.PrivateIP + } + + if ipAddr == "" { + utils.ErrorWithMessage(ctx, "云资源没有可用的IP地址") + return + } + + // 设置默认端口 + port := cloud.Port + if port == 0 { + port = 22 + } + + // 设置默认用户名 + username := cloud.Username + if username == "" { + username = "root" + } + + // 配置SSH连接 + sshConfig := &ssh.Config{ + Host: ipAddr, + Port: port, + Username: username, + Password: cloud.Password, + Key: cloud.Key, + Mode: ssh.AuthMode(cloud.AuthMode), + Timeout: 10, + } + + // 建立SSH连接 + if err := h.sshClient.Connect(sshConfig); err != nil { + utils.ErrorWithMessage(ctx, "连接SSH失败: "+err.Error()) + return + } + + // 确保SSH连接在函数退出时关闭 + defer func() { + if closeErr := h.sshClient.Close(); closeErr != nil { + utils.ErrorWithMessage(ctx, "关闭SSH连接失败: "+closeErr.Error()) + } + }() + + // 升级WebSocket连接 + ws, err := utils.UpGrader.Upgrade(ctx.Writer, ctx.Request, nil) + if err != nil { + utils.ErrorWithMessage(ctx, "升级WebSocket连接失败: "+err.Error()) + return + } + defer ws.Close() + + // 启动终端会话 + if err := h.sshClient.WebTerminal(uc.Uid, ws); err != nil { + utils.ErrorWithMessage(ctx, "启动Web终端失败: "+err.Error()) + return + } +} + +// UpdateCloudResourceStatus 更新云资源状态 +func (h *TreeCloudHandler) UpdateCloudResourceStatus(ctx *gin.Context) { + var req model.UpdateCloudResourceStatusReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.UpdateCloudResourceStatus(ctx, &req) + }) +} diff --git a/internal/tree/api/tree_local_handler.go b/internal/tree/api/tree_local_handler.go index ba0a3ffb..c240b7b0 100644 --- a/internal/tree/api/tree_local_handler.go +++ b/internal/tree/api/tree_local_handler.go @@ -80,7 +80,7 @@ func (h *TreeLocalHandler) GetTreeLocalDetail(ctx *gin.Context) { id, err := utils.GetParamID(ctx) if err != nil { - utils.ErrorWithMessage(ctx, "invalid param id") + utils.ErrorWithMessage(ctx, "无效的资源ID") return } diff --git a/internal/tree/dao/cloud_account_dao.go b/internal/tree/dao/cloud_account_dao.go new file mode 100644 index 00000000..ca70f24e --- /dev/null +++ b/internal/tree/dao/cloud_account_dao.go @@ -0,0 +1,176 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package dao + +import ( + "context" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type CloudAccountDAO interface { + Create(ctx context.Context, account *model.CloudAccount) error + Update(ctx context.Context, account *model.CloudAccount) error + Delete(ctx context.Context, id int) error + GetByID(ctx context.Context, id int) (*model.CloudAccount, error) + GetList(ctx context.Context, req *model.GetCloudAccountListReq) ([]*model.CloudAccount, int64, error) + UpdateStatus(ctx context.Context, id int, status model.CloudAccountStatus) error + GetByProviderAndRegion(ctx context.Context, provider model.CloudProvider, region string) ([]*model.CloudAccount, error) +} + +type cloudAccountDAO struct { + logger *zap.Logger + db *gorm.DB +} + +func NewCloudAccountDAO(db *gorm.DB, logger *zap.Logger) CloudAccountDAO { + return &cloudAccountDAO{ + logger: logger, + db: db, + } +} + +// Create 创建云账户 +func (d *cloudAccountDAO) Create(ctx context.Context, account *model.CloudAccount) error { + if err := d.db.WithContext(ctx).Create(account).Error; err != nil { + d.logger.Error("创建云账户失败", zap.Error(err)) + return err + } + + return nil +} + +// Update 更新云账户 +func (d *cloudAccountDAO) Update(ctx context.Context, account *model.CloudAccount) error { + if err := d.db.WithContext(ctx).Model(account).Updates(account).Error; err != nil { + d.logger.Error("更新云账户失败", zap.Error(err)) + return err + } + + return nil +} + +// Delete 删除云账户 +func (d *cloudAccountDAO) Delete(ctx context.Context, id int) error { + if err := d.db.WithContext(ctx).Delete(&model.CloudAccount{}, id).Error; err != nil { + d.logger.Error("删除云账户失败", zap.Error(err), zap.Int("id", id)) + return err + } + + return nil +} + +// GetByID 根据ID获取云账户详情 +func (d *cloudAccountDAO) GetByID(ctx context.Context, id int) (*model.CloudAccount, error) { + var account model.CloudAccount + + err := d.db.WithContext(ctx).Preload("CloudResources").Where("id = ?", id).First(&account).Error + if err != nil { + d.logger.Error("根据ID获取云账户详情失败", zap.Error(err), zap.Int("id", id)) + return nil, err + } + + return &account, nil +} + +// GetList 获取云账户列表 +func (d *cloudAccountDAO) GetList(ctx context.Context, req *model.GetCloudAccountListReq) ([]*model.CloudAccount, int64, error) { + var accounts []*model.CloudAccount + var total int64 + + query := d.db.WithContext(ctx).Model(&model.CloudAccount{}) + + // 添加查询条件 + if req.Provider != 0 { + query = query.Where("provider = ?", req.Provider) + } + + if req.Region != "" { + query = query.Where("region = ?", req.Region) + } + + if req.Status != 0 { + query = query.Where("status = ?", req.Status) + } + + if req.Search != "" { + query = query.Where("name LIKE ? OR account_name LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") + } + + // 计算总数 + err := query.Count(&total).Error + if err != nil { + d.logger.Error("获取云账户总数失败", zap.Error(err)) + return nil, 0, err + } + + // 分页查询 + offset := (req.Page - 1) * req.Size + err = query. + Order("created_at DESC"). + Limit(req.Size). + Offset(offset). + Find(&accounts).Error + if err != nil { + d.logger.Error("获取云账户列表失败", zap.Error(err)) + return nil, 0, err + } + + return accounts, total, nil +} + +// UpdateStatus 更新云账户状态 +func (d *cloudAccountDAO) UpdateStatus(ctx context.Context, id int, status model.CloudAccountStatus) error { + if err := d.db.WithContext(ctx). + Model(&model.CloudAccount{}). + Where("id = ?", id). + Update("status", status).Error; err != nil { + d.logger.Error("更新云账户状态失败", zap.Error(err), zap.Int("id", id), zap.Int8("status", int8(status))) + return err + } + + return nil +} + +// GetByProviderAndRegion 根据云厂商和区域获取云账户列表 +func (d *cloudAccountDAO) GetByProviderAndRegion(ctx context.Context, provider model.CloudProvider, region string) ([]*model.CloudAccount, error) { + var accounts []*model.CloudAccount + + query := d.db.WithContext(ctx).Where("provider = ?", provider) + if region != "" { + query = query.Where("region = ?", region) + } + + err := query.Find(&accounts).Error + if err != nil { + d.logger.Error("根据云厂商和区域获取云账户列表失败", zap.Error(err)) + return nil, err + } + + return accounts, nil +} diff --git a/internal/tree/dao/tree_cloud_dao.go b/internal/tree/dao/tree_cloud_dao.go new file mode 100644 index 00000000..0d4d5fd9 --- /dev/null +++ b/internal/tree/dao/tree_cloud_dao.go @@ -0,0 +1,312 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package dao + +import ( + "context" + "errors" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type TreeCloudDAO interface { + Create(ctx context.Context, cloud *model.TreeCloudResource) error + Update(ctx context.Context, cloud *model.TreeCloudResource) error + Delete(ctx context.Context, id int) error + GetByID(ctx context.Context, id int) (*model.TreeCloudResource, error) + GetList(ctx context.Context, req *model.GetTreeCloudResourceListReq) ([]*model.TreeCloudResource, int64, error) + GetByAccountAndInstanceID(ctx context.Context, cloudAccountID int, instanceID string) (*model.TreeCloudResource, error) + GetByNodeID(ctx context.Context, nodeID int, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) + BindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error + UnBindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error + BatchGetByIDs(ctx context.Context, ids []int) ([]*model.TreeCloudResource, error) + BatchCreate(ctx context.Context, clouds []*model.TreeCloudResource) error + UpdateStatus(ctx context.Context, id int, status model.CloudResourceStatus) error +} + +type treeCloudDAO struct { + logger *zap.Logger + db *gorm.DB +} + +func NewTreeCloudDAO(db *gorm.DB, logger *zap.Logger) TreeCloudDAO { + return &treeCloudDAO{ + logger: logger, + db: db, + } +} + +// Create 创建云资源 +func (d *treeCloudDAO) Create(ctx context.Context, cloud *model.TreeCloudResource) error { + if err := d.db.WithContext(ctx).Create(cloud).Error; err != nil { + d.logger.Error("创建云资源失败", zap.Error(err)) + return err + } + + return nil +} + +// Update 更新云资源 +func (d *treeCloudDAO) Update(ctx context.Context, cloud *model.TreeCloudResource) error { + if err := d.db.WithContext(ctx).Model(cloud).Updates(cloud).Error; err != nil { + d.logger.Error("更新云资源失败", zap.Error(err)) + return err + } + + return nil +} + +// Delete 删除云资源 +func (d *treeCloudDAO) Delete(ctx context.Context, id int) error { + if err := d.db.WithContext(ctx).Delete(&model.TreeCloudResource{}, id).Error; err != nil { + d.logger.Error("删除云资源失败", zap.Error(err), zap.Int("id", id)) + return err + } + + return nil +} + +// GetByID 根据ID获取云资源详情 +func (d *treeCloudDAO) GetByID(ctx context.Context, id int) (*model.TreeCloudResource, error) { + var cloud model.TreeCloudResource + + err := d.db.WithContext(ctx).Preload("TreeNodes").Where("id = ?", id).First(&cloud).Error + if err != nil { + d.logger.Error("根据ID获取云资源详情失败", zap.Error(err), zap.Int("id", id)) + return nil, err + } + + return &cloud, nil +} + +// GetList 获取云资源列表 +func (d *treeCloudDAO) GetList(ctx context.Context, req *model.GetTreeCloudResourceListReq) ([]*model.TreeCloudResource, int64, error) { + var clouds []*model.TreeCloudResource + var total int64 + + query := d.db.WithContext(ctx).Model(&model.TreeCloudResource{}) + + // 添加查询条件 + if req.CloudAccountID != 0 { + query = query.Where("cloud_account_id = ?", req.CloudAccountID) + } + + if req.ResourceType != 0 { + query = query.Where("resource_type = ?", req.ResourceType) + } + + if req.Status != 0 { + query = query.Where("status = ?", req.Status) + } + + if req.Environment != "" { + query = query.Where("environment = ?", req.Environment) + } + + if req.Search != "" { + query = query.Where("name LIKE ? OR instance_id LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") + } + + // 计算总数 + err := query.Count(&total).Error + if err != nil { + d.logger.Error("获取云资源总数失败", zap.Error(err)) + return nil, 0, err + } + + // 分页查询,关联云账户信息 + offset := (req.Page - 1) * req.Size + err = query. + Order("created_at DESC"). + Preload("CloudAccount"). + Preload("TreeNodes"). + Limit(req.Size). + Offset(offset). + Find(&clouds).Error + if err != nil { + d.logger.Error("获取云资源列表失败", zap.Error(err)) + return nil, 0, err + } + + return clouds, total, nil +} + +// GetByAccountAndInstanceID 根据云账户ID和实例ID获取云资源 +func (d *treeCloudDAO) GetByAccountAndInstanceID(ctx context.Context, cloudAccountID int, instanceID string) (*model.TreeCloudResource, error) { + var cloud model.TreeCloudResource + + err := d.db.WithContext(ctx). + Where("cloud_account_id = ? AND instance_id = ?", cloudAccountID, instanceID). + First(&cloud).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + d.logger.Error("根据云账户和实例ID获取云资源失败", zap.Error(err), zap.Int("cloudAccountID", cloudAccountID), zap.String("instanceID", instanceID)) + return nil, err + } + + return &cloud, nil +} + +// GetByNodeID 根据树节点ID获取云资源列表 +func (d *treeCloudDAO) GetByNodeID(ctx context.Context, nodeID int, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) { + var clouds []*model.TreeCloudResource + + query := d.db.WithContext(ctx). + Joins("JOIN cl_tree_node_cloud ON cl_tree_node_cloud.tree_cloud_resource_id = cl_tree_cloud_resource.id"). + Where("cl_tree_node_cloud.tree_node_id = ?", nodeID) + + // 添加过滤条件 + if req.CloudAccountID != 0 { + query = query.Where("cl_tree_cloud_resource.cloud_account_id = ?", req.CloudAccountID) + } + + if req.ResourceType != 0 { + query = query.Where("cl_tree_cloud_resource.resource_type = ?", req.ResourceType) + } + + if req.Status != 0 { + query = query.Where("cl_tree_cloud_resource.status = ?", req.Status) + } + + err := query.Preload("CloudAccount").Find(&clouds).Error + if err != nil { + d.logger.Error("根据节点ID获取云资源失败", zap.Error(err), zap.Int("nodeID", nodeID)) + return nil, err + } + + return clouds, nil +} + +// BatchGetByIDs 批量获取云资源 +func (d *treeCloudDAO) BatchGetByIDs(ctx context.Context, ids []int) ([]*model.TreeCloudResource, error) { + if len(ids) == 0 { + return nil, nil + } + + var clouds []*model.TreeCloudResource + + if err := d.db.WithContext(ctx).Where("id IN ?", ids).Find(&clouds).Error; err != nil { + d.logger.Error("批量获取云资源失败", zap.Error(err), zap.Ints("ids", ids)) + return nil, err + } + + return clouds, nil +} + +// BatchCreate 批量创建云资源 +func (d *treeCloudDAO) BatchCreate(ctx context.Context, clouds []*model.TreeCloudResource) error { + if len(clouds) == 0 { + return nil + } + + if err := d.db.WithContext(ctx).Create(&clouds).Error; err != nil { + d.logger.Error("批量创建云资源失败", zap.Error(err)) + return err + } + + d.logger.Info("批量创建云资源成功", zap.Int("count", len(clouds))) + return nil +} + +// UpdateStatus 更新云资源状态 +func (d *treeCloudDAO) UpdateStatus(ctx context.Context, id int, status model.CloudResourceStatus) error { + if err := d.db.WithContext(ctx). + Model(&model.TreeCloudResource{}). + Where("id = ?", id). + Update("status", status).Error; err != nil { + d.logger.Error("更新云资源状态失败", zap.Error(err), zap.Int("id", id), zap.Int8("status", int8(status))) + return err + } + + return nil +} + +// BindTreeNodes 绑定树节点 +func (d *treeCloudDAO) BindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error { + if !treeUtils.ValidateTreeNodeIDs(treeNodeIds) { + d.logger.Info("没有需要绑定的树节点") + return nil + } + + // 获取云资源 + var cloud model.TreeCloudResource + if err := d.db.WithContext(ctx).First(&cloud, cloudID).Error; err != nil { + d.logger.Error("获取云资源失败", zap.Error(err), zap.Int("cloudID", cloudID)) + return err + } + + // 构建要绑定的树节点列表 + var treeNodes []model.TreeNode + for _, nodeID := range treeNodeIds { + treeNodes = append(treeNodes, model.TreeNode{Model: model.Model{ID: nodeID}}) + } + + // 通过many2many关系绑定树节点 + if err := d.db.WithContext(ctx).Model(&cloud).Association("TreeNodes").Append(treeNodes); err != nil { + d.logger.Error("绑定树节点失败", zap.Error(err), zap.Int("cloudID", cloudID), zap.Ints("treeNodeIds", treeNodeIds)) + return err + } + + d.logger.Info("绑定树节点成功", zap.Int("cloudID", cloudID), zap.Ints("treeNodeIds", treeNodeIds)) + + return nil +} + +// UnBindTreeNodes 解绑树节点 +func (d *treeCloudDAO) UnBindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error { + if !treeUtils.ValidateTreeNodeIDs(treeNodeIds) { + d.logger.Info("没有需要解绑的树节点") + return nil + } + + // 获取云资源 + var cloud model.TreeCloudResource + if err := d.db.WithContext(ctx).First(&cloud, cloudID).Error; err != nil { + d.logger.Error("获取云资源失败", zap.Error(err), zap.Int("cloudID", cloudID)) + return err + } + + // 构建要解绑的树节点列表 + var treeNodes []model.TreeNode + for _, nodeID := range treeNodeIds { + treeNodes = append(treeNodes, model.TreeNode{Model: model.Model{ID: nodeID}}) + } + + // 通过many2many关系解绑树节点 + if err := d.db.WithContext(ctx).Model(&cloud).Association("TreeNodes").Delete(treeNodes); err != nil { + d.logger.Error("解绑树节点失败", zap.Error(err), zap.Int("cloudID", cloudID), zap.Ints("treeNodeIds", treeNodeIds)) + return err + } + + d.logger.Info("解绑树节点成功", zap.Int("cloudID", cloudID), zap.Ints("treeNodeIds", treeNodeIds)) + + return nil +} diff --git a/internal/tree/service/cloud_account_service.go b/internal/tree/service/cloud_account_service.go new file mode 100644 index 00000000..3adc296b --- /dev/null +++ b/internal/tree/service/cloud_account_service.go @@ -0,0 +1,281 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package service + +import ( + "context" + "errors" + "fmt" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "github.com/GoSimplicity/AI-CloudOps/internal/tree/dao" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type CloudAccountService interface { + GetCloudAccountList(ctx context.Context, req *model.GetCloudAccountListReq) (model.ListResp[*model.CloudAccount], error) + GetCloudAccountDetail(ctx context.Context, req *model.GetCloudAccountDetailReq) (*model.CloudAccount, error) + CreateCloudAccount(ctx context.Context, req *model.CreateCloudAccountReq) error + UpdateCloudAccount(ctx context.Context, req *model.UpdateCloudAccountReq) error + DeleteCloudAccount(ctx context.Context, req *model.DeleteCloudAccountReq) error + UpdateCloudAccountStatus(ctx context.Context, req *model.UpdateCloudAccountStatusReq) error + VerifyCloudAccount(ctx context.Context, req *model.VerifyCloudAccountReq) error +} + +type cloudAccountService struct { + logger *zap.Logger + dao dao.CloudAccountDAO +} + +func NewCloudAccountService(logger *zap.Logger, dao dao.CloudAccountDAO) CloudAccountService { + return &cloudAccountService{ + logger: logger, + dao: dao, + } +} + +// GetCloudAccountList 获取云账户列表 +func (s *cloudAccountService) GetCloudAccountList(ctx context.Context, req *model.GetCloudAccountListReq) (model.ListResp[*model.CloudAccount], error) { + // 兜底分页参数 + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) + + accounts, total, err := s.dao.GetList(ctx, req) + if err != nil { + s.logger.Error("获取云账户列表失败", zap.Error(err)) + return model.ListResp[*model.CloudAccount]{}, err + } + + return model.ListResp[*model.CloudAccount]{ + Items: accounts, + Total: total, + }, nil +} + +// GetCloudAccountDetail 获取云账户详情 +func (s *cloudAccountService) GetCloudAccountDetail(ctx context.Context, req *model.GetCloudAccountDetailReq) (*model.CloudAccount, error) { + if err := treeUtils.ValidateID(req.ID); err != nil { + return nil, fmt.Errorf("无效的云账户ID: %w", err) + } + + account, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("云账户不存在") + } + s.logger.Error("获取云账户详情失败", zap.Int("id", req.ID), zap.Error(err)) + return nil, err + } + + return account, nil +} + +// CreateCloudAccount 创建云账户 +func (s *cloudAccountService) CreateCloudAccount(ctx context.Context, req *model.CreateCloudAccountReq) error { + // 加密 AccessKey 和 SecretKey + encryptedAccessKey, err := treeUtils.EncryptPassword(req.AccessKey) + if err != nil { + s.logger.Error("加密AccessKey失败", zap.Error(err)) + return fmt.Errorf("加密AccessKey失败: %w", err) + } + + encryptedSecretKey, err := treeUtils.EncryptPassword(req.SecretKey) + if err != nil { + s.logger.Error("加密SecretKey失败", zap.Error(err)) + return fmt.Errorf("加密SecretKey失败: %w", err) + } + + // 创建云账户对象 + account := &model.CloudAccount{ + Name: req.Name, + Provider: req.Provider, + Region: req.Region, + AccessKey: encryptedAccessKey, + SecretKey: encryptedSecretKey, + AccountID: req.AccountID, + AccountName: req.AccountName, + AccountAlias: req.AccountAlias, + Description: req.Description, + Status: model.CloudAccountEnabled, // 默认启用 + CreateUserID: req.CreateUserID, + CreateUserName: req.CreateUserName, + } + + if err := s.dao.Create(ctx, account); err != nil { + s.logger.Error("创建云账户失败", zap.Error(err)) + return err + } + + return nil +} + +// UpdateCloudAccount 更新云账户 +func (s *cloudAccountService) UpdateCloudAccount(ctx context.Context, req *model.UpdateCloudAccountReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云账户ID: %w", err) + } + + // 检查云账户是否存在 + _, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云账户不存在") + } + return err + } + + // 构建更新对象 + account := &model.CloudAccount{ + Model: model.Model{ID: req.ID}, + Name: req.Name, + AccountID: req.AccountID, + AccountName: req.AccountName, + AccountAlias: req.AccountAlias, + Description: req.Description, + } + + // 如果需要更新 AccessKey + if req.AccessKey != "" { + encryptedAccessKey, err := treeUtils.EncryptPassword(req.AccessKey) + if err != nil { + s.logger.Error("加密AccessKey失败", zap.Error(err)) + return fmt.Errorf("加密AccessKey失败: %w", err) + } + account.AccessKey = encryptedAccessKey + } + + // 如果需要更新 SecretKey + if req.SecretKey != "" { + encryptedSecretKey, err := treeUtils.EncryptPassword(req.SecretKey) + if err != nil { + s.logger.Error("加密SecretKey失败", zap.Error(err)) + return fmt.Errorf("加密SecretKey失败: %w", err) + } + account.SecretKey = encryptedSecretKey + } + + if err := s.dao.Update(ctx, account); err != nil { + s.logger.Error("更新云账户失败", zap.Error(err)) + return err + } + + return nil +} + +// DeleteCloudAccount 删除云账户 +func (s *cloudAccountService) DeleteCloudAccount(ctx context.Context, req *model.DeleteCloudAccountReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云账户ID: %w", err) + } + + // 检查云账户是否存在 + account, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云账户不存在") + } + return err + } + + // 检查是否有关联的云资源 + if len(account.CloudResources) > 0 { + return fmt.Errorf("云账户下还有 %d 个云资源,请先删除相关资源", len(account.CloudResources)) + } + + if err := s.dao.Delete(ctx, req.ID); err != nil { + s.logger.Error("删除云账户失败", zap.Error(err)) + return err + } + + return nil +} + +// UpdateCloudAccountStatus 更新云账户状态 +func (s *cloudAccountService) UpdateCloudAccountStatus(ctx context.Context, req *model.UpdateCloudAccountStatusReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云账户ID: %w", err) + } + + if err := s.dao.UpdateStatus(ctx, req.ID, req.Status); err != nil { + s.logger.Error("更新云账户状态失败", zap.Error(err)) + return err + } + + return nil +} + +// VerifyCloudAccount 验证云账户凭证 +func (s *cloudAccountService) VerifyCloudAccount(ctx context.Context, req *model.VerifyCloudAccountReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云账户ID: %w", err) + } + + account, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云账户不存在") + } + return err + } + + // 解密密钥 + accessKey, err := treeUtils.DecryptPassword(account.AccessKey) + if err != nil { + s.logger.Error("解密AccessKey失败", zap.Error(err)) + return fmt.Errorf("解密AccessKey失败: %w", err) + } + + secretKey, err := treeUtils.DecryptPassword(account.SecretKey) + if err != nil { + s.logger.Error("解密SecretKey失败", zap.Error(err)) + return fmt.Errorf("解密SecretKey失败: %w", err) + } + + // TODO: 根据 Provider 调用相应的云厂商 SDK 验证凭证 + // 这里需要实现具体的云厂商验证逻辑 + + // 截断敏感信息用于日志 + akLog := accessKey + if len(akLog) > 10 { + akLog = akLog[:10] + "..." + } + skLog := secretKey + if len(skLog) > 10 { + skLog = skLog[:10] + "..." + } + + s.logger.Info("验证云账户凭证", + zap.Int("id", req.ID), + zap.Int8("provider", int8(account.Provider)), + zap.String("region", account.Region), + zap.String("accessKey", akLog), + zap.String("secretKey", skLog), + ) + + // 暂时返回成功,实际应该调用云厂商API验证 + return nil +} diff --git a/internal/tree/service/tree_cloud_service.go b/internal/tree/service/tree_cloud_service.go new file mode 100644 index 00000000..337a3132 --- /dev/null +++ b/internal/tree/service/tree_cloud_service.go @@ -0,0 +1,451 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package service + +import ( + "context" + "errors" + "fmt" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "github.com/GoSimplicity/AI-CloudOps/internal/tree/dao" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type TreeCloudService interface { + GetTreeCloudResourceList(ctx context.Context, req *model.GetTreeCloudResourceListReq) (model.ListResp[*model.TreeCloudResource], error) + GetTreeCloudResourceDetail(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) + GetTreeCloudResourceForConnection(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) + CreateTreeCloudResource(ctx context.Context, req *model.CreateTreeCloudResourceReq) error + UpdateTreeCloudResource(ctx context.Context, req *model.UpdateTreeCloudResourceReq) error + DeleteTreeCloudResource(ctx context.Context, req *model.DeleteTreeCloudResourceReq) error + BindTreeCloudResource(ctx context.Context, req *model.BindTreeCloudResourceReq) error + UnBindTreeCloudResource(ctx context.Context, req *model.UnBindTreeCloudResourceReq) error + GetTreeNodeCloudResources(ctx context.Context, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) + UpdateCloudResourceStatus(ctx context.Context, req *model.UpdateCloudResourceStatusReq) error + VerifyCloudCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq) error + SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) error + BatchImportCloudResource(ctx context.Context, req *model.BatchImportCloudResourceReq) ([]int, error) +} + +type treeCloudService struct { + logger *zap.Logger + dao dao.TreeCloudDAO + cloudAccountDAO dao.CloudAccountDAO +} + +func NewTreeCloudService(logger *zap.Logger, dao dao.TreeCloudDAO, cloudAccountDAO dao.CloudAccountDAO) TreeCloudService { + return &treeCloudService{ + logger: logger, + dao: dao, + cloudAccountDAO: cloudAccountDAO, + } +} + +// GetTreeCloudResourceList 获取云资源列表 +func (s *treeCloudService) GetTreeCloudResourceList(ctx context.Context, req *model.GetTreeCloudResourceListReq) (model.ListResp[*model.TreeCloudResource], error) { + // 兜底分页参数,避免offset为负或size为0 + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) + + clouds, total, err := s.dao.GetList(ctx, req) + if err != nil { + s.logger.Error("获取云资源列表失败", zap.Error(err)) + return model.ListResp[*model.TreeCloudResource]{}, err + } + + return model.ListResp[*model.TreeCloudResource]{ + Items: clouds, + Total: total, + }, nil +} + +// GetTreeCloudResourceDetail 获取云资源详情 +func (s *treeCloudService) GetTreeCloudResourceDetail(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) { + if err := treeUtils.ValidateID(req.ID); err != nil { + return nil, fmt.Errorf("无效的云资源ID: %w", err) + } + + cloud, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("云资源不存在") + } + s.logger.Error("获取云资源详情失败", zap.Int("id", req.ID), zap.Error(err)) + return nil, err + } + + return cloud, nil +} + +// GetTreeCloudResourceForConnection 获取用于连接的云资源详情(包含解密后的密码) +func (s *treeCloudService) GetTreeCloudResourceForConnection(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) { + if err := treeUtils.ValidateID(req.ID); err != nil { + return nil, fmt.Errorf("无效的云资源ID: %w", err) + } + + cloud, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("云资源不存在") + } + s.logger.Error("获取云资源详情失败", zap.Int("id", req.ID), zap.Error(err)) + return nil, err + } + + // 解密SSH密码(针对ECS类型) + if cloud.AuthMode == model.AuthModePassword && cloud.Password != "" { + plainPassword, err := treeUtils.DecryptPassword(cloud.Password) + if err != nil { + s.logger.Error("SSH密码解密失败", zap.Int("id", req.ID), zap.Error(err)) + return nil, fmt.Errorf("SSH密码解密失败: %w", err) + } + cloud.Password = plainPassword + } + + return cloud, nil +} + +// CreateTreeCloudResource 创建云资源 +func (s *treeCloudService) CreateTreeCloudResource(ctx context.Context, req *model.CreateTreeCloudResourceReq) error { + // 检查实例ID是否已存在(如果提供了实例ID) + if req.InstanceID != "" { + existing, err := s.dao.GetByAccountAndInstanceID(ctx, req.CloudAccountID, req.InstanceID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.logger.Error("检查实例ID是否存在失败", zap.Error(err)) + return fmt.Errorf("检查实例ID失败: %w", err) + } + if existing != nil { + return fmt.Errorf("云账户 %d 下的实例 %s 已存在", req.CloudAccountID, req.InstanceID) + } + } + + // 加密SSH密码 + var encryptedPassword string + var err error + + if req.Password != "" { + encryptedPassword, err = treeUtils.EncryptPassword(req.Password) + if err != nil { + s.logger.Error("密码加密失败", zap.Error(err)) + return fmt.Errorf("密码加密失败: %w", err) + } + } + + // 创建云资源对象 + cloud := &model.TreeCloudResource{ + Name: req.Name, + ResourceType: req.ResourceType, + Status: model.CloudResourceRunning, + Environment: req.Environment, + Description: req.Description, + Tags: req.Tags, + CreateUserID: req.CreateUserID, + CreateUserName: req.CreateUserName, + CloudAccountID: req.CloudAccountID, + InstanceID: req.InstanceID, + InstanceType: req.InstanceType, + Cpu: req.Cpu, + Memory: req.Memory, + Disk: req.Disk, + PublicIP: req.PublicIP, + PrivateIP: req.PrivateIP, + VpcID: req.VpcID, + ZoneID: req.ZoneID, + ChargeType: req.ChargeType, + ExpireTime: req.ExpireTime, + MonthlyCost: req.MonthlyCost, + Currency: model.Currency(req.Currency), + OSType: req.OSType, + OSName: req.OSName, + ImageID: req.ImageID, + ImageName: req.ImageName, + Port: req.Port, + Username: req.Username, + Password: encryptedPassword, + Key: req.Key, + AuthMode: req.AuthMode, + } + + // 设置默认值 + treeUtils.SetSSHDefaults(&cloud.Port, &cloud.Username) + + if err := s.dao.Create(ctx, cloud); err != nil { + s.logger.Error("创建云资源失败", zap.Error(err)) + return err + } + + return nil +} + +// UpdateTreeCloudResource 更新云资源 +func (s *treeCloudService) UpdateTreeCloudResource(ctx context.Context, req *model.UpdateTreeCloudResourceReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云资源ID: %w", err) + } + + // 检查是否存在 + _, err := s.dao.GetByID(ctx, req.ID) + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + return errors.New("云资源不存在") + case err != nil: + s.logger.Error("获取云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + // 加密SSH密码 + if req.Password != "" { + encrypted, err := treeUtils.EncryptPassword(req.Password) + if err != nil { + s.logger.Error("密码加密失败", zap.Error(err)) + return fmt.Errorf("密码加密失败: %w", err) + } + req.Password = encrypted + } + + // 构建更新对象 + cloud := &model.TreeCloudResource{ + Model: model.Model{ID: req.ID}, + Name: req.Name, + Environment: req.Environment, + Description: req.Description, + Tags: req.Tags, + ResourceType: req.ResourceType, + CloudAccountID: req.CloudAccountID, + InstanceType: req.InstanceType, + PublicIP: req.PublicIP, + PrivateIP: req.PrivateIP, + ChargeType: req.ChargeType, + ExpireTime: req.ExpireTime, + MonthlyCost: req.MonthlyCost, + Currency: model.Currency(req.Currency), + Port: req.Port, + Username: req.Username, + Password: req.Password, + Key: req.Key, + AuthMode: req.AuthMode, + } + + // 直接更新 + if err := s.dao.Update(ctx, cloud); err != nil { + s.logger.Error("更新云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + return nil +} + +// DeleteTreeCloudResource 删除云资源 +func (s *treeCloudService) DeleteTreeCloudResource(ctx context.Context, req *model.DeleteTreeCloudResourceReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云资源ID: %w", err) + } + + if err := s.dao.Delete(ctx, req.ID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云资源不存在") + } + s.logger.Error("删除云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + return nil +} + +// BindTreeCloudResource 绑定云资源到树节点 +func (s *treeCloudService) BindTreeCloudResource(ctx context.Context, req *model.BindTreeCloudResourceReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云资源ID: %w", err) + } + + if err := s.dao.BindTreeNodes(ctx, req.ID, req.TreeNodeIDs); err != nil { + s.logger.Error("绑定云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + return nil +} + +// UnBindTreeCloudResource 解绑云资源与树节点 +func (s *treeCloudService) UnBindTreeCloudResource(ctx context.Context, req *model.UnBindTreeCloudResourceReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云资源ID: %w", err) + } + + if err := s.dao.UnBindTreeNodes(ctx, req.ID, req.TreeNodeIDs); err != nil { + s.logger.Error("解绑云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + return nil +} + +// GetTreeNodeCloudResources 获取树节点下的云资源 +func (s *treeCloudService) GetTreeNodeCloudResources(ctx context.Context, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) { + if err := treeUtils.ValidateID(req.NodeID); err != nil { + return nil, fmt.Errorf("无效的节点ID: %w", err) + } + + clouds, err := s.dao.GetByNodeID(ctx, req.NodeID, req) + if err != nil { + s.logger.Error("获取树节点云资源失败", zap.Int("nodeID", req.NodeID), zap.Error(err)) + return nil, err + } + + return clouds, nil +} + +// UpdateCloudResourceStatus 更新云资源状态 +func (s *treeCloudService) UpdateCloudResourceStatus(ctx context.Context, req *model.UpdateCloudResourceStatusReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云资源ID: %w", err) + } + + // 检查云资源是否存在 + _, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云资源不存在") + } + s.logger.Error("获取云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + if err := s.dao.UpdateStatus(ctx, req.ID, req.Status); err != nil { + s.logger.Error("更新云资源状态失败", zap.Int("id", req.ID), zap.Int8("status", int8(req.Status)), zap.Error(err)) + return err + } + + return nil +} + +// VerifyCloudCredentials 验证云厂商凭证 +func (s *treeCloudService) VerifyCloudCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq) error { + // TODO: 实现具体的云厂商SDK验证逻辑 + // 这里需要根据不同的云厂商(阿里云、腾讯云、AWS等)调用对应的SDK验证凭证 + s.logger.Info("验证云厂商凭证", + zap.Int8("provider", int8(req.Provider)), + zap.String("region", req.Region)) + + // 根据云厂商类型验证凭证 + switch req.Provider { + case model.ProviderAliyun: + return treeUtils.VerifyAliyunCredentials(ctx, req, s.logger) + case model.ProviderTencent: + return treeUtils.VerifyTencentCredentials(ctx, req, s.logger) + case model.ProviderAWS: + return treeUtils.VerifyAWSCredentials(ctx, req, s.logger) + case model.ProviderHuawei: + return treeUtils.VerifyHuaweiCredentials(ctx, req, s.logger) + case model.ProviderAzure: + return treeUtils.VerifyAzureCredentials(ctx, req, s.logger) + case model.ProviderGCP: + return treeUtils.VerifyGCPCredentials(ctx, req, s.logger) + default: + return fmt.Errorf("不支持的云厂商: %d", req.Provider) + } +} + +// SyncTreeCloudResource 从云厂商同步资源 +func (s *treeCloudService) SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) error { + // 获取云账户信息 + account, err := s.cloudAccountDAO.GetByID(ctx, req.CloudAccountID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云账户不存在") + } + s.logger.Error("获取云账户失败", zap.Int("cloudAccountID", req.CloudAccountID), zap.Error(err)) + return err + } + + // 检查云账户状态 + if account.Status != model.CloudAccountEnabled { + return errors.New("云账户已禁用,无法同步资源") + } + + s.logger.Info("同步云资源", + zap.Int("cloudAccountID", req.CloudAccountID), + zap.Int8("provider", int8(account.Provider)), + zap.String("region", account.Region), + zap.String("syncMode", string(req.SyncMode))) + + // TODO: 根据不同的云厂商调用对应的同步逻辑 + // 这里需要实现具体的云厂商SDK调用 + switch account.Provider { + case model.ProviderAliyun: + return errors.New("阿里云资源同步功能待实现") + case model.ProviderTencent: + return errors.New("腾讯云资源同步功能待实现") + case model.ProviderAWS: + return errors.New("AWS资源同步功能待实现") + case model.ProviderHuawei: + return errors.New("华为云资源同步功能待实现") + case model.ProviderAzure: + return errors.New("Azure资源同步功能待实现") + case model.ProviderGCP: + return errors.New("GCP资源同步功能待实现") + default: + return fmt.Errorf("不支持的云厂商: %d", account.Provider) + } +} + +// BatchImportCloudResource 批量导入云资源 +func (s *treeCloudService) BatchImportCloudResource(ctx context.Context, req *model.BatchImportCloudResourceReq) ([]int, error) { + if len(req.InstanceIDs) == 0 { + return nil, errors.New("实例ID列表不能为空") + } + + // 获取云账户信息 + account, err := s.cloudAccountDAO.GetByID(ctx, req.CloudAccountID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("云账户不存在") + } + s.logger.Error("获取云账户失败", zap.Int("cloudAccountID", req.CloudAccountID), zap.Error(err)) + return nil, err + } + + // 检查云账户状态 + if account.Status != model.CloudAccountEnabled { + return nil, errors.New("云账户已禁用,无法导入资源") + } + + s.logger.Info("批量导入云资源", + zap.Int("cloudAccountID", req.CloudAccountID), + zap.Int8("provider", int8(account.Provider)), + zap.String("region", account.Region), + zap.Int("count", len(req.InstanceIDs))) + + // TODO: 实现批量导入逻辑 + // 1. 根据云厂商调用对应的SDK获取实例详情 + // 2. 批量创建云资源记录 + // 3. 返回创建的资源ID列表 + + // 暂时返回空列表 + return []int{}, errors.New("批量导入云资源功能待实现") +} diff --git a/internal/tree/service/tree_local_service.go b/internal/tree/service/tree_local_service.go index 44570a51..aab94bfc 100644 --- a/internal/tree/service/tree_local_service.go +++ b/internal/tree/service/tree_local_service.go @@ -144,19 +144,13 @@ func (s *treeLocalService) CreateTreeLocal(ctx context.Context, req *model.Creat CreateUserName: req.CreateUserName, Key: req.Key, AuthMode: req.AuthMode, - OsType: req.OsType, + OSType: req.OSType, OSName: req.OSName, ImageName: req.ImageName, } // 设置默认值 - if local.Port == 0 { - local.Port = 22 - } - - if local.Username == "" { - local.Username = "root" - } + treeUtils.SetSSHDefaults(&local.Port, &local.Username) // 加密 if local.AuthMode == model.AuthModePassword && req.Password != "" { @@ -210,7 +204,7 @@ func (s *treeLocalService) UpdateTreeLocal(ctx context.Context, req *model.Updat Status: model.STARTING, IpAddr: req.IpAddr, Port: req.Port, - OsType: req.OsType, + OSType: req.OSType, OSName: req.OSName, ImageName: req.ImageName, AuthMode: req.AuthMode, diff --git a/internal/tree/utils/cloud_account_util.go b/internal/tree/utils/cloud_account_util.go new file mode 100644 index 00000000..51920e84 --- /dev/null +++ b/internal/tree/utils/cloud_account_util.go @@ -0,0 +1,145 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import ( + "context" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "go.uber.org/zap" +) + +// VerifyAliyunCredentials 验证阿里云凭证 +func VerifyAliyunCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + // TODO: 实现阿里云凭证验证 + // 1. 使用阿里云SDK初始化客户端 + // 2. 调用DescribeRegions等基础API验证凭证有效性 + // 3. 返回验证结果 + logger.Info("验证阿里云凭证", + zap.String("region", req.Region)) + return nil +} + +// VerifyTencentCredentials 验证腾讯云凭证 +func VerifyTencentCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + // TODO: 实现腾讯云凭证验证 + // 1. 使用腾讯云SDK初始化客户端 + // 2. 调用DescribeRegions等基础API验证凭证有效性 + // 3. 返回验证结果 + logger.Info("验证腾讯云凭证", + zap.String("region", req.Region)) + return nil +} + +// VerifyAWSCredentials 验证AWS凭证 +func VerifyAWSCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + // TODO: 实现AWS凭证验证 + // 1. 使用AWS SDK初始化客户端 + // 2. 调用DescribeRegions等基础API验证凭证有效性 + // 3. 返回验证结果 + logger.Info("验证AWS凭证", + zap.String("region", req.Region)) + return nil +} + +// VerifyHuaweiCredentials 验证华为云凭证 +func VerifyHuaweiCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + // TODO: 实现华为云凭证验证 + // 1. 使用华为云SDK初始化客户端 + // 2. 调用DescribeRegions等基础API验证凭证有效性 + // 3. 返回验证结果 + logger.Info("验证华为云凭证", + zap.String("region", req.Region)) + return nil +} + +// VerifyAzureCredentials 验证Azure凭证 +func VerifyAzureCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + // TODO: 实现Azure凭证验证 + // 1. 使用Azure SDK初始化客户端 + // 2. 调用相关API验证凭证有效性 + // 3. 返回验证结果 + logger.Info("验证Azure凭证", + zap.String("region", req.Region)) + return nil +} + +// VerifyGCPCredentials 验证GCP凭证 +func VerifyGCPCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + // TODO: 实现GCP凭证验证 + // 1. 使用GCP SDK初始化客户端 + // 2. 调用相关API验证凭证有效性 + // 3. 返回验证结果 + logger.Info("验证GCP凭证", + zap.String("region", req.Region)) + return nil +} + +// MaskAccessKey 遮蔽AccessKey,用于日志输出 +func MaskAccessKey(accessKey string) string { + if len(accessKey) <= 10 { + return "***" + } + return accessKey[:10] + "..." +} + +// MaskSecretKey 遮蔽SecretKey,用于日志输出 +func MaskSecretKey(secretKey string) string { + if len(secretKey) <= 10 { + return "***" + } + return secretKey[:10] + "..." +} + +// ValidateCloudProvider 验证云厂商类型 +func ValidateCloudProvider(provider model.CloudProvider) bool { + return provider >= model.ProviderAliyun && provider <= model.ProviderGCP +} + +// ValidateCloudAccountStatus 验证云账户状态 +func ValidateCloudAccountStatus(status model.CloudAccountStatus) bool { + return status == model.CloudAccountEnabled || status == model.CloudAccountDisabled +} + +// GetProviderName 获取云厂商名称 +func GetProviderName(provider model.CloudProvider) string { + switch provider { + case model.ProviderAliyun: + return "阿里云" + case model.ProviderTencent: + return "腾讯云" + case model.ProviderAWS: + return "AWS" + case model.ProviderHuawei: + return "华为云" + case model.ProviderAzure: + return "Azure" + case model.ProviderGCP: + return "Google Cloud" + default: + return "未知云厂商" + } +} diff --git a/internal/tree/utils/crypto_util.go b/internal/tree/utils/common_util.go similarity index 77% rename from internal/tree/utils/crypto_util.go rename to internal/tree/utils/common_util.go index 650bb59e..918230cb 100644 --- a/internal/tree/utils/crypto_util.go +++ b/internal/tree/utils/common_util.go @@ -32,8 +32,40 @@ import ( "github.com/spf13/viper" ) +// ValidateAndSetPaginationDefaults 验证并设置分页参数的默认值 +func ValidateAndSetPaginationDefaults(page, size *int) { + if *page <= 0 { + *page = 1 + } + if *size <= 0 { + *size = 10 + } +} + +// ValidateID 验证ID是否有效 +func ValidateID(id int) error { + if id <= 0 { + return errors.New("无效的ID") + } + return nil +} + +// ValidateTreeNodeIDs 验证树节点ID列表 +func ValidateTreeNodeIDs(treeNodeIDs []int) bool { + return len(treeNodeIDs) > 0 +} + +// SetSSHDefaults 设置SSH连接的默认值 +func SetSSHDefaults(port *int, username *string) { + if *port == 0 { + *port = 22 + } + if *username == "" { + *username = "root" + } +} + // EncryptPassword 加密密码 -// 使用配置文件中的加密密钥对密码进行加密 func EncryptPassword(password string) (string, error) { if password == "" { return "", nil @@ -51,7 +83,6 @@ func EncryptPassword(password string) (string, error) { } // DecryptPassword 解密密码 -// 使用配置文件中的加密密钥对加密后的密码进行解密 func DecryptPassword(encryptedPassword string) (string, error) { if encryptedPassword == "" { return "", nil diff --git a/internal/tree/utils/tree_cloud_util.go b/internal/tree/utils/tree_cloud_util.go new file mode 100644 index 00000000..3a1291b9 --- /dev/null +++ b/internal/tree/utils/tree_cloud_util.go @@ -0,0 +1,161 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import ( + "context" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "go.uber.org/zap" +) + +// SyncAliyunResources 同步阿里云资源 +func SyncAliyunResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + // TODO: 实现阿里云资源同步 + // 1. 使用阿里云SDK初始化客户端 + // 2. 根据资源类型调用对应的API获取资源列表 + // 3. 将资源信息转换为内部模型 + // 4. 根据同步模式(全量/增量)更新数据库 + logger.Info("同步阿里云资源", + zap.String("syncMode", string(req.SyncMode)), + zap.Int8("resourceType", int8(req.ResourceType))) + return nil +} + +// SyncTencentResources 同步腾讯云资源 +func SyncTencentResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + // TODO: 实现腾讯云资源同步 + // 1. 使用腾讯云SDK初始化客户端 + // 2. 根据资源类型调用对应的API获取资源列表 + // 3. 将资源信息转换为内部模型 + // 4. 根据同步模式(全量/增量)更新数据库 + logger.Info("同步腾讯云资源", + zap.String("syncMode", string(req.SyncMode)), + zap.Int8("resourceType", int8(req.ResourceType))) + return nil +} + +// SyncAWSResources 同步AWS资源 +func SyncAWSResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + // TODO: 实现AWS资源同步 + // 1. 使用AWS SDK初始化客户端 + // 2. 根据资源类型调用对应的API获取资源列表 + // 3. 将资源信息转换为内部模型 + // 4. 根据同步模式(全量/增量)更新数据库 + logger.Info("同步AWS资源", + zap.String("syncMode", string(req.SyncMode)), + zap.Int8("resourceType", int8(req.ResourceType))) + return nil +} + +// SyncHuaweiResources 同步华为云资源 +func SyncHuaweiResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + // TODO: 实现华为云资源同步 + // 1. 使用华为云SDK初始化客户端 + // 2. 根据资源类型调用对应的API获取资源列表 + // 3. 将资源信息转换为内部模型 + // 4. 根据同步模式(全量/增量)更新数据库 + logger.Info("同步华为云资源", + zap.String("syncMode", string(req.SyncMode)), + zap.Int8("resourceType", int8(req.ResourceType))) + return nil +} + +// SyncAzureResources 同步Azure资源 +func SyncAzureResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + // TODO: 实现Azure资源同步 + // 1. 使用Azure SDK初始化客户端 + // 2. 根据资源类型调用对应的API获取资源列表 + // 3. 将资源信息转换为内部模型 + // 4. 根据同步模式(全量/增量)更新数据库 + logger.Info("同步Azure资源", + zap.String("syncMode", string(req.SyncMode)), + zap.Int8("resourceType", int8(req.ResourceType))) + return nil +} + +// SyncGCPResources 同步GCP资源 +func SyncGCPResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + // TODO: 实现GCP资源同步 + // 1. 使用GCP SDK初始化客户端 + // 2. 根据资源类型调用对应的API获取资源列表 + // 3. 将资源信息转换为内部模型 + // 4. 根据同步模式(全量/增量)更新数据库 + logger.Info("同步GCP资源", + zap.String("syncMode", string(req.SyncMode)), + zap.Int8("resourceType", int8(req.ResourceType))) + return nil +} + +// ValidateCloudResourceType 验证云资源类型 +func ValidateCloudResourceType(resourceType model.CloudResourceType) bool { + return resourceType >= model.ResourceTypeECS && resourceType <= model.ResourceTypeOther +} + +// ValidateCloudResourceStatus 验证云资源状态 +func ValidateCloudResourceStatus(status model.CloudResourceStatus) bool { + return status >= model.CloudResourceRunning && status <= model.CloudResourceUnknown +} + +// GetResourceTypeName 获取资源类型名称 +func GetResourceTypeName(resourceType model.CloudResourceType) string { + switch resourceType { + case model.ResourceTypeECS: + return "云服务器" + case model.ResourceTypeRDS: + return "云数据库" + case model.ResourceTypeSLB: + return "负载均衡" + case model.ResourceTypeOSS: + return "对象存储" + case model.ResourceTypeVPC: + return "虚拟私有云" + case model.ResourceTypeOther: + return "其他资源" + default: + return "未知资源类型" + } +} + +// GetResourceStatusName 获取资源状态名称 +func GetResourceStatusName(status model.CloudResourceStatus) string { + switch status { + case model.CloudResourceRunning: + return "运行中" + case model.CloudResourceStopped: + return "已停止" + case model.CloudResourceStarting: + return "启动中" + case model.CloudResourceStopping: + return "停止中" + case model.CloudResourceDeleted: + return "已删除" + case model.CloudResourceUnknown: + return "未知状态" + default: + return "未知" + } +} diff --git a/internal/tree/utils/tree_util.go b/internal/tree/utils/tree_node_util.go similarity index 69% rename from internal/tree/utils/tree_util.go rename to internal/tree/utils/tree_node_util.go index 415f6ab0..71a87b94 100644 --- a/internal/tree/utils/tree_util.go +++ b/internal/tree/utils/tree_node_util.go @@ -25,11 +25,45 @@ package utils -import "github.com/GoSimplicity/AI-CloudOps/internal/model" +import ( + "errors" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" +) + +// ValidateParentID 验证父节点ID是否有效 +func ValidateParentID(parentID int) error { + if parentID < 0 { + return errors.New("父节点ID不能为负数") + } + return nil +} + +// ValidateNodeMove 验证节点移动操作 +func ValidateNodeMove(nodeID, newParentID int) error { + if nodeID == newParentID { + return errors.New("节点不能移动到自己") + } + return nil +} + +// ValidateMemberType 验证成员类型 +func ValidateMemberType(memberType string) error { + if memberType != "" && memberType != "admin" && memberType != "member" && memberType != "all" { + return errors.New("成员类型只能是admin、member或all") + } + return nil +} + +// ValidateResourceIDs 验证资源ID列表 +func ValidateResourceIDs(resourceIDs []int) error { + if len(resourceIDs) == 0 { + return errors.New("资源ID列表不能为空") + } + return nil +} // BuildTreeStructure 构建树形结构 -// 将扁平化的节点列表转换为具有父子关系的树形结构 -// 返回根节点列表,每个节点的Children字段包含其子节点 func BuildTreeStructure(nodes []*model.TreeNode) []*model.TreeNode { // 创建节点映射表,用于快速查找节点 nodeMap := make(map[int]*model.TreeNode) diff --git a/internal/tree/utils/validation_util.go b/internal/tree/utils/validation_util.go deleted file mode 100644 index cd4e88c5..00000000 --- a/internal/tree/utils/validation_util.go +++ /dev/null @@ -1,91 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Bamboo - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -package utils - -import "errors" - -// ValidateAndSetPaginationDefaults 验证并设置分页参数的默认值 -// 如果page <= 0,设置为1 -// 如果size <= 0,设置为10 -func ValidateAndSetPaginationDefaults(page, size *int) { - if *page <= 0 { - *page = 1 - } - if *size <= 0 { - *size = 10 - } -} - -// ValidateID 验证ID是否有效 -// ID必须大于0 -func ValidateID(id int) error { - if id <= 0 { - return errors.New("无效的ID") - } - return nil -} - -// ValidateParentID 验证父节点ID是否有效 -// 父节点ID不能为负数 -func ValidateParentID(parentID int) error { - if parentID < 0 { - return errors.New("父节点ID不能为负数") - } - return nil -} - -// ValidateNodeMove 验证节点移动操作 -// 节点不能移动到自己 -func ValidateNodeMove(nodeID, newParentID int) error { - if nodeID == newParentID { - return errors.New("节点不能移动到自己") - } - return nil -} - -// ValidateMemberType 验证成员类型 -// 成员类型只能是 admin、member 或 all -func ValidateMemberType(memberType string) error { - if memberType != "" && memberType != "admin" && memberType != "member" && memberType != "all" { - return errors.New("成员类型只能是admin、member或all") - } - return nil -} - -// ValidateResourceIDs 验证资源ID列表 -// 资源ID列表不能为空 -func ValidateResourceIDs(resourceIDs []int) error { - if len(resourceIDs) == 0 { - return errors.New("资源ID列表不能为空") - } - return nil -} - -// ValidateTreeNodeIDs 验证树节点ID列表 -// 如果列表为空,返回false(表示没有需要处理的节点) -func ValidateTreeNodeIDs(treeNodeIDs []int) bool { - return len(treeNodeIDs) > 0 -} diff --git a/pkg/di/init.go b/pkg/di/init.go index ffc29c15..becbd40a 100644 --- a/pkg/di/init.go +++ b/pkg/di/init.go @@ -50,8 +50,8 @@ func InitTables(db *gorm.DB) error { // tree &model.TreeNode{}, &model.TreeLocalResource{}, - - // k8s + &model.TreeCloudResource{}, + &model.CloudAccount{}, &model.K8sCluster{}, // prometheus diff --git a/pkg/di/web.go b/pkg/di/web.go index 52b09fd6..8fb743c7 100644 --- a/pkg/di/web.go +++ b/pkg/di/web.go @@ -82,6 +82,8 @@ func InitGinServer( instanceTimeLineHdl *workorderApi.InstanceTimeLineHandler, treeNodeHdl *resourceApi.TreeNodeHandler, treeLocalHdl *resourceApi.TreeLocalHandler, + treeCloudHdl *resourceApi.TreeCloudHandler, + cloudAccountHdl *resourceApi.CloudAccountHandler, notificationHdl *workorderApi.NotificationHandler, ingressHdl *k8sApi.K8sIngressHandler, k8sPodHdl *k8sApi.K8sPodHandler, @@ -133,6 +135,8 @@ func InitGinServer( categoryHdl.RegisterRouters(server) treeNodeHdl.RegisterRouters(server) treeLocalHdl.RegisterRouters(server) + treeCloudHdl.RegisterRouters(server) + cloudAccountHdl.RegisterRouters(server) notificationHdl.RegisterRouters(server) ingressHdl.RegisterRouters(server) k8sPodHdl.RegisterRouters(server) diff --git a/pkg/di/wire.go b/pkg/di/wire.go index 8609cc61..1e75ca4d 100644 --- a/pkg/di/wire.go +++ b/pkg/di/wire.go @@ -131,6 +131,8 @@ var HandlerSet = wire.NewSet( workorderHandler.NewNotificationHandler, treeHandler.NewTreeNodeHandler, treeHandler.NewTreeLocalHandler, + treeHandler.NewTreeCloudHandler, + treeHandler.NewCloudAccountHandler, terminal.NewTerminalHandler, cronApi.NewCronJobHandler, ) @@ -185,6 +187,8 @@ var ServiceSet = wire.NewSet( workorderService.NewWorkorderNotificationService, treeService.NewTreeNodeService, treeService.NewTreeLocalService, + treeService.NewTreeCloudService, + treeService.NewCloudAccountService, cronService.NewCronService, ) @@ -216,6 +220,8 @@ var DaoSet = wire.NewSet( workorderDao.NewNotificationDAO, treeDao.NewTreeNodeDAO, treeDao.NewTreeLocalDAO, + treeDao.NewTreeCloudDAO, + treeDao.NewCloudAccountDAO, cronDao.NewCronJobDAO, ) diff --git a/pkg/di/wire_gen.go b/pkg/di/wire_gen.go index 312e6f67..d28c9b15 100644 --- a/pkg/di/wire_gen.go +++ b/pkg/di/wire_gen.go @@ -203,6 +203,12 @@ func ProvideCmd() *Cmd { treeLocalService := service6.NewTreeLocalService(logger, treeLocalDAO) sshClient := ssh.NewClient(logger) treeLocalHandler := api7.NewTreeLocalHandler(treeLocalService, sshClient) + treeCloudDAO := dao5.NewTreeCloudDAO(db, logger) + cloudAccountDAO := dao5.NewCloudAccountDAO(db, logger) + treeCloudService := service6.NewTreeCloudService(logger, treeCloudDAO, cloudAccountDAO) + treeCloudHandler := api7.NewTreeCloudHandler(treeCloudService, sshClient) + cloudAccountService := service6.NewCloudAccountService(logger, cloudAccountDAO) + cloudAccountHandler := api7.NewCloudAccountHandler(cloudAccountService) notificationHandler := api6.NewNotificationHandler(workorderNotificationService) ingressManager := manager.NewIngressManager(k8sClient, logger) ingressService := service4.NewIngressService(ingressManager, logger) @@ -222,7 +228,7 @@ func ProvideCmd() *Cmd { cronScheduler := scheduler.NewCronScheduler(logger, cronJobDAO, asynqScheduler, asynqClient) cronService := service7.NewCronService(logger, cronJobDAO, userDAO, asynqClient, cronScheduler) cronJobHandler := api8.NewCronJobHandler(logger, cronService) - engine := InitGinServer(v, userHandler, apiHandler, roleHandler, systemHandler, notAuthHandler, k8sClusterHandler, k8sDeploymentHandler, k8sNamespaceHandler, k8sNodeHandler, k8sSvcHandler, k8sYamlTaskHandler, k8sYamlTemplateHandler, k8sDaemonSetHandler, k8sEventHandler, k8sStatefulSetHandler, k8sServiceAccountHandler, k8sRoleHandler, k8sClusterRoleHandler, k8sRoleBindingHandler, k8sClusterRoleBindingHandler, k8sConfigMapHandler, k8sSecretHandler, alertEventHandler, alertPoolHandler, alertRuleHandler, monitorConfigHandler, onDutyGroupHandler, recordRuleHandler, scrapePoolHandler, scrapeJobHandler, sendGroupHandler, auditHandler, formDesignHandler, workorderProcessHandler, templateHandler, instanceHandler, instanceFlowHandler, instanceCommentHandler, categoryGroupHandler, instanceTimeLineHandler, treeNodeHandler, treeLocalHandler, notificationHandler, k8sIngressHandler, k8sPodHandler, k8sPVHandler, k8sPVCHandler, cronJobHandler) + engine := InitGinServer(v, userHandler, apiHandler, roleHandler, systemHandler, notAuthHandler, k8sClusterHandler, k8sDeploymentHandler, k8sNamespaceHandler, k8sNodeHandler, k8sSvcHandler, k8sYamlTaskHandler, k8sYamlTemplateHandler, k8sDaemonSetHandler, k8sEventHandler, k8sStatefulSetHandler, k8sServiceAccountHandler, k8sRoleHandler, k8sClusterRoleHandler, k8sRoleBindingHandler, k8sClusterRoleBindingHandler, k8sConfigMapHandler, k8sSecretHandler, alertEventHandler, alertPoolHandler, alertRuleHandler, monitorConfigHandler, onDutyGroupHandler, recordRuleHandler, scrapePoolHandler, scrapeJobHandler, sendGroupHandler, auditHandler, formDesignHandler, workorderProcessHandler, templateHandler, instanceHandler, instanceFlowHandler, instanceCommentHandler, categoryGroupHandler, instanceTimeLineHandler, treeNodeHandler, treeLocalHandler, treeCloudHandler, cloudAccountHandler, notificationHandler, k8sIngressHandler, k8sPodHandler, k8sPVHandler, k8sPVCHandler, cronJobHandler) applicationBootstrap := startup.NewApplicationBootstrap(clusterManager, logger) builtinTaskManager := cron.NewBuiltinTaskManager(logger, cronJobDAO) cronManager := cron.NewUnifiedCronManager(logger, alertManagerOnDutyDAO, clusterDAO, k8sClient, clusterManager, monitorCache, cronScheduler, builtinTaskManager) @@ -252,11 +258,11 @@ type Cmd struct { CronHandlers *handler.CronHandlers } -var HandlerSet = wire.NewSet(api2.NewRoleHandler, api2.NewApiHandler, api2.NewAuditHandler, api2.NewSystemHandler, api.NewUserHandler, api3.NewNotAuthHandler, api4.NewK8sNodeHandler, api4.NewK8sClusterHandler, api4.NewK8sDeploymentHandler, api4.NewK8sNamespaceHandler, api4.NewK8sSvcHandler, api4.NewK8sYamlTaskHandler, api4.NewK8sYamlTemplateHandler, api4.NewK8sDaemonSetHandler, api4.NewK8sEventHandler, api4.NewK8sStatefulSetHandler, api4.NewK8sServiceAccountHandler, api4.NewK8sRoleHandler, api4.NewK8sClusterRoleHandler, api4.NewK8sRoleBindingHandler, api4.NewK8sClusterRoleBindingHandler, api4.NewK8sRBACHandler, api4.NewK8sIngressHandler, api4.NewK8sPodHandler, api4.NewK8sConfigMapHandler, api4.NewK8sSecretHandler, api4.NewK8sPVHandler, api4.NewK8sPVCHandler, api5.NewAlertPoolHandler, api5.NewMonitorConfigHandler, api5.NewOnDutyGroupHandler, api5.NewRecordRuleHandler, api5.NewAlertRuleHandler, api5.NewSendGroupHandler, api5.NewScrapeJobHandler, api5.NewScrapePoolHandler, api5.NewAlertEventHandler, api6.NewFormDesignHandler, api6.NewInstanceHandler, api6.NewInstanceFlowHandler, api6.NewInstanceCommentHandler, api6.NewInstanceTimeLineHandler, api6.NewTemplateHandler, api6.NewWorkorderProcessHandler, api6.NewCategoryGroupHandler, api6.NewNotificationHandler, api7.NewTreeNodeHandler, api7.NewTreeLocalHandler, terminal.NewTerminalHandler, api8.NewCronJobHandler) +var HandlerSet = wire.NewSet(api2.NewRoleHandler, api2.NewApiHandler, api2.NewAuditHandler, api2.NewSystemHandler, api.NewUserHandler, api3.NewNotAuthHandler, api4.NewK8sNodeHandler, api4.NewK8sClusterHandler, api4.NewK8sDeploymentHandler, api4.NewK8sNamespaceHandler, api4.NewK8sSvcHandler, api4.NewK8sYamlTaskHandler, api4.NewK8sYamlTemplateHandler, api4.NewK8sDaemonSetHandler, api4.NewK8sEventHandler, api4.NewK8sStatefulSetHandler, api4.NewK8sServiceAccountHandler, api4.NewK8sRoleHandler, api4.NewK8sClusterRoleHandler, api4.NewK8sRoleBindingHandler, api4.NewK8sClusterRoleBindingHandler, api4.NewK8sRBACHandler, api4.NewK8sIngressHandler, api4.NewK8sPodHandler, api4.NewK8sConfigMapHandler, api4.NewK8sSecretHandler, api4.NewK8sPVHandler, api4.NewK8sPVCHandler, api5.NewAlertPoolHandler, api5.NewMonitorConfigHandler, api5.NewOnDutyGroupHandler, api5.NewRecordRuleHandler, api5.NewAlertRuleHandler, api5.NewSendGroupHandler, api5.NewScrapeJobHandler, api5.NewScrapePoolHandler, api5.NewAlertEventHandler, api6.NewFormDesignHandler, api6.NewInstanceHandler, api6.NewInstanceFlowHandler, api6.NewInstanceCommentHandler, api6.NewInstanceTimeLineHandler, api6.NewTemplateHandler, api6.NewWorkorderProcessHandler, api6.NewCategoryGroupHandler, api6.NewNotificationHandler, api7.NewTreeNodeHandler, api7.NewTreeLocalHandler, api7.NewTreeCloudHandler, api7.NewCloudAccountHandler, terminal.NewTerminalHandler, api8.NewCronJobHandler) -var ServiceSet = wire.NewSet(service4.NewClusterService, service4.NewDeploymentService, service4.NewNamespaceService, service4.NewSvcService, service4.NewNodeService, service4.NewTaintService, service4.NewYamlTaskService, service4.NewYamlTemplateService, service4.NewDaemonSetService, service4.NewEventService, service4.NewStatefulSetService, service4.NewServiceAccountService, service4.NewRoleService, service4.NewClusterRoleService, service4.NewRoleBindingService, service4.NewClusterRoleBindingService, service4.NewRBACService, service4.NewIngressService, service4.NewPodService, service4.NewConfigMapService, service4.NewSecretService, service4.NewPVService, service4.NewPVCService, service2.NewUserService, service.NewApiService, service.NewRoleService, service.NewAuditService, service.NewSystemService, alert2.NewAlertManagerEventService, alert2.NewAlertManagerOnDutyService, alert2.NewAlertManagerPoolService, alert2.NewAlertManagerRecordService, alert2.NewAlertManagerRuleService, alert2.NewAlertManagerSendService, scrape2.NewPrometheusScrapeService, scrape2.NewPrometheusPoolService, config2.NewMonitorConfigService, service3.NewNotAuthService, service5.NewFormDesignService, service5.NewInstanceService, service5.NewInstanceFlowService, service5.NewInstanceCommentService, service5.NewWorkorderInstanceTimeLineService, service5.NewWorkorderTemplateService, service5.NewWorkorderProcessService, service5.NewCategoryGroupService, service5.NewWorkorderNotificationService, service6.NewTreeNodeService, service6.NewTreeLocalService, service7.NewCronService) +var ServiceSet = wire.NewSet(service4.NewClusterService, service4.NewDeploymentService, service4.NewNamespaceService, service4.NewSvcService, service4.NewNodeService, service4.NewTaintService, service4.NewYamlTaskService, service4.NewYamlTemplateService, service4.NewDaemonSetService, service4.NewEventService, service4.NewStatefulSetService, service4.NewServiceAccountService, service4.NewRoleService, service4.NewClusterRoleService, service4.NewRoleBindingService, service4.NewClusterRoleBindingService, service4.NewRBACService, service4.NewIngressService, service4.NewPodService, service4.NewConfigMapService, service4.NewSecretService, service4.NewPVService, service4.NewPVCService, service2.NewUserService, service.NewApiService, service.NewRoleService, service.NewAuditService, service.NewSystemService, alert2.NewAlertManagerEventService, alert2.NewAlertManagerOnDutyService, alert2.NewAlertManagerPoolService, alert2.NewAlertManagerRecordService, alert2.NewAlertManagerRuleService, alert2.NewAlertManagerSendService, scrape2.NewPrometheusScrapeService, scrape2.NewPrometheusPoolService, config2.NewMonitorConfigService, service3.NewNotAuthService, service5.NewFormDesignService, service5.NewInstanceService, service5.NewInstanceFlowService, service5.NewInstanceCommentService, service5.NewWorkorderInstanceTimeLineService, service5.NewWorkorderTemplateService, service5.NewWorkorderProcessService, service5.NewCategoryGroupService, service5.NewWorkorderNotificationService, service6.NewTreeNodeService, service6.NewTreeLocalService, service6.NewTreeCloudService, service6.NewCloudAccountService, service7.NewCronService) -var DaoSet = wire.NewSet(alert.NewAlertManagerEventDAO, alert.NewAlertManagerOnDutyDAO, alert.NewAlertManagerPoolDAO, alert.NewAlertManagerRecordDAO, alert.NewAlertManagerRuleDAO, alert.NewAlertManagerSendDAO, scrape.NewScrapeJobDAO, scrape.NewScrapePoolDAO, config.NewMonitorConfigDAO, dao2.NewUserDAO, dao.NewRoleDAO, dao.NewApiDAO, dao.NewAuditDAO, dao3.NewClusterDAO, dao3.NewYamlTaskDAO, dao3.NewYamlTemplateDAO, dao4.NewWorkorderFormDesignDAO, dao4.NewTemplateDAO, dao4.NewWorkorderInstanceDAO, dao4.NewProcessDAO, dao4.NewWorkorderCategoryDAO, dao4.NewWorkorderInstanceCommentDAO, dao4.NewInstanceFlowDAO, dao4.NewInstanceTimeLineDAO, dao4.NewNotificationDAO, dao5.NewTreeNodeDAO, dao5.NewTreeLocalDAO, dao6.NewCronJobDAO) +var DaoSet = wire.NewSet(alert.NewAlertManagerEventDAO, alert.NewAlertManagerOnDutyDAO, alert.NewAlertManagerPoolDAO, alert.NewAlertManagerRecordDAO, alert.NewAlertManagerRuleDAO, alert.NewAlertManagerSendDAO, scrape.NewScrapeJobDAO, scrape.NewScrapePoolDAO, config.NewMonitorConfigDAO, dao2.NewUserDAO, dao.NewRoleDAO, dao.NewApiDAO, dao.NewAuditDAO, dao3.NewClusterDAO, dao3.NewYamlTaskDAO, dao3.NewYamlTemplateDAO, dao4.NewWorkorderFormDesignDAO, dao4.NewTemplateDAO, dao4.NewWorkorderInstanceDAO, dao4.NewProcessDAO, dao4.NewWorkorderCategoryDAO, dao4.NewWorkorderInstanceCommentDAO, dao4.NewInstanceFlowDAO, dao4.NewInstanceTimeLineDAO, dao4.NewNotificationDAO, dao5.NewTreeNodeDAO, dao5.NewTreeLocalDAO, dao5.NewTreeCloudDAO, dao5.NewCloudAccountDAO, dao6.NewCronJobDAO) var SSHSet = wire.NewSet(ssh.NewClient) From 4a634dd49311eda9241a15baf8c498934a1fe037 Mon Sep 17 00:00:00 2001 From: Bamboo <13664854532@163.com> Date: Thu, 9 Oct 2025 22:07:35 +0800 Subject: [PATCH 3/8] feat: Add CloudAccount model and related request types for cloud account management --- ...cloud_account.go => tree_cloud_account.go} | 0 internal/tree/api/tree_local_handler.go | 6 -- internal/tree/dao/tree_node_dao.go | 58 +++++++++++-------- internal/tree/service/tree_node_service.go | 12 ---- internal/tree/ssh/ssh.go | 1 - internal/tree/utils/cloud_account_util.go | 46 --------------- internal/tree/utils/tree_cloud_util.go | 50 ---------------- 7 files changed, 35 insertions(+), 138 deletions(-) rename internal/model/{cloud_account.go => tree_cloud_account.go} (100%) diff --git a/internal/model/cloud_account.go b/internal/model/tree_cloud_account.go similarity index 100% rename from internal/model/cloud_account.go rename to internal/model/tree_cloud_account.go diff --git a/internal/tree/api/tree_local_handler.go b/internal/tree/api/tree_local_handler.go index c240b7b0..ebef9422 100644 --- a/internal/tree/api/tree_local_handler.go +++ b/internal/tree/api/tree_local_handler.go @@ -30,24 +30,18 @@ import ( "github.com/GoSimplicity/AI-CloudOps/internal/tree/service" "github.com/GoSimplicity/AI-CloudOps/pkg/ssh" "github.com/GoSimplicity/AI-CloudOps/pkg/utils" - "github.com/GoSimplicity/AI-CloudOps/pkg/websocket" "github.com/gin-gonic/gin" ) type TreeLocalHandler struct { service service.TreeLocalService sshClient ssh.Client - wsManager websocket.Manager } func NewTreeLocalHandler(service service.TreeLocalService, sshClient ssh.Client) *TreeLocalHandler { - // 初始化WebSocket管理器 - wsManager := websocket.NewManager(nil, nil) - return &TreeLocalHandler{ service: service, sshClient: sshClient, - wsManager: wsManager, } } diff --git a/internal/tree/dao/tree_node_dao.go b/internal/tree/dao/tree_node_dao.go index 3f0cfed7..05212585 100644 --- a/internal/tree/dao/tree_node_dao.go +++ b/internal/tree/dao/tree_node_dao.go @@ -149,41 +149,53 @@ func (t *treeNodeDAO) GetChildNodes(ctx context.Context, parentID int) ([]*model // GetTreeStatistics 获取服务树统计数据 func (t *treeNodeDAO) GetTreeStatistics(ctx context.Context) (*model.TreeNodeStatisticsResp, error) { var stats model.TreeNodeStatisticsResp + var count int64 // 节点总数 - if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Count((*int64)(&[]int64{0}[0])).Error; err != nil { + if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Count(&count).Error; err != nil { t.logger.Error("统计节点总数失败", zap.Error(err)) - } - // 为了避免使用中间变量,这里分别统计并赋值 - var c int64 - if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Count(&c).Error; err == nil { - stats.TotalNodes = int(c) + } else { + stats.TotalNodes = int(count) } - // 活跃/非活跃 - c = 0 - if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("status = ?", model.ACTIVE).Count(&c).Error; err == nil { - stats.ActiveNodes = int(c) + // 活跃节点数 + count = 0 + if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("status = ?", model.ACTIVE).Count(&count).Error; err != nil { + t.logger.Error("统计活跃节点失败", zap.Error(err)) + } else { + stats.ActiveNodes = int(count) } - c = 0 - if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("status = ?", model.INACTIVE).Count(&c).Error; err == nil { - stats.InactiveNodes = int(c) + + // 非活跃节点数 + count = 0 + if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("status = ?", model.INACTIVE).Count(&count).Error; err != nil { + t.logger.Error("统计非活跃节点失败", zap.Error(err)) + } else { + stats.InactiveNodes = int(count) } // 资源总数 - c = 0 - if err := t.db.WithContext(ctx).Model(&model.TreeLocalResource{}).Count(&c).Error; err == nil { - stats.TotalResources = int(c) + count = 0 + if err := t.db.WithContext(ctx).Model(&model.TreeLocalResource{}).Count(&count).Error; err != nil { + t.logger.Error("统计资源总数失败", zap.Error(err)) + } else { + stats.TotalResources = int(count) } - // 管理员与成员总数(关联关系条目数) - c = 0 - if err := t.db.WithContext(ctx).Table("cl_tree_node_admin").Count(&c).Error; err == nil { - stats.TotalAdmins = int(c) + // 管理员总数(关联关系条目数) + count = 0 + if err := t.db.WithContext(ctx).Table("cl_tree_node_admin").Count(&count).Error; err != nil { + t.logger.Error("统计管理员总数失败", zap.Error(err)) + } else { + stats.TotalAdmins = int(count) } - c = 0 - if err := t.db.WithContext(ctx).Table("cl_tree_node_member").Count(&c).Error; err == nil { - stats.TotalMembers = int(c) + + // 成员总数(关联关系条目数) + count = 0 + if err := t.db.WithContext(ctx).Table("cl_tree_node_member").Count(&count).Error; err != nil { + t.logger.Error("统计成员总数失败", zap.Error(err)) + } else { + stats.TotalMembers = int(count) } return &stats, nil diff --git a/internal/tree/service/tree_node_service.go b/internal/tree/service/tree_node_service.go index b5204035..116b3ac5 100644 --- a/internal/tree/service/tree_node_service.go +++ b/internal/tree/service/tree_node_service.go @@ -37,18 +37,6 @@ import ( "go.uber.org/zap" ) -const ( - NodeAdminRole = "admin" // 管理员角色 - NodeMemberRole = "member" // 普通成员角色 - NodeStatusActive = "active" // 活跃状态 - NodeStatusInactive = "inactive" // 非活跃状态 - NodeStatusDeleted = "deleted" // 删除状态 - - // 默认值 - DefaultLevel = 1 - DefaultStatus = NodeStatusActive -) - type TreeNodeService interface { // 树结构相关接口 GetTreeList(ctx context.Context, req *model.GetTreeNodeListReq) (model.ListResp[*model.TreeNode], error) diff --git a/internal/tree/ssh/ssh.go b/internal/tree/ssh/ssh.go index 2a5f18c7..50f0cc1a 100644 --- a/internal/tree/ssh/ssh.go +++ b/internal/tree/ssh/ssh.go @@ -30,7 +30,6 @@ type ecsSSH struct { UserID int // 用户ID,用于区分不同用户的会话 Sessions map[int]*ssh.Session // 用户会话映射表,key为UserID,value为对应的SSH会话 sessionMu sync.RWMutex // 保护Sessions的读写锁 - Channel ssh.Channel // SSH通信通道(当前版本未使用) LastResult string // 最近一次执行命令的结果 logger *zap.Logger // 日志记录器 } diff --git a/internal/tree/utils/cloud_account_util.go b/internal/tree/utils/cloud_account_util.go index 51920e84..dd6132e9 100644 --- a/internal/tree/utils/cloud_account_util.go +++ b/internal/tree/utils/cloud_account_util.go @@ -97,49 +97,3 @@ func VerifyGCPCredentials(ctx context.Context, req *model.VerifyCloudCredentials zap.String("region", req.Region)) return nil } - -// MaskAccessKey 遮蔽AccessKey,用于日志输出 -func MaskAccessKey(accessKey string) string { - if len(accessKey) <= 10 { - return "***" - } - return accessKey[:10] + "..." -} - -// MaskSecretKey 遮蔽SecretKey,用于日志输出 -func MaskSecretKey(secretKey string) string { - if len(secretKey) <= 10 { - return "***" - } - return secretKey[:10] + "..." -} - -// ValidateCloudProvider 验证云厂商类型 -func ValidateCloudProvider(provider model.CloudProvider) bool { - return provider >= model.ProviderAliyun && provider <= model.ProviderGCP -} - -// ValidateCloudAccountStatus 验证云账户状态 -func ValidateCloudAccountStatus(status model.CloudAccountStatus) bool { - return status == model.CloudAccountEnabled || status == model.CloudAccountDisabled -} - -// GetProviderName 获取云厂商名称 -func GetProviderName(provider model.CloudProvider) string { - switch provider { - case model.ProviderAliyun: - return "阿里云" - case model.ProviderTencent: - return "腾讯云" - case model.ProviderAWS: - return "AWS" - case model.ProviderHuawei: - return "华为云" - case model.ProviderAzure: - return "Azure" - case model.ProviderGCP: - return "Google Cloud" - default: - return "未知云厂商" - } -} diff --git a/internal/tree/utils/tree_cloud_util.go b/internal/tree/utils/tree_cloud_util.go index 3a1291b9..61465416 100644 --- a/internal/tree/utils/tree_cloud_util.go +++ b/internal/tree/utils/tree_cloud_util.go @@ -109,53 +109,3 @@ func SyncGCPResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, zap.Int8("resourceType", int8(req.ResourceType))) return nil } - -// ValidateCloudResourceType 验证云资源类型 -func ValidateCloudResourceType(resourceType model.CloudResourceType) bool { - return resourceType >= model.ResourceTypeECS && resourceType <= model.ResourceTypeOther -} - -// ValidateCloudResourceStatus 验证云资源状态 -func ValidateCloudResourceStatus(status model.CloudResourceStatus) bool { - return status >= model.CloudResourceRunning && status <= model.CloudResourceUnknown -} - -// GetResourceTypeName 获取资源类型名称 -func GetResourceTypeName(resourceType model.CloudResourceType) string { - switch resourceType { - case model.ResourceTypeECS: - return "云服务器" - case model.ResourceTypeRDS: - return "云数据库" - case model.ResourceTypeSLB: - return "负载均衡" - case model.ResourceTypeOSS: - return "对象存储" - case model.ResourceTypeVPC: - return "虚拟私有云" - case model.ResourceTypeOther: - return "其他资源" - default: - return "未知资源类型" - } -} - -// GetResourceStatusName 获取资源状态名称 -func GetResourceStatusName(status model.CloudResourceStatus) string { - switch status { - case model.CloudResourceRunning: - return "运行中" - case model.CloudResourceStopped: - return "已停止" - case model.CloudResourceStarting: - return "启动中" - case model.CloudResourceStopping: - return "停止中" - case model.CloudResourceDeleted: - return "已删除" - case model.CloudResourceUnknown: - return "未知状态" - default: - return "未知" - } -} From e2a5b9a23fb4b2c619057cde73d4aef5b59969a1 Mon Sep 17 00:00:00 2001 From: Bamboo <13664854532@163.com> Date: Thu, 9 Oct 2025 22:43:36 +0800 Subject: [PATCH 4/8] feat: Integrate Alibaba Cloud SDK for resource management and implement credential verification --- go.mod | 2 + go.sum | 31 ++- internal/model/tree_node.go | 2 +- internal/tree/api/tree_cloud_handler.go | 10 - internal/tree/dao/tree_node_dao.go | 12 +- .../tree/service/cloud_account_service.go | 58 +++-- internal/tree/service/tree_cloud_service.go | 233 ++++++++++++++---- internal/tree/service/tree_node_service.go | 3 + internal/tree/utils/aliyun_util.go | 201 +++++++++++++++ internal/tree/utils/cloud_account_util.go | 63 ++--- internal/tree/utils/tree_cloud_util.go | 107 ++++---- 11 files changed, 552 insertions(+), 170 deletions(-) create mode 100644 internal/tree/utils/aliyun_util.go diff --git a/go.mod b/go.mod index aa439431..010d0f1a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/GoSimplicity/AI-CloudOps go 1.24.6 require ( + github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 github.com/casbin/casbin/v2 v2.93.0 github.com/fatih/color v1.16.0 github.com/gin-contrib/cors v1.7.2 @@ -169,6 +170,7 @@ require ( github.com/oklog/run v1.2.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/onsi/ginkgo v1.16.5 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect diff --git a/go.sum b/go.sum index 6cd4482c..a4bdd25e 100644 --- a/go.sum +++ b/go.sum @@ -55,12 +55,14 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -68,6 +70,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -145,6 +149,7 @@ github.com/coder/quartz v0.1.2/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKm github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -181,6 +186,7 @@ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -257,12 +263,14 @@ github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -326,7 +334,6 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -434,6 +441,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -514,6 +522,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -539,6 +548,8 @@ github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrB github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/openkruise/kruise-api v1.7.0 h1:Mg13oePPZZ1XfOEXqXTFgg4wNC07CnPK6fcPePLkv/U= github.com/openkruise/kruise-api v1.7.0/go.mod h1:BXZAyzIPmaF0JEI0YT1fWEYTAcOCJRCzOdCg4BpXbXQ= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -665,6 +676,10 @@ github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= @@ -691,6 +706,7 @@ go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCRE go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= @@ -717,7 +733,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= @@ -729,6 +748,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -902,8 +922,10 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -955,6 +977,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1043,6 +1069,7 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -1068,6 +1095,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= @@ -1117,6 +1145,7 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/controller-runtime v0.16.6 h1:FiXwTuFF5ZJKmozfP2Z0j7dh6kmxP4Ou1KLfxgKKC3I= diff --git a/internal/model/tree_node.go b/internal/model/tree_node.go index 4e1dfc0e..12a23df8 100644 --- a/internal/model/tree_node.go +++ b/internal/model/tree_node.go @@ -69,9 +69,9 @@ func (t *TreeNode) TableName() string { // GetTreeNodeListReq 获取树节点列表请求 type GetTreeNodeListReq struct { + ListReq Level int `json:"level" form:"level" binding:"omitempty,min=1"` Status TreeNodeStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2"` - Search string `json:"search" form:"search" binding:"omitempty"` } // GetTreeNodeDetailReq 获取节点详情请求 diff --git a/internal/tree/api/tree_cloud_handler.go b/internal/tree/api/tree_cloud_handler.go index 761e77a5..aa932822 100644 --- a/internal/tree/api/tree_cloud_handler.go +++ b/internal/tree/api/tree_cloud_handler.go @@ -55,7 +55,6 @@ func (h *TreeCloudHandler) RegisterRouters(server *gin.Engine) { cloudGroup.DELETE("/:id/delete", h.DeleteTreeCloudResource) cloudGroup.POST("/:id/bind", h.BindTreeCloudResource) cloudGroup.POST("/:id/unbind", h.UnBindTreeCloudResource) - cloudGroup.POST("/verify", h.VerifyCloudCredentials) cloudGroup.POST("/sync", h.SyncTreeCloudResource) cloudGroup.POST("/batch_import", h.BatchImportCloudResource) cloudGroup.GET("/:id/node", h.GetTreeNodeCloudResources) @@ -172,15 +171,6 @@ func (h *TreeCloudHandler) UnBindTreeCloudResource(ctx *gin.Context) { }) } -// VerifyCloudCredentials 验证云厂商凭证 -func (h *TreeCloudHandler) VerifyCloudCredentials(ctx *gin.Context) { - var req model.VerifyCloudCredentialsReq - - utils.HandleRequest(ctx, &req, func() (interface{}, error) { - return nil, h.service.VerifyCloudCredentials(ctx, &req) - }) -} - // SyncTreeCloudResource 从云厂商同步资源 func (h *TreeCloudHandler) SyncTreeCloudResource(ctx *gin.Context) { var req model.SyncTreeCloudResourceReq diff --git a/internal/tree/dao/tree_node_dao.go b/internal/tree/dao/tree_node_dao.go index 05212585..dda92171 100644 --- a/internal/tree/dao/tree_node_dao.go +++ b/internal/tree/dao/tree_node_dao.go @@ -95,22 +95,26 @@ func (t *treeNodeDAO) GetTreeList(ctx context.Context, req *model.GetTreeNodeLis return nil, 0, err } - // 预加载关联数据并查询 - if err := query.Preload("AdminUsers"). + // 分页查询 + offset := (req.Page - 1) * req.Size + if err := query. + Preload("AdminUsers"). Preload("MemberUsers"). Preload("TreeLocalResources"). Order("level ASC, parent_id ASC, name ASC"). + Limit(req.Size). + Offset(offset). Find(&nodes).Error; err != nil { t.logger.Error("获取树节点列表失败", zap.Error(err)) return nil, 0, err } - // 如果指定了层级,直接返回列表 + // 如果指定了层级,直接返回列表(已分页) if req.Level > 0 { return nodes, count, nil } - // 构建树形结构 + // 构建树形结构(基于已分页的数据) return treeUtils.BuildTreeStructure(nodes), count, nil } diff --git a/internal/tree/service/cloud_account_service.go b/internal/tree/service/cloud_account_service.go index 3adc296b..9828c92b 100644 --- a/internal/tree/service/cloud_account_service.go +++ b/internal/tree/service/cloud_account_service.go @@ -255,27 +255,53 @@ func (s *cloudAccountService) VerifyCloudAccount(ctx context.Context, req *model return fmt.Errorf("解密SecretKey失败: %w", err) } - // TODO: 根据 Provider 调用相应的云厂商 SDK 验证凭证 - // 这里需要实现具体的云厂商验证逻辑 - - // 截断敏感信息用于日志 - akLog := accessKey - if len(akLog) > 10 { - akLog = akLog[:10] + "..." + // 根据 Provider 调用相应的云厂商 SDK 验证凭证 + verifyReq := &model.VerifyCloudCredentialsReq{ + Provider: account.Provider, + Region: account.Region, + AccessKey: accessKey, + SecretKey: secretKey, } - skLog := secretKey - if len(skLog) > 10 { - skLog = skLog[:10] + "..." + + switch account.Provider { + case model.ProviderAliyun: + if err := treeUtils.VerifyAliyunCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("阿里云凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("阿里云凭证验证失败: %w", err) + } + case model.ProviderTencent: + if err := treeUtils.VerifyTencentCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("腾讯云凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("腾讯云凭证验证失败: %w", err) + } + case model.ProviderAWS: + if err := treeUtils.VerifyAWSCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("AWS凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("AWS凭证验证失败: %w", err) + } + case model.ProviderHuawei: + if err := treeUtils.VerifyHuaweiCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("华为云凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("华为云凭证验证失败: %w", err) + } + case model.ProviderAzure: + if err := treeUtils.VerifyAzureCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("Azure凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("Azure凭证验证失败: %w", err) + } + case model.ProviderGCP: + if err := treeUtils.VerifyGCPCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("GCP凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("GCP凭证验证失败: %w", err) + } + default: + return fmt.Errorf("不支持的云厂商: %d", account.Provider) } - s.logger.Info("验证云账户凭证", + s.logger.Info("云账户凭证验证成功", zap.Int("id", req.ID), zap.Int8("provider", int8(account.Provider)), - zap.String("region", account.Region), - zap.String("accessKey", akLog), - zap.String("secretKey", skLog), - ) + zap.String("region", account.Region)) - // 暂时返回成功,实际应该调用云厂商API验证 return nil } diff --git a/internal/tree/service/tree_cloud_service.go b/internal/tree/service/tree_cloud_service.go index 337a3132..7db41f13 100644 --- a/internal/tree/service/tree_cloud_service.go +++ b/internal/tree/service/tree_cloud_service.go @@ -48,7 +48,6 @@ type TreeCloudService interface { UnBindTreeCloudResource(ctx context.Context, req *model.UnBindTreeCloudResourceReq) error GetTreeNodeCloudResources(ctx context.Context, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) UpdateCloudResourceStatus(ctx context.Context, req *model.UpdateCloudResourceStatusReq) error - VerifyCloudCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq) error SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) error BatchImportCloudResource(ctx context.Context, req *model.BatchImportCloudResourceReq) ([]int, error) } @@ -344,33 +343,6 @@ func (s *treeCloudService) UpdateCloudResourceStatus(ctx context.Context, req *m return nil } -// VerifyCloudCredentials 验证云厂商凭证 -func (s *treeCloudService) VerifyCloudCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq) error { - // TODO: 实现具体的云厂商SDK验证逻辑 - // 这里需要根据不同的云厂商(阿里云、腾讯云、AWS等)调用对应的SDK验证凭证 - s.logger.Info("验证云厂商凭证", - zap.Int8("provider", int8(req.Provider)), - zap.String("region", req.Region)) - - // 根据云厂商类型验证凭证 - switch req.Provider { - case model.ProviderAliyun: - return treeUtils.VerifyAliyunCredentials(ctx, req, s.logger) - case model.ProviderTencent: - return treeUtils.VerifyTencentCredentials(ctx, req, s.logger) - case model.ProviderAWS: - return treeUtils.VerifyAWSCredentials(ctx, req, s.logger) - case model.ProviderHuawei: - return treeUtils.VerifyHuaweiCredentials(ctx, req, s.logger) - case model.ProviderAzure: - return treeUtils.VerifyAzureCredentials(ctx, req, s.logger) - case model.ProviderGCP: - return treeUtils.VerifyGCPCredentials(ctx, req, s.logger) - default: - return fmt.Errorf("不支持的云厂商: %d", req.Provider) - } -} - // SyncTreeCloudResource 从云厂商同步资源 func (s *treeCloudService) SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) error { // 获取云账户信息 @@ -388,32 +360,135 @@ func (s *treeCloudService) SyncTreeCloudResource(ctx context.Context, req *model return errors.New("云账户已禁用,无法同步资源") } + // 解密密钥 + accessKey, err := treeUtils.DecryptPassword(account.AccessKey) + if err != nil { + s.logger.Error("解密AccessKey失败", zap.Error(err)) + return fmt.Errorf("解密AccessKey失败: %w", err) + } + + secretKey, err := treeUtils.DecryptPassword(account.SecretKey) + if err != nil { + s.logger.Error("解密SecretKey失败", zap.Error(err)) + return fmt.Errorf("解密SecretKey失败: %w", err) + } + s.logger.Info("同步云资源", zap.Int("cloudAccountID", req.CloudAccountID), zap.Int8("provider", int8(account.Provider)), zap.String("region", account.Region), zap.String("syncMode", string(req.SyncMode))) - // TODO: 根据不同的云厂商调用对应的同步逻辑 - // 这里需要实现具体的云厂商SDK调用 + // 根据不同的云厂商调用对应的同步逻辑 switch account.Provider { case model.ProviderAliyun: - return errors.New("阿里云资源同步功能待实现") + return s.syncAliyunResources(ctx, account, accessKey, secretKey, req) case model.ProviderTencent: - return errors.New("腾讯云资源同步功能待实现") + return errors.New("腾讯云资源同步功能暂未实现") case model.ProviderAWS: - return errors.New("AWS资源同步功能待实现") + return errors.New("AWS资源同步功能暂未实现") case model.ProviderHuawei: - return errors.New("华为云资源同步功能待实现") + return errors.New("华为云资源同步功能暂未实现") case model.ProviderAzure: - return errors.New("Azure资源同步功能待实现") + return errors.New("Azure资源同步功能暂未实现") case model.ProviderGCP: - return errors.New("GCP资源同步功能待实现") + return errors.New("GCP资源同步功能暂未实现") default: return fmt.Errorf("不支持的云厂商: %d", account.Provider) } } +// syncAliyunResources 同步阿里云资源 +func (s *treeCloudService) syncAliyunResources(ctx context.Context, account *model.CloudAccount, accessKey, secretKey string, req *model.SyncTreeCloudResourceReq) error { + // 构建同步配置 + config := &treeUtils.AliyunSyncConfig{ + AccessKey: accessKey, + SecretKey: secretKey, + Region: account.Region, + CloudAccountID: account.ID, + ResourceType: req.ResourceType, + InstanceIDs: req.InstanceIDs, + SyncMode: req.SyncMode, + } + + // 从阿里云获取资源列表 + resources, err := treeUtils.SyncAliyunResources(ctx, config, s.logger) + if err != nil { + return err + } + + // 根据同步模式处理资源 + if req.SyncMode == model.SyncModeFull { + // 全量同步:先删除该云账户下的所有ECS资源,再重新创建 + return s.fullSyncResources(ctx, account.ID, resources) + } + + // 增量同步:更新已存在的资源,创建不存在的资源 + return s.incrementalSyncResources(ctx, account.ID, resources) +} + +// fullSyncResources 全量同步资源 +func (s *treeCloudService) fullSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource) error { + // 获取该云账户下的所有ECS资源 + req := &model.GetTreeCloudResourceListReq{ + ListReq: model.ListReq{ + Page: 1, + Size: 10000, // 获取所有资源 + }, + CloudAccountID: cloudAccountID, + ResourceType: model.ResourceTypeECS, + } + existingResources, _, err := s.dao.GetList(ctx, req) + if err != nil { + s.logger.Error("获取现有资源失败", zap.Error(err)) + return err + } + + // 删除不在新资源列表中的资源 + newInstanceIDSet := make(map[string]bool) + for _, resource := range resources { + newInstanceIDSet[resource.InstanceID] = true + } + + for _, existingResource := range existingResources { + if !newInstanceIDSet[existingResource.InstanceID] { + if err := s.dao.Delete(ctx, existingResource.ID); err != nil { + s.logger.Error("删除资源失败", zap.Int("id", existingResource.ID), zap.Error(err)) + } + } + } + + // 更新或创建资源 + return s.incrementalSyncResources(ctx, cloudAccountID, resources) +} + +// incrementalSyncResources 增量同步资源 +func (s *treeCloudService) incrementalSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource) error { + for _, resource := range resources { + // 检查资源是否已存在 + existing, err := s.dao.GetByAccountAndInstanceID(ctx, cloudAccountID, resource.InstanceID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.logger.Error("查询资源失败", zap.String("instanceID", resource.InstanceID), zap.Error(err)) + continue + } + + if existing != nil { + // 更新现有资源 + resource.ID = existing.ID + if err := s.dao.Update(ctx, resource); err != nil { + s.logger.Error("更新资源失败", zap.Int("id", existing.ID), zap.Error(err)) + } + } else { + // 创建新资源 + if err := s.dao.Create(ctx, resource); err != nil { + s.logger.Error("创建资源失败", zap.String("instanceID", resource.InstanceID), zap.Error(err)) + } + } + } + + return nil +} + // BatchImportCloudResource 批量导入云资源 func (s *treeCloudService) BatchImportCloudResource(ctx context.Context, req *model.BatchImportCloudResourceReq) ([]int, error) { if len(req.InstanceIDs) == 0 { @@ -435,17 +510,91 @@ func (s *treeCloudService) BatchImportCloudResource(ctx context.Context, req *mo return nil, errors.New("云账户已禁用,无法导入资源") } + // 解密密钥 + accessKey, err := treeUtils.DecryptPassword(account.AccessKey) + if err != nil { + s.logger.Error("解密AccessKey失败", zap.Error(err)) + return nil, fmt.Errorf("解密AccessKey失败: %w", err) + } + + secretKey, err := treeUtils.DecryptPassword(account.SecretKey) + if err != nil { + s.logger.Error("解密SecretKey失败", zap.Error(err)) + return nil, fmt.Errorf("解密SecretKey失败: %w", err) + } + s.logger.Info("批量导入云资源", zap.Int("cloudAccountID", req.CloudAccountID), zap.Int8("provider", int8(account.Provider)), zap.String("region", account.Region), zap.Int("count", len(req.InstanceIDs))) - // TODO: 实现批量导入逻辑 - // 1. 根据云厂商调用对应的SDK获取实例详情 - // 2. 批量创建云资源记录 - // 3. 返回创建的资源ID列表 + // 根据云厂商调用对应的导入逻辑 + switch account.Provider { + case model.ProviderAliyun: + return s.batchImportAliyunResources(ctx, account, accessKey, secretKey, req) + case model.ProviderTencent: + return nil, errors.New("腾讯云批量导入功能暂未实现") + case model.ProviderAWS: + return nil, errors.New("AWS批量导入功能暂未实现") + case model.ProviderHuawei: + return nil, errors.New("华为云批量导入功能暂未实现") + case model.ProviderAzure: + return nil, errors.New("Azure批量导入功能暂未实现") + case model.ProviderGCP: + return nil, errors.New("GCP批量导入功能暂未实现") + default: + return nil, fmt.Errorf("不支持的云厂商: %d", account.Provider) + } +} + +// batchImportAliyunResources 批量导入阿里云资源 +func (s *treeCloudService) batchImportAliyunResources(ctx context.Context, account *model.CloudAccount, accessKey, secretKey string, req *model.BatchImportCloudResourceReq) ([]int, error) { + // 构建同步配置,指定要导入的实例ID + config := &treeUtils.AliyunSyncConfig{ + AccessKey: accessKey, + SecretKey: secretKey, + Region: account.Region, + CloudAccountID: account.ID, + ResourceType: model.ResourceTypeECS, + InstanceIDs: req.InstanceIDs, + SyncMode: model.SyncModeIncremental, + } + + // 从阿里云获取指定实例的详情 + resources, err := treeUtils.SyncAliyunResources(ctx, config, s.logger) + if err != nil { + return nil, err + } + + // 批量导入资源 + var importedIDs []int + for _, resource := range resources { + // 检查资源是否已存在 + existing, err := s.dao.GetByAccountAndInstanceID(ctx, account.ID, resource.InstanceID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.logger.Error("查询资源失败", zap.String("instanceID", resource.InstanceID), zap.Error(err)) + continue + } + + if existing != nil { + // 资源已存在,更新 + resource.ID = existing.ID + if err := s.dao.Update(ctx, resource); err != nil { + s.logger.Error("更新资源失败", zap.Int("id", existing.ID), zap.Error(err)) + continue + } + importedIDs = append(importedIDs, existing.ID) + } else { + // 创建新资源 + if err := s.dao.Create(ctx, resource); err != nil { + s.logger.Error("创建资源失败", zap.String("instanceID", resource.InstanceID), zap.Error(err)) + continue + } + importedIDs = append(importedIDs, resource.ID) + } + } - // 暂时返回空列表 - return []int{}, errors.New("批量导入云资源功能待实现") + s.logger.Info("批量导入阿里云资源成功", zap.Int("count", len(importedIDs))) + return importedIDs, nil } diff --git a/internal/tree/service/tree_node_service.go b/internal/tree/service/tree_node_service.go index 116b3ac5..4151c827 100644 --- a/internal/tree/service/tree_node_service.go +++ b/internal/tree/service/tree_node_service.go @@ -80,6 +80,9 @@ func (s *treeService) GetTreeList(ctx context.Context, req *model.GetTreeNodeLis return model.ListResp[*model.TreeNode]{}, errors.New("层级不能为负数") } + // 设置分页默认值 + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) + s.logger.Debug("获取树节点列表", zap.Int("level", req.Level), zap.Int("status", int(req.Status))) trees, total, err := s.dao.GetTreeList(ctx, req) diff --git a/internal/tree/utils/aliyun_util.go b/internal/tree/utils/aliyun_util.go new file mode 100644 index 00000000..85514cde --- /dev/null +++ b/internal/tree/utils/aliyun_util.go @@ -0,0 +1,201 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" + "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" + "go.uber.org/zap" +) + +// AliyunClient 阿里云客户端封装 +type AliyunClient struct { + client *ecs.Client + logger *zap.Logger +} + +// NewAliyunClient 创建阿里云客户端 +func NewAliyunClient(accessKey, secretKey, region string, logger *zap.Logger) (*AliyunClient, error) { + client, err := ecs.NewClientWithAccessKey(region, accessKey, secretKey) + if err != nil { + return nil, fmt.Errorf("创建阿里云客户端失败: %w", err) + } + + return &AliyunClient{ + client: client, + logger: logger, + }, nil +} + +// VerifyCredentials 验证阿里云凭证 +func (c *AliyunClient) VerifyCredentials(ctx context.Context) error { + request := ecs.CreateDescribeRegionsRequest() + request.Scheme = "https" + + _, err := c.client.DescribeRegions(request) + if err != nil { + return fmt.Errorf("阿里云凭证验证失败: %w", err) + } + + c.logger.Info("阿里云凭证验证成功") + return nil +} + +// ListECSInstances 获取ECS实例列表(支持分页获取所有实例) +func (c *AliyunClient) ListECSInstances(ctx context.Context, instanceIDs []string) ([]*model.TreeCloudResource, error) { + var allResources []*model.TreeCloudResource + pageNumber := 1 + pageSize := 100 + + for { + request := ecs.CreateDescribeInstancesRequest() + request.Scheme = "https" + request.PageSize = requests.NewInteger(pageSize) + request.PageNumber = requests.NewInteger(pageNumber) + + // 如果指定了实例ID,则只获取指定的实例 + if len(instanceIDs) > 0 { + request.InstanceIds = fmt.Sprintf(`["%s"]`, strings.Join(instanceIDs, `","`)) + } + + response, err := c.client.DescribeInstances(request) + if err != nil { + return nil, fmt.Errorf("获取ECS实例列表失败(页码:%d): %w", pageNumber, err) + } + + // 转换实例数据 + for _, instance := range response.Instances.Instance { + resource := c.convertECSToResource(&instance) + allResources = append(allResources, resource) + } + + // 检查是否还有下一页 + if len(response.Instances.Instance) < pageSize { + break + } + + // 如果指定了实例ID,不需要分页 + if len(instanceIDs) > 0 { + break + } + + pageNumber++ + } + + c.logger.Info("获取阿里云ECS实例成功", zap.Int("count", len(allResources))) + return allResources, nil +} + +// convertECSToResource 将阿里云ECS实例转换为内部资源模型 +func (c *AliyunClient) convertECSToResource(instance *ecs.Instance) *model.TreeCloudResource { + resource := &model.TreeCloudResource{ + Name: instance.InstanceName, + ResourceType: model.ResourceTypeECS, + InstanceID: instance.InstanceId, + InstanceType: instance.InstanceType, + Status: c.convertECSStatus(instance.Status), + ZoneID: instance.ZoneId, + VpcID: instance.VpcAttributes.VpcId, + OSType: instance.OSType, + OSName: instance.OSName, + ImageID: instance.ImageId, + Cpu: instance.Cpu, + Memory: instance.Memory / 1024, // 阿里云返回的是MB,转换为GB + } + + if len(instance.PublicIpAddress.IpAddress) > 0 { + resource.PublicIP = instance.PublicIpAddress.IpAddress[0] + } + + if len(instance.VpcAttributes.PrivateIpAddress.IpAddress) > 0 { + resource.PrivateIP = instance.VpcAttributes.PrivateIpAddress.IpAddress[0] + } + + switch instance.InstanceChargeType { + case "PostPaid": + resource.ChargeType = model.ChargeTypePostPaid + case "PrePaid": + resource.ChargeType = model.ChargeTypePrePaid + if instance.ExpiredTime != "" { + expireTime, err := time.Parse("2006-01-02T15:04Z", instance.ExpiredTime) + if err == nil { + resource.ExpireTime = &expireTime + } + } + } + + resource.Currency = model.CurrencyCNY + + var tags model.KeyValueList + for _, tag := range instance.Tags.Tag { + tags = append(tags, model.KeyValue{ + Key: tag.TagKey, + Value: tag.TagValue, + }) + } + resource.Tags = tags + + resource.Port = 22 + resource.Username = "root" + + return resource +} + +// convertECSStatus 转换阿里云ECS状态到内部状态 +func (c *AliyunClient) convertECSStatus(status string) model.CloudResourceStatus { + switch status { + case "Running": + return model.CloudResourceRunning + case "Stopped": + return model.CloudResourceStopped + case "Starting": + return model.CloudResourceStarting + case "Stopping": + return model.CloudResourceStopping + default: + return model.CloudResourceUnknown + } +} + +// GetECSInstanceByID 根据实例ID获取单个ECS实例 +func (c *AliyunClient) GetECSInstanceByID(ctx context.Context, instanceID string) (*model.TreeCloudResource, error) { + resources, err := c.ListECSInstances(ctx, []string{instanceID}) + if err != nil { + return nil, err + } + + if len(resources) == 0 { + return nil, fmt.Errorf("未找到实例: %s", instanceID) + } + + return resources[0], nil +} diff --git a/internal/tree/utils/cloud_account_util.go b/internal/tree/utils/cloud_account_util.go index dd6132e9..bd03831a 100644 --- a/internal/tree/utils/cloud_account_util.go +++ b/internal/tree/utils/cloud_account_util.go @@ -27,6 +27,7 @@ package utils import ( "context" + "fmt" "github.com/GoSimplicity/AI-CloudOps/internal/model" "go.uber.org/zap" @@ -34,66 +35,46 @@ import ( // VerifyAliyunCredentials 验证阿里云凭证 func VerifyAliyunCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { - // TODO: 实现阿里云凭证验证 - // 1. 使用阿里云SDK初始化客户端 - // 2. 调用DescribeRegions等基础API验证凭证有效性 - // 3. 返回验证结果 - logger.Info("验证阿里云凭证", - zap.String("region", req.Region)) + client, err := NewAliyunClient(req.AccessKey, req.SecretKey, req.Region, logger) + if err != nil { + logger.Error("创建阿里云客户端失败", zap.Error(err)) + return err + } + + if err := client.VerifyCredentials(ctx); err != nil { + logger.Error("验证阿里云凭证失败", zap.Error(err)) + return err + } + return nil } // VerifyTencentCredentials 验证腾讯云凭证 func VerifyTencentCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { - // TODO: 实现腾讯云凭证验证 - // 1. 使用腾讯云SDK初始化客户端 - // 2. 调用DescribeRegions等基础API验证凭证有效性 - // 3. 返回验证结果 - logger.Info("验证腾讯云凭证", - zap.String("region", req.Region)) - return nil + logger.Warn("腾讯云凭证验证功能暂未实现") + return fmt.Errorf("腾讯云凭证验证功能暂未实现") } // VerifyAWSCredentials 验证AWS凭证 func VerifyAWSCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { - // TODO: 实现AWS凭证验证 - // 1. 使用AWS SDK初始化客户端 - // 2. 调用DescribeRegions等基础API验证凭证有效性 - // 3. 返回验证结果 - logger.Info("验证AWS凭证", - zap.String("region", req.Region)) - return nil + logger.Warn("AWS凭证验证功能暂未实现") + return fmt.Errorf("AWS凭证验证功能暂未实现") } // VerifyHuaweiCredentials 验证华为云凭证 func VerifyHuaweiCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { - // TODO: 实现华为云凭证验证 - // 1. 使用华为云SDK初始化客户端 - // 2. 调用DescribeRegions等基础API验证凭证有效性 - // 3. 返回验证结果 - logger.Info("验证华为云凭证", - zap.String("region", req.Region)) - return nil + logger.Warn("华为云凭证验证功能暂未实现") + return fmt.Errorf("华为云凭证验证功能暂未实现") } // VerifyAzureCredentials 验证Azure凭证 func VerifyAzureCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { - // TODO: 实现Azure凭证验证 - // 1. 使用Azure SDK初始化客户端 - // 2. 调用相关API验证凭证有效性 - // 3. 返回验证结果 - logger.Info("验证Azure凭证", - zap.String("region", req.Region)) - return nil + logger.Warn("Azure凭证验证功能暂未实现") + return fmt.Errorf("Azure凭证验证功能暂未实现") } // VerifyGCPCredentials 验证GCP凭证 func VerifyGCPCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { - // TODO: 实现GCP凭证验证 - // 1. 使用GCP SDK初始化客户端 - // 2. 调用相关API验证凭证有效性 - // 3. 返回验证结果 - logger.Info("验证GCP凭证", - zap.String("region", req.Region)) - return nil + logger.Warn("GCP凭证验证功能暂未实现") + return fmt.Errorf("GCP凭证验证功能暂未实现") } diff --git a/internal/tree/utils/tree_cloud_util.go b/internal/tree/utils/tree_cloud_util.go index 61465416..79587f5e 100644 --- a/internal/tree/utils/tree_cloud_util.go +++ b/internal/tree/utils/tree_cloud_util.go @@ -27,85 +27,82 @@ package utils import ( "context" + "fmt" "github.com/GoSimplicity/AI-CloudOps/internal/model" "go.uber.org/zap" ) +// AliyunSyncConfig 阿里云同步配置 +type AliyunSyncConfig struct { + AccessKey string + SecretKey string + Region string + CloudAccountID int + ResourceType model.CloudResourceType + InstanceIDs []string + SyncMode model.SyncMode +} + // SyncAliyunResources 同步阿里云资源 -func SyncAliyunResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { - // TODO: 实现阿里云资源同步 - // 1. 使用阿里云SDK初始化客户端 - // 2. 根据资源类型调用对应的API获取资源列表 - // 3. 将资源信息转换为内部模型 - // 4. 根据同步模式(全量/增量)更新数据库 - logger.Info("同步阿里云资源", - zap.String("syncMode", string(req.SyncMode)), - zap.Int8("resourceType", int8(req.ResourceType))) - return nil +func SyncAliyunResources(ctx context.Context, config *AliyunSyncConfig, logger *zap.Logger) ([]*model.TreeCloudResource, error) { + // 创建阿里云客户端 + client, err := NewAliyunClient(config.AccessKey, config.SecretKey, config.Region, logger) + if err != nil { + logger.Error("创建阿里云客户端失败", zap.Error(err)) + return nil, err + } + + // 目前只支持ECS资源类型的同步 + if config.ResourceType != 0 && config.ResourceType != model.ResourceTypeECS { + return nil, fmt.Errorf("暂不支持该资源类型的同步: %d", config.ResourceType) + } + + // 获取ECS实例列表 + resources, err := client.ListECSInstances(ctx, config.InstanceIDs) + if err != nil { + logger.Error("获取阿里云ECS实例失败", zap.Error(err)) + return nil, err + } + + // 为每个资源设置云账户ID + for _, resource := range resources { + resource.CloudAccountID = config.CloudAccountID + } + + logger.Info("同步阿里云资源成功", + zap.String("syncMode", string(config.SyncMode)), + zap.Int("count", len(resources))) + + return resources, nil } // SyncTencentResources 同步腾讯云资源 func SyncTencentResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { - // TODO: 实现腾讯云资源同步 - // 1. 使用腾讯云SDK初始化客户端 - // 2. 根据资源类型调用对应的API获取资源列表 - // 3. 将资源信息转换为内部模型 - // 4. 根据同步模式(全量/增量)更新数据库 - logger.Info("同步腾讯云资源", - zap.String("syncMode", string(req.SyncMode)), - zap.Int8("resourceType", int8(req.ResourceType))) - return nil + logger.Warn("腾讯云资源同步功能暂未实现") + return fmt.Errorf("腾讯云资源同步功能暂未实现") } // SyncAWSResources 同步AWS资源 func SyncAWSResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { - // TODO: 实现AWS资源同步 - // 1. 使用AWS SDK初始化客户端 - // 2. 根据资源类型调用对应的API获取资源列表 - // 3. 将资源信息转换为内部模型 - // 4. 根据同步模式(全量/增量)更新数据库 - logger.Info("同步AWS资源", - zap.String("syncMode", string(req.SyncMode)), - zap.Int8("resourceType", int8(req.ResourceType))) - return nil + logger.Warn("AWS资源同步功能暂未实现") + return fmt.Errorf("AWS资源同步功能暂未实现") } // SyncHuaweiResources 同步华为云资源 func SyncHuaweiResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { - // TODO: 实现华为云资源同步 - // 1. 使用华为云SDK初始化客户端 - // 2. 根据资源类型调用对应的API获取资源列表 - // 3. 将资源信息转换为内部模型 - // 4. 根据同步模式(全量/增量)更新数据库 - logger.Info("同步华为云资源", - zap.String("syncMode", string(req.SyncMode)), - zap.Int8("resourceType", int8(req.ResourceType))) - return nil + logger.Warn("华为云资源同步功能暂未实现") + return fmt.Errorf("华为云资源同步功能暂未实现") } // SyncAzureResources 同步Azure资源 func SyncAzureResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { - // TODO: 实现Azure资源同步 - // 1. 使用Azure SDK初始化客户端 - // 2. 根据资源类型调用对应的API获取资源列表 - // 3. 将资源信息转换为内部模型 - // 4. 根据同步模式(全量/增量)更新数据库 - logger.Info("同步Azure资源", - zap.String("syncMode", string(req.SyncMode)), - zap.Int8("resourceType", int8(req.ResourceType))) - return nil + logger.Warn("Azure资源同步功能暂未实现") + return fmt.Errorf("Azure资源同步功能暂未实现") } // SyncGCPResources 同步GCP资源 func SyncGCPResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { - // TODO: 实现GCP资源同步 - // 1. 使用GCP SDK初始化客户端 - // 2. 根据资源类型调用对应的API获取资源列表 - // 3. 将资源信息转换为内部模型 - // 4. 根据同步模式(全量/增量)更新数据库 - logger.Info("同步GCP资源", - zap.String("syncMode", string(req.SyncMode)), - zap.Int8("resourceType", int8(req.ResourceType))) - return nil + logger.Warn("GCP资源同步功能暂未实现") + return fmt.Errorf("GCP资源同步功能暂未实现") } From 14c48ffe2838ac045a97ea5e0249c0ce56266cc3 Mon Sep 17 00:00:00 2001 From: Bamboo <13664854532@163.com> Date: Sat, 11 Oct 2025 21:04:03 +0800 Subject: [PATCH 5/8] feat: Add Region field to TreeCloudResource and update related service methods for cloud account integration --- go.sum | 1 + internal/model/tree_cloud.go | 1 + internal/tree/service/tree_cloud_service.go | 18 ++++++++++++++++++ internal/tree/utils/aliyun_util.go | 10 ++++++++++ 4 files changed, 30 insertions(+) diff --git a/go.sum b/go.sum index a4bdd25e..0de748f3 100644 --- a/go.sum +++ b/go.sum @@ -334,6 +334,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/internal/model/tree_cloud.go b/internal/model/tree_cloud.go index 61af21f0..4e4bc2c9 100644 --- a/internal/model/tree_cloud.go +++ b/internal/model/tree_cloud.go @@ -101,6 +101,7 @@ type TreeCloudResource struct { CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` CloudAccountID int `json:"cloud_account_id" gorm:"not null;comment:云账户ID"` CloudAccount *CloudAccount `json:"cloud_account,omitempty" gorm:"foreignKey:CloudAccountID"` + Region string `json:"region" gorm:"type:varchar(50);comment:区域,如cn-hangzhou"` InstanceID string `json:"instance_id" gorm:"type:varchar(100);comment:云资源实例ID"` InstanceType string `json:"instance_type" gorm:"type:varchar(100);comment:实例规格(如ecs.g6.large)"` Cpu int `json:"cpu" gorm:"comment:CPU核数;default:0"` diff --git a/internal/tree/service/tree_cloud_service.go b/internal/tree/service/tree_cloud_service.go index 7db41f13..f30e518c 100644 --- a/internal/tree/service/tree_cloud_service.go +++ b/internal/tree/service/tree_cloud_service.go @@ -155,6 +155,13 @@ func (s *treeCloudService) CreateTreeCloudResource(ctx context.Context, req *mod } } + // 获取云账户以获取Region信息 + account, err := s.cloudAccountDAO.GetByID(ctx, req.CloudAccountID) + if err != nil { + s.logger.Error("获取云账户失败", zap.Int("cloudAccountID", req.CloudAccountID), zap.Error(err)) + return fmt.Errorf("获取云账户失败: %w", err) + } + // 创建云资源对象 cloud := &model.TreeCloudResource{ Name: req.Name, @@ -166,6 +173,7 @@ func (s *treeCloudService) CreateTreeCloudResource(ctx context.Context, req *mod CreateUserID: req.CreateUserID, CreateUserName: req.CreateUserName, CloudAccountID: req.CloudAccountID, + Region: account.Region, InstanceID: req.InstanceID, InstanceType: req.InstanceType, Cpu: req.Cpu, @@ -250,6 +258,16 @@ func (s *treeCloudService) UpdateTreeCloudResource(ctx context.Context, req *mod AuthMode: req.AuthMode, } + // 如果CloudAccountID发生变化,需要更新Region + if req.CloudAccountID != 0 { + account, err := s.cloudAccountDAO.GetByID(ctx, req.CloudAccountID) + if err != nil { + s.logger.Error("获取云账户失败", zap.Int("cloudAccountID", req.CloudAccountID), zap.Error(err)) + return fmt.Errorf("获取云账户失败: %w", err) + } + cloud.Region = account.Region + } + // 直接更新 if err := s.dao.Update(ctx, cloud); err != nil { s.logger.Error("更新云资源失败", zap.Int("id", req.ID), zap.Error(err)) diff --git a/internal/tree/utils/aliyun_util.go b/internal/tree/utils/aliyun_util.go index 85514cde..72a8677f 100644 --- a/internal/tree/utils/aliyun_util.go +++ b/internal/tree/utils/aliyun_util.go @@ -40,6 +40,7 @@ import ( // AliyunClient 阿里云客户端封装 type AliyunClient struct { client *ecs.Client + region string logger *zap.Logger } @@ -52,6 +53,7 @@ func NewAliyunClient(accessKey, secretKey, region string, logger *zap.Logger) (* return &AliyunClient{ client: client, + region: region, logger: logger, }, nil } @@ -123,6 +125,7 @@ func (c *AliyunClient) convertECSToResource(instance *ecs.Instance) *model.TreeC InstanceID: instance.InstanceId, InstanceType: instance.InstanceType, Status: c.convertECSStatus(instance.Status), + Region: c.region, ZoneID: instance.ZoneId, VpcID: instance.VpcAttributes.VpcId, OSType: instance.OSType, @@ -140,6 +143,13 @@ func (c *AliyunClient) convertECSToResource(instance *ecs.Instance) *model.TreeC resource.PrivateIP = instance.VpcAttributes.PrivateIpAddress.IpAddress[0] } + // 设置镜像名称(如果OSName为空,使用ImageId) + if instance.OSName != "" { + resource.ImageName = instance.OSName + } else { + resource.ImageName = instance.ImageId + } + switch instance.InstanceChargeType { case "PostPaid": resource.ChargeType = model.ChargeTypePostPaid From e5a1101b35af8a988ceb267ae1cd519f53d69bf6 Mon Sep 17 00:00:00 2001 From: Bamboo <13664854532@163.com> Date: Mon, 13 Oct 2025 21:57:22 +0800 Subject: [PATCH 6/8] feat: Enhance cloud resource management with synchronization history and change log features --- internal/model/tree_cloud.go | 209 +++++--- internal/tree/api/tree_cloud_handler.go | 49 +- internal/tree/dao/tree_cloud_dao.go | 137 ++++- internal/tree/service/tree_cloud_service.go | 557 +++++++++++--------- internal/tree/utils/aliyun_util.go | 35 +- internal/tree/utils/tree_cloud_util.go | 8 + pkg/di/init.go | 2 + pkg/di/logger.go | 29 +- 8 files changed, 687 insertions(+), 339 deletions(-) diff --git a/internal/model/tree_cloud.go b/internal/model/tree_cloud.go index 4e4bc2c9..cb2320a6 100644 --- a/internal/model/tree_cloud.go +++ b/internal/model/tree_cloud.go @@ -39,6 +39,26 @@ const ( ProviderGCP // Google Cloud ) +// String 返回云厂商的字符串表示(用于日志和调试) +func (p CloudProvider) String() string { + switch p { + case ProviderAliyun: + return "阿里云" + case ProviderTencent: + return "腾讯云" + case ProviderAWS: + return "AWS" + case ProviderHuawei: + return "华为云" + case ProviderAzure: + return "Azure" + case ProviderGCP: + return "Google Cloud" + default: + return "未知" + } +} + // CloudResourceType 云资源类型 type CloudResourceType int8 @@ -51,6 +71,26 @@ const ( ResourceTypeOther // 其他资源 ) +// String 返回资源类型的字符串表示(用于日志和调试) +func (r CloudResourceType) String() string { + switch r { + case ResourceTypeECS: + return "云服务器" + case ResourceTypeRDS: + return "云数据库" + case ResourceTypeSLB: + return "负载均衡" + case ResourceTypeOSS: + return "对象存储" + case ResourceTypeVPC: + return "虚拟私有云" + case ResourceTypeOther: + return "其他" + default: + return "未知" + } +} + // CloudResourceStatus 云资源状态 type CloudResourceStatus int8 @@ -63,6 +103,26 @@ const ( CloudResourceUnknown // 未知状态 ) +// String 返回资源状态的字符串表示(用于日志和调试) +func (s CloudResourceStatus) String() string { + switch s { + case CloudResourceRunning: + return "运行中" + case CloudResourceStopped: + return "已停止" + case CloudResourceStarting: + return "启动中" + case CloudResourceStopping: + return "停止中" + case CloudResourceDeleted: + return "已删除" + case CloudResourceUnknown: + return "未知" + default: + return "未知" + } +} + // Currency 货币单位 type Currency string @@ -145,74 +205,44 @@ type GetTreeCloudResourceDetailReq struct { ID int `json:"id" form:"id" binding:"required,gt=0"` } -// CreateTreeCloudResourceReq 创建云资源请求(录入已有云资源) -type CreateTreeCloudResourceReq struct { - Name string `json:"name" binding:"required"` - ResourceType CloudResourceType `json:"resource_type" binding:"required,oneof=1 2 3 4 5 6"` - Environment string `json:"environment"` - Description string `json:"description"` - Tags KeyValueList `json:"tags"` - CreateUserID int `json:"create_user_id"` - CreateUserName string `json:"create_user_name"` - CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` - InstanceID string `json:"instance_id"` - InstanceType string `json:"instance_type"` - Cpu int `json:"cpu" binding:"omitempty,gte=0"` - Memory int `json:"memory" binding:"omitempty,gte=0"` - Disk int `json:"disk" binding:"omitempty,gte=0"` - PublicIP string `json:"public_ip" binding:"omitempty,ip"` - PrivateIP string `json:"private_ip" binding:"omitempty,ip"` - VpcID string `json:"vpc_id"` - ZoneID string `json:"zone_id"` - ChargeType ChargeType `json:"charge_type"` - ExpireTime *time.Time `json:"expire_time"` - MonthlyCost float64 `json:"monthly_cost" binding:"omitempty,gte=0"` - Currency string `json:"currency"` - OSType string `json:"os_type"` - OSName string `json:"os_name"` - ImageID string `json:"image_id"` - ImageName string `json:"image_name"` - Port int `json:"port" binding:"omitempty,gte=1,lte=65535"` - Username string `json:"username"` - Password string `json:"password"` - Key string `json:"key"` - AuthMode AuthMode `json:"auth_mode" binding:"omitempty,oneof=1 2"` -} - -// UpdateTreeCloudResourceReq 更新云资源请求 +// UpdateTreeCloudResourceReq 更新云资源本地元数据请求(不影响云上资源) type UpdateTreeCloudResourceReq struct { - ID int `json:"id" binding:"required,gt=0"` - Name string `json:"name"` - Environment string `json:"environment"` - Description string `json:"description"` - Tags KeyValueList `json:"tags"` - ResourceType CloudResourceType `json:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` - CloudAccountID int `json:"cloud_account_id" binding:"omitempty,gt=0"` - InstanceType string `json:"instance_type"` - PublicIP string `json:"public_ip" binding:"omitempty,ip"` - PrivateIP string `json:"private_ip" binding:"omitempty,ip"` - ChargeType ChargeType `json:"charge_type"` - ExpireTime *time.Time `json:"expire_time"` - MonthlyCost float64 `json:"monthly_cost" binding:"omitempty,gte=0"` - Currency string `json:"currency"` - Port int `json:"port" binding:"omitempty,gte=1,lte=65535"` - Username string `json:"username"` - Password string `json:"password"` - Key string `json:"key"` - AuthMode AuthMode `json:"auth_mode" binding:"omitempty,oneof=1 2"` + ID int `json:"id" binding:"required,gt=0"` + Environment string `json:"environment"` // 环境标识 + Description string `json:"description"` // 资源描述 + Tags KeyValueList `json:"tags"` // 自定义标签 + Port int `json:"port" binding:"omitempty,gte=1,lte=65535"` // SSH端口 + Username string `json:"username"` // SSH用户名 + Password string `json:"password"` // SSH密码 + Key string `json:"key"` // SSH密钥 + AuthMode AuthMode `json:"auth_mode" binding:"omitempty,oneof=1 2"` // SSH认证方式 } -// DeleteTreeCloudResourceReq 删除云资源请求 +// DeleteTreeCloudResourceReq 删除云资源请求(仅从平台删除,不影响云上资源) type DeleteTreeCloudResourceReq struct { ID int `json:"id" binding:"required,gt=0"` } // SyncTreeCloudResourceReq 从云厂商同步资源请求 type SyncTreeCloudResourceReq struct { - CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` - ResourceType CloudResourceType `json:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` // 同步的资源类型,为空则同步所有 - InstanceIDs []string `json:"instance_ids"` // 指定同步的实例ID列表,为空则同步所有 - SyncMode SyncMode `json:"sync_mode" binding:"omitempty,oneof=full incremental"` // 同步模式: full-全量, incremental-增量 + CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` + ResourceTypes []CloudResourceType `json:"resource_types" binding:"omitempty"` // 同步的资源类型列表,为空则同步所有 + Regions []string `json:"regions"` // 指定同步的区域列表,为空则同步账号配置的区域 + InstanceIDs []string `json:"instance_ids"` // 指定同步的实例ID列表,为空则同步所有 + SyncMode SyncMode `json:"sync_mode" binding:"omitempty,oneof=full incremental"` // 同步模式: full-全量, incremental-增量 + AutoBind bool `json:"auto_bind"` // 是否自动绑定到服务树节点 + BindNodeID int `json:"bind_node_id" binding:"omitempty,gt=0"` // 自动绑定的目标节点ID +} + +// SyncCloudResourceResp 同步云资源响应 +type SyncCloudResourceResp struct { + TotalCount int `json:"total_count"` // 总共同步的资源数量 + NewCount int `json:"new_count"` // 新增的资源数量 + UpdateCount int `json:"update_count"` // 更新的资源数量 + DeleteCount int `json:"delete_count"` // 删除的资源数量(全量同步时) + FailedCount int `json:"failed_count"` // 同步失败的数量 + FailedInstances []string `json:"failed_instances"` // 同步失败的实例ID列表 + SyncTime time.Time `json:"sync_time"` // 同步时间 } // VerifyCloudCredentialsReq 验证云厂商凭证请求 @@ -232,12 +262,6 @@ type GetTreeNodeCloudResourcesReq struct { Status CloudResourceStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2 3 4 5 6"` } -// BatchImportCloudResourceReq 批量导入云资源请求 -type BatchImportCloudResourceReq struct { - CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` - InstanceIDs []string `json:"instance_ids" binding:"required,min=1"` // 要导入的实例ID列表 -} - // BindTreeCloudResourceReq 绑定云资源到树节点请求 type BindTreeCloudResourceReq struct { ID int `json:"id" binding:"required,gt=0"` @@ -261,3 +285,58 @@ type UpdateCloudResourceStatusReq struct { ID int `json:"id" binding:"required,gt=0"` Status CloudResourceStatus `json:"status" binding:"required,oneof=1 2 3 4 5 6"` } + +// CloudResourceSyncHistory 云资源同步历史 +type CloudResourceSyncHistory struct { + Model + CloudAccountID int `json:"cloud_account_id" gorm:"not null;comment:云账户ID"` + SyncMode SyncMode `json:"sync_mode" gorm:"type:varchar(20);comment:同步模式"` + TotalCount int `json:"total_count" gorm:"comment:同步总数"` + NewCount int `json:"new_count" gorm:"comment:新增数量"` + UpdateCount int `json:"update_count" gorm:"comment:更新数量"` + DeleteCount int `json:"delete_count" gorm:"comment:删除数量"` + FailedCount int `json:"failed_count" gorm:"comment:失败数量"` + FailedInstances string `json:"failed_instances" gorm:"type:text;comment:失败的实例ID列表(JSON)"` + SyncStatus string `json:"sync_status" gorm:"type:varchar(20);comment:同步状态(success/failed/partial)"` + ErrorMessage string `json:"error_message" gorm:"type:text;comment:错误信息"` + StartTime time.Time `json:"start_time" gorm:"comment:开始时间"` + EndTime time.Time `json:"end_time" gorm:"comment:结束时间"` + Duration int `json:"duration" gorm:"comment:同步耗时(秒)"` +} + +func (c *CloudResourceSyncHistory) TableName() string { + return "cl_cloud_resource_sync_history" +} + +// GetCloudResourceSyncHistoryReq 获取同步历史请求 +type GetCloudResourceSyncHistoryReq struct { + ListReq + CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` + SyncStatus string `json:"sync_status" form:"sync_status"` +} + +// CloudResourceChangeLog 云资源变更日志 +type CloudResourceChangeLog struct { + Model + ResourceID int `json:"resource_id" gorm:"not null;comment:云资源ID"` + InstanceID string `json:"instance_id" gorm:"type:varchar(100);comment:实例ID"` + ChangeType string `json:"change_type" gorm:"type:varchar(20);comment:变更类型(created/updated/deleted/status_changed)"` + FieldName string `json:"field_name" gorm:"type:varchar(100);comment:变更字段名"` + OldValue string `json:"old_value" gorm:"type:text;comment:旧值"` + NewValue string `json:"new_value" gorm:"type:text;comment:新值"` + ChangeSource string `json:"change_source" gorm:"type:varchar(50);comment:变更来源(sync/manual)"` + OperatorID int `json:"operator_id" gorm:"comment:操作人ID"` + OperatorName string `json:"operator_name" gorm:"type:varchar(100);comment:操作人姓名"` + ChangeTime time.Time `json:"change_time" gorm:"comment:变更时间"` +} + +func (c *CloudResourceChangeLog) TableName() string { + return "cl_cloud_resource_change_log" +} + +// GetCloudResourceChangeLogReq 获取资源变更日志请求 +type GetCloudResourceChangeLogReq struct { + ListReq + ResourceID int `json:"resource_id" form:"resource_id" binding:"omitempty,gt=0"` + ChangeType string `json:"change_type" form:"change_type"` +} diff --git a/internal/tree/api/tree_cloud_handler.go b/internal/tree/api/tree_cloud_handler.go index aa932822..4625f054 100644 --- a/internal/tree/api/tree_cloud_handler.go +++ b/internal/tree/api/tree_cloud_handler.go @@ -50,16 +50,16 @@ func (h *TreeCloudHandler) RegisterRouters(server *gin.Engine) { { cloudGroup.GET("/list", h.GetTreeCloudResourceList) cloudGroup.GET("/:id/detail", h.GetTreeCloudResourceDetail) - cloudGroup.POST("/create", h.CreateTreeCloudResource) + cloudGroup.GET("/:id/node", h.GetTreeNodeCloudResources) + cloudGroup.GET("/:id/terminal", h.ConnectCloudResourceTerminal) + cloudGroup.POST("/sync", h.SyncTreeCloudResource) + cloudGroup.GET("/sync/history", h.GetSyncHistory) cloudGroup.PUT("/:id/update", h.UpdateTreeCloudResource) cloudGroup.DELETE("/:id/delete", h.DeleteTreeCloudResource) + cloudGroup.PUT("/:id/status", h.UpdateCloudResourceStatus) cloudGroup.POST("/:id/bind", h.BindTreeCloudResource) cloudGroup.POST("/:id/unbind", h.UnBindTreeCloudResource) - cloudGroup.POST("/sync", h.SyncTreeCloudResource) - cloudGroup.POST("/batch_import", h.BatchImportCloudResource) - cloudGroup.GET("/:id/node", h.GetTreeNodeCloudResources) - cloudGroup.GET("/:id/terminal", h.ConnectCloudResourceTerminal) - cloudGroup.PUT("/:id/status", h.UpdateCloudResourceStatus) + cloudGroup.GET("/changelog", h.GetChangeLog) } } @@ -89,21 +89,7 @@ func (h *TreeCloudHandler) GetTreeCloudResourceDetail(ctx *gin.Context) { }) } -// CreateTreeCloudResource 创建云资源 -func (h *TreeCloudHandler) CreateTreeCloudResource(ctx *gin.Context) { - var req model.CreateTreeCloudResourceReq - - user := ctx.MustGet("user").(utils.UserClaims) - - req.CreateUserID = user.Uid - req.CreateUserName = user.Username - - utils.HandleRequest(ctx, &req, func() (interface{}, error) { - return nil, h.service.CreateTreeCloudResource(ctx, &req) - }) -} - -// UpdateTreeCloudResource 更新云资源 +// UpdateTreeCloudResource 更新云资源本地元数据(不影响云上资源) func (h *TreeCloudHandler) UpdateTreeCloudResource(ctx *gin.Context) { var req model.UpdateTreeCloudResourceReq @@ -171,21 +157,30 @@ func (h *TreeCloudHandler) UnBindTreeCloudResource(ctx *gin.Context) { }) } -// SyncTreeCloudResource 从云厂商同步资源 +// SyncTreeCloudResource 从云厂商同步资源(核心功能) func (h *TreeCloudHandler) SyncTreeCloudResource(ctx *gin.Context) { var req model.SyncTreeCloudResourceReq utils.HandleRequest(ctx, &req, func() (interface{}, error) { - return nil, h.service.SyncTreeCloudResource(ctx, &req) + return h.service.SyncTreeCloudResource(ctx, &req) + }) +} + +// GetSyncHistory 获取云资源同步历史 +func (h *TreeCloudHandler) GetSyncHistory(ctx *gin.Context) { + var req model.GetCloudResourceSyncHistoryReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetSyncHistory(ctx, &req) }) } -// BatchImportCloudResource 批量导入云资源 -func (h *TreeCloudHandler) BatchImportCloudResource(ctx *gin.Context) { - var req model.BatchImportCloudResourceReq +// GetChangeLog 获取云资源变更日志 +func (h *TreeCloudHandler) GetChangeLog(ctx *gin.Context) { + var req model.GetCloudResourceChangeLogReq utils.HandleRequest(ctx, &req, func() (interface{}, error) { - return h.service.BatchImportCloudResource(ctx, &req) + return h.service.GetChangeLog(ctx, &req) }) } diff --git a/internal/tree/dao/tree_cloud_dao.go b/internal/tree/dao/tree_cloud_dao.go index 0d4d5fd9..33f884d2 100644 --- a/internal/tree/dao/tree_cloud_dao.go +++ b/internal/tree/dao/tree_cloud_dao.go @@ -36,18 +36,34 @@ import ( ) type TreeCloudDAO interface { + // 云资源基础操作 Create(ctx context.Context, cloud *model.TreeCloudResource) error Update(ctx context.Context, cloud *model.TreeCloudResource) error + UpdateMetadata(ctx context.Context, id int, metadata map[string]interface{}) error Delete(ctx context.Context, id int) error GetByID(ctx context.Context, id int) (*model.TreeCloudResource, error) GetList(ctx context.Context, req *model.GetTreeCloudResourceListReq) ([]*model.TreeCloudResource, int64, error) GetByAccountAndInstanceID(ctx context.Context, cloudAccountID int, instanceID string) (*model.TreeCloudResource, error) GetByNodeID(ctx context.Context, nodeID int, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) + + // 树节点绑定 BindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error UnBindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error + + // 批量操作 BatchGetByIDs(ctx context.Context, ids []int) ([]*model.TreeCloudResource, error) BatchCreate(ctx context.Context, clouds []*model.TreeCloudResource) error + + // 状态更新 UpdateStatus(ctx context.Context, id int, status model.CloudResourceStatus) error + + // 同步历史 + CreateSyncHistory(ctx context.Context, history *model.CloudResourceSyncHistory) error + GetSyncHistoryList(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) ([]*model.CloudResourceSyncHistory, int64, error) + + // 变更日志 + CreateChangeLog(ctx context.Context, log *model.CloudResourceChangeLog) error + GetChangeLogList(ctx context.Context, req *model.GetCloudResourceChangeLogReq) ([]*model.CloudResourceChangeLog, int64, error) } type treeCloudDAO struct { @@ -72,9 +88,11 @@ func (d *treeCloudDAO) Create(ctx context.Context, cloud *model.TreeCloudResourc return nil } -// Update 更新云资源 +// Update 更新云资源(用于同步场景,更新所有字段) func (d *treeCloudDAO) Update(ctx context.Context, cloud *model.TreeCloudResource) error { - if err := d.db.WithContext(ctx).Model(cloud).Updates(cloud).Error; err != nil { + // 使用Save方法确保所有字段都会被更新,包括零值字段 + // 这对于同步场景很重要,因为云资源的某些字段可能变为零值 + if err := d.db.WithContext(ctx).Save(cloud).Error; err != nil { d.logger.Error("更新云资源失败", zap.Error(err)) return err } @@ -82,6 +100,19 @@ func (d *treeCloudDAO) Update(ctx context.Context, cloud *model.TreeCloudResourc return nil } +// UpdateMetadata 更新云资源的本地元数据(只更新指定字段) +func (d *treeCloudDAO) UpdateMetadata(ctx context.Context, id int, metadata map[string]interface{}) error { + if err := d.db.WithContext(ctx). + Model(&model.TreeCloudResource{}). + Where("id = ?", id). + Updates(metadata).Error; err != nil { + d.logger.Error("更新云资源元数据失败", zap.Error(err), zap.Int("id", id)) + return err + } + + return nil +} + // Delete 删除云资源 func (d *treeCloudDAO) Delete(ctx context.Context, id int) error { if err := d.db.WithContext(ctx).Delete(&model.TreeCloudResource{}, id).Error; err != nil { @@ -310,3 +341,105 @@ func (d *treeCloudDAO) UnBindTreeNodes(ctx context.Context, cloudID int, treeNod return nil } + +// CreateSyncHistory 创建同步历史记录 +func (d *treeCloudDAO) CreateSyncHistory(ctx context.Context, history *model.CloudResourceSyncHistory) error { + if err := d.db.WithContext(ctx).Create(history).Error; err != nil { + d.logger.Error("创建同步历史失败", zap.Error(err)) + return err + } + + d.logger.Info("创建同步历史成功", + zap.Int("cloudAccountID", history.CloudAccountID), + zap.String("syncStatus", history.SyncStatus)) + return nil +} + +// GetSyncHistoryList 获取同步历史列表 +func (d *treeCloudDAO) GetSyncHistoryList(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) ([]*model.CloudResourceSyncHistory, int64, error) { + var histories []*model.CloudResourceSyncHistory + var total int64 + + query := d.db.WithContext(ctx).Model(&model.CloudResourceSyncHistory{}) + + // 添加查询条件 + if req.CloudAccountID != 0 { + query = query.Where("cloud_account_id = ?", req.CloudAccountID) + } + + if req.SyncStatus != "" { + query = query.Where("sync_status = ?", req.SyncStatus) + } + + if req.Search != "" { + query = query.Where("error_message LIKE ?", "%"+req.Search+"%") + } + + // 计算总数 + if err := query.Count(&total).Error; err != nil { + d.logger.Error("获取同步历史总数失败", zap.Error(err)) + return nil, 0, err + } + + // 分页查询 + offset := (req.Page - 1) * req.Size + if err := query.Order("created_at DESC"). + Limit(req.Size). + Offset(offset). + Find(&histories).Error; err != nil { + d.logger.Error("获取同步历史列表失败", zap.Error(err)) + return nil, 0, err + } + + return histories, total, nil +} + +// CreateChangeLog 创建变更日志 +func (d *treeCloudDAO) CreateChangeLog(ctx context.Context, log *model.CloudResourceChangeLog) error { + if err := d.db.WithContext(ctx).Create(log).Error; err != nil { + d.logger.Error("创建变更日志失败", zap.Error(err)) + return err + } + + return nil +} + +// GetChangeLogList 获取变更日志列表 +func (d *treeCloudDAO) GetChangeLogList(ctx context.Context, req *model.GetCloudResourceChangeLogReq) ([]*model.CloudResourceChangeLog, int64, error) { + var logs []*model.CloudResourceChangeLog + var total int64 + + query := d.db.WithContext(ctx).Model(&model.CloudResourceChangeLog{}) + + // 添加查询条件 + if req.ResourceID != 0 { + query = query.Where("resource_id = ?", req.ResourceID) + } + + if req.ChangeType != "" { + query = query.Where("change_type = ?", req.ChangeType) + } + + if req.Search != "" { + query = query.Where("instance_id LIKE ? OR field_name LIKE ?", + "%"+req.Search+"%", "%"+req.Search+"%") + } + + // 计算总数 + if err := query.Count(&total).Error; err != nil { + d.logger.Error("获取变更日志总数失败", zap.Error(err)) + return nil, 0, err + } + + // 分页查询 + offset := (req.Page - 1) * req.Size + if err := query.Order("change_time DESC"). + Limit(req.Size). + Offset(offset). + Find(&logs).Error; err != nil { + d.logger.Error("获取变更日志列表失败", zap.Error(err)) + return nil, 0, err + } + + return logs, total, nil +} diff --git a/internal/tree/service/tree_cloud_service.go b/internal/tree/service/tree_cloud_service.go index f30e518c..054e6cf9 100644 --- a/internal/tree/service/tree_cloud_service.go +++ b/internal/tree/service/tree_cloud_service.go @@ -27,8 +27,10 @@ package service import ( "context" + "encoding/json" "errors" "fmt" + "time" "github.com/GoSimplicity/AI-CloudOps/internal/model" "github.com/GoSimplicity/AI-CloudOps/internal/tree/dao" @@ -38,18 +40,27 @@ import ( ) type TreeCloudService interface { + // 查询相关 GetTreeCloudResourceList(ctx context.Context, req *model.GetTreeCloudResourceListReq) (model.ListResp[*model.TreeCloudResource], error) GetTreeCloudResourceDetail(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) GetTreeCloudResourceForConnection(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) - CreateTreeCloudResource(ctx context.Context, req *model.CreateTreeCloudResourceReq) error + GetTreeNodeCloudResources(ctx context.Context, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) + + // 同步相关(核心功能) + SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) (*model.SyncCloudResourceResp, error) + GetSyncHistory(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) (model.ListResp[*model.CloudResourceSyncHistory], error) + + // 本地管理相关 UpdateTreeCloudResource(ctx context.Context, req *model.UpdateTreeCloudResourceReq) error DeleteTreeCloudResource(ctx context.Context, req *model.DeleteTreeCloudResourceReq) error + UpdateCloudResourceStatus(ctx context.Context, req *model.UpdateCloudResourceStatusReq) error + + // 服务树绑定相关 BindTreeCloudResource(ctx context.Context, req *model.BindTreeCloudResourceReq) error UnBindTreeCloudResource(ctx context.Context, req *model.UnBindTreeCloudResourceReq) error - GetTreeNodeCloudResources(ctx context.Context, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) - UpdateCloudResourceStatus(ctx context.Context, req *model.UpdateCloudResourceStatusReq) error - SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) error - BatchImportCloudResource(ctx context.Context, req *model.BatchImportCloudResourceReq) ([]int, error) + + // 变更日志相关 + GetChangeLog(ctx context.Context, req *model.GetCloudResourceChangeLogReq) (model.ListResp[*model.CloudResourceChangeLog], error) } type treeCloudService struct { @@ -129,93 +140,13 @@ func (s *treeCloudService) GetTreeCloudResourceForConnection(ctx context.Context return cloud, nil } -// CreateTreeCloudResource 创建云资源 -func (s *treeCloudService) CreateTreeCloudResource(ctx context.Context, req *model.CreateTreeCloudResourceReq) error { - // 检查实例ID是否已存在(如果提供了实例ID) - if req.InstanceID != "" { - existing, err := s.dao.GetByAccountAndInstanceID(ctx, req.CloudAccountID, req.InstanceID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.logger.Error("检查实例ID是否存在失败", zap.Error(err)) - return fmt.Errorf("检查实例ID失败: %w", err) - } - if existing != nil { - return fmt.Errorf("云账户 %d 下的实例 %s 已存在", req.CloudAccountID, req.InstanceID) - } - } - - // 加密SSH密码 - var encryptedPassword string - var err error - - if req.Password != "" { - encryptedPassword, err = treeUtils.EncryptPassword(req.Password) - if err != nil { - s.logger.Error("密码加密失败", zap.Error(err)) - return fmt.Errorf("密码加密失败: %w", err) - } - } - - // 获取云账户以获取Region信息 - account, err := s.cloudAccountDAO.GetByID(ctx, req.CloudAccountID) - if err != nil { - s.logger.Error("获取云账户失败", zap.Int("cloudAccountID", req.CloudAccountID), zap.Error(err)) - return fmt.Errorf("获取云账户失败: %w", err) - } - - // 创建云资源对象 - cloud := &model.TreeCloudResource{ - Name: req.Name, - ResourceType: req.ResourceType, - Status: model.CloudResourceRunning, - Environment: req.Environment, - Description: req.Description, - Tags: req.Tags, - CreateUserID: req.CreateUserID, - CreateUserName: req.CreateUserName, - CloudAccountID: req.CloudAccountID, - Region: account.Region, - InstanceID: req.InstanceID, - InstanceType: req.InstanceType, - Cpu: req.Cpu, - Memory: req.Memory, - Disk: req.Disk, - PublicIP: req.PublicIP, - PrivateIP: req.PrivateIP, - VpcID: req.VpcID, - ZoneID: req.ZoneID, - ChargeType: req.ChargeType, - ExpireTime: req.ExpireTime, - MonthlyCost: req.MonthlyCost, - Currency: model.Currency(req.Currency), - OSType: req.OSType, - OSName: req.OSName, - ImageID: req.ImageID, - ImageName: req.ImageName, - Port: req.Port, - Username: req.Username, - Password: encryptedPassword, - Key: req.Key, - AuthMode: req.AuthMode, - } - - // 设置默认值 - treeUtils.SetSSHDefaults(&cloud.Port, &cloud.Username) - - if err := s.dao.Create(ctx, cloud); err != nil { - s.logger.Error("创建云资源失败", zap.Error(err)) - return err - } - - return nil -} - -// UpdateTreeCloudResource 更新云资源 +// UpdateTreeCloudResource 更新云资源本地元数据(不影响云上资源) func (s *treeCloudService) UpdateTreeCloudResource(ctx context.Context, req *model.UpdateTreeCloudResourceReq) error { if err := treeUtils.ValidateID(req.ID); err != nil { return fmt.Errorf("无效的云资源ID: %w", err) } - // 检查是否存在 + // 检查资源是否存在 _, err := s.dao.GetByID(ctx, req.ID) switch { case errors.Is(err, gorm.ErrRecordNotFound): @@ -225,72 +156,115 @@ func (s *treeCloudService) UpdateTreeCloudResource(ctx context.Context, req *mod return err } - // 加密SSH密码 + // 构建要更新的字段map + metadata := make(map[string]interface{}) + + // 只添加非空字段 + if req.Environment != "" { + metadata["environment"] = req.Environment + } + if req.Description != "" { + metadata["description"] = req.Description + } + if req.Tags != nil { + metadata["tags"] = req.Tags + } + if req.Port > 0 { + metadata["port"] = req.Port + } + if req.Username != "" { + metadata["username"] = req.Username + } if req.Password != "" { + // 加密SSH密码 encrypted, err := treeUtils.EncryptPassword(req.Password) if err != nil { s.logger.Error("密码加密失败", zap.Error(err)) return fmt.Errorf("密码加密失败: %w", err) } - req.Password = encrypted + metadata["password"] = encrypted + } + if req.Key != "" { + metadata["key"] = req.Key + } + if req.AuthMode > 0 { + metadata["auth_mode"] = req.AuthMode } - // 构建更新对象 - cloud := &model.TreeCloudResource{ - Model: model.Model{ID: req.ID}, - Name: req.Name, - Environment: req.Environment, - Description: req.Description, - Tags: req.Tags, - ResourceType: req.ResourceType, - CloudAccountID: req.CloudAccountID, - InstanceType: req.InstanceType, - PublicIP: req.PublicIP, - PrivateIP: req.PrivateIP, - ChargeType: req.ChargeType, - ExpireTime: req.ExpireTime, - MonthlyCost: req.MonthlyCost, - Currency: model.Currency(req.Currency), - Port: req.Port, - Username: req.Username, - Password: req.Password, - Key: req.Key, - AuthMode: req.AuthMode, - } - - // 如果CloudAccountID发生变化,需要更新Region - if req.CloudAccountID != 0 { - account, err := s.cloudAccountDAO.GetByID(ctx, req.CloudAccountID) - if err != nil { - s.logger.Error("获取云账户失败", zap.Int("cloudAccountID", req.CloudAccountID), zap.Error(err)) - return fmt.Errorf("获取云账户失败: %w", err) - } - cloud.Region = account.Region + // 如果没有字段需要更新 + if len(metadata) == 0 { + s.logger.Info("没有字段需要更新", zap.Int("id", req.ID)) + return nil } - // 直接更新 - if err := s.dao.Update(ctx, cloud); err != nil { - s.logger.Error("更新云资源失败", zap.Int("id", req.ID), zap.Error(err)) + // 更新元数据 + if err := s.dao.UpdateMetadata(ctx, req.ID, metadata); err != nil { + s.logger.Error("更新云资源元数据失败", zap.Int("id", req.ID), zap.Error(err)) return err } + // 记录变更日志(记录每个字段的变更) + // 获取资源实例ID用于日志 + resource, _ := s.dao.GetByID(ctx, req.ID) + instanceID := "" + if resource != nil { + instanceID = resource.InstanceID + } + + // 为每个更新的字段创建变更日志 + for fieldName, newValue := range metadata { + changeLog := &model.CloudResourceChangeLog{ + ResourceID: req.ID, + InstanceID: instanceID, + ChangeType: "updated", + FieldName: fieldName, + OldValue: "", // 简化处理,不记录旧值 + NewValue: fmt.Sprintf("%v", newValue), + ChangeSource: "manual", + OperatorID: 0, + OperatorName: "", + ChangeTime: time.Now(), + } + // 异步记录,不影响主流程 + go func(log *model.CloudResourceChangeLog) { + if err := s.dao.CreateChangeLog(context.Background(), log); err != nil { + s.logger.Error("记录变更日志失败", zap.Error(err)) + } + }(changeLog) + } + + s.logger.Info("更新云资源元数据成功", zap.Int("id", req.ID), zap.Int("fields", len(metadata))) return nil } -// DeleteTreeCloudResource 删除云资源 +// DeleteTreeCloudResource 删除云资源(仅从平台删除,不影响云上资源) func (s *treeCloudService) DeleteTreeCloudResource(ctx context.Context, req *model.DeleteTreeCloudResourceReq) error { if err := treeUtils.ValidateID(req.ID); err != nil { return fmt.Errorf("无效的云资源ID: %w", err) } - if err := s.dao.Delete(ctx, req.ID); err != nil { + // 获取资源信息用于日志记录 + cloud, err := s.dao.GetByID(ctx, req.ID) + if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("云资源不存在") } + s.logger.Error("获取云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + // 记录删除日志 + s.recordChangeLog(ctx, cloud, nil, "manual", 0, "") + + if err := s.dao.Delete(ctx, req.ID); err != nil { s.logger.Error("删除云资源失败", zap.Int("id", req.ID), zap.Error(err)) return err } + s.logger.Info("从平台删除云资源成功", + zap.Int("id", req.ID), + zap.String("instanceID", cloud.InstanceID), + zap.String("name", cloud.Name)) return nil } @@ -361,70 +335,139 @@ func (s *treeCloudService) UpdateCloudResourceStatus(ctx context.Context, req *m return nil } -// SyncTreeCloudResource 从云厂商同步资源 -func (s *treeCloudService) SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) error { +// SyncTreeCloudResource 从云厂商同步资源(核心功能) +func (s *treeCloudService) SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) (*model.SyncCloudResourceResp, error) { + startTime := time.Now() + + // 设置默认的同步模式 + if req.SyncMode == "" { + req.SyncMode = model.SyncModeIncremental + } + + // 初始化同步响应 + resp := &model.SyncCloudResourceResp{ + SyncTime: startTime, + FailedInstances: []string{}, + } + + // 创建同步历史记录 + syncHistory := &model.CloudResourceSyncHistory{ + CloudAccountID: req.CloudAccountID, + SyncMode: req.SyncMode, + StartTime: startTime, + SyncStatus: "running", + } + // 获取云账户信息 account, err := s.cloudAccountDAO.GetByID(ctx, req.CloudAccountID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("云账户不存在") + return nil, errors.New("云账户不存在") } s.logger.Error("获取云账户失败", zap.Int("cloudAccountID", req.CloudAccountID), zap.Error(err)) - return err + return nil, err } // 检查云账户状态 if account.Status != model.CloudAccountEnabled { - return errors.New("云账户已禁用,无法同步资源") + return nil, errors.New("云账户已禁用,无法同步资源") } // 解密密钥 accessKey, err := treeUtils.DecryptPassword(account.AccessKey) if err != nil { s.logger.Error("解密AccessKey失败", zap.Error(err)) - return fmt.Errorf("解密AccessKey失败: %w", err) + syncHistory.SyncStatus = "failed" + syncHistory.ErrorMessage = fmt.Sprintf("解密AccessKey失败: %v", err) + s.saveSyncHistory(ctx, syncHistory) + return nil, fmt.Errorf("解密AccessKey失败: %w", err) } secretKey, err := treeUtils.DecryptPassword(account.SecretKey) if err != nil { s.logger.Error("解密SecretKey失败", zap.Error(err)) - return fmt.Errorf("解密SecretKey失败: %w", err) + syncHistory.SyncStatus = "failed" + syncHistory.ErrorMessage = fmt.Sprintf("解密SecretKey失败: %v", err) + s.saveSyncHistory(ctx, syncHistory) + return nil, fmt.Errorf("解密SecretKey失败: %w", err) } - s.logger.Info("同步云资源", + s.logger.Info("开始同步云资源", zap.Int("cloudAccountID", req.CloudAccountID), zap.Int8("provider", int8(account.Provider)), zap.String("region", account.Region), zap.String("syncMode", string(req.SyncMode))) // 根据不同的云厂商调用对应的同步逻辑 + var syncErr error switch account.Provider { case model.ProviderAliyun: - return s.syncAliyunResources(ctx, account, accessKey, secretKey, req) + syncErr = s.syncAliyunResourcesWithStats(ctx, account, accessKey, secretKey, req, resp) case model.ProviderTencent: - return errors.New("腾讯云资源同步功能暂未实现") + syncErr = errors.New("腾讯云资源同步功能暂未实现") case model.ProviderAWS: - return errors.New("AWS资源同步功能暂未实现") + syncErr = errors.New("AWS资源同步功能暂未实现") case model.ProviderHuawei: - return errors.New("华为云资源同步功能暂未实现") + syncErr = errors.New("华为云资源同步功能暂未实现") case model.ProviderAzure: - return errors.New("Azure资源同步功能暂未实现") + syncErr = errors.New("Azure资源同步功能暂未实现") case model.ProviderGCP: - return errors.New("GCP资源同步功能暂未实现") + syncErr = errors.New("GCP资源同步功能暂未实现") default: - return fmt.Errorf("不支持的云厂商: %d", account.Provider) + syncErr = fmt.Errorf("不支持的云厂商: %d", account.Provider) + } + + // 更新同步历史记录 + endTime := time.Now() + syncHistory.EndTime = endTime + syncHistory.Duration = int(endTime.Sub(startTime).Seconds()) + syncHistory.TotalCount = resp.TotalCount + syncHistory.NewCount = resp.NewCount + syncHistory.UpdateCount = resp.UpdateCount + syncHistory.DeleteCount = resp.DeleteCount + syncHistory.FailedCount = resp.FailedCount + + if len(resp.FailedInstances) > 0 { + // 将失败的实例ID列表转为JSON字符串 + failedJSON, _ := json.Marshal(resp.FailedInstances) + syncHistory.FailedInstances = string(failedJSON) + } + + if syncErr != nil { + syncHistory.SyncStatus = "failed" + syncHistory.ErrorMessage = syncErr.Error() + s.saveSyncHistory(ctx, syncHistory) + return resp, syncErr + } + + if resp.FailedCount > 0 { + syncHistory.SyncStatus = "partial" + } else { + syncHistory.SyncStatus = "success" } + + s.saveSyncHistory(ctx, syncHistory) + + s.logger.Info("云资源同步完成", + zap.Int("total", resp.TotalCount), + zap.Int("new", resp.NewCount), + zap.Int("update", resp.UpdateCount), + zap.Int("delete", resp.DeleteCount), + zap.Int("failed", resp.FailedCount), + zap.Duration("duration", endTime.Sub(startTime))) + + return resp, nil } -// syncAliyunResources 同步阿里云资源 -func (s *treeCloudService) syncAliyunResources(ctx context.Context, account *model.CloudAccount, accessKey, secretKey string, req *model.SyncTreeCloudResourceReq) error { +// syncAliyunResourcesWithStats 同步阿里云资源并返回统计信息 +func (s *treeCloudService) syncAliyunResourcesWithStats(ctx context.Context, account *model.CloudAccount, accessKey, secretKey string, req *model.SyncTreeCloudResourceReq, resp *model.SyncCloudResourceResp) error { // 构建同步配置 config := &treeUtils.AliyunSyncConfig{ AccessKey: accessKey, SecretKey: secretKey, Region: account.Region, CloudAccountID: account.ID, - ResourceType: req.ResourceType, + ResourceType: 0, // 暂时只同步ECS InstanceIDs: req.InstanceIDs, SyncMode: req.SyncMode, } @@ -438,15 +481,15 @@ func (s *treeCloudService) syncAliyunResources(ctx context.Context, account *mod // 根据同步模式处理资源 if req.SyncMode == model.SyncModeFull { // 全量同步:先删除该云账户下的所有ECS资源,再重新创建 - return s.fullSyncResources(ctx, account.ID, resources) + return s.fullSyncResources(ctx, account.ID, resources, resp, req.AutoBind, req.BindNodeID) } // 增量同步:更新已存在的资源,创建不存在的资源 - return s.incrementalSyncResources(ctx, account.ID, resources) + return s.incrementalSyncResources(ctx, account.ID, resources, resp, req.AutoBind, req.BindNodeID) } // fullSyncResources 全量同步资源 -func (s *treeCloudService) fullSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource) error { +func (s *treeCloudService) fullSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource, resp *model.SyncCloudResourceResp, autoBind bool, bindNodeID int) error { // 获取该云账户下的所有ECS资源 req := &model.GetTreeCloudResourceListReq{ ListReq: model.ListReq{ @@ -472,21 +515,31 @@ func (s *treeCloudService) fullSyncResources(ctx context.Context, cloudAccountID if !newInstanceIDSet[existingResource.InstanceID] { if err := s.dao.Delete(ctx, existingResource.ID); err != nil { s.logger.Error("删除资源失败", zap.Int("id", existingResource.ID), zap.Error(err)) + resp.FailedCount++ + resp.FailedInstances = append(resp.FailedInstances, existingResource.InstanceID) + } else { + resp.DeleteCount++ + // 记录删除日志 + s.recordChangeLog(ctx, existingResource, nil, "sync", 0, "") } } } // 更新或创建资源 - return s.incrementalSyncResources(ctx, cloudAccountID, resources) + return s.incrementalSyncResources(ctx, cloudAccountID, resources, resp, autoBind, bindNodeID) } // incrementalSyncResources 增量同步资源 -func (s *treeCloudService) incrementalSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource) error { +func (s *treeCloudService) incrementalSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource, resp *model.SyncCloudResourceResp, autoBind bool, bindNodeID int) error { for _, resource := range resources { + resp.TotalCount++ + // 检查资源是否已存在 existing, err := s.dao.GetByAccountAndInstanceID(ctx, cloudAccountID, resource.InstanceID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { s.logger.Error("查询资源失败", zap.String("instanceID", resource.InstanceID), zap.Error(err)) + resp.FailedCount++ + resp.FailedInstances = append(resp.FailedInstances, resource.InstanceID) continue } @@ -495,11 +548,33 @@ func (s *treeCloudService) incrementalSyncResources(ctx context.Context, cloudAc resource.ID = existing.ID if err := s.dao.Update(ctx, resource); err != nil { s.logger.Error("更新资源失败", zap.Int("id", existing.ID), zap.Error(err)) + resp.FailedCount++ + resp.FailedInstances = append(resp.FailedInstances, resource.InstanceID) + } else { + resp.UpdateCount++ + // 记录更新日志 + s.recordChangeLog(ctx, existing, resource, "sync", 0, "") } } else { // 创建新资源 if err := s.dao.Create(ctx, resource); err != nil { s.logger.Error("创建资源失败", zap.String("instanceID", resource.InstanceID), zap.Error(err)) + resp.FailedCount++ + resp.FailedInstances = append(resp.FailedInstances, resource.InstanceID) + } else { + resp.NewCount++ + // 记录创建日志 + s.recordChangeLog(ctx, nil, resource, "sync", 0, "") + + // 如果启用自动绑定,则绑定到指定节点 + if autoBind && bindNodeID > 0 { + if err := s.dao.BindTreeNodes(ctx, resource.ID, []int{bindNodeID}); err != nil { + s.logger.Error("自动绑定资源到节点失败", + zap.Int("resourceID", resource.ID), + zap.Int("nodeID", bindNodeID), + zap.Error(err)) + } + } } } } @@ -507,112 +582,112 @@ func (s *treeCloudService) incrementalSyncResources(ctx context.Context, cloudAc return nil } -// BatchImportCloudResource 批量导入云资源 -func (s *treeCloudService) BatchImportCloudResource(ctx context.Context, req *model.BatchImportCloudResourceReq) ([]int, error) { - if len(req.InstanceIDs) == 0 { - return nil, errors.New("实例ID列表不能为空") - } - - // 获取云账户信息 - account, err := s.cloudAccountDAO.GetByID(ctx, req.CloudAccountID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("云账户不存在") +// recordChangeLog 记录资源变更日志 +func (s *treeCloudService) recordChangeLog(ctx context.Context, oldResource, newResource *model.TreeCloudResource, source string, operatorID int, operatorName string) { + // 如果是删除操作 + if oldResource != nil && newResource == nil { + changeLog := &model.CloudResourceChangeLog{ + ResourceID: oldResource.ID, + InstanceID: oldResource.InstanceID, + ChangeType: "deleted", + FieldName: "", + OldValue: oldResource.Name, + NewValue: "", + ChangeSource: source, + OperatorID: operatorID, + OperatorName: operatorName, + ChangeTime: time.Now(), } - s.logger.Error("获取云账户失败", zap.Int("cloudAccountID", req.CloudAccountID), zap.Error(err)) - return nil, err + // 保存变更日志 + if err := s.dao.CreateChangeLog(ctx, changeLog); err != nil { + s.logger.Error("保存删除日志失败", zap.Error(err)) + } + return + } + + // 如果是创建操作 + if oldResource == nil && newResource != nil { + changeLog := &model.CloudResourceChangeLog{ + ResourceID: newResource.ID, + InstanceID: newResource.InstanceID, + ChangeType: "created", + FieldName: "", + OldValue: "", + NewValue: newResource.Name, + ChangeSource: source, + OperatorID: operatorID, + OperatorName: operatorName, + ChangeTime: time.Now(), + } + // 保存变更日志 + if err := s.dao.CreateChangeLog(ctx, changeLog); err != nil { + s.logger.Error("保存创建日志失败", zap.Error(err)) + } + return + } + + // 如果是更新操作,比较字段变化 + if oldResource != nil && newResource != nil { + // 比较状态 + if oldResource.Status != newResource.Status { + changeLog := &model.CloudResourceChangeLog{ + ResourceID: newResource.ID, + InstanceID: newResource.InstanceID, + ChangeType: "status_changed", + FieldName: "status", + OldValue: fmt.Sprintf("%d", oldResource.Status), + NewValue: fmt.Sprintf("%d", newResource.Status), + ChangeSource: source, + OperatorID: operatorID, + OperatorName: operatorName, + ChangeTime: time.Now(), + } + // 保存变更日志 + if err := s.dao.CreateChangeLog(ctx, changeLog); err != nil { + s.logger.Error("保存状态变更日志失败", zap.Error(err)) + } + } + // 可以继续比较其他字段... } +} - // 检查云账户状态 - if account.Status != model.CloudAccountEnabled { - return nil, errors.New("云账户已禁用,无法导入资源") +// saveSyncHistory 保存同步历史 +func (s *treeCloudService) saveSyncHistory(ctx context.Context, history *model.CloudResourceSyncHistory) { + if err := s.dao.CreateSyncHistory(ctx, history); err != nil { + s.logger.Error("保存同步历史失败", zap.Error(err)) } +} - // 解密密钥 - accessKey, err := treeUtils.DecryptPassword(account.AccessKey) - if err != nil { - s.logger.Error("解密AccessKey失败", zap.Error(err)) - return nil, fmt.Errorf("解密AccessKey失败: %w", err) - } +// GetSyncHistory 获取同步历史 +func (s *treeCloudService) GetSyncHistory(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) (model.ListResp[*model.CloudResourceSyncHistory], error) { + // 兜底分页参数 + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) - secretKey, err := treeUtils.DecryptPassword(account.SecretKey) + histories, total, err := s.dao.GetSyncHistoryList(ctx, req) if err != nil { - s.logger.Error("解密SecretKey失败", zap.Error(err)) - return nil, fmt.Errorf("解密SecretKey失败: %w", err) + s.logger.Error("获取同步历史失败", zap.Error(err)) + return model.ListResp[*model.CloudResourceSyncHistory]{}, err } - s.logger.Info("批量导入云资源", - zap.Int("cloudAccountID", req.CloudAccountID), - zap.Int8("provider", int8(account.Provider)), - zap.String("region", account.Region), - zap.Int("count", len(req.InstanceIDs))) - - // 根据云厂商调用对应的导入逻辑 - switch account.Provider { - case model.ProviderAliyun: - return s.batchImportAliyunResources(ctx, account, accessKey, secretKey, req) - case model.ProviderTencent: - return nil, errors.New("腾讯云批量导入功能暂未实现") - case model.ProviderAWS: - return nil, errors.New("AWS批量导入功能暂未实现") - case model.ProviderHuawei: - return nil, errors.New("华为云批量导入功能暂未实现") - case model.ProviderAzure: - return nil, errors.New("Azure批量导入功能暂未实现") - case model.ProviderGCP: - return nil, errors.New("GCP批量导入功能暂未实现") - default: - return nil, fmt.Errorf("不支持的云厂商: %d", account.Provider) - } + return model.ListResp[*model.CloudResourceSyncHistory]{ + Items: histories, + Total: total, + }, nil } -// batchImportAliyunResources 批量导入阿里云资源 -func (s *treeCloudService) batchImportAliyunResources(ctx context.Context, account *model.CloudAccount, accessKey, secretKey string, req *model.BatchImportCloudResourceReq) ([]int, error) { - // 构建同步配置,指定要导入的实例ID - config := &treeUtils.AliyunSyncConfig{ - AccessKey: accessKey, - SecretKey: secretKey, - Region: account.Region, - CloudAccountID: account.ID, - ResourceType: model.ResourceTypeECS, - InstanceIDs: req.InstanceIDs, - SyncMode: model.SyncModeIncremental, - } +// GetChangeLog 获取资源变更日志 +func (s *treeCloudService) GetChangeLog(ctx context.Context, req *model.GetCloudResourceChangeLogReq) (model.ListResp[*model.CloudResourceChangeLog], error) { + // 兜底分页参数 + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) - // 从阿里云获取指定实例的详情 - resources, err := treeUtils.SyncAliyunResources(ctx, config, s.logger) + logs, total, err := s.dao.GetChangeLogList(ctx, req) if err != nil { - return nil, err + s.logger.Error("获取变更日志失败", zap.Error(err)) + return model.ListResp[*model.CloudResourceChangeLog]{}, err } - // 批量导入资源 - var importedIDs []int - for _, resource := range resources { - // 检查资源是否已存在 - existing, err := s.dao.GetByAccountAndInstanceID(ctx, account.ID, resource.InstanceID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.logger.Error("查询资源失败", zap.String("instanceID", resource.InstanceID), zap.Error(err)) - continue - } - - if existing != nil { - // 资源已存在,更新 - resource.ID = existing.ID - if err := s.dao.Update(ctx, resource); err != nil { - s.logger.Error("更新资源失败", zap.Int("id", existing.ID), zap.Error(err)) - continue - } - importedIDs = append(importedIDs, existing.ID) - } else { - // 创建新资源 - if err := s.dao.Create(ctx, resource); err != nil { - s.logger.Error("创建资源失败", zap.String("instanceID", resource.InstanceID), zap.Error(err)) - continue - } - importedIDs = append(importedIDs, resource.ID) - } - } - - s.logger.Info("批量导入阿里云资源成功", zap.Int("count", len(importedIDs))) - return importedIDs, nil + return model.ListResp[*model.CloudResourceChangeLog]{ + Items: logs, + Total: total, + }, nil } diff --git a/internal/tree/utils/aliyun_util.go b/internal/tree/utils/aliyun_util.go index 72a8677f..b5a7552b 100644 --- a/internal/tree/utils/aliyun_util.go +++ b/internal/tree/utils/aliyun_util.go @@ -78,6 +78,10 @@ func (c *AliyunClient) ListECSInstances(ctx context.Context, instanceIDs []strin pageNumber := 1 pageSize := 100 + c.logger.Info("开始获取阿里云ECS实例", + zap.String("region", c.region), + zap.Int("specifiedInstanceCount", len(instanceIDs))) + for { request := ecs.CreateDescribeInstancesRequest() request.Scheme = "https" @@ -89,19 +93,44 @@ func (c *AliyunClient) ListECSInstances(ctx context.Context, instanceIDs []strin request.InstanceIds = fmt.Sprintf(`["%s"]`, strings.Join(instanceIDs, `","`)) } + c.logger.Debug("调用阿里云API", + zap.Int("pageNumber", pageNumber), + zap.Int("pageSize", pageSize)) + response, err := c.client.DescribeInstances(request) if err != nil { + c.logger.Error("阿里云API调用失败", + zap.Int("pageNumber", pageNumber), + zap.Error(err)) return nil, fmt.Errorf("获取ECS实例列表失败(页码:%d): %w", pageNumber, err) } + c.logger.Info("阿里云API返回", + zap.Int("pageNumber", pageNumber), + zap.Int("totalCount", response.TotalCount), + zap.Int("currentPageCount", len(response.Instances.Instance)), + zap.Int("pageSize", response.PageSize)) + // 转换实例数据 for _, instance := range response.Instances.Instance { resource := c.convertECSToResource(&instance) allResources = append(allResources, resource) } - // 检查是否还有下一页 + // 使用TotalCount来判断是否还有下一页 + // 如果已经获取的资源数量大于等于总数,停止分页 + if len(allResources) >= response.TotalCount { + c.logger.Info("已获取所有实例", + zap.Int("totalCount", response.TotalCount), + zap.Int("fetchedCount", len(allResources))) + break + } + + // 如果当前页的实例数小于pageSize,说明这是最后一页 if len(response.Instances.Instance) < pageSize { + c.logger.Info("到达最后一页", + zap.Int("currentPageCount", len(response.Instances.Instance)), + zap.Int("pageSize", pageSize)) break } @@ -113,7 +142,9 @@ func (c *AliyunClient) ListECSInstances(ctx context.Context, instanceIDs []strin pageNumber++ } - c.logger.Info("获取阿里云ECS实例成功", zap.Int("count", len(allResources))) + c.logger.Info("获取阿里云ECS实例成功", + zap.Int("count", len(allResources)), + zap.String("region", c.region)) return allResources, nil } diff --git a/internal/tree/utils/tree_cloud_util.go b/internal/tree/utils/tree_cloud_util.go index 79587f5e..a750a7a0 100644 --- a/internal/tree/utils/tree_cloud_util.go +++ b/internal/tree/utils/tree_cloud_util.go @@ -46,6 +46,12 @@ type AliyunSyncConfig struct { // SyncAliyunResources 同步阿里云资源 func SyncAliyunResources(ctx context.Context, config *AliyunSyncConfig, logger *zap.Logger) ([]*model.TreeCloudResource, error) { + logger.Info("开始同步阿里云资源", + zap.Int("cloudAccountID", config.CloudAccountID), + zap.String("region", config.Region), + zap.String("syncMode", string(config.SyncMode)), + zap.Int("specifiedInstanceCount", len(config.InstanceIDs))) + // 创建阿里云客户端 client, err := NewAliyunClient(config.AccessKey, config.SecretKey, config.Region, logger) if err != nil { @@ -65,6 +71,8 @@ func SyncAliyunResources(ctx context.Context, config *AliyunSyncConfig, logger * return nil, err } + logger.Info("阿里云API返回资源数量", zap.Int("count", len(resources))) + // 为每个资源设置云账户ID for _, resource := range resources { resource.CloudAccountID = config.CloudAccountID diff --git a/pkg/di/init.go b/pkg/di/init.go index becbd40a..42c90120 100644 --- a/pkg/di/init.go +++ b/pkg/di/init.go @@ -53,6 +53,8 @@ func InitTables(db *gorm.DB) error { &model.TreeCloudResource{}, &model.CloudAccount{}, &model.K8sCluster{}, + &model.CloudResourceSyncHistory{}, + &model.CloudResourceChangeLog{}, // prometheus &model.MonitorScrapePool{}, diff --git a/pkg/di/logger.go b/pkg/di/logger.go index d2711d3f..f7a0eb8d 100644 --- a/pkg/di/logger.go +++ b/pkg/di/logger.go @@ -65,10 +65,13 @@ func InitLogger() *zap.Logger { // 创建控制台输出 consoleWriter := zapcore.AddSync(os.Stdout) + // 从配置读取日志等级 + logLevel := getLogLevel(viper.GetString("log.level")) + // 创建 Core core := zapcore.NewTee( - zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), consoleWriter, zapcore.WarnLevel), // 控制台输出warn及以上级别 - zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), fileWriter, zapcore.WarnLevel), // 文件记录warn及以上级别 + zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), consoleWriter, logLevel), // 控制台输出 + zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), fileWriter, logLevel), // 文件记录 ) // 创建 logger @@ -76,3 +79,25 @@ func InitLogger() *zap.Logger { return logger } + +// getLogLevel 根据配置字符串返回对应的日志等级 +func getLogLevel(level string) zapcore.Level { + switch level { + case "debug": + return zapcore.DebugLevel + case "info": + return zapcore.InfoLevel + case "warn": + return zapcore.WarnLevel + case "error": + return zapcore.ErrorLevel + case "dpanic": + return zapcore.DPanicLevel + case "panic": + return zapcore.PanicLevel + case "fatal": + return zapcore.FatalLevel + default: + return zapcore.InfoLevel // 默认使用 Info 级别 + } +} From ccb41e542316dc0db0723aacdeb1b20ec7a815b8 Mon Sep 17 00:00:00 2001 From: Bamboo <13664854532@163.com> Date: Tue, 14 Oct 2025 23:45:40 +0800 Subject: [PATCH 7/8] chore: Update logging level to 'warn' in development and production configurations; refactor TreeCloudDAO interface by removing commented sections for cleaner code --- config/config.development.yaml | 2 +- config/config.production.yaml | 2 +- internal/tree/dao/tree_cloud_dao.go | 15 +-------------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/config/config.development.yaml b/config/config.development.yaml index ecd8d113..40461de9 100644 --- a/config/config.development.yaml +++ b/config/config.development.yaml @@ -2,7 +2,7 @@ server: port: "8889" log: dir: "./logs" - level: "debug" + level: "warn" jwt: key1: "ebe3vxIP7sblVvUHXb7ZaiMPuz4oXo0l" key2: "ebe3vxIP7sblVvUHXb7ZaiMPuz4oXo0z" diff --git a/config/config.production.yaml b/config/config.production.yaml index 236a4230..7c4bab18 100644 --- a/config/config.production.yaml +++ b/config/config.production.yaml @@ -2,7 +2,7 @@ server: port: "8889" log: dir: "./logs" - level: "debug" + level: "warn" jwt: key1: "ebe3vxIP7sblVvUHXb7ZaiMPuz4oXo0l" key2: "ebe3vxIP7sblVvUHXb7ZaiMPuz4oXo0z" diff --git a/internal/tree/dao/tree_cloud_dao.go b/internal/tree/dao/tree_cloud_dao.go index 33f884d2..e8a3216d 100644 --- a/internal/tree/dao/tree_cloud_dao.go +++ b/internal/tree/dao/tree_cloud_dao.go @@ -36,7 +36,6 @@ import ( ) type TreeCloudDAO interface { - // 云资源基础操作 Create(ctx context.Context, cloud *model.TreeCloudResource) error Update(ctx context.Context, cloud *model.TreeCloudResource) error UpdateMetadata(ctx context.Context, id int, metadata map[string]interface{}) error @@ -45,23 +44,13 @@ type TreeCloudDAO interface { GetList(ctx context.Context, req *model.GetTreeCloudResourceListReq) ([]*model.TreeCloudResource, int64, error) GetByAccountAndInstanceID(ctx context.Context, cloudAccountID int, instanceID string) (*model.TreeCloudResource, error) GetByNodeID(ctx context.Context, nodeID int, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) - - // 树节点绑定 BindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error UnBindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error - - // 批量操作 BatchGetByIDs(ctx context.Context, ids []int) ([]*model.TreeCloudResource, error) BatchCreate(ctx context.Context, clouds []*model.TreeCloudResource) error - - // 状态更新 UpdateStatus(ctx context.Context, id int, status model.CloudResourceStatus) error - - // 同步历史 CreateSyncHistory(ctx context.Context, history *model.CloudResourceSyncHistory) error GetSyncHistoryList(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) ([]*model.CloudResourceSyncHistory, int64, error) - - // 变更日志 CreateChangeLog(ctx context.Context, log *model.CloudResourceChangeLog) error GetChangeLogList(ctx context.Context, req *model.GetCloudResourceChangeLogReq) ([]*model.CloudResourceChangeLog, int64, error) } @@ -90,9 +79,7 @@ func (d *treeCloudDAO) Create(ctx context.Context, cloud *model.TreeCloudResourc // Update 更新云资源(用于同步场景,更新所有字段) func (d *treeCloudDAO) Update(ctx context.Context, cloud *model.TreeCloudResource) error { - // 使用Save方法确保所有字段都会被更新,包括零值字段 - // 这对于同步场景很重要,因为云资源的某些字段可能变为零值 - if err := d.db.WithContext(ctx).Save(cloud).Error; err != nil { + if err := d.db.WithContext(ctx).Model(cloud).Updates(cloud).Error; err != nil { d.logger.Error("更新云资源失败", zap.Error(err)) return err } From 67cbae5503409cf9a575dcd425024cbc7189be65 Mon Sep 17 00:00:00 2001 From: Bamboo <13664854532@163.com> Date: Wed, 15 Oct 2025 23:48:26 +0800 Subject: [PATCH 8/8] feat: Introduce change type and source constants for cloud resource management; enhance update and delete requests with operator information --- internal/model/tree_cloud.go | 40 ++++++++++++---- internal/tree/api/tree_cloud_handler.go | 17 ++++++- internal/tree/dao/tree_cloud_dao.go | 4 +- internal/tree/dao/tree_node_dao.go | 12 ++--- internal/tree/service/tree_cloud_service.go | 53 +++++++++------------ internal/tree/ssh/ssh.go | 25 +++++----- internal/tree/utils/aliyun_util.go | 6 +-- 7 files changed, 90 insertions(+), 67 deletions(-) diff --git a/internal/model/tree_cloud.go b/internal/model/tree_cloud.go index cb2320a6..bc241da7 100644 --- a/internal/model/tree_cloud.go +++ b/internal/model/tree_cloud.go @@ -27,6 +27,20 @@ package model import "time" +// 变更类型常量 +const ( + ChangeTypeCreated = "created" // 创建 + ChangeTypeUpdated = "updated" // 更新 + ChangeTypeDeleted = "deleted" // 删除 + ChangeTypeStatusChanged = "status_changed" // 状态变更 +) + +// 变更来源常量 +const ( + ChangeSourceManual = "manual" // 手动操作 + ChangeSourceSync = "sync" // 同步操作 +) + // CloudProvider 云厂商类型 type CloudProvider int8 @@ -207,20 +221,24 @@ type GetTreeCloudResourceDetailReq struct { // UpdateTreeCloudResourceReq 更新云资源本地元数据请求(不影响云上资源) type UpdateTreeCloudResourceReq struct { - ID int `json:"id" binding:"required,gt=0"` - Environment string `json:"environment"` // 环境标识 - Description string `json:"description"` // 资源描述 - Tags KeyValueList `json:"tags"` // 自定义标签 - Port int `json:"port" binding:"omitempty,gte=1,lte=65535"` // SSH端口 - Username string `json:"username"` // SSH用户名 - Password string `json:"password"` // SSH密码 - Key string `json:"key"` // SSH密钥 - AuthMode AuthMode `json:"auth_mode" binding:"omitempty,oneof=1 2"` // SSH认证方式 + ID int `json:"id" binding:"required,gt=0"` + Environment string `json:"environment"` // 环境标识 + Description string `json:"description"` // 资源描述 + Tags KeyValueList `json:"tags"` // 自定义标签 + Port int `json:"port" binding:"omitempty,gte=1,lte=65535"` // SSH端口 + Username string `json:"username"` // SSH用户名 + Password string `json:"password"` // SSH密码 + Key string `json:"key"` // SSH密钥 + AuthMode AuthMode `json:"auth_mode" binding:"omitempty,oneof=1 2"` // SSH认证方式 + OperatorID int `json:"-"` // 操作人ID (不通过JSON传递) + OperatorName string `json:"-"` // 操作人姓名 (不通过JSON传递) } // DeleteTreeCloudResourceReq 删除云资源请求(仅从平台删除,不影响云上资源) type DeleteTreeCloudResourceReq struct { - ID int `json:"id" binding:"required,gt=0"` + ID int `json:"id" binding:"required,gt=0"` + OperatorID int `json:"-"` // 操作人ID (不通过JSON传递) + OperatorName string `json:"-"` // 操作人姓名 (不通过JSON传递) } // SyncTreeCloudResourceReq 从云厂商同步资源请求 @@ -232,6 +250,8 @@ type SyncTreeCloudResourceReq struct { SyncMode SyncMode `json:"sync_mode" binding:"omitempty,oneof=full incremental"` // 同步模式: full-全量, incremental-增量 AutoBind bool `json:"auto_bind"` // 是否自动绑定到服务树节点 BindNodeID int `json:"bind_node_id" binding:"omitempty,gt=0"` // 自动绑定的目标节点ID + OperatorID int `json:"-"` // 操作人ID (不通过JSON传递) + OperatorName string `json:"-"` // 操作人姓名 (不通过JSON传递) } // SyncCloudResourceResp 同步云资源响应 diff --git a/internal/tree/api/tree_cloud_handler.go b/internal/tree/api/tree_cloud_handler.go index 4625f054..e781fb74 100644 --- a/internal/tree/api/tree_cloud_handler.go +++ b/internal/tree/api/tree_cloud_handler.go @@ -89,7 +89,7 @@ func (h *TreeCloudHandler) GetTreeCloudResourceDetail(ctx *gin.Context) { }) } -// UpdateTreeCloudResource 更新云资源本地元数据(不影响云上资源) +// UpdateTreeCloudResource 更新云资源本地元数据 func (h *TreeCloudHandler) UpdateTreeCloudResource(ctx *gin.Context) { var req model.UpdateTreeCloudResourceReq @@ -99,7 +99,11 @@ func (h *TreeCloudHandler) UpdateTreeCloudResource(ctx *gin.Context) { return } + // 获取当前用户信息 + uc := ctx.MustGet("user").(utils.UserClaims) req.ID = id + req.OperatorID = uc.Uid + req.OperatorName = uc.Username utils.HandleRequest(ctx, &req, func() (interface{}, error) { return nil, h.service.UpdateTreeCloudResource(ctx, &req) @@ -116,7 +120,11 @@ func (h *TreeCloudHandler) DeleteTreeCloudResource(ctx *gin.Context) { return } + // 获取当前用户信息 + uc := ctx.MustGet("user").(utils.UserClaims) req.ID = id + req.OperatorID = uc.Uid + req.OperatorName = uc.Username utils.HandleRequest(ctx, &req, func() (interface{}, error) { return nil, h.service.DeleteTreeCloudResource(ctx, &req) @@ -157,10 +165,15 @@ func (h *TreeCloudHandler) UnBindTreeCloudResource(ctx *gin.Context) { }) } -// SyncTreeCloudResource 从云厂商同步资源(核心功能) +// SyncTreeCloudResource 从云厂商同步资源 func (h *TreeCloudHandler) SyncTreeCloudResource(ctx *gin.Context) { var req model.SyncTreeCloudResourceReq + // 获取当前用户信息 + uc := ctx.MustGet("user").(utils.UserClaims) + req.OperatorID = uc.Uid + req.OperatorName = uc.Username + utils.HandleRequest(ctx, &req, func() (interface{}, error) { return h.service.SyncTreeCloudResource(ctx, &req) }) diff --git a/internal/tree/dao/tree_cloud_dao.go b/internal/tree/dao/tree_cloud_dao.go index e8a3216d..20f5ffa6 100644 --- a/internal/tree/dao/tree_cloud_dao.go +++ b/internal/tree/dao/tree_cloud_dao.go @@ -77,7 +77,7 @@ func (d *treeCloudDAO) Create(ctx context.Context, cloud *model.TreeCloudResourc return nil } -// Update 更新云资源(用于同步场景,更新所有字段) +// Update 更新云资源 func (d *treeCloudDAO) Update(ctx context.Context, cloud *model.TreeCloudResource) error { if err := d.db.WithContext(ctx).Model(cloud).Updates(cloud).Error; err != nil { d.logger.Error("更新云资源失败", zap.Error(err)) @@ -87,7 +87,7 @@ func (d *treeCloudDAO) Update(ctx context.Context, cloud *model.TreeCloudResourc return nil } -// UpdateMetadata 更新云资源的本地元数据(只更新指定字段) +// UpdateMetadata 更新云资源元数据 func (d *treeCloudDAO) UpdateMetadata(ctx context.Context, id int, metadata map[string]interface{}) error { if err := d.db.WithContext(ctx). Model(&model.TreeCloudResource{}). diff --git a/internal/tree/dao/tree_node_dao.go b/internal/tree/dao/tree_node_dao.go index dda92171..921db318 100644 --- a/internal/tree/dao/tree_node_dao.go +++ b/internal/tree/dao/tree_node_dao.go @@ -109,12 +109,12 @@ func (t *treeNodeDAO) GetTreeList(ctx context.Context, req *model.GetTreeNodeLis return nil, 0, err } - // 如果指定了层级,直接返回列表(已分页) + // 指定层级时直接返回列表 if req.Level > 0 { return nodes, count, nil } - // 构建树形结构(基于已分页的数据) + // 构建树形结构 return treeUtils.BuildTreeStructure(nodes), count, nil } @@ -186,7 +186,7 @@ func (t *treeNodeDAO) GetTreeStatistics(ctx context.Context) (*model.TreeNodeSta stats.TotalResources = int(count) } - // 管理员总数(关联关系条目数) + // 管理员总数 count = 0 if err := t.db.WithContext(ctx).Table("cl_tree_node_admin").Count(&count).Error; err != nil { t.logger.Error("统计管理员总数失败", zap.Error(err)) @@ -194,7 +194,7 @@ func (t *treeNodeDAO) GetTreeStatistics(ctx context.Context) (*model.TreeNodeSta stats.TotalAdmins = int(count) } - // 成员总数(关联关系条目数) + // 成员总数 count = 0 if err := t.db.WithContext(ctx).Table("cl_tree_node_member").Count(&count).Error; err != nil { t.logger.Error("统计成员总数失败", zap.Error(err)) @@ -293,7 +293,7 @@ func (t *treeNodeDAO) UpdateNode(ctx context.Context, node *model.TreeNode) erro // 如果父节点发生变化,需要验证和计算层级 if node.ParentID != existingNode.ParentID { - // 验证新父节点存在(如果不是根节点) + // 验证新父节点存在 if node.ParentID != 0 { var count int64 if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("id = ?", node.ParentID).Count(&count).Error; err != nil { @@ -558,7 +558,7 @@ func (t *treeNodeDAO) GetNodeMembers(ctx context.Context, nodeId int, memberType return nil, err } case "all", "": - // 获取所有用户(管理员+成员) + // 获取所有用户 var adminUsers []*model.User var memberUsers []*model.User diff --git a/internal/tree/service/tree_cloud_service.go b/internal/tree/service/tree_cloud_service.go index 054e6cf9..e263086e 100644 --- a/internal/tree/service/tree_cloud_service.go +++ b/internal/tree/service/tree_cloud_service.go @@ -40,26 +40,17 @@ import ( ) type TreeCloudService interface { - // 查询相关 GetTreeCloudResourceList(ctx context.Context, req *model.GetTreeCloudResourceListReq) (model.ListResp[*model.TreeCloudResource], error) GetTreeCloudResourceDetail(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) GetTreeCloudResourceForConnection(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) GetTreeNodeCloudResources(ctx context.Context, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) - - // 同步相关(核心功能) SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) (*model.SyncCloudResourceResp, error) GetSyncHistory(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) (model.ListResp[*model.CloudResourceSyncHistory], error) - - // 本地管理相关 UpdateTreeCloudResource(ctx context.Context, req *model.UpdateTreeCloudResourceReq) error DeleteTreeCloudResource(ctx context.Context, req *model.DeleteTreeCloudResourceReq) error UpdateCloudResourceStatus(ctx context.Context, req *model.UpdateCloudResourceStatusReq) error - - // 服务树绑定相关 BindTreeCloudResource(ctx context.Context, req *model.BindTreeCloudResourceReq) error UnBindTreeCloudResource(ctx context.Context, req *model.UnBindTreeCloudResourceReq) error - - // 变更日志相关 GetChangeLog(ctx context.Context, req *model.GetCloudResourceChangeLogReq) (model.ListResp[*model.CloudResourceChangeLog], error) } @@ -127,7 +118,7 @@ func (s *treeCloudService) GetTreeCloudResourceForConnection(ctx context.Context return nil, err } - // 解密SSH密码(针对ECS类型) + // 解密SSH密码 if cloud.AuthMode == model.AuthModePassword && cloud.Password != "" { plainPassword, err := treeUtils.DecryptPassword(cloud.Password) if err != nil { @@ -140,7 +131,7 @@ func (s *treeCloudService) GetTreeCloudResourceForConnection(ctx context.Context return cloud, nil } -// UpdateTreeCloudResource 更新云资源本地元数据(不影响云上资源) +// UpdateTreeCloudResource 更新云资源本地元数据 func (s *treeCloudService) UpdateTreeCloudResource(ctx context.Context, req *model.UpdateTreeCloudResourceReq) error { if err := treeUtils.ValidateID(req.ID); err != nil { return fmt.Errorf("无效的云资源ID: %w", err) @@ -203,7 +194,7 @@ func (s *treeCloudService) UpdateTreeCloudResource(ctx context.Context, req *mod return err } - // 记录变更日志(记录每个字段的变更) + // 记录变更日志 // 获取资源实例ID用于日志 resource, _ := s.dao.GetByID(ctx, req.ID) instanceID := "" @@ -216,13 +207,13 @@ func (s *treeCloudService) UpdateTreeCloudResource(ctx context.Context, req *mod changeLog := &model.CloudResourceChangeLog{ ResourceID: req.ID, InstanceID: instanceID, - ChangeType: "updated", + ChangeType: model.ChangeTypeUpdated, FieldName: fieldName, - OldValue: "", // 简化处理,不记录旧值 + OldValue: "", NewValue: fmt.Sprintf("%v", newValue), - ChangeSource: "manual", - OperatorID: 0, - OperatorName: "", + ChangeSource: model.ChangeSourceManual, + OperatorID: req.OperatorID, + OperatorName: req.OperatorName, ChangeTime: time.Now(), } // 异步记录,不影响主流程 @@ -237,7 +228,7 @@ func (s *treeCloudService) UpdateTreeCloudResource(ctx context.Context, req *mod return nil } -// DeleteTreeCloudResource 删除云资源(仅从平台删除,不影响云上资源) +// DeleteTreeCloudResource 删除云资源 func (s *treeCloudService) DeleteTreeCloudResource(ctx context.Context, req *model.DeleteTreeCloudResourceReq) error { if err := treeUtils.ValidateID(req.ID); err != nil { return fmt.Errorf("无效的云资源ID: %w", err) @@ -254,7 +245,7 @@ func (s *treeCloudService) DeleteTreeCloudResource(ctx context.Context, req *mod } // 记录删除日志 - s.recordChangeLog(ctx, cloud, nil, "manual", 0, "") + s.recordChangeLog(ctx, cloud, nil, model.ChangeSourceManual, req.OperatorID, req.OperatorName) if err := s.dao.Delete(ctx, req.ID); err != nil { s.logger.Error("删除云资源失败", zap.Int("id", req.ID), zap.Error(err)) @@ -335,7 +326,7 @@ func (s *treeCloudService) UpdateCloudResourceStatus(ctx context.Context, req *m return nil } -// SyncTreeCloudResource 从云厂商同步资源(核心功能) +// SyncTreeCloudResource 从云厂商同步资源 func (s *treeCloudService) SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) (*model.SyncCloudResourceResp, error) { startTime := time.Now() @@ -481,15 +472,15 @@ func (s *treeCloudService) syncAliyunResourcesWithStats(ctx context.Context, acc // 根据同步模式处理资源 if req.SyncMode == model.SyncModeFull { // 全量同步:先删除该云账户下的所有ECS资源,再重新创建 - return s.fullSyncResources(ctx, account.ID, resources, resp, req.AutoBind, req.BindNodeID) + return s.fullSyncResources(ctx, account.ID, resources, resp, req.AutoBind, req.BindNodeID, req.OperatorID, req.OperatorName) } // 增量同步:更新已存在的资源,创建不存在的资源 - return s.incrementalSyncResources(ctx, account.ID, resources, resp, req.AutoBind, req.BindNodeID) + return s.incrementalSyncResources(ctx, account.ID, resources, resp, req.AutoBind, req.BindNodeID, req.OperatorID, req.OperatorName) } // fullSyncResources 全量同步资源 -func (s *treeCloudService) fullSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource, resp *model.SyncCloudResourceResp, autoBind bool, bindNodeID int) error { +func (s *treeCloudService) fullSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource, resp *model.SyncCloudResourceResp, autoBind bool, bindNodeID int, operatorID int, operatorName string) error { // 获取该云账户下的所有ECS资源 req := &model.GetTreeCloudResourceListReq{ ListReq: model.ListReq{ @@ -520,17 +511,17 @@ func (s *treeCloudService) fullSyncResources(ctx context.Context, cloudAccountID } else { resp.DeleteCount++ // 记录删除日志 - s.recordChangeLog(ctx, existingResource, nil, "sync", 0, "") + s.recordChangeLog(ctx, existingResource, nil, model.ChangeSourceSync, operatorID, operatorName) } } } // 更新或创建资源 - return s.incrementalSyncResources(ctx, cloudAccountID, resources, resp, autoBind, bindNodeID) + return s.incrementalSyncResources(ctx, cloudAccountID, resources, resp, autoBind, bindNodeID, operatorID, operatorName) } // incrementalSyncResources 增量同步资源 -func (s *treeCloudService) incrementalSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource, resp *model.SyncCloudResourceResp, autoBind bool, bindNodeID int) error { +func (s *treeCloudService) incrementalSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource, resp *model.SyncCloudResourceResp, autoBind bool, bindNodeID int, operatorID int, operatorName string) error { for _, resource := range resources { resp.TotalCount++ @@ -553,7 +544,7 @@ func (s *treeCloudService) incrementalSyncResources(ctx context.Context, cloudAc } else { resp.UpdateCount++ // 记录更新日志 - s.recordChangeLog(ctx, existing, resource, "sync", 0, "") + s.recordChangeLog(ctx, existing, resource, model.ChangeSourceSync, operatorID, operatorName) } } else { // 创建新资源 @@ -564,7 +555,7 @@ func (s *treeCloudService) incrementalSyncResources(ctx context.Context, cloudAc } else { resp.NewCount++ // 记录创建日志 - s.recordChangeLog(ctx, nil, resource, "sync", 0, "") + s.recordChangeLog(ctx, nil, resource, model.ChangeSourceSync, operatorID, operatorName) // 如果启用自动绑定,则绑定到指定节点 if autoBind && bindNodeID > 0 { @@ -589,7 +580,7 @@ func (s *treeCloudService) recordChangeLog(ctx context.Context, oldResource, new changeLog := &model.CloudResourceChangeLog{ ResourceID: oldResource.ID, InstanceID: oldResource.InstanceID, - ChangeType: "deleted", + ChangeType: model.ChangeTypeDeleted, FieldName: "", OldValue: oldResource.Name, NewValue: "", @@ -610,7 +601,7 @@ func (s *treeCloudService) recordChangeLog(ctx context.Context, oldResource, new changeLog := &model.CloudResourceChangeLog{ ResourceID: newResource.ID, InstanceID: newResource.InstanceID, - ChangeType: "created", + ChangeType: model.ChangeTypeCreated, FieldName: "", OldValue: "", NewValue: newResource.Name, @@ -633,7 +624,7 @@ func (s *treeCloudService) recordChangeLog(ctx context.Context, oldResource, new changeLog := &model.CloudResourceChangeLog{ ResourceID: newResource.ID, InstanceID: newResource.InstanceID, - ChangeType: "status_changed", + ChangeType: model.ChangeTypeStatusChanged, FieldName: "status", OldValue: fmt.Sprintf("%d", oldResource.Status), NewValue: fmt.Sprintf("%d", newResource.Status), diff --git a/internal/tree/ssh/ssh.go b/internal/tree/ssh/ssh.go index 50f0cc1a..a7fcd424 100644 --- a/internal/tree/ssh/ssh.go +++ b/internal/tree/ssh/ssh.go @@ -24,8 +24,8 @@ type ecsSSH struct { Port int // SSH端口号,默认22 Username string // SSH用户名 Mode int8 // 认证方式:1:密码,2:密钥 - Password string // 密码(当Mode为password时使用) - Key string // SSH私钥内容(当Mode为key时使用) + Password string // 密码 + Key string // SSH私钥内容 Client *ssh.Client // SSH客户端连接 UserID int // 用户ID,用于区分不同用户的会话 Sessions map[int]*ssh.Session // 用户会话映射表,key为UserID,value为对应的SSH会话 @@ -70,7 +70,7 @@ func (s *ecsSSH) Connect(ip string, port int, username string, password string, s.Mode = mode s.UserID = userID - // 初始化Sessions映射(双重检查锁定模式) + // 初始化Sessions映射 if s.Sessions == nil { s.sessionMu.Lock() if s.Sessions == nil { @@ -89,8 +89,8 @@ func (s *ecsSSH) Connect(ip string, port int, username string, password string, // 配置SSH客户端 config := &ssh.ClientConfig{ User: s.Username, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 注意:生产环境应使用更安全的主机密钥验证 - Timeout: 10 * time.Second, // 连接超时时间 + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 10 * time.Second, } // 根据认证方式配置认证方法 @@ -185,7 +185,7 @@ func (s *ecsSSH) Run(command string) (string, error) { } } - // 为每个命令创建新的会话(避免会话状态污染) + // 为每个命令创建新的会话 session, err := s.Client.NewSession() if err != nil { s.logger.Error("创建命令执行会话失败", zap.Error(err)) @@ -199,7 +199,7 @@ func (s *ecsSSH) Run(command string) (string, error) { } }() - // 执行命令并获取输出(合并stdout和stderr) + // 执行命令并获取输出 s.logger.Debug("执行命令", zap.String("命令", command)) buf, err := session.CombinedOutput(command) s.LastResult = string(buf) @@ -283,7 +283,7 @@ func (r MyReader) Read(p []byte) (n int, err error) { // 将接收到的消息转换为命令字符串 cmdStr := string(message) - // 确保命令以换行符结尾(终端需要) + // 确保命令以换行符结尾 if !strings.HasSuffix(cmdStr, "\n") { cmdStr = cmdStr + "\n" } @@ -363,13 +363,12 @@ func (s *ecsSSH) Web2SSH(ws *websocket.Conn) { // 配置伪终端模式 modes := ssh.TerminalModes{ - ssh.ECHO: 0, // 禁用回显(避免重复显示用户输入) - ssh.TTY_OP_ISPEED: 14400, // 输入波特率 + ssh.ECHO: 0, // 禁用回显 + ssh.TTY_OP_ISPEED: 14400, ssh.TTY_OP_OSPEED: 14400, // 输出波特率 } - // 请求伪终端(PTY) - // xterm: 终端类型 + // 请求伪终端 // 25: 终端行数 // 80: 终端列数 if err := session.RequestPty("xterm", 25, 80, modes); err != nil { @@ -400,7 +399,7 @@ func (s *ecsSSH) Web2SSH(ws *websocket.Conn) { s.logger.Info("Web终端SSH会话已启动", zap.Int("用户ID", s.UserID)) - // 等待SSH会话结束(阻塞直到用户退出或连接断开) + // 等待SSH会话结束 if err := session.Wait(); err != nil { s.logger.Info("SSH会话结束", zap.Int("用户ID", s.UserID), zap.Error(err)) } else { diff --git a/internal/tree/utils/aliyun_util.go b/internal/tree/utils/aliyun_util.go index b5a7552b..41b942e6 100644 --- a/internal/tree/utils/aliyun_util.go +++ b/internal/tree/utils/aliyun_util.go @@ -72,7 +72,7 @@ func (c *AliyunClient) VerifyCredentials(ctx context.Context) error { return nil } -// ListECSInstances 获取ECS实例列表(支持分页获取所有实例) +// ListECSInstances 获取ECS实例列表 func (c *AliyunClient) ListECSInstances(ctx context.Context, instanceIDs []string) ([]*model.TreeCloudResource, error) { var allResources []*model.TreeCloudResource pageNumber := 1 @@ -126,7 +126,7 @@ func (c *AliyunClient) ListECSInstances(ctx context.Context, instanceIDs []strin break } - // 如果当前页的实例数小于pageSize,说明这是最后一页 + // 判断是否为最后一页 if len(response.Instances.Instance) < pageSize { c.logger.Info("到达最后一页", zap.Int("currentPageCount", len(response.Instances.Instance)), @@ -174,7 +174,7 @@ func (c *AliyunClient) convertECSToResource(instance *ecs.Instance) *model.TreeC resource.PrivateIP = instance.VpcAttributes.PrivateIpAddress.IpAddress[0] } - // 设置镜像名称(如果OSName为空,使用ImageId) + // 设置镜像名称 if instance.OSName != "" { resource.ImageName = instance.OSName } else {