初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user