mirror of
https://github.com/labring/sealos.git
synced 2026-08-28 17:22:42 +08:00
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:
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
//}
|
||||
}
|
||||
|
||||
@@ -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(®ionENV).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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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{
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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, Subscription,UpgradeSubscription...
|
||||
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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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(¤tLock).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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
@@ -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(¤tCredits).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,
|
||||
})
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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, ¬ification); 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)
|
||||
}
|
||||
@@ -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, ¬ification); 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
package helper
|
||||
|
||||
import "fmt"
|
||||
|
||||
var ErrNullAuth = fmt.Errorf("null auth found")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+813
-38
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user