Support subscriptions, atom payments, credits, and optimize debt systems (#5545)

* add credits

* create table

* change name & add table name

* optimize interface

* print result err log

* fix

* add credits for debt

* creditsTransaction add region

* refactor debt status:
Normal\LowBalance\CriticalBalance\Debt\DebtDeletion\Final Period.

* refactor debt status:
Normal\LowBalance\CriticalBalance\Debt\DebtDeletion\Final Period.

* add retry transaction

* optimize determine status

* support card pay & subscription plan & operator service

* add pay notify & redirect url

* fix

* optimize

* add error test log

* fix req time

* fix

* fix

* add subscription payment api

* only processes the `CAPTURE_RESULT` notify type

* fix

* fix

* fixed notify structure parsing

* fix

* fix

* init a Free initial subscription plan √/ Initialize credits Create √

* init resourcequota wiht subscription

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix card type

* fix

* Subscribe controller: real-time monitoring subscriptionTransaction processing created/upgrade/downgrade

* fix

* fix

* fix

* determine current debt status with subscription

* optimize pay api resp

* process subscription auto renewal

* optimize log

* fix

* fix

* add GetAppResourceCosts api

* fix

* add GetLastSubscriptionTransaction api

* add GetSubscriptionUpgradeAmount api

* add /subscription/flush-quota api

* fix

* add remote flush subscription quota

* go mod tidy

* fix

* add retry flush subscription quota

* add custom debt status send email body

* fix

* fix

* fix

* fix

* fix

* add free process subscription

* fix

* add default pay expire time

* optimize

* support kyc & optimize

* optimiz

* add descributed lock with gorm db

* optimize debt reconcile

* optimize send email

* optimize split sms vms code map; handler failure reflush debt status.

* optimize

* add email username

* optimize

* skip zero user

* optimize flush svc

* add convert test func

* fix

* optimize log

* use the k8s client with cache

* fix golang ci lint
This commit is contained in:
Jiahui
2025-05-06 16:56:25 +08:00
committed by GitHub
parent 969d0a8c9e
commit 8c2db08fd4
71 changed files with 11421 additions and 1491 deletions
+8
View File
@@ -40,6 +40,13 @@ const (
DaySecond = 24 * 60 * 60
)
const (
LowBalancePeriod DebtStatusType = "LowBalancePeriod"
CriticalBalancePeriod DebtStatusType = "CriticalBalancePeriod"
DebtPeriod DebtStatusType = "DebtPeriod"
DebtDeletionPeriod DebtStatusType = "DebtDeletionPeriod"
)
type DebtStatusType string
var DefaultDebtConfig = map[DebtStatusType]int64{
@@ -53,6 +60,7 @@ const DebtNamespaceAnnoStatusKey = "debt.sealos/status"
const (
NormalDebtNamespaceAnnoStatus = "Normal"
SuspendDebtNamespaceAnnoStatus = "Suspend"
FinalDeletionDebtNamespaceAnnoStatus = "FinalDeletion"
ResumeDebtNamespaceAnnoStatus = "Resume"
TerminateSuspendDebtNamespaceAnnoStatus = "TerminateSuspend"
)
+10 -5
View File
@@ -151,13 +151,18 @@ func (d *DebtValidate) checkOption(ctx context.Context, logger logr.Logger, c cl
return admission.ValidationResponse(false, fmt.Sprintf("this namespace is not user namespace %s,or have not create", ns.Name))
}
logger.V(1).Info("check user namespace", "ns", ns.Name, "user", user)
account, err := d.AccountV2.GetAccount(&pkgtype.UserQueryOpts{Owner: user})
userUID, err := d.AccountV2.GetUserUID(&pkgtype.UserQueryOpts{Owner: user})
if err != nil {
logger.Error(err, "get user error", "user", user)
return admission.ValidationResponse(true, err.Error())
}
account, err := d.AccountV2.GetAccountWithCredits(userUID)
if err != nil {
logger.Error(err, "get account error", "user", user)
return admission.ValidationResponse(true, err.Error())
}
if account.Balance < account.DeductionBalance {
return admission.ValidationResponse(false, fmt.Sprintf(code.MessageFormat, code.InsufficientBalance, fmt.Sprintf("account balance less than 0,now account is %.2f¥. Please recharge the user %s.", GetAccountDebtBalance(*account), user)))
if account.Balance+account.UsableCredits <= account.DeductionBalance {
return admission.ValidationResponse(false, fmt.Sprintf(code.MessageFormat, code.InsufficientBalance, fmt.Sprintf("account balance less than 0,now account is %.2f¥. Please recharge the user %s.", GetAccountDebtBalance(account), user)))
}
return admission.Allowed(fmt.Sprintf("pass user %s , namespace %s", user, ns.Name))
}
@@ -166,8 +171,8 @@ func isDefaultQuotaName(name string) bool {
return strings.HasPrefix(name, "quota-") || name == debtLimit0QuotaName
}
func GetAccountDebtBalance(account pkgtype.Account) float64 {
return account2.GetCurrencyBalance(account.Balance - account.DeductionBalance)
func GetAccountDebtBalance(account *pkgtype.UsableBalanceWithCredits) float64 {
return account2.GetCurrencyBalance(account.Balance + account.UsableCredits - account.DeductionBalance)
}
const debtLimit0QuotaName = "debt-limit0"
@@ -26,6 +26,14 @@ import (
"strings"
"time"
"github.com/sirupsen/logrus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/labring/sealos/controllers/pkg/utils"
corev1 "k8s.io/api/core/v1"
"go.mongodb.org/mongo-driver/bson/primitive"
"github.com/google/uuid"
@@ -89,20 +97,34 @@ const (
DEFAULTACCOUNTNAMESPACE = "sealos-system"
RECHARGEGIFT = "recharge-gift"
SEALOS = "sealos"
EnvSubscriptionEnabled = "SUBSCRIPTION_ENABLED"
EnvJwtSecret = "ACCOUNT_API_JWT_SECRET"
EnvDesktopJwtSecret = "DESKTOP_API_JWT_SECRET"
)
var SubscriptionEnabled = false
// AccountReconciler reconciles an Account object
type AccountReconciler struct {
client.Client
AccountV2 database.AccountV2
Scheme *runtime.Scheme
Logger logr.Logger
AccountSystemNamespace string
DBClient database.Account
CVMDBClient database.CVM
MongoDBURI string
Activities pkgtypes.Activities
DefaultDiscount pkgtypes.RechargeDiscount
AccountV2 database.AccountV2
InitUserAccountFunc func(user *pkgtypes.UserQueryOpts) (*pkgtypes.Account, error)
Scheme *runtime.Scheme
Logger logr.Logger
accountSystemNamespace string
DBClient database.Account
CVMDBClient database.CVM
MongoDBURI string
Activities pkgtypes.Activities
DefaultDiscount pkgtypes.RechargeDiscount
SubscriptionQuotaLimit map[string]corev1.ResourceList
SyncNSQuotaFunc func(ctx context.Context, owner, nsName string) error
SkipExpiredUserTimeDuration time.Duration
localDomain string
allRegionDomain []string
jwtManager *utils.JWTManager
desktopJwtManager *utils.JWTManager
}
//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts,verbs=get;list;watch;create;update;patch;delete
@@ -126,7 +148,7 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
// determine the resource quota created by the owner user and the resource quota initialized by the account user,
// and only the resource quota created by the team user
_, err = r.syncAccount(ctx, owner, "ns-"+user.Name)
if errors.Is(err, gorm.ErrRecordNotFound) && user.CreationTimestamp.Add(20*24*time.Hour).Before(time.Now()) {
if errors.Is(err, gorm.ErrRecordNotFound) && user.CreationTimestamp.Add(r.SkipExpiredUserTimeDuration).Before(time.Now()) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
@@ -137,24 +159,127 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
return ctrl.Result{}, nil
}
func (r *AccountReconciler) syncAccount(ctx context.Context, owner string, userNamespace string) (*pkgtypes.Account, error) {
if err := r.syncResourceQuotaAndLimitRange(ctx, userNamespace); err != nil {
r.Logger.Error(err, "sync resource resourceQuota and limitRange failed")
}
func (r *AccountReconciler) syncAccount(ctx context.Context, owner string, userNamespace string) (account *pkgtypes.Account, err error) {
//if err := r.adaptEphemeralStorageLimitRange(ctx, userNamespace); err != nil {
// r.Logger.Error(err, "adapt ephemeral storage limitRange failed")
//}
if getUsername(userNamespace) != owner {
return nil, nil
if getUsername(userNamespace) == owner {
user, err := r.AccountV2.GetUser(&pkgtypes.UserQueryOpts{Owner: owner, IgnoreEmpty: true})
if err != nil {
return nil, err
}
if user == nil {
return nil, gorm.ErrRecordNotFound
}
account, err = r.InitUserAccountFunc(&pkgtypes.UserQueryOpts{Owner: owner})
if err != nil {
return nil, err
}
if err := r.syncDebt(ctx, owner); err != nil {
return nil, fmt.Errorf("sync user debt failed: %v", err)
}
}
account, err := r.AccountV2.NewAccount(&pkgtypes.UserQueryOpts{Owner: owner})
if err != nil {
return nil, err
if err = r.SyncNSQuotaFunc(ctx, owner, userNamespace); err != nil {
r.Logger.Error(err, "sync resource resourceQuota and limitRange failed")
}
return account, nil
return
}
func (r *AccountReconciler) syncResourceQuotaAndLimitRange(ctx context.Context, nsName string) error {
func (r *AccountReconciler) syncDebt(ctx context.Context, owner string) error {
userUID, err := r.AccountV2.GetUserUID(&pkgtypes.UserQueryOpts{Owner: owner})
if err != nil {
return fmt.Errorf("get userUID failed: %v", err)
}
var count int64
err = r.AccountV2.GetGlobalDB().Model(&pkgtypes.Debt{}).Where("user_uid = ?", userUID).Count(&count).Error
if err != nil {
return fmt.Errorf("check user debt existence failed: %v", err)
}
if count <= 0 {
createDebt, err := r.initializeDebt(ctx, owner, userUID)
if err != nil {
return fmt.Errorf("initialize user debt failed: %v", err)
}
if err = r.AccountV2.GetGlobalDB().Create(createDebt).Error; err != nil {
return fmt.Errorf("create user debt failed: %v", err)
}
}
return nil
}
func (r *AccountReconciler) initializeDebt(ctx context.Context, owner string, userUID uuid.UUID) (*pkgtypes.Debt, error) {
debtCr := &accountv1.Debt{}
err := r.Get(ctx, client.ObjectKey{Namespace: r.accountSystemNamespace, Name: "debt-" + owner}, debtCr)
if err != nil {
if !apierrors.IsNotFound(err) {
return nil, fmt.Errorf("failed to get user debt from CR: %v", err)
}
return &pkgtypes.Debt{
UserUID: userUID,
AccountDebtStatus: pkgtypes.NormalPeriod,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}, nil
}
return convertDebtCrToDebt(debtCr, userUID), nil
}
func convertDebtCrToDebt(debtCr *accountv1.Debt, userUID uuid.UUID) *pkgtypes.Debt {
debt := &pkgtypes.Debt{
UserUID: userUID,
AccountDebtStatus: convertDebtStatus(debtCr.Status.AccountDebtStatus),
CreatedAt: debtCr.CreationTimestamp.Time.UTC(),
}
if debtCr.Status.LastUpdateTimestamp > 0 {
debt.UpdatedAt = time.Unix(debtCr.Status.LastUpdateTimestamp, 0).UTC()
} else {
debt.UpdatedAt = debtCr.CreationTimestamp.Time.UTC()
}
statusRecords := make([]pkgtypes.DebtStatusRecord, len(debtCr.Status.DebtStatusRecords))
for i, record := range debtCr.Status.DebtStatusRecords {
statusRecords[i] = pkgtypes.DebtStatusRecord{
ID: uuid.New(),
UserUID: userUID,
LastStatus: convertDebtStatus(record.LastStatus),
CurrentStatus: convertDebtStatus(record.CurrentStatus),
CreateAt: record.UpdateTime.UTC(),
}
}
debt.StatusRecords = statusRecords
return debt
}
func convertDebtStatus(statusType accountv1.DebtStatusType) pkgtypes.DebtStatusType {
switch statusType {
case accountv1.NormalPeriod:
return pkgtypes.NormalPeriod
case accountv1.WarningPeriod:
return pkgtypes.DebtPeriod
case accountv1.ApproachingDeletionPeriod:
return pkgtypes.DebtPeriod
case accountv1.ImminentDeletionPeriod:
return pkgtypes.DebtDeletionPeriod
case accountv1.LowBalancePeriod:
return pkgtypes.LowBalancePeriod
case accountv1.CriticalBalancePeriod:
return pkgtypes.CriticalBalancePeriod
case accountv1.DebtPeriod:
return pkgtypes.DebtPeriod
case accountv1.DebtDeletionPeriod:
return pkgtypes.DebtDeletionPeriod
case accountv1.FinalDeletionPeriod:
return pkgtypes.FinalDeletionPeriod
case "":
return pkgtypes.NormalPeriod
default:
logrus.Errorf("unknown debt status type: %v", statusType)
return ""
}
}
func (r *AccountReconciler) syncResourceQuotaAndLimitRange(ctx context.Context, _, nsName string) error {
objs := []client.Object{client.Object(resources.GetDefaultLimitRange(nsName, nsName)), client.Object(resources.GetDefaultResourceQuota(nsName, ResourceQuotaPrefix+nsName))}
for i := range objs {
err := retry.Retry(10, 1*time.Second, func() error {
@@ -170,6 +295,46 @@ func (r *AccountReconciler) syncResourceQuotaAndLimitRange(ctx context.Context,
return nil
}
func (r *AccountReconciler) syncResourceQuotaAndLimitRangeBySubscription(ctx context.Context, owner, nsName string) error {
userUID, err := r.AccountV2.GetUserUID(&pkgtypes.UserQueryOpts{Owner: owner})
if err != nil {
return fmt.Errorf("get userUID failed: %v", err)
}
userSub, err := r.AccountV2.GetSubscription(&pkgtypes.UserQueryOpts{UID: userUID})
if err != nil {
return fmt.Errorf("get user subscription failed: %v", err)
}
quota, ok := r.SubscriptionQuotaLimit[userSub.PlanName]
if !ok {
return fmt.Errorf("subscription plan %s not found", userSub.PlanName)
}
objs := []client.Object{client.Object(resources.GetDefaultLimitRange(nsName, nsName)), client.Object(getDefaultResourceQuota(nsName, ResourceQuotaPrefix+nsName, quota))}
for i := range objs {
err := retry.Retry(10, 1*time.Second, func() error {
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, objs[i], func() error {
return nil
})
return err
})
if err != nil {
return fmt.Errorf("sync resource %T failed: %v", objs[i], err)
}
}
return nil
}
func getDefaultResourceQuota(ns, name string, hard corev1.ResourceList) *corev1.ResourceQuota {
return &corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
Spec: corev1.ResourceQuotaSpec{
Hard: hard,
},
}
}
//func (r *AccountReconciler) adaptEphemeralStorageLimitRange(ctx context.Context, nsName string) error {
// limit := resources.GetDefaultLimitRange(nsName, nsName)
// return retry.Retry(10, 1*time.Second, func() error {
@@ -190,7 +355,44 @@ func (r *AccountReconciler) syncResourceQuotaAndLimitRange(ctx context.Context,
// SetupWithManager sets up the controller with the Manager.
func (r *AccountReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error {
r.Logger = ctrl.Log.WithName("account_controller")
r.AccountSystemNamespace = env.GetEnvWithDefault(ACCOUNTNAMESPACEENV, DEFAULTACCOUNTNAMESPACE)
r.accountSystemNamespace = env.GetEnvWithDefault(accountv1.AccountSystemNamespaceEnv, "account-system")
regions, err := r.AccountV2.GetRegions()
if err != nil {
return fmt.Errorf("get regions failed: %v", err)
}
r.allRegionDomain = make([]string, len(regions))
for i, region := range regions {
r.allRegionDomain[i] = region.Domain
}
r.localDomain = r.AccountV2.GetLocalRegion().Domain
r.jwtManager = utils.NewJWTManager(os.Getenv(EnvJwtSecret), 10*time.Minute)
SubscriptionEnabled = os.Getenv(EnvSubscriptionEnabled) == trueStatus
if SubscriptionEnabled {
r.InitUserAccountFunc = r.AccountV2.NewAccountWithFreeSubscriptionPlan
r.SyncNSQuotaFunc = r.syncResourceQuotaAndLimitRangeBySubscription
plans, err := r.AccountV2.GetSubscriptionPlanList()
if err != nil {
return fmt.Errorf("get subscription plan list failed: %v", err)
}
if len(plans) == 0 {
return fmt.Errorf("subscription plan list is empty")
}
r.SubscriptionQuotaLimit, err = resources.ParseResourceLimitWithSubscription(plans)
if err != nil {
return fmt.Errorf("parse resource limit with subscription failed: %v", err)
}
for plan, limit := range r.SubscriptionQuotaLimit {
r.Logger.Info("subscription plan", "name", plan, "quota", limit)
}
// manager 添加 subscription controller
if err := mgr.Add(NewSubscriptionProcessor(r)); err != nil {
return fmt.Errorf("add subscription processor failed: %v", err)
}
r.desktopJwtManager = utils.NewJWTManager(os.Getenv(EnvDesktopJwtSecret), 10*time.Minute)
} else {
r.InitUserAccountFunc = r.AccountV2.NewAccount
r.SyncNSQuotaFunc = r.syncResourceQuotaAndLimitRange
}
return ctrl.NewControllerManagedBy(mgr).
For(&userv1.User{}, builder.WithPredicates(OnlyCreatePredicate{})).
WithOptions(rateOpts).
@@ -329,3 +531,7 @@ func (r *AccountReconciler) BillingCVM() error {
}
return nil
}
func init() {
SubscriptionEnabled = os.Getenv(EnvSubscriptionEnabled) == trueStatus
}
@@ -19,9 +19,15 @@ package controllers
import (
"context"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/labring/sealos/controllers/pkg/utils/maps"
"k8s.io/client-go/rest"
"k8s.io/client-go/kubernetes/scheme"
@@ -50,28 +56,41 @@ type BillingTaskRunner struct {
*BillingReconciler
}
func (r *BillingTaskRunner) Start(ctx context.Context) error {
if err := r.ExecuteBillingTask(); err != nil {
r.Logger.Error(err, "failed to execute billing task")
}
defer func() {
r.Logger.Info("stop billing reconcile", "time", time.Now().Format(time.RFC3339))
}()
now := time.Now()
nextHour := now.Truncate(time.Hour).Add(time.Hour).Add(5 * time.Minute)
r.Logger.Info("next billing reconcile time", "time", nextHour.Format(time.RFC3339))
time.Sleep(nextHour.Sub(now))
var DebtUserMap *maps.ConcurrentNullValueMap
func (r *BillingTaskRunner) Start(ctx context.Context) error {
defer func() {
r.Logger.Info("stopping billing reconcile", "time", time.Now().Format(time.RFC3339))
}()
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := r.ExecuteBillingTask(); err != nil {
r.Logger.Error(err, "failed to execute billing task")
}
case <-ctx.Done():
return nil
default:
now := time.Now()
minutesLeft := 60 - now.Minute()
// Execute if 30 or more minutes remain, else wait for next hour
if minutesLeft >= 30 {
if err := r.ExecuteBillingTask(); err != nil {
r.Logger.Error(err, "failed to execute billing task")
}
}
// Calculate sleep duration to next hour + 5 minutes
nextHour := now.Truncate(time.Hour).Add(time.Hour).Add(5 * time.Minute)
sleepDuration := nextHour.Sub(now)
r.Logger.Info("next billing reconcile time", "time", nextHour.Format(time.RFC3339))
// Sleep until next scheduled time or context cancellation
select {
case <-time.After(sleepDuration):
continue
case <-ctx.Done():
return nil
}
}
}
}
@@ -88,14 +107,23 @@ type BillingReconciler struct {
client.Client
Scheme *runtime.Scheme
logr.Logger
DBClient database.Account
AccountV2 database.AccountV2
Properties *resources.PropertyTypeLS
concurrentLimit int64
DBClient database.Account
AccountV2 database.AccountV2
Properties *resources.PropertyTypeLS
reconcileBillingFunc func(owner string, billings []*resources.Billing) error
concurrentLimit int64
DebtUserMap *maps.ConcurrentMap
}
func (r *BillingReconciler) ExecuteBillingTask() error {
r.Logger.Info("start billing reconcile", "time", time.Now().Format(time.RFC3339))
DebtUserMap = maps.NewConcurrentNullValueMap()
var users []string
if err := r.AccountV2.GetGlobalDB().Model(&types.Debt{}).Where("account_debt_status IN (?, ?, ?) ", types.DebtPeriod, types.DebtDeletionPeriod, types.FinalDeletionPeriod).
Distinct("user_uid").Pluck("user_uid", &users).Error; err != nil {
return fmt.Errorf("failed to query unique users: %w", err)
}
DebtUserMap.Set(users...)
ownerListMap, err := r.getRecentUsedOwners()
if err != nil {
return fmt.Errorf("failed to get the owner list of the recently used resource: %w", err)
@@ -111,44 +139,58 @@ func (r *BillingReconciler) ExecuteBillingTask() error {
func (r *BillingReconciler) reconcileOwnerList(ownerListMap map[string][]string, now time.Time) error {
endHourTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.Local).UTC()
startHourTime := endHourTime.Add(-1 * time.Hour)
var ownerList, failedList []string
var ownerList []string
for owner := range ownerListMap {
ownerList = append(ownerList, owner)
}
updateOwnerList, err := r.DBClient.GetOwnersRecentUpdates(ownerList, endHourTime)
ownersRecentUpdates, err := r.DBClient.GetOwnersRecentUpdates(ownerList, endHourTime)
if err != nil {
return fmt.Errorf("get owners without recent updates failed: %w", err)
}
// remove the owner that does not need to be updated
for _, owner := range updateOwnerList {
// remove the owner that does not need to be updated; final State The user deletes the service at any time and does not perform billing processing
for _, owner := range append(ownersRecentUpdates, DebtUserMap.GetAllKey()...) {
delete(ownerListMap, owner)
}
r.Logger.Info("get owners recent updates", "already update owner count", len(updateOwnerList), "remaining owner count", len(ownerListMap))
r.Logger.Info("get owners recent updates", "already update owner count", len(ownersRecentUpdates), "remaining owner count", len(ownerListMap))
ownerBillings, err := r.DBClient.GenerateBillingData(startHourTime, endHourTime, r.Properties, ownerListMap)
if err != nil {
return fmt.Errorf("generate billing data failed: %w", err)
}
r.Logger.Info("generate billing data", "count", len(ownerBillings))
type result struct {
owner string
err error
}
workers := make(chan struct{}, r.concurrentLimit)
resultChan := make(chan result, len(ownerBillings))
var wg sync.WaitGroup
for owner, billings := range ownerBillings {
amount := int64(0)
orderIDs := make([]string, 0, len(billings))
for _, billing := range billings {
amount += billing.Amount
orderIDs = append(orderIDs, billing.OrderID)
}
if err = r.DBClient.SaveBillings(billings...); err != nil {
r.Logger.Error(err, "save billings failed", "owner", owner, "amount", amount)
failedList = append(failedList, owner)
if len(billings) == 0 {
continue
}
if err := r.rechargeBalance(owner, amount); err != nil {
r.Logger.Error(err, "recharge balance failed", "owner", owner, "amount", amount)
failedList = append(failedList, owner)
if err := r.DBClient.UpdateBillingStatus(orderIDs, resources.Unsettled); err != nil {
r.Logger.Error(err, "update billing unsettled status failed", "orderIDs", orderIDs)
wg.Add(1)
go func(owner string, billings []*resources.Billing) {
defer wg.Done()
workers <- struct{}{}
defer func() {
<-workers
}()
reconcileErr := r.reconcileBillingFunc(owner, billings)
if reconcileErr != nil {
r.Logger.Error(reconcileErr, "failed to reconcile owner", "owner", owner, "billings", billings)
}
resultChan <- result{owner: owner, err: reconcileErr}
}(owner, billings)
}
wg.Wait()
close(resultChan)
var failedList []string
for res := range resultChan {
if res.err != nil {
failedList = append(failedList, res.owner)
}
}
if len(failedList) > 0 {
@@ -157,6 +199,49 @@ func (r *BillingReconciler) reconcileOwnerList(ownerListMap map[string][]string,
return nil
}
func (r *BillingReconciler) reconcileBilling(owner string, billings []*resources.Billing) error {
amount := int64(0)
orderIDs := make([]string, 0, len(billings))
for _, billing := range billings {
amount += billing.Amount
orderIDs = append(orderIDs, billing.OrderID)
}
if err := r.DBClient.SaveBillings(billings...); err != nil {
return fmt.Errorf("save billings failed: %w", err)
}
if err := r.rechargeBalance(owner, amount); err != nil {
r.Logger.Error(err, "recharge balance failed", "owner", owner, "amount", amount)
if updateErr := r.DBClient.UpdateBillingStatus(orderIDs, resources.Unsettled); updateErr != nil {
r.Logger.Error(updateErr, "update billing unsettled status failed", "orderIDs", orderIDs)
}
return fmt.Errorf("recharge balance failed: %w", err)
}
return nil
}
func (r *BillingReconciler) reconcileBillingWithCredits(owner string, billings []*resources.Billing) error {
amount := int64(0)
orderIDs := make([]string, 0, len(billings))
for _, billing := range billings {
amount += billing.Amount
orderIDs = append(orderIDs, billing.OrderID)
}
if amount <= 0 {
return nil
}
if err := r.DBClient.SaveBillings(billings...); err != nil {
return fmt.Errorf("save billings failed: %w", err)
}
if err := r.AccountV2.AddDeductionBalanceWithCredits(&types.UserQueryOpts{Owner: owner}, amount, orderIDs); err != nil {
r.Logger.Error(err, "AddDeductionBalanceWithCredits failed", "owner", owner, "amount", amount)
if updateErr := r.DBClient.UpdateBillingStatus(orderIDs, resources.Unsettled); updateErr != nil {
r.Logger.Error(updateErr, "update billing unsettled status failed", "owner", owner, "amount", amount, "orderIDs", orderIDs)
}
return fmt.Errorf("recharge balance failed: %w", err)
}
return nil
}
// reconcileOwnerListBatch process ownerlistmap in batch mode
func (r *BillingReconciler) reconcileOwnerListBatch(
ownerListMap map[string][]string, // The owner -> namespaces mapping needs to be handled
@@ -221,6 +306,19 @@ func (r *BillingReconciler) getRecentUsedOwners() (map[string][]string, error) {
for _, ns := range namespaceList {
if owner, ok := nsToOwnerMap[ns]; ok {
if _, ok := usedOwnerList[owner]; !ok {
userUID, err := r.AccountV2.GetUserUID(&types.UserQueryOpts{Owner: owner, IgnoreEmpty: true})
if err != nil {
return nil, fmt.Errorf("get user uid failed: %w", err)
}
if userUID == uuid.Nil {
r.Logger.Error(fmt.Errorf("user uid is nil"), "get user uid failed", "owner", owner)
continue
}
_, inDebt := DebtUserMap.Get(userUID.String())
if inDebt {
//r.Logger.Info("user is in debt", "user uid", userUID.String())
continue
}
usedOwnerList[owner] = []string{}
}
usedOwnerList[owner] = append(usedOwnerList[owner], ns)
@@ -239,7 +337,11 @@ func (r *BillingReconciler) Init() error {
if err := r.DBClient.CreateBillingIfNotExist(); err != nil {
return fmt.Errorf("create billing collection failed: %w", err)
}
r.concurrentLimit = env.GetInt64EnvWithDefault("BILLING_CONCURRENT_LIMIT", 100)
r.concurrentLimit = env.GetInt64EnvWithDefault("BILLING_CONCURRENT_LIMIT", 10)
r.reconcileBillingFunc = r.reconcileBilling
if os.Getenv("CREDITS_ENABLED") == "true" || os.Getenv("SUBSCRIPTION_ENABLED") == "true" {
r.reconcileBillingFunc = r.reconcileBillingWithCredits
}
return nil
}
+588
View File
@@ -0,0 +1,588 @@
package controllers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"strconv"
"strings"
"sync"
"text/template"
"time"
utils2 "github.com/labring/sealos/controllers/account/controllers/utils"
client2 "github.com/alibabacloud-go/dysmsapi-20170525/v3/client"
"github.com/alibabacloud-go/tea/tea"
dlock "github.com/labring/sealos/controllers/pkg/utils/lock"
"github.com/labring/sealos/controllers/pkg/utils"
v1 "github.com/labring/sealos/controllers/account/api/v1"
"github.com/google/uuid"
"github.com/labring/sealos/controllers/pkg/types"
"gorm.io/gorm"
)
func (r *DebtReconciler) Start(ctx context.Context) error {
lock := dlock.NewDistributedLock(r.AccountV2.GetGlobalDB(), "debt_reconciler", r.processID)
if err := lock.TryLock(context.Background(), 15*time.Second); err != nil {
if err == dlock.ErrLockNotAcquired {
time.Sleep(5 * time.Second)
return r.Start(ctx)
}
}
defer func() {
if err := lock.Unlock(); err != nil {
log.Printf("failed to unlock: %v", err)
}
}()
log.Printf("debt reconciler lock acquired, process ID: %s", r.processID)
r.start()
log.Printf("debt reconciler started")
return nil
}
func (r *DebtReconciler) start() {
db := r.AccountV2.GetGlobalDB()
var wg sync.WaitGroup
// 1.1 account update processing
wg.Add(1)
go func() {
defer wg.Done()
r.processWithTimeRange(&types.Account{}, "updated_at", 1*time.Minute, 24*time.Hour, func(db *gorm.DB, start, end time.Time) {
users := getUniqueUsers(db, &types.Account{}, "updated_at", start, end)
if len(users) > 0 {
r.Logger.Info("processed account updates", "count", len(users), "start", start, "end", end)
r.processUsersInParallel(users)
}
})
}()
// 1.2 the arrears are transferred to the clearing state
wg.Add(1)
go func() {
defer wg.Done()
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
var users []uuid.UUID
if err := db.Model(&types.Debt{}).Where("account_debt_status = ? AND updated_at < ?", types.DebtPeriod, time.Now().UTC().Add(-7*24*time.Hour)).
Distinct("user_uid").Pluck("user_uid", &users).Error; err != nil {
r.Logger.Error(err, "failed to query unique users", "account_debt_status", types.DebtPeriod, "updated_at", time.Now().Add(-7*24*time.Hour))
continue
}
if len(users) > 0 {
r.processUsersInParallel(users)
r.Logger.Info("processed debt status", "count", len(users), "updated_at", time.Now().Add(-7*24*time.Hour))
}
}
}()
// 1.3 clearing changes to delete state
wg.Add(1)
go func() {
defer wg.Done()
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
var users []uuid.UUID
if err := db.Model(&types.Debt{}).Where("account_debt_status = ? AND updated_at < ?", types.DebtDeletionPeriod, time.Now().UTC().Add(-7*24*time.Hour)).
Distinct("user_uid").Pluck("user_uid", &users).Error; err != nil {
r.Logger.Error(err, "failed to query unique users", "account_debt_status", types.DebtPeriod, "updated_at", time.Now().Add(-7*24*time.Hour))
continue
}
if len(users) > 0 {
r.processUsersInParallel(users)
r.Logger.Info("processed debt status", "count", len(users), "updated_at", time.Now().Add(-7*24*time.Hour))
}
}
}()
// 2.1 recharge record processing
//wg.Add(1)
//go func() {
// defer wg.Done()
// r.processWithTimeRange(&types.Payment{}, "created_at", 1*time.Minute, 24*time.Hour, func(db *gorm.DB, start, end time.Time) {
// users := getUniqueUsers(db, &types.Payment{}, "created_at", start, end)
// if len(users) > 0 {
// r.processUsersInParallel(users)
// r.Logger.Info("processed payment records", "count", len(users), "users", users, "start", start, "end", end)
// }
// })
//}()
// 2.2 subscription change processing
wg.Add(1)
go func() {
defer wg.Done()
r.processWithTimeRange(&types.Subscription{}, "update_at", 1*time.Minute, 24*time.Hour, func(db *gorm.DB, start, end time.Time) {
users := getUniqueUsers(db, &types.Subscription{}, "update_at", start, end)
if len(users) > 0 {
r.processUsersInParallel(users)
r.Logger.Info("processed subscription changes", "count", len(users), "users", users, "start", start, "end", end)
}
})
}()
// 2.3 credits refresh processing
wg.Add(1)
go func() {
defer wg.Done()
r.processWithTimeRange(&types.Credits{}, "created_at", 1*time.Minute, 24*time.Hour, func(db *gorm.DB, start, end time.Time) {
users := getUniqueUsers(db, &types.Credits{}, "created_at", start, end)
if len(users) > 0 {
r.processUsersInParallel(users)
r.Logger.Info("processed credits refresh", "count", len(users), "users", users, "start", start, "end", end)
}
})
}()
// 3 retry failed users
wg.Add(1)
go func() {
defer wg.Done()
r.retryFailedUsers()
}()
wg.Wait()
}
func (r *DebtReconciler) RefreshDebtStatus(userUID uuid.UUID) error {
return r.refreshDebtStatus(userUID, false)
}
func (r *DebtReconciler) refreshDebtStatus(userUID uuid.UUID, skipSendMsg bool) error {
account, err := r.AccountV2.GetAccountWithCredits(userUID)
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to get account %s: %v", userUID, err)
}
if account == nil {
return fmt.Errorf("account %s not found", userUID)
}
debt := types.Debt{}
err = r.AccountV2.GetGlobalDB().Model(&types.Debt{}).Where("user_uid = ?", userUID).First(&debt).Error
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to get debt %s: %v", userUID, err)
}
if err == gorm.ErrRecordNotFound {
return nil
}
isBasicUser := account.Balance <= 10*BaseUnit
oweamount := account.Balance - account.DeductionBalance + account.UsableCredits
//update interval seconds
updateIntervalSeconds := time.Now().UTC().Unix() - debt.UpdatedAt.UTC().Unix()
lastStatus := debt.AccountDebtStatus
update := false
if lastStatus == "" {
lastStatus = types.NormalPeriod
update = true
}
currentStatusRaw, err := r.DetermineCurrentStatus(oweamount, account.UserUID, updateIntervalSeconds, v1.DebtStatusType(lastStatus))
if err != nil {
return fmt.Errorf("failed to determine current status for user %s: %v", userUID, err)
}
currentStatus := types.DebtStatusType(currentStatusRaw)
if lastStatus == currentStatus && !update {
return nil
}
if lastStatus != currentStatus {
if err := r.sendFlushDebtResourceStatusRequest(AdminFlushResourceStatusReq{
UserUID: userUID,
LastDebtStatus: lastStatus,
CurrentDebtStatus: currentStatus,
IsBasicUser: isBasicUser,
}); err != nil {
return fmt.Errorf("failed to send flush resource status request: %w", err)
}
}
switch lastStatus {
case types.NormalPeriod, types.LowBalancePeriod, types.CriticalBalancePeriod:
if types.ContainDebtStatus(types.DebtStates, currentStatus) {
// resume user account
if err = r.ResumeBalance(userUID); err != nil {
return fmt.Errorf("failed to resume balance: %w", err)
}
}
if types.StatusMap[currentStatus] > types.StatusMap[lastStatus] {
//TODO send sms
if !skipSendMsg && account.Balance > 0 {
if err := r.SendUserDebtMsg(userUID, oweamount, currentStatus, isBasicUser); err != nil {
return NewErrSendMsg(err, userUID)
}
}
}
case types.DebtPeriod, types.DebtDeletionPeriod, types.FinalDeletionPeriod: // The current status may be: (Normal, LowBalance, CriticalBalance) Period [Service needs to be restored], DebtDeletionPeriod [Service suspended]
if types.ContainDebtStatus(types.DebtStates, currentStatus) {
if err = r.ResumeBalance(userUID); err != nil {
return fmt.Errorf("failed to resume balance: %w", err)
}
}
if currentStatus != types.FinalDeletionPeriod {
// TODO send sms
if !skipSendMsg && account.Balance > 0 {
if err := r.SendUserDebtMsg(userUID, oweamount, currentStatus, isBasicUser); err != nil {
return fmt.Errorf("failed to send user debt message: %w", err)
}
}
}
}
r.Logger.V(1).Info("update debt status", "account", debt.UserUID,
"last status", lastStatus, "last update time", debt.UpdatedAt.Format(time.RFC3339),
"current status", debt.AccountDebtStatus, "time", time.Now().UTC().Format(time.RFC3339))
debt.AccountDebtStatus = currentStatus
debt.UpdatedAt = time.Now()
debtRecord := types.DebtStatusRecord{
ID: uuid.New(),
UserUID: userUID,
LastStatus: lastStatus,
CurrentStatus: currentStatus,
CreateAt: time.Now().UTC(),
}
err = r.AccountV2.GlobalTransactionHandler(func(tx *gorm.DB) error {
dErr := tx.Model(&types.Debt{}).Where("user_uid = ?", userUID).Save(debt).Error
if dErr != nil {
return fmt.Errorf("failed to save debt: %w", dErr)
}
sErr := tx.Model(&types.DebtStatusRecord{}).Create(&debtRecord).Error
if sErr != nil {
return fmt.Errorf("failed to save debt status record: %w", sErr)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to save debt status: %w", err)
}
return nil
}
func (r *DebtReconciler) ResumeBalance(userUID uuid.UUID) error {
account, err := r.AccountV2.GetAccount(&types.UserQueryOpts{UID: userUID})
if err != nil {
return fmt.Errorf("failed to get account %s: %w", userUID, err)
}
if account.DeductionBalance <= account.Balance {
return nil
}
err = r.AccountV2.GlobalTransactionHandler(func(tx *gorm.DB) error {
result := tx.Model(&types.Account{}).Where(`"userUid" = ?`, userUID).Where(`"deduction_balance" > "balance"`).Updates(map[string]interface{}{
"deduction_balance": gorm.Expr("balance"),
})
if result.Error != nil {
return fmt.Errorf("failed to update account balance: %w", result.Error)
}
if result.RowsAffected > 0 {
return tx.Create(&types.DebtResumeDeductionBalanceTransaction{
UserUID: userUID,
BeforeDeductionBalance: account.DeductionBalance,
AfterDeductionBalance: account.Balance,
BeforeBalance: account.Balance,
}).Error
}
return nil
})
if err != nil {
return fmt.Errorf("failed to update account balance: %w", err)
}
return nil
}
type ErrSendMsg struct {
UserUID uuid.UUID `json:"userUID" bson:"userUID"`
Err error `json:"err" bson:"err"`
}
func NewErrSendMsg(err error, userUID uuid.UUID) error {
return ErrSendMsg{
UserUID: userUID,
Err: err,
}
}
func (e ErrSendMsg) Error() string {
return fmt.Sprintf("failed to send message to user %s: %v", e.UserUID, e.Err)
}
func (r *DebtReconciler) SendUserDebtMsg(userUID uuid.UUID, oweamount int64, currentStatus types.DebtStatusType, isBasicUser bool) error {
if r.SmsConfig == nil && r.VmsConfig == nil && r.smtpConfig == nil {
return nil
}
emailTmpl, ok := r.SendDebtStatusEmailBody[v1.DebtStatusType(currentStatus)]
if !ok {
return nil
}
if isBasicUser && currentStatus == types.LowBalancePeriod {
return nil
}
_user, err := r.AccountV2.GetUser(&types.UserQueryOpts{UID: userUID})
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
// skip abnormal user
if _user.Status != types.UserStatusNormal {
return nil
}
outh, err := r.AccountV2.GetUserOauthProvider(&types.UserQueryOpts{UID: _user.UID, ID: _user.ID})
if err != nil {
return fmt.Errorf("failed to get user oauth provider: %w", err)
}
phone, email := "", ""
for i := range outh {
if outh[i].ProviderType == types.OauthProviderTypePhone {
phone = outh[i].ProviderID
} else if outh[i].ProviderType == types.OauthProviderTypeEmail {
email = outh[i].ProviderID
}
}
fmt.Printf("user: %s, phone: %s, email: %s\n", userUID, phone, email)
if phone != "" {
if r.SmsConfig != nil && r.SmsConfig.SmsCode[string(currentStatus)] != "" {
oweamount := strconv.FormatInt(int64(math.Abs(math.Ceil(float64(oweamount)/1_000_000))), 10)
err = utils2.SendSms(r.SmsConfig.Client, &client2.SendSmsRequest{
PhoneNumbers: tea.String(phone),
SignName: tea.String(r.SmsConfig.SmsSignName),
TemplateCode: tea.String(r.SmsConfig.SmsCode[string(currentStatus)]),
// ownAmount/1_000_000
TemplateParam: tea.String("{\"user_id\":\"" + userUID.String() + "\",\"oweamount\":\"" + oweamount + "\"}"),
})
if err != nil {
return fmt.Errorf("failed to send sms notice: %w", err)
}
}
if r.VmsConfig != nil && types.ContainDebtStatus(types.DebtStates, currentStatus) && r.VmsConfig.TemplateCode[string(currentStatus)] != "" {
err = utils2.SendVms(phone, r.VmsConfig.TemplateCode[string(currentStatus)], r.VmsConfig.NumberPoll, GetSendVmsTimeInUTCPlus8(time.Now()), forbidTimes)
if err != nil {
return fmt.Errorf("failed to send vms notice: %w", err)
}
}
}
if r.smtpConfig != nil && email != "" {
var emailBody string
var emailSubject = "Low Account Balance Reminder"
if SubscriptionEnabled {
var userInfo types.UserInfo
err = r.AccountV2.GetGlobalDB().Where(types.UserInfo{UserUID: userUID}).Find(&userInfo).Error
if err != nil {
return fmt.Errorf("failed to get user info: %w", err)
}
emailRender := &utils.EmailDebtRender{
Type: string(currentStatus),
CurrentStatus: currentStatus,
Domain: r.AccountV2.GetLocalRegion().Domain,
}
if types.ContainDebtStatus(types.DebtStates, currentStatus) {
if oweamount <= 0 {
emailRender.GraceReason = []string{string(utils.GraceReasonNoBalance)}
} else {
emailRender.GraceReason = []string{string(utils.GraceReasonSubExpired)}
}
}
emailRender.SetUserInfo(&userInfo)
tmp, err := template.New("debt-reconcile").Parse(emailTmpl)
if err != nil {
return fmt.Errorf("failed to parse email template: %w", err)
}
var rendered bytes.Buffer
if err = tmp.Execute(&rendered, emailRender.Build()); err != nil {
return fmt.Errorf("failed to render email template: %w", err)
}
emailBody = rendered.String()
emailSubject = emailRender.GetSubject()
} else {
emailBody = emailTmpl
}
if err = r.smtpConfig.SendEmailWithTitle(emailSubject, emailBody, email); err != nil {
return fmt.Errorf("failed to send email notice: %w", err)
}
}
return nil
}
type AdminFlushResourceStatusReq struct {
UserUID uuid.UUID `json:"userUID" bson:"userUID"`
LastDebtStatus types.DebtStatusType `json:"lastDebtStatus" bson:"lastDebtStatus"`
CurrentDebtStatus types.DebtStatusType `json:"currentDebtStatus" bson:"currentDebtStatus"`
IsBasicUser bool `json:"isBasicUser" bson:"isBasicUser"`
}
// TODO flush desktop message (send or read) && flush resource quota (suspend or resume or delete)
func (r *DebtReconciler) sendFlushDebtResourceStatusRequest(quotaReq AdminFlushResourceStatusReq) error {
for _, domain := range r.allRegionDomain {
token, err := r.jwtManager.GenerateToken(utils.JwtUser{
Requester: AdminUserName,
})
if err != nil {
return fmt.Errorf("failed to generate token: %w", err)
}
prefix := "https://"
if strings.Contains(domain, "nip.io") {
prefix = "http://"
}
url := fmt.Sprintf(prefix+"account-api.%s/admin/v1alpha1/flush-debt-resource-status", domain)
quotaReqBody, err := json.Marshal(quotaReq)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
var lastErr error
backoffTime := time.Second
maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ {
req, err := http.NewRequest("POST", url, bytes.NewBuffer(quotaReqBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
lastErr = fmt.Errorf("failed to send request: %w", err)
} else {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
lastErr = nil
break
}
body, err := io.ReadAll(resp.Body)
if err != nil {
lastErr = fmt.Errorf("unexpected status code: %d, failed to read response body: %w", resp.StatusCode, err)
} else {
lastErr = fmt.Errorf("unexpected status code: %d, response body: %s", resp.StatusCode, string(body))
}
}
// 进行重试
if attempt < maxRetries {
fmt.Printf("Attempt %d failed: %v. Retrying in %v...\n", attempt, lastErr, backoffTime)
time.Sleep(backoffTime)
backoffTime *= 2 // 指数增长退避时间
}
}
if lastErr != nil {
return fmt.Errorf("failed to send %s request after %d attempts: %w", url, maxRetries, lastErr)
}
}
return nil
}
// 获取时间范围内的不重复用户 UUID
func getUniqueUsers(db *gorm.DB, table interface{}, timeField string, startTime, endTime time.Time) []uuid.UUID {
var users []uuid.UUID
switch table.(type) {
case *types.AccountTransaction, *types.Payment, *types.Account:
if err := db.Model(table).Where(fmt.Sprintf("%s BETWEEN ? AND ?", timeField), startTime, endTime).
Distinct(`"userUid"`).Pluck(`"userUid"`, &users).Error; err != nil {
log.Printf("failed to query unique users: %v", err)
return nil
}
default:
if err := db.Model(table).Where(fmt.Sprintf("%s BETWEEN ? AND ?", timeField), startTime, endTime).
Distinct("user_uid").Pluck("user_uid", &users).Error; err != nil {
log.Printf("failed to query unique users: %v", err)
return nil
}
}
return users
}
func (r *DebtReconciler) retryFailedUsers() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
var failedUsers []uuid.UUID
r.failedUserLocks.Range(func(key, value interface{}) bool {
userUID, ok := key.(uuid.UUID)
if ok {
failedUsers = append(failedUsers, userUID)
}
return true
})
if len(failedUsers) > 0 {
r.Logger.Info("retrying failed users", "count", len(failedUsers), "users", failedUsers)
r.processUsersInParallel(failedUsers)
}
}
}
// Parallel processing of user debt status, the same user simultaneously through the lock to implement a debt refresh processing.
func (r *DebtReconciler) processUsersInParallel(users []uuid.UUID) {
var (
wg sync.WaitGroup
semaphore = make(chan struct{}, 50)
)
for _, user := range users {
wg.Add(1)
semaphore <- struct{}{}
go func(u uuid.UUID) {
defer wg.Done()
defer func() { <-semaphore }()
lock, _ := r.userLocks.LoadOrStore(u, &sync.Mutex{})
mutex := lock.(*sync.Mutex)
if !mutex.TryLock() {
//r.Logger.V(1).Info("user debt processing skipped due to existing lock",
// "userUID", u)
return
}
defer mutex.Unlock()
if err := r.RefreshDebtStatus(u); err != nil {
r.Logger.Error(err, fmt.Sprintf("failed to refresh debt status for user %s", u))
sendMsgNumber := 1
if value, ok := r.failedUserLocks.LoadOrStore(u, sendMsgNumber); ok {
if sendMsgNumber, ok = value.(int); ok {
if sendMsgNumber >= 3 {
if err = r.refreshDebtStatus(u, true); err != nil {
r.Logger.Error(err, fmt.Sprintf("failed to refresh debt status for user %s", u))
} else {
r.failedUserLocks.Delete(u)
}
return
}
sendMsgNumber++
r.failedUserLocks.Store(u, sendMsgNumber)
}
}
} else {
r.failedUserLocks.Delete(u)
}
}(user)
}
wg.Wait()
}
// 时间区间轮询处理
func (r *DebtReconciler) processWithTimeRange(table interface{}, timeField string, interval time.Duration, initialDuration time.Duration, processFunc func(*gorm.DB, time.Time, time.Time)) {
// 首次处理
startTime := time.Now().Add(-initialDuration)
endTime := time.Now().Add(-2 * time.Minute)
users := getUniqueUsers(r.AccountV2.GetGlobalDB(), table, timeField, startTime, endTime)
r.processUsersInParallel(users)
r.Logger.Info("processed table updates", "table", fmt.Sprintf("%T", table), "count", len(users), "start", startTime, "end", endTime)
// 后续按时间区间轮询
lastEndTime := endTime
ticker := time.NewTicker(interval)
for range ticker.C {
startTime = lastEndTime
endTime = time.Now().Add(-interval)
processFunc(r.AccountV2.GetGlobalDB(), startTime, endTime)
lastEndTime = endTime
}
}
+374 -325
View File
@@ -20,21 +20,24 @@ import (
"context"
"errors"
"fmt"
"math"
"os"
"reflect"
runtime2 "runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/alibabacloud-go/tea/tea"
"github.com/labring/sealos/controllers/pkg/utils/maps"
"gorm.io/gorm"
"github.com/google/uuid"
"github.com/volcengine/volc-sdk-golang/service/vms"
"github.com/labring/sealos/controllers/pkg/pay"
"gorm.io/gorm"
"sigs.k8s.io/controller-runtime/pkg/handler"
"github.com/labring/sealos/controllers/pkg/database/cockroach"
@@ -83,6 +86,7 @@ const (
SMTPHostEnv = "SMTP_HOST"
SMTPPortEnv = "SMTP_PORT"
SMTPFromEnv = "SMTP_FROM"
SMTPUserEnv = "SMTP_USER"
SMTPPasswordEnv = "SMTP_PASSWORD"
SMTPTitleEnv = "SMTP_TITLE"
)
@@ -90,26 +94,35 @@ const (
// DebtReconciler reconciles a Debt object
type DebtReconciler struct {
client.Client
AccountV2 database.AccountV2
Scheme *runtime.Scheme
DebtDetectionCycle time.Duration
LocalRegionID string
*AccountReconciler
AccountV2 database.AccountV2
InitUserAccountFunc func(user *pkgtypes.UserQueryOpts) (*pkgtypes.Account, error)
Scheme *runtime.Scheme
DebtDetectionCycle time.Duration
LocalRegionID string
logr.Logger
accountSystemNamespace string
SmsConfig *SmsConfig
VmsConfig *VmsConfig
smtpConfig *utils.SMTPConfig
DebtUserMap *maps.ConcurrentMap
// TODO need init
userLocks *sync.Map
failedUserLocks *sync.Map
processID string
SkipExpiredUserTimeDuration time.Duration
SendDebtStatusEmailBody map[accountv1.DebtStatusType]string
}
type VmsConfig struct {
TemplateCode map[int]string
TemplateCode map[string]string
NumberPoll string
}
type SmsConfig struct {
Client *client2.Client
SmsSignName string
SmsCode map[int]string
SmsCode map[string]string
}
var DebtConfig = accountv1.DefaultDebtConfig
@@ -138,26 +151,18 @@ func (r *DebtReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
} else if client.IgnoreNotFound(err) != nil {
return ctrl.Result{}, fmt.Errorf("failed to get payment %s: %v", req.Name, err)
} else {
cr, err := r.AccountV2.GetUserCr(&pkgtypes.UserQueryOpts{Owner: req.NamespacedName.Name})
userID, err := r.AccountV2.GetUserID(&pkgtypes.UserQueryOpts{Owner: req.NamespacedName.Name, IgnoreEmpty: true})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
r.Logger.Info("user cr not exist, skip", "user", req.NamespacedName.Name)
return ctrl.Result{}, nil
}
return ctrl.Result{RequeueAfter: 10 * time.Minute}, fmt.Errorf("failed to get user cr %s: %v", req.NamespacedName.Name, err)
return ctrl.Result{RequeueAfter: 10 * time.Minute}, fmt.Errorf("failed to get user id %s: %v", req.NamespacedName.Name, err)
}
user, err := r.AccountV2.GetUser(&pkgtypes.UserQueryOpts{Owner: req.NamespacedName.Name, UID: cr.UserUID})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
r.Logger.Info("user not exist, skip", "user", req.NamespacedName.Name)
return ctrl.Result{}, nil
}
return ctrl.Result{RequeueAfter: 10 * time.Minute}, fmt.Errorf("failed to get user %s: %v", req.NamespacedName.Name, err)
if userID == "" {
r.Logger.Info("user id not exist, skip", "user", req.NamespacedName.Name)
return ctrl.Result{}, nil
}
reconcileErr = r.reconcile(ctx, req.NamespacedName.Name, user.ID)
reconcileErr = r.reconcile(ctx, req.NamespacedName.Name, userID)
}
if reconcileErr != nil {
if reconcileErr == ErrAccountNotExist {
if reconcileErr == ErrAccountNotExist || reconcileErr == ErrDebtNotExist {
return ctrl.Result{RequeueAfter: 10 * time.Minute}, nil
}
r.Logger.Error(reconcileErr, "reconcile debt error")
@@ -166,52 +171,39 @@ func (r *DebtReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
return ctrl.Result{RequeueAfter: r.DebtDetectionCycle}, nil
}
//func (r *DebtReconciler) getNamespaceOwner(namespace string) (string, error) {
// ns := &corev1.Namespace{}
// if err := r.Get(context.Background(), client.ObjectKey{Name: namespace}, ns); err != nil {
// return "", fmt.Errorf("failed to get namespace %s: %v", namespace, err)
// }
// if ns.Labels == nil {
// return "", fmt.Errorf("namespace %s labels is nil", namespace)
// }
// owner, ok := ns.Labels[userv1.UserAnnotationOwnerKey]
// if !ok {
// return "", fmt.Errorf("namespace %s owner is not exist", namespace)
// }
// return owner, nil
//}
func (r *DebtReconciler) reconcile(ctx context.Context, userCr, userID string) error {
debt := &accountv1.Debt{}
userQueryOpts := &pkgtypes.UserQueryOpts{Owner: userCr, ID: userID}
account, err := r.AccountV2.GetAccount(userQueryOpts)
ops := &pkgtypes.UserQueryOpts{Owner: userCr, ID: userID, IgnoreEmpty: true}
userUID, err := r.AccountV2.GetUserUID(&pkgtypes.UserQueryOpts{Owner: userCr, IgnoreEmpty: true})
if err != nil {
return fmt.Errorf("failed to get user uid %s: %v", userCr, err)
}
if userUID == uuid.Nil {
r.Logger.Info("user uid not exist, skip", "user", userCr)
return nil
}
ops.UID = userUID
account, err := r.AccountV2.GetAccountWithCredits(userUID)
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to get account %s: %v", userCr, err)
}
// if account not exist, create account
if account == nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
_, err = r.AccountV2.NewAccount(userQueryOpts)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("failed to create account %v: %v", userQueryOpts, err)
}
userOwner := &userv1.User{}
if err := r.Get(ctx, types.NamespacedName{Name: userCr, Namespace: r.accountSystemNamespace}, userOwner); err != nil {
// if user not exist, skip
if client.IgnoreNotFound(err) == nil {
return nil
}
return fmt.Errorf("failed to get usercr %s: %v", userCr, err)
}
userOwner := &userv1.User{}
if err := r.Get(ctx, types.NamespacedName{Name: userCr, Namespace: r.accountSystemNamespace}, userOwner); err != nil {
// if user not exist, skip
if userOwner.CreationTimestamp.Add(20 * 24 * time.Hour).Before(time.Now()) {
if client.IgnoreNotFound(err) == nil {
return nil
}
return fmt.Errorf("failed to get usercr %s: %v", userCr, err)
}
// if user not exist, skip
if userOwner.CreationTimestamp.Add(r.SkipExpiredUserTimeDuration).Before(time.Now()) {
return nil
}
_, err = r.InitUserAccountFunc(ops)
if err != nil {
r.Logger.Error(fmt.Errorf("account %v not exist", userQueryOpts), err.Error())
}
return ErrAccountNotExist
}
if account.CreateRegionID == "" {
if err = r.AccountV2.SetAccountCreateLocalRegion(account, r.LocalRegionID); err != nil {
return fmt.Errorf("failed to set account %v create region: %v", userQueryOpts, err)
return fmt.Errorf("failed to create account %s: %v", userCr, err)
}
}
// In a multi-region scenario, select the region where the account is created for SMS notification
@@ -224,6 +216,7 @@ func (r *DebtReconciler) reconcile(ctx context.Context, userCr, userID string) e
if err := r.syncDebt(ctx, userCr, userID, debt); err != nil {
return err
}
return ErrDebtNotExist
//r.Logger.Info("create or update debt success", "debt", debt)
}
// backward compatibility
@@ -260,6 +253,26 @@ func getOwnNsList(clt client.Client, user string) ([]string, error) {
var ErrAccountNotExist = errors.New("account not exist")
var ErrDebtNotExist = errors.New("debt not exist")
const (
NormalPeriod = iota
LowBalancePeriod
CriticalBalancePeriod
DebtPeriod
DebtDeletionPeriod
FinalDeletionPeriod
)
var statusMap = map[accountv1.DebtStatusType]int{
accountv1.NormalPeriod: NormalPeriod,
accountv1.LowBalancePeriod: LowBalancePeriod,
accountv1.CriticalBalancePeriod: CriticalBalancePeriod,
accountv1.DebtPeriod: DebtPeriod,
accountv1.DebtDeletionPeriod: DebtDeletionPeriod,
accountv1.FinalDeletionPeriod: FinalDeletionPeriod,
}
/*
NormalPeriod -> WarningPeriod -> ApproachingDeletionPeriod -> ImmediateDeletePeriod -> FinalDeletePeriod
正常期:账户余额大于等于0
@@ -270,135 +283,86 @@ NormalPeriod -> WarningPeriod -> ApproachingDeletionPeriod -> ImmediateDeletePer
欠费后到完全删除的总周期=WarningPeriodSeconds+ApproachingDeletionPeriodSeconds+ImmediateDeletePeriodSeconds+FinalDeletePeriodSeconds
*/
func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv1.Debt, account *pkgtypes.Account, userNamespaceList []string, smsEnable bool) error {
oweamount := account.Balance - account.DeductionBalance
func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv1.Debt, account *pkgtypes.UsableBalanceWithCredits, userNamespaceList []string, smsEnable bool) error {
// Basic users should avoid alarm notification affecting experience
isBasicUser := account.Balance <= 10*BaseUnit
oweamount := account.Balance - account.DeductionBalance + account.UsableCredits
//更新间隔秒钟数
updateIntervalSeconds := time.Now().UTC().Unix() - debt.Status.LastUpdateTimestamp
lastStatus := debt.Status
//userNamespace := GetUserNamespace(account.Name)
lastStatus := debt.Status.AccountDebtStatus
update := false
if lastStatus == "" {
lastStatus = accountv1.NormalPeriod
update = true
}
currentStatus, err := r.DetermineCurrentStatus(oweamount, account.UserUID, updateIntervalSeconds, lastStatus)
if err != nil {
return fmt.Errorf("failed to determine current status: %w", err)
}
r.updateDebtUserMap(debt.Spec.UserName, currentStatus)
if lastStatus == currentStatus && !update {
return nil
}
update = update || SetDebtStatus(debt, lastStatus, currentStatus)
nonDebtStates := []accountv1.DebtStatusType{accountv1.NormalPeriod, accountv1.LowBalancePeriod, accountv1.CriticalBalancePeriod}
debtStates := []accountv1.DebtStatusType{accountv1.DebtPeriod, accountv1.DebtDeletionPeriod, accountv1.FinalDeletionPeriod}
// 判断上次状态到当前的状态
switch lastStatus.AccountDebtStatus {
case accountv1.NormalPeriod:
/*
余额大于等于0:
正常期 -> 正常期: 无操作返回
余额小于0:
正常期 -> 警告期: 更新status 状态warning事件及更新事件,并发送警告消息通知
*/
if oweamount >= 0 {
return nil
}
update = SetDebtStatus(debt, accountv1.NormalPeriod, accountv1.WarningPeriod)
if err := r.sendWarningNotice(ctx, debt.Spec.UserName, oweamount, userNamespaceList, smsEnable); err != nil {
r.Logger.Error(err, "send warning notice error")
}
case accountv1.WarningPeriod:
/*
余额大于等于0
警告期 -> 正常期:更新status 状态normal事件及更新时间,撤销warning消息通知
余额小于0
上次更新时间小于临近删除时间, 且欠费小于总金额的一半:
警告期 -> 警告期:无操作返回
else
警告期 -> 临近删除期: 更新status 状态approachingDeletion事件及更新时间,发送临近删除消息通知
*/
if oweamount >= 0 {
update = SetDebtStatus(debt, accountv1.WarningPeriod, accountv1.NormalPeriod)
if err := r.readNotice(ctx, userNamespaceList, WarningNotice); err != nil {
r.Logger.Error(err, "readNotice WarningNotice error")
switch lastStatus {
case accountv1.NormalPeriod, accountv1.LowBalancePeriod, accountv1.CriticalBalancePeriod:
if statusMap[currentStatus] > statusMap[lastStatus] {
if err := r.sendDesktopNoticeAndSms(ctx, debt.Spec.UserName, oweamount, currentStatus, userNamespaceList, smsEnable, isBasicUser); err != nil {
r.Logger.Error(err, fmt.Sprintf("send %s notice error", currentStatus))
}
break
}
//上次更新时间小于临近删除时间
if updateIntervalSeconds < DebtConfig[accountv1.ApproachingDeletionPeriod] && (account.Balance/2)+oweamount > 0 {
return nil
}
update = SetDebtStatus(debt, accountv1.WarningPeriod, accountv1.ApproachingDeletionPeriod)
if err := r.sendApproachingDeletionNotice(ctx, debt.Spec.UserName, oweamount, userNamespaceList, smsEnable); err != nil {
r.Logger.Error(err, "sendApproachingDeletionNotice error")
}
case accountv1.ApproachingDeletionPeriod:
/*
余额大于0
临近删除期 -> 正常期:更新status 状态normal事件及更新时间,撤销临近删除消息通知
余额大于0
上次更新时间小于最终删除时间,且欠费不大于总金额:
临近删除期 -> 临近删除期:无操作返回
else
临近删除期 -> 即刻删除期: 执行暂停用户资源,更新status 状态imminentDeletionPeriod事件及更新时间,发送最终删除消息通知
*/
if oweamount >= 0 {
update = SetDebtStatus(debt, accountv1.ApproachingDeletionPeriod, accountv1.NormalPeriod)
if err := r.readNotice(ctx, userNamespaceList, ApproachingDeletionNotice, WarningNotice); err != nil {
r.Logger.Error(err, "readNotice ApproachingDeletionNotice error")
} else {
if err := r.readNotice(ctx, userNamespaceList, lastStatus); err != nil {
r.Logger.Error(err, "read low balance notice error")
}
break
}
if updateIntervalSeconds < DebtConfig[accountv1.ImminentDeletionPeriod] && account.Balance+oweamount > 0 {
return nil
}
update = SetDebtStatus(debt, accountv1.ApproachingDeletionPeriod, accountv1.ImminentDeletionPeriod)
if err := r.sendImminentDeletionNotice(ctx, debt.Spec.UserName, oweamount, userNamespaceList, smsEnable); err != nil {
r.Logger.Error(err, "sendImminentDeletionNotice error")
}
if err := r.SuspendUserResource(ctx, userNamespaceList); err != nil {
return err
}
case accountv1.ImminentDeletionPeriod:
/*
余额大于0
即刻删除期 -> 正常期:恢复用户资源,更新status 状态normal事件及更新时间,撤销最终删除消息通知
上次更新时间小于最终删除时间:
即刻删除期 -> 即刻删除期:无操作返回
else
即刻删除期 -> 最终删除期: 删除用户全部资源,更新status 状态finalDeletionPeriod事件及更新时间。发生最终删除消息通知
*/
if oweamount >= 0 {
update = SetDebtStatus(debt, accountv1.ImminentDeletionPeriod, accountv1.NormalPeriod)
// 恢复用户资源
if err := r.ResumeUserResource(ctx, userNamespaceList); err != nil {
if contains(debtStates, currentStatus) {
if err := r.SuspendUserResource(ctx, userNamespaceList); err != nil {
return err
}
if err := r.readNotice(ctx, userNamespaceList, ImminentDeletionNotice, ApproachingDeletionNotice, WarningNotice); err != nil {
r.Logger.Error(err, "readNotice ImminentDeletionNotice error")
}
//TODO update debt status
//if lastStatus == accountv1.FinalDeletionPeriod {
// err = r.AccountV2.GetGlobalDB().Save(&pkgtypes.UserDebt{
// UserID: debt.Spec.UserID,
// //Status: pkgtypes.DebtStatusNormal,
// }).Error
// if err != nil {
// return fmt.Errorf("failed to save user debt: %w", err)
// }
//}
case accountv1.DebtPeriod, accountv1.DebtDeletionPeriod, accountv1.FinalDeletionPeriod: // The current status may be: (Normal, LowBalance, CriticalBalance) Period [Service needs to be restored], DebtDeletionPeriod [Service suspended]
if contains(nonDebtStates, currentStatus) {
if err := r.readNotice(ctx, userNamespaceList, debtStates...); err != nil {
r.Logger.Error(err, "read low balance notice error")
}
break
}
//上次更新时间小于最终删除时间, 且欠费不大于总金额的两倍
if updateIntervalSeconds < DebtConfig[accountv1.FinalDeletionPeriod] {
return nil
}
// TODO 暂时只暂停资源,后续会添加真正删除全部资源逻辑, 或直接删除namespace
update = SetDebtStatus(debt, accountv1.ImminentDeletionPeriod, accountv1.FinalDeletionPeriod)
if err := r.sendFinalDeletionNotice(ctx, debt.Spec.UserName, oweamount, userNamespaceList, smsEnable); err != nil {
r.Error(err, "sendFinalDeletionNotice error")
}
if err := r.SuspendUserResource(ctx, userNamespaceList); err != nil {
return err
}
case accountv1.FinalDeletionPeriod:
/*
余额大于0
最终删除期 -> 正常期:更新status 状态normal事件及更新时间
*/
if oweamount >= 0 {
if err := r.readNotice(ctx, userNamespaceList, FinalDeletionNotice, ImminentDeletionNotice, ApproachingDeletionNotice, WarningNotice); err != nil {
r.Logger.Error(err, "readNotice FinalDeletionNotice error")
}
//TODO 用户从欠费到正常,是否需要发送消息通知
update = SetDebtStatus(debt, accountv1.FinalDeletionPeriod, accountv1.NormalPeriod)
// TODO 暂时非真正完全删除,仍可恢复用户资源,后续会添加真正删除全部资源逻辑,不在执行恢复逻辑
if err := r.ResumeUserResource(ctx, userNamespaceList); err != nil {
return err
}
break
}
if err := r.SuspendUserResource(ctx, userNamespaceList); err != nil {
return err
if currentStatus != accountv1.FinalDeletionPeriod {
err = r.sendDesktopNoticeAndSms(ctx, debt.Spec.UserName, oweamount, currentStatus, userNamespaceList, smsEnable, isBasicUser)
if err != nil {
r.Logger.Error(err, fmt.Sprintf("send %s notice error", currentStatus))
}
if err = r.SuspendUserResource(ctx, userNamespaceList); err != nil {
return err
}
} else {
// TODO DELETE
//err = r.AccountV2.GetGlobalDB().Save(&pkgtypes.UserDebt{
// UserID: debt.Spec.UserID,
// Status: pkgtypes.DebtStatusDeletionPeriod,
//}).Error
//if err != nil {
// return fmt.Errorf("failed to save user debt: %w", err)
//}
if err = r.DeleteUserResource(ctx, userNamespaceList); err != nil {
return err
}
}
//兼容老版本
default:
@@ -414,6 +378,96 @@ func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv
return nil
}
func contains(statuses []accountv1.DebtStatusType, status accountv1.DebtStatusType) bool {
for _, s := range statuses {
if s == status {
return true
}
}
return false
}
func (r *DebtReconciler) updateDebtUserMap(username string, currentStatus accountv1.DebtStatusType) {
isDebtState := currentStatus == accountv1.DebtPeriod || currentStatus == accountv1.DebtDeletionPeriod || currentStatus == accountv1.FinalDeletionPeriod
_, exists := r.DebtUserMap.Get(username)
if isDebtState && !exists {
r.DebtUserMap.Set(username, struct{}{})
} else if !isDebtState && exists {
r.DebtUserMap.Delete(username)
}
}
func newStatusConversion(debt *accountv1.Debt) bool {
switch debt.Status.AccountDebtStatus {
case accountv1.NormalPeriod, accountv1.FinalDeletionPeriod:
return false
case accountv1.WarningPeriod:
debt.Status.AccountDebtStatus = accountv1.DebtPeriod
case accountv1.ApproachingDeletionPeriod:
debt.Status.AccountDebtStatus = accountv1.DebtDeletionPeriod
case accountv1.ImminentDeletionPeriod:
debt.Status.AccountDebtStatus = accountv1.DebtDeletionPeriod
default:
debt.Status.AccountDebtStatus = accountv1.NormalPeriod
}
return true
}
func determineCurrentStatus(oweamount int64, updateIntervalSeconds int64, lastStatus accountv1.DebtStatusType) accountv1.DebtStatusType {
if oweamount > 0 {
if oweamount > 10*BaseUnit {
return accountv1.NormalPeriod
} else if oweamount > 5*BaseUnit {
return accountv1.LowBalancePeriod
}
return accountv1.CriticalBalancePeriod
}
if lastStatus == accountv1.NormalPeriod || lastStatus == accountv1.LowBalancePeriod || lastStatus == accountv1.CriticalBalancePeriod {
return accountv1.DebtPeriod
}
if lastStatus == accountv1.DebtPeriod && updateIntervalSeconds >= DebtConfig[accountv1.DebtDeletionPeriod] {
return accountv1.DebtDeletionPeriod
}
if lastStatus == accountv1.DebtDeletionPeriod && updateIntervalSeconds >= DebtConfig[accountv1.FinalDeletionPeriod] {
return accountv1.FinalDeletionPeriod
}
return lastStatus // Maintain current debt state if no transition
}
func (r *DebtReconciler) determineCurrentStatusWithSubscription(oweamount int64, userUID uuid.UUID, updateIntervalSeconds int64, lastStatus accountv1.DebtStatusType) (accountv1.DebtStatusType, error) {
userSubscription, err := r.AccountV2.GetSubscription(&pkgtypes.UserQueryOpts{UID: userUID})
if err != nil {
return accountv1.NormalPeriod, fmt.Errorf("failed to get user subscription: %w", err)
}
if oweamount > 0 && userSubscription.Status == pkgtypes.SubscriptionStatusNormal {
if oweamount >= 5*BaseUnit {
return accountv1.NormalPeriod, nil
} else if oweamount > 1*BaseUnit {
return accountv1.LowBalancePeriod, nil
}
return accountv1.CriticalBalancePeriod, nil
}
if lastStatus == accountv1.NormalPeriod || lastStatus == accountv1.LowBalancePeriod || lastStatus == accountv1.CriticalBalancePeriod {
return accountv1.DebtPeriod, nil
}
if lastStatus == accountv1.DebtPeriod && updateIntervalSeconds >= DebtConfig[accountv1.DebtDeletionPeriod] {
return accountv1.DebtDeletionPeriod, nil
}
if lastStatus == accountv1.DebtDeletionPeriod && updateIntervalSeconds >= DebtConfig[accountv1.FinalDeletionPeriod] {
return accountv1.FinalDeletionPeriod, nil
}
return lastStatus, nil // Maintain current debt state if no transition
}
func (r *DebtReconciler) DetermineCurrentStatus(oweamount int64, userUID uuid.UUID, updateIntervalSeconds int64, lastStatus accountv1.DebtStatusType) (accountv1.DebtStatusType, error) {
if SubscriptionEnabled {
return r.determineCurrentStatusWithSubscription(oweamount, userUID, updateIntervalSeconds, lastStatus)
}
return determineCurrentStatus(oweamount, updateIntervalSeconds, lastStatus), nil
}
func (r *DebtReconciler) syncDebt(ctx context.Context, owner, userID string, debt *accountv1.Debt) error {
debt.Name = GetDebtName(owner)
debt.Namespace = r.accountSystemNamespace
@@ -430,6 +484,9 @@ func (r *DebtReconciler) syncDebt(ctx context.Context, owner, userID string, deb
var MaxDebtHistoryStatusLength = env.GetIntEnvWithDefault("MAX_DEBT_HISTORY_STATUS_LENGTH", 10)
func SetDebtStatus(debt *accountv1.Debt, lastStatus, currentStatus accountv1.DebtStatusType) bool {
if lastStatus == currentStatus {
return false
}
debt.Status.AccountDebtStatus = currentStatus
now := time.Now().UTC()
debt.Status.LastUpdateTimestamp = now.Unix()
@@ -448,35 +505,10 @@ func SetDebtStatus(debt *accountv1.Debt, lastStatus, currentStatus accountv1.Deb
return true
}
func newStatusConversion(debt *accountv1.Debt) bool {
switch debt.Status.AccountDebtStatus {
case accountv1.PreWarningPeriod:
debt.Status.AccountDebtStatus = accountv1.NormalPeriod
case accountv1.SuspendPeriod:
debt.Status.AccountDebtStatus = accountv1.ImminentDeletionPeriod
case accountv1.RemovedPeriod:
debt.Status.AccountDebtStatus = accountv1.FinalDeletionPeriod
default:
debt.Status.AccountDebtStatus = accountv1.NormalPeriod
}
return true
}
func GetDebtName(AccountName string) string {
return fmt.Sprintf("%s%s", accountv1.DebtPrefix, AccountName)
}
func GetUserNamespace(AccountName string) string {
return "ns-" + AccountName
}
const (
WarningNotice = iota
ApproachingDeletionNotice
ImminentDeletionNotice
FinalDeletionNotice
)
const (
fromEn = "Debt-System"
fromZh = "欠费系统"
@@ -489,25 +521,24 @@ const (
)
var (
TitleTemplateZH = map[int]string{
WarningNotice: "欠费告警",
ApproachingDeletionNotice: "资源暂停告警",
ImminentDeletionNotice: "资源释放告警",
FinalDeletionNotice: "资源释放告警",
TitleTemplateZHMap = map[accountv1.DebtStatusType]string{
accountv1.LowBalancePeriod: "余额不足",
accountv1.CriticalBalancePeriod: "余额即将耗尽",
accountv1.DebtPeriod: "余额耗尽",
accountv1.DebtDeletionPeriod: "即将资源释放",
accountv1.FinalDeletionPeriod: "彻底资源释放",
}
TitleTemplateEN = map[int]string{
WarningNotice: "Debt Warning",
ApproachingDeletionNotice: "Resource Suspension Warning",
ImminentDeletionNotice: "Resource Release Warning",
FinalDeletionNotice: "Resource Release Warning",
TitleTemplateENMap = map[accountv1.DebtStatusType]string{
accountv1.LowBalancePeriod: "Low Balance",
accountv1.CriticalBalancePeriod: "Critical Balance",
accountv1.DebtPeriod: "Debt",
accountv1.DebtDeletionPeriod: "Imminent Resource Release",
accountv1.FinalDeletionPeriod: "Radical resource release",
}
)
var (
EmailTemplateEN map[int]string
EmailTemplateZH map[int]string
NoticeTemplateEN map[int]string
NoticeTemplateZH map[int]string
NoticeTemplateENMap map[accountv1.DebtStatusType]string
NoticeTemplateZHMap map[accountv1.DebtStatusType]string
EmailTemplateENMap map[accountv1.DebtStatusType]string
EmailTemplateZHMap map[accountv1.DebtStatusType]string
)
var (
@@ -515,7 +546,7 @@ var (
UTCPlus8 = time.FixedZone("UTC+8", 8*3600)
)
func (r *DebtReconciler) sendSMSNotice(user string, oweAmount int64, noticeType int) error {
func (r *DebtReconciler) sendSMSNotice(user string, oweAmount int64, noticeType accountv1.DebtStatusType) error {
if r.SmsConfig == nil && r.VmsConfig == nil && r.smtpConfig == nil {
return nil
}
@@ -539,29 +570,30 @@ func (r *DebtReconciler) sendSMSNotice(user string, oweAmount int64, noticeType
email = outh[i].ProviderID
}
}
if phone != "" {
if r.SmsConfig != nil && r.SmsConfig.SmsCode[noticeType] != "" {
oweamount := strconv.FormatInt(int64(math.Abs(math.Ceil(float64(oweAmount)/1_000_000))), 10)
err = utils.SendSms(r.SmsConfig.Client, &client2.SendSmsRequest{
PhoneNumbers: tea.String(phone),
SignName: tea.String(r.SmsConfig.SmsSignName),
TemplateCode: tea.String(r.SmsConfig.SmsCode[noticeType]),
// ownAmount/1_000_000
TemplateParam: tea.String("{\"user_id\":\"" + user + "\",\"oweamount\":\"" + oweamount + "\"}"),
})
if err != nil {
return fmt.Errorf("failed to send sms notice: %w", err)
}
}
if r.VmsConfig != nil && noticeType == WarningNotice && r.VmsConfig.TemplateCode[noticeType] != "" {
err = utils.SendVms(phone, r.VmsConfig.TemplateCode[noticeType], r.VmsConfig.NumberPoll, GetSendVmsTimeInUTCPlus8(time.Now()), forbidTimes)
if err != nil {
return fmt.Errorf("failed to send vms notice: %w", err)
}
}
}
fmt.Printf("user: %s, phone: %s, email: %s\n", user, phone, email)
//if phone != "" {
// if r.SmsConfig != nil && r.SmsConfig.SmsCode[noticeType] != "" {
// oweamount := strconv.FormatInt(int64(math.Abs(math.Ceil(float64(oweAmount)/1_000_000))), 10)
// err = utils.SendSms(r.SmsConfig.Client, &client2.SendSmsRequest{
// PhoneNumbers: tea.String(phone),
// SignName: tea.String(r.SmsConfig.SmsSignName),
// TemplateCode: tea.String(r.SmsConfig.SmsCode[noticeType]),
// // ownAmount/1_000_000
// TemplateParam: tea.String("{\"user_id\":\"" + user + "\",\"oweamount\":\"" + oweamount + "\"}"),
// })
// if err != nil {
// return fmt.Errorf("failed to send sms notice: %w", err)
// }
// }
// if r.VmsConfig != nil && noticeType == WarningNotice && r.VmsConfig.TemplateCode[noticeType] != "" {
// err = utils.SendVms(phone, r.VmsConfig.TemplateCode[noticeType], r.VmsConfig.NumberPoll, GetSendVmsTimeInUTCPlus8(time.Now()), forbidTimes)
// if err != nil {
// return fmt.Errorf("failed to send vms notice: %w", err)
// }
// }
//}
if r.smtpConfig != nil && email != "" {
if err = r.smtpConfig.SendEmail(EmailTemplateZH[noticeType]+"\n"+EmailTemplateEN[noticeType], email); err != nil {
if err = r.smtpConfig.SendEmail(r.SendDebtStatusEmailBody[noticeType], email); err != nil {
return fmt.Errorf("failed to send email notice: %w", err)
}
}
@@ -584,11 +616,11 @@ func GetSendVmsTimeInUTCPlus8(t time.Time) time.Time {
return next10AM.In(time.Local)
}
func (r *DebtReconciler) readNotice(ctx context.Context, namespaces []string, noticeTypes ...int) error {
func (r *DebtReconciler) readNotice(ctx context.Context, namespaces []string, noticeTypes ...accountv1.DebtStatusType) error {
for i := range namespaces {
for j := range noticeTypes {
for _, noticeStatus := range noticeTypes {
ntf := &v1.Notification{}
if err := r.Get(ctx, types.NamespacedName{Name: debtChoicePrefix + strconv.Itoa(noticeTypes[j]), Namespace: namespaces[i]}, ntf); client.IgnoreNotFound(err) != nil {
if err := r.Get(ctx, types.NamespacedName{Name: debtChoicePrefix + strings.ToLower(string(noticeStatus)), Namespace: namespaces[i]}, ntf); client.IgnoreNotFound(err) != nil {
return err
} else if err != nil {
continue
@@ -607,25 +639,38 @@ func (r *DebtReconciler) readNotice(ctx context.Context, namespaces []string, no
return nil
}
func (r *DebtReconciler) sendNotice(ctx context.Context, user string, oweAmount int64, noticeType int, namespaces []string, smsEnable bool) error {
func (r *DebtReconciler) sendDesktopNoticeAndSms(ctx context.Context, user string, oweAmount int64, noticeType accountv1.DebtStatusType, namespaces []string, smsEnable, isBasicUser bool) error {
if isBasicUser && noticeType != accountv1.DebtPeriod && noticeType != accountv1.DebtDeletionPeriod && noticeType != accountv1.FinalDeletionPeriod && noticeType != accountv1.CriticalBalancePeriod {
return nil
}
if err := r.sendDesktopNotice(ctx, noticeType, namespaces); err != nil {
return fmt.Errorf("send notice error: %w", err)
}
if !smsEnable || (isBasicUser && noticeType == accountv1.CriticalBalancePeriod) {
return nil
}
return r.sendSMSNotice(user, oweAmount, noticeType)
}
func (r *DebtReconciler) sendDesktopNotice(ctx context.Context, noticeType accountv1.DebtStatusType, namespaces []string) error {
now := time.Now().UTC().Unix()
ntfTmp := &v1.Notification{
ObjectMeta: metav1.ObjectMeta{
Name: debtChoicePrefix + strconv.Itoa(noticeType),
Name: debtChoicePrefix + strings.ToLower(string(noticeType)),
},
}
ntfTmpSpc := v1.NotificationSpec{
Title: TitleTemplateEN[noticeType],
Message: NoticeTemplateEN[noticeType],
Title: TitleTemplateENMap[noticeType],
Message: NoticeTemplateENMap[noticeType],
From: fromEn,
Importance: v1.High,
DesktopPopup: true,
Timestamp: now,
I18n: map[string]v1.I18n{
languageZh: {
Title: TitleTemplateZH[noticeType],
Title: TitleTemplateZHMap[noticeType],
From: fromZh,
Message: NoticeTemplateZH[noticeType],
Message: NoticeTemplateZHMap[noticeType],
},
},
}
@@ -644,32 +689,17 @@ func (r *DebtReconciler) sendNotice(ctx context.Context, user string, oweAmount
return err
}
}
if smsEnable && (noticeType == WarningNotice || noticeType == ImminentDeletionNotice) {
return r.sendSMSNotice(user, oweAmount, noticeType)
}
return nil
}
func (r *DebtReconciler) sendWarningNotice(ctx context.Context, user string, oweAmount int64, namespaces []string, smsEnable bool) error {
return r.sendNotice(ctx, user, oweAmount, WarningNotice, namespaces, smsEnable)
}
func (r *DebtReconciler) sendApproachingDeletionNotice(ctx context.Context, user string, oweAmount int64, namespaces []string, smsEnable bool) error {
return r.sendNotice(ctx, user, oweAmount, ApproachingDeletionNotice, namespaces, smsEnable)
}
func (r *DebtReconciler) sendImminentDeletionNotice(ctx context.Context, user string, oweAmount int64, namespaces []string, smsEnable bool) error {
return r.sendNotice(ctx, user, oweAmount, ImminentDeletionNotice, namespaces, smsEnable)
}
func (r *DebtReconciler) sendFinalDeletionNotice(ctx context.Context, user string, oweAmount int64, namespaces []string, smsEnable bool) error {
return r.sendNotice(ctx, user, oweAmount, FinalDeletionNotice, namespaces, smsEnable)
}
func (r *DebtReconciler) SuspendUserResource(ctx context.Context, namespaces []string) error {
return r.updateNamespaceStatus(ctx, accountv1.SuspendDebtNamespaceAnnoStatus, namespaces)
}
func (r *DebtReconciler) DeleteUserResource(ctx context.Context, namespace []string) error {
return r.updateNamespaceStatus(ctx, accountv1.FinalDeletionDebtNamespaceAnnoStatus, namespace)
}
func (r *DebtReconciler) ResumeUserResource(ctx context.Context, namespaces []string) error {
return r.updateNamespaceStatus(ctx, accountv1.ResumeDebtNamespaceAnnoStatus, namespaces)
}
@@ -680,6 +710,9 @@ func (r *DebtReconciler) updateNamespaceStatus(ctx context.Context, status strin
if err := r.Get(ctx, types.NamespacedName{Name: namespaces[i], Namespace: r.accountSystemNamespace}, ns); err != nil {
return err
}
if ns.Annotations[accountv1.DebtNamespaceAnnoStatusKey] == status {
continue
}
// 交给namespace controller处理
ns.Annotations[accountv1.DebtNamespaceAnnoStatusKey] = status
if err := r.Client.Update(ctx, ns); err != nil {
@@ -690,18 +723,14 @@ func (r *DebtReconciler) updateNamespaceStatus(ctx context.Context, status strin
}
// convert "1:code1,2:code2" to map[int]string
func splitSmsCodeMap(codeStr string) (map[int]string, error) {
codeMap := make(map[int]string)
func splitSmsCodeMap(codeStr string) (map[string]string, error) {
codeMap := make(map[string]string)
for _, code := range strings.Split(codeStr, ",") {
split := strings.SplitN(code, ":", 2)
if len(split) != 2 {
return nil, fmt.Errorf("invalid sms code map: %s", codeStr)
}
codeInt, err := strconv.Atoi(split[0])
if err != nil {
return nil, fmt.Errorf("invalid sms code map: %s", codeStr)
}
codeMap[codeInt] = split[1]
codeMap[split[0]] = split[1]
}
return codeMap, nil
}
@@ -715,7 +744,12 @@ func (r *DebtReconciler) setupSmsConfig() error {
if err != nil {
return fmt.Errorf("split sms code map error: %w", err)
}
for key := range smsCodeMap {
if _, ok := pkgtypes.StatusMap[pkgtypes.DebtStatusType(key)]; !ok {
return fmt.Errorf("invalid sms code map key: %s", key)
}
}
r.Logger.Info("set sms code map", "smsCodeMap", smsCodeMap, "smsSignName", os.Getenv(SMSSignNameEnv))
smsClient, err := utils.CreateSMSClient(os.Getenv(SMSAccessKeyIDEnv), os.Getenv(SMSAccessKeySecretEnv), os.Getenv(SMSEndpointEnv))
if err != nil {
return fmt.Errorf("create sms client error: %w", err)
@@ -739,6 +773,12 @@ func (r *DebtReconciler) setupVmsConfig() error {
if err != nil {
return fmt.Errorf("split vms code map error: %w", err)
}
for key := range vmsCodeMap {
if _, ok := pkgtypes.StatusMap[pkgtypes.DebtStatusType(key)]; !ok {
return fmt.Errorf("invalid sms code map key: %s", key)
}
}
r.Logger.Info("set vms code map", "vmsCodeMap", vmsCodeMap)
r.VmsConfig = &VmsConfig{
TemplateCode: vmsCodeMap,
NumberPoll: os.Getenv(VmsNumberPollEnv),
@@ -747,16 +787,17 @@ func (r *DebtReconciler) setupVmsConfig() error {
}
func (r *DebtReconciler) setupSMTPConfig() error {
if err := env.CheckEnvSetting([]string{SMTPHostEnv, SMTPPortEnv, SMTPFromEnv, SMTPPasswordEnv, SMTPTitleEnv}); err != nil {
if err := env.CheckEnvSetting([]string{SMTPHostEnv, SMTPFromEnv, SMTPPasswordEnv, SMTPTitleEnv}); err != nil {
return fmt.Errorf("check env setting error: %w", err)
}
serverPort, err := strconv.Atoi(os.Getenv(SMTPPortEnv))
serverPort, err := strconv.Atoi(env.GetEnvWithDefault(SMTPPortEnv, "465"))
if err != nil {
return fmt.Errorf("invalid smtp port: %w", err)
}
r.smtpConfig = &utils.SMTPConfig{
ServerHost: os.Getenv(SMTPHostEnv),
ServerPort: serverPort,
Username: env.GetEnvWithDefault(SMTPUserEnv, os.Getenv(SMTPFromEnv)),
FromEmail: os.Getenv(SMTPFromEnv),
Passwd: os.Getenv(SMTPPasswordEnv),
EmailTitle: os.Getenv(SMTPTitleEnv),
@@ -766,23 +807,7 @@ func (r *DebtReconciler) setupSMTPConfig() error {
// SetupWithManager sets up the controller with the Manager.
func (r *DebtReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error {
r.Logger = ctrl.Log.WithName("DebtController")
r.accountSystemNamespace = env.GetEnvWithDefault(accountv1.AccountSystemNamespaceEnv, "account-system")
r.LocalRegionID = os.Getenv(cockroach.EnvLocalRegion)
debtDetectionCycleSecond := env.GetInt64EnvWithDefault(DebtDetectionCycleEnv, 1800)
r.DebtDetectionCycle = time.Duration(debtDetectionCycleSecond) * time.Second
setupList := []func() error{
r.setupSmsConfig,
r.setupVmsConfig,
r.setupSMTPConfig,
}
for i := range setupList {
if err := setupList[i](); err != nil {
r.Logger.Error(err, fmt.Sprintf("failed to set up %s", runtime2.FuncForPC(reflect.ValueOf(setupList[i]).Pointer()).Name()))
}
}
r.Init()
/*
{"DebtConfig":{
"ApproachingDeletionPeriod":345600,
@@ -801,34 +826,62 @@ func (r *DebtReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.
Complete(r)
}
func (r *DebtReconciler) Init() {
r.Logger = ctrl.Log.WithName("DebtController")
r.accountSystemNamespace = env.GetEnvWithDefault(accountv1.AccountSystemNamespaceEnv, "account-system")
r.LocalRegionID = os.Getenv(cockroach.EnvLocalRegion)
debtDetectionCycleSecond := env.GetInt64EnvWithDefault(DebtDetectionCycleEnv, 1800)
r.DebtDetectionCycle = time.Duration(debtDetectionCycleSecond) * time.Second
r.userLocks = &sync.Map{}
r.failedUserLocks = &sync.Map{}
r.processID = uuid.NewString()
setupList := []func() error{
r.setupSmsConfig,
r.setupVmsConfig,
r.setupSMTPConfig,
}
for i := range setupList {
if err := setupList[i](); err != nil {
r.Logger.Error(err, fmt.Sprintf("failed to set up %s", runtime2.FuncForPC(reflect.ValueOf(setupList[i]).Pointer()).Name()))
}
}
setDefaultDebtPeriodWaitSecond()
r.SendDebtStatusEmailBody = make(map[accountv1.DebtStatusType]string)
for _, status := range []accountv1.DebtStatusType{accountv1.LowBalancePeriod, accountv1.CriticalBalancePeriod, accountv1.DebtPeriod, accountv1.DebtDeletionPeriod, accountv1.FinalDeletionPeriod} {
email := os.Getenv(string(status) + "EmailBody")
if email == "" {
email = EmailTemplateZHMap[status] + "\n" + EmailTemplateENMap[status]
} else {
r.Logger.Info("set email body", "status", status, "body", email)
}
r.SendDebtStatusEmailBody[status] = email
}
r.Logger.Info("debt config", "DebtConfig", DebtConfig, "DebtDetectionCycle", r.DebtDetectionCycle)
}
func setDefaultDebtPeriodWaitSecond() {
/*
WarningPeriod: WarnPeriodWaitSecond,
ApproachingDeletionPeriod: ApproachingDeletionPeriodWaitSecond,
ImminentDeletionPeriod: IminentDeletionPeriodWaitSecond,
FinalDeletionPeriod: FinalDeletionPeriodWaitSecond,
*/
DebtConfig[accountv1.WarningPeriod] = env.GetInt64EnvWithDefault(string(accountv1.WarningPeriod), 0*accountv1.DaySecond)
DebtConfig[accountv1.ApproachingDeletionPeriod] = env.GetInt64EnvWithDefault(string(accountv1.ApproachingDeletionPeriod), 4*accountv1.DaySecond)
DebtConfig[accountv1.ImminentDeletionPeriod] = env.GetInt64EnvWithDefault(string(accountv1.ImminentDeletionPeriod), 3*accountv1.DaySecond)
DebtConfig[accountv1.DebtDeletionPeriod] = env.GetInt64EnvWithDefault(string(accountv1.DebtDeletionPeriod), 7*accountv1.DaySecond)
DebtConfig[accountv1.FinalDeletionPeriod] = env.GetInt64EnvWithDefault(string(accountv1.FinalDeletionPeriod), 7*accountv1.DaySecond)
NoticeTemplateZH = map[int]string{
WarningNotice: "当前工作空间所属账户余额不足,系统将为您暂停服务,请及时充值,以免影响您的正常使用。",
ApproachingDeletionNotice: fmt.Sprintf("当前工作空间所属账户余额不足,系统将在%2.f小时后或欠费超过充值金额后释放当前空间的资源,请及时充值,以免影响您的正常使用。", math.Ceil(float64(DebtConfig[accountv1.ImminentDeletionPeriod])/3600)),
ImminentDeletionNotice: fmt.Sprintf("当前工作空间容器实例资源已被暂停,系统将在%2.f小时后彻底释放资源,无法恢复,请及时充值,以免影响您的正常使用。", math.Ceil(float64(DebtConfig[accountv1.FinalDeletionPeriod])/3600)),
FinalDeletionNotice: "系统将随时彻底释放当前工作空间所属账户下的所有资源,请及时充值,以免影响您的正常使用。",
}
NoticeTemplateEN = map[int]string{
WarningNotice: "Your account balance is not enough to pay this month's bill, and services will be suspended for you. Please recharge in time to avoid affecting your normal use.",
ApproachingDeletionNotice: fmt.Sprintf("Your account balance is not enough to pay this month's bill, and your resources will be released after %2.f hours or when the arrears exceed the recharge amount. Please recharge in time to avoid affecting your normal use.", math.Ceil(float64(DebtConfig[accountv1.ImminentDeletionPeriod])/3600)),
ImminentDeletionNotice: fmt.Sprintf("Your container instance resources have been suspended, and the system will completely release the resources after %2.f hours, which cannot be recovered. Please recharge in time to avoid affecting your normal use.", math.Ceil(float64(DebtConfig[accountv1.FinalDeletionPeriod])/3600)),
FinalDeletionNotice: "The system will completely release all your resources at any time. Please recharge in time to avoid affecting your normal use.",
}
domain := os.Getenv("DOMAIN")
EmailTemplateEN, EmailTemplateZH = make(map[int]string), make(map[int]string)
for _, i := range []int{WarningNotice, ApproachingDeletionNotice, ImminentDeletionNotice, FinalDeletionNotice} {
EmailTemplateEN[i] = TitleTemplateEN[i] + "" + NoticeTemplateEN[i] + "(" + domain + ")"
EmailTemplateZH[i] = TitleTemplateZH[i] + "" + NoticeTemplateZH[i] + "(" + domain + ")"
NoticeTemplateZHMap = map[accountv1.DebtStatusType]string{
accountv1.LowBalancePeriod: "当前工作空间所属账户余额过低,请及时充值,以免影响您的正常使用。",
accountv1.CriticalBalancePeriod: "当前工作空间所属账户余额即将耗尽,请及时充值,以免影响您的正常使用。",
accountv1.DebtPeriod: "当前工作空间所属账户余额已耗尽,系统将为您暂停服务,请及时充值,以免影响您的正常使用。",
accountv1.DebtDeletionPeriod: "系统即将释放当前空间的资源,请及时充值,以免影响您的正常使用。",
accountv1.FinalDeletionPeriod: "系统将随时彻底释放当前工作空间所属账户下的所有资源,请及时充值,以免影响您的正常使用。",
}
NoticeTemplateENMap = map[accountv1.DebtStatusType]string{
accountv1.LowBalancePeriod: "Your account balance is too low, please recharge in time to avoid affecting your normal use.",
accountv1.CriticalBalancePeriod: "Your account balance is about to run out, please recharge in time to avoid affecting your normal use.",
accountv1.DebtPeriod: "Your account balance has been exhausted, and services will be suspended for you. Please recharge in time to avoid affecting your normal use.",
accountv1.DebtDeletionPeriod: "The system will release the resources of the current space soon. Please recharge in time to avoid affecting your normal use.",
accountv1.FinalDeletionPeriod: "The system will completely release all resources under the current account at any time. Please recharge in time to avoid affecting your normal use.",
}
EmailTemplateZHMap, EmailTemplateENMap = make(map[accountv1.DebtStatusType]string), make(map[accountv1.DebtStatusType]string)
for _, i := range []accountv1.DebtStatusType{accountv1.LowBalancePeriod, accountv1.CriticalBalancePeriod, accountv1.DebtPeriod, accountv1.DebtDeletionPeriod, accountv1.FinalDeletionPeriod} {
EmailTemplateENMap[i] = TitleTemplateENMap[i] + "" + NoticeTemplateENMap[i] + "(" + domain + ")"
EmailTemplateZHMap[i] = TitleTemplateZHMap[i] + "" + NoticeTemplateZHMap[i] + "(" + domain + ")"
}
}
@@ -856,7 +909,3 @@ func (OnlyCreatePredicate) Update(_ event.UpdateEvent) bool {
func (OnlyCreatePredicate) Create(_ event.CreateEvent) bool {
return true
}
func init() {
setDefaultDebtPeriodWaitSecond()
}
@@ -15,8 +15,34 @@
package controllers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/labring/sealos/controllers/pkg/database/cockroach"
"k8s.io/client-go/tools/clientcmd"
"gorm.io/gorm"
accountv1 "github.com/labring/sealos/controllers/account/api/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/google/uuid"
"github.com/labring/sealos/controllers/pkg/types"
"github.com/labring/sealos/controllers/pkg/database"
"github.com/labring/sealos/controllers/pkg/utils"
)
func Test_splitSmsCodeMap(t *testing.T) {
@@ -28,13 +54,13 @@ func Test_splitSmsCodeMap(t *testing.T) {
if len(codeMap) != 3 {
t.Fatal("invalid codeMap")
}
if codeMap[0] != "SMS_123456" {
if codeMap["0"] != "SMS_123456" {
t.Fatal("invalid codeMap")
}
if codeMap[1] != "SMS_654321" {
if codeMap["1"] != "SMS_654321" {
t.Fatal("invalid codeMap")
}
if codeMap[2] != "SMS_987654" {
if codeMap["2"] != "SMS_987654" {
t.Fatal("invalid codeMap")
}
}
@@ -51,3 +77,365 @@ func TestGetTimeInUTCPlus8(t *testing.T) {
t.Logf("time: %v, timeInUTCPlus8: %v", _t, GetSendVmsTimeInUTCPlus8(_t))
}
}
const (
processedUsersFile = "processed_users.txt"
)
// TestReconcileAllFinalUser 测试方法
func TestReconcileAllFinalUser(t *testing.T) {
os.Setenv("LOCAL_REGION", "4b55d7c5-ff65-4eb7-9bcf-726c730a0fad")
account, err := database.NewAccountV2("postgresql://sealos:fb9jg8te4x78ocqrr2vgbs99qauh9flfd1u6g300kq7ywjay3ah7cndr60udd6wg@192.168.10.35:32749/global", "postgresql://sealos:vtzfqp8hbkn7jdstzkbac6cd4u84n6w3s28f8wnqzrts2b96xcs7v58r1a18ihds@192.168.10.35:32749/local")
if err != nil {
t.Fatalf("failed to new account: %v", err)
}
defer func() {
if err := account.Close(); err != nil {
t.Errorf("failed close connection: %v", err)
}
}()
regions, err := account.GetRegions()
if err != nil {
t.Fatalf("failed to get regions: %v", err)
}
allRegionDomain := make([]string, 0)
for _, region := range regions {
if region.Domain != "" {
allRegionDomain = append(allRegionDomain, region.Domain)
}
}
jwtManager := utils.NewJWTManager("98r7c1zjllv4kgn67trj1cknprnpcwup3hh38b44puhfrbkmzy9bjipbw4tclr3f", time.Hour*24)
// 获取全部 Debt 状态为 FinalDeletionPeriod 的用户
allUserUID := make([]uuid.UUID, 0)
err = account.GetGlobalDB().Model(&types.Debt{}).Where("account_debt_status = ?", types.FinalDeletionPeriod).Pluck("user_uid", &allUserUID).Error
if err != nil {
t.Fatalf("failed to get all user: %v", err)
}
if len(allUserUID) == 0 {
t.Logf("no user need to flush")
return
}
// 创建临时文件路径
tempDir := os.TempDir()
processedFilePath := filepath.Join(tempDir, processedUsersFile)
// 加载已处理的用户 UID
processedUsers, err := loadProcessedUsers(processedFilePath)
if err != nil {
t.Fatalf("failed to load processed users: %v", err)
}
for _, user := range allUserUID {
if processedUsers[user] {
t.Logf("user %s already processed, skipping", user)
continue
}
err = sendFlushDebtResourceStatusRequest(allRegionDomain, jwtManager, user, processedFilePath)
if err != nil {
t.Fatalf("failed to send flush debt resource status request for user %s: %v", user, err)
}
if err := recordProcessedUser(processedFilePath, user); err != nil {
t.Fatalf("failed to record processed user %s: %v", user, err)
}
}
t.Logf("all users processed successfully")
// 删除临时文件
//if err := os.Remove(processedFilePath); err != nil {
// t.Fatalf("failed to remove processed users file: %v", err)
//}
}
// loadProcessedUsers 从文件中加载已处理的用户 UID
func loadProcessedUsers(filePath string) (map[uuid.UUID]bool, error) {
processed := make(map[uuid.UUID]bool)
data, err := os.ReadFile(filePath)
if os.IsNotExist(err) {
return processed, nil
}
if err != nil {
return nil, fmt.Errorf("failed to read processed users file: %w", err)
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
uid, err := uuid.Parse(line)
if err != nil {
return nil, fmt.Errorf("invalid UUID in processed users file: %s", line)
}
processed[uid] = true
}
return processed, nil
}
// recordProcessedUser 将处理成功的用户 UID 追加到文件中
func recordProcessedUser(filePath string, userUID uuid.UUID) error {
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open processed users file: %w", err)
}
defer f.Close()
if _, err := f.WriteString(userUID.String() + "\n"); err != nil {
return fmt.Errorf("failed to write user UID to file: %w", err)
}
return nil
}
// sendFlushDebtResourceStatusRequest 发送请求并记录成功处理的用户
func sendFlushDebtResourceStatusRequest(allRegionDomain []string, jwtManager *utils.JWTManager, userUID uuid.UUID, processedFilePath string) error {
for _, domain := range allRegionDomain {
fmt.Println("domain:", domain, " userUID:", userUID)
token, err := jwtManager.GenerateToken(utils.JwtUser{
Requester: AdminUserName,
})
if err != nil {
return fmt.Errorf("failed to generate token: %w", err)
}
url := fmt.Sprintf("https://account-api.%s/admin/v1alpha1/flush-debt-resource-status", domain)
quotaReqBody, err := json.Marshal(AdminFlushResourceStatusReq{
LastDebtStatus: types.DebtDeletionPeriod,
CurrentDebtStatus: types.FinalDeletionPeriod,
UserUID: userUID,
})
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
var lastErr error
backoffTime := time.Second
maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ {
req, err := http.NewRequest("POST", url, bytes.NewBuffer(quotaReqBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
lastErr = fmt.Errorf("failed to send request: %w", err)
} else {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
lastErr = nil
break
}
lastErr = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
if attempt < maxRetries {
fmt.Printf("Attempt %d failed: %v. Retrying in %v...\n", attempt, lastErr, backoffTime)
time.Sleep(backoffTime)
backoffTime *= 2
}
}
if lastErr != nil {
return lastErr
}
}
return nil
}
type regionConfig struct {
Region string `json:"region"`
KCPath string `json:"kc_path"`
GlobalDB string `json:"global_db"`
LocalDB string `json:"local_db"`
RegionUID string `json:"region_uid"`
}
var regions = []regionConfig{}
// 1. pause account controller
// 2. convert all region debt
// 3. upgrade and restore the account controller
func TestConvertDebt(t *testing.T) {
for i := range regions {
//先获取全部的debt crd
fmt.Printf("Start converting debts for region %s at %s\n", regions[i].Region, time.Now().Format(time.RFC3339))
config, err := clientcmd.BuildConfigFromFlags("", regions[i].KCPath)
if err != nil {
t.Fatalf("failed to get in cluster config: %v", err)
}
emptyScheme := runtime.NewScheme()
utilruntime.Must(accountv1.AddToScheme(emptyScheme))
clt, err := client.New(config, client.Options{Scheme: emptyScheme})
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
os.Setenv("LOCAL_REGION", regions[i].RegionUID)
account, err := database.NewAccountV2(regions[i].GlobalDB, regions[i].LocalDB)
if err != nil {
t.Fatalf("failed to new account: %v", err)
}
defer func() {
if err := account.Close(); err != nil {
t.Errorf("failed close connection: %v", err)
}
}()
if !account.GetGlobalDB().Migrator().HasTable(&types.Debt{}) {
err = account.GetGlobalDB().Migrator().AutoMigrate(&types.Debt{})
if err != nil {
t.Fatalf("failed to migrate debt table: %v", err)
}
}
if !account.GetGlobalDB().Migrator().HasTable(&types.DebtStatusRecord{}) {
err = account.GetGlobalDB().Migrator().AutoMigrate(&types.DebtStatusRecord{})
if err != nil {
t.Fatalf("failed to migrate debt status record table: %v", err)
}
}
err = convertAllDebtCr(account, clt)
if err != nil {
t.Fatalf("failed to convert all debt cr: %v", err)
}
fmt.Printf("Finished converting debts for region %s at %s\n", regions[i].Region, time.Now().Format(time.RFC3339))
}
}
func convertAllDebtCr(account database.AccountV2, clt client.Client) error {
// 1. 获取已存在的 user_uid
// 2. 预加载所有 userID -> userUID
// 3. 拉取 CRs 并过滤掉已存在的 userUID // 添加 userUID 以避免重复(主线程去重)
// 4. worker pool 执行写入
// 5. 转换并推入任务队列
const (
workerCount = 10
batchSize = 100
maxRetries = 3
)
existing := make(map[uuid.UUID]struct{})
var existingUIDs []uuid.UUID
if err := account.GetGlobalDB().Model(&types.Debt{}).Pluck("user_uid", &existingUIDs).Error; err != nil {
return fmt.Errorf("failed to preload existing debts: %v", err)
}
for _, uid := range existingUIDs {
existing[uid] = struct{}{}
}
userIDToUIDMap, err := GetUserIDToUIDMap(account.GetGlobalDB())
if err != nil {
return fmt.Errorf("failed to preload user UID map: %v", err)
}
var allDebts []accountv1.Debt
listOpts := &client.ListOptions{Limit: 1000}
for {
debtCRList := &accountv1.DebtList{}
if err := clt.List(context.Background(), debtCRList, listOpts); err != nil {
return fmt.Errorf("failed to list debts: %v", err)
}
for _, debt := range debtCRList.Items {
if debt.Spec.UserID == "" {
continue
}
userUID, ok := userIDToUIDMap[debt.Spec.UserID]
if !ok || userUID == uuid.Nil {
continue
}
if _, exists := existing[userUID]; exists {
continue
}
existing[userUID] = struct{}{}
allDebts = append(allDebts, debt)
}
if cont := debtCRList.GetContinue(); cont == "" {
break
} else {
listOpts.Continue = cont
}
}
fmt.Printf("Total debts to insert: %d\n", len(allDebts))
var wg sync.WaitGroup
tasks := make(chan *types.Debt, len(allDebts))
var firstErr error
var errMu sync.Mutex
for i := 0; i < workerCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
var batch []*types.Debt
for debt := range tasks {
batch = append(batch, debt)
if len(batch) >= batchSize {
if err := insertDebts(account, batch); err != nil {
fmt.Printf("failed to insert debts: %v", err)
errMu.Lock()
if firstErr == nil {
firstErr = err
}
errMu.Unlock()
}
batch = batch[:0]
}
}
if len(batch) > 0 {
if err := insertDebts(account, batch); err != nil {
fmt.Printf("failed to insert debts: %v", err)
errMu.Lock()
if firstErr == nil {
firstErr = err
}
errMu.Unlock()
}
}
}()
}
for _, debt := range allDebts {
userUID := userIDToUIDMap[debt.Spec.UserID]
tasks <- convertDebtCrToDebt(&debt, userUID)
}
close(tasks)
wg.Wait()
return firstErr
}
func GetUserIDToUIDMap(db *gorm.DB) (map[string]uuid.UUID, error) {
type user struct {
ID string `gorm:"column:id"`
UserUID uuid.UUID `gorm:"column:uid"`
}
var users []user
if err := db.Model(&types.User{}).Select("id, uid").Find(&users).Error; err != nil {
return nil, fmt.Errorf("failed to preload users: %v", err)
}
result := make(map[string]uuid.UUID, len(users))
for _, u := range users {
result[u.ID] = u.UserUID
}
return result, nil
}
func insertDebts(account database.AccountV2, debts []*types.Debt) error {
return cockroach.RetryableTransaction(account.GetGlobalDB().Session(&gorm.Session{PrepareStmt: true}), 3, func(tx *gorm.DB) error {
return tx.Create(&debts).Error
})
}
@@ -23,6 +23,14 @@ import (
"strings"
"time"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/client-go/rest"
v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -54,6 +62,7 @@ import (
// NamespaceReconciler reconciles a Namespace object
type NamespaceReconciler struct {
Client client.WithWatch
dynamicClient dynamic.Interface
Log logr.Logger
Scheme *runtime.Scheme
OSAdminClient *madmin.AdminClient
@@ -113,6 +122,11 @@ func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
logger.Error(err, "suspend namespace resources failed")
return ctrl.Result{}, err
}
case v1.FinalDeletionDebtNamespaceAnnoStatus:
if err := r.DeleteUserResource(ctx, req.NamespacedName.Name); err != nil {
logger.Error(err, "delete namespace resources failed")
return ctrl.Result{}, err
}
case v1.ResumeDebtNamespaceAnnoStatus:
if err := r.ResumeUserResource(ctx, req.NamespacedName.Name); err != nil {
logger.Error(err, "resume namespace resources failed")
@@ -156,6 +170,20 @@ func (r *NamespaceReconciler) SuspendUserResource(ctx context.Context, namespace
return nil
}
func (r *NamespaceReconciler) DeleteUserResource(_ context.Context, namespace string) error {
deleteResources := []string{
"backup", "cluster.apps.kubeblocks.io", "backupschedules", "devboxes", "devboxreleases", "cronjob",
"objectstorageuser", "deploy", "sts", "pvc", "Service", "Ingress",
"Issuer", "Certificate", "HorizontalPodAutoscaler", "instance",
}
for _, rs := range deleteResources {
if err := deleteResource(r.dynamicClient, rs, namespace); err != nil {
return err
}
}
return nil
}
func (r *NamespaceReconciler) ResumeUserResource(ctx context.Context, namespace string) error {
// delete limit0 resource quota
// resume pod
@@ -455,6 +483,15 @@ func (r *NamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.OSAdminSecret = os.Getenv(OSAdminSecret)
r.InternalEndpoint = os.Getenv(OSInternalEndpointEnv)
r.OSNamespace = os.Getenv(OSNamespace)
config, err := rest.InClusterConfig()
if err != nil {
panic(fmt.Sprintf("Failed to load in-cluster config: %v", err))
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
panic(fmt.Sprintf("Failed to create dynamic client: %v", err))
}
r.dynamicClient = dynamicClient
if r.OSAdminSecret == "" || r.InternalEndpoint == "" || r.OSNamespace == "" {
r.Log.V(1).Info("failed to get the endpoint or namespace or admin secret env of object storage")
@@ -506,3 +543,118 @@ func (r *NamespaceReconciler) suspendCronJob(ctx context.Context, namespace stri
}
return nil
}
func deleteResource(dynamicClient dynamic.Interface, resource, namespace string) error {
ctx := context.Background()
deletePolicy := v12.DeletePropagationForeground
var gvr schema.GroupVersionResource
switch resource {
case "backup":
gvr = schema.GroupVersionResource{
Group: "dataprotection.kubeblocks.io",
Version: "v1alpha1",
Resource: "backups",
}
case "cluster.apps.kubeblocks.io":
gvr = schema.GroupVersionResource{
Group: "apps.kubeblocks.io",
Version: "v1alpha1",
Resource: "clusters",
}
case "backupschedules":
gvr = schema.GroupVersionResource{
Group: "dataprotection.kubeblocks.io",
Version: "v1alpha1",
Resource: "backupschedules",
}
case "cronjob":
gvr = schema.GroupVersionResource{
Group: "batch",
Version: "v1",
Resource: "cronjobs",
}
case "objectstorageuser":
gvr = schema.GroupVersionResource{
Group: "objectstorage.sealos.io",
Version: "v1",
Resource: "objectstorageusers",
}
case "deploy":
gvr = schema.GroupVersionResource{
Group: "apps",
Version: "v1",
Resource: "deployments",
}
case "sts":
gvr = schema.GroupVersionResource{
Group: "apps",
Version: "v1",
Resource: "statefulsets",
}
case "pvc":
gvr = schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "persistentvolumeclaims",
}
case "Service":
gvr = schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "services",
}
case "Ingress":
gvr = schema.GroupVersionResource{
Group: "networking.k8s.io",
Version: "v1",
Resource: "ingresses",
}
case "Issuer":
gvr = schema.GroupVersionResource{
Group: "cert-manager.io",
Version: "v1",
Resource: "issuers",
}
case "Certificate":
gvr = schema.GroupVersionResource{
Group: "cert-manager.io",
Version: "v1",
Resource: "certificates",
}
case "HorizontalPodAutoscaler":
gvr = schema.GroupVersionResource{
Group: "autoscaling",
Version: "v1",
Resource: "horizontalpodautoscalers",
}
case "instance":
gvr = schema.GroupVersionResource{
Group: "app.sealos.io",
Version: "v1",
Resource: "instances",
}
case "devboxes":
gvr = schema.GroupVersionResource{
Group: "devbox.sealos.io",
Version: "v1alpha1",
Resource: "devboxes",
}
case "devboxreleases":
gvr = schema.GroupVersionResource{
Group: "devbox.sealos.io",
Version: "v1alpha1",
Resource: "devboxreleases",
}
default:
return fmt.Errorf("unknown resource: %s", resource)
}
err := dynamicClient.Resource(gvr).Namespace(namespace).DeleteCollection(ctx, v12.DeleteOptions{
PropagationPolicy: &deletePolicy,
}, v12.ListOptions{})
if err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("failed to delete %s: %v", resource, err)
}
return nil
}
@@ -206,15 +206,15 @@ func (r *PaymentReconciler) reconcilePayment(payment *accountv1.Payment) error {
}
switch status {
case pay.PaymentSuccess:
user, err := r.Account.AccountV2.GetUser(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID})
userUID, err := r.Account.AccountV2.GetUserUID(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID})
if err != nil {
return fmt.Errorf("get user failed: %w", err)
return fmt.Errorf("get user UID failed: %w", err)
}
if r.userLock[user.UID] == nil {
r.userLock[user.UID] = &sync.Mutex{}
if r.userLock[userUID] == nil {
r.userLock[userUID] = &sync.Mutex{}
}
r.userLock[user.UID].Lock()
defer r.userLock[user.UID].Unlock()
r.userLock[userUID].Lock()
defer r.userLock[userUID].Unlock()
userDiscount, err := r.Account.AccountV2.GetUserRechargeDiscount(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID})
if err != nil {
return fmt.Errorf("get user discount failed: %w", err)
@@ -223,7 +223,7 @@ func (r *PaymentReconciler) reconcilePayment(payment *accountv1.Payment) error {
payAmount := orderAmount * 10000
isFirstRecharge, gift := getFirstRechargeDiscount(payAmount, userDiscount)
paymentRaw := pkgtypes.PaymentRaw{
UserUID: user.UID,
UserUID: userUID,
Amount: payAmount,
Gift: gift,
CreatedAt: payment.CreationTimestamp.Time,
@@ -295,14 +295,11 @@ func (r *PaymentReconciler) reconcileNewPayment(payment *accountv1.Payment) erro
return fmt.Errorf("user ID is empty")
}
payment.Spec.UserCR = payment.Spec.UserID
user, err := r.Account.AccountV2.GetUser(&pkgtypes.UserQueryOpts{Owner: payment.Spec.UserCR})
id, err := r.Account.AccountV2.GetUserID(&pkgtypes.UserQueryOpts{Owner: payment.Spec.UserCR})
if err != nil {
return fmt.Errorf("get user failed: %w", err)
return fmt.Errorf("get user ID failed: %w", err)
}
if user == nil {
return fmt.Errorf("user not found")
}
payment.Spec.UserID = user.ID
payment.Spec.UserID = id
}
if err := r.Update(context.Background(), payment); err != nil {
return fmt.Errorf("create payment failed: %w", err)
@@ -313,7 +310,7 @@ func (r *PaymentReconciler) reconcileNewPayment(payment *accountv1.Payment) erro
return fmt.Errorf("get account failed: %w", err)
}
if account == nil {
_, err := r.Account.AccountV2.NewAccount(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID})
_, err := r.Account.InitUserAccountFunc(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID})
if err != nil {
return fmt.Errorf("create account failed: %w", err)
}
@@ -0,0 +1,618 @@
package controllers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sync"
"time"
"github.com/labring/sealos/controllers/pkg/utils"
"github.com/google/uuid"
"github.com/labring/sealos/controllers/pkg/database/cockroach"
"github.com/labring/sealos/controllers/pkg/types"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// SubscriptionProcessor 处理订阅事务的处理器
type SubscriptionProcessor struct {
db *gorm.DB
pollInterval time.Duration
wg sync.WaitGroup
stopChan chan struct{}
*AccountReconciler
}
// NewSubscriptionProcessor 创建新的处理器实例
func NewSubscriptionProcessor(reconciler *AccountReconciler) *SubscriptionProcessor {
return &SubscriptionProcessor{
db: reconciler.AccountV2.GetGlobalDB(),
pollInterval: time.Second,
stopChan: make(chan struct{}),
AccountReconciler: reconciler,
}
}
// Start 开始监听和处理订阅事务
func (sp *SubscriptionProcessor) Start(ctx context.Context) error {
sp.wg.Add(1)
go func() {
defer sp.wg.Done()
ticker := time.NewTicker(sp.pollInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-sp.stopChan:
return
case <-ticker.C:
if err := sp.processPendingTransactions(ctx); err != nil {
log.Printf("Failed to process pending transactions: %v", err)
}
}
}
}()
return nil
}
// Stop 停止处理器
func (sp *SubscriptionProcessor) Stop() {
close(sp.stopChan)
sp.wg.Wait()
}
// processPendingTransactions 处理待处理的事务
func (sp *SubscriptionProcessor) processPendingTransactions(ctx context.Context) error {
var transactions []types.SubscriptionTransaction
now := time.Now()
// 查询待处理事务并加锁
err := sp.db.WithContext(ctx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("pay_status IN (?, ?) AND start_at <= ? AND status NOT IN (?, ?)",
types.SubscriptionPayStatusPaid,
types.SubscriptionPayStatusNoNeed,
now,
types.SubscriptionTransactionStatusCompleted,
types.SubscriptionTransactionStatusFailed).
Find(&transactions).Error
if err != nil {
return fmt.Errorf("failed to query pending transactions: %w", err)
}
for i := range transactions {
acc := &types.Account{}
dErr := sp.db.Model(&types.Account{}).Where(&types.Account{UserUID: transactions[i].UserUID}).Find(acc).Error
if dErr != nil {
sp.Logger.Error(fmt.Errorf("failed to fetch account: %w", dErr), "", "user_uid", transactions[i].UserUID)
continue
}
if acc.CreateRegionID != sp.AccountV2.GetLocalRegion().UID.String() {
continue
}
sp.AccountReconciler.Logger.Info("Processing transaction", "id", transactions[i].SubscriptionID, "operator", transactions[i].Operator, "status", transactions[i].Status, "plan", transactions[i].NewPlanName)
if err := sp.processTransaction(ctx, &transactions[i]); err != nil {
sp.Logger.Error(fmt.Errorf("failed to process transaction: %w", err), "", "id", transactions[i].ID)
}
}
return nil
}
// processTransaction 处理单个事务
func (sp *SubscriptionProcessor) processTransaction(ctx context.Context, tx *types.SubscriptionTransaction) error {
return sp.db.Transaction(func(dbTx *gorm.DB) error {
//var latestTx types.SubscriptionTransaction
//if err := dbTx.Clauses(clause.Locking{Strength: "UPDATE"}).
// Find(&latestTx, "subscription_id = ?", tx.SubscriptionID).Error; err != nil {
// return fmt.Errorf("failed to lock transaction %s: %w", tx.SubscriptionID, err)
//}
latestTx := *tx
// 检查是否仍需处理
if !sp.shouldProcessTransaction(&latestTx) {
sp.Logger.Info("Transaction needn't processed", "id", latestTx.ID)
return nil
}
// 根据操作类型分发处理
handler, exists := map[types.SubscriptionOperator]func(context.Context, *gorm.DB, *types.SubscriptionTransaction) error{
types.SubscriptionTransactionTypeCreated: sp.handleCreated,
types.SubscriptionTransactionTypeUpgraded: sp.handleUpgrade,
types.SubscriptionTransactionTypeDowngraded: sp.handleDowngrade,
types.SubscriptionTransactionTypeRenewed: sp.handleRenewal,
}[latestTx.Operator]
if !exists {
sp.Logger.Info("Unknown operator", "operator", latestTx.Operator)
return nil // 未知操作类型,跳过
}
return handler(ctx, dbTx, &latestTx)
})
}
// shouldProcessTransaction 检查事务是否需要处理
func (sp *SubscriptionProcessor) shouldProcessTransaction(tx *types.SubscriptionTransaction) bool {
now := time.Now()
return (tx.PayStatus == types.SubscriptionPayStatusPaid || tx.PayStatus == types.SubscriptionPayStatusNoNeed) &&
!tx.StartAt.After(now) &&
tx.Status != types.SubscriptionTransactionStatusCompleted &&
tx.Status != types.SubscriptionTransactionStatusFailed
}
// If the account service network is too slow, can synchronize the database
//func (sp *SubscriptionProcessor) flushOtherDomainQuota(_ context.Context, userUID uuid.UUID) error {
// var regionTaskList []*types.AccountRegionUserTask
// for _, domain := range sp.allRegionDomain {
// if domain == sp.localDomain {
// continue
// }
// regionTaskList = append(regionTaskList, &types.AccountRegionUserTask{
// UserUID: userUID,
// RegionDomain: domain,
// Type: types.AccountRegionUserTaskTypeFlushQuota,
// StartAt: time.Now().UTC(),
// Status: types.AccountRegionUserTaskStatusPending,
// })
// }
// err := sp.AccountV2.GetGlobalDB().Transaction(func(tx *gorm.DB) error {
// for _, task := range regionTaskList {
// if err := tx.Create(task).Error; err != nil {
// return err
// }
// }
// return nil
// })
// if err != nil {
// return fmt.Errorf("failed to create account region user task: %w", err)
// }
// return nil
//}
// updateQuota 更新用户的资源配额
func (sp *SubscriptionProcessor) updateQuota(_ context.Context, userUID, planID uuid.UUID, planName string) error {
//if err := sp.flushOtherDomainQuota(ctx, userUID); err != nil {
// return fmt.Errorf("failed to flush other domain quota: %w", err)
//}
if err := sp.sendFlushQuotaRequest(userUID, planID, planName); err != nil {
return fmt.Errorf("failed to send flush quota request: %w", err)
}
return nil
}
const AdminUserName = "sealos-admin"
type AdminFlushSubscriptionQuotaReq struct {
UserUID uuid.UUID `json:"userUID" bson:"userUID"`
PlanName string `json:"planName" bson:"planName"`
PlanID uuid.UUID `json:"planID" bson:"planID"`
}
// 延迟过高
func (sp *SubscriptionProcessor) sendFlushQuotaRequest(userUID, planID uuid.UUID, planName string) error {
for _, domain := range sp.allRegionDomain {
token, err := sp.jwtManager.GenerateToken(utils.JwtUser{
Requester: AdminUserName,
})
if err != nil {
return fmt.Errorf("failed to generate token: %w", err)
}
url := fmt.Sprintf("https://account-api.%s/admin/v1alpha1/flush-sub-quota", domain)
quotaReq := AdminFlushSubscriptionQuotaReq{
UserUID: userUID,
PlanID: planID,
PlanName: planName,
}
quotaReqBody, err := json.Marshal(quotaReq)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
var lastErr error
backoffTime := time.Second
maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ {
req, err := http.NewRequest("POST", url, bytes.NewBuffer(quotaReqBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
lastErr = fmt.Errorf("failed to send request: %w", err)
} else {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
lastErr = nil
break
}
lastErr = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
// 进行重试
if attempt < maxRetries {
fmt.Printf("Attempt %d failed: %v. Retrying in %v...\n", attempt, lastErr, backoffTime)
time.Sleep(backoffTime)
backoffTime *= 2 // 指数增长退避时间
}
}
if lastErr != nil {
return lastErr
}
}
return nil
}
// handleCreated 处理创建订阅
func (sp *SubscriptionProcessor) handleCreated(ctx context.Context, dbTx *gorm.DB, tx *types.SubscriptionTransaction) error {
var sub types.Subscription
if err := dbTx.Model(&types.Subscription{}).Where(&types.Subscription{UserUID: tx.UserUID, ID: tx.SubscriptionID}).Find(&sub).Error; err != nil {
return fmt.Errorf("failed to fetch subscription: %w", err)
}
now := time.Now().UTC()
sub.PlanID = tx.NewPlanID
sub.PlanName = tx.NewPlanName
sub.Status = types.SubscriptionStatusNormal
sub.StartAt = now
sub.UpdateAt = now
sub.ExpireAt = now.AddDate(0, 1, 0)
sub.NextCycleDate = sub.ExpireAt
if err := dbTx.Save(&sub).Error; err != nil {
return fmt.Errorf("failed to update subscription: %w", err)
}
// 更新配额
if err := sp.updateQuota(ctx, sub.UserUID, tx.NewPlanID, tx.NewPlanName); err != nil {
return fmt.Errorf("failed to update quota: %w", err)
}
// 创建积分
plan, err := sp.AccountV2.GetSubscriptionPlan(tx.NewPlanName)
if err != nil {
return fmt.Errorf("failed to get subscription plan: %w", err)
}
if err := cockroach.CreateCredits(dbTx, &types.Credits{
UserUID: sub.UserUID,
Amount: plan.GiftAmount,
FromID: sub.PlanID.String(),
FromType: types.CreditsFromTypeSubscription,
ExpireAt: sub.ExpireAt,
CreatedAt: now,
StartAt: now,
Status: types.CreditsStatusActive,
}); err != nil {
return fmt.Errorf("failed to create credits: %w", err)
}
//TODO create Credits Transaction
tx.Status = types.SubscriptionTransactionStatusCompleted
tx.UpdatedAt = now
return dbTx.Save(tx).Error
}
// handleUpgrade 处理升级
func (sp *SubscriptionProcessor) handleUpgrade(ctx context.Context, dbTx *gorm.DB, tx *types.SubscriptionTransaction) error {
var sub types.Subscription
if err := dbTx.Model(&types.Subscription{}).Where(&types.Subscription{UserUID: tx.UserUID, ID: tx.SubscriptionID}).Find(&sub).Error; err != nil {
return fmt.Errorf("failed to fetch subscription: %w", err)
}
now := time.Now()
// 更新订阅信息
sub.PlanID = tx.NewPlanID
sub.PlanName = tx.NewPlanName
sub.Status = types.SubscriptionStatusNormal
sub.StartAt = now
sub.UpdateAt = now
sub.ExpireAt = now.AddDate(0, 1, 0)
sub.NextCycleDate = sub.ExpireAt
if err := dbTx.Save(&sub).Error; err != nil {
return fmt.Errorf("failed to update subscription: %w", err)
}
// 更新配额
if err := sp.updateQuota(ctx, sub.UserUID, tx.NewPlanID, tx.NewPlanName); err != nil {
return err
}
err := dbTx.Model(&types.Credits{}).Where(&types.Credits{
UserUID: sub.UserUID,
FromID: tx.OldPlanID.String(),
FromType: types.CreditsFromTypeSubscription,
}).Where("expire_at > ? AND status = ?", now, types.CreditsStatusActive).Update("status", types.CreditsStatusExpired).Error
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to update credits: %w", err)
}
// 更新积分
plan, err := sp.AccountV2.GetSubscriptionPlan(tx.NewPlanName)
if err != nil {
return fmt.Errorf("failed to get subscription plan: %w", err)
}
var credits = types.Credits{
UserUID: sub.UserUID,
FromType: types.CreditsFromTypeSubscription,
FromID: sub.PlanID.String(),
Status: types.CreditsStatusActive,
Amount: plan.GiftAmount,
ExpireAt: sub.NextCycleDate,
CreatedAt: now,
StartAt: now,
}
if err := dbTx.Save(&credits).Error; err != nil {
return fmt.Errorf("failed to update credits: %w", err)
}
tx.Status = types.SubscriptionTransactionStatusCompleted
tx.UpdatedAt = time.Now().UTC()
return dbTx.Save(tx).Error
}
// handleDowngrade 处理降级
func (sp *SubscriptionProcessor) handleDowngrade(ctx context.Context, dbTx *gorm.DB, tx *types.SubscriptionTransaction) error {
var sub types.Subscription
if err := dbTx.Model(&types.Subscription{}).Where(&types.Subscription{UserUID: tx.UserUID, ID: tx.SubscriptionID}).Find(&sub).Error; err != nil {
return fmt.Errorf("failed to fetch subscription: %w", err)
}
if ok, err := sp.checkDowngradeConditions(ctx, &sub, tx.NewPlanID); err != nil {
return fmt.Errorf("failed to check downgrade conditions: %w", err)
} else if !ok {
tx.Status = types.SubscriptionTransactionStatusFailed
return dbTx.Save(tx).Error
}
if ok, err := sp.checkQuotaConditions(ctx, sub.UserUID, tx.NewPlanID, tx.NewPlanName); err != nil {
return fmt.Errorf("failed to check quota conditions: %w", err)
} else if !ok {
tx.Status = types.SubscriptionTransactionStatusFailed
return dbTx.Save(tx).Error
}
now := time.Now()
sub.PlanID = tx.NewPlanID
sub.PlanName = tx.NewPlanName
sub.Status = types.SubscriptionStatusNormal
sub.StartAt = now
sub.UpdateAt = now
sub.ExpireAt = now.AddDate(0, 1, 0)
sub.NextCycleDate = sub.ExpireAt
if err := dbTx.Save(&sub).Error; err != nil {
return fmt.Errorf("failed to update subscription: %w", err)
}
tx.Status = types.SubscriptionTransactionStatusCompleted
tx.UpdatedAt = now
if err := dbTx.Save(tx).Error; err != nil {
return fmt.Errorf("failed to update transaction: %w", err)
}
// 更新配额
return sp.updateQuota(ctx, sub.UserUID, tx.NewPlanID, tx.NewPlanName)
}
// handleRenewal 处理续订
func (sp *SubscriptionProcessor) handleRenewal(ctx context.Context, dbTx *gorm.DB, tx *types.SubscriptionTransaction) error {
var sub types.Subscription
if err := dbTx.Model(&types.Subscription{}).Where(&types.Subscription{UserUID: tx.UserUID, ID: tx.SubscriptionID}).Find(&sub).Error; err != nil {
return fmt.Errorf("failed to fetch subscription: %w", err)
}
// 更新订阅时间
now := time.Now()
sub.Status = types.SubscriptionStatusNormal
sub.StartAt = now
sub.UpdateAt = now
sub.ExpireAt = now.AddDate(0, 1, 0)
sub.NextCycleDate = sub.ExpireAt
if err := dbTx.Save(&sub).Error; err != nil {
return fmt.Errorf("failed to update subscription: %w", err)
}
// //TODO: 续费赠送 credits
plan, err := sp.AccountV2.GetSubscriptionPlan(tx.NewPlanName)
if err != nil {
return fmt.Errorf("failed to get subscription plan: %w", err)
}
if plan.GiftAmount > 0 {
// 过期之前的 credits
err := dbTx.Model(&types.Credits{}).Where(&types.Credits{
UserUID: sub.UserUID,
FromID: sub.PlanID.String(),
FromType: types.CreditsFromTypeSubscription,
}).Update("status", types.CreditsStatusExpired).Error
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to update credits: %w", err)
}
if err := cockroach.CreateCredits(dbTx, &types.Credits{
UserUID: sub.UserUID,
Amount: plan.GiftAmount,
FromID: sub.PlanID.String(),
FromType: types.CreditsFromTypeSubscription,
ExpireAt: sub.NextCycleDate,
CreatedAt: now,
StartAt: now,
Status: types.CreditsStatusActive,
}); err != nil {
return fmt.Errorf("failed to create credits: %w", err)
}
}
tx.Status = types.SubscriptionTransactionStatusCompleted
tx.UpdatedAt = now
return dbTx.Save(tx).Error
}
// checkDowngradeConditions 检查降级条件
func (sp *SubscriptionProcessor) checkDowngradeConditions(_ context.Context, subscription *types.Subscription, planID uuid.UUID) (bool, error) {
// //TODO: 检查 disk、namespace、seat 等条件
token, err := sp.desktopJwtManager.GenerateToken(utils.JwtUser{
UserUID: subscription.UserUID,
RegionUID: sp.AccountV2.GetLocalRegion().UID.String(),
})
if err != nil {
return false, fmt.Errorf("failed to generate token: %w", err)
}
url := "http://desktop-frontend.sealos.svc.cluster.local:3000/api/v1alpha/downGrade/check"
var lastErr error
backoffTime := time.Second
maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ {
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(fmt.Sprintf(`{"subscriptionPlanId": "%s"}`, planID))))
if err != nil {
return false, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", token)
req.Header.Set("Content-Type", "application/json")
client := http.Client{
Timeout: 10 * time.Minute,
}
resp, err := client.Do(req)
if err != nil {
lastErr = fmt.Errorf("failed to send request: %w", err)
} else {
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, fmt.Errorf("failed to read response: %w", err)
} else if resp.StatusCode == http.StatusOK {
var response APIResponse
if err = json.Unmarshal(body, &response); err != nil {
return false, fmt.Errorf("failed to unmarshal response: %w", err)
}
if response.Code != 200 {
return false, fmt.Errorf("response code is not 200: %d", response.Code)
}
return response.Data.AllWorkspaceReady && response.Data.SeatReady, nil
} else if resp.StatusCode >= 500 {
lastErr = fmt.Errorf("unexpected status code: %d; %s", resp.StatusCode, string(body))
} else {
return false, fmt.Errorf("client error: %d; %s", resp.StatusCode, string(body))
}
}
// 进行重试
if attempt < maxRetries {
fmt.Printf("Attempt %d failed: %v. Retrying in %v...\n", attempt, lastErr, backoffTime)
time.Sleep(backoffTime)
backoffTime *= 2 // 指数增长退避时间
}
}
return false, lastErr
}
type SubscriptionQuotaCheckReq struct {
// @Summary PlanID
// @Description PlanID
PlanID uuid.UUID `json:"planID" bson:"planID" example:"123e4567-e89b-12d3-a456-426614174000"`
// @Summary PlanName
// @Description PlanName
PlanName string `json:"planName" bson:"planName" example:"planName"`
}
type SubscriptionQuotaCheckResp struct {
//allWorkspaceReady
AllWorkspaceReady bool `json:"allWorkspaceReady" bson:"allWorkspaceReady" example:"true"`
ReadyWorkspace []string `json:"readyWorkspace" bson:"readyWorkspace" example:"workspace1,workspace2"`
UnReadyWorkspace []string `json:"unReadyWorkspace" bson:"unReadyWorkspace" example:"workspace3,workspace4"`
}
// checkDowngradeConditions 检查降级条件
func (sp *SubscriptionProcessor) checkQuotaConditions(_ context.Context, userUID, planID uuid.UUID, planName string) (bool, error) {
for _, domain := range sp.allRegionDomain {
token, err := sp.jwtManager.GenerateToken(utils.JwtUser{
UserUID: userUID,
//RegionUID: sp.AccountV2.GetLocalRegion().UID.String(),
})
if err != nil {
return false, fmt.Errorf("failed to generate token: %w", err)
}
url := fmt.Sprintf("http://account-api.%s/payment/v1alpha1/subscription/quota-check", domain)
quotaReq := SubscriptionQuotaCheckReq{
PlanID: planID,
PlanName: planName,
}
quotaReqBody, err := json.Marshal(quotaReq)
if err != nil {
return false, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(quotaReqBody))
if err != nil {
return false, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", token)
req.Header.Set("Content-Type", "application/json")
client := http.Client{
Timeout: 10 * time.Minute,
}
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("failed to send request: %w", err)
} else {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, fmt.Errorf("failed to read response: %w", err)
} else if resp.StatusCode == http.StatusOK {
var response SubscriptionQuotaCheckResp
if err = json.Unmarshal(body, &response); err != nil {
return false, fmt.Errorf("failed to unmarshal response: %w", err)
}
if !response.AllWorkspaceReady {
return false, nil
}
continue
}
return false, fmt.Errorf("client error: %d; %s", resp.StatusCode, string(body))
}
}
return true, nil
}
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data Data `json:"data"`
}
// Data 数据部分结构体
type Data struct {
AllWorkspaceReady bool `json:"allWorkspaceReady"`
SeatReady bool `json:"seatReady"`
MaxWorkspace int `json:"max_workspace"`
MaxSeat int `json:"max_seat"`
GroupedWorkspaceUsage map[string]WorkspaceGroup `json:"groupedWorkspaceUsage"`
}
// WorkspaceGroup 工作空间组结构体
type WorkspaceGroup struct {
Workspaces []Workspace `json:"workspaces"`
}
// Workspace 单个工作空间结构体
type Workspace struct {
RegionUID string `json:"regionUid"`
WorkspaceUID string `json:"workspaceUid"`
Seat int `json:"seat"`
}
+12 -1
View File
@@ -20,6 +20,7 @@ type SMTPConfig struct {
ServerHost string
ServerPort int
FromEmail string
Username string
Passwd string
EmailTitle string
}
@@ -30,6 +31,16 @@ func (c *SMTPConfig) SendEmail(emailBody, to string) error {
m.SetAddressHeader("From", c.FromEmail, c.EmailTitle)
m.SetHeader("Subject", c.EmailTitle)
m.SetBody("text/html", emailBody)
d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.FromEmail, c.Passwd)
d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd)
return d.DialAndSend(m)
}
func (c *SMTPConfig) SendEmailWithTitle(subject, emailBody, to string) error {
m := gomail.NewMessage()
m.SetHeader("To", to)
m.SetAddressHeader("From", c.FromEmail, c.EmailTitle)
m.SetHeader("Subject", subject)
m.SetBody("text/html", emailBody)
d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd)
return d.DialAndSend(m)
}
@@ -566,6 +566,39 @@ rules:
- get
- list
- watch
- apiGroups: [""]
resources: ["persistentvolumeclaims", "services"]
verbs: ["delete", "deletecollection"]
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets"]
verbs: ["delete", "deletecollection"]
- apiGroups: ["batch"]
resources: ["cronjobs"]
verbs: ["delete", "deletecollection"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["delete", "deletecollection"]
- apiGroups: ["autoscaling"]
resources: ["horizontalpodautoscalers"]
verbs: ["delete", "deletecollection"]
- apiGroups: ["cert-manager.io"]
resources: ["issuers", "certificates"]
verbs: ["delete", "deletecollection"]
- apiGroups: ["dataprotection.kubeblocks.io"]
resources: ["backups", "backupschedules"]
verbs: ["delete", "deletecollection"]
- apiGroups: ["apps.kubeblocks.io"]
resources: ["clusters"]
verbs: ["delete", "deletecollection"]
- apiGroups: ["objectstorage.sealos.io"]
resources: ["objectstorageusers"]
verbs: ["delete", "deletecollection"]
- apiGroups: ["app.sealos.io"]
resources: ["instances"]
verbs: ["delete", "deletecollection"]
- apiGroups: ["devbox.sealos.io"]
resources: ["devboxes", "devboxreleases"]
verbs: ["delete", "deletecollection"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
+13 -12
View File
@@ -3,6 +3,7 @@ module github.com/labring/sealos/controllers/account
go 1.22
replace (
github.com/apecloud/kubeblocks => github.com/apecloud/kubeblocks v0.8.4
k8s.io/api => k8s.io/api v0.28.3
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.28.3
k8s.io/apimachinery => k8s.io/apimachinery v0.28.3
@@ -29,11 +30,11 @@ require (
github.com/onsi/gomega v1.30.0
github.com/volcengine/volc-sdk-golang v1.0.159
go.mongodb.org/mongo-driver v1.12.1
golang.org/x/sync v0.6.0
gorm.io/gorm v1.25.5
k8s.io/api v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/client-go v12.0.0+incompatible
k8s.io/utils v0.0.0-20231127182322-b307cd553661
sigs.k8s.io/controller-runtime v0.17.2
)
@@ -60,6 +61,7 @@ require (
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
@@ -78,7 +80,8 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@@ -91,7 +94,7 @@ require (
github.com/montanaflynn/stats v0.6.6 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
@@ -125,19 +128,18 @@ require (
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/grpc v1.61.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
@@ -151,7 +153,6 @@ require (
k8s.io/component-base v0.29.0 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
+20 -18
View File
@@ -156,8 +156,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 h1:4GI5eviCwbPxDE311KryyyPUTO7IDVyHGp3Iyl+fEZY=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49/go.mod h1:sbm1DAsayX+XsXCOC2CFAAU9JZhX0SPKwnybDjSd0Ls=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
@@ -239,6 +237,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
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/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@@ -414,8 +414,8 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
@@ -432,6 +432,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de h1:V53FWzU6KAZVi1tPp5UIsMoUWJ2/PNwYIDXnu7QuBCE=
github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
@@ -524,8 +526,8 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM=
github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
@@ -648,7 +650,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@@ -745,8 +746,9 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
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=
@@ -847,8 +849,8 @@ golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -954,8 +956,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -965,8 +968,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -982,8 +986,9 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1132,8 +1137,6 @@ google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1154,8 +1157,6 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1169,8 +1170,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
+56 -16
View File
@@ -22,6 +22,10 @@ import (
"os"
"time"
"github.com/labring/sealos/controllers/pkg/utils/env"
"github.com/labring/sealos/controllers/pkg/utils/maps"
"github.com/labring/sealos/controllers/account/controllers/cache"
"github.com/labring/sealos/controllers/pkg/database"
"github.com/labring/sealos/controllers/pkg/database/cockroach"
@@ -169,12 +173,26 @@ func main() {
setupLog.Error(err, "unable to disconnect from cockroach")
}
}()
if err = database.InitRegionEnv(v2Account.GetGlobalDB(), v2Account.GetLocalRegion().Domain); err != nil {
setupLog.Error(err, "unable to init region env")
os.Exit(1)
}
skipExpiredUserTimeDuration := time.Hour * 24 * 2
if os.Getenv("SKIP_EXPIRED_USER_TIME") != "" {
skipExpiredUserTimeDuration, err = time.ParseDuration(os.Getenv("SKIP_EXPIRED_USER_TIME"))
if err != nil {
setupLog.Error(err, "unable to parse skip expired user time")
os.Exit(1)
}
}
setupLog.Info("skip expired user time", "duration", skipExpiredUserTimeDuration)
accountReconciler := &controllers.AccountReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
DBClient: dbClient,
AccountV2: v2Account,
CVMDBClient: cvmDBClient,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
DBClient: dbClient,
AccountV2: v2Account,
CVMDBClient: cvmDBClient,
SkipExpiredUserTimeDuration: skipExpiredUserTimeDuration,
}
activities, discountSteps, discountRatios, err := controllers.RawParseRechargeConfig()
if err != nil {
@@ -194,13 +212,28 @@ func main() {
if err = (accountReconciler).SetupWithManager(mgr, rateOpts); err != nil {
setupManagerError(err, "Account")
}
if err = (&controllers.DebtReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
AccountV2: v2Account,
}).SetupWithManager(mgr, rateOpts); err != nil {
setupManagerError(err, "Debt")
debtUserMap := maps.NewConcurrentMap()
debtController := &controllers.DebtReconciler{
AccountReconciler: accountReconciler,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
AccountV2: v2Account,
DebtUserMap: debtUserMap,
InitUserAccountFunc: accountReconciler.InitUserAccountFunc,
SkipExpiredUserTimeDuration: skipExpiredUserTimeDuration,
}
debtController.Init()
//if err = (&controllers.DebtReconciler{
// AccountReconciler: accountReconciler,
// Client: mgr.GetClient(),
// Scheme: mgr.GetScheme(),
// AccountV2: v2Account,
// DebtUserMap: debtUserMap,
// InitUserAccountFunc: accountReconciler.InitUserAccountFunc,
// SkipExpiredUserTimeDuration: skipExpiredUserTimeDuration,
//}).SetupWithManager(mgr, rateOpts); err != nil {
// setupManagerError(err, "Debt")
//}
if err = cache.SetupCache(mgr); err != nil {
setupLog.Error(err, "unable to cache controller")
@@ -218,11 +251,12 @@ func main() {
os.Exit(1)
}
billingReconciler := controllers.BillingReconciler{
DBClient: dbClient,
Properties: resources.DefaultPropertyTypeLS,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
AccountV2: v2Account,
DBClient: dbClient,
Properties: resources.DefaultPropertyTypeLS,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
AccountV2: v2Account,
DebtUserMap: debtUserMap,
}
if err = billingReconciler.Init(); err != nil {
setupLog.Error(err, "unable to init billing reconciler")
@@ -235,6 +269,12 @@ func main() {
setupLog.Error(err, "unable to add billing task runner")
os.Exit(1)
}
if env.GetEnvWithDefault("SUPPORT_DEBT", "true") == "true" {
if err := mgr.Add(debtController); err != nil {
setupLog.Error(err, "unable to add debt controller")
os.Exit(1)
}
}
if err = (&controllers.PodReconciler{
Client: mgr.GetClient(),
+24 -16
View File
@@ -11,14 +11,14 @@ require (
github.com/onsi/gomega v1.30.0
k8s.io/api v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/client-go v0.29.0
k8s.io/client-go v12.0.0+incompatible
sigs.k8s.io/controller-runtime v0.17.2
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.8.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
@@ -26,10 +26,10 @@ require (
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
@@ -38,29 +38,28 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -69,10 +68,19 @@ require (
k8s.io/component-base v0.29.0 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
replace github.com/labring/sealos/controllers/pkg => ../../pkg
replace (
k8s.io/api => k8s.io/api v0.28.3
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.28.3
k8s.io/apimachinery => k8s.io/apimachinery v0.28.3
k8s.io/client-go => k8s.io/client-go v0.28.3
k8s.io/component-base => k8s.io/component-base v0.28.3
sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.17.2
)
+56 -43
View File
@@ -4,8 +4,9 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
@@ -25,8 +26,9 @@ github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
@@ -35,7 +37,6 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -43,8 +44,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -81,8 +83,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -107,12 +107,12 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
@@ -127,10 +127,11 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -140,25 +141,29 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -167,23 +172,31 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -192,8 +205,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -202,8 +215,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=
@@ -221,22 +234,22 @@ 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A=
k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA=
k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0=
k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc=
k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o=
k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis=
k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8=
k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38=
k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s=
k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M=
k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM=
k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc=
k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08=
k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc=
k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4=
k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo=
k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI=
k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8=
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI=
k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0=
sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+1049 -5
View File
File diff suppressed because it is too large Load Diff
+8 -11
View File
@@ -6,7 +6,6 @@ require (
github.com/google/uuid v1.6.0
github.com/labring/sealos/controllers/pkg v0.0.0-20240715064441-d1193f70675b
github.com/labring/sealos/controllers/user v0.0.0
github.com/matoous/go-nanoid/v2 v2.0.0
gorm.io/gorm v1.25.5
k8s.io/apimachinery v0.29.0
k8s.io/client-go v12.0.0+incompatible
@@ -40,8 +39,9 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/labring/sealos/controllers/account v0.0.0-00010101000000-000000000000 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matoous/go-nanoid/v2 v2.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -51,24 +51,21 @@ require (
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.4 // indirect
go.mongodb.org/mongo-driver v1.12.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/grpc v1.61.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
+16 -20
View File
@@ -7,8 +7,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 h1:4GI5eviCwbPxDE311KryyyPUTO7IDVyHGp3Iyl+fEZY=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49/go.mod h1:sbm1DAsayX+XsXCOC2CFAAU9JZhX0SPKwnybDjSd0Ls=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
@@ -82,6 +80,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
@@ -128,8 +128,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
@@ -150,8 +150,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -164,8 +164,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -182,20 +182,20 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -213,14 +213,10 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=
+1 -1
View File
@@ -85,7 +85,7 @@ require (
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/grpc v1.61.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
+2 -4
View File
@@ -11,8 +11,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 h1:4GI5eviCwbPxDE311KryyyPUTO7IDVyHGp3Iyl+fEZY=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49/go.mod h1:sbm1DAsayX+XsXCOC2CFAAU9JZhX0SPKwnybDjSd0Ls=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
@@ -241,8 +239,8 @@ google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=
File diff suppressed because it is too large Load Diff
@@ -64,3 +64,54 @@ func TestCockroach_GetAccountWithWorkspace(t *testing.T) {
}
t.Logf("account: %+v", account)
}
func TestCockroach_InitTables(t *testing.T) {
os.Setenv("LOCAL_REGION", "")
ck, err := NewCockRoach("", "")
if err != nil {
t.Errorf("NewCockRoach() error = %v", err)
return
}
defer ck.Close()
//uid, err := uuid.Parse("9477dc81-de9a-48b0-b88e-5b3ec6c33a54")
//if err != nil {
// t.Fatalf("uuid.Parse() error = %v", err)
//}
//-2.77
ops := &types.UserQueryOpts{
//UID: uid,
//ID: "9F5NY4_lbS",
Owner: "6it2bra2",
IgnoreEmpty: true,
}
userUID, err := ck.GetUser(ops)
if err != nil {
t.Fatalf("GetUserUID() error = %v", err)
}
t.Logf("userUID: %+v", userUID)
//err = ck.InitTables()
//if err != nil {
// t.Errorf("InitTables() error = %v", err)
// return
//}
//
//
//err = ck.CreateCredits(&types.Credits{
// UserUID: uid,
// Amount: 100000000,
// ExpireAt: time.Now().UTC().Add(10 * 365 * 24 * time.Hour),
// StartAt: time.Now().UTC(),
// Status: types.CreditsStatusActive,
//})
//if err != nil {
// t.Fatalf("CreateCredits() error = %v", err)
//}
//
//err = ck.AddDeductionBalanceWithCredits(ops, 10_000000, []string{"order1", "order2"})
//if err != nil {
// t.Fatalf("AddDeductionBalanceWithCredits() error = %v", err)
//}
}
+51 -1
View File
@@ -16,8 +16,13 @@ package database
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
"go.mongodb.org/mongo-driver/bson/primitive"
@@ -90,9 +95,14 @@ type Traffic interface {
type AccountV2 interface {
Close() error
GetGlobalDB() *gorm.DB
GetUserCr(user *types.UserQueryOpts) (*types.RegionUserCr, error)
GetUser(ops *types.UserQueryOpts) (*types.User, error)
GetUserUID(ops *types.UserQueryOpts) (uuid.UUID, error)
GetUserID(ops *types.UserQueryOpts) (string, error)
GetAccount(user *types.UserQueryOpts) (*types.Account, error)
GetAccountWithCredits(userUID uuid.UUID) (*types.UsableBalanceWithCredits, error)
GetBalanceWithCredits(ops *types.UserQueryOpts) (*types.BalanceWithCredits, error)
GetAccountConfig() (types.AccountConfig, error)
InsertAccountConfig(config *types.AccountConfig) error
GetRegions() ([]types.Region, error)
@@ -103,12 +113,28 @@ type AccountV2 interface {
SetAccountCreateLocalRegion(account *types.Account, region string) error
CreateUser(oAuth *types.OauthProvider, regionUserCr *types.RegionUserCr, user *types.User, workspace *types.Workspace, userWorkspace *types.UserWorkspace) error
AddBalance(user *types.UserQueryOpts, balance int64) error
AddDeductionBalanceWithCredits(ops *types.UserQueryOpts, amount int64, orderIDs []string) error
ReduceBalance(ops *types.UserQueryOpts, amount int64) error
ReduceDeductionBalance(ops *types.UserQueryOpts, amount int64) error
NewAccount(user *types.UserQueryOpts) (*types.Account, error)
NewAccountWithFreeSubscriptionPlan(ops *types.UserQueryOpts) (*types.Account, error)
GetSubscriptionPlan(planName string) (*types.SubscriptionPlan, error)
Payment(payment *types.Payment) error
PaymentWithFunc(payment *types.Payment, preDo, postDo func(tx *gorm.DB) error) error
GlobalTransactionHandler(funcs ...func(tx *gorm.DB) error) error
SavePayment(payment *types.Payment) error
GetUnInvoicedPaymentListWithIds(ids []string) ([]types.Payment, error)
GetUnInvoicedPaymentListWithIDs(ids []string) ([]types.Payment, error)
CreatePaymentOrder(order *types.PaymentOrder) error
CreateSubscription(subscription *types.Subscription) error
SetCardInfo(info *types.CardInfo) (uuid.UUID, error)
GetCardInfo(cardID, userUID uuid.UUID) (*types.CardInfo, error)
GetAllCardInfo(ops *types.UserQueryOpts) ([]types.CardInfo, error)
GetSubscription(ops *types.UserQueryOpts) (*types.Subscription, error)
GetSubscriptionPlanList() ([]types.SubscriptionPlan, error)
SetSubscriptionPlanList(plans []types.SubscriptionPlan) error
GetCardList(ops *types.UserQueryOpts) ([]types.CardInfo, error)
DeleteCardInfo(id uuid.UUID, userUID uuid.UUID) error
SetDefaultCard(cardID uuid.UUID, userUID uuid.UUID) error
CreateAccount(ops *types.UserQueryOpts, account *types.Account) (*types.Account, error)
TransferAccount(from, to *types.UserQueryOpts, amount int64) error
TransferAccountAll(from, to *types.UserQueryOpts) error
@@ -137,3 +163,27 @@ var _ = AccountV2(&cockroach.Cockroach{})
func NewAccountV2(globalURI, localURI string) (AccountV2, error) {
return cockroach.NewCockRoach(globalURI, localURI)
}
func InitRegionEnv(db *gorm.DB, localDomain string) error {
var regionENV []types.RegionConfig
if err := db.Model(&types.RegionConfig{}).Find(&regionENV).Error; err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to get region env: %v", err)
}
// set global env
for _, envCfg := range regionENV {
if strings.ToUpper(envCfg.Region) == "GLOBAL" {
if err := os.Setenv(envCfg.Key, envCfg.Value); err != nil {
return fmt.Errorf("set global env error: %v", err)
}
}
}
// region env Cover
for _, envCfg := range regionENV {
if envCfg.Region == localDomain {
if err := os.Setenv(envCfg.Key, envCfg.Value); err != nil {
return fmt.Errorf("set region env error: %v", err)
}
}
}
return nil
}
@@ -513,6 +513,9 @@ func (m *mongoDB) GenerateBillingData(startTime, endTime time.Time, prols *resou
if err != nil {
return nil, fmt.Errorf("failed to generate billing data: %v", err)
}
if len(billings) == 0 {
continue
}
ownerBillings[owner] = billings
}
return ownerBillings, nil
+24 -34
View File
@@ -20,67 +20,61 @@ replace (
require (
github.com/containers/storage v1.50.2
github.com/dustin/go-humanize v1.0.1
github.com/go-logr/logr v1.4.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.6.0
github.com/labring/sealos/controllers/account v0.0.0-00010101000000-000000000000
github.com/lib/pq v1.10.9
github.com/matoous/go-nanoid/v2 v2.0.0
github.com/minio/minio-go/v7 v7.0.64
github.com/prometheus/client_golang v1.18.0
github.com/prometheus/client_golang v1.19.0
github.com/prometheus/client_model v0.5.0
github.com/prometheus/common v0.45.0
github.com/prometheus/common v0.48.0
github.com/prometheus/prom2json v1.3.3
github.com/spf13/pflag v1.0.5
github.com/stripe/stripe-go/v74 v74.30.0
github.com/wechatpay-apiv3/wechatpay-go v0.2.17
go.mongodb.org/mongo-driver v1.12.1
go.uber.org/zap v1.26.0
golang.org/x/time v0.3.0
golang.org/x/time v0.5.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
k8s.io/api v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/client-go v0.29.0
k8s.io/client-go v12.0.0+incompatible
sigs.k8s.io/controller-runtime v0.17.2
sigs.k8s.io/yaml v1.4.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.8.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jackc/pgx/v5 v5.5.4 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/labring/sealos/controllers/user v0.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
@@ -91,10 +85,10 @@ require (
github.com/opencontainers/runc v1.1.9 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
@@ -102,27 +96,23 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/apiextensions-apiserver v0.29.0 // indirect
k8s.io/component-base v0.29.0 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
+45 -59
View File
@@ -8,8 +8,9 @@ github.com/containers/storage v1.50.2 h1:Fys4BjFUVNRBEXlO70hFI48VW4EXsgnGisTpk9t
github.com/containers/storage v1.50.2/go.mod h1:dpspZsUrcKD8SpTofvKWhwPDHD0MkO4Q7VE+oYdWkiA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -20,8 +21,6 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro=
github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -31,21 +30,22 @@ github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
@@ -69,8 +69,10 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=
github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -87,8 +89,8 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -98,6 +100,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
@@ -105,8 +109,6 @@ github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.64 h1:Zdza8HwOzkld0ZG/og50w56fKi6AAyfqfifmasD9n2Q=
@@ -127,10 +129,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134=
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY=
github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
@@ -143,12 +141,12 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo=
@@ -170,8 +168,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
@@ -204,15 +202,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -220,10 +217,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -241,23 +238,22 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -269,18 +265,12 @@ 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=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=
@@ -290,8 +280,6 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.8/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=
@@ -310,14 +298,12 @@ k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4=
k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo=
k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI=
k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8=
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI=
k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0=
sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+56
View File
@@ -15,9 +15,13 @@
package resources
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/labring/sealos/controllers/pkg/types"
"go.mongodb.org/mongo-driver/bson/primitive"
"github.com/google/uuid"
@@ -176,6 +180,13 @@ const (
Settled
)
const (
// Consumption 消费
Consumption common.Type = iota
//Subconsumption 子消费
SubConsumption
)
const (
// DB = 1
// APP = 2
@@ -440,6 +451,51 @@ func DefaultResourceQuotaHard() corev1.ResourceList {
}
}
func ParseResourceLimitWithSubscription(plans []types.SubscriptionPlan) (map[string]corev1.ResourceList, error) {
subPlansLimit := make(map[string]corev1.ResourceList)
for i := range plans {
//max_resources: {"cpu":"128","memory":"256Gi","storage":"500Gi"}
res := plans[i].MaxResources
if res == "" {
subPlansLimit[plans[i].Name] = DefaultResourceQuotaHard()
} else {
var maxResources map[string]string
if err := json.Unmarshal([]byte(res), &maxResources); err != nil {
return nil, fmt.Errorf("parse max_resources failed: %v", err)
}
rl := make(corev1.ResourceList)
for k, v := range maxResources {
_v, err := ParseCustomQuantity(v)
if err != nil {
return nil, fmt.Errorf("parse %s failed: %v", k, err)
}
switch k {
case "cpu":
rl[corev1.ResourceLimitsCPU] = _v
case "memory":
rl[corev1.ResourceLimitsMemory] = _v
case "storage":
rl[corev1.ResourceRequestsStorage] = _v
case "nodeports":
rl[corev1.ResourceServicesNodePorts] = _v
case ResourceObjectStorageSize.String():
rl[ResourceObjectStorageSize] = _v
case ResourceObjectStorageBucket.String():
rl[ResourceObjectStorageBucket] = _v
}
}
subPlansLimit[plans[i].Name] = rl
}
}
return subPlansLimit, nil
}
func ParseCustomQuantity(s string) (resource.Quantity, error) {
s = strings.Replace(s, "GiB", "Gi", 1)
s = strings.Replace(s, "MiB", "Mi", 1)
return resource.ParseQuantity(s)
}
func DefaultLimitRangeLimits() []corev1.LimitRangeItem {
return []corev1.LimitRangeItem{
{
+76
View File
@@ -0,0 +1,76 @@
// Copyright © 2025 sealos.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"time"
"github.com/google/uuid"
)
// Credits 表示用户的credits信息
type Credits struct {
ID uuid.UUID `json:"id" gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` // credits ID
UserUID uuid.UUID `json:"user_uid" gorm:"column:user_uid;type:uuid"` // 用户ID
Amount int64 `json:"amount" gorm:"column:amount;type:bigint"` // 总额度
UsedAmount int64 `json:"used_amount" gorm:"column:used_amount;type:bigint"` // 已使用额度
FromID string `json:"from_id" gorm:"column:from_id;type:text"` // 来源ID
FromType CreditsFromType `json:"from_type" gorm:"column:from_type;type:text"` // 来源分类
ExpireAt time.Time `json:"expire_at" gorm:"column:expire_at;type:timestamp"` // 过期时间
CreatedAt time.Time `json:"created_at" gorm:"column:created_at;type:timestamp(3) with time zone;default:current_timestamp()"` // 创建时间
StartAt time.Time `json:"start_at" gorm:"column:start_at;type:timestamp"` // 开始时间
Status CreditsStatus `json:"status" gorm:"column:status;type:text"` // 状态
}
type (
CreditsStatus string
CreditsRecordType string
CreditsRecordReason string
CreditsFromType string
)
const (
CreditsStatusActive CreditsStatus = "active"
CreditsStatusExpired CreditsStatus = "expired"
CreditsStatusUsedUp CreditsStatus = "used_up"
CreditsFromTypeSubscription CreditsFromType = "subscription"
CreditsRecordTypeIssue CreditsRecordType = "issue"
CreditsRecordTypeConsume CreditsRecordType = "consume"
CreditsRecordReasonResourceAccountTransaction CreditsRecordReason = "AccountTransaction"
)
// CreditsTransaction 表示credits的使用或发放记录
type CreditsTransaction struct {
ID uuid.UUID `json:"id"` // 记录ID
UserUID uuid.UUID `json:"user_uid"` // 用户ID
AccountTransactionID *uuid.UUID `json:"account_transaction_id,omitempty"` // 关联的AccountTransaction ID
RegionUID uuid.UUID `json:"region_uid"` // 区域ID
CreditsID uuid.UUID `json:"credits_id"` // 关联的Credits ID
UsedAmount int64 `json:"used_amount"` // 使用额度
CreatedAt time.Time `json:"created_at"` // 操作时间
Reason CreditsRecordReason `json:"reason"` // 操作原因(如"AccountTransaction"
}
func (Credits) TableName() string {
return "Credits"
}
func (CreditsTransaction) TableName() string {
return "CreditsTransaction"
}
+91
View File
@@ -0,0 +1,91 @@
package types
import (
"time"
"github.com/google/uuid"
)
// DebtStatusType 定义债务状态类型
type DebtStatusType string
// 定义状态常量
const (
NormalPeriod DebtStatusType = "NormalPeriod"
LowBalancePeriod DebtStatusType = "LowBalancePeriod"
CriticalBalancePeriod DebtStatusType = "CriticalBalancePeriod"
DebtPeriod DebtStatusType = "DebtPeriod"
DebtDeletionPeriod DebtStatusType = "DebtDeletionPeriod"
FinalDeletionPeriod DebtStatusType = "FinalDeletionPeriod"
NormalDebtNamespaceAnnoStatus = "Normal"
SuspendDebtNamespaceAnnoStatus = "Suspend"
FinalDeletionDebtNamespaceAnnoStatus = "FinalDeletion"
ResumeDebtNamespaceAnnoStatus = "Resume"
TerminateSuspendDebtNamespaceAnnoStatus = "TerminateSuspend"
)
const (
DebtPrefix = "debt-"
DaySecond = 24 * 60 * 60
)
var StatusMap = map[DebtStatusType]int{
NormalPeriod: 0,
LowBalancePeriod: 1,
CriticalBalancePeriod: 2,
DebtPeriod: 3,
DebtDeletionPeriod: 4,
FinalDeletionPeriod: 5,
}
var NonDebtStates = []DebtStatusType{NormalPeriod, LowBalancePeriod, CriticalBalancePeriod}
var DebtStates = []DebtStatusType{DebtPeriod, DebtDeletionPeriod, FinalDeletionPeriod}
func ContainDebtStatus(statuses []DebtStatusType, status DebtStatusType) bool {
for _, s := range statuses {
if s == status {
return true
}
}
return false
}
// Debt 表示 debts 表
type Debt struct {
UserUID uuid.UUID `gorm:"column:user_uid;type:uuid;not null;primary_key"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;default:current_timestamp()"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;default:current_timestamp()"` // 更新时间
AccountDebtStatus DebtStatusType `gorm:"column:account_debt_status;not null" json:"account_debt_status,omitempty"`
StatusRecords []DebtStatusRecord `gorm:"foreignKey:UserUID;references:UserUID"`
}
// DebtStatusRecord 表示 debt_status_records 表
type DebtStatusRecord struct {
ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"`
UserUID uuid.UUID `gorm:"column:user_uid;type:uuid;not null" json:"user_uid"` // 外键,关联 User.ID
LastStatus DebtStatusType `gorm:"column:last_status" json:"last_status,omitempty"`
CurrentStatus DebtStatusType `gorm:"column:current_status" json:"current_status,omitempty"`
CreateAt time.Time `gorm:"column:create_at;not null;autoCreateTime;default:current_timestamp()" json:"create_at,omitempty"`
}
func (Debt) TableName() string {
return "Debt"
}
func (DebtStatusRecord) TableName() string {
return "DebtStatusRecord"
}
type DebtResumeDeductionBalanceTransaction struct {
ID uuid.UUID `json:"id" gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"`
UserUID uuid.UUID `json:"user_uid" gorm:"column:user_uid;type:uuid;not null"`
BeforeDeductionBalance int64 `json:"before_deduction_balance" gorm:"column:before_deduction_balance;not null"`
AfterDeductionBalance int64 `json:"after_deduction_balance" gorm:"column:after_deduction_balance;not null"`
BeforeBalance int64 `json:"before_balance" gorm:"column:before_balance;not null"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at;autoCreateTime;default:current_timestamp()"`
}
func (DebtResumeDeductionBalanceTransaction) TableName() string {
return "DebtResumeDeductionBalanceTransaction"
}
+146 -47
View File
@@ -15,9 +15,13 @@
package types
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/lib/pq"
"github.com/google/uuid"
)
@@ -25,9 +29,10 @@ type Account struct {
UserUID uuid.UUID `gorm:"column:userUid;type:uuid;default:gen_random_uuid();primary_key"`
ActivityBonus int64 `gorm:"column:activityBonus;type:bigint;not null"`
// Discard EncryptBalance and EncryptDeductionBalance
EncryptBalance string `gorm:"column:encryptBalance;type:text;not null"`
EncryptDeductionBalance string `gorm:"column:encryptDeductionBalance;type:text;not null"`
EncryptBalance string `gorm:"column:encryptBalance;type:text"`
EncryptDeductionBalance string `gorm:"column:encryptDeductionBalance;type:text"`
CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp()"`
UpdatedAt time.Time `gorm:"type:timestamp(3) with time zone;autoUpdateTime;default:current_timestamp()"`
CreateRegionID string `gorm:"type:text;not null"`
Balance int64
DeductionBalance int64
@@ -37,6 +42,23 @@ func (Account) TableName() string {
return "Account"
}
type UsableBalanceWithCredits struct {
UserUID uuid.UUID
Balance int64 // Separate balance
DeductionBalance int64 // Separate deduction balance
UsableCredits int64
CreateRegionID string
}
type BalanceWithCredits struct {
UserUID uuid.UUID `json:"userUid"`
Balance int64 `json:"balance"`
DeductionBalance int64 `json:"deductionBalance"`
Credits int64 `json:"credits"`
DeductionCredits int64 `json:"deductionCredits"`
CreateRegionID string `json:"createRegionId"`
}
type Region struct {
UID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primary_key"`
DisplayName string `gorm:"column:displayName;type:text"`
@@ -157,7 +179,7 @@ const (
OauthProviderTypePhone OauthProviderType = "PHONE"
OauthProviderTypeEmail OauthProviderType = "EMAIL"
OauthProviderTypePassword OauthProviderType = "PASSWORD"
//OauthProviderTypeGithub OauthProviderType = "GITHUB"
OauthProviderTypeGithub OauthProviderType = "GITHUB"
//OauthProviderTypeWechat OauthProviderType = "WECHAT"
RoleOwner Role = "OWNER"
@@ -187,44 +209,13 @@ func (RegionUserCr) TableName() string {
return "UserCr"
}
type PaymentRaw struct {
UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null"`
RegionUID uuid.UUID `gorm:"column:regionUid;type:uuid;not null"`
CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp()"`
RegionUserOwner string `gorm:"column:regionUserOwner;type:text;not null"`
Method string `gorm:"type:text;not null"`
Amount int64 `gorm:"type:bigint;not null"`
Gift int64 `gorm:"type:bigint"`
TradeNO string `gorm:"type:text;unique;not null"`
// CodeURL is the codeURL of wechatpay
CodeURL string `gorm:"type:text"`
InvoicedAt bool `gorm:"type:boolean;default:false"`
Remark string `gorm:"type:text"`
ActivityType ActivityType `gorm:"type:text;column:activityType"`
Message string `gorm:"type:text;not null"`
}
type ActivityType string
const (
ActivityTypeFirstRecharge ActivityType = "FIRST_RECHARGE"
)
type Payment struct {
ID string `gorm:"type:text;primary_key"`
PaymentRaw
}
func (Payment) TableName() string {
return "Payment"
}
type InvoiceStatus string
const (
PendingInvoiceStatus = "PENDING"
CompletedInvoiceStatus = "COMPLETED"
RejectedInvoiceStatus = "REJECTED"
PendingInvoiceStatus = "PENDING"
ProcessingInvoiceStatus = "PROCESSING"
CompletedInvoiceStatus = "COMPLETED"
RejectedInvoiceStatus = "REJECTED"
)
type Invoice struct {
@@ -270,15 +261,21 @@ func (GiftCode) TableName() string {
}
type AccountTransaction struct {
ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"`
Type string `gorm:"column:type;type:text"`
UserUID uuid.UUID `gorm:"column:userUid;type:uuid"`
DeductionBalance int64 `gorm:"column:deduction_balance;type:bigint"`
Balance int64 `gorm:"column:balance;type:bigint"`
Message *string `gorm:"column:message;type:text"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp(3) with time zone;default:current_timestamp()"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(3) with time zone;default:current_timestamp()"`
BillingID uuid.UUID `gorm:"column:billing_id;type:uuid"`
ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"`
RegionUID uuid.UUID `gorm:"column:region;type:uuid;"`
Type string `gorm:"column:type;type:text"`
UserUID uuid.UUID `gorm:"column:userUid;type:uuid"`
DeductionBalance int64 `gorm:"column:deduction_balance;type:bigint"`
Balance int64 `gorm:"column:balance;type:bigint"`
DeductionCredit int64 `gorm:"column:deduction_credit;type:bigint"`
Message *string `gorm:"column:message;type:text"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp(3) with time zone;default:current_timestamp()"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(3) with time zone;default:current_timestamp()"`
BillingID uuid.UUID `gorm:"column:billing_id;type:uuid"`
BillingIDList pq.StringArray `gorm:"column:billing_id_list;type:text[]"`
CreditIDList pq.StringArray `gorm:"column:credit_id_list;type:text[]"`
BalanceBefore int64 `gorm:"column:balance_before;type:bigint"`
DeductionBalanceBefore int64 `gorm:"column:deduction_balance_before;type:bigint"`
}
func (AccountTransaction) TableName() string {
@@ -319,3 +316,105 @@ type EnterpriseRealNameInfo struct {
func (EnterpriseRealNameInfo) TableName() string {
return "EnterpriseRealNameInfo"
}
type UserInfo struct {
ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"`
UserUID uuid.UUID `gorm:"column:userUid;type:uuid;unique"`
FirstName string `gorm:"column:firstname;type:text;default:''::STRING"`
LastName string `gorm:"column:lastname;type:text;default:''::STRING"`
Config *UserInfoConfig `gorm:"column:config;type:jsonb"`
//Config datatypes.JSO `gorm:"column:config;type:jsonb" json:"config"`
}
func (UserInfo) TableName() string {
return "UserInfo"
}
func (j *UserInfoConfig) Scan(value interface{}) error {
if value == nil {
return nil
}
b, ok := value.([]byte)
if !ok {
return fmt.Errorf("failed to unmarshal JSONB: %v", value)
}
return json.Unmarshal(b, j)
}
func (j *UserInfoConfig) Value() (driver.Value, error) {
if j == nil {
return nil, nil
}
b, err := json.Marshal(j)
if err != nil {
return nil, err
}
return string(b), nil
}
type UserInfoConfig struct {
Github Github `json:"github"`
}
type Github struct {
CreatedAt string `json:"created_at"`
Login string `json:"login"`
Name interface{} `json:"name"`
ID int `json:"id"`
//AvatarURL string `json:"avatar_url"`
//Bio interface{} `json:"bio"`
//Blog string `json:"blog"`
//Collaborators int `json:"collaborators"`
//Company interface{} `json:"company"`
//DiskUsage int `json:"disk_usage"`
//Email interface{} `json:"email"`
//EventsURL string `json:"events_url"`
//Followers int `json:"followers"`
//FollowersURL string `json:"followers_url"`
//Following int `json:"following"`
//FollowingURL string `json:"following_url"`
//GistsURL string `json:"gists_url"`
//GravatarID string `json:"gravatar_id"`
//Hireable interface{} `json:"hireable"`
//HtmlURL string `json:"html_url"`
//Location interface{} `json:"location"`
//NodeID string `json:"node_id"`
//NotificationEmail interface{} `json:"notification_email"`
//OrganizationsURL string `json:"organizations_url"`
//OwnedPrivateRepos int `json:"owned_private_repos"`
//Plan Plan `json:"plan"`
//PrivateGists int `json:"private_gists"`
//PublicGists int `json:"public_gists"`
//PublicRepos int `json:"public_repos"`
//ReceivedEventsURL string `json:"received_events_url"`
//ReposURL string `json:"repos_url"`
//SiteAdmin bool `json:"site_admin"`
//StarredURL string `json:"starred_url"`
//SubscriptionsURL string `json:"subscriptions_url"`
//TotalPrivateRepos int `json:"total_private_repos"`
//TwitterUsername interface{} `json:"twitter_username"`
//TwoFactorAuthentication bool `json:"two_factor_authentication"`
//Type string `json:"type"`
//UpdatedAt string `json:"updated_at"`
//URL string `json:"url"`
//UserViewType string `json:"user_view_type"`
}
//type Plan struct {
// Collaborators int `json:"collaborators"`
// Name string `json:"name"`
// PrivateRepos int `json:"private_repos"`
// Space int `json:"space"`
//}
type RegionConfig struct {
ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"`
// domain_region_key: domain_key
Key string `gorm:"column:key;type:text"`
Value string `gorm:"column:value;type:text"`
Region string `gorm:"column:region;type:text"`
}
func (RegionConfig) TableName() string {
return "RegionConfig"
}
+220
View File
@@ -0,0 +1,220 @@
package types
import (
"encoding/json"
"time"
"github.com/google/uuid"
)
type PaymentRaw struct {
UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null"`
RegionUID uuid.UUID `gorm:"column:regionUid;type:uuid;not null"`
CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp()"`
RegionUserOwner string `gorm:"column:regionUserOwner;type:text"`
Method string `gorm:"type:text;not null"`
Amount int64 `gorm:"type:bigint;not null"`
Gift int64 `gorm:"type:bigint"`
// 订单号
TradeNO string `gorm:"type:text;unique;not null"`
// CodeURL is the codeURL of wechatpay
CodeURL string `gorm:"type:text"`
InvoicedAt bool `gorm:"type:boolean;default:false"`
Remark string `gorm:"type:text"`
ActivityType ActivityType `gorm:"type:text;column:activityType"`
Message string `gorm:"type:text;not null"`
//TODO 初始化判断 新加字段
CardUID *uuid.UUID `gorm:"type:uuid"`
Type PaymentType `gorm:"type:text"` // 交易类型: AccountRecharge, SubscriptionUpgradeSubscription...
ChargeSource ChargeSource `gorm:"type:text"`
}
type ChargeSource string
const (
ChargeSourceBalance ChargeSource = "BALANCE"
ChargeSourceNewCard ChargeSource = "CARD"
ChargeSourceBindCard ChargeSource = "BIND_CARD"
)
type PaymentOrder struct {
ID string `gorm:"type:text;primary_key"`
PaymentRaw
// 支付状态
Status PaymentOrderStatus `gorm:"type:text;column:status;not null"`
}
type (
PaymentOrderStatus string
CardPaymentStatus string
PaymentType string
)
const (
PaymentOrderStatusPending PaymentOrderStatus = "PENDING"
PaymentOrderStatusSuccess PaymentOrderStatus = "SUCCESS"
PaymentOrderStatusFailed PaymentOrderStatus = "FAILED"
CardPaymentStatusActive CardPaymentStatus = "ACTIVE"
// "paymentStatus": "FAIL",
// "paymentResultCode": "ACCESS_DENIED",
// "paymentResultMessage": "Access denied.",
CardPaymentStatusFail CardPaymentStatus = "FAIL"
)
const (
PaymentTypeAccountRecharge PaymentType = "ACCOUNT_RECHARGE"
PaymentTypeSubscription PaymentType = "SUBSCRIPTION"
)
func (PaymentOrder) TableName() string {
return "PaymentOrder"
}
type ActivityType string
const (
ActivityTypeFirstRecharge ActivityType = "FIRST_RECHARGE"
)
type Payment struct {
ID string `gorm:"type:text;primary_key"`
PaymentRaw
}
func (Payment) TableName() string {
return "Payment"
}
type CardInfo struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
UserUID uuid.UUID `gorm:"type:uuid;not null"`
CardNo string `gorm:"type:text"`
CardBrand string `gorm:"type:text"`
CardToken string `gorm:"type:text"`
CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp()"`
NetworkTransactionID string `gorm:"type:text"`
Default bool `gorm:"type:boolean;default:false"`
//上次支付状态
LastPaymentStatus PaymentOrderStatus `gorm:"type:text"`
}
func (CardInfo) TableName() string {
return "CardInfo"
}
// PaymentNotificationType 支付通知类型
const (
PaymentResultNotification = "PAYMENT_RESULT"
PaymentPendingNotification = "PAYMENT_PENDING"
)
// PaymentNotification 支付通知请求结构体
type PaymentNotification struct {
NotifyType string `json:"notifyType"`
Result Result `json:"result"`
PaymentRequestID string `json:"paymentRequestId"`
PaymentID string `json:"paymentId"`
PaymentAmount Amount `json:"paymentAmount"`
PaymentCreateTime time.Time `json:"paymentCreateTime"`
PaymentTime *time.Time `json:"paymentTime,omitempty"`
PspCustomerInfo *PspCustomerInfo `json:"pspCustomerInfo,omitempty"`
CustomsDeclarationAmount *Amount `json:"customsDeclarationAmount,omitempty"`
GrossSettlementAmount *Amount `json:"grossSettlementAmount,omitempty"`
SettlementQuote *Quote `json:"settlementQuote,omitempty"`
AcquirerReferenceNo string `json:"acquirerReferenceNo,omitempty"`
PaymentResultInfo interface{} `json:"paymentResultInfo,omitempty"`
PromotionResult []PromotionResult `json:"promotionResult,omitempty"`
PaymentMethodType string `json:"paymentMethodType,omitempty"`
}
// PspCustomerInfo PSP客户信息
type PspCustomerInfo struct {
PspName string `json:"pspName,omitempty"`
PspCustomerID string `json:"pspCustomerId,omitempty"`
DisplayCustomerID string `json:"displayCustomerId,omitempty"`
}
// Quote 汇率信息
type Quote struct {
Guaranteed bool `json:"guaranteed,omitempty"`
QuoteCurrencyPair string `json:"quoteCurrencyPair"`
QuoteExpiryTime *time.Time `json:"quoteExpiryTime,omitempty"`
QuoteID string `json:"quoteId,omitempty"`
QuotePrice string `json:"quotePrice"`
QuoteStartTime *time.Time `json:"quoteStartTime,omitempty"`
}
// PromotionResult 优惠结果
type PromotionResult struct {
PromotionType string `json:"promotionType"`
Discount *Discount `json:"discount,omitempty"`
}
// Discount 折扣信息
type Discount struct {
// 根据实际需求添加字段
DiscountAmount Amount `json:"discountAmount"`
}
// Result 通用结果结构体
type Result struct {
ResultCode string `json:"resultCode"`
ResultMessage string `json:"resultMessage"`
ResultStatus string `json:"resultStatus"`
}
const NotifyTypePaymentResult = "PAYMENT_RESULT"
const NotifyTypeCaptureResult = "CAPTURE_RESULT"
const OrderClosedResultCode = "ORDER_IS_CLOSED"
// Amount 通用金额结构体
type Amount struct {
Currency string `json:"currency"`
Value string `json:"value"`
}
// CommonResponse 通用响应结构体
type CommonResponse struct {
Result Result `json:"result"`
}
func (c *CommonResponse) Raw() []byte {
data, _ := json.Marshal(c)
return data
}
// NewSuccessResponse 创建成功响应
func NewSuccessResponse() CommonResponse {
return CommonResponse{
Result: Result{
ResultCode: "SUCCESS",
ResultMessage: "success",
ResultStatus: "S",
},
}
}
// CaptureNotification 请款通知请求结构体
type CaptureNotification struct {
Result Result `json:"result"`
NotifyType string `json:"notifyType"` // 通知类型,固定为CAPTURE_RESULT
CaptureRequestID string `json:"captureRequestId"` // 商户分配的请款请求ID
PaymentID string `json:"paymentId"` // 支付ID
CaptureID string `json:"captureId"` // 请款ID
CaptureAmount Amount `json:"captureAmount"` // 请款金额
CaptureTime *time.Time `json:"captureTime,omitempty"` // 请款完成时间
AcquirerReferenceNo string `json:"acquirerReferenceNo,omitempty"` // 收单机构交易ID
}
// CaptureResponse 请款通知响应结构体
type CaptureResponse struct {
Result Result `json:"result"`
}
// Raw 返回JSON格式的字节数组
func (c *CaptureResponse) Raw() []byte {
data, _ := json.Marshal(c)
return data
}
+160
View File
@@ -0,0 +1,160 @@
package types
import (
"time"
"github.com/lib/pq"
"github.com/google/uuid"
)
type Subscription struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // 订阅 ID
PlanID uuid.UUID `gorm:"type:uuid;column:plan_id"` // 计划 ID
PlanName string `gorm:"type:varchar(50);column:plan_name"` // 计划名称
UserUID uuid.UUID `gorm:"unique;not null;type:uuid;column:user_uid"` // 用户 ID
Status SubscriptionStatus `gorm:"type:varchar(50);column:status"` // 状态
StartAt time.Time `gorm:"column:start_at;autoCreateTime"` // 开始时间
UpdateAt time.Time `gorm:"column:update_at;autoCreateTime"` // 更新时间
ExpireAt time.Time `gorm:"column:expire_at;autoCreateTime"` // 过期时间
CardID *uuid.UUID `gorm:"type:uuid;column:card_id"` // 银行卡 ID
NextCycleDate time.Time `gorm:"column:next_cycle_date"` // 下一个周期的日期
}
type SubscriptionStatus string
const (
SubscriptionStatusNormal SubscriptionStatus = "NORMAL"
SubscriptionStatusDebt SubscriptionStatus = "DEBT"
)
// 订阅变更记录表
type SubscriptionTransaction struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // ID
SubscriptionID uuid.UUID `gorm:"type:uuid;not null;index;column:subscription_id"` // 关联的订阅 ID
UserUID uuid.UUID `gorm:"type:uuid;not null;index;column:user_uid"` // 用户 ID
OldPlanID uuid.UUID `gorm:"type:uuid;column:old_plan_id"` // 旧的订阅计划 ID
NewPlanID uuid.UUID `gorm:"type:uuid;column:new_plan_id"` // 新的订阅计划 ID
OldPlanName string `gorm:"type:varchar(50);column:old_plan_name"` // 旧的订阅计划名称
NewPlanName string `gorm:"type:varchar(50);column:new_plan_name"` // 新的订阅计划名称
OldPlanStatus SubscriptionStatus `gorm:"type:varchar(50);column:old_plan_status"` // 旧的订阅状态
Operator SubscriptionOperator `gorm:"type:varchar(50);column:operator"` // 操作类型(created/upgraded/downgraded/canceled/renewed)
StartAt time.Time `gorm:"column:start_at"` // 变更开始时间
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` // 更新时间
Status SubscriptionTransactionStatus `gorm:"type:varchar(50);column:status"` // 状态
PayStatus SubscriptionPayStatus `gorm:"type:varchar(50);column:pay_status"` // 支付状态
PayID string `gorm:"type:text;column:pay_id"` // 支付订单号
Amount int64 `gorm:"type:bigint;column:amount"` // 金额
}
type SubscriptionTransactionStatus string
type SubscriptionOperator string
type SubscriptionPayStatus string
const (
SubscriptionTransactionTypeCreated SubscriptionOperator = "created"
SubscriptionTransactionTypeUpgraded SubscriptionOperator = "upgraded"
SubscriptionTransactionTypeDowngraded SubscriptionOperator = "downgraded"
SubscriptionTransactionTypeCanceled SubscriptionOperator = "canceled"
SubscriptionTransactionTypeRenewed SubscriptionOperator = "renewed"
SubscriptionTransactionStatusCompleted SubscriptionTransactionStatus = "completed"
SubscriptionTransactionStatusPending SubscriptionTransactionStatus = "pending"
SubscriptionTransactionStatusProcessing SubscriptionTransactionStatus = "processing"
SubscriptionTransactionStatusFailed SubscriptionTransactionStatus = "failed"
SubscriptionPayStatusPending SubscriptionPayStatus = "pending"
SubscriptionPayStatusPaid SubscriptionPayStatus = "paid"
SubscriptionPayStatusNoNeed SubscriptionPayStatus = "no_need"
SubscriptionPayStatusFailed SubscriptionPayStatus = "failed"
)
const (
FreeSubscriptionPlanName = "Free"
)
type SubscriptionPlan struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // 计划 ID
Name string `gorm:"unique;not null;column:name;type:text"` // 计划名称
Description string `gorm:"type:text;column:description"` // 描述
Amount int64 `gorm:"type:bigint;column:amount"` // 金额
GiftAmount int64 `gorm:"type:bigint;column:gift_amount"` // 赠送金额
Period string `gorm:"type:varchar(50);column:period"` // 周期
UpgradePlanList pq.StringArray `gorm:"type:text[];column:upgrade_plan_list"` // 可升级的计划列表
DowngradePlanList pq.StringArray `gorm:"type:text[];column:downgrade_plan_list"` // 可降级的计划列表
// <0 Unrestricted
MaxSeats int `gorm:"not null;column:max_seats"` // 最大席位数
MaxWorkspaces int `gorm:"not null;column:max_workspaces"` // 最大 Workspace 数量
MaxResources string `gorm:"column:max_resources"` // 最大资源数: map[string]string: {"cpu": "4", "memory": "8Gi", "storage": "100Gi"}
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` // 更新时间
//Most Popular
MostPopular bool `gorm:"column:most_popular"`
}
func (Subscription) TableName() string {
return "Subscription"
}
func (SubscriptionPlan) TableName() string {
return "SubscriptionPlan"
}
func (SubscriptionTransaction) TableName() string {
return "SubscriptionTransaction"
}
type AccountRegionUserTask struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // ID
//RegionUID uuid.UUID `gorm:"type:uuid;not null;index;column:region_uid"` // Region ID
RegionDomain string `gorm:"type:varchar(50);not null;column:region_domain"` // Region Domain
UserUID uuid.UUID `gorm:"type:uuid;not null;index;column:user_uid"` // 用户 ID
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间
// flush-quota
Type AccountRegionUserTaskType `gorm:"column:type"` // 类型
//TaskID uuid.UUID `gorm:"type:uuid;column:task_id"` // 任务 ID
//Executed bool `gorm:"column:executed"` // 是否已执行
StartAt time.Time `gorm:"column:start_at"` // 开始时间
EndAt time.Time `gorm:"column:end_at"` // 结束时间
Status AccountRegionUserTaskStatus
}
func (AccountRegionUserTask) TableName() string {
return "AccountRegionUserTask"
}
type AccountRegionUserTaskType string
type AccountRegionUserTaskStatus string
const (
AccountRegionUserTaskTypeFlushQuota AccountRegionUserTaskType = "flush-quota"
AccountRegionUserTaskTypeFlushDebt AccountRegionUserTaskType = "flush-debt"
AccountRegionUserTaskStatusPending AccountRegionUserTaskStatus = "pending"
AccountRegionUserTaskStatusCompleted AccountRegionUserTaskStatus = "completed"
AccountRegionUserTaskStatusFailed AccountRegionUserTaskStatus = "failed"
)
type UserKYC struct {
UserUID uuid.UUID `gorm:"type:uuid;not null;primaryKey;column:user_uid"` // 用户 ID
Status KYCStatus `gorm:"type:varchar(50);column:status"` // KYC 状态
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` // 更新时间
NextAt time.Time `gorm:"column:next_at"` // 下次credits时间
}
func (UserKYC) TableName() string {
return "UserKYC"
}
type KYCStatus string
const (
UserKYCStatusPending KYCStatus = "pending"
UserKYCStatusCompleted KYCStatus = "completed"
UserKYCStatusFailed KYCStatus = "failed"
)
+206
View File
@@ -0,0 +1,206 @@
package utils
import (
"strconv"
"time"
"github.com/go-gomail/gomail"
"github.com/labring/sealos/controllers/pkg/types"
)
type SMTPConfig struct {
ServerHost string
ServerPort int
Username string
FromEmail string
Passwd string
EmailTitle string
}
func (c *SMTPConfig) SendEmail(emailBody, to string) error {
m := gomail.NewMessage()
m.SetHeader("To", to)
m.SetAddressHeader("From", c.FromEmail, c.EmailTitle)
m.SetHeader("Subject", c.EmailTitle)
m.SetBody("text/html", emailBody)
d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd)
return d.DialAndSend(m)
}
func (c *SMTPConfig) SendEmailWithSubject(subject, emailBody, to string) error {
m := gomail.NewMessage()
m.SetHeader("To", to)
m.SetAddressHeader("From", c.FromEmail, c.EmailTitle)
m.SetHeader("Subject", subject)
m.SetBody("text/html", emailBody)
d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd)
return d.DialAndSend(m)
}
const (
EnvSMTPHost = "SMTP_HOST"
EnvSMTPPort = "SMTP_PORT"
EnvSMTPFrom = "SMTP_FROM"
EnvSMTPUser = "SMTP_USER"
EnvSMTPPassword = "SMTP_PASSWORD"
EnvSMTPTitle = "SMTP_TITLE"
EnvPaySuccessEmailTmpl = "PAY_SUCCESS_EMAIL_TMPL"
EnvPayFailedEmailTmpl = "PAY_FAILED_EMAIL_TMPL"
EnvSubSuccessEmailTmpl = "SUB_SUCCESS_EMAIL_TMPL"
EnvSubFailedEmailTmpl = "SUB_FAILED_EMAIL_TMPL"
)
type EmailRenderBuilder interface {
Build() map[string]interface{}
GetType() string
SetUserInfo(userInfo *types.UserInfo)
GetSubject() string
}
type EmailPayRender struct {
Type string
userInfo *types.UserInfo
Domain string
TopUpAmount int64
AccountBalance int64
}
func (e *EmailPayRender) Build() map[string]interface{} {
return map[string]interface{}{
"FirstName": e.userInfo.FirstName,
"LastName": e.userInfo.LastName,
"Domain": e.Domain,
"TopUpAmount": strconv.FormatInt(e.TopUpAmount, 10),
"AccountBalance": strconv.FormatInt(e.AccountBalance, 10),
}
}
func (e *EmailPayRender) GetType() string {
return e.Type
}
func (e *EmailPayRender) GetSubject() string {
return "Top-Up Successful"
}
func (e *EmailPayRender) SetUserInfo(userInfo *types.UserInfo) {
e.userInfo = userInfo
}
func (e *EmailSubRender) Build() map[string]interface{} {
build := map[string]interface{}{
"FirstName": e.userInfo.FirstName,
"LastName": e.userInfo.LastName,
"Domain": e.Domain,
"SubscriptionPlanName": e.SubscriptionPlanName,
"StartDate": e.StartDate.Format("2006-01-02"),
"EndDate": e.EndDate.Format("2006-01-02"),
}
switch e.SubscriptionPlanName {
case "Hobby":
build["SubscriptionFeatures"] = []string{
"Includes $5 credits",
"16 vCPU / 32GiB RAM",
"Unlimited disk & traffic within plan",
"Multiple regions",
"3 workspaces / region",
"5 seats / workspace",
}
case "Pro":
build["SubscriptionFeatures"] = []string{
"Includes $20 credits",
"128 vCPU / 256GiB RAM",
"Unlimited disk & traffic within plan",
"Multiple regions",
"Multiple workspace / region",
"Multiple seat / workspace",
}
}
return build
}
type EmailSubRender struct {
Type string
Operator types.SubscriptionOperator
userInfo types.UserInfo
Domain string
SubscriptionPlanName string
StartDate time.Time
EndDate time.Time
}
func (e *EmailSubRender) GetType() string {
return e.Type
}
func (e *EmailSubRender) SetUserInfo(userInfo *types.UserInfo) {
e.userInfo = *userInfo
}
func (e *EmailSubRender) GetSubject() string {
switch e.Operator {
case types.SubscriptionTransactionTypeUpgraded:
return "Your Subscription Has Been Successfully Updated"
case types.SubscriptionTransactionTypeDowngraded:
return "Your Subscription Has Been Successfully Downgraded"
case types.SubscriptionTransactionTypeCanceled:
return "Your Subscription Has Been Successfully Canceled"
case types.SubscriptionTransactionTypeRenewed:
return "Your Subscription Has Been Successfully Renewed"
default:
return "Your Subscription Has Been Successfully Activated"
}
}
type EmailDebtRender struct {
Type string
CurrentStatus types.DebtStatusType
userInfo types.UserInfo
Domain string
GraceReason []string
}
type DebtGraceReason string
const (
GraceReasonNoBalance DebtGraceReason = "insufficient balance"
GraceReasonSubExpired DebtGraceReason = "subscription expired"
)
func (e *EmailDebtRender) GetType() string {
return e.Type
}
func (e *EmailDebtRender) SetUserInfo(userInfo *types.UserInfo) {
e.userInfo = *userInfo
}
func (e *EmailDebtRender) GetSubject() string {
if types.ContainDebtStatus(types.DebtStates, e.CurrentStatus) {
if e.CurrentStatus == types.FinalDeletionPeriod {
return "Important: Your Resources Had Expired"
}
return "Important: Your Account Has Entered Grace Period"
}
return "Low Account Balance Reminder"
}
func (e *EmailDebtRender) Build() map[string]interface{} {
build := map[string]interface{}{
"FirstName": e.userInfo.FirstName,
"LastName": e.userInfo.LastName,
"Domain": e.Domain,
"GraceReason": e.GraceReason,
}
if e.Type == "CriticalBalancePeriod" {
build["CreditsAvailable"] = "1"
}
if e.Type == "LowBalancePeriod" {
build["CreditsAvailable"] = "5"
}
return build
}
@@ -1,14 +1,11 @@
package helper
package utils
import (
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
)
@@ -75,13 +72,7 @@ func (manager *JWTManager) VerifyToken(tokenString string) (*UserClaims, error)
return claims, nil
}
func (manager *JWTManager) ParseUser(c *gin.Context) (*JwtUser, error) {
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
return nil, ErrNullAuth
}
token := strings.TrimPrefix(tokenString, "Bearer ")
func (manager *JWTManager) ParseUser(token string) (*JwtUser, error) {
claims, err := manager.VerifyToken(token)
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
@@ -0,0 +1,183 @@
package dlock
import (
"context"
"errors"
"fmt"
"sync"
"time"
"gorm.io/gorm"
)
var (
ErrLockNotAcquired = errors.New("lock not acquired")
ErrLockNotHeld = errors.New("lock not held by this instance")
)
type DistributedLock struct {
db *gorm.DB
lockName string
holderID string
stopRenew chan struct{}
once sync.Once
}
func NewDistributedLock(db *gorm.DB, lockName string, holderID string) *DistributedLock {
return &DistributedLock{
db: db,
lockName: lockName,
holderID: holderID,
stopRenew: make(chan struct{}),
once: sync.Once{},
}
}
// TryLock acquisition method
func (dl *DistributedLock) TryLock(ctx context.Context, ttl time.Duration) error {
expiresAt := time.Now().UTC().Add(ttl)
err := dl.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err := tx.Exec(`
CREATE TABLE IF NOT EXISTS distributed_locks (
lock_name STRING PRIMARY KEY,
holder_id STRING NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
version INT NOT NULL DEFAULT 1
)
`).Error
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
// try to get the current lock state first
var currentLock struct {
HolderID string
ExpiresAt time.Time
}
err = tx.Raw(`
SELECT holder_id, expires_at
FROM distributed_locks
WHERE lock_name = ? FOR UPDATE
`, dl.lockName).Scan(&currentLock).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
// determines whether the lock can be obtained
if currentLock.HolderID != "" &&
currentLock.ExpiresAt.After(time.Now().UTC()) &&
currentLock.HolderID != dl.holderID {
return ErrLockNotAcquired
}
// use upsert atomic operations
result := tx.Exec(`
INSERT INTO distributed_locks (lock_name, holder_id, expires_at, version)
VALUES (?, ?, ?, 1)
ON CONFLICT (lock_name) DO UPDATE
SET
holder_id = excluded.holder_id,
expires_at = excluded.expires_at,
version = distributed_locks.version + 1
WHERE distributed_locks.expires_at <= now() OR distributed_locks.holder_id = excluded.holder_id
`, dl.lockName, dl.holderID, expiresAt)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrLockNotAcquired
}
return nil
})
if err != nil {
return err
}
go dl.renewLock(ttl)
return nil
}
func (dl *DistributedLock) renewLock(ttl time.Duration) {
ticker := time.NewTicker(ttl / 2)
defer ticker.Stop()
for {
select {
case <-ticker.C:
expiresAt := time.Now().UTC().Add(ttl)
err := dl.db.Transaction(func(tx *gorm.DB) error {
result := tx.Exec(`
UPDATE distributed_locks
SET expires_at = ?, version = version + 1
WHERE lock_name = ? AND holder_id = ?
`, expiresAt, dl.lockName, dl.holderID)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrLockNotHeld
}
return nil
})
if err != nil {
// Failed to renew the lock. The lock may have been acquired by another instance
close(dl.stopRenew)
return
}
case <-dl.stopRenew:
return
}
}
}
func (dl *DistributedLock) Unlock() error {
dl.once.Do(func() {
close(dl.stopRenew)
})
return dl.db.Transaction(func(tx *gorm.DB) error {
result := tx.Exec(`
DELETE FROM distributed_locks
WHERE lock_name = ? AND holder_id = ?
`, dl.lockName, dl.holderID)
if result.Error != nil {
return result.Error
}
//
//if result.RowsAffected == 0 {
// return ErrLockNotHeld
//}
return nil
})
}
func (dl *DistributedLock) IsHeld(ctx context.Context) (bool, error) {
var count int64
err := dl.db.WithContext(ctx).Model(&struct {
LockName string `gorm:"column:lock_name"`
}{}).
Table("distributed_locks").
Where("lock_name = ? AND holder_id = ? AND expires_at > now()", dl.lockName, dl.holderID).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
@@ -0,0 +1,322 @@
package dlock
import (
"context"
"errors"
"fmt"
"os"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// setupTestDB 创建测试数据库连接
func setupTestDB(t *testing.T) *gorm.DB {
//TODO need to set up a real test database
dsn := os.Getenv("TEST_DB_URI")
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
require.NoError(t, err)
// 确保表存在
err = db.Exec(`
CREATE TABLE IF NOT EXISTS distributed_locks (
lock_name STRING PRIMARY KEY,
holder_id STRING NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
version INT NOT NULL DEFAULT 1
)
`).Error
require.NoError(t, err)
// 清空测试数据
err = db.Exec("DELETE FROM distributed_locks").Error
require.NoError(t, err)
return db
}
func checkAssert(ok bool, t *testing.T) {
if !ok {
t.Fatalf("[%s] Test failed: %v", time.Now().UTC().Format(time.RFC3339), errors.New("test failed"))
}
}
const (
instanceID = "instance-1"
)
func TestLockAcquireAndRelease(t *testing.T) {
db := setupTestDB(t)
lockName := "test-lock"
holderID := instanceID
lock := NewDistributedLock(db, lockName, holderID)
// 测试获取锁
err := lock.TryLock(context.Background(), 10*time.Second)
checkAssert(assert.NoError(t, err), t)
// 验证锁确实被持有
held, err := lock.IsHeld(context.Background())
checkAssert(assert.NoError(t, err), t)
checkAssert(assert.True(t, held), t)
// 测试释放锁
err = lock.Unlock()
checkAssert(assert.NoError(t, err), t)
// 验证锁已释放
held, err = lock.IsHeld(context.Background())
checkAssert(assert.NoError(t, err), t)
checkAssert(assert.False(t, held), t)
}
func TestLockMutualExclusion(t *testing.T) {
db := setupTestDB(t)
lockName := "mutex-lock"
// 第一个实例获取锁
lock1 := NewDistributedLock(db, lockName, instanceID)
err := lock1.TryLock(context.Background(), 10*time.Second)
checkAssert(assert.NoError(t, err), t)
// 第二个实例尝试获取相同的锁
lock2 := NewDistributedLock(db, lockName, "instance-2")
err = lock2.TryLock(context.Background(), 10*time.Second)
checkAssert(assert.Error(t, err), t)
checkAssert(assert.True(t, errors.Is(err, ErrLockNotAcquired)), t)
// 第一个实例释放锁
err = lock1.Unlock()
checkAssert(assert.NoError(t, err), t)
// 现在第二个实例应该能获取锁
err = lock2.TryLock(context.Background(), 10*time.Second)
checkAssert(assert.NoError(t, err), t)
// 清理
err = lock2.Unlock()
checkAssert(assert.NoError(t, err), t)
}
func TestConcurrentLockAcquisition(t *testing.T) {
db := setupTestDB(t)
lockName := "concurrent-lock"
numClients := 10
var wg sync.WaitGroup
successCh := make(chan bool, numClients)
barrier := make(chan struct{}) // 添加并发屏障
for i := 0; i < numClients; i++ {
wg.Add(1)
go func(instanceID int) {
defer wg.Done()
holderID := fmt.Sprintf("instance-%d", instanceID)
lock := NewDistributedLock(db, lockName, holderID)
<-barrier // 等待所有goroutine就绪
err := lock.TryLock(context.Background(), 5*time.Second)
if err == nil {
successCh <- true
fmt.Printf("Instance %d acquired the lock\n", instanceID)
time.Sleep(100 * time.Millisecond) // 模拟工作
err = lock.Unlock()
checkAssert(assert.NoError(t, err), t)
} else {
fmt.Printf("Instance %d failed to acquire the lock: %v\n", instanceID, err)
checkAssert(assert.True(t, errors.Is(err, ErrLockNotAcquired)), t)
}
}(i)
}
close(barrier) // 同时释放所有goroutine
wg.Wait()
close(successCh)
// 验证只有一个成功获取锁
successCount := 0
for range successCh {
successCount++
}
checkAssert(assert.Equal(t, 1, successCount), t)
}
func TestLockRenewal(t *testing.T) {
db := setupTestDB(t)
lockName := "renewal-lock"
holderID := instanceID
lock := NewDistributedLock(db, lockName, holderID)
// 获取锁,TTL很短
err := lock.TryLock(context.Background(), 1*time.Second)
checkAssert(assert.NoError(t, err), t)
// 等待超过初始TTL,但续约应该保持锁
time.Sleep(2 * time.Second)
// 验证锁仍然被持有
held, err := lock.IsHeld(context.Background())
checkAssert(assert.NoError(t, err), t)
checkAssert(assert.True(t, held), t)
// 停止续约
err = lock.Unlock()
checkAssert(assert.NoError(t, err), t)
// 验证锁已释放
held, err = lock.IsHeld(context.Background())
checkAssert(assert.NoError(t, err), t)
checkAssert(assert.False(t, held), t)
}
func TestLockExpiration(t *testing.T) {
db := setupTestDB(t)
lockName := "expiring-lock"
// 第一个实例获取锁,TTL很短
lock1 := NewDistributedLock(db, lockName, instanceID)
err := lock1.TryLock(context.Background(), 10*time.Second)
checkAssert(assert.NoError(t, err), t)
fmt.Printf("%s Instance 1 acquired the lock\n", time.Now().UTC())
// 等待锁过期
time.Sleep(11 * time.Second)
fmt.Printf("%s Instance 1 lock expired\n", time.Now().UTC())
// 第二个实例仍然不能获取锁,因为第一个实例持有锁后台不断续约
lock2 := NewDistributedLock(db, lockName, "instance-2")
err = lock2.TryLock(context.Background(), 10*time.Second)
checkAssert(assert.Error(t, err), t)
// 清理
err = lock2.Unlock()
checkAssert(assert.NoError(t, err), t)
}
func TestDoubleUnlock(t *testing.T) {
db := setupTestDB(t)
lockName := "double-unlock-lock"
holderID := instanceID
lock := NewDistributedLock(db, lockName, holderID)
// 获取锁
err := lock.TryLock(context.Background(), 10*time.Second)
checkAssert(assert.NoError(t, err), t)
// 第一次释放
err = lock.Unlock()
checkAssert(assert.NoError(t, err), t)
// 第二次释放也成功
err = lock.Unlock()
checkAssert(assert.NoError(t, err), t)
}
func TestContextCancellation(t *testing.T) {
db := setupTestDB(t)
lockName := "ctx-cancel-lock"
holderID := instanceID
// 先让另一个实例持有锁
otherLock := NewDistributedLock(db, lockName, "instance-2")
err := otherLock.TryLock(context.Background(), 10*time.Second)
checkAssert(assert.NoError(t, err), t)
// 创建可取消的上下文
ctx, cancel := context.WithCancel(context.Background())
// 在新的goroutine中尝试获取锁
var wg sync.WaitGroup
wg.Add(1)
var acquireErr error
go func() {
defer wg.Done()
lock := NewDistributedLock(db, lockName, holderID)
acquireErr = lock.TryLock(ctx, 10*time.Second)
}()
// 等待一会儿然后取消上下文
time.Sleep(1 * time.Millisecond)
cancel()
wg.Wait()
// 验证获取被取消
checkAssert(assert.Error(t, acquireErr), t)
checkAssert(assert.True(t, errors.Is(acquireErr, context.Canceled)), t)
// 清理
err = otherLock.Unlock()
checkAssert(assert.NoError(t, err), t)
}
func TestLongRunningTaskWithLock(t *testing.T) {
db := setupTestDB(t)
lockName := "long-task-lock"
holderID := instanceID
lock := NewDistributedLock(db, lockName, holderID)
// 获取锁,TTL较短以测试续约
err := lock.TryLock(context.Background(), 2*time.Second)
checkAssert(assert.NoError(t, err), t)
// 模拟长时间运行任务
done := make(chan struct{})
go func() {
for i := 0; i < 10; i++ {
held, err := lock.IsHeld(context.Background())
checkAssert(assert.NoError(t, err), t)
if !held {
t.Log("锁丢失,任务中止")
return
}
time.Sleep(1 * time.Second)
}
close(done)
}()
// 等待任务完成或超时
select {
case <-done:
t.Log("任务成功完成")
case <-time.After(11 * time.Second):
t.Fatal("任务超时")
}
// 清理
err = lock.Unlock()
checkAssert(assert.NoError(t, err), t)
}
func TestAll(t *testing.T) {
//setupTestDB(t)
TestLockAcquireAndRelease(t)
TestLockMutualExclusion(t)
TestConcurrentLockAcquisition(t)
TestLockRenewal(t)
TestLockExpiration(t)
TestDoubleUnlock(t)
TestContextCancellation(t)
TestLongRunningTaskWithLock(t)
}
@@ -0,0 +1,125 @@
// Copyright © 2025 sealos.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import "sync"
type ConcurrentMap struct {
mu sync.RWMutex
m map[string]interface{}
}
func NewConcurrentMap() *ConcurrentMap {
return &ConcurrentMap{
m: make(map[string]interface{}),
}
}
func (cm *ConcurrentMap) Set(key string, value interface{}) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.m[key] = value
}
func (cm *ConcurrentMap) Get(key string) (interface{}, bool) {
cm.mu.RLock()
defer cm.mu.RUnlock()
val, ok := cm.m[key]
return val, ok
}
func (cm *ConcurrentMap) GetAllKey() []string {
cm.mu.RLock()
defer cm.mu.RUnlock()
keys := make([]string, 0, len(cm.m))
for k := range cm.m {
keys = append(keys, k)
}
return keys
}
func (cm *ConcurrentMap) Delete(key string) {
cm.mu.Lock()
defer cm.mu.Unlock()
delete(cm.m, key)
}
func (cm *ConcurrentMap) DeleteAll() {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.m = make(map[string]interface{})
}
func (cm *ConcurrentMap) Len() int {
cm.mu.RLock()
defer cm.mu.RUnlock()
return len(cm.m)
}
type ConcurrentNullValueMap struct {
mu sync.RWMutex
m map[string]struct{}
}
func NewConcurrentNullValueMap() *ConcurrentNullValueMap {
return &ConcurrentNullValueMap{
m: make(map[string]struct{}),
}
}
func (cm *ConcurrentNullValueMap) Set(keys ...string) {
cm.mu.Lock()
defer cm.mu.Unlock()
for _, key := range keys {
cm.m[key] = struct{}{}
}
}
func (cm *ConcurrentNullValueMap) Get(key string) (struct{}, bool) {
cm.mu.RLock()
defer cm.mu.RUnlock()
val, ok := cm.m[key]
return val, ok
}
func (cm *ConcurrentNullValueMap) GetAllKey() []string {
cm.mu.RLock()
defer cm.mu.RUnlock()
keys := make([]string, 0, len(cm.m))
for k := range cm.m {
keys = append(keys, k)
}
return keys
}
func (cm *ConcurrentNullValueMap) Delete(key string) {
cm.mu.Lock()
defer cm.mu.Unlock()
delete(cm.m, key)
}
func (cm *ConcurrentNullValueMap) DeleteAll() {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.m = make(map[string]struct{})
}
func (cm *ConcurrentNullValueMap) Len() int {
cm.mu.RLock()
defer cm.mu.RUnlock()
return len(cm.m)
}
@@ -595,8 +595,10 @@ func (r *MonitorReconciler) monitorObjectStorageTraffic() error {
})
}
}
if err := r.DBClient.SaveObjTraffic(objTraffic...); err != nil {
return fmt.Errorf("failed to save object storage traffic: %w", err)
if len(objTraffic) != 0 {
if err := r.DBClient.SaveObjTraffic(objTraffic...); err != nil {
return fmt.Errorf("failed to save object storage traffic: %w", err)
}
}
return nil
}
+13 -12
View File
@@ -14,6 +14,7 @@ replace (
)
require (
github.com/apecloud/kubeblocks v0.9.3
github.com/go-logr/logr v1.4.1
github.com/labring/sealos/controllers/app v0.0.0-20240807070504-eb838607f089
github.com/labring/sealos/controllers/pkg v0.0.0-20240715064441-d1193f70675b
@@ -21,7 +22,7 @@ require (
github.com/minio/minio-go/v7 v7.0.64
github.com/onsi/ginkgo v1.16.4
github.com/onsi/gomega v1.30.0
golang.org/x/sync v0.6.0
golang.org/x/sync v0.8.0
k8s.io/api v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/client-go v12.0.0+incompatible
@@ -58,8 +59,9 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matoous/go-nanoid/v2 v2.0.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
@@ -85,18 +87,17 @@ require (
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.mongodb.org/mongo-driver v1.12.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.19.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.5.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+38 -46
View File
@@ -1,3 +1,5 @@
github.com/apecloud/kubeblocks v0.9.3 h1:06hUB4oVZdHfkFg/wez6LJahiJCE+vp5DDyrcXHqCZc=
github.com/apecloud/kubeblocks v0.9.3/go.mod h1:uC7CHg8mTEEhYeJSyiZI1tX2Ep7rpq5qumcJyTIqlCg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@@ -7,14 +9,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 h1:4GI5eviCwbPxDE311KryyyPUTO7IDVyHGp3Iyl+fEZY=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49/go.mod h1:sbm1DAsayX+XsXCOC2CFAAU9JZhX0SPKwnybDjSd0Ls=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro=
github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -49,8 +49,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -62,15 +60,14 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -97,11 +94,11 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -111,6 +108,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labring/sealos/controllers/app v0.0.0-20240807070504-eb838607f089 h1:UVeQQ61npBKmrT7r6GrLLmCMUSgAY3c5/fFEAjVN+II=
github.com/labring/sealos/controllers/app v0.0.0-20240807070504-eb838607f089/go.mod h1:F/fdFEzWKs0mDmXWDxcaAVPv2SKfl/DXH4X4pimApHE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
@@ -151,8 +150,9 @@ github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
@@ -163,8 +163,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo=
github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -181,8 +181,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -200,15 +200,15 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -223,18 +223,18 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -251,20 +251,20 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -273,30 +273,22 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=
+15 -16
View File
@@ -20,14 +20,14 @@ require (
github.com/onsi/gomega v1.30.0
k8s.io/api v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/client-go v0.29.0
k8s.io/client-go v12.0.0+incompatible
sigs.k8s.io/controller-runtime v0.17.2
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.8.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
@@ -35,10 +35,10 @@ require (
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
@@ -47,29 +47,28 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -78,7 +77,7 @@ require (
k8s.io/component-base v0.29.0 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
+46 -33
View File
@@ -4,8 +4,9 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
@@ -25,8 +26,9 @@ github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
@@ -35,7 +37,6 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -43,8 +44,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -81,8 +83,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -107,12 +107,12 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
@@ -127,10 +127,11 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -140,25 +141,29 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -167,23 +172,31 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -192,8 +205,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -202,8 +215,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=
@@ -235,8 +248,8 @@ k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI=
k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0=
sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
-2
View File
@@ -1238,8 +1238,6 @@ github.com/digitalocean/godo v1.95.0/go.mod h1:NRpFznZFvhHjBoqZAaOD3khVzsJ3EibzK
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 h1:4GI5eviCwbPxDE311KryyyPUTO7IDVyHGp3Iyl+fEZY=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49/go.mod h1:sbm1DAsayX+XsXCOC2CFAAU9JZhX0SPKwnybDjSd0Ls=
github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4=
github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
+7 -1
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -158,7 +159,12 @@ func AdminGetUserRealNameInfo(c *gin.Context) {
const AdminUserName = "sealos-admin"
func authenticateAdminRequest(c *gin.Context) error {
user, err := dao.JwtMgr.ParseUser(c)
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
return fmt.Errorf("null auth found")
}
token := strings.TrimPrefix(tokenString, "Bearer ")
user, err := dao.JwtMgr.ParseUser(token)
if err != nil {
return fmt.Errorf("failed to parse user: %v", err)
}
+63 -10
View File
@@ -9,6 +9,11 @@ import (
"io"
"net/http"
"os"
"strings"
"github.com/google/uuid"
"github.com/labring/sealos/controllers/pkg/utils"
"gorm.io/gorm"
@@ -109,10 +114,13 @@ func GetConsumptionAmount(c *gin.Context) {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
amount, err := dao.DBClient.GetConsumptionAmount(*req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get consumption amount : %v", err)})
return
var amount int64
if req.Owner != "" {
amount, err = dao.DBClient.GetConsumptionAmount(*req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get consumption amount : %v", err)})
return
}
}
c.JSON(http.StatusOK, gin.H{
"amount": amount,
@@ -175,7 +183,7 @@ func GetAllRegionConsumptionAmount(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to create request: %v", err)})
return
}
token, err := dao.JwtMgr.GenerateToken(helper.JwtUser{
token, err := dao.JwtMgr.GenerateToken(utils.JwtUser{
UserID: req.GetAuth().UserID,
})
if err != nil {
@@ -535,6 +543,46 @@ func GetAPPCosts(c *gin.Context) {
})
}
// GetAppTypeCosts
// @Summary Get app type costs
// @Description Get app type costs within a specified time range
// @Tags AppTypeCosts
// @Accept json
// @Produce json
// @Param request body helper.AppCostsReq true "App type costs request"
// @Success 200 {object} map[string]interface{} "successfully retrieved app type costs"
// @Failure 400 {object} map[string]interface{} "failed to parse get app type cost request"
// @Failure 401 {object} map[string]interface{} "authenticate error"
// @Failure 500 {object} map[string]interface{} "failed to get app type cost"
// @Router /account/v1alpha1/costs/app-type [post]
func GetAppTypeCosts(c *gin.Context) {
req, err := helper.ParseAppCostsReq(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse get app type cost request: %v", err)})
return
}
if err := authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
costs, err := dao.DBClient.GetAppResourceCosts(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get app type cost : %v", err)})
return
}
appCosts := common.AppCosts{}
for _type, resourceUsage := range costs.ResourcesByType {
appCosts.Costs = append(appCosts.Costs, common.AppCost{
AppType: int32(resources.AppType[_type]),
Used: resourceUsage.Used,
UsedAmount: resourceUsage.UsedAmount,
})
}
c.JSON(http.StatusOK, gin.H{
"app_costs": appCosts,
})
}
// CheckPermission
// @Summary Check permission
// @Description Check permission
@@ -718,12 +766,17 @@ func GetAppCostTimeRange(c *gin.Context) {
}
func ParseAuthTokenUser(c *gin.Context) (auth *helper.Auth, err error) {
user, err := dao.JwtMgr.ParseUser(c)
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
return nil, fmt.Errorf("null auth found")
}
token := strings.TrimPrefix(tokenString, "Bearer ")
user, err := dao.JwtMgr.ParseUser(token)
if err != nil {
return nil, fmt.Errorf("failed to parse user: %v", err)
}
if user.UserID == "" {
return nil, fmt.Errorf("invalid user: %v", user)
if user.UserID == "" && user.UserUID == uuid.Nil {
return nil, fmt.Errorf("invalid user: %v", *user)
}
auth = &helper.Auth{
Owner: user.UserCrName,
@@ -731,8 +784,8 @@ func ParseAuthTokenUser(c *gin.Context) (auth *helper.Auth, err error) {
UserUID: user.UserUID,
}
// if the user is not in the local region, get the user cr name from db
if dao.DBClient.GetLocalRegion().UID.String() != user.RegionUID {
auth.Owner, err = dao.DBClient.GetUserCrName(types.UserQueryOpts{ID: user.UserID})
if dao.DBClient.GetLocalRegion().UID.String() != user.RegionUID || auth.Owner == "" {
auth.Owner, err = dao.DBClient.GetUserCrName(types.UserQueryOpts{ID: user.UserID, UID: user.UserUID})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Printf("failed to get user cr name: %v\n", err)
+124
View File
@@ -0,0 +1,124 @@
package api
import (
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/gin-gonic/gin"
"github.com/labring/sealos/controllers/pkg/types"
"github.com/labring/sealos/service/account/dao"
"github.com/labring/sealos/service/account/helper"
)
// @Summary List user card info
// @Description List user card info
// @Tags Payment
// @Accept json
// @Produce json
// @Param req body CardListReq true "CardListReq"
// @Success 200 {object} CardListResp
// @Router /payment/v1alpha1/card/list [post]
func ListCard(c *gin.Context) {
req := &helper.AuthBase{}
if err := authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
cards, err := dao.DBClient.GetCardList(&types.UserQueryOpts{UID: req.UserUID})
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get card list: %v", err)})
return
}
type cardInfo struct {
ID uuid.UUID
UserUID uuid.UUID
CardNo string
CardBrand string
CreatedAt time.Time
Default bool
LastPaymentStatus types.PaymentOrderStatus
}
var _cards []cardInfo
for i := range cards {
_cards = append(_cards, cardInfo{
ID: cards[i].ID,
UserUID: cards[i].UserUID,
CardNo: cards[i].CardNo,
CardBrand: cards[i].CardBrand,
CreatedAt: cards[i].CreatedAt,
Default: cards[i].Default,
LastPaymentStatus: cards[i].LastPaymentStatus,
})
}
c.JSON(http.StatusOK, gin.H{
"cards": _cards,
})
}
// @Summary Delete user card info
// @Description Delete user card info
// @Tags Payment
// @Accept json
// @Produce json
// @Param req body CardDeleteReq true "CardDeleteReq"
// @Success 200 {object} CardDeleteResp
// @Router /payment/v1alpha1/card/delete [post]
func DeleteCard(c *gin.Context) {
req, err := helper.ParseCardOperationReq(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprint("failed to parse request: ", err)})
return
}
if err := authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
if req.CardID == uuid.Nil {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: "empty card id"})
return
}
if err := dao.DBClient.DeleteCardInfo(req.CardID, req.UserUID); err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to delete card: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{
"card_id": req.CardID,
"data": "success",
})
}
// @Summary Set default user card
// @Description Set default user card
// @Tags Payment
// @Accept json
// @Produce json
// @Param req body CardOperationReq true "CardOperationReq"
// @Success 200 {object} CardOperationResp
// @Router /payment/v1alpha1/card/set-default [post]
func SetDefaultCard(c *gin.Context) {
req, err := helper.ParseCardOperationReq(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprint("failed to parse request: ", err)})
return
}
if err := authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
if req.CardID == uuid.Nil {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: "empty card id"})
return
}
if err := dao.DBClient.SetDefaultCard(req.CardID, req.UserUID); err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to set default card: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{
"card_id": req.CardID,
"data": "success",
})
}
+93
View File
@@ -0,0 +1,93 @@
package api
import (
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/gin-gonic/gin"
"github.com/labring/sealos/controllers/pkg/types"
"github.com/labring/sealos/service/account/dao"
"github.com/labring/sealos/service/account/helper"
)
// @Summary Get credits info
// @Description Get credits info
// @Tags Credits
// @Accept json
// @Produce json
// @Param req body CreditsInfoReq true "CreditsInfoReq"
// @Success 200 {object} CreditsInfoResp
// @Router /account/v1alpha1/credits/info [post]
func GetCreditsInfo(c *gin.Context) {
req := &helper.AuthBase{}
if err := authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
type CreditsInfoReq struct {
UserUID uuid.UUID `json:"userUid"`
Balance int64 `json:"balance"`
DeductionBalance int64 `json:"deductionBalance"`
Credits int64 `json:"credits"`
DeductionCredits int64 `json:"deductionCredits"`
KYCDeductionCreditsDeductionBalance int64 `json:"kycDeductionCreditsDeductionBalance"`
KYCDeductionCreditsBalance int64 `json:"kycDeductionCreditsBalance"`
CurrentPlanCreditsBalance int64 `json:"currentPlanCreditsBalance"`
CurrentPlanCreditsDeductionBalance int64 `json:"currentPlanCreditsDeductionBalance"`
}
var creditsInfo CreditsInfoReq
subscription, err := dao.DBClient.GetSubscription(&types.UserQueryOpts{UID: req.UserUID})
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get subscription info: %v", err)})
return
}
currentPlan, err := dao.DBClient.GetSubscriptionPlan(subscription.PlanName)
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get subscription plan info: %v", err)})
return
}
var currentCredits types.Credits
err = dao.DBClient.GetGlobalDB().Model(&types.Credits{}).Where("expire_at > ? AND user_uid = ? AND from_id = ? AND status != ?", time.Now().UTC(), req.UserUID, currentPlan.ID, types.CreditsStatusExpired).Find(&currentCredits).Error
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get credits list: %v", err)})
return
}
creditsInfo.CurrentPlanCreditsBalance = currentCredits.Amount
creditsInfo.CurrentPlanCreditsDeductionBalance = currentCredits.UsedAmount
if subscription.PlanName != types.FreeSubscriptionPlanName {
freePlan, err := dao.DBClient.GetSubscriptionPlan(types.FreeSubscriptionPlanName)
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get subscription plan info: %v", err)})
return
}
var freeCredits types.Credits
err = dao.DBClient.GetGlobalDB().Model(&types.Credits{}).Where("expire_at > ? AND user_uid = ? AND from_id = ? AND status != ?", time.Now().UTC(), req.UserUID, freePlan.ID, types.CreditsStatusExpired).Find(&freeCredits).Error
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get credits list: %v", err)})
return
}
creditsInfo.KYCDeductionCreditsBalance = freeCredits.Amount
creditsInfo.KYCDeductionCreditsDeductionBalance = freeCredits.UsedAmount
} else {
creditsInfo.KYCDeductionCreditsBalance = creditsInfo.CurrentPlanCreditsBalance
creditsInfo.KYCDeductionCreditsDeductionBalance = creditsInfo.CurrentPlanCreditsDeductionBalance
}
creditss, err := dao.DBClient.GetBalanceWithCredits(&types.UserQueryOpts{UID: req.UserUID})
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get credits info: %v", err)})
return
}
creditsInfo.UserUID = req.UserUID
creditsInfo.Balance = creditss.Balance
creditsInfo.DeductionBalance = creditss.DeductionBalance
creditsInfo.Credits = creditss.Credits
creditsInfo.DeductionCredits = creditss.DeductionCredits
c.JSON(http.StatusOK, gin.H{
"credits": creditsInfo,
})
}
+372
View File
@@ -0,0 +1,372 @@
package api
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
corev1 "k8s.io/api/core/v1"
types2 "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
clt_log "sigs.k8s.io/controller-runtime/pkg/log"
v1 "github.com/labring/sealos/controllers/pkg/notification/api/v1"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"github.com/gin-gonic/gin"
"github.com/labring/sealos/controllers/pkg/types"
"github.com/labring/sealos/service/account/dao"
"github.com/labring/sealos/service/account/helper"
"gorm.io/gorm"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func init() {
clt_log.SetLogger(zap.New(zap.WriteTo(os.Stdout), zap.UseDevMode(false)))
}
func AdminFlushDebtResourceStatus(c *gin.Context) {
err := authenticateAdminRequest(c)
if err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
req, err := helper.ParseAdminFlushDebtResourceStatusReq(c)
if err != nil {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: fmt.Sprintf("failed to parse request: %v", err)})
return
}
owner, err := dao.DBClient.GetUserCrName(types.UserQueryOpts{UID: req.UserUID})
if err != nil && err != gorm.ErrRecordNotFound {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get user cr name: %v", err)})
return
}
if owner == "" {
c.JSON(http.StatusOK, gin.H{"success": true})
return
}
namespaces, err := getOwnNsListWithClt(dao.K8sManager.GetClient(), owner)
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("get own namespace list failed: %v", err)})
return
}
if err = flushUserDebtResourceStatus(req, dao.K8sManager.GetClient(), namespaces); err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to flush user resource status: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
func flushUserDebtResourceStatus(req *helper.AdminFlushDebtResourceStatusReq, clt client.Client, namespaces []string) error {
switch req.LastDebtStatus {
case types.NormalPeriod, types.LowBalancePeriod, types.CriticalBalancePeriod:
if types.StatusMap[req.CurrentDebtStatus] > types.StatusMap[req.LastDebtStatus] {
//if err := r.sendDesktopNoticeAndSms(ctx, debt.Spec.UserName, oweamount, currentStatus, userNamespaceList, smsEnable, isBasicUser); err != nil {
// r.Logger.Error(err, fmt.Sprintf("send %s notice error", currentStatus))
//}
if err := SendDesktopNotice(context.Background(), clt, req, namespaces); err != nil {
return fmt.Errorf("send desktop notice error: %w", err)
}
} else {
if err := readNotice(context.Background(), clt, namespaces, getAllGtStatus(req.CurrentDebtStatus)...); err != nil {
return fmt.Errorf("read notice error: %w", err)
}
}
if types.ContainDebtStatus(types.DebtStates, req.CurrentDebtStatus) {
//if err := r.SuspendUserResource(ctx, userNamespaceList); err != nil {
// return err
//}
if err := updateNamespaceStatus(context.Background(), clt, SuspendDebtNamespaceAnnoStatus, namespaces); err != nil {
return fmt.Errorf("update namespace status error: %w", err)
}
}
case types.DebtPeriod, types.DebtDeletionPeriod, types.FinalDeletionPeriod: // The current status may be: (Normal, LowBalance, CriticalBalance) Period [Service needs to be restored], DebtDeletionPeriod [Service suspended]
if types.ContainDebtStatus(types.NonDebtStates, req.CurrentDebtStatus) {
// TODO flash all region resume user resource
//if err := r.readNotice(ctx, userNamespaceList, debtStates...); err != nil {
// r.Logger.Error(err, "read low balance notice error")
//}
//if err := r.ResumeUserResource(ctx, userNamespaceList); err != nil {
// return err
//}
if err := readNotice(context.Background(), clt, namespaces, getAllGtStatus(req.CurrentDebtStatus)...); err != nil {
return fmt.Errorf("read notice error: %w", err)
}
if err := updateNamespaceStatus(context.Background(), clt, ResumeDebtNamespaceAnnoStatus, namespaces); err != nil {
return fmt.Errorf("update namespace status error: %w", err)
}
break
}
if req.CurrentDebtStatus != types.FinalDeletionPeriod {
//err = r.sendDesktopNoticeAndSms(ctx, debt.Spec.UserName, oweamount, currentStatus, userNamespaceList, smsEnable, isBasicUser)
//if err != nil {
// r.Logger.Error(err, fmt.Sprintf("send %s notice error", currentStatus))
//}
//if err = r.SuspendUserResource(ctx, userNamespaceList); err != nil {
// return err
//}
if err := SendDesktopNotice(context.Background(), clt, req, namespaces); err != nil {
return fmt.Errorf("send desktop notice error: %w", err)
}
if err := updateNamespaceStatus(context.Background(), clt, SuspendDebtNamespaceAnnoStatus, namespaces); err != nil {
return fmt.Errorf("update namespace status error: %w", err)
}
} else {
//if err = r.DeleteUserResource(ctx, userNamespaceList); err != nil {
// return err
//}
if err := updateNamespaceStatus(context.Background(), clt, FinalDeletionDebtNamespaceAnnoStatus, namespaces); err != nil {
return fmt.Errorf("update namespace status error: %w", err)
}
}
}
return nil
}
func updateNamespaceStatus(ctx context.Context, clt client.Client, status string, namespaces []string) error {
for i := range namespaces {
ns := &corev1.Namespace{}
if err := clt.Get(ctx, types2.NamespacedName{Name: namespaces[i]}, ns); err != nil {
return err
}
if ns.Annotations[DebtNamespaceAnnoStatusKey] == status {
continue
}
original := ns.DeepCopy()
ns.Annotations[DebtNamespaceAnnoStatusKey] = status
if err := clt.Patch(ctx, ns, client.MergeFrom(original)); err != nil {
return fmt.Errorf("patch namespace annotation failed: %w", err)
}
//if err := clt.Update(ctx, ns); err != nil {
// return err
//}
}
return nil
}
const (
debtChoicePrefix = "debt-choice-"
fromEn = "Debt-System"
fromZh = "欠费系统"
languageZh = "zh"
readStatusLabel = "isRead"
falseStatus = "false"
trueStatus = "true"
)
const DebtNamespaceAnnoStatusKey = "debt.sealos/status"
const (
NormalDebtNamespaceAnnoStatus = "Normal"
SuspendDebtNamespaceAnnoStatus = "Suspend"
FinalDeletionDebtNamespaceAnnoStatus = "FinalDeletion"
ResumeDebtNamespaceAnnoStatus = "Resume"
TerminateSuspendDebtNamespaceAnnoStatus = "TerminateSuspend"
)
func SendDesktopNotice(ctx context.Context, clt client.Client, req *helper.AdminFlushDebtResourceStatusReq, namespaces []string) error {
if req.IsBasicUser && req.CurrentDebtStatus != types.DebtPeriod && req.CurrentDebtStatus != types.DebtDeletionPeriod && req.CurrentDebtStatus != types.FinalDeletionPeriod && req.CurrentDebtStatus != types.CriticalBalancePeriod {
return nil
}
if err := sendDesktopNotice(ctx, clt, req.CurrentDebtStatus, namespaces); err != nil {
return fmt.Errorf("send notice error: %w", err)
}
return nil
}
func sendDesktopNotice(ctx context.Context, clt client.Client, noticeType types.DebtStatusType, namespaces []string) error {
now := time.Now().UTC().Unix()
ntfTmp := &v1.Notification{
ObjectMeta: metav1.ObjectMeta{
Name: debtChoicePrefix + strings.ToLower(string(noticeType)),
},
}
ntfTmpSpc := v1.NotificationSpec{
Title: dao.TitleTemplateENMap[noticeType],
Message: dao.NoticeTemplateENMap[noticeType],
From: fromEn,
Importance: v1.High,
DesktopPopup: true,
Timestamp: now,
I18n: map[string]v1.I18n{
languageZh: {
Title: dao.TitleTemplateZHMap[noticeType],
From: fromZh,
Message: dao.NoticeTemplateZHMap[noticeType],
},
},
}
for i := range namespaces {
ntf := ntfTmp.DeepCopy()
ntfSpec := ntfTmpSpc.DeepCopy()
ntf.Namespace = namespaces[i]
if _, err := controllerutil.CreateOrUpdate(ctx, clt, ntf, func() error {
ntf.Spec = *ntfSpec
if ntf.Labels == nil {
ntf.Labels = make(map[string]string)
}
ntf.Labels[readStatusLabel] = falseStatus
return nil
}); err != nil {
return err
}
}
return nil
}
func getAllGtStatus(currentStatus types.DebtStatusType) []types.DebtStatusType {
lessStatus := make([]types.DebtStatusType, 0)
for k, v := range types.StatusMap {
if v > types.StatusMap[currentStatus] {
lessStatus = append(lessStatus, k)
}
}
return lessStatus
}
func readNotice(ctx context.Context, clt client.Client, namespaces []string, noticeTypes ...types.DebtStatusType) error {
for i := range namespaces {
for _, noticeStatus := range noticeTypes {
ntf := &v1.Notification{}
if err := clt.Get(ctx, types2.NamespacedName{Name: debtChoicePrefix + strings.ToLower(string(noticeStatus)), Namespace: namespaces[i]}, ntf); client.IgnoreNotFound(err) != nil {
return err
} else if err != nil {
continue
}
if ntf.Labels == nil {
ntf.Labels = make(map[string]string)
} else if ntf.Labels[readStatusLabel] == trueStatus {
continue
}
ntf.Labels[readStatusLabel] = trueStatus
if err := clt.Update(ctx, ntf); err != nil {
return err
}
}
}
return nil
}
func AdminFlushSubscriptionQuota(c *gin.Context) {
err := authenticateAdminRequest(c)
if err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
req, err := helper.ParseAdminFlushSubscriptionQuotaReq(c)
if err != nil {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: fmt.Sprintf("failed to parse request: %v", err)})
return
}
owner, err := dao.DBClient.GetUserCrName(types.UserQueryOpts{UID: req.UserUID})
if err != nil && err != gorm.ErrRecordNotFound {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get user cr name: %v", err)})
return
}
if owner == "" {
c.JSON(http.StatusOK, gin.H{"success": true})
return
}
nsList, err := getOwnNsListWithClt(dao.K8sManager.GetClient(), owner)
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("get own namespace list failed: %v", err)})
return
}
rs, ok := dao.SubPlanResourceQuota[req.PlanName]
if !ok {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: fmt.Sprintf("plan name is not in plan resource quota: %v", req.PlanName)})
return
}
for _, ns := range nsList {
quota := getDefaultResourceQuota(ns, "quota-"+ns, rs)
if err = dao.K8sManager.GetClient().Update(context.Background(), quota); err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("update resource quota failed: %v", err)})
return
}
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// FlushSubscriptionQuota
// @Summary flush user quota with subscription
// @Description flush user quota with subscription
// @Tags Subscription
// @Accept json
// @Produce json
// @Success 200 {object} SubscriptionFlushQuotaResp
// @Router /payment/v1alpha1/subscription/flush-quota [post]
func FlushSubscriptionQuota(c *gin.Context) {
// 初始化日志前的时间点
startTime := time.Now().UTC()
lastTime := startTime
// 定义一个辅助函数来记录时间间隔并更新lastTime
logWithDuration := func(message string) {
now := time.Now().UTC()
duration := now.Sub(lastTime)
log.Printf("%s (took %v since last step, %v since start)", message, duration, now.Sub(startTime))
lastTime = now
}
logWithDuration("Starting FlushSubscriptionQuota")
req := &helper.AuthBase{}
if err := authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error: %v", err)})
return
}
logWithDuration("Authentication completed")
if req.Owner == "" {
c.JSON(http.StatusOK, gin.H{"success": true})
logWithDuration("Request completed early due to empty owner")
return
}
nsList, err := getOwnNsListWithClt(dao.K8sManager.GetClient(), req.Owner)
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("get own namespace list failed: %v", err)})
return
}
logWithDuration(fmt.Sprintf("Retrieved namespace list: %v", nsList))
userSub, err := dao.DBClient.GetSubscription(&types.UserQueryOpts{UID: req.UserUID})
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("get user subscription failed: %v", err)})
return
}
logWithDuration("User subscription retrieved")
for _, ns := range nsList {
logWithDuration(fmt.Sprintf("Starting quota flush for namespace: %s", ns))
quota := getDefaultResourceQuota(ns, "quota-"+ns, dao.SubPlanResourceQuota[userSub.PlanName])
err = Retry(2, time.Second, func() error {
fErr := dao.K8sManager.GetClient().Update(context.Background(), quota)
if err != nil {
log.Printf("Failed to update resource quota for %s: %v", ns, fErr)
return fmt.Errorf("failed to update resource quota for %s: %w", ns, fErr)
}
return nil
})
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("update resource quota failed: %v", err)})
return
}
logWithDuration(fmt.Sprintf("Quota updated for namespace: %s", ns))
}
logWithDuration("FlushSubscriptionQuota completed")
c.JSON(http.StatusOK, gin.H{"success": true})
}
+486
View File
@@ -0,0 +1,486 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/labring/sealos/controllers/pkg/utils"
"gorm.io/gorm"
"github.com/sirupsen/logrus"
"github.com/google/uuid"
responsePay "github.com/alipay/global-open-sdk-go/com/alipay/api/response/pay"
services "github.com/labring/sealos/service/pkg/pay"
"github.com/labring/sealos/controllers/pkg/types"
"github.com/labring/sealos/service/account/dao"
"github.com/gin-gonic/gin"
"github.com/labring/sealos/service/account/helper"
)
const (
SuccessStatus = "SUCCESS"
PaymentInProcess = "PAYMENT_IN_PROCESS"
)
// CreateCardPay creates a payment
// @Summary Create a payment
// @Description Create a payment
// @Tags account
// @Accept json
// @Produce json
// @Param req body CreatePayReq true "CreatePayReq"
// @Success 200 {object} CreatePayResp
// @Router /account/v1alpha1/createPay [post]
func CreateCardPay(c *gin.Context) {
req, err := helper.ParseCreatePayReq(c)
if err != nil {
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": fmt.Sprint("failed to parse request: ", err)})
return
}
if err := authenticateRequest(c, req); err != nil {
SetErrorResp(c, http.StatusUnauthorized, gin.H{"error": fmt.Sprint("authenticate error: ", err)})
return
}
if req.Method == "CARD" {
paymentReq := services.PaymentRequest{
RequestID: uuid.NewString(),
UserUID: req.UserUID,
Amount: req.Amount,
Currency: dao.PaymentCurrency,
UserAgent: c.GetHeader("User-Agent"),
ClientIP: c.ClientIP(),
DeviceTokenID: c.GetHeader("Device-Token-ID"),
}
var paySvcResp *responsePay.AlipayPayResponse
var createPayHandler func(tx *gorm.DB) error
if req.BindCardInfo != nil {
createPayHandler = func(tx *gorm.DB) error {
card, err := dao.DBClient.GetCardInfo(req.BindCardInfo.CardID, req.UserUID)
if err != nil {
return fmt.Errorf("failed to get card info: %w", err)
}
paySvcResp, err = dao.PaymentService.CreatePaymentWithCard(paymentReq, card)
if err != nil {
return fmt.Errorf("failed to create payment with card: %w", err)
}
if (paySvcResp.Result.ResultCode == SuccessStatus && paySvcResp.Result.ResultStatus == "S") || (paySvcResp.Result.ResultCode == PaymentInProcess && paySvcResp.Result.ResultStatus == "U") {
return nil
}
return fmt.Errorf("payment result is not SUCCESS: %#+v", paySvcResp.Result)
}
} else {
createPayHandler = func(tx *gorm.DB) error {
paySvcResp, err = dao.PaymentService.CreateNewPayment(paymentReq)
if err != nil {
return fmt.Errorf("failed to create payment: %w", err)
}
if paySvcResp.Result.ResultCode != PaymentInProcess || paySvcResp.Result.ResultStatus != "U" {
return fmt.Errorf("payment result is not PAYMENT_IN_PROCESS: %#+v", paySvcResp.Result)
}
return nil
}
}
//if req.BindCardInfo != nil {
// card, err := dao.DBClient.GetCardInfo(req.BindCardInfo.CardID, req.UserUID)
// if err != nil {
// SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to get card info: ", err)})
// return
// }
// if card == nil {
// SetErrorResp(c, http.StatusBadRequest, gin.H{"error": "card not found"})
// return
// }
// if card.CardToken == "" {
// SetErrorResp(c, http.StatusBadRequest, gin.H{"error": "card token not set, please rebind card"})
// return
// }
// err = dao.DBClient.PaymentWithFunc(&types.Payment{
// PaymentRaw: types.PaymentRaw{
// UserUID: req.UserUID,
// Amount: req.Amount,
// Method: req.Method,
// RegionUID: dao.DBClient.GetLocalRegion().UID,
// TradeNO: paymentReq.RequestID,
// Type: types.PaymentTypeAccountRecharge,
// ChargeSource: types.ChargeSourceBindCard,
// },
// }, nil, func(_ *gorm.DB) error {
// paySvcResp, err = dao.PaymentService.CreatePaymentWithCard(paymentReq, card)
// if err != nil {
// return fmt.Errorf("failed to create payment with card: %w", err)
// }
// if paySvcResp.Result.ResultCode != SuccessStatus || paySvcResp.Result.ResultStatus != "S" {
// return fmt.Errorf("payment result is not SUCCESS: %#+v", paySvcResp.Result)
// }
// return nil
// })
// if err != nil {
// SetErrorResp(c, http.StatusConflict, gin.H{"error": fmt.Sprint("failed to create payment: ", err)})
// } else {
// SetSuccessResp(c)
// // TODO 发邮箱通知
// account, err := dao.DBClient.GetAccount(types.UserQueryOpts{UID: req.UserUID})
// if err != nil {
// logrus.Errorf("failed to get account: %v", err)
// }
// if account != nil {
// if err = sendUserPayEmail(req.UserUID, &utils.EmailPayRender{
// Type: utils.EnvPaySuccessEmailTmpl,
// Domain: dao.DBClient.GetLocalRegion().Domain,
// TopUpAmount: req.Amount / 1_000_000,
// AccountBalance: (account.Balance - account.DeductionBalance) / 1_000_000,
// }); err != nil {
// logrus.Errorf("failed to send user %s email: %v", req.UserID, err)
// }
// }
// }
// return
//} else {
// paySvcResp, err = dao.PaymentService.CreateNewPayment(paymentReq)
// if err != nil {
// SetErrorResp(c, http.StatusConflict, gin.H{"error": fmt.Sprint("failed to create payment: ", err)})
// return
// }
// if paySvcResp.Result.ResultCode != "PAYMENT_IN_PROCESS" || paySvcResp.Result.ResultStatus != "U" {
// SetErrorResp(c, http.StatusConflict, gin.H{"error": fmt.Sprintf("payment result is not PAYMENT_IN_PROCESS: %#+v", paySvcResp.Result)})
// return
// }
//
// // do something
// err = dao.DBClient.CreatePaymentOrder(&types.PaymentOrder{
// PaymentRaw: types.PaymentRaw{
// UserUID: req.UserUID,
// Amount: req.Amount,
// Method: req.Method,
// RegionUID: dao.DBClient.GetLocalRegion().UID,
// TradeNO: paymentReq.RequestID,
// //CodeURL: paySvcResp.NormalUrl,
// Type: types.PaymentTypeAccountRecharge,
// ChargeSource: types.ChargeSourceNewCard,
// },
// Status: types.PaymentOrderStatusPending,
// })
// if err != nil {
// SetErrorResp(c, http.StatusConflict, gin.H{"error": fmt.Sprint("failed to create payment order: ", err)})
// return
// }
// c.JSON(http.StatusOK, gin.H{
// "redirectUrl": paySvcResp.NormalUrl,
// "success": true,
// })
//}
paymentID, err := gonanoid.New(12)
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to create payment id: ", err)})
return
}
err = dao.DBClient.GlobalTransactionHandler(func(tx *gorm.DB) error {
return tx.Model(&types.PaymentOrder{}).Create(&types.PaymentOrder{
ID: paymentID,
PaymentRaw: types.PaymentRaw{
UserUID: req.UserUID,
Amount: req.Amount,
Method: req.Method,
RegionUID: dao.DBClient.GetLocalRegion().UID,
TradeNO: paymentReq.RequestID,
CreatedAt: time.Now().UTC(),
//CodeURL: paySvcResp.NormalUrl,
Type: types.PaymentTypeAccountRecharge,
ChargeSource: types.ChargeSourceNewCard,
},
Status: types.PaymentOrderStatusPending,
}).Error
}, createPayHandler, func(tx *gorm.DB) error {
if paySvcResp.NormalUrl != "" {
//Set payment order normalurl with paymentID
dErr := tx.Model(&types.PaymentOrder{}).Where("id = ?", paymentID).Update("code_url", paySvcResp.NormalUrl).Error
if dErr != nil {
logrus.Warnf("failed to update payment order code url: %v", dErr)
}
}
return nil
})
if err != nil {
SetErrorResp(c, http.StatusConflict, gin.H{"error": fmt.Sprint("failed to create payment: ", err)})
} else {
c.JSON(http.StatusOK, gin.H{
"redirectUrl": paySvcResp.NormalUrl,
"success": true,
})
}
return
}
SetErrorResp(c, http.StatusBadGateway, gin.H{"error": "unsupported payment method"})
}
type requestInfoStruct struct {
Path string
Method string
ResponseTime string
ClientID string
Signature string
Body []byte
}
func NewPayNotifyHandler(c *gin.Context) {
requestInfo := requestInfoStruct{
Path: c.Request.RequestURI,
Method: c.Request.Method,
ResponseTime: c.GetHeader("request-time"),
ClientID: c.GetHeader("client-id"),
Signature: c.GetHeader("signature"),
}
var err error
requestInfo.Body, err = c.GetRawData()
if err != nil {
logrus.Errorf("Failed to get raw data: %v", err)
sendError(c, http.StatusBadRequest, "failed to get raw data", err)
return
}
if ok, err := dao.PaymentService.CheckRspSign(
requestInfo.Path,
requestInfo.Method,
requestInfo.ClientID,
requestInfo.ResponseTime,
string(requestInfo.Body),
requestInfo.Signature,
); err != nil {
logrus.Errorf("Failed to check response sign: %v", err)
logrus.Errorf("Path: %s\n Method: %s\n ClientID: %s\n ResponseTime: %s\n Body: %s\n Signature: %s", requestInfo.Path, requestInfo.Method, requestInfo.ClientID, requestInfo.ResponseTime, string(requestInfo.Body), requestInfo.Signature)
sendError(c, http.StatusUnauthorized, "failed to check response sign", err)
return
} else if !ok {
logrus.Errorf("Check signature fail")
sendError(c, http.StatusBadRequest, "check signature fail", nil)
return
}
var notification types.CaptureNotification
if err := json.Unmarshal(requestInfo.Body, &notification); err != nil {
logrus.Errorf("Failed to unmarshal notification: %v", err)
sendError(c, http.StatusBadRequest, "failed to unmarshal notification", err)
return
}
notifyType := notification.NotifyType
notifyResult := notification.Result
paymentRequestID := notification.CaptureRequestID
paymentID := notification.PaymentID
if notification.NotifyType == types.NotifyTypePaymentResult {
var paymentNotification types.PaymentNotification
if err := json.Unmarshal(requestInfo.Body, &paymentNotification); err != nil {
logrus.Errorf("Failed to unmarshal payment notification: %v", err)
sendError(c, http.StatusBadRequest, "failed to unmarshal payment notification", err)
return
}
notifyResult = paymentNotification.Result
paymentRequestID = paymentNotification.PaymentRequestID
paymentID = paymentNotification.PaymentID
}
logNotification(notification)
if err := processPaymentResult(c, notifyType, notifyResult, paymentRequestID, paymentID); err != nil {
logrus.Errorf("Failed to process payment result: %v", err)
return // 错误已在 processPaymentResult 中处理
}
sendSuccessResponse(c)
}
func sendError(c *gin.Context, status int, message string, err error) {
if err != nil {
message = fmt.Sprintf("%s: %v", message, err)
}
c.JSON(status, gin.H{"error": message})
}
// TODO delete
func logNotification(notification interface{}) {
if prettyJSON, err := json.MarshalIndent(notification, "", " "); err != nil {
logrus.Errorf("Failed to marshal notification: %v", err)
} else {
logrus.Infof("Notify: %s", string(prettyJSON))
}
}
// 辅助函数:处理支付结果
func processPaymentResult(c *gin.Context, notifyType string, notifyResult types.Result, paymentRequestID, paymentID string) error {
return processPaymentResultWithHandler(c, notifyType, notifyResult, paymentRequestID, paymentID, newCardPaymentHandler, newCardPaymentFailureHandler)
}
func newCardPaymentHandler(paymentID string, card types.CardInfo) error {
userUID, err := dao.DBClient.NewCardPaymentHandler(paymentID, card)
if err != nil {
return err
}
if userUID != uuid.Nil {
if err = sendCardPaymentPayEmail(userUID, paymentID, utils.EnvPaySuccessEmailTmpl); err != nil {
logrus.Errorf("Failed to send PAY_SUCCESS_EMAIL_TMPL email to %s: %v", userUID, err)
}
}
return nil
}
func sendCardPaymentPayEmail(userUID uuid.UUID, paymentID string, payType string) error {
var order types.PaymentOrder
if err := dao.DBClient.GetGlobalDB().Model(&types.PaymentOrder{}).Where(types.PaymentOrder{PaymentRaw: types.PaymentRaw{TradeNO: paymentID, UserUID: userUID}}).Find(&order).Error; err != nil {
return fmt.Errorf("failed to get payment order: %v", err)
}
account, err := dao.DBClient.GetAccount(types.UserQueryOpts{UID: userUID})
if err != nil {
return fmt.Errorf("failed to get account: %v", err)
}
if err := sendUserPayEmail(userUID, &utils.EmailPayRender{
Type: payType,
Domain: dao.DBClient.GetLocalRegion().Domain,
TopUpAmount: order.Amount / 1_000_000,
AccountBalance: (account.Balance - account.DeductionBalance) / 1_000_000,
}); err != nil {
return err
}
return nil
}
func newCardPaymentFailureHandler(paymentRequestID string) error {
_, err := dao.DBClient.NewCardPaymentFailureHandler(paymentRequestID)
if err != nil {
return err
}
//if userUID != uuid.Nil {
// if err := SendUserPayEmail(userUID, utils.EnvPayFailedEmailTmpl); err != nil {
// logrus.Errorf("Failed to send PAY_FAILED_EMAIL_TMPL email to %s: %v", userUID, err)
// }
//}
return nil
}
func newCardSubscriptionPaymentHandler(paymentReqID string, card types.CardInfo) error {
userUID, err := dao.DBClient.NewCardSubscriptionPaymentHandler(paymentReqID, card)
if err != nil {
return err
}
if userUID != uuid.Nil {
if err = sendUserSubPayEmailWith(userUID); err != nil {
logrus.Errorf("Failed to send SUB_SUCCESS_EMAIL_TMPL email to %s: %v", userUID, err)
}
}
return nil
}
func sendUserSubPayEmailWith(userUID uuid.UUID) error {
lastSubTransaction, err := dao.DBClient.GetLastSubscriptionTransaction(userUID)
if err != nil {
return fmt.Errorf("failed to get last subscription transaction: %v", err)
}
if lastSubTransaction.PayStatus != types.SubscriptionPayStatusPaid && lastSubTransaction.PayStatus != types.SubscriptionPayStatusNoNeed {
return fmt.Errorf("last subscription transaction pay status is not paid: %v", lastSubTransaction.PayStatus)
}
if err := sendUserPayEmail(userUID, &utils.EmailSubRender{
Type: utils.EnvSubSuccessEmailTmpl,
Operator: lastSubTransaction.Operator,
Domain: dao.DBClient.GetLocalRegion().Domain,
SubscriptionPlanName: lastSubTransaction.NewPlanName,
StartDate: lastSubTransaction.StartAt,
EndDate: lastSubTransaction.StartAt.AddDate(0, 1, 0),
}); err != nil {
return fmt.Errorf("failed to send SUB_SUCCESS_EMAIL_TMPL email to %s: %v", userUID, err)
}
return nil
}
func newCardSubscriptionPaymentFailureHandler(paymentRequestID string) error {
_, err := dao.DBClient.NewCardSubscriptionPaymentFailureHandler(paymentRequestID)
if err != nil {
return err
}
//if userUID != uuid.Nil {
// if err := SendUserPayEmail(userUID, utils.EnvSubFailedEmailTmpl); err != nil {
// logrus.Errorf("Failed to send SUB_FAILED_EMAIL_TMPL email to %s: %v", userUID, err)
// }
//}
return nil
}
func processSubscriptionPayResult(c *gin.Context, notifyType string, notifyResult types.Result, paymentRequestID, paymentID string) error {
return processPaymentResultWithHandler(c, notifyType, notifyResult, paymentRequestID, paymentID, newCardSubscriptionPaymentHandler, newCardSubscriptionPaymentFailureHandler)
}
func processPaymentResultWithHandler(c *gin.Context, notifyType string, notifyResult types.Result, paymentRequestID, paymentID string, paySuccessHandler func(paymentID string, card types.CardInfo) error, payFailureHandler func(paymentRequestID string) error) error {
if notifyType == types.NotifyTypePaymentResult && notifyResult.ResultCode == types.OrderClosedResultCode {
err := payFailureHandler(paymentRequestID)
if err != nil {
sendError(c, http.StatusInternalServerError, "failed to set payment order status", err)
} else {
sendSuccessResponse(c)
}
return err
}
if notifyType != types.NotifyTypeCaptureResult {
return nil
}
resp, err := dao.PaymentService.GetPayment(paymentRequestID, paymentID)
if err != nil {
sendError(c, http.StatusInternalServerError, "failed to get payment", err)
return err
}
if paymentRequestID == "" || paymentID == "" {
sendError(c, http.StatusBadRequest, "payment request id or payment id is empty", nil)
return errors.New("payment request id or payment id is empty")
}
if notifyResult.ResultCode != SuccessStatus || notifyResult.ResultStatus != "S" {
err = payFailureHandler(paymentRequestID)
if err != nil {
sendError(c, http.StatusInternalServerError, "failed to set payment order status", err)
} else {
sendSuccessResponse(c)
}
return err
}
if resp.Result.ResultCode != SuccessStatus || resp.Result.ResultStatus != "S" {
return fmt.Errorf("payment result is not SUCCESS: %#+v", resp.Result)
}
card := types.CardInfo{
ID: uuid.New(),
CardNo: resp.PaymentResultInfo.CardNo,
CardBrand: resp.PaymentResultInfo.CardBrand,
CardToken: resp.PaymentResultInfo.CardToken,
NetworkTransactionID: resp.PaymentResultInfo.NetworkTransactionId,
}
if err = paySuccessHandler(paymentRequestID, card); err != nil {
return err
}
return nil
}
// 辅助函数:发送成功响应
func sendSuccessResponse(c *gin.Context) {
if c.Writer.Written() {
return
}
resp := types.NewSuccessResponse()
if _, err := c.Writer.Write(resp.Raw()); err != nil {
sendError(c, http.StatusInternalServerError, "failed to write response", err)
return
}
c.Status(http.StatusOK)
}
+964
View File
@@ -0,0 +1,964 @@
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"text/template"
"time"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/labring/sealos/controllers/pkg/utils"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/labring/sealos/controllers/pkg/database/cockroach"
"gorm.io/gorm"
"github.com/sirupsen/logrus"
responsePay "github.com/alipay/global-open-sdk-go/com/alipay/api/response/pay"
"github.com/google/uuid"
services "github.com/labring/sealos/service/pkg/pay"
"github.com/gin-gonic/gin"
"github.com/labring/sealos/controllers/pkg/types"
"github.com/labring/sealos/service/account/dao"
"github.com/labring/sealos/service/account/helper"
)
// GetSubscriptionUserInfo
// @Summary Get user subscription info
// @Description Get user subscription info
// @Tags Subscription
// @Accept json
// @Produce json
// @Param req body SubscriptionUserInfoReq true "SubscriptionUserInfoReq"
// @Success 200 {object} SubscriptionUserInfoResp
// @Router /payment/v1alpha1/subscription/user-info [post]
func GetSubscriptionUserInfo(c *gin.Context) {
req := &helper.AuthBase{}
if err := authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
subscription, err := dao.DBClient.GetSubscription(&types.UserQueryOpts{UID: req.UserUID})
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get subscription info: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{
"subscription": subscription,
})
}
// GetSubscriptionPlanList
// @Summary Get subscription plan list
// @Description Get subscription plan list
// @Tags Subscription
// @Accept json
// @Produce json
// @Param req body SubscriptionPlanListReq true "SubscriptionPlanListReq"
// @Success 200 {object} SubscriptionPlanListResp
// @Router /payment/v1alpha1/subscription/plan-list [post]
func GetSubscriptionPlanList(c *gin.Context) {
plans, err := dao.DBClient.GetSubscriptionPlanList()
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get subscription plan list: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{
"plans": plans,
})
}
// GetSubscriptionLastTransaction
// @Summary Get user last subscription transaction
// @Description Get user last subscription transaction
// @Tags Subscription
// @Accept json
// @Produce json
// @Param req body SubscriptionLastTransactionReq true "SubscriptionLastTransactionReq"
// @Success 200 {object} SubscriptionLastTransactionResp
// @Router /payment/v1alpha1/subscription/last-transaction [post]
func GetLastSubscriptionTransaction(c *gin.Context) {
req := &helper.AuthBase{}
if err := authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
transaction, err := dao.DBClient.GetLastSubscriptionTransaction(req.UserUID)
if err != nil && err != gorm.ErrRecordNotFound {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get last subscription transaction: %v", err)})
return
}
if transaction == nil {
transaction = &types.SubscriptionTransaction{}
}
c.JSON(http.StatusOK, gin.H{
"transaction": transaction,
})
}
// GetSubscriptionUpgradeAmount
// @Summary Get subscription upgrade amount
// @Description Get subscription upgrade amount
// @Tags Subscription
// @Accept json
// @Produce json
// @Param req body SubscriptionUpgradeAmountReq true "SubscriptionUpgradeAmountReq"
// @Success 200 {object} SubscriptionUpgradeAmountResp
// @Router /payment/v1alpha1/subscription/upgrade-amount [post]
func GetSubscriptionUpgradeAmount(c *gin.Context) {
req, err := helper.ParseSubscriptionOperatorReq(c)
if err != nil {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: fmt.Sprintf("failed to parse request: %v", err)})
return
}
if err := authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
userSubscription, err := dao.DBClient.GetSubscription(&types.UserQueryOpts{UID: req.UserUID})
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get subscription info: %v", err)})
return
}
if userSubscription.PlanName == req.PlanName {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: "plan name is same as current plan"})
return
}
currentSubPlan, err := dao.DBClient.GetSubscriptionPlan(userSubscription.PlanName)
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get current plan: %v", err)})
return
}
if currentSubPlan.Amount <= 0 {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: "current plan is free plan"})
return
}
describeSubPlan, err := dao.DBClient.GetSubscriptionPlan(req.PlanName)
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get describe plan: %v", err)})
return
}
if describeSubPlan.Amount <= currentSubPlan.Amount {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: "describe plan amount is less than current plan amount"})
return
}
alreadyUsedDays := time.Since(userSubscription.StartAt).Hours() / 24
usedAmount := (30 - alreadyUsedDays) / 30 * float64(currentSubPlan.Amount)
value := float64(describeSubPlan.Amount) - usedAmount
c.JSON(http.StatusOK, gin.H{
"amount": int64(value),
})
}
// CheckSubscriptionQuota
// @Summary Check user subscription quota
// @Description Check user subscription quota
// @Tags Subscription
// @Accept json
// @Produce json
// @Param req body SubscriptionQuotaCheckReq true "SubscriptionQuotaCheckReq"
// @Success 200 {object} SubscriptionQuotaCheckResp
// @Router /payment/v1alpha1/subscription/quota-check [post]
func CheckSubscriptionQuota(c *gin.Context) {
req, err := helper.ParseSubscriptionQuotaCheckReq(c)
if err != nil {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: fmt.Sprintf("failed to parse request: %v", err)})
return
}
if err = authenticateRequest(c, req); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
if req.Owner == "" {
//SubscriptionQuotaCheckResp
c.JSON(http.StatusOK, helper.SubscriptionQuotaCheckResp{
AllWorkspaceReady: true,
})
return
}
planQuota, ok := dao.SubPlanResourceQuota[req.PlanName]
if !ok {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: fmt.Sprintf("plan name is not in plan resource quota: %v", req.PlanName)})
return
}
readyWorkspace, unReadyWorkspace := []string{}, []string{}
config, err := rest.InClusterConfig()
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("get in cluster config failed: %v", err)})
return
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("new client set failed: %v", err)})
return
}
nsList, err := getOwnNsListWithClt(dao.K8sManager.GetClient(), req.Owner)
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("get own namespace list failed: %v", err)})
return
}
for _, ns := range nsList {
quota, err := clientset.CoreV1().ResourceQuotas(ns).Get(context.Background(), "quota-"+ns, metav1.GetOptions{})
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("get resource quota failed: %v", err)})
return
}
if checkQuota(quota.Status.Used, planQuota) {
readyWorkspace = append(readyWorkspace, ns)
} else {
unReadyWorkspace = append(unReadyWorkspace, ns)
}
}
c.JSON(http.StatusOK, helper.SubscriptionQuotaCheckResp{
AllWorkspaceReady: len(unReadyWorkspace) == 0,
ReadyWorkspace: readyWorkspace,
UnReadyWorkspace: unReadyWorkspace,
})
}
func checkQuota(req corev1.ResourceList, des corev1.ResourceList) bool {
for key, value := range req {
if value.Cmp(des[key]) > 0 {
return false
}
}
return true
}
func Retry(attempts int, sleep time.Duration, f func() error) error {
var err error
for i := 0; i < attempts; i++ {
err = f()
if err == nil {
return nil
}
time.Sleep(sleep)
}
return err
}
func getOwnNsListWithClt(clt client.Client, user string) ([]string, error) {
if user == "" {
return nil, fmt.Errorf("user is empty")
}
nsList := &corev1.NamespaceList{}
err := clt.List(context.Background(), nsList, client.MatchingLabels{dao.UserOwnerLabel: user})
if err != nil {
return nil, fmt.Errorf("list namespace failed: %w", err)
}
nsListStr := make([]string, len(nsList.Items))
for i := range nsList.Items {
nsListStr[i] = nsList.Items[i].Name
}
return nsListStr, nil
}
func getDefaultResourceQuota(ns, name string, hard corev1.ResourceList) *corev1.ResourceQuota {
return &corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
Spec: corev1.ResourceQuotaSpec{
Hard: hard,
},
}
}
// SubscriptionPay
// @Summary Subscription pay
// @Description Subscription pay
// @Tags Subscription
// @Accept json
// @Produce json
// @Param req body SubscriptionPayReq true "SubscriptionPayReq"
// @Success 200 {object} SubscriptionPayResp
// @Router /payment/v1alpha1/subscription/pay [post]
func CreateSubscriptionPay(c *gin.Context) {
req, err := helper.ParseSubscriptionOperatorReq(c)
if err != nil {
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse request: %v", err)})
return
}
if err := authenticateRequest(c, req); err != nil {
SetErrorResp(c, http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("authenticate error : %v", err)})
return
}
userSubscription, err := dao.DBClient.GetSubscription(&types.UserQueryOpts{UID: req.UserUID})
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get subscription info: %v", err)})
return
}
if userSubscription.PlanName == req.PlanName && req.PlanType != helper.Renewal {
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": "plan name is same as current plan"})
return
}
if req.PlanName == types.FreeSubscriptionPlanName && req.PlanType != helper.Downgrade {
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": "free plan can only downgrade"})
return
}
planList, err := dao.DBClient.GetSubscriptionPlanList()
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get subscription plan list: %v", err)})
return
}
var userCurrentPlan, userDescribePlan types.SubscriptionPlan
for _, plan := range planList {
if plan.Name == req.PlanName {
userDescribePlan = plan
}
if plan.Name == userSubscription.PlanName {
userCurrentPlan = plan
}
}
subTransaction := types.SubscriptionTransaction{
ID: uuid.New(),
SubscriptionID: userSubscription.ID,
UserUID: req.UserUID,
OldPlanID: userCurrentPlan.ID,
OldPlanName: userCurrentPlan.Name,
OldPlanStatus: userSubscription.Status,
StartAt: time.Now().UTC(),
NewPlanID: userDescribePlan.ID,
NewPlanName: userDescribePlan.Name,
CreatedAt: time.Now().UTC(),
Status: types.SubscriptionTransactionStatusProcessing,
}
//TODO 预检测同一个用户同时只能有一个未处理或处理中(status: Pending,Processing)的订阅操作订单
switch req.PlanType {
case helper.Upgrade:
// TODO implement subscription upgrade
if !contain(userCurrentPlan.UpgradePlanList, req.PlanName) {
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": fmt.Sprintf("plan name is not in upgrade plan list: %v", userCurrentPlan.UpgradePlanList)})
return
}
// TODO implement subscription pay
if userCurrentPlan.Amount <= 0 {
subTransaction.Operator = types.SubscriptionTransactionTypeCreated
//TODO 新订阅
subTransaction.Amount = userDescribePlan.Amount
} else {
//TODO 升级订阅
subTransaction.Operator = types.SubscriptionTransactionTypeUpgraded
// TODO free->hobby:直接购买
//hobby->pro:按照hobby未使用天数补充差价。补充差价为a,hobby已使用天数为d,计算公式:a = 5*(d/30) + 15
// userSubscription.StartAt 到 now的天数
alreadyUsedDays := time.Since(userSubscription.StartAt).Hours() / 24
usedAmount := (30 - alreadyUsedDays) / 30 * float64(userCurrentPlan.Amount)
value := float64(userDescribePlan.Amount) - usedAmount
//remainingDays := float64(30) - alreadyUsedDays
//currentPlanSurplusValue := math.Ceil(float64(userCurrentPlan.Amount) * math.Ceil(remainingDays/30))
//describePlanSurplusValue := math.Ceil(float64(userDescribePlan.Amount) * math.Ceil(remainingDays/30))
//// amount
subTransaction.Amount = int64(value)
//TODO 临近到期的情况处理
}
case helper.Downgrade:
// TODO implement subscription downgrade
// 符合降级规则
if !contain(userCurrentPlan.DowngradePlanList, req.PlanName) {
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": fmt.Sprintf("plan name is not in downgrade plan list: %v", userCurrentPlan.DowngradePlanList)})
return
}
//执行时间为计划的下个周期开始变为对应的版本,目前降级为Free
subTransaction.StartAt = userSubscription.NextCycleDate.Add(-20 * time.Minute)
subTransaction.Operator = types.SubscriptionTransactionTypeDowngraded
subTransaction.Status = types.SubscriptionTransactionStatusPending
case helper.Renewal:
if userSubscription.PlanName != req.PlanName {
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": "plan name is not same as current plan"})
return
}
// TODO 只变更到期时间
subTransaction.Amount = userDescribePlan.Amount
subTransaction.Operator = types.SubscriptionTransactionTypeRenewed
}
if subTransaction.Amount > 0 {
PayForSubscription(c, req, subTransaction)
return
} else {
SubscriptionWithOutPay(c, req, subTransaction)
}
}
func SetSuccessResp(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": "true"})
}
func SetErrorResp(c *gin.Context, code int, h map[string]any) {
h["success"] = false
c.JSON(code, h)
}
func PayForSubscription(c *gin.Context, req *helper.SubscriptionOperatorReq, subTransaction types.SubscriptionTransaction) {
if req.PayMethod != helper.CARD {
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": "invalid pay method"})
return
}
lastSubTransaction, err := dao.DBClient.GetLastSubscriptionTransaction(req.UserUID)
if err != nil && err != gorm.ErrRecordNotFound {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to get last subscription transaction: ", err)})
return
}
logrus.Infof("last subscription transaction: %v", lastSubTransaction)
if lastSubTransaction != nil && (lastSubTransaction.Status == types.SubscriptionTransactionStatusProcessing || lastSubTransaction.Status == types.SubscriptionTransactionStatusPending) {
if lastSubTransaction.Operator == types.SubscriptionTransactionTypeDowngraded {
if err := dao.DBClient.GetGlobalDB().Delete(&lastSubTransaction).Error; err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to delete last subscription transaction: ", err)})
return
}
PayForSubscription(c, req, subTransaction)
return
}
if lastSubTransaction.PayStatus == types.SubscriptionPayStatusNoNeed {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": "The last subscription operation was not processed, please wait for the next cycle"})
return
}
// TODO
// Check if the last transaction is the same as this one
if lastSubTransaction.PayStatus == types.SubscriptionPayStatusFailed {
dao.DBClient.GetGlobalDB().Model(&lastSubTransaction).Update("status", types.SubscriptionTransactionStatusFailed)
logrus.Errorf("last subscription transaction pay failed, user: %s", req.UserUID)
PayForSubscription(c, req, subTransaction)
return
}
payment := &types.PaymentOrder{}
if err := dao.DBClient.GetGlobalDB().Model(&types.PaymentOrder{}).Where(
`"userUid" = ? AND "id" = ?`, req.UserUID, lastSubTransaction.PayID).Find(&payment).Error; err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to get payment: ", err)})
return
}
logrus.Infof("payment: %v", payment)
if payment.TradeNO == "" {
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": "payment trade no is empty"})
return
}
logrus.Infof("payment trade no: %s", payment.TradeNO)
payQueryResp, err := dao.PaymentService.QueryPayment(payment.TradeNO, "")
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to query payment: ", err)})
return
}
if payQueryResp.Result.ResultCode != SuccessStatus && payQueryResp.Result.ResultStatus != "S" {
data, err := json.MarshalIndent(payQueryResp, "", " ")
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to marshal pay query response: ", err)})
return
}
logrus.Errorf("payment is failed, payQueryResp: %s", data)
err = dao.DBClient.GetGlobalDB().Model(&payment).Update("status", types.PaymentOrderStatusFailed).Error
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to update payment status: ", err)})
return
}
err = dao.DBClient.GetGlobalDB().Model(&lastSubTransaction).Update("status", types.SubscriptionTransactionStatusFailed).Error
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to update subscription transaction status: ", err)})
return
}
PayForSubscription(c, req, subTransaction)
return
}
/*
SUCCESS支付成功
FAIL支付失败
PROCESSING支付处理中
CANCELLED支付已取消
PENDING支付完成等待最终支付结果
*/
switch payQueryResp.PaymentStatus {
case "SUCCESS":
if req.CardID != nil {
cardInfo, err := dao.DBClient.GetCardInfo(req.UserUID, req.UserUID)
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to get card info: ", err)})
return
}
cardInfo.CardToken = payQueryResp.CardInfo.CardToken
if err = newCardSubscriptionPaymentHandler(payment.TradeNO, *cardInfo); err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to handle new card subscription payment: ", err)})
return
}
SetSuccessResp(c)
return
}
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": "payment success"})
return
case "PROCESSING":
if payment.CodeURL == "" {
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": "payment code url is empty"})
return
}
c.JSON(http.StatusOK, gin.H{"redirectUrl": payment.CodeURL, "success": true})
return
case "FAIL":
// TODO
err := dao.DBClient.GetGlobalDB().Model(&payment).Update("status", types.PaymentOrderStatusFailed).Error
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to update payment status: ", err)})
return
}
err = dao.DBClient.GetGlobalDB().Model(&lastSubTransaction).Update("status", types.SubscriptionTransactionStatusFailed).Error
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to update subscription transaction status: ", err)})
return
}
PayForSubscription(c, req, subTransaction)
return
case "CANCELLED":
// TODO 处理上次状态
err = dao.DBClient.GlobalTransactionHandler(func(tx *gorm.DB) error {
if err := tx.Model(&payment).Update("status", types.PaymentOrderStatusFailed).Error; err != nil {
return fmt.Errorf("failed to update payment status: %w", err)
}
if err := tx.Model(&lastSubTransaction).Update("status", types.SubscriptionTransactionStatusFailed).Error; err != nil {
return fmt.Errorf("failed to update subscription transaction status: %w", err)
}
return nil
})
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to update payment status: ", err)})
return
}
PayForSubscription(c, req, subTransaction)
return
case "PENDING":
time.Sleep(time.Second * 1)
logrus.Errorf("payment is pending, user: %s, %s", req.UserUID, payment.TradeNO)
PayForSubscription(c, req, subTransaction)
return
default:
SetErrorResp(c, http.StatusBadRequest, gin.H{"error": fmt.Sprintf("payment status is invalid: %v", payQueryResp.PaymentStatus)})
return
}
}
paymentReq := services.PaymentRequest{
RequestID: uuid.NewString(),
UserUID: req.UserUID,
Amount: subTransaction.Amount,
Currency: dao.PaymentCurrency,
UserAgent: c.GetHeader("User-Agent"),
ClientIP: c.ClientIP(),
DeviceTokenID: c.GetHeader("Device-Token-ID"),
}
paymentID, err := gonanoid.New(12)
if err != nil {
SetErrorResp(c, http.StatusInternalServerError, gin.H{"error": fmt.Sprint("failed to create payment id: ", err)})
return
}
subTransaction.PayID = paymentID
var paySvcResp *responsePay.AlipayPayResponse
var createPayHandler func(tx *gorm.DB) error
if req.CardID != nil {
createPayHandler = func(tx *gorm.DB) error {
card, hErr := dao.DBClient.GetCardInfo(*req.CardID, req.UserUID)
if hErr != nil {
return fmt.Errorf("failed to get card info: %w", hErr)
}
paySvcResp, hErr = dao.PaymentService.CreateSubscriptionPayWithCard(paymentReq, card)
if hErr != nil {
return fmt.Errorf("failed to create payment with card: %w", hErr)
}
if (paySvcResp.Result.ResultCode == "SUCCESS" && paySvcResp.Result.ResultStatus == "S") || (paySvcResp.Result.ResultCode == "PAYMENT_IN_PROCESS" && paySvcResp.Result.ResultStatus == "U") {
return nil
}
return fmt.Errorf("payment result is not SUCCESS: %#+v", paySvcResp.Result)
}
} else {
createPayHandler = func(tx *gorm.DB) error {
var hErr error
paySvcResp, hErr = dao.PaymentService.CreateNewSubscriptionPay(paymentReq)
if err != nil {
return fmt.Errorf("failed to create payment: %w", hErr)
}
if paySvcResp.Result.ResultCode != "PAYMENT_IN_PROCESS" || paySvcResp.Result.ResultStatus != "U" {
return fmt.Errorf("payment result is not PAYMENT_IN_PROCESS: %#+v", paySvcResp.Result)
}
return nil
}
}
err = dao.DBClient.GlobalTransactionHandler(func(tx *gorm.DB) error {
// check that there are no subscription changes
count, dErr := cockroach.GetActiveSubscriptionTransactionCount(tx, req.UserUID)
if err != nil {
return fmt.Errorf("failed to get active subscription transaction count: %w", dErr)
}
if count > 0 {
return fmt.Errorf("there is active subscription transaction")
}
subTransaction.PayStatus = types.SubscriptionPayStatusPending
dErr = cockroach.CreateSubscriptionTransaction(tx, &subTransaction)
if dErr != nil {
return fmt.Errorf("failed to create subscription transaction: %w", dErr)
}
return nil
}, func(tx *gorm.DB) error {
dErr := cockroach.CreatePaymentOrder(
tx, &types.PaymentOrder{
ID: paymentID,
PaymentRaw: types.PaymentRaw{
UserUID: req.UserUID,
Amount: subTransaction.Amount,
Method: req.PayMethod,
RegionUID: dao.DBClient.GetLocalRegion().UID,
TradeNO: paymentReq.RequestID,
//CodeURL: paySvcResp.NormalUrl,
Type: types.PaymentTypeSubscription,
ChargeSource: types.ChargeSourceNewCard,
},
Status: types.PaymentOrderStatusPending,
})
if dErr != nil {
return fmt.Errorf("failed to create payment order: %w", dErr)
}
return nil
}, createPayHandler, func(tx *gorm.DB) error {
if paySvcResp.NormalUrl != "" {
dErr := tx.Model(&types.PaymentOrder{}).Where("id = ?", paymentID).Update("code_url", paySvcResp.NormalUrl).Error
if dErr != nil {
logrus.Warnf("failed to update payment order code url: %v", dErr)
}
}
return nil
})
if err != nil {
SetErrorResp(c, http.StatusConflict, gin.H{"error": fmt.Sprint("failed to create payment order: ", err)})
return
}
c.JSON(http.StatusOK, gin.H{"redirectUrl": paySvcResp.NormalUrl, "success": true})
}
func SubscriptionWithOutPay(c *gin.Context, req *helper.SubscriptionOperatorReq, subTransaction types.SubscriptionTransaction) {
err := dao.DBClient.GlobalTransactionHandler(func(tx *gorm.DB) error {
// TODO 检查没有订阅变更
count, err := cockroach.GetActiveSubscriptionTransactionCount(tx, req.UserUID)
if err != nil {
return fmt.Errorf("failed to get active subscription transaction count: %w", err)
}
if count > 0 {
return fmt.Errorf("there is active subscription transaction")
}
subTransaction.PayStatus = types.SubscriptionPayStatusNoNeed
err = cockroach.CreateSubscriptionTransaction(tx, &subTransaction)
if err != nil {
return fmt.Errorf("failed to create subscription transaction: %w", err)
}
return nil
})
if err != nil {
SetErrorResp(c, http.StatusConflict, gin.H{"error": fmt.Sprint("failed to create subscription transaction: ", err)})
return
}
if req.PlanName != types.FreeSubscriptionPlanName {
err = sendUserSubPayEmailWith(req.UserUID)
if err != nil {
logrus.Errorf("failed to send user %s SubscriptionWithOutPay email: %v", req.UserID, err)
}
}
SetSuccessResp(c)
}
func SubscriptionPayForBindCard(paymentReq services.PaymentRequest, req *helper.SubscriptionOperatorReq, subTransaction *types.SubscriptionTransaction) error {
paymentID, err := gonanoid.New(12)
if err != nil {
return fmt.Errorf("failed to create payment id: %w", err)
}
subTransaction.PayID = paymentID
var paySvcResp *responsePay.AlipayPayResponse
card, err := dao.DBClient.GetCardInfo(*req.CardID, req.UserUID)
if err != nil {
return fmt.Errorf("failed to get card info: %w", err)
}
if card.CardToken == "" {
return fmt.Errorf("card token is empty, please rebind card")
}
payment := types.Payment{
ID: paymentID,
PaymentRaw: types.PaymentRaw{
UserUID: req.UserUID,
Amount: subTransaction.Amount,
Method: req.PayMethod,
RegionUID: dao.DBClient.GetLocalRegion().UID,
TradeNO: paymentReq.RequestID,
Type: types.PaymentTypeSubscription,
CardUID: req.CardID,
ChargeSource: types.ChargeSourceBindCard,
},
}
err = dao.DBClient.GlobalTransactionHandler(func(tx *gorm.DB) error {
// TODO 检查没有订阅变更
count, err := cockroach.GetActiveSubscriptionTransactionCount(tx, req.UserUID)
if err != nil {
return fmt.Errorf("failed to get active subscription transaction count: %w", err)
}
if count > 0 {
return fmt.Errorf("there is active subscription transaction")
}
subTransaction.PayStatus = types.SubscriptionPayStatusPaid
err = cockroach.CreateSubscriptionTransaction(tx, subTransaction)
if err != nil {
return fmt.Errorf("failed to create subscription transaction: %w", err)
}
if err := tx.First(&types.Payment{ID: payment.ID}).Error; err == nil {
return nil
}
if err := tx.Create(&payment).Error; err != nil {
return fmt.Errorf("failed to save payment: %w", err)
}
err = tx.Model(&types.Subscription{}).Where(&types.Subscription{ID: subTransaction.SubscriptionID, UserUID: req.UserUID}).Update("card_id", req.CardID).Error
if err != nil {
return fmt.Errorf("failed to update subscription card id: %w", err)
}
paySvcResp, err = dao.PaymentService.CreateSubscriptionPayWithCard(paymentReq, card)
if err != nil {
return fmt.Errorf("failed to create payment with card: %w", err)
}
if paySvcResp.Result.ResultCode != SuccessStatus || paySvcResp.Result.ResultStatus != "S" {
return fmt.Errorf("payment result is not SUCCESS: %#+v", paySvcResp.Result)
}
// TODO 发邮箱通知
//if err := SendUserPayEmail(req.UserUID, utils.EnvSubSuccessEmailTmpl); err != nil {
// logrus.Errorf("failed to send user %s email: %v", req.UserID, err)
//}
return nil
})
if err != nil {
return fmt.Errorf("failed to create payment: %w", err)
}
return nil
}
func sendUserPayEmail(userUID uuid.UUID, emailRender utils.EmailRenderBuilder) error {
if dao.EmailTmplMap[emailRender.GetType()] == "" {
return fmt.Errorf("email type %s is invalid", emailRender.GetType())
}
tx := dao.DBClient.GetGlobalDB()
var emailProvider types.OauthProvider
var userInfo types.UserInfo
err := dao.DBClient.GetGlobalDB().Where(&types.OauthProvider{UserUID: userUID, ProviderType: types.OauthProviderTypeEmail}).First(&emailProvider).Error
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to get email provider: %w", err)
}
if err == gorm.ErrRecordNotFound {
return fmt.Errorf("email provider is not found")
}
fmt.Printf("emailProvider.ProviderID: %s\n", emailProvider.ProviderID)
if emailProvider.ProviderID != "" {
err = tx.Where(types.UserInfo{UserUID: userUID}).Find(&userInfo).Error
if err != nil {
return fmt.Errorf("failed to get user info: %w", err)
}
emailRender.SetUserInfo(&userInfo)
funcMap := template.FuncMap{
"sub": func(a, b int) int {
return a - b
},
}
tmp, err := template.New("subscription-success").Funcs(funcMap).Parse(dao.EmailTmplMap[emailRender.GetType()])
if err != nil {
return fmt.Errorf("failed to parse email template: %w", err)
}
var rendered bytes.Buffer
if err = tmp.Execute(&rendered, emailRender.Build()); err != nil {
return fmt.Errorf("failed to render email template: %w", err)
}
if err := dao.SMTPConfig.SendEmailWithSubject(emailRender.GetSubject(), rendered.String(), emailProvider.ProviderID); err != nil {
return fmt.Errorf("failed to send email: %w", err)
}
return nil
}
return fmt.Errorf("email provider is empty")
}
func SendUserPayEmail(userUID uuid.UUID, payType string) error {
tx := dao.DBClient.GetGlobalDB()
var emailProvider types.OauthProvider
var userInfo types.UserInfo
err := dao.DBClient.GetGlobalDB().Where(&types.OauthProvider{UserUID: userUID, ProviderType: types.OauthProviderTypeEmail}).First(&emailProvider).Error
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to get email provider: %w", err)
}
if emailProvider.ProviderID != "" {
err = tx.Where(types.UserInfo{UserUID: userUID}).Find(&userInfo).Error
if err != nil {
return fmt.Errorf("failed to get user info: %w", err)
}
tmp, err := template.New("subscription-success").Parse(dao.EmailTmplMap[payType])
if err != nil {
return fmt.Errorf("failed to parse email template: %w", err)
}
var rendered bytes.Buffer
if err = tmp.Execute(&rendered, map[string]string{
"FirstName": userInfo.FirstName,
"LastName": userInfo.LastName,
"Domain": dao.DBClient.GetLocalRegion().Domain,
}); err != nil {
return fmt.Errorf("failed to render email template: %w", err)
}
if err := dao.SMTPConfig.SendEmail(rendered.String(), emailProvider.ProviderID); err != nil {
return fmt.Errorf("failed to send email: %w", err)
}
}
return nil
}
func SubscriptionPayByBalance(req *helper.SubscriptionOperatorReq, subTransaction *types.SubscriptionTransaction) error {
if subTransaction.PayID == "" {
paymentID, err := gonanoid.New(12)
if err != nil {
return fmt.Errorf("failed to create payment id: %w", err)
}
subTransaction.PayID = paymentID
}
err := dao.DBClient.GlobalTransactionHandler(func(tx *gorm.DB) error {
// TODO 检查没有订阅变更
count, dErr := cockroach.GetActiveSubscriptionTransactionCount(tx, req.UserUID)
if dErr != nil {
return fmt.Errorf("failed to get active subscription transaction count: %w", dErr)
}
if count > 0 {
return fmt.Errorf("there is active subscription transaction")
}
// check account balance
var account types.Account
if dErr = tx.Where(types.Account{UserUID: subTransaction.UserUID}).First(&account).Error; dErr != nil {
return fmt.Errorf("failed to get account: %w", dErr)
}
if account.Balance-account.DeductionBalance < subTransaction.Amount {
return fmt.Errorf("insufficient balance")
}
subTransaction.PayStatus = types.SubscriptionPayStatusPaid
dErr = cockroach.CreateSubscriptionTransaction(tx, subTransaction)
if dErr != nil {
return fmt.Errorf("failed to create subscription transaction: %w", dErr)
}
payment := types.Payment{
ID: subTransaction.PayID,
PaymentRaw: types.PaymentRaw{
UserUID: req.UserUID,
Amount: subTransaction.Amount,
Method: req.PayMethod,
RegionUID: dao.DBClient.GetLocalRegion().UID,
Type: types.PaymentTypeSubscription,
ChargeSource: types.ChargeSourceBalance,
TradeNO: subTransaction.PayID,
},
}
if dErr = tx.Save(&payment).Error; dErr != nil {
return fmt.Errorf("failed to save payment %#+v: %w", payment, dErr)
}
dErr = cockroach.AddDeductionAccount(tx, subTransaction.UserUID, subTransaction.Amount)
if dErr != nil {
return fmt.Errorf("failed to add deduction account: %w", dErr)
}
return nil
})
return err
}
func NewSubscriptionPayNotifyHandler(c *gin.Context) {
requestInfo := requestInfoStruct{
Path: c.Request.RequestURI,
Method: c.Request.Method,
ResponseTime: c.GetHeader("request-time"),
ClientID: c.GetHeader("client-id"),
Signature: c.GetHeader("signature"),
}
var err error
requestInfo.Body, err = c.GetRawData()
if err != nil {
logrus.Errorf("Failed to get raw data: %v", err)
sendError(c, http.StatusBadRequest, "failed to get raw data", err)
return
}
if ok, err := dao.PaymentService.CheckRspSign(
requestInfo.Path,
requestInfo.Method,
requestInfo.ClientID,
requestInfo.ResponseTime,
string(requestInfo.Body),
requestInfo.Signature,
); err != nil {
logrus.Errorf("Failed to check response sign: %v", err)
logrus.Errorf("Path: %s\n Method: %s\n ClientID: %s\n ResponseTime: %s\n Body: %s\n Signature: %s", requestInfo.Path, requestInfo.Method, requestInfo.ClientID, requestInfo.ResponseTime, string(requestInfo.Body), requestInfo.Signature)
sendError(c, http.StatusUnauthorized, "failed to check response sign", err)
return
} else if !ok {
logrus.Errorf("Check signature fail")
sendError(c, http.StatusBadRequest, "check signature fail", nil)
return
}
var notification types.CaptureNotification
if err := json.Unmarshal(requestInfo.Body, &notification); err != nil {
logrus.Errorf("Failed to unmarshal notification: %v", err)
sendError(c, http.StatusBadRequest, "failed to unmarshal notification", err)
return
}
notifyType := notification.NotifyType
notifyResult := notification.Result
paymentRequestID := notification.CaptureRequestID
paymentID := notification.PaymentID
if notification.NotifyType == types.NotifyTypePaymentResult {
var paymentNotification types.PaymentNotification
if err := json.Unmarshal(requestInfo.Body, &paymentNotification); err != nil {
logrus.Errorf("Failed to unmarshal payment notification: %v", err)
sendError(c, http.StatusBadRequest, "failed to unmarshal payment notification", err)
return
}
notifyResult = paymentNotification.Result
paymentRequestID = paymentNotification.PaymentRequestID
paymentID = paymentNotification.PaymentID
logNotification(paymentNotification)
} else {
logNotification(notification)
}
if err = processSubscriptionPayResult(c, notifyType, notifyResult, paymentRequestID, paymentID); err != nil && err != dao.ErrPaymentOrderAlreadyHandle {
logrus.Errorf("Failed to process sub payment result: %v", err)
return // 错误已在 processPaymentResult 中处理
}
sendSuccessResponse(c)
}
func contain(planList []string, planName string) bool {
for _, plan := range planList {
if plan == planName {
return true
}
}
return false
}
+543
View File
@@ -0,0 +1,543 @@
package api
import (
"context"
"fmt"
"time"
"github.com/labring/sealos/service/account/helper"
services "github.com/labring/sealos/service/pkg/pay"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"github.com/labring/sealos/controllers/pkg/types"
"github.com/labring/sealos/service/account/dao"
)
const (
// ExpirationReminderDays 订阅到期前多少天开始提醒
ExpirationReminderDays = 7
// PollingInterval 轮询间隔
PollingInterval = 1 * time.Hour
// LockTimeout 分布式锁超时时间
LockTimeout = 10 * time.Minute
// BatchSize 处理批次大小
BatchSize = 100
)
// SubscriptionProcessor 处理订阅到期和自动续费
type SubscriptionProcessor struct {
db *gorm.DB
}
// NewSubscriptionProcessor 创建订阅处理器
func NewSubscriptionProcessor(db *gorm.DB) *SubscriptionProcessor {
return &SubscriptionProcessor{
db: db,
}
}
// StartProcessing 开始处理订阅
func (p *SubscriptionProcessor) StartProcessing(ctx context.Context) {
logrus.Info("Starting subscription expiration processor")
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
// 立即执行一次,然后按照间隔定期执行
p.processSubscriptions()
for {
select {
case <-ticker.C:
p.processSubscriptions()
case <-ctx.Done():
logrus.Info("Stopping subscription expiration processor")
return
}
}
}
// StartKYCProcessing 开始处理 KYC
func (p *SubscriptionProcessor) StartKYCProcessing(ctx context.Context) {
logrus.Info("Starting KYC processor")
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
p.ProcessKYCCredits()
for {
select {
case <-ticker.C:
p.ProcessKYCCredits()
case <-ctx.Done():
logrus.Info("Stopping KYC processor")
return
}
}
}
func (p *SubscriptionProcessor) StartFlushQuotaProcessing(ctx context.Context) {
logrus.Info("Starting flush quota processor")
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
err := dao.FlushQuotaProcesser.Execute()
if err != nil {
logrus.Errorf("Failed to execute flush quota task: %v", err)
}
for {
select {
case <-ticker.C:
err := dao.FlushQuotaProcesser.Execute()
if err != nil {
logrus.Errorf("Failed to execute flush quota task: %v", err)
}
case <-ctx.Done():
logrus.Info("Stopping flush quota processor")
return
}
}
}
// acquireProcessingLock 获取分布式锁
func (p *SubscriptionProcessor) acquireProcessingLock(lockID string) (bool, error) {
// 使用数据库实现分布式锁
// 这里使用一个简单的表来实现锁机制
var result struct {
Acquired bool
}
err := p.db.Raw(`
INSERT INTO subscription_processor_locks (id, lock_until)
VALUES (?, ?)
ON CONFLICT (id) DO UPDATE
SET lock_until = EXCLUDED.lock_until
WHERE subscription_processor_locks.lock_until < NOW()
RETURNING true as acquired
`, lockID, time.Now().UTC().Add(LockTimeout)).Scan(&result).Error
if err != nil {
return false, err
}
return result.Acquired, nil
}
// releaseProcessingLock 释放分布式锁
func (p *SubscriptionProcessor) releaseProcessingLock(lockID string) error {
return p.db.Exec(`
UPDATE subscription_processor_locks
SET lock_until = NOW()
WHERE id = ?
`, lockID).Error
}
// processExpiredSubscriptions 处理已过期的订阅
func (p *SubscriptionProcessor) processExpiredSubscriptions() error {
logrus.Info("Processing expired subscriptions")
// find subscriptions that have expired
var expiredSubscriptions []types.Subscription
err := p.db.Transaction(func(tx *gorm.DB) error {
// 查找已过期但状态仍为正常的订阅
return tx.Raw(`
SELECT s.* FROM "Subscription" s
WHERE s.expire_at < ?AND s.status = ?
AND s.plan_name != ?
LIMIT ?
`, time.Now().UTC().Add(10*time.Minute), types.SubscriptionStatusNormal, types.FreeSubscriptionPlanName, BatchSize).Scan(&expiredSubscriptions).Error
})
if err != nil {
return fmt.Errorf("failed to query expired subscriptions: %w", err)
}
logrus.Infof("Found %d expired subscriptions", len(expiredSubscriptions))
// process each expired subscription
for _, subscription := range expiredSubscriptions {
logrus.Infof("Renewal subscription plan: %v", subscription)
//TODO transaction operation:
// 1. Create a renewal subscription transaction for an expiring subscription
// 2. Automatically renew payment by binding cardID (create payment order, manage transaction PayID, initiate tied card payment, deduct payment by balance if card payment fails, and change payment information to ChargeSourceBalance)
// 3. Notification of successful renewal and information on the source of deduction
// 4. Renewal failure to send a payment failure notification
// 5. Change the subscription transaction pay_status
// 6. The specific updating of the subscription table is handled by another controller and is not required here
if err := p.HandlerSubscriptionTransaction(&subscription); err != nil {
logrus.Errorf("Failed to process subscription %s: %v", subscription.ID, err)
}
}
return nil
}
func (p *SubscriptionProcessor) HandlerSubscriptionTransaction(subscription *types.Subscription) error {
subPlan, err := dao.DBClient.GetSubscriptionPlan(subscription.PlanName)
if err != nil {
return fmt.Errorf("failed to get subscription plan: %w", err)
}
// TODO Gets a subscription transaction record and skips if there are already unprocessed renewal transactions
subTransaction := types.SubscriptionTransaction{
ID: uuid.New(),
SubscriptionID: subscription.ID,
UserUID: subscription.UserUID,
OldPlanID: subPlan.ID,
OldPlanName: subPlan.Name,
OldPlanStatus: subscription.Status,
StartAt: time.Now().UTC(),
NewPlanID: subPlan.ID,
NewPlanName: subPlan.Name,
Amount: subPlan.Amount,
Operator: types.SubscriptionTransactionTypeRenewed,
CreatedAt: time.Now().UTC(),
Status: types.SubscriptionTransactionStatusProcessing,
}
//TODO if free subscription, determine whether to bind github account. If bound, renewal subscription; otherwise, the status changes to Debt
if subscription.PlanName == types.FreeSubscriptionPlanName && subscription.Status == types.SubscriptionStatusNormal {
// TODO 待删除逻辑
//ok, err := HasGithubOauthProvider(p.db, subscription.UserUID)
//if err != nil {
// return fmt.Errorf("failed to check github oauth provider: %w", err)
//}
//if ok {
// subTransaction.PayStatus = types.SubscriptionPayStatusNoNeed
// err = dao.DBClient.GlobalTransactionHandler(func(tx *gorm.DB) error {
// return tx.Create(&subTransaction).Error
// })
// if err != nil {
// return fmt.Errorf("failed to create subscription transaction: %w", err)
// }
//} else {
// subscription.Status = types.SubscriptionStatusDebt
// err = dao.DBClient.GlobalTransactionHandler(func(tx *gorm.DB) error {
// return tx.Model(&types.Subscription{}).Where(&types.Subscription{ID: subscription.ID}).Update("status", types.SubscriptionStatusDebt).Error
// })
// if err != nil {
// return fmt.Errorf("failed to update subscription status: %w", err)
// }
//}
return nil
}
//TODO card binding payment
if subscription.CardID != nil {
paymentReq := services.PaymentRequest{
RequestID: uuid.NewString(),
UserUID: subTransaction.UserUID,
Amount: subTransaction.Amount,
Currency: dao.PaymentCurrency,
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
ClientIP: dao.ClientIP,
DeviceTokenID: dao.DeviceTokenID,
}
err = SubscriptionPayForBindCard(paymentReq, &helper.SubscriptionOperatorReq{
AuthBase: helper.AuthBase{Auth: &helper.Auth{UserUID: subTransaction.UserUID}},
CardID: subscription.CardID,
PayMethod: "CARD",
}, &subTransaction)
if err == nil {
if err = sendUserSubPayEmailWith(subscription.UserUID); err != nil {
logrus.Errorf("Failed to send subscription success email: %v", err)
}
return nil
}
logrus.Errorf("Failed to pay for bind card: %v, subscription: %v", err, subscription)
}
// if card payment fails, deduct payment by balance
if err = SubscriptionPayByBalance(&helper.SubscriptionOperatorReq{
AuthBase: helper.AuthBase{Auth: &helper.Auth{UserUID: subTransaction.UserUID}},
}, &subTransaction); err == nil {
return nil
}
logrus.Errorf("Failed to pay by balance: %v, subscription: %v", err, subscription)
//TODO Send a subscription failure notification
if err := p.sendRenewalFailureNotification(subscription, subTransaction); err != nil {
logrus.Errorf("Failed to send renewal failure notification for subscription %s: %v",
subscription.ID, err)
}
err = dao.DBClient.GlobalTransactionHandler(func(tx *gorm.DB) error {
subTransaction.Status = types.SubscriptionTransactionStatusFailed
subTransaction.PayStatus = types.SubscriptionPayStatusFailed
subscription.Status = types.SubscriptionStatusDebt
dErr := tx.Create(&subTransaction).Error
if dErr != nil {
return fmt.Errorf("failed to create subscription transaction: %w", dErr)
}
return tx.Model(&types.Subscription{}).Where(&types.Subscription{ID: subscription.ID}).Update("status", types.SubscriptionStatusDebt).Error
})
if err != nil {
return fmt.Errorf("failed to update subscription status: %w", err)
}
return nil
}
func HasGithubOauthProvider(db *gorm.DB, userUID uuid.UUID) (bool, error) {
var provider types.OauthProvider
err := db.
Where(`"userUid" = ? AND "providerType" = ?`, userUID, types.OauthProviderTypeGithub).
Limit(1).
Find(&provider).Error
if err != nil {
return false, err
}
return provider.UID != uuid.Nil, nil
}
func GetGithubOauthProviderID(db *gorm.DB, userUID uuid.UUID) (string, error) {
var provider types.OauthProvider
err := db.
Where(`"userUid" = ? AND "providerType" = ?`, userUID, types.OauthProviderTypeGithub).
Limit(1).
Find(&provider).Error
if err != nil {
return "", err
}
return provider.ProviderID, nil
}
func InitSubscriptionProcessorTables(db *gorm.DB) error {
// 创建处理器锁表
err := db.Exec(`
CREATE TABLE IF NOT EXISTS subscription_processor_locks (
id TEXT PRIMARY KEY,
lock_until TIMESTAMP WITH TIME ZONE NOT NULL
)
`).Error
return err
}
// sendRenewalFailureNotification 发送续费失败通知
func (p *SubscriptionProcessor) sendRenewalFailureNotification(subscription *types.Subscription, transaction types.SubscriptionTransaction) error {
//logrus.Infof("Sending renewal failure notification to user %s for subscription %s. Plan: %s, transaction ID: %s",
// subscription.UserUID, subscription.ID, subscription.PlanName, transaction.ID)
//
//// TODO: implement the actual notification logic
//if err := SendUserPayEmail(subscription.UserUID, utils.EnvSubFailedEmailTmpl); err != nil {
// logrus.Errorf("Failed to send subscription success email: %v", err)
//}
return nil
}
func (p *SubscriptionProcessor) processSubscriptions() {
logrus.Info("Processing subscriptions")
// 获取分布式锁
lockID := "subscription_processor"
acquired, err := p.acquireProcessingLock(lockID)
if err != nil {
logrus.Errorf("Failed to acquire processing lock: %v", err)
return
}
if !acquired {
logrus.Info("Another instance is currently processing subscriptions")
return
}
defer func() {
if err := p.releaseProcessingLock(lockID); err != nil {
logrus.Errorf("Failed to release processing lock: %v", err)
}
}()
if err = p.processExpiredSubscriptions(); err != nil {
logrus.Errorf("Failed to process expired subscriptions: %v", err)
}
}
//func (p *SubscriptionProcessor) ProcessKYCStatus() {
// logrus.Info("Processing KYC")
//
// // 获取分布式锁
// lockID := "kyc_processor"
// acquired, err := p.acquireProcessingLock(lockID)
// if err != nil {
// logrus.Errorf("Failed to acquire processing lock: %v", err)
// return
// }
//
// if !acquired {
// logrus.Info("Another instance is currently processing KYC")
// return
// }
//
// defer func() {
// if err := p.releaseProcessingLock(lockID); err != nil {
// logrus.Errorf("Failed to release processing lock: %v", err)
// }
// }()
//
// if err = p.processKYCStatus(); err != nil {
// logrus.Errorf("Failed to process KYC: %v", err)
// }
//}
//
//func (p *SubscriptionProcessor) processKYCStatus() error {
// logrus.Info("Processing KYC status")
//
// var users []types.UserKYC
// err := p.db.Transaction(func(tx *gorm.DB) error {
// return tx.Where("status = ?", types.UserKYCStatusPending).Find(&users).Error
// })
// if err != nil {
// return fmt.Errorf("failed to query pending KYC: %w", err)
// }
//
// logrus.Infof("Found %d pending KYC", len(users))
//
// for _, user := range users {
// // If KYC is not processed within 30 days, the status is set to failed
// if !user.CreatedAt.Add(30 * 24 * time.Hour).Before(time.Now()) {
// dErr := p.db.Transaction(func(tx *gorm.DB) error {
// return tx.Model(&types.UserKYC{}).Where("user_uid = ?", user.UserUID).Update("status", types.UserKYCStatusFailed).Error
// })
// if dErr != nil {
// logrus.Errorf("Failed to update KYC status: %v", dErr)
// }
// continue
// }
//
// // If github is bound, set the KYC status to completed
// err = p.db.Transaction(func(tx *gorm.DB) error {
// bindCount := int64(0)
// dErr := tx.Model(&types.OauthProvider{}).Where(`"userUid" = ? AND "providerType" = ?`, user.UserUID, types.OauthProviderTypeGithub).Count(&bindCount).Error
// if dErr != nil {
// return fmt.Errorf("failed to check github oauth provider: %w", dErr)
// }
// if bindCount == 0 {
// return nil
// }
// return tx.Model(&types.UserKYC{}).Where("user_uid = ?", user.UserUID).Update("status", types.UserKYCStatusCompleted).Error
// })
// if err != nil {
// logrus.Errorf("Failed to update KYC status: %v", err)
// }
// }
// return nil
//}
func (p *SubscriptionProcessor) ProcessKYCCredits() {
logrus.Info("Processing KYC credits")
// 获取分布式锁
lockID := "kyc_credits_processor"
acquired, err := p.acquireProcessingLock(lockID)
if err != nil {
logrus.Errorf("Failed to acquire processing lock: %v", err)
return
}
if !acquired {
logrus.Info("Another instance is currently processing KYC credits")
return
}
defer func() {
if err := p.releaseProcessingLock(lockID); err != nil {
logrus.Errorf("Failed to release processing lock: %v", err)
}
}()
if err = p.processKYCCredits(); err != nil {
logrus.Errorf("Failed to process KYC credits: %v", err)
}
}
func (p *SubscriptionProcessor) processKYCCredits() error {
logrus.Info("Processing KYC credits")
var users []types.UserKYC
err := p.db.Transaction(func(tx *gorm.DB) error {
return tx.Where("next_at < ? AND (status = ? OR status = ?)", time.Now().UTC().Add(10*time.Minute), types.UserKYCStatusPending, types.UserKYCStatusCompleted).Find(&users).Error
})
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to query completed KYC: %w", err)
}
logrus.Infof("Found %d completed KYC", len(users))
if len(users) == 0 {
return nil
}
freePlan, err := dao.DBClient.GetSubscriptionPlan(types.FreeSubscriptionPlanName)
if err != nil {
return fmt.Errorf("failed to get subscription plan: %w", err)
}
for _, user := range users {
err = p.db.Transaction(func(tx *gorm.DB) error {
// If the status is Pending, check whether KYC has been completed
if user.Status == types.UserKYCStatusPending {
userInfo := &types.UserInfo{}
dErr := dao.DBClient.GetGlobalDB().Model(&types.UserInfo{}).Where(`"userUid" = ?`, user.UserUID).Find(userInfo).Error
if dErr != nil {
return fmt.Errorf("failed to get user info: %w", dErr)
}
status := types.UserKYCStatusCompleted
if userInfo.Config == nil {
status = types.UserKYCStatusFailed
} else {
if userInfo.Config.Github.CreatedAt == "" {
status = types.UserKYCStatusFailed
} else {
createAt, dErr := time.Parse(time.RFC3339, userInfo.Config.Github.CreatedAt)
if dErr != nil {
return fmt.Errorf("failed to parse github user created time: %w", dErr)
}
if createAt.AddDate(0, 0, 180).After(time.Now()) {
status = types.UserKYCStatusFailed
}
}
}
// 判断创建时间是否为180天以前
if status == types.UserKYCStatusFailed {
return tx.Model(&types.UserKYC{}).Where("user_uid = ?", user.UserUID).Update("status", types.UserKYCStatusFailed).Error
} else {
return tx.Model(&types.UserKYC{}).Where("user_uid = ?", user.UserUID).Update("status", types.UserKYCStatusCompleted).Error
}
}
// Obtain the integral records that are in the active state within normal time. If yes, change the status to failed and create a new one
dErr := tx.Model(&types.Credits{}).Where("user_uid = ? AND from_id = ? AND from_type = ? AND status = ? AND expire_at > ?", user.UserUID, freePlan.ID, types.CreditsFromTypeSubscription, types.CreditsStatusActive, time.Now().UTC()).Update("status", types.CreditsStatusExpired).Error
if dErr != nil && dErr != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to check credits: %w", dErr)
}
now := time.Now().UTC()
credits := &types.Credits{
ID: uuid.New(),
UserUID: user.UserUID,
Amount: freePlan.GiftAmount,
UsedAmount: 0,
FromID: freePlan.ID.String(),
FromType: types.CreditsFromTypeSubscription,
ExpireAt: now.AddDate(0, 1, 0),
CreatedAt: now,
StartAt: now,
Status: types.CreditsStatusActive,
}
if dErr = tx.Create(credits).Error; dErr != nil {
return fmt.Errorf("failed to create credits: %w", dErr)
}
return tx.Model(&types.UserKYC{}).Where("user_uid = ?", user.UserUID).Update("next_at", now.AddDate(0, 1, 0)).Error
})
if err != nil {
logrus.Errorf("Failed to update %#+v credits: %v", user, err)
}
}
return nil
}
+114
View File
@@ -0,0 +1,114 @@
package dao
import (
"context"
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/labring/sealos/controllers/pkg/types"
"gorm.io/gorm"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
type FlushQuotaTask struct {
LocalDomain string
}
func (a *FlushQuotaTask) Execute() error {
config, err := rest.InClusterConfig()
if err != nil {
return fmt.Errorf("get in cluster config failed: %v", err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return fmt.Errorf("new client set failed: %v", err)
}
err = DBClient.GetGlobalDB().Transaction(func(tx *gorm.DB) error {
var tasks []types.AccountRegionUserTask
err := tx.Where("start_at < ? AND region_domain = ? AND type = ? AND status = ?", time.Now().UTC(), a.LocalDomain, types.AccountRegionUserTaskTypeFlushQuota, types.AccountRegionUserTaskStatusPending).Find(&tasks).Error
if err != nil {
return err
}
fmt.Printf("FlushQuotaTask found %d tasks\n", len(tasks))
for _, task := range tasks {
crName, err := DBClient.GetUserCrName(types.UserQueryOpts{UID: task.UserUID})
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("get user cr name failed: %v", err)
}
if crName != "" {
nsList, err := getOwnNsList(clientset, crName)
if err != nil {
return fmt.Errorf("get own namespace list failed: %v", err)
}
userSub, err := DBClient.GetSubscription(&types.UserQueryOpts{UID: task.UserUID})
if err != nil {
return fmt.Errorf("get user subscription failed: %v", err)
}
for _, ns := range nsList {
quota := getDefaultResourceQuota(ns, "quota-"+ns, SubPlanResourceQuota[userSub.PlanName])
err = Retry(10, time.Second, func() error {
_, err := clientset.CoreV1().ResourceQuotas(ns).Update(context.Background(), quota, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("failed to update resource quota for %s: %w", ns, err)
}
return nil
})
if err != nil {
return fmt.Errorf("update resource quota failed: %v", err)
}
}
}
err = tx.Model(&task).Where(&types.AccountRegionUserTask{RegionDomain: a.LocalDomain, Type: types.AccountRegionUserTaskTypeFlushQuota, UserUID: task.UserUID, Status: types.AccountRegionUserTaskStatusPending}).Update("status", types.AccountRegionUserTaskStatusCompleted).Error
if err != nil {
return fmt.Errorf("update task status failed: %v", err)
}
}
return nil
})
fmt.Printf("FlushQuotaTask executed: %v\n", err)
return err
}
func Retry(attempts int, sleep time.Duration, f func() error) error {
var err error
for i := 0; i < attempts; i++ {
err = f()
if err == nil {
return nil
}
time.Sleep(sleep)
}
return err
}
// getOwnNsWith *kubernetes.Clientset
func getOwnNsList(clientset *kubernetes.Clientset, user string) ([]string, error) {
if user == "" {
return nil, fmt.Errorf("user is empty")
}
nsList, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{LabelSelector: fmt.Sprintf("%s=%s", "user.sealos.io/owner", user)})
if err != nil {
return nil, fmt.Errorf("list namespace failed: %w", err)
}
nsListStr := make([]string, len(nsList.Items))
for i := range nsList.Items {
nsListStr[i] = nsList.Items[i].Name
}
return nsListStr, nil
}
func getDefaultResourceQuota(ns, name string, hard corev1.ResourceList) *corev1.ResourceQuota {
return &corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
Spec: corev1.ResourceQuotaSpec{
Hard: hard,
},
}
}
+204 -8
View File
@@ -6,6 +6,31 @@ import (
"os"
"time"
accountv1 "github.com/labring/sealos/controllers/account/api/v1"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
v1 "github.com/labring/sealos/controllers/pkg/notification/api/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/labring/sealos/controllers/pkg/types"
"github.com/sirupsen/logrus"
"github.com/labring/sealos/controllers/pkg/database"
"github.com/labring/sealos/controllers/pkg/utils"
"github.com/labring/sealos/controllers/pkg/resources"
corev1 "k8s.io/api/core/v1"
defaultAlipayClient "github.com/alipay/global-open-sdk-go/com/alipay/api"
services "github.com/labring/sealos/service/pkg/pay"
"github.com/labring/sealos/controllers/pkg/utils/env"
"github.com/goccy/go-json"
@@ -28,11 +53,22 @@ type Region struct {
}
var (
DBClient Interface
JwtMgr *helper.JWTManager
Cfg *Config
BillingTask *helper.TaskQueue
Debug bool
DBClient Interface
EmailTmplMap map[string]string
SMTPConfig *utils.SMTPConfig
ClientIP string
DeviceTokenID string
PaymentService *services.AtomPaymentService
PaymentCurrency string
SubPlanResourceQuota map[string]corev1.ResourceList
JwtMgr *utils.JWTManager
Cfg *Config
BillingTask *helper.TaskQueue
FlushQuotaProcesser *FlushQuotaTask
K8sManager ctrl.Manager
SendDebtStatusEmailBody map[types.DebtStatusType]string
//Debug bool
)
func Init(ctx context.Context) error {
@@ -50,7 +86,7 @@ func Init(ctx context.Context) error {
if mongoURI == "" {
return fmt.Errorf("empty mongo uri, please check env: %s", helper.EnvMongoURI)
}
Debug = os.Getenv("DEBUG") == "true"
//Debug = os.Getenv("DEBUG") == "true"
DBClient, err = NewAccountInterface(mongoURI, globalCockroach, localCockroach)
if err != nil {
return err
@@ -58,7 +94,11 @@ func Init(ctx context.Context) error {
if _, err = DBClient.GetProperties(); err != nil {
return fmt.Errorf("get properties error: %v", err)
}
// init region env
err = database.InitRegionEnv(DBClient.GetGlobalDB(), DBClient.GetLocalRegion().Domain)
if err != nil {
return fmt.Errorf("init region env error: %v", err)
}
file := helper.ConfigPath
Cfg = &Config{} // Initialize Cfg regardless of file existence
if _, err := os.Stat(file); err == nil {
@@ -99,6 +139,162 @@ func Init(ctx context.Context) error {
if jwtSecret == "" {
return fmt.Errorf("empty jwt secret env: %s", helper.EnvJwtSecret)
}
JwtMgr = helper.NewJWTManager(os.Getenv(helper.EnvJwtSecret), time.Minute*30)
JwtMgr = utils.NewJWTManager(os.Getenv(helper.EnvJwtSecret), time.Minute*30)
gatewayURL, clientID, privateKey, publicKey := os.Getenv(helper.EnvAlipayGatewayURL), os.Getenv(helper.EnvAlipayClientID), os.Getenv(helper.EnvAlipayPrivateKey), os.Getenv(helper.EnvAlipayPublicKey)
if gatewayURL != "" && clientID != "" && privateKey != "" && publicKey != "" {
fmt.Printf("init alipay client with gatewayUrl: %s, clientID: %s\n", gatewayURL, clientID)
if Cfg.LocalRegionDomain == "" {
return fmt.Errorf("empty local region domain, please check config")
}
payNotificationURL := "https://" + "account-api." + Cfg.LocalRegionDomain
if err != nil {
return fmt.Errorf("join pay notification url error: %v", err)
}
payRedirectURL := "https://" + "account-center." + Cfg.LocalRegionDomain
fmt.Printf("init alipay client with payNotificationURL: %s , payRedirectURL: %s\n", payNotificationURL, payRedirectURL)
PaymentService = services.NewPaymentService(defaultAlipayClient.NewDefaultAlipayClient(gatewayURL, clientID, privateKey, publicKey), payNotificationURL, payRedirectURL)
ClientIP, DeviceTokenID = os.Getenv(helper.EnvClientIP), os.Getenv(helper.EnvDeviceTokenID)
if ClientIP == "" {
return fmt.Errorf("empty client ip, please check env: %s", helper.EnvClientIP)
}
}
if PaymentCurrency = os.Getenv(helper.EnvPaymentCurrency); PaymentCurrency == "" {
PaymentCurrency = "USD"
}
if os.Getenv(helper.EnvSubscriptionEnabled) == "true" {
plans, err := DBClient.GetSubscriptionPlanList()
if err != nil {
return fmt.Errorf("get subscription plan list error: %v", err)
}
SubPlanResourceQuota, err = resources.ParseResourceLimitWithSubscription(plans)
if err != nil {
return fmt.Errorf("parse resource limit with subscription error: %v", err)
}
FlushQuotaProcesser = &FlushQuotaTask{
LocalDomain: Cfg.LocalRegionDomain,
}
}
EmailTmplMap = map[string]string{
utils.EnvPaySuccessEmailTmpl: os.Getenv(utils.EnvPaySuccessEmailTmpl),
utils.EnvPayFailedEmailTmpl: os.Getenv(utils.EnvPayFailedEmailTmpl),
utils.EnvSubSuccessEmailTmpl: os.Getenv(utils.EnvSubSuccessEmailTmpl),
utils.EnvSubFailedEmailTmpl: os.Getenv(utils.EnvSubFailedEmailTmpl),
}
SMTPConfig = &utils.SMTPConfig{
ServerHost: os.Getenv(utils.EnvSMTPHost),
ServerPort: env.GetIntEnvWithDefault(utils.EnvSMTPPort, 465),
FromEmail: os.Getenv(utils.EnvSMTPFrom),
Username: env.GetEnvWithDefault(utils.EnvSMTPUser, os.Getenv(utils.EnvSMTPFrom)),
Passwd: os.Getenv(utils.EnvSMTPPassword),
EmailTitle: os.Getenv(utils.EnvSMTPTitle),
}
if SMTPConfig.ServerHost == "" || SMTPConfig.FromEmail == "" || SMTPConfig.Passwd == "" || SMTPConfig.EmailTitle == "" {
return fmt.Errorf("empty smtp config: %v", SMTPConfig)
}
setDefaultDebtPeriodWaitSecond()
SetDebtConfig()
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(corev1.AddToScheme(scheme))
utilruntime.Must(v1.AddToScheme(scheme))
K8sManager, err = ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
})
if err != nil {
return fmt.Errorf("unable to start manager: %v", err)
}
if err = SetupCache(K8sManager); err != nil {
return fmt.Errorf("setup cache error: %v", err)
}
go func() {
if err := K8sManager.Start(ctrl.SetupSignalHandler()); err != nil {
logrus.Errorf("unable to start manager: %v", err)
os.Exit(1)
}
}()
return nil
}
const UserOwnerLabel = "user.sealos.io/owner"
func SetupCache(mgr ctrl.Manager) error {
ns := &corev1.Namespace{}
nsNameFunc := func(obj client.Object) []string {
return []string{obj.(*corev1.Namespace).Name}
}
nsOwnerFunc := func(obj client.Object) []string {
return []string{obj.(*corev1.Namespace).Labels[UserOwnerLabel]}
}
for _, idx := range []struct {
obj client.Object
field string
extractValue client.IndexerFunc
}{
{ns, accountv1.Name, nsNameFunc},
{ns, accountv1.Owner, nsOwnerFunc}} {
if err := mgr.GetFieldIndexer().IndexField(context.TODO(), idx.obj, idx.field, idx.extractValue); err != nil {
return err
}
}
return nil
}
var (
TitleTemplateZHMap = map[types.DebtStatusType]string{
types.LowBalancePeriod: "余额不足",
types.CriticalBalancePeriod: "余额即将耗尽",
types.DebtPeriod: "余额耗尽",
types.DebtDeletionPeriod: "即将资源释放",
types.FinalDeletionPeriod: "彻底资源释放",
}
TitleTemplateENMap = map[types.DebtStatusType]string{
types.LowBalancePeriod: "Low Balance",
types.CriticalBalancePeriod: "Critical Balance",
types.DebtPeriod: "Debt",
types.DebtDeletionPeriod: "Imminent Resource Release",
types.FinalDeletionPeriod: "Radical resource release",
}
NoticeTemplateENMap map[types.DebtStatusType]string
NoticeTemplateZHMap map[types.DebtStatusType]string
EmailTemplateENMap map[types.DebtStatusType]string
EmailTemplateZHMap map[types.DebtStatusType]string
)
func setDefaultDebtPeriodWaitSecond() {
domain := os.Getenv("DOMAIN")
NoticeTemplateZHMap = map[types.DebtStatusType]string{
types.LowBalancePeriod: "当前工作空间所属账户余额过低,请及时充值,以免影响您的正常使用。",
types.CriticalBalancePeriod: "当前工作空间所属账户余额即将耗尽,请及时充值,以免影响您的正常使用。",
types.DebtPeriod: "当前工作空间所属账户余额已耗尽,系统将为您暂停服务,请及时充值,以免影响您的正常使用。",
types.DebtDeletionPeriod: "系统即将释放当前空间的资源,请及时充值,以免影响您的正常使用。",
types.FinalDeletionPeriod: "系统将随时彻底释放当前工作空间所属账户下的所有资源,请及时充值,以免影响您的正常使用。",
}
NoticeTemplateENMap = map[types.DebtStatusType]string{
types.LowBalancePeriod: "Your account balance is too low, please recharge in time to avoid affecting your normal use.",
types.CriticalBalancePeriod: "Your account balance is about to run out, please recharge in time to avoid affecting your normal use.",
types.DebtPeriod: "Your account balance has been exhausted, and services will be suspended for you. Please recharge in time to avoid affecting your normal use.",
types.DebtDeletionPeriod: "The system will release the resources of the current space soon. Please recharge in time to avoid affecting your normal use.",
types.FinalDeletionPeriod: "The system will completely release all resources under the current account at any time. Please recharge in time to avoid affecting your normal use.",
}
EmailTemplateZHMap, EmailTemplateENMap = make(map[types.DebtStatusType]string), make(map[types.DebtStatusType]string)
for _, i := range []types.DebtStatusType{types.LowBalancePeriod, types.CriticalBalancePeriod, types.DebtPeriod, types.DebtDeletionPeriod, types.FinalDeletionPeriod} {
EmailTemplateENMap[i] = TitleTemplateENMap[i] + "" + NoticeTemplateENMap[i] + "(" + domain + ")"
EmailTemplateZHMap[i] = TitleTemplateZHMap[i] + "" + NoticeTemplateZHMap[i] + "(" + domain + ")"
}
}
func SetDebtConfig() {
SendDebtStatusEmailBody = make(map[types.DebtStatusType]string)
for _, status := range []types.DebtStatusType{types.LowBalancePeriod, types.CriticalBalancePeriod, types.DebtPeriod, types.DebtDeletionPeriod, types.FinalDeletionPeriod} {
email := os.Getenv(string(status) + "EmailBody")
if email == "" {
email = EmailTemplateZHMap[status] + "\n" + EmailTemplateENMap[status]
} else {
logrus.Info("email body is not empty, use env: ", email, " for status: ", status)
}
SendDebtStatusEmailBody[status] = email
}
}
+352 -8
View File
@@ -9,8 +9,6 @@ import (
"github.com/sirupsen/logrus"
accountv1 "github.com/labring/sealos/controllers/account/api/v1"
"gorm.io/gorm"
gonanoid "github.com/matoous/go-nanoid/v2"
@@ -34,11 +32,13 @@ import (
)
type Interface interface {
GetGlobalDB() *gorm.DB
GetBillingHistoryNamespaceList(req *helper.NamespaceBillingHistoryReq) ([][]string, error)
GetAccountWithWorkspace(workspace string) (*types.Account, error)
GetProperties() ([]common.PropertyQuery, error)
GetCosts(req helper.ConsumptionRecordReq) (common.TimeCostsMap, error)
GetAppCosts(req *helper.AppCostsReq) (*common.AppCosts, error)
GetAppResourceCosts(req *helper.AppCostsReq) (*helper.AppResourceCostsResponse, error)
ChargeBilling(req *helper.AdminChargeBillingReq) error
GetAppCostTimeRange(req helper.GetCostAppListReq) (helper.TimeRange, error)
GetCostOverview(req helper.GetCostAppListReq) (helper.CostOverviewResp, error)
@@ -57,6 +57,8 @@ type Interface interface {
SetStatusInvoice(req *helper.SetInvoiceStatusReq) error
GetWorkspaceName(namespaces []string) ([][]string, error)
SetPaymentInvoice(req *helper.SetPaymentInvoiceReq) error
CreatePaymentOrder(order *types.PaymentOrder) error
SetPaymentOrderStatusWithTradeNo(status types.PaymentOrderStatus, orderID string) error
Transfer(req *helper.TransferAmountReq) error
GetTransfer(ops *types.GetTransfersReq) (*types.GetTransfersResp, error)
GetUserID(ops types.UserQueryOpts) (string, error)
@@ -73,6 +75,23 @@ type Interface interface {
ArchiveHourlyBilling(hourStart, hourEnd time.Time) error
ActiveBilling(req resources.ActiveBilling) error
GetCockroach() *cockroach.Cockroach
SetCardInfo(info *types.CardInfo) (uuid.UUID, error)
GetCardInfo(cardID, userUID uuid.UUID) (*types.CardInfo, error)
GetAllCardInfo(ops *types.UserQueryOpts) ([]types.CardInfo, error)
PaymentWithFunc(payment *types.Payment, preDo, postDo func(tx *gorm.DB) error) error
NewCardPaymentHandler(paymentRequestID string, card types.CardInfo) (uuid.UUID, error)
NewCardSubscriptionPaymentHandler(paymentRequestID string, card types.CardInfo) (uuid.UUID, error)
NewCardSubscriptionPaymentFailureHandler(paymentRequestID string) (uuid.UUID, error)
NewCardPaymentFailureHandler(paymentRequestID string) (uuid.UUID, error)
GetSubscription(ops *types.UserQueryOpts) (*types.Subscription, error)
GetSubscriptionPlanList() ([]types.SubscriptionPlan, error)
GetLastSubscriptionTransaction(userUID uuid.UUID) (*types.SubscriptionTransaction, error)
GetCardList(ops *types.UserQueryOpts) ([]types.CardInfo, error)
DeleteCardInfo(id uuid.UUID, userUID uuid.UUID) error
SetDefaultCard(cardID uuid.UUID, userUID uuid.UUID) error
GetBalanceWithCredits(ops *types.UserQueryOpts) (*types.BalanceWithCredits, error)
GlobalTransactionHandler(funcs ...func(tx *gorm.DB) error) error
GetSubscriptionPlan(planName string) (*types.SubscriptionPlan, error)
}
type Account struct {
@@ -90,13 +109,22 @@ type MongoDB struct {
}
type Cockroach struct {
ck *cockroach.Cockroach
ck *cockroach.Cockroach
subscriptionPlanList []types.SubscriptionPlan
}
func (g *Cockroach) GetCockroach() *cockroach.Cockroach {
return g.ck
}
func (g *Cockroach) GetGlobalDB() *gorm.DB {
return g.ck.GetGlobalDB()
}
func (g *Cockroach) GlobalTransactionHandler(funcs ...func(tx *gorm.DB) error) error {
return g.ck.GlobalTransactionHandler(funcs...)
}
func (g *Cockroach) GetAccount(ops types.UserQueryOpts) (*types.Account, error) {
account, err := g.ck.GetAccount(&ops)
if err != nil {
@@ -155,10 +183,208 @@ func (g *Cockroach) GetPayment(ops *types.UserQueryOpts, req *helper.GetPaymentR
}, req.Invoiced)
}
func (g *Cockroach) CreatePayment(req *types.Payment) error {
return g.ck.Payment(req)
}
func (g *Cockroach) SetPaymentInvoice(req *helper.SetPaymentInvoiceReq) error {
return g.ck.SetPaymentInvoice(&types.UserQueryOpts{Owner: req.Auth.Owner}, req.PaymentIDList)
}
func (g *Cockroach) CreatePaymentOrder(order *types.PaymentOrder) error {
return g.ck.CreatePaymentOrder(order)
}
func (g *Cockroach) SetPaymentOrderStatusWithTradeNo(status types.PaymentOrderStatus, orderID string) error {
return g.ck.SetPaymentOrderStatusWithTradeNo(status, orderID)
}
func (g *Cockroach) PaymentWithFunc(payment *types.Payment, preDo, postDo func(tx *gorm.DB) error) error {
return g.ck.PaymentWithFunc(payment, preDo, postDo)
}
func (g *Cockroach) SetCardInfo(info *types.CardInfo) (uuid.UUID, error) {
return g.ck.SetCardInfo(info)
}
func (g *Cockroach) GetCardInfo(cardID, userUID uuid.UUID) (*types.CardInfo, error) {
return g.ck.GetCardInfo(cardID, userUID)
}
func (g *Cockroach) GetAllCardInfo(ops *types.UserQueryOpts) ([]types.CardInfo, error) {
return g.ck.GetAllCardInfo(ops)
}
func (g *Cockroach) GetSubscription(ops *types.UserQueryOpts) (*types.Subscription, error) {
return g.ck.GetSubscription(ops)
}
func (g *Cockroach) GetSubscriptionPlanList() ([]types.SubscriptionPlan, error) {
var err error
if len(g.subscriptionPlanList) == 0 {
g.subscriptionPlanList, err = g.ck.GetSubscriptionPlanList()
}
return g.subscriptionPlanList, err
}
func (g *Cockroach) GetLastSubscriptionTransaction(userUID uuid.UUID) (*types.SubscriptionTransaction, error) {
return GetLastSubscriptionTransaction(g.ck.GetGlobalDB(), userUID)
}
func GetLastSubscriptionTransaction(db *gorm.DB, userUID uuid.UUID) (*types.SubscriptionTransaction, error) {
transaction := &types.SubscriptionTransaction{}
err := db.Where("user_uid = ?", userUID).Order("created_at desc").First(transaction).Error
if err != nil {
return nil, err
}
return transaction, nil
}
func (g *Cockroach) NewCardPaymentHandler(paymentRequestID string, card types.CardInfo) (uuid.UUID, error) {
order, err := g.ck.GetPaymentOrderWithTradeNo(paymentRequestID)
if err != nil {
return uuid.Nil, fmt.Errorf("failed to get payment order with trade no: %v", err)
}
if order.Status != types.PaymentOrderStatusPending {
//fmt.Printf("payment order status is not pending: %v\n", order)
return uuid.Nil, ErrPaymentOrderAlreadyHandle
//return fmt.Errorf("payment order status is not pending: %v", order)
}
if card.ID == uuid.Nil {
card.ID = uuid.New()
}
card.UserUID = order.UserUID
order.PaymentRaw.ChargeSource = types.ChargeSourceNewCard
// TODO
err = g.ck.PaymentWithFunc(&types.Payment{
ID: order.ID,
PaymentRaw: order.PaymentRaw,
}, func(tx *gorm.DB) error {
if card.CardToken != "" {
card.ID, err = cockroach.SetCardInfo(tx, &card)
if err != nil {
return fmt.Errorf("failed to set card info: %v", err)
}
order.PaymentRaw.CardUID = &card.ID
}
return nil
}, func(tx *gorm.DB) error {
return cockroach.SetPaymentOrderStatusWithTradeNo(tx, types.PaymentOrderStatusSuccess, order.TradeNO)
})
return order.UserUID, err
}
func (g *Cockroach) NewCardPaymentFailureHandler(paymentRequestID string) (uuid.UUID, error) {
order, err := g.ck.GetPaymentOrderWithTradeNo(paymentRequestID)
if err != nil {
return uuid.Nil, fmt.Errorf("failed to get payment order with trade no: %v", err)
}
if order.Status == types.PaymentOrderStatusFailed {
return uuid.Nil, nil
}
if order.Status != types.PaymentOrderStatusPending {
fmt.Printf("payment order status is not pending: %v\n", order)
return uuid.Nil, nil
}
return order.UserUID, g.ck.SetPaymentOrderStatusWithTradeNo(types.PaymentOrderStatusFailed, order.TradeNO)
}
var ErrPaymentOrderAlreadyHandle = fmt.Errorf("payment order already handle")
func (g *Cockroach) NewCardSubscriptionPaymentHandler(paymentRequestID string, card types.CardInfo) (uuid.UUID, error) {
if paymentRequestID == "" {
return uuid.Nil, fmt.Errorf("payment request id is empty")
}
var userUID uuid.UUID
err := g.ck.GlobalTransactionHandler(func(tx *gorm.DB) error {
order, err := g.ck.GetPaymentOrderWithTradeNo(paymentRequestID)
if err != nil {
return fmt.Errorf("failed to get payment order with trade no: %v", err)
}
userUID = order.UserUID
if order.Status != types.PaymentOrderStatusPending {
return ErrPaymentOrderAlreadyHandle
}
if card.CardToken != "" {
card.UserUID = order.UserUID
card.ID, err = cockroach.SetCardInfo(tx, &card)
if err != nil {
return fmt.Errorf("failed to set card info: %v", err)
}
}
order.PaymentRaw.CardUID = &card.ID
order.PaymentRaw.ChargeSource = types.ChargeSourceNewCard
// TODO List
// 1. set payment order status with tradeNo
// 2. save success payment
// 3. set transaction pay status to paid
// 4. save card info
err = cockroach.SetPaymentOrderStatusWithTradeNo(tx, types.PaymentOrderStatusSuccess, order.TradeNO)
if err != nil {
return fmt.Errorf("failed to set payment order status: %v", err)
}
if err = tx.Model(&types.Payment{}).Create(&types.Payment{
ID: order.ID,
PaymentRaw: order.PaymentRaw,
}).Error; err != nil {
return fmt.Errorf("failed to save payment: %v", err)
}
if err = tx.Model(&types.SubscriptionTransaction{}).Where(&types.SubscriptionTransaction{PayID: order.ID}).Update("pay_status", types.SubscriptionPayStatusPaid).Error; err != nil {
return fmt.Errorf("failed to update subscription transaction pay status: %v", err)
}
if err = tx.Model(&types.Subscription{}).Where(&types.Subscription{UserUID: order.UserUID}).Update("card_id", card.ID).Error; err != nil {
return fmt.Errorf("failed to update subscription card id: %v", err)
}
return nil
})
return userUID, err
}
func (g *Cockroach) NewCardSubscriptionPaymentFailureHandler(paymentRequestID string) (uuid.UUID, error) {
if paymentRequestID == "" {
return uuid.Nil, fmt.Errorf("payment request id is empty")
}
var userUID uuid.UUID
err := g.ck.GlobalTransactionHandler(func(tx *gorm.DB) error {
order, err := g.ck.GetPaymentOrderWithTradeNo(paymentRequestID)
if err != nil {
return fmt.Errorf("failed to get payment order with trade no: %v", err)
}
userUID = order.UserUID
if order.Status != types.PaymentOrderStatusPending {
return nil
}
// 1. set payment order status with tradeNo
// 2. set transaction pay status to failed
err = cockroach.SetPaymentOrderStatusWithTradeNo(tx, types.PaymentOrderStatusFailed, order.TradeNO)
if err != nil {
return fmt.Errorf("failed to set payment order status: %v", err)
}
if err = tx.Model(&types.SubscriptionTransaction{}).Where(&types.SubscriptionTransaction{PayID: order.ID}).Update("pay_status", types.SubscriptionPayStatusFailed).Update("status", types.SubscriptionTransactionStatusFailed).
Error; err != nil {
return fmt.Errorf("failed to update subscription transaction pay status: %v", err)
}
return nil
})
return userUID, err
}
func (g *Cockroach) GetCardList(ops *types.UserQueryOpts) ([]types.CardInfo, error) {
return g.ck.GetCardList(ops)
}
func (g *Cockroach) DeleteCardInfo(id uuid.UUID, userUID uuid.UUID) error {
return g.ck.DeleteCardInfo(id, userUID)
}
func (g *Cockroach) SetDefaultCard(cardID uuid.UUID, userUID uuid.UUID) error {
return g.ck.SetDefaultCard(cardID, userUID)
}
func (g *Cockroach) GetBalanceWithCredits(ops *types.UserQueryOpts) (*types.BalanceWithCredits, error) {
return g.ck.GetBalanceWithCredits(ops)
}
func (g *Cockroach) Transfer(req *helper.TransferAmountReq) error {
if req.TransferAll {
return g.ck.TransferAccountAll(&types.UserQueryOpts{ID: req.Auth.UserID, Owner: req.Owner}, &types.UserQueryOpts{ID: req.ToUser})
@@ -174,6 +400,10 @@ func (g *Cockroach) GetRegions() ([]types.Region, error) {
return g.ck.GetRegions()
}
func (g *Cockroach) GetSubscriptionPlan(planName string) (*types.SubscriptionPlan, error) {
return g.ck.GetSubscriptionPlan(planName)
}
func (g *Cockroach) GetLocalRegion() types.Region {
return g.ck.GetLocalRegion()
}
@@ -322,6 +552,120 @@ func (m *MongoDB) SaveBillings(billing ...*resources.Billing) error {
return err
}
// GetAppResourceCosts 获取指定时间范围内应用资源的使用情况和花费
func (m *MongoDB) GetAppResourceCosts(req *helper.AppCostsReq) (*helper.AppResourceCostsResponse, error) {
appType := strings.ToUpper(req.AppType)
result := &helper.AppResourceCostsResponse{
AppType: appType,
}
result.ResourcesByType = map[string]*helper.ResourceUsage{
appType: {
Used: make(map[uint8]int64),
UsedAmount: make(map[uint8]int64),
Count: 0,
},
}
if appType == resources.AppStore {
delete(result.ResourcesByType, appType)
}
matchConditions := bson.D{
{Key: "owner", Value: req.Owner},
{Key: "time", Value: bson.M{
"$gte": req.StartTime,
"$lte": req.EndTime,
}},
}
if req.Namespace != "" {
matchConditions = append(matchConditions, bson.E{Key: "namespace", Value: req.Namespace})
}
if strings.ToUpper(req.AppType) == resources.AppStore {
if req.AppName != "" {
matchConditions = append(matchConditions, bson.E{Key: "app_name", Value: req.AppName})
}
matchConditions = append(matchConditions, bson.E{Key: "app_type", Value: resources.AppType[resources.AppStore]})
cursor, err := m.getBillingCollection().Find(context.Background(), matchConditions)
if err != nil {
return nil, fmt.Errorf("failed to find billing collection: %v", err)
}
defer cursor.Close(context.Background())
for cursor.Next(context.Background()) {
var billing resources.Billing
if err := cursor.Decode(&billing); err != nil {
return nil, fmt.Errorf("failed to decode billing: %v", err)
}
appTypeMap := make(map[string]struct{})
for _, appCost := range billing.AppCosts {
appTypeStr := resources.AppTypeReverse[appCost.Type]
if appTypeStr == "" {
appTypeStr = "UNKNOWN"
}
if _, exists := result.ResourcesByType[appTypeStr]; !exists {
result.ResourcesByType[appTypeStr] = &helper.ResourceUsage{
Used: make(map[uint8]int64),
UsedAmount: make(map[uint8]int64),
Count: 0,
}
}
for k, v := range appCost.Used {
result.ResourcesByType[appTypeStr].Used[k] += v
result.ResourcesByType[appTypeStr].UsedAmount[k] += appCost.UsedAmount[k]
}
appTypeMap[appTypeStr] = struct{}{}
}
for _type := range appTypeMap {
result.ResourcesByType[_type].Count++
}
}
if err := cursor.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate cursor: %v", err)
}
} else {
if req.AppType != "" {
matchConditions = append(matchConditions, bson.E{Key: "app_type", Value: resources.AppType[appType]})
}
pipeline := mongo.Pipeline{
{{Key: "$match", Value: matchConditions}},
{{Key: "$unwind", Value: "$app_costs"}},
}
if req.AppName != "" {
pipeline = append(pipeline, bson.D{{Key: "$match", Value: bson.D{{Key: "app_costs.name", Value: req.AppName}}}})
}
cursor, err := m.getBillingCollection().Aggregate(context.Background(), pipeline)
if err != nil {
return nil, fmt.Errorf("failed to aggregate billing collection: %v", err)
}
defer cursor.Close(context.Background())
for cursor.Next(context.Background()) {
var resultDoc struct {
AppCosts resources.AppCost `bson:"app_costs"`
}
if err := cursor.Decode(&resultDoc); err != nil {
return nil, fmt.Errorf("failed to decode result doc: %v", err)
}
for resourceKey, usedValue := range resultDoc.AppCosts.Used {
result.ResourcesByType[appType].Used[resourceKey] += usedValue
result.ResourcesByType[appType].UsedAmount[resourceKey] += resultDoc.AppCosts.UsedAmount[resourceKey]
}
result.ResourcesByType[appType].Count++
}
if err := cursor.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate cursor: %v", err)
}
}
for _, resourceUsage := range result.ResourcesByType {
for k, v := range resourceUsage.Used {
resourceUsage.Used[k] = v / int64(resourceUsage.Count)
}
}
return result, nil
}
func (m *MongoDB) GetAppCosts(req *helper.AppCostsReq) (results *common.AppCosts, rErr error) {
if req.Page <= 0 {
req.Page = 1
@@ -720,7 +1064,7 @@ func (m *MongoDB) GetCostAppList(req helper.GetCostAppListReq) (resp helper.Cost
if strings.ToUpper(req.AppType) != resources.AppStore {
match := bson.M{
"owner": req.Owner,
"type": accountv1.Consumption,
"type": resources.Consumption,
"app_type": bson.M{"$ne": resources.AppType[resources.AppStore]},
}
if req.Namespace != "" {
@@ -1458,7 +1802,7 @@ func (m *Account) ApplyInvoice(req *helper.ApplyInvoiceReq) (invoice types.Invoi
if len(req.PaymentIDList) == 0 {
return
}
payments, err = m.ck.GetUnInvoicedPaymentListWithIds(req.PaymentIDList)
payments, err = m.ck.GetUnInvoicedPaymentListWithIDs(req.PaymentIDList)
if err != nil {
err = fmt.Errorf("failed to get payment list: %v", err)
return
@@ -1754,7 +2098,7 @@ func (m *Account) ReconcileUnsettledLLMBilling(startTime, endTime time.Time) err
// 2. update billing status
filter := bson.M{
"user_uid": userUID,
"type": accountv1.SubConsumption,
"type": resources.SubConsumption,
"status": resources.Unsettled,
"app_type": resources.AppType[resources.LLMToken],
"time": bson.M{
@@ -1864,12 +2208,12 @@ func (m *Account) ArchiveHourlyBilling(hourStart, hourEnd time.Time) error {
"namespace": result.ID.Namespace,
"owner": result.ID.Owner,
"time": hourStart,
"type": accountv1.Consumption,
"type": resources.Consumption,
}
billing := bson.M{
"order_id": gonanoid.Must(12),
"type": accountv1.Consumption,
"type": resources.Consumption,
"namespace": result.ID.Namespace,
"app_type": resources.AppType[result.ID.AppType],
"app_name": result.ID.AppName,
@@ -87,3 +87,25 @@ spec:
name: region-info
name: region-info
serviceAccountName: account-controller-manager
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: account-node-viewer
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: account-node-viewer-binding
subjects:
- kind: ServiceAccount
name: account-controller-manager
namespace: account-system
roleRef:
kind: ClusterRole
name: account-node-viewer
apiGroup: rbac.authorization.k8s.io
-2
View File
@@ -14,8 +14,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 h1:4GI5eviCwbPxDE311KryyyPUTO7IDVyHGp3Iyl+fEZY=
github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49/go.mod h1:sbm1DAsayX+XsXCOC2CFAAU9JZhX0SPKwnybDjSd0Ls=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+37
View File
@@ -11,6 +11,7 @@ const (
GetAllRegionConsumptionAmount = "/costs/all-region-consumption"
GetPropertiesUsed = "/costs/properties"
GetAPPCosts = "/costs/app"
GetAppTypeCosts = "/costs/app-type"
SetPaymentInvoice = "/payment/set-invoice"
GetUserCosts = "/costs"
SetTransfer = "/transfer"
@@ -38,14 +39,50 @@ const (
AdminChargeBilling = "/charge-billing"
AdminActiveBilling = "/active-billing"
AdminGetUserRealNameInfo = "/real-name-info"
AdminFlushSubQuota = "/flush-sub-quota"
AdminFlushDebtResourceStatus = "/flush-debt-resource-status"
)
const (
PaymentGroup = "/payment/v1alpha1"
CreatePay = "/pay"
Notify = "/notify"
SubscriptionUserInfo = "/subscription/user-info"
SubscriptionPlanList = "/subscription/plan-list"
SubscriptionLastTransaction = "/subscription/last-transaction"
SubscriptionUpgradeAmount = "/subscription/upgrade-amount"
SubscriptionFlushQuota = "/subscription/flush-quota"
SubscriptionQuotaCheck = "/subscription/quota-check"
SubscriptionNotify = "/subscription/notify"
SubscriptionPay = "/subscription/pay"
CardList = "/card/list"
CardDelete = "/card/delete"
CardSetDefault = "/card/set-default"
CreditsList = "/credits/list"
CreditsInfo = "/credits/info"
)
const PayNotificationPath = PaymentGroup + Notify
// env
const (
ConfigPath = "/config/config.json"
EnvMongoURI = "MONGO_URI"
EnvClientIP = "CLIENT_IP"
EnvDeviceTokenID = "DEVICE_TOKEN_ID"
ENVGlobalCockroach = "GLOBAL_COCKROACH_URI"
ENVLocalCockroach = "LOCAL_COCKROACH_URI"
EnvLocalRegion = "LOCAL_REGION"
EnvJwtSecret = "ACCOUNT_API_JWT_SECRET"
EnvSubscriptionEnabled = "SUBSCRIPTION_ENABLED"
)
const (
EnvAlipayGatewayURL = "ALIPAY_GATEWAY_URL"
EnvAlipayClientID = "ALIPAY_CLIENT_ID"
EnvAlipayPublicKey = "ALIPAY_PUBLIC_KEY"
EnvAlipayPrivateKey = "ALIPAY_PRIVATE_KEY"
EnvPaymentCurrency = "PAYMENT_CURRENCY"
)
-4
View File
@@ -1,5 +1 @@
package helper
import "fmt"
var ErrNullAuth = fmt.Errorf("null auth found")
-24
View File
@@ -1,24 +0,0 @@
package helper
import (
"testing"
"time"
)
func TestJWTManager_GenerateToken(t *testing.T) {
manager := &JWTManager{
secretKey: []byte("y"),
tokenDuration: 1000 * time.Second,
}
got, err := manager.GenerateToken(JwtUser{})
if err != nil {
t.Errorf("error: %v", err)
}
t.Logf("token: %v", got)
userClaims, err := manager.VerifyToken(got)
if err != nil {
t.Errorf("error: %v", err)
}
t.Logf("userClaims: %v", userClaims)
}
+144
View File
@@ -0,0 +1,144 @@
package helper
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type CreatePayReq struct {
// @Summary Authentication information
// @Description Authentication information
AuthBase `json:",inline" bson:",inline"`
// @Summary Amount
// @Description Amount
// @JSONSchema required
Amount int64 `json:"amount" bson:"amount" example:"100000000"`
// @Summary Method
// @Description Method
// @JSONSchema required
Method string `json:"method" bson:"method" example:"CARD"`
*BindCardInfo `json:",inline" bson:",inline"`
}
type BindCardInfo struct {
// @Summary CardID
// @Description CardID
CardID uuid.UUID `json:"cardID" bson:"cardID" example:"123e4567-e89b-12d3-a456-426614174000"`
// @Summary CardNo
// @Description CardNo
CardNo string `json:"cardNo" bson:"cardNo" example:"1234567890"`
// @Summary CardBrand
// @Description CardBrand
CardBrand string `json:"cardBrand" bson:"cardBrand" example:"VISA"`
}
type CreatePayResp struct {
// @Summary RedirectURL
// @Description RedirectURL
RedirectURL string `json:"redirectUrl" bson:"redirectUrl" example:"https://www.example.com"`
}
func ParseCreatePayReq(c *gin.Context) (*CreatePayReq, error) {
req := &CreatePayReq{}
if err := c.ShouldBindJSON(req); err != nil {
return nil, err
}
return req, nil
}
type CardOperationReq struct {
// @Summary Authentication information
// @Description Authentication information
AuthBase `json:",inline" bson:",inline"`
*BindCardInfo `json:",inline" bson:",inline"`
}
func ParseCardOperationReq(c *gin.Context) (*CardOperationReq, error) {
req := &CardOperationReq{}
if err := c.ShouldBindJSON(req); err != nil {
return nil, err
}
return req, nil
}
type SubscriptionOperatorReq struct {
// @Summary Authentication information
// @Description Authentication information
AuthBase `json:",inline" bson:",inline"`
// @Summary PlanName
// @Description PlanName
PlanName string `json:"planName" bson:"planName" example:"planName"`
// @Summary PlanID
// @Description PlanID
PlanID uuid.UUID `json:"planID" bson:"planID" example:"123e4567-e89b-12d3-a456-426614174000"`
// @Summary PayMethod
// @Description PayMethod
PayMethod string `json:"payMethod" bson:"payMethod" example:"CARD"`
// @Summary CardID
// @Description CardID
CardID *uuid.UUID `json:"cardID" bson:"cardID" example:"123e4567-e89b-12d3-a456-426614174000"`
// @Summary PlanType
// @Description PlanType
PlanType PlanType `json:"planType" bson:"planType" example:"upgrade;downgrade;renewal"`
}
type PlanType string
const (
Upgrade PlanType = "upgrade"
Downgrade PlanType = "downgrade"
Renewal PlanType = "renewal"
CARD string = "CARD"
)
func ParseSubscriptionOperatorReq(c *gin.Context) (*SubscriptionOperatorReq, error) {
req := &SubscriptionOperatorReq{}
if err := c.ShouldBindJSON(req); err != nil {
return nil, err
}
return req, nil
}
type SubscriptionQuotaCheckReq struct {
// @Summary Authentication information
// @Description Authentication information
AuthBase `json:",inline" bson:",inline"`
// @Summary PlanID
// @Description PlanID
PlanID uuid.UUID `json:"planID" bson:"planID" example:"123e4567-e89b-12d3-a456-426614174000"`
// @Summary PlanName
// @Description PlanName
PlanName string `json:"planName" bson:"planName" example:"planName"`
}
func ParseSubscriptionQuotaCheckReq(c *gin.Context) (*SubscriptionQuotaCheckReq, error) {
req := &SubscriptionQuotaCheckReq{}
if err := c.ShouldBindJSON(req); err != nil {
return nil, err
}
return req, nil
}
type SubscriptionQuotaCheckResp struct {
//allWorkspaceReady
AllWorkspaceReady bool `json:"allWorkspaceReady" bson:"allWorkspaceReady" example:"true"`
ReadyWorkspace []string `json:"readyWorkspace" bson:"readyWorkspace" example:"workspace1,workspace2"`
UnReadyWorkspace []string `json:"unReadyWorkspace" bson:"unReadyWorkspace" example:"workspace3,workspace4"`
}
+59
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"time"
"github.com/labring/sealos/controllers/pkg/types"
"github.com/google/uuid"
"github.com/labring/sealos/service/account/common"
@@ -106,6 +108,19 @@ type UserTimeRangeReq struct {
AuthBase `json:",inline" bson:",inline"`
}
// ResourceUsage 定义资源使用情况的结构体
type ResourceUsage struct {
Used map[uint8]int64 `json:"used"` // 资源使用量,key为资源类型
UsedAmount map[uint8]int64 `json:"used_amount"` // 资源使用花费,key为资源类型
Count int `json:"count"` // 记录数量
}
// AppResourceCostsResponse 定义返回结果的结构体
type AppResourceCostsResponse struct {
ResourcesByType map[string]*ResourceUsage `json:"resources_by_type,omitempty"`
AppType string `json:"app_type"`
}
type AppCostsReq struct {
// @Summary Order ID
// @Description Order ID
@@ -611,3 +626,47 @@ func ParseAdminChargeBillingReq(c *gin.Context) (*AdminChargeBillingReq, error)
}
return rechargeBilling, nil
}
type AdminFlushSubscriptionQuotaReq struct {
UserUID uuid.UUID `json:"userUID" bson:"userUID"`
PlanName string `json:"planName" bson:"planName"`
PlanID uuid.UUID `json:"planID" bson:"planID"`
}
func ParseAdminFlushSubscriptionQuotaReq(c *gin.Context) (*AdminFlushSubscriptionQuotaReq, error) {
flushSubscriptionQuota := &AdminFlushSubscriptionQuotaReq{}
if err := c.ShouldBindJSON(flushSubscriptionQuota); err != nil {
return nil, fmt.Errorf("bind json error: %v", err)
}
if flushSubscriptionQuota.UserUID == uuid.Nil {
return nil, fmt.Errorf("userUID cannot be empty")
}
if flushSubscriptionQuota.PlanID == uuid.Nil {
return nil, fmt.Errorf("planID cannot be empty")
}
if flushSubscriptionQuota.PlanName == "" {
return nil, fmt.Errorf("planName cannot be empty")
}
return flushSubscriptionQuota, nil
}
type AdminFlushDebtResourceStatusReq struct {
UserUID uuid.UUID `json:"userUID" bson:"userUID"`
LastDebtStatus types.DebtStatusType `json:"lastDebtStatus" bson:"lastDebtStatus"`
CurrentDebtStatus types.DebtStatusType `json:"currentDebtStatus" bson:"currentDebtStatus"`
IsBasicUser bool `json:"isBasicUser" bson:"isBasicUser"`
}
func ParseAdminFlushDebtResourceStatusReq(c *gin.Context) (*AdminFlushDebtResourceStatusReq, error) {
flushDebtResourceStatus := &AdminFlushDebtResourceStatusReq{}
if err := c.ShouldBindJSON(flushDebtResourceStatus); err != nil {
return nil, fmt.Errorf("bind json error: %v", err)
}
if flushDebtResourceStatus.UserUID == uuid.Nil {
return nil, fmt.Errorf("userUID cannot be empty")
}
if flushDebtResourceStatus.CurrentDebtStatus == "" {
return nil, fmt.Errorf("currentDebtStatus cannot be empty")
}
return flushDebtResourceStatus, nil
}
+32 -2
View File
@@ -49,6 +49,7 @@ func RegisterPayRouter() {
POST(helper.GetProperties, api.GetProperties).
POST(helper.GetUserCosts, api.GetCosts).
POST(helper.GetAPPCosts, api.GetAPPCosts).
POST(helper.GetAppTypeCosts, api.GetAppTypeCosts).
POST(helper.GetAccount, api.GetAccount).
POST(helper.GetPayment, api.GetPayment).
POST(helper.GetRechargeAmount, api.GetRechargeAmount).
@@ -73,10 +74,39 @@ func RegisterPayRouter() {
POST(helper.UserUsage, api.UserUsage).
POST(helper.GetRechargeDiscount, api.GetRechargeDiscount).
POST(helper.GetUserRealNameInfo, api.GetUserRealNameInfo)
router.Group(helper.AdminGroup).
adminGroup := router.Group(helper.AdminGroup).
GET(helper.AdminGetAccountWithWorkspace, api.AdminGetAccountWithWorkspaceID).
GET(helper.AdminGetUserRealNameInfo, api.AdminGetUserRealNameInfo).
POST(helper.AdminChargeBilling, api.AdminChargeBilling)
POST(helper.AdminChargeBilling, api.AdminChargeBilling).
POST(helper.AdminFlushDebtResourceStatus, api.AdminFlushDebtResourceStatus)
paymentGroup := router.Group(helper.PaymentGroup).
POST(helper.CreatePay, api.CreateCardPay).
POST(helper.Notify, api.NewPayNotifyHandler).
POST(helper.CardList, api.ListCard).
POST(helper.CardDelete, api.DeleteCard).
POST(helper.CardSetDefault, api.SetDefaultCard).
POST(helper.CreditsInfo, api.GetCreditsInfo)
if os.Getenv(helper.EnvSubscriptionEnabled) == "true" {
paymentGroup.POST(helper.SubscriptionUserInfo, api.GetSubscriptionUserInfo).
POST(helper.SubscriptionPlanList, api.GetSubscriptionPlanList).
POST(helper.SubscriptionLastTransaction, api.GetLastSubscriptionTransaction).
POST(helper.SubscriptionUpgradeAmount, api.GetSubscriptionUpgradeAmount).
POST(helper.SubscriptionFlushQuota, api.FlushSubscriptionQuota).
POST(helper.SubscriptionQuotaCheck, api.CheckSubscriptionQuota).
POST(helper.SubscriptionPay, api.CreateSubscriptionPay).
POST(helper.SubscriptionNotify, api.NewSubscriptionPayNotifyHandler)
adminGroup.POST(helper.AdminFlushSubQuota, api.AdminFlushSubscriptionQuota)
processor := api.NewSubscriptionProcessor(dao.DBClient.GetGlobalDB())
err := api.InitSubscriptionProcessorTables(dao.DBClient.GetGlobalDB())
if err != nil {
log.Fatalf("Error initializing subscription processor tables: %v", err)
}
go processor.StartProcessing(ctx)
go processor.StartKYCProcessing(ctx)
go processor.StartFlushQuotaProcessing(ctx)
}
//POST(helper.AdminActiveBilling, api.AdminActiveBilling)
docs.SwaggerInfo.Host = env.GetEnvWithDefault("SWAGGER_HOST", "localhost:2333")
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
+2
View File
@@ -45,3 +45,5 @@ require (
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/ugorji/go => github.com/ugorji/go v1.2.12
+33 -24
View File
@@ -5,28 +5,36 @@ go 1.22
require (
github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6
github.com/alibabacloud-go/tea v1.2.1
github.com/labring/sealos/controllers/pkg v0.0.0-00010101000000-000000000000
github.com/labring/sealos/controllers/account v0.0.0-20250314064841-918ddc274406
github.com/labring/sealos/controllers/pkg v0.0.0-20240715064441-d1193f70675b
github.com/larksuite/oapi-sdk-go/v3 v3.2.9
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2
k8s.io/client-go v0.30.2
k8s.io/client-go v12.0.0+incompatible
)
require (
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5 // indirect
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea-utils v1.3.1 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.3 // indirect
github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
github.com/aliyun/credentials-go v1.1.2 // indirect
github.com/clbanning/mxj/v2 v2.5.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.4 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/credentials-go v1.3.1 // indirect
github.com/astaxie/beego v1.12.3 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/clbanning/mxj/v2 v2.5.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
@@ -35,40 +43,41 @@ require (
github.com/imdario/mergo v0.3.16 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jackc/pgx/v5 v5.5.4 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/larksuite/oapi-sdk-go/v3 v3.2.9 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matoous/go-nanoid/v2 v2.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tjfoc/gmsm v1.3.2 // indirect
github.com/volcengine/volc-sdk-golang v1.0.159 // indirect
go.mongodb.org/mongo-driver v1.12.1 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/postgres v1.5.4 // indirect
gorm.io/gorm v1.25.5 // indirect
k8s.io/api v0.30.2 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -1,6 +1,8 @@
module github.com/labring/sealos/service
go 1.22
go 1.22.5
toolchain go1.23.1
replace (
github.com/labring/sealos/service => ../service
@@ -11,6 +13,8 @@ replace (
)
require (
github.com/alipay/global-open-sdk-go v1.2.11
github.com/google/uuid v1.6.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.28.4
k8s.io/apimachinery v0.28.4
@@ -33,7 +37,6 @@ require (
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/josharian/intern v1.0.0 // indirect
+4 -2
View File
@@ -2,6 +2,8 @@ github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJ
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/alipay/global-open-sdk-go v1.2.11 h1:G+k5J9qgtmZKz5YTS4TL0hdZUjKWJP7Ujb2t9QI68r8=
github.com/alipay/global-open-sdk-go v1.2.11/go.mod h1:nzqEW6Mu1w55kTRyrsdDIsrtmXWxdH/7xppK6He/HNo=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -35,8 +37,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+264 -485
View File
File diff suppressed because it is too large Load Diff
+215
View File
@@ -0,0 +1,215 @@
package services
import (
"fmt"
"strconv"
"time"
"github.com/alipay/global-open-sdk-go/com/alipay/api/tools"
"github.com/labring/sealos/controllers/pkg/types"
defaultAlipayClient "github.com/alipay/global-open-sdk-go/com/alipay/api"
"github.com/alipay/global-open-sdk-go/com/alipay/api/model"
"github.com/alipay/global-open-sdk-go/com/alipay/api/request/pay"
responsePay "github.com/alipay/global-open-sdk-go/com/alipay/api/response/pay"
"github.com/google/uuid"
)
type AtomPaymentService struct {
Client *defaultAlipayClient.DefaultAlipayClient
PaymentRedirectURL string
PaymentNotifyURL string
}
func NewPaymentService(client *defaultAlipayClient.DefaultAlipayClient, notifyURL, redirectURL string) *AtomPaymentService {
return &AtomPaymentService{
Client: client,
PaymentNotifyURL: notifyURL,
PaymentRedirectURL: redirectURL,
}
}
// PaymentRequest 支付请求参数
type PaymentRequest struct {
RequestID string
UserUID uuid.UUID
Amount int64
Currency string
UserAgent string
ClientIP string
DeviceTokenID string
}
func (s *AtomPaymentService) CheckRspSign(requestURI, httpMethod, clientID, respTime, responseBody, signature string) (bool, error) {
return tools.CheckSignature(requestURI, httpMethod, clientID, respTime, responseBody, signature, s.Client.AlipayPublicKey)
}
func (s *AtomPaymentService) GenSign(httpMethod string, path string, reqTime string, reqBody string) (string, error) {
return tools.GenSign(httpMethod, path, s.Client.ClientId, reqTime, reqBody, s.Client.MerchantPrivateKey)
}
func (s *AtomPaymentService) CreateNewPayment(req PaymentRequest) (*responsePay.AlipayPayResponse, error) {
return s.createPaymentWithMethod(req, s.createNewCardPaymentMethod(), s.PaymentRedirectURL+"/?paymentType=ACCOUNT_RECHARGE", s.PaymentNotifyURL+"/payment/v1alpha1/notify")
}
func (s *AtomPaymentService) CreatePaymentWithCard(req PaymentRequest, card *types.CardInfo) (*responsePay.AlipayPayResponse, error) {
return s.createPaymentWithMethod(req, s.createCardPaymentMethod(card), s.PaymentRedirectURL+"/?paymentType=ACCOUNT_RECHARGE", s.PaymentNotifyURL+"/payment/v1alpha1/notify")
}
func (s *AtomPaymentService) CreateNewSubscriptionPay(req PaymentRequest) (*responsePay.AlipayPayResponse, error) {
return s.createPaymentWithMethod(req, s.createNewCardPaymentMethod(), s.PaymentRedirectURL+"/?paymentType=SUBSCRIPTION", s.PaymentNotifyURL+"/payment/v1alpha1/subscription/notify")
}
func (s *AtomPaymentService) CreateSubscriptionPayWithCard(req PaymentRequest, card *types.CardInfo) (*responsePay.AlipayPayResponse, error) {
return s.createPaymentWithMethod(req, s.CreateSubscriptionPay(card), s.PaymentRedirectURL+"/?paymentType=SUBSCRIPTION", s.PaymentNotifyURL+"/payment/v1alpha1/subscription/notify")
}
func (s *AtomPaymentService) GetPayment(paymentRequestID, paymentID string) (*responsePay.AlipayPayQueryResponse, error) {
queryRequest := pay.AlipayPayQueryRequest{}
queryRequest.PaymentRequestId = paymentRequestID
queryRequest.PaymentId = paymentID
request := queryRequest.NewRequest()
execute, err := s.Client.Execute(request)
if err != nil {
return nil, fmt.Errorf("failed to execute query request: %v", err)
}
response := execute.(*responsePay.AlipayPayQueryResponse)
return response, nil
}
func (s *AtomPaymentService) createPaymentWithMethod(req PaymentRequest, method *model.PaymentMethod, redirectPath, notifyPath string) (*responsePay.AlipayPayResponse, error) {
payRequest, request := pay.NewAlipayPayRequest()
request.PaymentRequestId = req.RequestID
// 设置订单信息
order := s.createOrder(req)
request.Order = order
request.PaymentAmount = model.NewAmount(strconv.FormatInt(req.Amount/10000, 10), req.Currency)
// 设置支付方法
request.PaymentMethod = method
request.PaymentExpiryTime = time.Now().Add(10 * time.Minute).Format(time.RFC3339)
// 设置环境信息
request.Env = &model.Env{
TerminalType: "WEB",
UserAgent: req.UserAgent,
ClientIp: req.ClientIP,
DeviceTokenId: req.DeviceTokenID,
}
// TODO 设置其他必要信息
request.PaymentRedirectUrl = redirectPath
request.PaymentNotifyUrl = notifyPath
request.PaymentFactor = &model.PaymentFactor{
IsAuthorization: true,
CaptureMode: "AUTOMATIC",
}
request.SettlementStrategy = &model.SettlementStrategy{
SettlementCurrency: req.Currency,
}
request.ProductCode = model.CASHIER_PAYMENT
// 执行支付请求
execute, err := s.Client.Execute(payRequest)
if err != nil {
return nil, err
}
return execute.(*responsePay.AlipayPayResponse), nil
}
func (s *AtomPaymentService) createOrder(req PaymentRequest) *model.Order {
amount := strconv.FormatInt(req.Amount/10000, 10)
return &model.Order{
OrderDescription: "payment",
ReferenceOrderId: uuid.NewString(),
OrderAmount: model.NewAmount(amount, req.Currency),
Buyer: &model.Buyer{
ReferenceBuyerId: req.UserUID.String(),
},
Goods: []model.Goods{
{
ReferenceGoodsId: uuid.NewString(),
GoodsName: fmt.Sprintf("account %b balance", req.Amount/10000),
GoodsQuantity: amount,
DeliveryMethodType: "DIGITAL",
GoodsUnitAmount: &model.Amount{
Currency: req.Currency,
Value: "1",
},
GoodsCategory: "Hosting",
},
},
}
}
func (s *AtomPaymentService) createNewCardPaymentMethod() *model.PaymentMethod {
return &model.PaymentMethod{
PaymentMethodType: "CARD",
PaymentMethodMetaData: map[string]any{
"is3DSAuthentication": false,
"tokenize": true,
"billingAddress": map[string]string{
"region": "CN",
},
},
}
}
func (s *AtomPaymentService) createCardPaymentMethod(card *types.CardInfo) *model.PaymentMethod {
return &model.PaymentMethod{
PaymentMethodType: "CARD",
PaymentMethodMetaData: map[string]any{
"is3DSAuthentication": false,
"billingAddress": map[string]string{
"region": "CN",
},
"isCardOnFile": true,
},
PaymentMethodId: card.CardToken,
}
}
func (s *AtomPaymentService) CreateSubscriptionPay(card *types.CardInfo) *model.PaymentMethod {
return &model.PaymentMethod{
PaymentMethodType: "CARD",
PaymentMethodMetaData: map[string]any{
"isCardOnFile": true,
"recurringType": "SCHEDULED",
"networkTransactionId": card.NetworkTransactionID,
"enableAuthenticationUpgrade": false,
"is3DSAuthentication": false,
},
PaymentMethodId: card.CardToken,
}
}
// QueryPayment 查询支付状态
func (s *AtomPaymentService) QueryPayment(paymentRequestID, paymentID string) (*responsePay.AlipayPayQueryResponse, error) {
if paymentRequestID == "" && paymentID == "" {
return nil, fmt.Errorf("paymentRequestID and paymentID cannot both be empty")
}
resp, err := s.GetPayment(paymentRequestID, paymentID)
if err != nil {
return nil, fmt.Errorf("failed to query payment: %v", err)
}
return resp, nil
}
// check resultStatus = "S" And ResultCode = "SUCCESS"
func (s *AtomPaymentService) CancelPayment(paymentRequestID, paymentID string) (*responsePay.AlipayPayCancelResponse, error) {
if paymentRequestID == "" && paymentID == "" {
return nil, fmt.Errorf("paymentRequestID and paymentID cannot both be empty")
}
request, cancelRequest := pay.NewAlipayPayCancelRequest()
cancelRequest.PaymentRequestId = paymentRequestID
cancelRequest.PaymentId = paymentID
execute, err := s.Client.Execute(request)
if err != nil {
return nil, fmt.Errorf("failed to execute cancel request: %v", err)
}
response := execute.(*responsePay.AlipayPayCancelResponse)
return response, nil
}