362 lines
12 KiB
Go
362 lines
12 KiB
Go
package oci
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/oracle/oci-go-sdk/v65/common"
|
|
"github.com/oracle/oci-go-sdk/v65/identity"
|
|
"github.com/oracle/oci-go-sdk/v65/limits"
|
|
)
|
|
|
|
// maxAvailabilityLookups 限制附带用量查询的条数:
|
|
// GetResourceAvailability 每条配额一次调用(OCI 无批量接口),
|
|
// 定义预筛 + 并发查询后 100 条约需 1~3 秒。
|
|
const maxAvailabilityLookups = 100
|
|
|
|
// limitsPageSize 是 limits 系列 List 接口的显式分页大小(OCI 通用上限 1000);
|
|
// 不传时服务端默认约百条一页,全量拉取要反复翻页。
|
|
const limitsPageSize = 1000
|
|
|
|
// availabilityConcurrency 是用量查询并发度;Limits 端点 TPS 不高,并发过高易撞限流。
|
|
const availabilityConcurrency = 8
|
|
|
|
// limitDefTTL 是配额定义缓存时长:定义(是否支持用量查询)几乎不随时间变化。
|
|
const limitDefTTL = 6 * time.Hour
|
|
|
|
// RegionSubscription 是租户对一个区域的订阅状态。
|
|
type RegionSubscription struct {
|
|
Key string `json:"key"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
IsHomeRegion bool `json:"isHomeRegion"`
|
|
}
|
|
|
|
// LimitValue 是一条服务配额;Used/Available 仅在请求用量时填充,
|
|
// 且 OCI 对部分配额不提供用量数据,此时保持为 nil。
|
|
type LimitValue struct {
|
|
Name string `json:"name"`
|
|
ScopeType string `json:"scopeType"`
|
|
AvailabilityDomain string `json:"availabilityDomain,omitempty"`
|
|
Value int64 `json:"value"`
|
|
Used *int64 `json:"used,omitempty"`
|
|
Available *int64 `json:"available,omitempty"`
|
|
}
|
|
|
|
// LimitsQuery 是一次配额查询的条件。
|
|
type LimitsQuery struct {
|
|
Region string // 目标区域,空则用凭据默认区域
|
|
Service string // 服务名,如 compute
|
|
Name string // 可选,配额名模糊过滤(大小写不敏感子串,本地过滤)
|
|
ScopeType string // 可选 GLOBAL / REGION / AD,下推给 OCI 过滤
|
|
AvailabilityDomain string // 可选,AD 全名,下推给 OCI 过滤
|
|
NonZero bool // 过滤掉上限为 0 的配额项(在用量查询前生效,不占并发名额)
|
|
WithAvailability bool // 并发附带 used/available
|
|
}
|
|
|
|
// LimitService 是配额体系支持的一个服务。
|
|
type LimitService struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// ListRegionSubscriptions 实现 Client。
|
|
func (c *RealClient) ListRegionSubscriptions(ctx context.Context, cred Credentials) ([]RegionSubscription, error) {
|
|
ic, err := identity.NewIdentityClientWithConfigurationProvider(provider(cred))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new identity client: %w", err)
|
|
}
|
|
applyProxy(&ic.BaseClient, cred)
|
|
resp, err := ic.ListRegionSubscriptions(ctx, identity.ListRegionSubscriptionsRequest{
|
|
TenancyId: &cred.TenancyOCID,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list region subscriptions: %w", err)
|
|
}
|
|
subs := make([]RegionSubscription, 0, len(resp.Items))
|
|
for _, item := range resp.Items {
|
|
subs = append(subs, RegionSubscription{
|
|
Key: deref(item.RegionKey),
|
|
Name: deref(item.RegionName),
|
|
Status: string(item.Status),
|
|
IsHomeRegion: item.IsHomeRegion != nil && *item.IsHomeRegion,
|
|
})
|
|
}
|
|
SortRegionSubscriptions(subs)
|
|
return subs, nil
|
|
}
|
|
|
|
// SortRegionSubscriptions 主区域置顶,其余按友好名(alias)字典序,
|
|
// 本地表查不到 alias 时回退区域名;实时接口与缓存读取共用,全局统一展示顺序。
|
|
func SortRegionSubscriptions(subs []RegionSubscription) {
|
|
sortKey := func(sub RegionSubscription) string {
|
|
if info, ok := RegionByKey(sub.Key); ok {
|
|
return info.Alias
|
|
}
|
|
return sub.Name
|
|
}
|
|
sort.SliceStable(subs, func(i, j int) bool {
|
|
if subs[i].IsHomeRegion != subs[j].IsHomeRegion {
|
|
return subs[i].IsHomeRegion
|
|
}
|
|
return sortKey(subs[i]) < sortKey(subs[j])
|
|
})
|
|
}
|
|
|
|
// SubscribeRegion 实现 Client:订阅新区域。OCI 要求该请求发往
|
|
// home region,且区域订阅一经创建不可取消。
|
|
func (c *RealClient) SubscribeRegion(ctx context.Context, cred Credentials, homeRegion, regionKey string) error {
|
|
ic, err := identity.NewIdentityClientWithConfigurationProvider(provider(cred))
|
|
if err != nil {
|
|
return fmt.Errorf("new identity client: %w", err)
|
|
}
|
|
applyProxy(&ic.BaseClient, cred)
|
|
if homeRegion != "" {
|
|
ic.SetRegion(normalizeRegion(homeRegion))
|
|
}
|
|
_, err = ic.CreateRegionSubscription(ctx, identity.CreateRegionSubscriptionRequest{
|
|
TenancyId: &cred.TenancyOCID,
|
|
CreateRegionSubscriptionDetails: identity.CreateRegionSubscriptionDetails{
|
|
RegionKey: ®ionKey,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("create region subscription %s: %w", regionKey, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// limitsClient 构造 limits 服务客户端;region 为空时用凭据默认区域。
|
|
func limitsClient(cred Credentials, region string) (limits.LimitsClient, error) {
|
|
lc, err := limits.NewLimitsClientWithConfigurationProvider(provider(cred))
|
|
if err != nil {
|
|
return lc, fmt.Errorf("new limits client: %w", err)
|
|
}
|
|
applyProxy(&lc.BaseClient, cred)
|
|
if region != "" {
|
|
lc.SetRegion(normalizeRegion(region))
|
|
}
|
|
return lc, nil
|
|
}
|
|
|
|
// ListLimits 实现 Client:全量拉取配额(scopeType / AD 条件下推),
|
|
// 名称本地子串过滤,按需并发附带用量。
|
|
func (c *RealClient) ListLimits(ctx context.Context, cred Credentials, q LimitsQuery) ([]LimitValue, error) {
|
|
lc, err := limitsClient(cred, q.Region)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values, err := listLimitValues(ctx, lc, cred.TenancyOCID, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values = filterLimitsByName(values, q.Name)
|
|
if q.NonZero {
|
|
values = filterNonZero(values)
|
|
}
|
|
if !q.WithAvailability {
|
|
return values, nil
|
|
}
|
|
if len(values) > maxAvailabilityLookups {
|
|
return nil, fmt.Errorf("list limits: %d entries exceed availability lookup cap %d, narrow with name filter",
|
|
len(values), maxAvailabilityLookups)
|
|
}
|
|
supported := c.availabilitySupport(ctx, lc, cred.TenancyOCID, q.Region, q.Service)
|
|
attachAvailability(ctx, lc, cred.TenancyOCID, q.Service, values, supported)
|
|
return values, nil
|
|
}
|
|
|
|
// listLimitValues 分页取回一个服务的配额;scopeType / availabilityDomain 下推给 OCI。
|
|
// OCI 的 name 参数只支持精确匹配,子串过滤由调用方本地完成。
|
|
func listLimitValues(ctx context.Context, lc limits.LimitsClient, tenancyOCID string, q LimitsQuery) ([]LimitValue, error) {
|
|
var values []LimitValue
|
|
var page *string
|
|
for {
|
|
req := limits.ListLimitValuesRequest{
|
|
CompartmentId: &tenancyOCID,
|
|
ServiceName: &q.Service,
|
|
Limit: common.Int(limitsPageSize),
|
|
Page: page,
|
|
}
|
|
if q.ScopeType != "" {
|
|
req.ScopeType = limits.ListLimitValuesScopeTypeEnum(q.ScopeType)
|
|
}
|
|
if q.AvailabilityDomain != "" {
|
|
req.AvailabilityDomain = &q.AvailabilityDomain
|
|
}
|
|
resp, err := lc.ListLimitValues(ctx, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list limit values of %s: %w", q.Service, err)
|
|
}
|
|
for _, item := range resp.Items {
|
|
values = append(values, toLimitValue(item))
|
|
}
|
|
if resp.OpcNextPage == nil {
|
|
return orEmpty(values), nil
|
|
}
|
|
page = resp.OpcNextPage
|
|
}
|
|
}
|
|
|
|
func toLimitValue(item limits.LimitValueSummary) LimitValue {
|
|
var value int64
|
|
if item.Value != nil {
|
|
value = *item.Value
|
|
}
|
|
return LimitValue{
|
|
Name: deref(item.Name),
|
|
ScopeType: string(item.ScopeType),
|
|
AvailabilityDomain: deref(item.AvailabilityDomain),
|
|
Value: value,
|
|
}
|
|
}
|
|
|
|
// filterLimitsByName 按大小写不敏感的子串匹配过滤配额名。
|
|
func filterLimitsByName(values []LimitValue, name string) []LimitValue {
|
|
if name == "" {
|
|
return values
|
|
}
|
|
needle := strings.ToLower(strings.TrimSpace(name))
|
|
filtered := make([]LimitValue, 0, len(values))
|
|
for _, v := range values {
|
|
if strings.Contains(strings.ToLower(v.Name), needle) {
|
|
filtered = append(filtered, v)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
// filterNonZero 过滤掉上限为 0 的配额项。
|
|
func filterNonZero(values []LimitValue) []LimitValue {
|
|
filtered := make([]LimitValue, 0, len(values))
|
|
for _, v := range values {
|
|
if v.Value > 0 {
|
|
filtered = append(filtered, v)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
// ListLimitServices 实现 Client:分页列出配额体系支持的全部服务。
|
|
func (c *RealClient) ListLimitServices(ctx context.Context, cred Credentials, region string) ([]LimitService, error) {
|
|
lc, err := limitsClient(cred, region)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var services []LimitService
|
|
var page *string
|
|
for {
|
|
resp, err := lc.ListServices(ctx, limits.ListServicesRequest{
|
|
CompartmentId: &cred.TenancyOCID,
|
|
Limit: common.Int(limitsPageSize),
|
|
Page: page,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list limit services: %w", err)
|
|
}
|
|
for _, item := range resp.Items {
|
|
services = append(services, LimitService{
|
|
Name: deref(item.Name),
|
|
Description: deref(item.Description),
|
|
})
|
|
}
|
|
if resp.OpcNextPage == nil {
|
|
return orEmpty(services), nil
|
|
}
|
|
page = resp.OpcNextPage
|
|
}
|
|
}
|
|
|
|
// limitDefEntry 是一份配额定义预筛结果的缓存条目。
|
|
type limitDefEntry struct {
|
|
expires time.Time
|
|
supported map[string]bool
|
|
}
|
|
|
|
// availabilitySupport 返回配额名 → 是否支持用量查询的映射(带 TTL 缓存);
|
|
// 拉取失败时返回 nil,调用方视为全部照查(保守降级)。
|
|
func (c *RealClient) availabilitySupport(ctx context.Context, lc limits.LimitsClient, tenancyOCID, region, service string) map[string]bool {
|
|
key := tenancyOCID + "|" + region + "|" + service
|
|
if v, ok := c.limitDefs.Load(key); ok {
|
|
if e := v.(limitDefEntry); time.Now().Before(e.expires) {
|
|
return e.supported
|
|
}
|
|
}
|
|
supported, err := listLimitDefinitions(ctx, lc, tenancyOCID, service)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
c.limitDefs.Store(key, limitDefEntry{expires: time.Now().Add(limitDefTTL), supported: supported})
|
|
return supported
|
|
}
|
|
|
|
// listLimitDefinitions 分页取回一个服务的配额定义,产出配额名 → 是否支持用量查询的映射;
|
|
// 字段缺失(nil)视为支持,仅明确 false 的会被预筛跳过。
|
|
func listLimitDefinitions(ctx context.Context, lc limits.LimitsClient, tenancyOCID, service string) (map[string]bool, error) {
|
|
supported := map[string]bool{}
|
|
var page *string
|
|
for {
|
|
resp, err := lc.ListLimitDefinitions(ctx, limits.ListLimitDefinitionsRequest{
|
|
CompartmentId: &tenancyOCID,
|
|
ServiceName: &service,
|
|
Limit: common.Int(limitsPageSize),
|
|
Page: page,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list limit definitions of %s: %w", service, err)
|
|
}
|
|
for _, item := range resp.Items {
|
|
if item.Name == nil {
|
|
continue
|
|
}
|
|
supported[*item.Name] = item.IsResourceAvailabilitySupported == nil || *item.IsResourceAvailabilitySupported
|
|
}
|
|
if resp.OpcNextPage == nil {
|
|
return supported, nil
|
|
}
|
|
page = resp.OpcNextPage
|
|
}
|
|
}
|
|
|
|
// attachAvailability 并发查询用量:预筛掉明确不支持用量查询的配额,
|
|
// 单条失败不影响整体,字段保持 nil。
|
|
func attachAvailability(ctx context.Context, lc limits.LimitsClient, tenancyOCID, service string, values []LimitValue, supported map[string]bool) {
|
|
sem := make(chan struct{}, availabilityConcurrency)
|
|
var wg sync.WaitGroup
|
|
for i := range values {
|
|
if s, ok := supported[values[i].Name]; ok && !s {
|
|
continue
|
|
}
|
|
wg.Add(1)
|
|
sem <- struct{}{}
|
|
go func(v *LimitValue) {
|
|
defer wg.Done()
|
|
defer func() { <-sem }()
|
|
fetchAvailability(ctx, lc, tenancyOCID, service, v)
|
|
}(&values[i])
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// fetchAvailability 查询单条配额的用量;AD 级配额必须带 availabilityDomain。
|
|
func fetchAvailability(ctx context.Context, lc limits.LimitsClient, tenancyOCID, service string, v *LimitValue) {
|
|
req := limits.GetResourceAvailabilityRequest{
|
|
ServiceName: &service,
|
|
LimitName: &v.Name,
|
|
CompartmentId: &tenancyOCID,
|
|
}
|
|
if strings.EqualFold(v.ScopeType, "AD") {
|
|
req.AvailabilityDomain = &v.AvailabilityDomain
|
|
}
|
|
resp, err := lc.GetResourceAvailability(ctx, req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
v.Used = resp.Used
|
|
v.Available = resp.Available
|
|
}
|