diff --git a/cmd/cloud-server/logics/load-balancer/create_layer4_listener_preview_executor.go b/cmd/cloud-server/logics/load-balancer/create_layer4_listener_preview_executor.go index 6beb8862ff..75f7cbe7f6 100644 --- a/cmd/cloud-server/logics/load-balancer/create_layer4_listener_preview_executor.go +++ b/cmd/cloud-server/logics/load-balancer/create_layer4_listener_preview_executor.go @@ -190,7 +190,7 @@ func (c *CreateLayer4ListenerPreviewExecutor) validateListener(kt *kit.Kit, return nil } - rule, err := c.getURLRule(kt, curDetail.CloudClbID, listener.CloudID) + rule, err := c.getURLRule(kt, listener.LbID, listener.ID) if err != nil { return err } @@ -223,15 +223,15 @@ func (c *CreateLayer4ListenerPreviewExecutor) validateListener(kt *kit.Kit, return nil } -func (c *CreateLayer4ListenerPreviewExecutor) getURLRule(kt *kit.Kit, lbCloudID, listenerCloudID string) ( +func (c *CreateLayer4ListenerPreviewExecutor) getURLRule(kt *kit.Kit, lbID, listenerID string) ( *corelb.TCloudLbUrlRule, error) { switch c.vendor { case enumor.TCloud: req := &core.ListReq{ Filter: tools.ExpressionAnd( - tools.RuleEqual("cloud_lb_id", lbCloudID), - tools.RuleEqual("cloud_lbl_id", listenerCloudID), + tools.RuleEqual("lb_id", lbID), + tools.RuleEqual("lbl_id", listenerID), ), Page: core.NewDefaultBasePage(), } diff --git a/cmd/cloud-server/service/account/get.go b/cmd/cloud-server/service/account/get.go index dd91f5cc24..b4bf968da9 100644 --- a/cmd/cloud-server/service/account/get.go +++ b/cmd/cloud-server/service/account/get.go @@ -100,13 +100,18 @@ func (a *accountSvc) GetAccount(cts *rest.Contexts) (interface{}, error) { func accountDetailFullFill[T protocloud.AccountExtensionGetResp](svc *accountSvc, cts *rest.Contexts, acc *protocloud.AccountGetResult[T]) (*protocloud.AccountGetResult[T], error) { acc.RecycleReserveTime = convertRecycleReverseTime(acc.RecycleReserveTime) - status, failedReason, err := svc.getAccountSyncDetail(cts, acc.ID, string(acc.Vendor)) + syncDetails, err := svc.getAccountsSyncDetail(cts, acc.ID) if err != nil { logs.Errorf("fail to get account sync detail, accountID: %s, rid: %s", acc.ID, cts.Kit.Rid) return nil, err } - acc.SyncStatus = status - acc.SyncFailedReason = failedReason + for _, detail := range syncDetails[acc.ID] { + acc.SyncStatus = detail.ResStatus + if detail.ResStatus == string(enumor.SyncFailed) { + acc.SyncFailedReason = string(detail.ResFailedReason) + break + } + } return acc, nil } diff --git a/cmd/cloud-server/service/account/list.go b/cmd/cloud-server/service/account/list.go index ac306144e9..84e293ee01 100644 --- a/cmd/cloud-server/service/account/list.go +++ b/cmd/cloud-server/service/account/list.go @@ -20,28 +20,37 @@ package account import ( + "fmt" + proto "hcm/pkg/api/cloud-server/account" "hcm/pkg/api/core" + "hcm/pkg/api/core/cloud" + coresync "hcm/pkg/api/core/cloud/sync" dataproto "hcm/pkg/api/data-service/cloud" "hcm/pkg/criteria/enumor" "hcm/pkg/criteria/errf" + "hcm/pkg/dal/dao/tools" "hcm/pkg/iam/meta" + "hcm/pkg/kit" + "hcm/pkg/logs" "hcm/pkg/rest" "hcm/pkg/runtime/filter" + "hcm/pkg/tools/converter" + "hcm/pkg/tools/slice" ) // ListAccount ... func (a *accountSvc) ListAccount(cts *rest.Contexts) (interface{}, error) { - return a.list(cts, meta.Account) + return a.listAccount(cts, meta.Account) } // ResourceList ... func (a *accountSvc) ResourceList(cts *rest.Contexts) (interface{}, error) { - return a.list(cts, meta.CloudResource) + return a.listResource(cts, meta.CloudResource) } -func (a *accountSvc) list(cts *rest.Contexts, typ meta.ResourceType) (interface{}, error) { - req := new(proto.AccountListReq) +func (a *accountSvc) listResource(cts *rest.Contexts, typ meta.ResourceType) (interface{}, error) { + req := new(proto.AccountListResourceReq) if err := cts.DecodeInto(req); err != nil { return nil, err } @@ -77,69 +86,253 @@ func (a *accountSvc) list(cts *rest.Contexts, typ meta.ResourceType) (interface{ } } - accounts, err := a.client.DataService().Global.Account.List( - cts.Kit.Ctx, - cts.Kit.Header(), - &dataproto.AccountListReq{ - Filter: reqFilter, - Page: req.Page, - }, - ) + listReq := &dataproto.AccountListReq{ + Filter: reqFilter, + Page: req.Page, + } + accounts, err := a.client.DataService().Global.Account.List(cts.Kit.Ctx, cts.Kit.Header(), listReq) if err != nil { + logs.Errorf("list account failed, err: %v, rid: %s", err, cts.Kit.Rid) return nil, err } + respIDs := make([]string, 0, len(accounts.Details)) + for _, one := range accounts.Details { + respIDs = append(respIDs, one.ID) + } + accountDetailsMap, err := a.getAccountsSyncDetail(cts, respIDs...) + if err != nil { + logs.Errorf("get account sync detail failed, err: %v, rid: %s", err, cts.Kit.Rid) + return nil, err + } for _, one := range accounts.Details { - status, failedReason, err := a.getAccountSyncDetail(cts, one.ID, string(one.Vendor)) + for _, detail := range accountDetailsMap[one.ID] { + one.SyncStatus = detail.ResStatus + if detail.ResStatus == string(enumor.SyncFailed) { + one.SyncFailedReason = string(detail.ResFailedReason) + break + } + } + one.RecycleReserveTime = convertRecycleReverseTime(one.RecycleReserveTime) + } + + return accounts, nil +} + +func (a *accountSvc) listAccount(cts *rest.Contexts, typ meta.ResourceType) (*dataproto.AccountListResult, error) { + req := new(proto.AccountListReq) + if err := cts.DecodeInto(req); err != nil { + return nil, err + } + + if err := req.Validate(); err != nil { + return nil, errf.NewFromErr(errf.InvalidParameter, err) + } + + // 校验用户是否有查看权限,有权限的ID列表 + accountIDs, isAny, err := a.listAuthorized(cts, meta.Find, typ) + if err != nil { + return nil, err + } + + if isAny { + accounts, err := a.listAccountByFilter(cts.Kit, req.Filter) + if err != nil { + logs.Errorf("list account by filter failed, err: %v, rid: %s", err, cts.Kit.Rid) + return nil, err + } + err = a.fillAccountSyncDetail(cts, accounts) + if err != nil { + logs.Errorf("fill account sync detail failed, err: %v, rid: %s", err, cts.Kit.Rid) + return nil, err + } + return &dataproto.AccountListResult{ + Details: accounts, + Count: uint64(len(accounts)), + }, nil + } + + bizAccounts, err := a.listAccountByBiz(cts) + if err != nil { + logs.Errorf("list account by biz failed, err: %v, rid: %s", err, cts.Kit.Rid) + return nil, err + } + accountIDs = append(accountIDs, bizAccounts...) + accountIDs = slice.Unique(accountIDs) + + // 构造权限过滤条件 + accounts := make([]*cloud.BaseAccount, 0) + for _, ids := range slice.Split(accountIDs, int(core.DefaultMaxPageLimit)) { + innerFilter := tools.ExpressionOr( + tools.RuleJSONContains("managers", cts.Kit.User), + tools.RuleIn("id", ids), + ) + // 加上请求里过滤条件 + var reqFilter *filter.Expression + if req.Filter != nil && !req.Filter.IsEmpty() { + reqFilter, err = tools.And(innerFilter, req.Filter) + if err != nil { + logs.Errorf("merge filter failed, err: %v, rid: %s", err, cts.Kit.Rid) + return nil, err + } + } else { + reqFilter = innerFilter + } + accountList, err := a.listAccountByFilter(cts.Kit, reqFilter) if err != nil { + logs.Errorf("list account by filter failed, err: %v, rid: %s", err, cts.Kit.Rid) return nil, err } - one.SyncStatus = status - one.SyncFailedReason = failedReason + accounts = append(accounts, accountList...) + } + + err = a.fillAccountSyncDetail(cts, accounts) + if err != nil { + logs.Errorf("fill account sync detail failed, err: %v, rid: %s", err, cts.Kit.Rid) + return nil, err + } + + return &dataproto.AccountListResult{ + Details: accounts, + Count: uint64(len(accounts)), + }, nil +} + +// fillAccountSyncDetail 补全同步状态信息 +func (a *accountSvc) fillAccountSyncDetail(cts *rest.Contexts, accounts []*cloud.BaseAccount) error { + syncAccountMap := make(map[string]*cloud.BaseAccount) + for _, one := range accounts { + if one.Type == enumor.RegistrationAccount { + continue + } + syncAccountMap[one.ID] = one + } + if len(syncAccountMap) == 0 { + return nil + } + syncDetails, err := a.getAccountsSyncDetail(cts, converter.MapKeyToSlice(syncAccountMap)...) + if err != nil { + logs.Errorf("get account sync detail failed, err: %v, rid: %s", err, cts.Kit.Rid) + return err + } + + for _, one := range syncAccountMap { + for _, detail := range syncDetails[one.ID] { + one.SyncStatus = detail.ResStatus + if detail.ResStatus == string(enumor.SyncFailed) { + one.SyncFailedReason = string(detail.ResFailedReason) + break + } + } one.RecycleReserveTime = convertRecycleReverseTime(one.RecycleReserveTime) } + return nil +} +func (a *accountSvc) listAccountByFilter(kt *kit.Kit, reqFilter *filter.Expression) ([]*cloud.BaseAccount, error) { + page := &core.BasePage{ + Count: false, + Start: 0, + Limit: core.DefaultMaxPageLimit, + Sort: "id", + } + accounts := make([]*cloud.BaseAccount, 0) + for { + listReq := &dataproto.AccountListReq{ + Filter: reqFilter, + Page: page, + } + resp, err := a.client.DataService().Global.Account.List(kt.Ctx, kt.Header(), listReq) + if err != nil { + logs.Errorf("list account failed, err: %v, req: %v, rid: %s", err, listReq, kt.Rid) + return nil, err + } + if len(resp.Details) == 0 { + break + } + accounts = append(accounts, resp.Details...) + page.Start += uint32(core.DefaultMaxPageLimit) + } return accounts, nil } -func (a *accountSvc) getAccountSyncDetail(cts *rest.Contexts, accountID string, - vendor string) (string, string, error) { +// listAccountByBiz 根据账号可见业务查询账号列表 +func (a *accountSvc) listAccountByBiz(cts *rest.Contexts) ([]string, error) { + bizIDs, _, err := a.listAuthorized(cts, meta.Access, meta.Biz) + if err != nil { + return nil, err + } - listReq := &core.ListReq{ - Filter: &filter.Expression{ - Op: filter.And, - Rules: []filter.RuleFactory{ - &filter.AtomRule{ - Field: "account_id", - Op: filter.Equal.Factory(), - Value: accountID, - }, - &filter.AtomRule{ - Field: "vendor", - Op: filter.Equal.Factory(), - Value: vendor, + resultMap := make(map[string]struct{}) + for _, ids := range slice.Split(bizIDs, int(core.DefaultMaxPageLimit)) { + + intIDs := converter.StringSliceToInt64Slice(ids) + offset := uint32(0) + for { + listReq := &core.ListReq{ + Filter: tools.ExpressionAnd( + tools.RuleIn("bk_biz_id", intIDs), + ), + Page: &core.BasePage{ + Count: false, + Start: offset, + Limit: core.DefaultMaxPageLimit, }, - }, - }, - Page: &core.BasePage{ - Start: 0, - Limit: core.DefaultMaxPageLimit, - }, + } + resp, err := a.client.DataService().Global.Account.ListAccountBizRel(cts.Kit.Ctx, cts.Kit.Header(), listReq) + if err != nil { + logs.Errorf("list account biz relation failed, err: %v, req: %v, rid: %s", err, listReq, cts.Kit.Rid) + return nil, err + } + if len(resp.Details) == 0 { + break + } + + for _, detail := range resp.Details { + resultMap[detail.AccountID] = struct{}{} + } + offset += uint32(core.DefaultMaxPageLimit) + } } - accountSyncDetail, err := a.client.DataService().Global.AccountSyncDetail.List(cts.Kit, listReq) - if err != nil { - return "", "", err + + return converter.MapKeyToSlice(resultMap), nil +} + +func getSliceByPage[T any](data []T, page *core.BasePage) []T { + length := len(data) + if length == 0 { + return []T{} } + // safe slice + begin := min(int(page.Start), length) + end := min(length, int(page.Start)+int(page.Limit)) + return data[begin:end] +} - status := "" - failedReason := "" - for _, one := range accountSyncDetail.Details { - status = one.ResStatus - if one.ResStatus == string(enumor.SyncFailed) { - failedReason = string(one.ResFailedReason) - break +func (a *accountSvc) getAccountsSyncDetail(cts *rest.Contexts, accountIDs ...string) ( + map[string][]coresync.AccountSyncDetailTable, error) { + + if len(accountIDs) == 0 { + return nil, fmt.Errorf("accountIDs is empty") + } + + result := make(map[string][]coresync.AccountSyncDetailTable) + for _, ids := range slice.Split(accountIDs, int(core.DefaultMaxPageLimit)) { + listReq := &core.ListReq{ + Filter: tools.ExpressionAnd( + tools.RuleIn("account_id", ids), + ), + Page: core.NewDefaultBasePage(), + } + accountSyncDetail, err := a.client.DataService().Global.AccountSyncDetail.List(cts.Kit, listReq) + if err != nil { + logs.Errorf("list account sync detail failed, err: %v, req: %v, rid: %s", err, listReq, cts.Kit.Rid) + return nil, err + } + for _, detail := range accountSyncDetail.Details { + result[detail.AccountID] = append(result[detail.AccountID], detail) } } - return status, failedReason, nil + return result, nil } diff --git a/cmd/cloud-server/service/account/list_with_extension.go b/cmd/cloud-server/service/account/list_with_extension.go index c85e09a060..271a92f32d 100644 --- a/cmd/cloud-server/service/account/list_with_extension.go +++ b/cmd/cloud-server/service/account/list_with_extension.go @@ -45,7 +45,7 @@ func canListAccountExtension(appCode string) error { // ListWithExtension 该接口返回了Extension,不包括SecretKey,只提供给安全使用 func (a *accountSvc) ListWithExtension(cts *rest.Contexts) (interface{}, error) { - req := new(proto.AccountListReq) + req := new(proto.AccountListWithExtReq) if err := cts.DecodeInto(req); err != nil { return nil, err } diff --git a/docs/api-docs/web-server/docs/resource/list_account.md b/docs/api-docs/web-server/docs/resource/list_account.md index 523de126f4..932c346a25 100644 --- a/docs/api-docs/web-server/docs/resource/list_account.md +++ b/docs/api-docs/web-server/docs/resource/list_account.md @@ -13,7 +13,8 @@ POST /api/v1/cloud/accounts/list | 参数名称 | 参数类型 | 必选 | 描述 | |--------|--------|----|--------| | filter | object | 是 | 查询过滤条件 | -| page | object | 是 | 分页设置 | + +说明:接口返回符合条件的全量数据, 前端需要在本地进行分页 #### filter @@ -82,16 +83,6 @@ POST /api/v1/cloud/accounts/list } ``` -#### page - -| 参数名称 | 参数类型 | 必选 | 描述 | -|-------|--------|----|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| count | bool | 是 | 是否返回总记录条数。 如果为true,查询结果返回总记录条数 count,但查询结果详情数据 details 为空数组,此时 start 和 limit 参数将无效,且必需设置为0。如果为false,则根据 start 和 limit 参数,返回查询结果详情数据,但总记录条数 count 为0 | -| start | uint32 | 否 | 记录开始位置,start 起始值为0 | -| limit | uint32 | 否 | 每页限制条数,最大500,不能为0 | -| sort | string | 否 | 排序字段,返回数据将按该字段进行排序 | -| order | string | 否 | 排序顺序(枚举值:ASC、DESC) | - #### 查询参数介绍: | 参数名称 | 参数类型 | 描述 | diff --git a/pkg/api/cloud-server/account/list.go b/pkg/api/cloud-server/account/list.go index c9d394a96c..38d5486d71 100644 --- a/pkg/api/cloud-server/account/list.go +++ b/pkg/api/cloud-server/account/list.go @@ -28,7 +28,6 @@ import ( // AccountListReq ... type AccountListReq struct { Filter *filter.Expression `json:"filter" validate:"omitempty"` - Page *core.BasePage `json:"page" validate:"required"` } // Validate ... @@ -36,6 +35,28 @@ func (req *AccountListReq) Validate() error { return validator.Validate.Struct(req) } +// AccountListResourceReq ... +type AccountListResourceReq struct { + Filter *filter.Expression `json:"filter" validate:"omitempty"` + Page *core.BasePage `json:"page" validate:"required"` +} + +// Validate ... +func (req *AccountListResourceReq) Validate() error { + return validator.Validate.Struct(req) +} + +// AccountListWithExtReq ... +type AccountListWithExtReq struct { + Filter *filter.Expression `json:"filter" validate:"omitempty"` + Page *core.BasePage `json:"page" validate:"required"` +} + +// Validate ... +func (req *AccountListWithExtReq) Validate() error { + return validator.Validate.Struct(req) +} + // ListSecretKeyReq ... type ListSecretKeyReq struct { IDs []string `json:"ids" validate:"required,min=1,max=100"` diff --git a/pkg/tools/converter/converter.go b/pkg/tools/converter/converter.go index fe392b9b68..9fdbf0a4fa 100644 --- a/pkg/tools/converter/converter.go +++ b/pkg/tools/converter/converter.go @@ -98,6 +98,18 @@ func StringSliceToUint64Slice(source []string) []uint64 { return target } +// StringSliceToInt64Slice []string to []int64. +func StringSliceToInt64Slice(source []string) []int64 { + target := make([]int64, len(source)) + for index, one := range source { + int64Tmp, err := strconv.ParseInt(one, 10, 64) + if err == nil { + target[index] = int64Tmp + } + } + return target +} + // SliceToMap convert slice to map, use kvFunc to get key value pair for map. // // k, v := kvFunc(one) diff --git a/pkg/tools/rand/string.go b/pkg/tools/rand/string.go index 64b893ef3b..be7688bffb 100644 --- a/pkg/tools/rand/string.go +++ b/pkg/tools/rand/string.go @@ -20,18 +20,26 @@ package rand import ( + cryptoRand "crypto/rand" + "math/big" "math/rand" "time" + + "hcm/pkg/logs" ) var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") // String randomly generate a string of specified length. func String(n int) string { - randX := rand.New(rand.NewSource(time.Now().UnixNano())) b := make([]rune, n) for i := range b { - b[i] = letterRunes[randX.Intn(len(letterRunes))] + num, err := cryptoRand.Int(cryptoRand.Reader, big.NewInt(int64(len(letterRunes)))) + if err != nil { + logs.Errorf("rand.Int failed: %v", err) + num = big.NewInt(0) + } + b[i] = letterRunes[num.Int64()] } return string(b)