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 }