mirror of
https://github.com/labring/sealos.git
synced 2026-08-29 01:39:49 +08:00
Unified account (#4576)
* accountv2 interface implementation: use the cockroach distributed database as a multi-region unified account data storage source to replace the old CRD interaction format; temporarily turn off account functions such as transfers and activities. * create account v2 with cockroachdb * add get account api service;
This commit is contained in:
@@ -46,6 +46,7 @@ env:
|
||||
# Common versions
|
||||
GO_VERSION: "1.20"
|
||||
DEFAULT_OWNER: "labring"
|
||||
CRYPTOKEY: ${{ secrets.CONTROLLER_BUILD_CRYPTOKEY }}
|
||||
|
||||
jobs:
|
||||
resolve-modules:
|
||||
|
||||
@@ -63,8 +63,16 @@ type DebtSpec struct {
|
||||
|
||||
// DebtStatus defines the observed state of Debt
|
||||
type DebtStatus struct {
|
||||
LastUpdateTimestamp int64 `json:"lastUpdateTimestamp,omitempty"`
|
||||
AccountDebtStatus DebtStatusType `json:"status,omitempty"`
|
||||
LastUpdateTimestamp int64 `json:"lastUpdateTimestamp,omitempty"`
|
||||
DebtStatusRecords []DebtStatusRecord `json:"debtStatusRecords,omitempty"`
|
||||
AccountDebtStatus DebtStatusType `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// DebtStatusRecord defines the observed state of Debt
|
||||
type DebtStatusRecord struct {
|
||||
LastStatus DebtStatusType `json:"lastDebtStatus,omitempty"`
|
||||
CurrentStatus DebtStatusType `json:"currentStatus,omitempty"`
|
||||
UpdateTime int64 `json:"updateTime,omitempty"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
|
||||
@@ -22,8 +22,11 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database/cockroach"
|
||||
|
||||
account2 "github.com/labring/sealos/controllers/pkg/account"
|
||||
"github.com/labring/sealos/controllers/pkg/code"
|
||||
pkgtype "github.com/labring/sealos/controllers/pkg/types"
|
||||
userv1 "github.com/labring/sealos/controllers/user/api/v1"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
@@ -55,7 +58,8 @@ var logger = logf.Log.WithName("debt-resource")
|
||||
// +kubebuilder:object:generate=false
|
||||
|
||||
type DebtValidate struct {
|
||||
Client client.Client
|
||||
Client client.Client
|
||||
AccountV2 *cockroach.Cockroach
|
||||
}
|
||||
|
||||
var kubeSystemGroup string
|
||||
@@ -63,7 +67,7 @@ var kubeSystemGroup string
|
||||
func init() {
|
||||
kubeSystemGroup = fmt.Sprintf("%s:%s", saPrefix, kubeSystemNamespace)
|
||||
}
|
||||
func (d DebtValidate) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
func (d *DebtValidate) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
logger.V(1).Info("checking user", "userInfo", req.UserInfo, "req.Namespace", req.Namespace, "req.Name", req.Name, "req.gvrk", getGVRK(req), "req.Operation", req.Operation)
|
||||
// skip delete request (删除quota资源除外)
|
||||
if req.Operation == admissionv1.Delete && !strings.Contains(getGVRK(req), "quotas") {
|
||||
@@ -99,7 +103,7 @@ func (d DebtValidate) Handle(ctx context.Context, req admission.Request) admissi
|
||||
if req.Kind.Kind == "Payment" && req.Operation == admissionv1.Update {
|
||||
return admission.Denied(fmt.Sprintf("ns %s request %s %s permission denied", req.Namespace, req.Kind.Kind, req.Operation))
|
||||
}
|
||||
return checkOption(ctx, logger, d.Client, req.Namespace)
|
||||
return d.checkOption(ctx, logger, d.Client, req.Namespace)
|
||||
}
|
||||
logger.V(1).Info("pass ", "req.Namespace", req.Namespace)
|
||||
return admission.ValidationResponse(true, "")
|
||||
@@ -127,11 +131,10 @@ func isWhiteList(req admission.Request) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkOption(ctx context.Context, logger logr.Logger, c client.Client, nsName string) admission.Response {
|
||||
func (d *DebtValidate) checkOption(ctx context.Context, logger logr.Logger, c client.Client, nsName string) admission.Response {
|
||||
if nsName == "" {
|
||||
return admission.Allowed("")
|
||||
}
|
||||
@@ -150,11 +153,13 @@ func checkOption(ctx context.Context, logger logr.Logger, c client.Client, nsNam
|
||||
logger.Error(err, "get account error", "user", user)
|
||||
return admission.ValidationResponse(true, err.Error())
|
||||
}
|
||||
|
||||
for _, account := range accountList.Items {
|
||||
if account.Status.Balance < account.Status.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)))
|
||||
}
|
||||
account, err := d.AccountV2.GetAccount(&pkgtype.UserQueryOpts{Owner: user})
|
||||
if err != nil {
|
||||
logger.Error(err, "get account error", "user", user)
|
||||
return admission.ValidationResponse(false, 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)))
|
||||
}
|
||||
return admission.Allowed(fmt.Sprintf("pass user %s , namespace %s", user, ns.Name))
|
||||
}
|
||||
@@ -163,8 +168,8 @@ func isDefaultQuotaName(name string) bool {
|
||||
return strings.HasPrefix(name, "quota-") || name == debtLimit0QuotaName
|
||||
}
|
||||
|
||||
func GetAccountDebtBalance(account Account) float64 {
|
||||
return account2.GetCurrencyBalance(account.Status.Balance - account.Status.DeductionBalance)
|
||||
func GetAccountDebtBalance(account pkgtype.Account) float64 {
|
||||
return account2.GetCurrencyBalance(account.Balance - account.DeductionBalance)
|
||||
}
|
||||
|
||||
const debtLimit0QuotaName = "debt-limit0"
|
||||
|
||||
@@ -19,23 +19,23 @@ package controllers
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
|
||||
accountv1 "github.com/labring/sealos/controllers/account/api/v1"
|
||||
"github.com/labring/sealos/controllers/pkg/crypto"
|
||||
"github.com/labring/sealos/controllers/pkg/database"
|
||||
"github.com/labring/sealos/controllers/pkg/pay"
|
||||
"github.com/labring/sealos/controllers/pkg/resources"
|
||||
@@ -44,18 +44,14 @@ import (
|
||||
"github.com/labring/sealos/controllers/pkg/utils/retry"
|
||||
userv1 "github.com/labring/sealos/controllers/user/api/v1"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
cretry "k8s.io/client-go/util/retry"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
)
|
||||
|
||||
@@ -73,14 +69,14 @@ const (
|
||||
// AccountReconciler reconciles an Account object
|
||||
type AccountReconciler struct {
|
||||
client.Client
|
||||
AccountV2 database.AccountV2
|
||||
Scheme *runtime.Scheme
|
||||
Logger logr.Logger
|
||||
AccountSystemNamespace string
|
||||
DBClient database.Account
|
||||
MongoDBURI string
|
||||
Activities pkgtypes.Activities
|
||||
RechargeStep []int64
|
||||
RechargeRatio []float64
|
||||
DefaultDiscount pkgtypes.RechargeDiscount
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts,verbs=get;list;watch;create;update;patch;delete
|
||||
@@ -108,7 +104,7 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
// This is only used to monitor and initialize user resource creation data,
|
||||
// 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, r.AccountSystemNamespace, "ns-"+user.Name)
|
||||
_, err = r.syncAccount(ctx, owner, "ns-"+user.Name)
|
||||
return ctrl.Result{}, err
|
||||
} else if client.IgnoreNotFound(err) != nil {
|
||||
return ctrl.Result{}, err
|
||||
@@ -128,7 +124,7 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
account, err := r.syncAccount(ctx, getUsername(payment.Spec.UserID), r.AccountSystemNamespace, payment.Namespace)
|
||||
account, err := r.syncAccount(ctx, getUsername(payment.Spec.UserID), payment.Namespace)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("get account failed: %v", err)
|
||||
}
|
||||
@@ -148,55 +144,32 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
r.Logger.V(1).Info("query order details", "orderStatus", status, "orderAmount", orderAmount)
|
||||
switch status {
|
||||
case pay.PaymentSuccess:
|
||||
now := time.Now().UTC()
|
||||
//1¥ = 100WechatPayAmount; 1 WechatPayAmount = 10000 SealosAmount
|
||||
payAmount := orderAmount * 10000
|
||||
updateAnno, gift, err := r.getAmountWithRates(payAmount, account)
|
||||
gift, err := r.getAmountWithRates(payAmount, account)
|
||||
if err != nil {
|
||||
r.Logger.Error(err, "get gift error")
|
||||
}
|
||||
err = crypto.RechargeBalance(account.Status.EncryptBalance, gift)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("recharge encrypt balance failed: %v", err)
|
||||
}
|
||||
if err := SyncAccountStatus(ctx, r.Client, account); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update account status failed: %v", err)
|
||||
if err = r.AccountV2.Payment(&pkgtypes.Payment{
|
||||
PaymentRaw: pkgtypes.PaymentRaw{
|
||||
UserUID: account.UserUID,
|
||||
Amount: payAmount,
|
||||
Gift: gift,
|
||||
CreatedAt: payment.CreationTimestamp.Time,
|
||||
RegionUserOwner: owner,
|
||||
Method: payment.Spec.PaymentMethod,
|
||||
TradeNO: payment.Status.TradeNO,
|
||||
CodeURL: payment.Status.CodeURL,
|
||||
},
|
||||
}); err != nil {
|
||||
r.Logger.Error(err, "save payment failed", "payment", payment)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
payment.Status.Status = pay.PaymentSuccess
|
||||
if err := r.Status().Update(ctx, payment); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update payment failed: %v", err)
|
||||
}
|
||||
if len(updateAnno) > 0 {
|
||||
account.Annotations = updateAnno
|
||||
if err := r.Update(ctx, account); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update account failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
id, err := gonanoid.New(12)
|
||||
if err != nil {
|
||||
r.Logger.Error(err, "create id failed", "id", id, "payment", payment)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
err = r.DBClient.SaveBillings(&resources.Billing{
|
||||
Time: now,
|
||||
OrderID: id,
|
||||
Amount: gift,
|
||||
Namespace: payment.Namespace,
|
||||
Owner: getUsername(payment.Spec.UserID),
|
||||
Type: accountv1.Recharge,
|
||||
Payment: &resources.Payment{
|
||||
Method: payment.Spec.PaymentMethod,
|
||||
TradeNO: payment.Status.TradeNO,
|
||||
CodeURL: payment.Status.CodeURL,
|
||||
UserID: payment.Spec.UserID,
|
||||
Amount: payAmount,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
r.Logger.Error(err, "save billings failed", "id", id, "payment", payment)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
case pay.PaymentProcessing, pay.PaymentNotPaid:
|
||||
return ctrl.Result{Requeue: true, RequeueAfter: time.Second}, nil
|
||||
case pay.PaymentFailed, pay.PaymentExpired:
|
||||
@@ -211,79 +184,24 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *AccountReconciler) syncAccount(ctx context.Context, owner, accountNamespace string, userNamespace string) (*accountv1.Account, error) {
|
||||
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")
|
||||
}
|
||||
if err := r.adaptEphemeralStorageLimitRange(ctx, userNamespace); err != nil {
|
||||
r.Logger.Error(err, "adapt ephemeral storage limitRange failed")
|
||||
}
|
||||
account := accountv1.Account{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: owner,
|
||||
Namespace: accountNamespace,
|
||||
},
|
||||
if getUsername(userNamespace) != owner {
|
||||
return nil, nil
|
||||
}
|
||||
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &account, func() error {
|
||||
if account.Annotations == nil {
|
||||
account.Annotations = make(map[string]string)
|
||||
account, err := r.AccountV2.NewAccount(&pkgtypes.UserQueryOpts{Owner: owner})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to create account %v, err: %v", account, err)
|
||||
return nil, fmt.Errorf("failed to create %s account: %v", owner, err)
|
||||
}
|
||||
// If the user is not the owner, the user represents the team and does not perform subsequent account initialization operations
|
||||
if owner != getUsername(userNamespace) {
|
||||
return &account, nil
|
||||
}
|
||||
// add role get account permission
|
||||
if err := r.syncRoleAndRoleBinding(ctx, owner, userNamespace); err != nil {
|
||||
return nil, fmt.Errorf("sync role and rolebinding failed: %v", err)
|
||||
}
|
||||
err := initBalance(&account)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sync init balance failed: %v", err)
|
||||
}
|
||||
// add account balance when account is new user
|
||||
stringAmount := os.Getenv(NEWACCOUNTAMOUNTENV)
|
||||
if stringAmount == "" {
|
||||
r.Logger.V(1).Info("NEWACCOUNTAMOUNTENV is empty", "account", account)
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
if account.Annotations[AccountAnnotationNewAccount] == "false" {
|
||||
//r.Logger.V(1).Info("account is not a new user ", "account", account)
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
amount, err := crypto.DecryptInt64(stringAmount)
|
||||
if err != nil {
|
||||
r.Logger.Error(err, "decrypt amount failed", "amount", stringAmount)
|
||||
amount = DefaultInitialBalance
|
||||
}
|
||||
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &account, func() error {
|
||||
if account.Annotations == nil {
|
||||
account.Annotations = make(map[string]string)
|
||||
}
|
||||
account.Annotations[AccountAnnotationNewAccount] = "false"
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = initBalance(&account)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sync init balance failed: %v", err)
|
||||
}
|
||||
err = crypto.RechargeBalance(account.Status.EncryptBalance, amount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("recharge balance failed: %v", err)
|
||||
}
|
||||
if err := SyncAccountStatus(ctx, r.Client, &account); err != nil {
|
||||
return nil, fmt.Errorf("update account failed: %v", err)
|
||||
}
|
||||
r.Logger.Info("account created,will charge new account some money", "account", account, "stringAmount", stringAmount)
|
||||
|
||||
return &account, nil
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (r *AccountReconciler) syncResourceQuotaAndLimitRange(ctx context.Context, nsName string) error {
|
||||
@@ -319,59 +237,6 @@ func (r *AccountReconciler) adaptEphemeralStorageLimitRange(ctx context.Context,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *AccountReconciler) syncRoleAndRoleBinding(ctx context.Context, name, namespace string) error {
|
||||
role := rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "userAccountRole-" + name,
|
||||
Namespace: r.AccountSystemNamespace,
|
||||
},
|
||||
}
|
||||
err := cretry.RetryOnConflict(cretry.DefaultRetry, func() error {
|
||||
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &role, func() error {
|
||||
role.Rules = []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"account.sealos.io"},
|
||||
Resources: []string{"accounts"},
|
||||
Verbs: []string{"get", "watch", "list"},
|
||||
ResourceNames: []string{name},
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("create role failed: %v,username: %v,namespace: %v", err, name, namespace)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
roleBinding := rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "userAccountRoleBinding-" + name,
|
||||
Namespace: r.AccountSystemNamespace,
|
||||
},
|
||||
}
|
||||
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &roleBinding, func() error {
|
||||
roleBinding.RoleRef = rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: role.Name,
|
||||
}
|
||||
roleBinding.Subjects = []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("create roleBinding failed: %v,rolename: %v,username: %v,ns: %v", err, role.Name, name, namespace)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePayment delete payments that exist for more than 5 minutes
|
||||
func (r *AccountReconciler) DeletePayment(ctx context.Context) error {
|
||||
payments := &accountv1.PaymentList{}
|
||||
@@ -409,44 +274,12 @@ func (r *AccountReconciler) DeletePayment(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncAccountStatus(ctx context.Context, client client.Client, account *accountv1.Account) error {
|
||||
balance, err := crypto.DecryptInt64(*account.Status.EncryptBalance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update decrypt balance failed: %v", err)
|
||||
}
|
||||
deductionBalance, err := crypto.DecryptInt64(*account.Status.EncryptDeductionBalance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update decrypt deduction balance failed: %v", err)
|
||||
}
|
||||
account.Status.Balance = balance
|
||||
account.Status.DeductionBalance = deductionBalance
|
||||
return client.Status().Update(ctx, account)
|
||||
}
|
||||
|
||||
func initBalance(account *accountv1.Account) (err error) {
|
||||
if account.Status.EncryptBalance == nil {
|
||||
encryptBalance, err := crypto.EncryptInt64(account.Status.Balance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync encrypt balance failed: %v", err)
|
||||
}
|
||||
account.Status.EncryptBalance = encryptBalance
|
||||
}
|
||||
if account.Status.EncryptDeductionBalance == nil {
|
||||
encryptDeductionBalance, err := crypto.EncryptInt64(account.Status.DeductionBalance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync encrypt deduction balance failed: %v", err)
|
||||
}
|
||||
account.Status.EncryptDeductionBalance = encryptDeductionBalance
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&userv1.User{}, builder.WithPredicates(predicate.And(OnlyCreatePredicate{}))).
|
||||
For(&userv1.User{}, builder.WithPredicates(OnlyCreatePredicate{})).
|
||||
Watches(&source.Kind{Type: &accountv1.Payment{}}, &handler.EnqueueRequestForObject{}).
|
||||
WithOptions(rateOpts).
|
||||
Complete(r)
|
||||
@@ -516,52 +349,40 @@ func GetUserOwner(user *userv1.User) string {
|
||||
return own
|
||||
}
|
||||
|
||||
type NamespaceFilterPredicate struct {
|
||||
Namespace string
|
||||
predicate.Funcs
|
||||
}
|
||||
|
||||
func (p *NamespaceFilterPredicate) Create(e event.CreateEvent) bool {
|
||||
return e.Object.GetNamespace() == p.Namespace
|
||||
}
|
||||
|
||||
func (p *NamespaceFilterPredicate) Delete(e event.DeleteEvent) bool {
|
||||
return e.Object.GetNamespace() == p.Namespace
|
||||
}
|
||||
|
||||
func (p *NamespaceFilterPredicate) Update(e event.UpdateEvent) bool {
|
||||
return e.ObjectOld.GetNamespace() == p.Namespace
|
||||
}
|
||||
|
||||
func (p *NamespaceFilterPredicate) Generic(e event.GenericEvent) bool {
|
||||
return e.Object.GetNamespace() == p.Namespace
|
||||
}
|
||||
|
||||
const BaseUnit = 1_000_000
|
||||
|
||||
func (r *AccountReconciler) getAmountWithRates(amount int64, account *accountv1.Account) (anno map[string]string, amt int64, err error) {
|
||||
userActivities, err := pkgtypes.ParseUserActivities(account.Annotations)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("parse user activities failed: %w", err)
|
||||
}
|
||||
func (r *AccountReconciler) getAmountWithRates(amount int64, account *pkgtypes.Account) (amt int64, err error) {
|
||||
//userActivities, err := pkgtypes.ParseUserActivities(account.Annotations)
|
||||
//if err != nil {
|
||||
// return nil, 0, fmt.Errorf("parse user activities failed: %w", err)
|
||||
//}
|
||||
//
|
||||
//rechargeDiscount := pkgtypes.RechargeDiscount{
|
||||
// DiscountSteps: r.RechargeStep,
|
||||
// DiscountRates: r.RechargeRatio,
|
||||
//}
|
||||
//if len(userActivities) > 0 {
|
||||
// if activityType, phase, _ := pkgtypes.GetUserActivityDiscount(r.Activities, &userActivities); phase != nil {
|
||||
// if len(phase.RechargeDiscount.DiscountSteps) > 0 {
|
||||
// rechargeDiscount.DiscountSteps = phase.RechargeDiscount.DiscountSteps
|
||||
// rechargeDiscount.DiscountRates = phase.RechargeDiscount.DiscountRates
|
||||
// }
|
||||
// rechargeDiscount.SpecialDiscount = phase.RechargeDiscount.SpecialDiscount
|
||||
// rechargeDiscount = phase.RechargeDiscount
|
||||
// currentPhase := userActivities[activityType].Phases[userActivities[activityType].CurrentPhase]
|
||||
// anno = pkgtypes.SetUserPhaseRechargeTimes(account.Annotations, activityType, currentPhase.Name, currentPhase.RechargeNums+1)
|
||||
// }
|
||||
//}
|
||||
//return anno, getAmountWithDiscount(amount, rechargeDiscount), nil
|
||||
|
||||
rechargeDiscount := pkgtypes.RechargeDiscount{
|
||||
DiscountSteps: r.RechargeStep,
|
||||
DiscountRates: r.RechargeRatio,
|
||||
discount, err := r.AccountV2.GetUserAccountRechargeDiscount(&pkgtypes.UserQueryOpts{UID: account.UserUID})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get user %s account recharge discount failed: %w", account.UserUID, err)
|
||||
}
|
||||
if len(userActivities) > 0 {
|
||||
if activityType, phase, _ := pkgtypes.GetUserActivityDiscount(r.Activities, &userActivities); phase != nil {
|
||||
if len(phase.RechargeDiscount.DiscountSteps) > 0 {
|
||||
rechargeDiscount.DiscountSteps = phase.RechargeDiscount.DiscountSteps
|
||||
rechargeDiscount.DiscountRates = phase.RechargeDiscount.DiscountRates
|
||||
}
|
||||
rechargeDiscount.SpecialDiscount = phase.RechargeDiscount.SpecialDiscount
|
||||
rechargeDiscount = phase.RechargeDiscount
|
||||
currentPhase := userActivities[activityType].Phases[userActivities[activityType].CurrentPhase]
|
||||
anno = pkgtypes.SetUserPhaseRechargeTimes(account.Annotations, activityType, currentPhase.Name, currentPhase.RechargeNums+1)
|
||||
}
|
||||
if discount == nil || discount.DiscountSteps == nil || discount.DiscountRates == nil {
|
||||
return getAmountWithDiscount(amount, r.DefaultDiscount), nil
|
||||
}
|
||||
return anno, getAmountWithDiscount(amount, rechargeDiscount), nil
|
||||
return getAmountWithDiscount(amount, *discount), nil
|
||||
}
|
||||
|
||||
func getAmountWithDiscount(amount int64, discount pkgtypes.RechargeDiscount) int64 {
|
||||
@@ -576,5 +397,5 @@ func getAmountWithDiscount(amount int64, discount pkgtypes.RechargeDiscount) int
|
||||
break
|
||||
}
|
||||
}
|
||||
return int64(math.Ceil(float64(amount)*r/100)) + amount
|
||||
return int64(math.Ceil(float64(amount) * r / 100))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
// Copyright © 2024 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 controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database/mongo"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database/cockroach"
|
||||
"github.com/labring/sealos/controllers/pkg/utils/logger"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
|
||||
accountv1 "github.com/labring/sealos/controllers/account/api/v1"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
)
|
||||
|
||||
var (
|
||||
testV2GlobalDBURI = ""
|
||||
testV2LocalDBURI = ""
|
||||
)
|
||||
|
||||
type testConfig struct {
|
||||
RegionID string
|
||||
V1dbURI string
|
||||
V2GlobalDBURI string
|
||||
V2LocalDBURI string
|
||||
Kubeconfig string
|
||||
}
|
||||
|
||||
var RegionsConfig = []testConfig{}
|
||||
|
||||
func mkdirs(dirs ...string) error {
|
||||
for _, dir := range dirs {
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAccount_V1ToV2(t *testing.T) {
|
||||
for i := range RegionsConfig {
|
||||
os.Unsetenv("LOCAL_REGION")
|
||||
err := os.Setenv("LOCAL_REGION", RegionsConfig[i].RegionID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set env: %v", err)
|
||||
}
|
||||
err = mkdirs(filepath.Join("transferv1tov2", "null_user_record", RegionsConfig[i].RegionID),
|
||||
filepath.Join("transferv1tov2", "transfer_account_v1", RegionsConfig[i].RegionID),
|
||||
filepath.Join("transferv1tov2", "transfer_account_v1_exist", RegionsConfig[i].RegionID))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create dir: %v", err)
|
||||
}
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(accountv1.AddToScheme(scheme))
|
||||
config, err := clientcmd.BuildConfigFromFlags("", RegionsConfig[i].Kubeconfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Error building kubeconfig: %v\n", err)
|
||||
}
|
||||
clt, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to new client: %v", err)
|
||||
}
|
||||
accounts := &accountv1.AccountList{}
|
||||
err = clt.List(context.Background(), accounts)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get account: %v", err)
|
||||
}
|
||||
t.Logf("success get region account len: %d, startTime: %s", len(accounts.Items), time.Now().UTC().Format("2006-01-02 15:04:05"))
|
||||
accountItf, err := database.NewAccountV2(RegionsConfig[i].V2GlobalDBURI, RegionsConfig[i].V2LocalDBURI)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to new account : %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := accountItf.Close(); err != nil {
|
||||
t.Errorf("failed close connection: %v", err)
|
||||
}
|
||||
}()
|
||||
wg, ctx := errgroup.WithContext(context.Background())
|
||||
wg.SetLimit(100)
|
||||
for _, a := range accounts.Items {
|
||||
account := a
|
||||
wg.Go(func() error {
|
||||
createAccount := &types.Account{
|
||||
EncryptBalance: *account.Status.EncryptBalance,
|
||||
EncryptDeductionBalance: *account.Status.EncryptDeductionBalance,
|
||||
Balance: account.Status.Balance,
|
||||
DeductionBalance: account.Status.DeductionBalance,
|
||||
CreatedAt: account.CreationTimestamp.Time,
|
||||
CreateRegionID: RegionsConfig[i].RegionID,
|
||||
ActivityBonus: account.Status.ActivityBonus,
|
||||
}
|
||||
_, err := accountItf.TransferAccountV1(account.Name, createAccount)
|
||||
if err != nil {
|
||||
logger.Error("failed to create account %s: %v", account.Name, err)
|
||||
if err = accountItf.CreateErrorAccountCreate(createAccount, account.Name, err.Error()); err != nil {
|
||||
logger.Error("failed to create err msg %s: %v", account.Name, err)
|
||||
ctx.Done()
|
||||
}
|
||||
return err
|
||||
}
|
||||
//t.Logf("success create account %s", account.Name)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := wg.Wait(); err != nil {
|
||||
t.Fatalf("failed to create account: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertPayment_V1ToV2(t *testing.T) {
|
||||
for i := range RegionsConfig {
|
||||
os.Unsetenv("LOCAL_REGION")
|
||||
err := os.Setenv("LOCAL_REGION", RegionsConfig[i].RegionID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to set env: %v", err)
|
||||
}
|
||||
accountV2, err := database.NewAccountV2(RegionsConfig[i].V2GlobalDBURI, RegionsConfig[i].V2LocalDBURI)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to new account : %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := accountV2.Close(); err != nil {
|
||||
t.Errorf("failed close connection: %v", err)
|
||||
}
|
||||
}()
|
||||
accountV1, err := mongo.NewMongoInterface(context.Background(), RegionsConfig[i].V1dbURI)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to new account : %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := accountV1.Disconnect(context.Background()); err != nil {
|
||||
t.Errorf("failed close connection: %v", err)
|
||||
}
|
||||
}()
|
||||
billings, err := accountV1.GetAllPayment()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get billing: %v", err)
|
||||
}
|
||||
eg := errgroup.Group{}
|
||||
eg.SetLimit(100)
|
||||
|
||||
for i := range billings {
|
||||
bill := billings[i]
|
||||
eg.Go(func() error {
|
||||
payment := types.Payment{
|
||||
ID: bill.OrderID,
|
||||
PaymentRaw: types.PaymentRaw{
|
||||
CreatedAt: bill.Time,
|
||||
Amount: bill.Payment.Amount,
|
||||
RegionUserOwner: bill.Owner,
|
||||
Method: bill.Payment.Method,
|
||||
CodeURL: bill.Payment.CodeURL,
|
||||
TradeNO: bill.Payment.TradeNO,
|
||||
Gift: bill.Amount - bill.Payment.Amount,
|
||||
},
|
||||
}
|
||||
err = accountV2.SavePayment(&payment)
|
||||
if err != nil {
|
||||
//logger.Error("failed to create payment %s: %v", payment.CrName, err)
|
||||
if err2 := accountV2.CreateErrorPaymentCreate(payment, err.Error()); err2 != nil {
|
||||
logger.Error("failed to create err msg %s: %v", payment.ID, err2)
|
||||
}
|
||||
}
|
||||
//logger.Info("success get payment %s", bill)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
t.Fatalf("failed to wait create payment: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Logf("success convert payment")
|
||||
}
|
||||
|
||||
//func TestAccountConvert(t *testing.T) {
|
||||
// scheme := runtime.NewScheme()
|
||||
// utilruntime.Must(accountv1.AddToScheme(scheme))
|
||||
// config, err := clientcmd.BuildConfigFromFlags("", testConfig)
|
||||
// if err != nil {
|
||||
// t.Fatalf("Error building kubeconfig: %v\n", err)
|
||||
// }
|
||||
// clt, err := client.New(config, client.Options{Scheme: scheme})
|
||||
// if err != nil {
|
||||
// t.Fatalf("failed to new client: %v", err)
|
||||
// }
|
||||
// accounts := &accountv1.AccountList{}
|
||||
// err = clt.List(context.Background(), accounts)
|
||||
// if err != nil {
|
||||
// t.Fatalf("failed to get account: %v", err)
|
||||
// }
|
||||
// for _, a := range accounts.Items {
|
||||
// account := a
|
||||
// if account.Status.EncryptBalance != nil || account.Status.EncryptDeductionBalance != nil {
|
||||
// if account.Status.EncryptBalance != nil && account.Status.EncryptDeductionBalance != nil {
|
||||
// continue
|
||||
// }
|
||||
// t.Logf("account %s already convert", account.Name)
|
||||
// continue
|
||||
// }
|
||||
// accountCopy := &accountv1.Account{}
|
||||
// err = json.Unmarshal([]byte(account.Annotations[v1.LastAppliedConfigAnnotation]), accountCopy)
|
||||
// if err != nil {
|
||||
// t.Fatalf("failed to unmarshal account %s: %v", account.Name, err)
|
||||
// }
|
||||
// account.Status = accountCopy.Status
|
||||
// err = clt.Status().Update(context.Background(), &account)
|
||||
// if err != nil {
|
||||
// t.Fatalf("failed to update account %s: %v", account.Name, err)
|
||||
// }
|
||||
// t.Logf("success updata status account %s: %+v", account.Name, account.Status)
|
||||
// }
|
||||
//}
|
||||
|
||||
func TestAccountV2_CreateAccount(t *testing.T) {
|
||||
account, err := database.NewAccountV2(testV2GlobalDBURI, testV2LocalDBURI)
|
||||
if err != nil {
|
||||
t.Errorf("failed to new account : %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := account.Close(); err != nil {
|
||||
t.Errorf("failed close connection: %v", err)
|
||||
}
|
||||
}()
|
||||
aa, err := account.NewAccount(&types.UserQueryOpts{Owner: "eoxwhh80"})
|
||||
if err != nil {
|
||||
t.Errorf("failed to create account: %v", err)
|
||||
}
|
||||
t.Logf("success create account: %v", aa)
|
||||
|
||||
aa, err = account.NewAccount(&types.UserQueryOpts{Owner: "1ycieb5b"})
|
||||
if err != nil {
|
||||
t.Errorf("failed to create account: %v", err)
|
||||
}
|
||||
t.Logf("success create account: %v", aa)
|
||||
}
|
||||
|
||||
func TestAccountV2_GetAccount(t *testing.T) {
|
||||
account, err := database.NewAccountV2(testV2GlobalDBURI, testV2LocalDBURI)
|
||||
if err != nil {
|
||||
t.Errorf("failed to new account : %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := account.Close(); err != nil {
|
||||
t.Errorf("failed close connection: %v", err)
|
||||
}
|
||||
}()
|
||||
aa, err := account.GetAccount(&types.UserQueryOpts{Owner: "zzxns1si"})
|
||||
if err != nil {
|
||||
t.Errorf("failed to get account: %v", err)
|
||||
}
|
||||
t.Logf("success create account: %+v", aa)
|
||||
|
||||
//aa, err = account.GetAccount(&types.UserQueryOpts{Owner: "1ycieb5b"})
|
||||
//if err != nil {
|
||||
// t.Errorf("failed to get account: %v", err)
|
||||
//}
|
||||
//t.Logf("success create account: %+v", aa)
|
||||
}
|
||||
|
||||
func TestAccountV2_GetUser(t *testing.T) {
|
||||
account, err := database.NewAccountV2(testV2GlobalDBURI, testV2LocalDBURI)
|
||||
if err != nil {
|
||||
t.Errorf("failed to new account : %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := account.Close(); err != nil {
|
||||
t.Errorf("failed close connection: %v", err)
|
||||
}
|
||||
}()
|
||||
user, err := account.GetUser(&types.UserQueryOpts{Owner: "eoxwhh80"})
|
||||
if err != nil {
|
||||
t.Errorf("failed to get user: %v", err)
|
||||
}
|
||||
t.Logf("success get user: %v", user)
|
||||
}
|
||||
|
||||
func TestAccountV2_TransferAccount(t *testing.T) {
|
||||
account, err := database.NewAccountV2(testV2GlobalDBURI, testV2LocalDBURI)
|
||||
if err != nil {
|
||||
t.Errorf("failed to new account : %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := account.Close(); err != nil {
|
||||
t.Errorf("failed close connection: %v", err)
|
||||
}
|
||||
}()
|
||||
err = account.TransferAccount(&types.UserQueryOpts{Owner: "eoxwhh80"}, &types.UserQueryOpts{Owner: "1ycieb5b"}, 85*cockroach.BaseUnit)
|
||||
if err != nil {
|
||||
t.Errorf("failed to transfer account: %v", err)
|
||||
}
|
||||
aa, err := account.GetAccount(&types.UserQueryOpts{Owner: "eoxwhh80"})
|
||||
if err != nil {
|
||||
t.Errorf("failed to get eoxwhh80 account: %v", err)
|
||||
}
|
||||
t.Logf("success create eoxwhh80 account: %+v", aa)
|
||||
|
||||
aa, err = account.GetAccount(&types.UserQueryOpts{Owner: "1ycieb5b"})
|
||||
if err != nil {
|
||||
t.Errorf("failed to get 1ycieb5b account: %v", err)
|
||||
}
|
||||
t.Logf("success create 1ycieb5b account: %+v", aa)
|
||||
}
|
||||
|
||||
func TestAccountV2_AddBalance(t *testing.T) {
|
||||
account, err := database.NewAccountV2(testV2GlobalDBURI, testV2LocalDBURI)
|
||||
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)
|
||||
}
|
||||
}()
|
||||
//err = account.AddBalance(&types.UserQueryOpts{Owner: "zzxns1si"}, 100*cockroach.BaseUnit)
|
||||
//if err != nil {
|
||||
// t.Errorf("failed to add balance: %v", err)
|
||||
//}
|
||||
//err = account.AddDeductionBalance(&types.UserQueryOpts{Owner: "zzxns1si"}, 999*cockroach.BaseUnit)
|
||||
//if err != nil {
|
||||
// t.Fatalf("failed to add deduction balance: %v", err)
|
||||
//}
|
||||
aa, err := account.GetAccount(&types.UserQueryOpts{Owner: "zzxns1si"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get account: %v", err)
|
||||
}
|
||||
t.Logf("success create DeductionBalance: %+v", aa.DeductionBalance/cockroach.BaseUnit)
|
||||
|
||||
t.Logf("success create Balance: %+v", aa.Balance/cockroach.BaseUnit)
|
||||
t.Logf("success create accountbalance: %+v", (aa.Balance-aa.DeductionBalance)/cockroach.BaseUnit)
|
||||
}
|
||||
@@ -16,159 +16,160 @@ limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/resources"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/crypto"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
accountv1 "github.com/labring/sealos/controllers/account/api/v1"
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
)
|
||||
|
||||
type ActivityReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
Logger logr.Logger
|
||||
Activity types.Activities
|
||||
DBClient database.Account
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/finalizers,verbs=update
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
// the Payment object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile
|
||||
func (r *ActivityReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
r.Logger = log.FromContext(ctx)
|
||||
|
||||
account := &accountv1.Account{}
|
||||
if err := r.Get(ctx, req.NamespacedName, account); err != nil {
|
||||
r.Logger.Error(err, "get account failed")
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
userActivities, err := types.ParseUserActivities(account.Annotations)
|
||||
if err != nil {
|
||||
r.Logger.Error(err, "parse user activities failed")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
anno, amount, err := r.giveAmount(userActivities, account)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
if anno != nil {
|
||||
if err := r.handleBonus(account, anno, amount); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("handle bonus failed: %v", err)
|
||||
}
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *ActivityReconciler) handleBonus(account *accountv1.Account, annotations map[string]string, amount int64) error {
|
||||
if err := SyncAccountStatus(context.Background(), r.Client, account); err != nil {
|
||||
return fmt.Errorf("update account status failed: %v", err)
|
||||
}
|
||||
|
||||
account.Annotations = annotations
|
||||
if err := r.Update(context.Background(), account); err != nil {
|
||||
return fmt.Errorf("update account failed: %v", err)
|
||||
}
|
||||
id, err := gonanoid.New(12)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create id failed: %v", err)
|
||||
}
|
||||
|
||||
if err = r.DBClient.SaveBillings(&resources.Billing{
|
||||
Time: time.Now().UTC(),
|
||||
OrderID: id,
|
||||
Amount: amount,
|
||||
Namespace: GetUserNamespace(account.Name),
|
||||
Owner: getUsername(account.Name),
|
||||
Type: accountv1.ActivityGiving,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("save billing failed: %v", err)
|
||||
}
|
||||
r.Logger.Info("update account success", "account", account.Name, "bonus amount", amount, "balance", account.Status.Balance)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ActivityReconciler) giveAmount(userActivities types.UserActivities, account *accountv1.Account) (annotations map[string]string, amount int64, err error) {
|
||||
for activityType, userActivity := range userActivities {
|
||||
activity, exist := r.Activity[activityType]
|
||||
if !exist {
|
||||
r.Logger.Error(nil, "activity not exist", "activity", activity)
|
||||
continue
|
||||
}
|
||||
userPhase, exist := userActivity.Phases[userActivity.CurrentPhase]
|
||||
if !exist {
|
||||
r.Logger.Error(nil, "userPhase not exist", "activity", activityType, "phase", userActivity.CurrentPhase)
|
||||
continue
|
||||
}
|
||||
if userPhase.EndTime.IsZero() {
|
||||
continue
|
||||
}
|
||||
giveAmount := activity.Phases[userActivity.CurrentPhase].GiveAmount
|
||||
if giveAmount != 0 && userPhase.GiveAmount == 0 {
|
||||
err := crypto.RechargeBalance(account.Status.EncryptBalance, giveAmount)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("give account %s amount failed: %w", account.Name, err)
|
||||
}
|
||||
account.Status.Balance += giveAmount
|
||||
account.Status.ActivityBonus += giveAmount
|
||||
return types.SetUserPhaseGiveAmount(account.Annotations, activityType, userActivity.CurrentPhase, giveAmount), giveAmount, nil
|
||||
}
|
||||
}
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *ActivityReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error {
|
||||
const controllerName = "activity_controller"
|
||||
r.Logger = ctrl.Log.WithName(controllerName)
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&accountv1.Account{}).
|
||||
WithEventFilter(predicate.Funcs{
|
||||
CreateFunc: func(e event.CreateEvent) bool {
|
||||
return len(e.Object.GetAnnotations()) > 1
|
||||
},
|
||||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
accountOld := e.ObjectOld.(*accountv1.Account)
|
||||
accountNew := e.ObjectNew.(*accountv1.Account)
|
||||
if len(accountNew.Annotations) == 0 {
|
||||
return false
|
||||
}
|
||||
return !reflect.DeepEqual(accountOld.Annotations, accountNew.Annotations)
|
||||
},
|
||||
DeleteFunc: func(e event.DeleteEvent) bool {
|
||||
return false
|
||||
},
|
||||
}).WithOptions(rateOpts).Complete(r)
|
||||
}
|
||||
//import (
|
||||
// "context"
|
||||
// "fmt"
|
||||
// "reflect"
|
||||
// "time"
|
||||
//
|
||||
// gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
//
|
||||
// "github.com/labring/sealos/controllers/pkg/resources"
|
||||
//
|
||||
// "github.com/labring/sealos/controllers/pkg/database"
|
||||
//
|
||||
// "github.com/labring/sealos/controllers/pkg/crypto"
|
||||
//
|
||||
// "sigs.k8s.io/controller-runtime/pkg/event"
|
||||
// "sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
//
|
||||
// "sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
//
|
||||
// "github.com/go-logr/logr"
|
||||
// "k8s.io/apimachinery/pkg/runtime"
|
||||
//
|
||||
// ctrl "sigs.k8s.io/controller-runtime"
|
||||
// "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
// "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
//
|
||||
// accountv1 "github.com/labring/sealos/controllers/account/api/v1"
|
||||
// "github.com/labring/sealos/controllers/pkg/types"
|
||||
//)
|
||||
//
|
||||
//type ActivityReconciler struct {
|
||||
// client.Client
|
||||
// Scheme *runtime.Scheme
|
||||
// Logger logr.Logger
|
||||
// Activity types.Activities
|
||||
// DBClient database.Account
|
||||
//}
|
||||
//
|
||||
////+kubebuilder:rbac:groups=account.sealos.io,resources=accounts,verbs=get;list;watch;create;update;patch;delete
|
||||
////+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/status,verbs=get;update;patch
|
||||
////+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/finalizers,verbs=update
|
||||
//
|
||||
//// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
//// move the current state of the cluster closer to the desired state.
|
||||
//// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
//// the Payment object against the actual cluster state, and then
|
||||
//// perform operations to make the cluster state reflect the state specified by
|
||||
//// the user.
|
||||
////
|
||||
//// For more details, check Reconcile and its Result here:
|
||||
//// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile
|
||||
//func (r *ActivityReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
// r.Logger = log.FromContext(ctx)
|
||||
//
|
||||
// account := &accountv1.Account{}
|
||||
// if err := r.Get(ctx, req.NamespacedName, account); err != nil {
|
||||
// r.Logger.Error(err, "get account failed")
|
||||
// return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
// }
|
||||
// userActivities, err := types.ParseUserActivities(account.Annotations)
|
||||
// if err != nil {
|
||||
// r.Logger.Error(err, "parse user activities failed")
|
||||
// return ctrl.Result{}, err
|
||||
// }
|
||||
// anno, amount, err := r.giveAmount(userActivities, account)
|
||||
// if err != nil {
|
||||
// return ctrl.Result{}, err
|
||||
// }
|
||||
// if anno != nil {
|
||||
// if err := r.handleBonus(account, anno, amount); err != nil {
|
||||
// return ctrl.Result{}, fmt.Errorf("handle bonus failed: %v", err)
|
||||
// }
|
||||
// }
|
||||
// return ctrl.Result{}, nil
|
||||
//}
|
||||
//
|
||||
//func (r *ActivityReconciler) handleBonus(account *accountv1.Account, annotations map[string]string, amount int64) error {
|
||||
// if err := SyncAccountStatus(context.Background(), r.Client, account); err != nil {
|
||||
// return fmt.Errorf("update account status failed: %v", err)
|
||||
// }
|
||||
//
|
||||
// account.Annotations = annotations
|
||||
// if err := r.Update(context.Background(), account); err != nil {
|
||||
// return fmt.Errorf("update account failed: %v", err)
|
||||
// }
|
||||
// id, err := gonanoid.New(12)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("create id failed: %v", err)
|
||||
// }
|
||||
//
|
||||
// if err = r.DBClient.SaveBillings(&resources.Billing{
|
||||
// Time: time.Now().UTC(),
|
||||
// OrderID: id,
|
||||
// Amount: amount,
|
||||
// Namespace: GetUserNamespace(account.Name),
|
||||
// Owner: getUsername(account.Name),
|
||||
// Type: accountv1.ActivityGiving,
|
||||
// }); err != nil {
|
||||
// return fmt.Errorf("save billing failed: %v", err)
|
||||
// }
|
||||
// r.Logger.Info("update account success", "account", account.Name, "bonus amount", amount, "balance", account.Status.Balance)
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//func (r *ActivityReconciler) giveAmount(userActivities types.UserActivities, account *accountv1.Account) (annotations map[string]string, amount int64, err error) {
|
||||
// for activityType, userActivity := range userActivities {
|
||||
// activity, exist := r.Activity[activityType]
|
||||
// if !exist {
|
||||
// r.Logger.Error(nil, "activity not exist", "activity", activity)
|
||||
// continue
|
||||
// }
|
||||
// userPhase, exist := userActivity.Phases[userActivity.CurrentPhase]
|
||||
// if !exist {
|
||||
// r.Logger.Error(nil, "userPhase not exist", "activity", activityType, "phase", userActivity.CurrentPhase)
|
||||
// continue
|
||||
// }
|
||||
// if userPhase.EndTime.IsZero() {
|
||||
// continue
|
||||
// }
|
||||
// giveAmount := activity.Phases[userActivity.CurrentPhase].GiveAmount
|
||||
// if giveAmount != 0 && userPhase.GiveAmount == 0 {
|
||||
// err := crypto.RechargeBalance(account.Status.EncryptBalance, giveAmount)
|
||||
// if err != nil {
|
||||
// return nil, 0, fmt.Errorf("give account %s amount failed: %w", account.Name, err)
|
||||
// }
|
||||
// account.Status.Balance += giveAmount
|
||||
// account.Status.ActivityBonus += giveAmount
|
||||
// return types.SetUserPhaseGiveAmount(account.Annotations, activityType, userActivity.CurrentPhase, giveAmount), giveAmount, nil
|
||||
// }
|
||||
// }
|
||||
// return nil, 0, nil
|
||||
//}
|
||||
//
|
||||
//// SetupWithManager sets up the controller with the Manager.
|
||||
//func (r *ActivityReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error {
|
||||
// const controllerName = "activity_controller"
|
||||
// r.Logger = ctrl.Log.WithName(controllerName)
|
||||
// return ctrl.NewControllerManagedBy(mgr).
|
||||
// For(&accountv1.Account{}).
|
||||
// WithEventFilter(predicate.Funcs{
|
||||
// CreateFunc: func(e event.CreateEvent) bool {
|
||||
// return len(e.Object.GetAnnotations()) > 1
|
||||
// },
|
||||
// UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
// accountOld := e.ObjectOld.(*accountv1.Account)
|
||||
// accountNew := e.ObjectNew.(*accountv1.Account)
|
||||
// if len(accountNew.Annotations) == 0 {
|
||||
// return false
|
||||
// }
|
||||
// return !reflect.DeepEqual(accountOld.Annotations, accountNew.Annotations)
|
||||
// },
|
||||
// DeleteFunc: func(e event.DeleteEvent) bool {
|
||||
// return false
|
||||
// },
|
||||
// }).WithOptions(rateOpts).Complete(r)
|
||||
//}
|
||||
|
||||
@@ -13,62 +13,3 @@
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
)
|
||||
|
||||
func Test_parseUserActivitiesAnnotation(t *testing.T) {
|
||||
annotations := map[string]string{
|
||||
"activity.beginner-guide.launchpad.startTime": "2016-03-04T15:04:05Z",
|
||||
"activity.beginner-guide.launchpad.rechargeNums": "1",
|
||||
"activity.beginner-guide.launchpad.giveAmount": "10000",
|
||||
"activity.beginner-guide.current-phase": "launchpad",
|
||||
}
|
||||
|
||||
userActivities, err := types.ParseUserActivities(annotations)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(userActivities) != 1 {
|
||||
t.Errorf("Expected 1 activity type, got %d", len(userActivities))
|
||||
}
|
||||
|
||||
activity, exists := userActivities["beginner-guide"]
|
||||
if !exists {
|
||||
t.Errorf("Expected activity type 'beginner-guide' not found")
|
||||
}
|
||||
|
||||
if activity.CurrentPhase != "launchpad" {
|
||||
t.Errorf("Expected current phase 'launchpad', got %s", activity.CurrentPhase)
|
||||
}
|
||||
|
||||
if len(activity.Phases) != 1 {
|
||||
t.Errorf("Expected 1 phase, got %d", len(activity.Phases))
|
||||
}
|
||||
|
||||
phase, exists := activity.Phases["launchpad"]
|
||||
if !exists {
|
||||
t.Errorf("Expected phase 'launchpad' not found")
|
||||
}
|
||||
|
||||
if phase.Name != "launchpad" {
|
||||
t.Errorf("Expected phase name 'launchpad', got %s", phase.Name)
|
||||
}
|
||||
tmpTime, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
|
||||
if phase.StartTime.Equal(tmpTime) {
|
||||
t.Errorf("Expected phase start time '$time', got %s", phase.StartTime)
|
||||
}
|
||||
|
||||
if phase.RechargeNums != 1 {
|
||||
t.Errorf("Expected recharge nums 1, got %d", phase.RechargeNums)
|
||||
}
|
||||
|
||||
if phase.GiveAmount != 10000 {
|
||||
t.Errorf("Expected give amount 10000, got %d", phase.GiveAmount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,14 +22,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/resources"
|
||||
"github.com/labring/sealos/controllers/pkg/utils/env"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/crypto"
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
|
||||
v12 "github.com/labring/sealos/controllers/account/api/v1"
|
||||
"github.com/labring/sealos/controllers/pkg/resources"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
|
||||
@@ -59,9 +55,9 @@ type BillingReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
logr.Logger
|
||||
AccountSystemNamespace string
|
||||
DBClient database.Account
|
||||
Properties *resources.PropertyTypeLS
|
||||
DBClient database.Account
|
||||
AccountV2 database.AccountV2
|
||||
Properties *resources.PropertyTypeLS
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch;delete
|
||||
@@ -133,6 +129,7 @@ func (r *BillingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("recharge balance failed: %w", err)
|
||||
}
|
||||
r.Logger.V(1).Info("success recharge balance", "owner", owner, "amount", consumAmount)
|
||||
}
|
||||
return ctrl.Result{Requeue: true, RequeueAfter: time.Until(currentHourTime.Add(1*time.Hour + 10*time.Minute))}, nil
|
||||
}
|
||||
@@ -141,18 +138,8 @@ func (r *BillingReconciler) rechargeBalance(owner string, amount int64) (err err
|
||||
if amount == 0 {
|
||||
return nil
|
||||
}
|
||||
account := &v12.Account{}
|
||||
if err = r.Get(context.Background(), types.NamespacedName{Name: owner, Namespace: r.AccountSystemNamespace}, account); err != nil {
|
||||
return fmt.Errorf("get account cr failed: %w", err)
|
||||
}
|
||||
if err = initBalance(account); err != nil {
|
||||
return fmt.Errorf("failed to init balance: %v", err)
|
||||
}
|
||||
if err = crypto.RechargeBalance(account.Status.EncryptDeductionBalance, amount); err != nil {
|
||||
return fmt.Errorf("recharge balance failed: %w", err)
|
||||
}
|
||||
if err = SyncAccountStatus(context.Background(), r.Client, account); err != nil {
|
||||
return fmt.Errorf("sync account status failed: %w", err)
|
||||
if err := r.AccountV2.AddDeductionBalance(&types.UserQueryOpts{Owner: owner}, amount); err != nil {
|
||||
return fmt.Errorf("add balance failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -179,7 +166,6 @@ func (r *BillingReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controll
|
||||
if err := r.initDB(); err != nil {
|
||||
r.Logger.Error(err, "init db failed")
|
||||
}
|
||||
r.AccountSystemNamespace = env.GetEnvWithDefault(ACCOUNTNAMESPACEENV, DEFAULTACCOUNTNAMESPACE)
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&corev1.Namespace{}, builder.WithPredicates(predicate.Funcs{
|
||||
CreateFunc: func(createEvent event.CreateEvent) bool {
|
||||
|
||||
@@ -44,14 +44,15 @@ import (
|
||||
type BillingInfoQueryReconciler struct {
|
||||
client.Client
|
||||
logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
DBClient database.Account
|
||||
Scheme *runtime.Scheme
|
||||
DBClient database.Account
|
||||
//TODO init
|
||||
AccountV2 database.AccountV2
|
||||
AccountSystemNamespace string
|
||||
Properties *resources.PropertyTypeLS
|
||||
propertiesQuery []accountv1.PropertyQuery
|
||||
Activities types.Activities
|
||||
RechargeStep []int64
|
||||
RechargeRatio []float64
|
||||
DefaultDiscount types.RechargeDiscount
|
||||
QueryFuncMap map[string]func(context.Context, ctrl.Request, *accountv1.BillingInfoQuery) (string, error)
|
||||
}
|
||||
|
||||
@@ -147,37 +148,19 @@ func (r *BillingInfoQueryReconciler) AppTypeQuery(_ context.Context, _ ctrl.Requ
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (r *BillingInfoQueryReconciler) RechargeQuery(ctx context.Context, req ctrl.Request, _ *accountv1.BillingInfoQuery) (result string, err error) {
|
||||
account := &accountv1.Account{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Namespace: r.AccountSystemNamespace, Name: getUsername(req.Namespace)}, account); err != nil {
|
||||
return "", fmt.Errorf("get account failed: %w", err)
|
||||
}
|
||||
|
||||
userActivities, err := types.ParseUserActivities(account.Annotations)
|
||||
func (r *BillingInfoQueryReconciler) RechargeQuery(_ context.Context, _ ctrl.Request, billingInfoQuery *accountv1.BillingInfoQuery) (result string, err error) {
|
||||
//TODO get owner
|
||||
userDiscount, err := r.AccountV2.GetUserAccountRechargeDiscount(&types.UserQueryOpts{Owner: getUsername(billingInfoQuery.Namespace)})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse user activities failed: %w", err)
|
||||
}
|
||||
|
||||
rechargeDiscount := types.RechargeDiscount{
|
||||
DiscountSteps: r.RechargeStep,
|
||||
DiscountRates: r.RechargeRatio,
|
||||
if userDiscount == nil || len(userDiscount.DiscountRates) == 0 || len(userDiscount.DiscountSteps) == 0 {
|
||||
userDiscount = &r.DefaultDiscount
|
||||
}
|
||||
|
||||
if len(userActivities) > 0 {
|
||||
if _, phase, _ := types.GetUserActivityDiscount(r.Activities, &userActivities); phase != nil {
|
||||
if len(phase.RechargeDiscount.DiscountSteps) > 0 {
|
||||
rechargeDiscount.DiscountSteps = phase.RechargeDiscount.DiscountSteps
|
||||
rechargeDiscount.DiscountRates = phase.RechargeDiscount.DiscountRates
|
||||
}
|
||||
rechargeDiscount.SpecialDiscount = phase.RechargeDiscount.SpecialDiscount
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(rechargeDiscount)
|
||||
data, err := json.Marshal(userDiscount)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal recharge discount failed: %w", err)
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
"github.com/labring/sealos/controllers/pkg/resources"
|
||||
"github.com/labring/sealos/controllers/pkg/utils/env"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -110,13 +109,6 @@ func (r *BillingRecordQueryReconciler) Reconcile(ctx context.Context, req ctrl.R
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if err = r.Get(ctx, client.ObjectKey{Name: getUsername(billingRecordQuery.Namespace), Namespace: r.AccountSystemNamespace}, &accountv1.Account{}); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
billingRecordQuery.Status.Status = "Please use the owner account to query"
|
||||
return ctrl.Result{}, r.Status().Update(ctx, billingRecordQuery)
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
err = dbClient.QueryBillingRecords(billingRecordQuery, getUsername(billingRecordQuery.Namespace))
|
||||
if err != nil {
|
||||
r.Logger.Error(err, "query billing records failed")
|
||||
|
||||
@@ -18,6 +18,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
@@ -25,6 +26,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database/cockroach"
|
||||
|
||||
pkgtypes "github.com/labring/sealos/controllers/pkg/types"
|
||||
|
||||
userv1 "github.com/labring/sealos/controllers/user/api/v1"
|
||||
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database"
|
||||
@@ -65,12 +74,13 @@ const (
|
||||
// DebtReconciler reconciles a Debt object
|
||||
type DebtReconciler struct {
|
||||
client.Client
|
||||
AccountV2 database.AccountV2
|
||||
DBClient database.Auth
|
||||
Scheme *runtime.Scheme
|
||||
DebtDetectionCycle time.Duration
|
||||
LocalRegionID string
|
||||
logr.Logger
|
||||
accountSystemNamespace string
|
||||
accountNamespace string
|
||||
SmsConfig *SmsConfig
|
||||
}
|
||||
|
||||
@@ -97,49 +107,45 @@ var DebtConfig = accountv1.DefaultDebtConfig
|
||||
|
||||
func (r *DebtReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
debt := &accountv1.Debt{}
|
||||
account := &accountv1.Account{}
|
||||
if err := r.Get(ctx, req.NamespacedName, account); err == nil {
|
||||
if account.DeletionTimestamp != nil {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: GetDebtName(account.Name), Namespace: r.accountSystemNamespace}, debt); client.IgnoreNotFound(err) != nil {
|
||||
return ctrl.Result{}, err
|
||||
} else if err != nil {
|
||||
if err := r.syncDebt(ctx, account, debt); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
owner := req.NamespacedName.Name
|
||||
account, err := r.AccountV2.GetAccount(&pkgtypes.UserQueryOpts{Owner: owner})
|
||||
if account == nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
userOwner := &userv1.User{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: owner, Namespace: r.accountSystemNamespace}, userOwner); err != nil {
|
||||
// if user not exist, skip
|
||||
if client.IgnoreNotFound(err) == nil {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("failed to get user %s: %v", owner, err)
|
||||
}
|
||||
// if user not exist, skip
|
||||
if userOwner.CreationTimestamp.Add(20 * 24 * time.Hour).Before(time.Now()) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
r.Logger.Info("create or update debt success", "debt", debt)
|
||||
}
|
||||
} else if client.IgnoreNotFound(err) != nil {
|
||||
r.Logger.Error(err, err.Error())
|
||||
return ctrl.Result{}, err
|
||||
r.Logger.Error(fmt.Errorf("account %s not exist", owner), "account not exist")
|
||||
return ctrl.Result{RequeueAfter: 60 * time.Minute}, nil
|
||||
}
|
||||
// In a multi-region scenario, select the region where the account is created for SMS notification
|
||||
smsEnable := account.CreateRegionID == r.LocalRegionID
|
||||
|
||||
if err := r.Get(ctx, req.NamespacedName, debt); err == nil {
|
||||
if debt.DeletionTimestamp != nil {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: debt.Spec.UserName, Namespace: r.accountNamespace}, account); err != nil {
|
||||
r.Logger.Info("reconcile debt", "account", owner, "balance", account.Balance, "deduction balance", account.DeductionBalance)
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: GetDebtName(owner), Namespace: r.accountSystemNamespace}, debt); client.IgnoreNotFound(err) != nil {
|
||||
return ctrl.Result{}, err
|
||||
} else if err != nil {
|
||||
if err := r.syncDebt(ctx, owner, debt); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
} else if client.IgnoreNotFound(err) != nil {
|
||||
r.Logger.Error(err, err.Error())
|
||||
return ctrl.Result{}, err
|
||||
r.Logger.Info("create or update debt success", "debt", debt)
|
||||
}
|
||||
|
||||
if debt.Name == "" || account.Name == "" {
|
||||
r.Logger.Info("not get debt or not get account", "debt name", debt.Name, "account name", account.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
// now should get debt and account
|
||||
//r.Logger.Info("debt info", "debt", debt)
|
||||
|
||||
nsList, err := getOwnNsList(r.Client, getUsername(account.Name))
|
||||
nsList, err := getOwnNsList(r.Client, getUsername(owner))
|
||||
if err != nil {
|
||||
r.Logger.Error(err, "get own ns list error")
|
||||
return ctrl.Result{}, fmt.Errorf("get own ns list error: %v", err)
|
||||
}
|
||||
if err := r.reconcileDebtStatus(ctx, debt, account, nsList); err != nil {
|
||||
if err := r.reconcileDebtStatus(ctx, debt, account, nsList, smsEnable); err != nil {
|
||||
r.Logger.Error(err, "reconcile debt status error")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
@@ -157,8 +163,8 @@ NormalPeriod -> WarningPeriod -> ApproachingDeletionPeriod -> ImmediateDeletePer
|
||||
|
||||
欠费后到完全删除的总周期=WarningPeriodSeconds+ApproachingDeletionPeriodSeconds+ImmediateDeletePeriodSeconds+FinalDeletePeriodSeconds
|
||||
*/
|
||||
func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv1.Debt, account *accountv1.Account, userNamespaceList []string) error {
|
||||
oweamount := account.Status.Balance - account.Status.DeductionBalance
|
||||
func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv1.Debt, account *pkgtypes.Account, userNamespaceList []string, smsEnable bool) error {
|
||||
oweamount := account.Balance - account.DeductionBalance
|
||||
//更新间隔秒钟数
|
||||
updateIntervalSeconds := time.Now().UTC().Unix() - debt.Status.LastUpdateTimestamp
|
||||
lastStatus := debt.Status
|
||||
@@ -178,7 +184,7 @@ func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv
|
||||
return nil
|
||||
}
|
||||
update = SetDebtStatus(debt, accountv1.WarningPeriod)
|
||||
if err := r.sendWarningNotice(ctx, debt.Spec.UserName, oweamount, userNamespaceList); err != nil {
|
||||
if err := r.sendWarningNotice(ctx, debt.Spec.UserName, oweamount, userNamespaceList, smsEnable); err != nil {
|
||||
r.Logger.Error(err, "send warning notice error")
|
||||
}
|
||||
case accountv1.WarningPeriod:
|
||||
@@ -197,11 +203,11 @@ func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv
|
||||
break
|
||||
}
|
||||
//上次更新时间小于临近删除时间
|
||||
if updateIntervalSeconds < DebtConfig[accountv1.ApproachingDeletionPeriod] && (account.Status.Balance/2)+oweamount > 0 {
|
||||
if updateIntervalSeconds < DebtConfig[accountv1.ApproachingDeletionPeriod] && (account.Balance/2)+oweamount > 0 {
|
||||
return nil
|
||||
}
|
||||
update = SetDebtStatus(debt, accountv1.ApproachingDeletionPeriod)
|
||||
if err := r.sendApproachingDeletionNotice(ctx, debt.Spec.UserName, oweamount, userNamespaceList); err != nil {
|
||||
if err := r.sendApproachingDeletionNotice(ctx, debt.Spec.UserName, oweamount, userNamespaceList, smsEnable); err != nil {
|
||||
r.Logger.Error(err, "sendApproachingDeletionNotice error")
|
||||
}
|
||||
|
||||
@@ -220,11 +226,11 @@ func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv
|
||||
//TODO 撤销临近删除消息通知
|
||||
break
|
||||
}
|
||||
if updateIntervalSeconds < DebtConfig[accountv1.ImminentDeletionPeriod] && account.Status.Balance+oweamount > 0 {
|
||||
if updateIntervalSeconds < DebtConfig[accountv1.ImminentDeletionPeriod] && account.Balance+oweamount > 0 {
|
||||
return nil
|
||||
}
|
||||
update = SetDebtStatus(debt, accountv1.ImminentDeletionPeriod)
|
||||
if err := r.sendImminentDeletionNotice(ctx, debt.Spec.UserName, oweamount, userNamespaceList); err != nil {
|
||||
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 {
|
||||
@@ -254,7 +260,7 @@ func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv
|
||||
}
|
||||
// TODO 暂时只暂停资源,后续会添加真正删除全部资源逻辑, 或直接删除namespace
|
||||
update = SetDebtStatus(debt, accountv1.FinalDeletionPeriod)
|
||||
if err := r.sendFinalDeletionNotice(ctx, debt.Spec.UserName, oweamount, userNamespaceList); err != nil {
|
||||
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 {
|
||||
@@ -284,7 +290,7 @@ func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv
|
||||
}
|
||||
|
||||
if update {
|
||||
r.Logger.Info("update debt status", "account", account.Name,
|
||||
r.Logger.Info("update debt status", "account", debt.Spec.UserName,
|
||||
"last status", lastStatus, "last update time", time.Unix(debt.Status.LastUpdateTimestamp, 0).Format(time.RFC3339),
|
||||
"current status", debt.Status.AccountDebtStatus, "time", time.Now().UTC().Format(time.RFC3339))
|
||||
return r.Status().Update(ctx, debt)
|
||||
@@ -292,11 +298,11 @@ func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DebtReconciler) syncDebt(ctx context.Context, account *accountv1.Account, debt *accountv1.Debt) error {
|
||||
debt.Name = GetDebtName(account.Name)
|
||||
func (r *DebtReconciler) syncDebt(ctx context.Context, owner string, debt *accountv1.Debt) error {
|
||||
debt.Name = GetDebtName(owner)
|
||||
debt.Namespace = r.accountSystemNamespace
|
||||
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, debt, func() error {
|
||||
debt.Spec.UserName = account.Name
|
||||
debt.Spec.UserName = owner
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -349,12 +355,7 @@ const (
|
||||
falseStatus = "false"
|
||||
)
|
||||
|
||||
var 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: "Your account balance is not enough to pay this month's bill. The system will delete your resources after three days or after the arrears exceed the recharge amount. Please recharge in time to avoid affecting your normal use.",
|
||||
ImminentDeletionNotice: "Your container instance resources have been suspended. If you are still in arrears for more than 7 days, the resources will be completely deleted and cannot be recovered. Please recharge in time to avoid affecting your normal use.",
|
||||
FinalDeletionNotice: "The system has completely deleted all your resources, please recharge in time to avoid affecting your normal use.",
|
||||
}
|
||||
var NoticeTemplateEN map[int]string
|
||||
|
||||
var TitleTemplateZH = map[int]string{
|
||||
WarningNotice: "欠费告警",
|
||||
@@ -370,29 +371,28 @@ var TitleTemplateEN = map[int]string{
|
||||
FinalDeletionNotice: "Resource Release Warning",
|
||||
}
|
||||
|
||||
var NoticeTemplateZH = map[int]string{
|
||||
WarningNotice: "您的账户余额不足,系统将为您暂停服务,请及时充值,以免影响您的正常使用。",
|
||||
ApproachingDeletionNotice: "您的账户余额不足,系统将在三天后或欠费超过充值金额后释放您的资源,请及时充值,以免影响您的正常使用。",
|
||||
ImminentDeletionNotice: "您的容器实例资源已被暂停,若您仍欠费超过7天,系统将彻底释放资源,无法恢复,请及时充值,以免影响您的正常使用。",
|
||||
FinalDeletionNotice: "系统已彻底释放您的所有资源,请及时充值,以免影响您的正常使用。",
|
||||
}
|
||||
var NoticeTemplateZH map[int]string
|
||||
|
||||
func (r *DebtReconciler) sendSMSNotice(user string, oweAmount int64, noticeType int) error {
|
||||
if r.SmsConfig == nil {
|
||||
return nil
|
||||
}
|
||||
// TODO send sms
|
||||
usr, err := r.DBClient.GetUser(user)
|
||||
//usr, err := r.DBClient.GetUser(user)
|
||||
//if err != nil {
|
||||
// return fmt.Errorf("failed to get user: %w", err)
|
||||
//}
|
||||
outh, err := r.AccountV2.GetUserOauthProvider(&pkgtypes.UserQueryOpts{Owner: user})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
return fmt.Errorf("failed to get user oauth provider: %w", err)
|
||||
}
|
||||
if usr == nil || usr.Phone == "" {
|
||||
if outh == nil || outh.ProviderID == "" || outh.ProviderType != pkgtypes.OauthProviderTypePhone {
|
||||
r.Logger.Info("user not exist or user phone is empty, skip sms notification", "user", user)
|
||||
return nil
|
||||
}
|
||||
oweamount := strconv.FormatInt(int64(math.Abs(math.Ceil(float64(oweAmount)/1_000_000))), 10)
|
||||
return utils.SendSms(r.SmsConfig.Client, &client2.SendSmsRequest{
|
||||
PhoneNumbers: tea.String(usr.Phone),
|
||||
PhoneNumbers: tea.String(outh.ProviderID),
|
||||
SignName: tea.String(r.SmsConfig.SmsSignName),
|
||||
TemplateCode: tea.String(r.SmsConfig.SmsCode[noticeType]),
|
||||
// |ownAmount/1_000_000|
|
||||
@@ -400,7 +400,7 @@ func (r *DebtReconciler) sendSMSNotice(user string, oweAmount int64, noticeType
|
||||
})
|
||||
}
|
||||
|
||||
func (r *DebtReconciler) sendNotice(ctx context.Context, user string, oweAmount int64, noticeType int, namespaces []string) error {
|
||||
func (r *DebtReconciler) sendNotice(ctx context.Context, user string, oweAmount int64, noticeType int, namespaces []string, smsEnable bool) error {
|
||||
now := time.Now().UTC().Unix()
|
||||
ntfTmp := &v1.Notification{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -437,23 +437,26 @@ func (r *DebtReconciler) sendNotice(ctx context.Context, user string, oweAmount
|
||||
return err
|
||||
}
|
||||
}
|
||||
return r.sendSMSNotice(user, oweAmount, noticeType)
|
||||
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) error {
|
||||
return r.sendNotice(ctx, user, oweAmount, WarningNotice, namespaces)
|
||||
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) error {
|
||||
return r.sendNotice(ctx, user, oweAmount, ApproachingDeletionNotice, namespaces)
|
||||
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) error {
|
||||
return r.sendNotice(ctx, user, oweAmount, ImminentDeletionNotice, namespaces)
|
||||
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) error {
|
||||
return r.sendNotice(ctx, user, oweAmount, FinalDeletionNotice, namespaces)
|
||||
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 {
|
||||
@@ -522,8 +525,7 @@ func setupSmsConfig() (*SmsConfig, error) {
|
||||
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.accountNamespace = env.GetEnvWithDefault(ACCOUNTNAMESPACEENV, "sealos-system")
|
||||
setDefaultDebtPeriodWaitSecond()
|
||||
r.LocalRegionID = os.Getenv(cockroach.EnvLocalRegion)
|
||||
debtDetectionCycleSecond := env.GetInt64EnvWithDefault(DebtDetectionCycleEnv, 60)
|
||||
r.DebtDetectionCycle = time.Duration(debtDetectionCycleSecond) * time.Second
|
||||
|
||||
@@ -544,10 +546,9 @@ func (r *DebtReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.
|
||||
"accountNamespace": "sealos-system"}
|
||||
*/
|
||||
r.Logger.Info("set config", "DebtConfig", DebtConfig, "DebtDetectionCycle", r.DebtDetectionCycle,
|
||||
"accountSystemNamespace", r.accountSystemNamespace, "accountNamespace", r.accountNamespace)
|
||||
"accountSystemNamespace", r.accountSystemNamespace)
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
// update status should not enter reconcile
|
||||
For(&accountv1.Account{}, builder.WithPredicates(OnlyCreatePredicate{})).
|
||||
For(&userv1.User{}, builder.WithPredicates(predicate.And(UserOwnerPredicate{}))).
|
||||
WithOptions(rateOpts).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -563,6 +564,31 @@ func setDefaultDebtPeriodWaitSecond() {
|
||||
DebtConfig[accountv1.ApproachingDeletionPeriod] = env.GetInt64EnvWithDefault(string(accountv1.ApproachingDeletionPeriod), 4*accountv1.DaySecond)
|
||||
DebtConfig[accountv1.ImminentDeletionPeriod] = env.GetInt64EnvWithDefault(string(accountv1.ImminentDeletionPeriod), 3*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.",
|
||||
}
|
||||
}
|
||||
|
||||
type UserOwnerPredicate struct {
|
||||
predicate.Funcs
|
||||
}
|
||||
|
||||
func (UserOwnerPredicate) Create(e event.CreateEvent) bool {
|
||||
owner := e.Object.GetAnnotations()[userv1.UserAnnotationOwnerKey]
|
||||
return owner != "" && owner == e.Object.GetName()
|
||||
}
|
||||
|
||||
func (UserOwnerPredicate) Update(_ event.UpdateEvent) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type OnlyCreatePredicate struct {
|
||||
@@ -576,3 +602,7 @@ func (OnlyCreatePredicate) Update(_ event.UpdateEvent) bool {
|
||||
func (OnlyCreatePredicate) Create(_ event.CreateEvent) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func init() {
|
||||
setDefaultDebtPeriodWaitSecond()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
@@ -41,6 +42,7 @@ type PaymentReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
Logger logr.Logger
|
||||
domain string
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=account.sealos.io,resources=payments,verbs=get;list;watch;create;update;patch;delete
|
||||
@@ -82,7 +84,7 @@ func (r *PaymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// get tradeNO and codeURL
|
||||
tradeNO, codeURL, err := payHandler.CreatePayment(p.Spec.Amount/10000, p.Spec.UserID)
|
||||
tradeNO, codeURL, err := payHandler.CreatePayment(p.Spec.Amount/10000, p.Spec.UserID, "sealos cloud pay [domain="+r.domain+"]")
|
||||
if err != nil {
|
||||
r.Logger.Error(err, "get tradeNO and codeURL failed")
|
||||
return ctrl.Result{Requeue: true, RequeueAfter: time.Second}, err
|
||||
@@ -104,6 +106,7 @@ func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controll
|
||||
const controllerName = "payment_controller"
|
||||
r.Logger = ctrl.Log.WithName(controllerName)
|
||||
r.Logger.V(1).Info("init reconcile controller payment")
|
||||
r.domain = os.Getenv("DOMAIN")
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&accountv1.Payment{}, builder.WithPredicates(OnlyCreatePredicate{})).
|
||||
WithOptions(rateOpts).
|
||||
|
||||
@@ -16,240 +16,241 @@ limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/common"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/resources"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/crypto"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
|
||||
v1 "github.com/labring/sealos/controllers/pkg/notification/api/v1"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
accountv1 "github.com/labring/sealos/controllers/account/api/v1"
|
||||
)
|
||||
|
||||
var MinBalance int64 = 10_000000
|
||||
|
||||
// TransferReconciler reconciles a Transfer object
|
||||
type TransferReconciler struct {
|
||||
Logger logr.Logger
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
AccountSystemNamespace string
|
||||
DBClient database.Account
|
||||
}
|
||||
|
||||
//TODO add user, account role
|
||||
//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts,verbs=get;list;watch;create
|
||||
//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/status,verbs=get
|
||||
//+kubebuilder:rbac:groups=account.sealos.io,resources=transfers,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=account.sealos.io,resources=transfers/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=account.sealos.io,resources=transfers/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups=notification.sealos.io,resources=notifications,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
// the Transfer object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile
|
||||
func (r *TransferReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
transfer := accountv1.Transfer{}
|
||||
if err := r.Get(ctx, req.NamespacedName, &transfer); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
transfer.Spec.From = getUsername(transfer.Namespace)
|
||||
if time.Since(transfer.CreationTimestamp.Time) > time.Minute*3 {
|
||||
return ctrl.Result{}, r.Delete(ctx, &transfer)
|
||||
}
|
||||
//TODO Error rollback required
|
||||
pipeLine := []func(ctx context.Context, transfer *accountv1.Transfer) error{
|
||||
r.check,
|
||||
r.transferSaver,
|
||||
r.transferAccount,
|
||||
}
|
||||
for _, f := range pipeLine {
|
||||
if err := f(ctx, &transfer); err != nil {
|
||||
transfer.Status.Reason = err.Error()
|
||||
transfer.Status.Progress = accountv1.TransferStateFailed
|
||||
break
|
||||
}
|
||||
}
|
||||
if transfer.Status.Progress != accountv1.TransferStateFailed {
|
||||
transfer.Status.Progress = accountv1.TransferStateCompleted
|
||||
}
|
||||
if err := r.Status().Update(ctx, &transfer); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update transfer status failed: %w", err)
|
||||
}
|
||||
return ctrl.Result{RequeueAfter: 3 * time.Minute}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *TransferReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
r.AccountSystemNamespace = os.Getenv(ACCOUNTNAMESPACEENV)
|
||||
if r.AccountSystemNamespace == "" {
|
||||
r.AccountSystemNamespace = DEFAULTACCOUNTNAMESPACE
|
||||
}
|
||||
r.Logger = ctrl.Log.WithName("transfer-controller")
|
||||
if m := os.Getenv("TRANSFERMINBALANCE"); m != "" {
|
||||
minBalance, err := strconv.ParseInt(m, 10, 64)
|
||||
if err != nil {
|
||||
r.Logger.Error(err, "parse min balance failed")
|
||||
} else {
|
||||
MinBalance = minBalance
|
||||
}
|
||||
}
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&accountv1.Transfer{}, builder.WithPredicates(OnlyCreatePredicate{})).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *TransferReconciler) transferSaver(ctx context.Context, transfer *accountv1.Transfer) error {
|
||||
idOut, err := gonanoid.New(12)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create id failed: %w", err)
|
||||
}
|
||||
idIn, err := gonanoid.New(12)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create id failed: %w", err)
|
||||
}
|
||||
err = r.DBClient.SaveBillings(&resources.Billing{
|
||||
OrderID: idOut,
|
||||
Amount: transfer.Spec.Amount,
|
||||
Owner: getUsername(transfer.Namespace),
|
||||
Type: accountv1.TransferOut,
|
||||
Namespace: transfer.Namespace,
|
||||
Time: transfer.CreationTimestamp.Time,
|
||||
Transfer: &resources.Transfer{
|
||||
To: transfer.Spec.To,
|
||||
Amount: transfer.Spec.Amount,
|
||||
},
|
||||
}, &resources.Billing{
|
||||
OrderID: idIn,
|
||||
Amount: transfer.Spec.Amount,
|
||||
Owner: getUsername(transfer.Spec.To),
|
||||
Type: accountv1.TransferIn,
|
||||
Namespace: transfer.Namespace,
|
||||
Time: transfer.CreationTimestamp.Time,
|
||||
Transfer: &resources.Transfer{
|
||||
From: transfer.Spec.From,
|
||||
Amount: transfer.Spec.Amount,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("save billing failed: %w", err)
|
||||
}
|
||||
if err = r.sendNotice(ctx, transfer.Namespace, transfer.Spec.To, transfer.Spec.Amount, accountv1.TransferOut); err != nil {
|
||||
r.Logger.Error(err, "send notice failed")
|
||||
}
|
||||
if err := r.sendNotice(ctx, transfer.Spec.To, transfer.Namespace, transfer.Spec.Amount, accountv1.TransferIn); err != nil {
|
||||
r.Logger.Error(err, "send notice failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TransferReconciler) transferAccount(ctx context.Context, transfer *accountv1.Transfer) error {
|
||||
from, to := transfer.Namespace, transfer.Spec.To
|
||||
var fromAccount, toAccount accountv1.Account
|
||||
if r.Get(ctx, client.ObjectKey{Namespace: r.AccountSystemNamespace, Name: getUsername(from)}, &fromAccount) != nil {
|
||||
return fmt.Errorf("owner %s account not found", from)
|
||||
}
|
||||
if r.Get(ctx, client.ObjectKey{Namespace: r.AccountSystemNamespace, Name: getUsername(to)}, &toAccount) != nil {
|
||||
return fmt.Errorf("owner %s account not found", to)
|
||||
}
|
||||
balance, _ := crypto.DecryptInt64(*fromAccount.Status.EncryptBalance)
|
||||
deductionBalance, _ := crypto.DecryptInt64(*fromAccount.Status.EncryptDeductionBalance)
|
||||
// check balance is enough ( balance - deductionBalance - transferAmount - MinBalance - ActivityBonus) activity give amount not included
|
||||
if balance < deductionBalance+transfer.Spec.Amount+MinBalance+fromAccount.Status.ActivityBonus {
|
||||
return fmt.Errorf("balance not enough")
|
||||
}
|
||||
if r.Get(ctx, client.ObjectKey{Namespace: r.AccountSystemNamespace, Name: getUsername(to)}, &accountv1.Account{}) != nil {
|
||||
return fmt.Errorf("user %s account not found", transfer.Spec.To)
|
||||
}
|
||||
err := crypto.RechargeBalance(toAccount.Status.EncryptBalance, transfer.Spec.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = crypto.DeductBalance(fromAccount.Status.EncryptBalance, transfer.Spec.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = SyncAccountStatus(ctx, r.Client, &toAccount); err != nil {
|
||||
return fmt.Errorf("sync account status failed: %w", err)
|
||||
}
|
||||
if err = SyncAccountStatus(ctx, r.Client, &fromAccount); err != nil {
|
||||
return fmt.Errorf("sync account status failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
TransferInNotification = `You have a new transfer from %s, amount: %d`
|
||||
TransferOutNotification = `You have a new transfer to %s, amount: %d`
|
||||
)
|
||||
|
||||
var transferNotification = map[common.Type]string{
|
||||
accountv1.TransferIn: TransferInNotification,
|
||||
accountv1.TransferOut: TransferOutNotification,
|
||||
}
|
||||
|
||||
func (r *TransferReconciler) sendNotice(ctx context.Context, namespace string, user string, amount int64, _type common.Type) error {
|
||||
now := time.Now().UTC().Unix()
|
||||
ntf := v1.Notification{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "transfer-notice-" + strconv.FormatInt(now, 10),
|
||||
Namespace: GetUserNamespace(getUsername(namespace)),
|
||||
},
|
||||
Spec: v1.NotificationSpec{
|
||||
Title: "Transfer Notice",
|
||||
Message: fmt.Sprintf(transferNotification[_type], GetUserNamespace(getUsername(user)), convertAmount(amount)),
|
||||
From: "Account-System",
|
||||
Timestamp: now,
|
||||
Importance: v1.Low,
|
||||
},
|
||||
}
|
||||
return r.Create(ctx, &ntf)
|
||||
}
|
||||
|
||||
// Convert amount 1¥:1000000
|
||||
func convertAmount(amount int64) int64 {
|
||||
return amount / 1_000_000
|
||||
}
|
||||
|
||||
func (r *TransferReconciler) check(_ context.Context, transfer *accountv1.Transfer) error {
|
||||
if transfer.Spec.Amount <= 0 {
|
||||
return fmt.Errorf("amount must be greater than 0")
|
||||
}
|
||||
if transfer.Status.Progress == accountv1.TransferStateFailed {
|
||||
return fmt.Errorf(transfer.Status.Reason)
|
||||
}
|
||||
if transfer.Status.Progress == accountv1.TransferStateCompleted {
|
||||
return fmt.Errorf("transfer already completed")
|
||||
}
|
||||
from := transfer.Namespace
|
||||
to := transfer.Spec.To
|
||||
if getUsername(from) == getUsername(to) {
|
||||
return fmt.Errorf("can not transfer to self")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
//import (
|
||||
// "context"
|
||||
// "fmt"
|
||||
// "os"
|
||||
// "strconv"
|
||||
// "time"
|
||||
//
|
||||
// "github.com/labring/sealos/controllers/pkg/common"
|
||||
//
|
||||
// "github.com/labring/sealos/controllers/pkg/resources"
|
||||
//
|
||||
// "github.com/labring/sealos/controllers/pkg/database"
|
||||
//
|
||||
// "github.com/labring/sealos/controllers/pkg/crypto"
|
||||
//
|
||||
// "github.com/go-logr/logr"
|
||||
// gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
// "sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
//
|
||||
// v1 "github.com/labring/sealos/controllers/pkg/notification/api/v1"
|
||||
//
|
||||
// "k8s.io/apimachinery/pkg/runtime"
|
||||
// ctrl "sigs.k8s.io/controller-runtime"
|
||||
// "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
//
|
||||
// accountv1 "github.com/labring/sealos/controllers/account/api/v1"
|
||||
//)
|
||||
//
|
||||
//var MinBalance int64 = 10_000000
|
||||
//
|
||||
//// TransferReconciler reconciles a Transfer object
|
||||
//type TransferReconciler struct {
|
||||
// Logger logr.Logger
|
||||
// client.Client
|
||||
// Scheme *runtime.Scheme
|
||||
// AccountSystemNamespace string
|
||||
// DBClient database.Account
|
||||
//}
|
||||
//
|
||||
////TODO add user, account role
|
||||
////+kubebuilder:rbac:groups=account.sealos.io,resources=accounts,verbs=get;list;watch;create
|
||||
////+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/status,verbs=get
|
||||
////+kubebuilder:rbac:groups=account.sealos.io,resources=transfers,verbs=get;list;watch;create;update;patch;delete
|
||||
////+kubebuilder:rbac:groups=account.sealos.io,resources=transfers/status,verbs=get;update;patch
|
||||
////+kubebuilder:rbac:groups=account.sealos.io,resources=transfers/finalizers,verbs=update
|
||||
////+kubebuilder:rbac:groups=notification.sealos.io,resources=notifications,verbs=get;list;watch;create;update;patch;delete
|
||||
//
|
||||
//// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
//// move the current state of the cluster closer to the desired state.
|
||||
//// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
//// the Transfer object against the actual cluster state, and then
|
||||
//// perform operations to make the cluster state reflect the state specified by
|
||||
//// the user.
|
||||
////
|
||||
//// For more details, check Reconcile and its Result here:
|
||||
//// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile
|
||||
//func (r *TransferReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
// transfer := accountv1.Transfer{}
|
||||
// if err := r.Get(ctx, req.NamespacedName, &transfer); err != nil {
|
||||
// return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
// }
|
||||
// transfer.Spec.From = getUsername(transfer.Namespace)
|
||||
// if time.Since(transfer.CreationTimestamp.Time) > time.Minute*3 {
|
||||
// return ctrl.Result{}, r.Delete(ctx, &transfer)
|
||||
// }
|
||||
// //TODO Error rollback required
|
||||
// pipeLine := []func(ctx context.Context, transfer *accountv1.Transfer) error{
|
||||
// r.check,
|
||||
// r.transferSaver,
|
||||
// r.transferAccount,
|
||||
// }
|
||||
// for _, f := range pipeLine {
|
||||
// if err := f(ctx, &transfer); err != nil {
|
||||
// transfer.Status.Reason = err.Error()
|
||||
// transfer.Status.Progress = accountv1.TransferStateFailed
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if transfer.Status.Progress != accountv1.TransferStateFailed {
|
||||
// transfer.Status.Progress = accountv1.TransferStateCompleted
|
||||
// }
|
||||
// if err := r.Status().Update(ctx, &transfer); err != nil {
|
||||
// return ctrl.Result{}, fmt.Errorf("update transfer status failed: %w", err)
|
||||
// }
|
||||
// return ctrl.Result{RequeueAfter: 3 * time.Minute}, nil
|
||||
//}
|
||||
//
|
||||
//// SetupWithManager sets up the controller with the Manager.
|
||||
//func (r *TransferReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
// r.AccountSystemNamespace = os.Getenv(ACCOUNTNAMESPACEENV)
|
||||
// if r.AccountSystemNamespace == "" {
|
||||
// r.AccountSystemNamespace = DEFAULTACCOUNTNAMESPACE
|
||||
// }
|
||||
// r.Logger = ctrl.Log.WithName("transfer-controller")
|
||||
// if m := os.Getenv("TRANSFERMINBALANCE"); m != "" {
|
||||
// minBalance, err := strconv.ParseInt(m, 10, 64)
|
||||
// if err != nil {
|
||||
// r.Logger.Error(err, "parse min balance failed")
|
||||
// } else {
|
||||
// MinBalance = minBalance
|
||||
// }
|
||||
// }
|
||||
// return ctrl.NewControllerManagedBy(mgr).
|
||||
// For(&accountv1.Transfer{}, builder.WithPredicates(OnlyCreatePredicate{})).
|
||||
// Complete(r)
|
||||
//}
|
||||
//
|
||||
//func (r *TransferReconciler) transferSaver(ctx context.Context, transfer *accountv1.Transfer) error {
|
||||
// idOut, err := gonanoid.New(12)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("create id failed: %w", err)
|
||||
// }
|
||||
// idIn, err := gonanoid.New(12)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("create id failed: %w", err)
|
||||
// }
|
||||
// err = r.DBClient.SaveBillings(&resources.Billing{
|
||||
// OrderID: idOut,
|
||||
// Amount: transfer.Spec.Amount,
|
||||
// Owner: getUsername(transfer.Namespace),
|
||||
// Type: accountv1.TransferOut,
|
||||
// Namespace: transfer.Namespace,
|
||||
// Time: transfer.CreationTimestamp.Time,
|
||||
// Transfer: &resources.Transfer{
|
||||
// To: transfer.Spec.To,
|
||||
// Amount: transfer.Spec.Amount,
|
||||
// },
|
||||
// }, &resources.Billing{
|
||||
// OrderID: idIn,
|
||||
// Amount: transfer.Spec.Amount,
|
||||
// Owner: getUsername(transfer.Spec.To),
|
||||
// Type: accountv1.TransferIn,
|
||||
// Namespace: transfer.Namespace,
|
||||
// Time: transfer.CreationTimestamp.Time,
|
||||
// Transfer: &resources.Transfer{
|
||||
// From: transfer.Spec.From,
|
||||
// Amount: transfer.Spec.Amount,
|
||||
// },
|
||||
// })
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("save billing failed: %w", err)
|
||||
// }
|
||||
// if err = r.sendNotice(ctx, transfer.Namespace, transfer.Spec.To, transfer.Spec.Amount, accountv1.TransferOut); err != nil {
|
||||
// r.Logger.Error(err, "send notice failed")
|
||||
// }
|
||||
// if err := r.sendNotice(ctx, transfer.Spec.To, transfer.Namespace, transfer.Spec.Amount, accountv1.TransferIn); err != nil {
|
||||
// r.Logger.Error(err, "send notice failed")
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//func (r *TransferReconciler) transferAccount(ctx context.Context, transfer *accountv1.Transfer) error {
|
||||
// from, to := transfer.Namespace, transfer.Spec.To
|
||||
// var fromAccount, toAccount accountv1.Account
|
||||
// if r.Get(ctx, client.ObjectKey{Namespace: r.AccountSystemNamespace, Name: getUsername(from)}, &fromAccount) != nil {
|
||||
// return fmt.Errorf("owner %s account not found", from)
|
||||
// }
|
||||
// if r.Get(ctx, client.ObjectKey{Namespace: r.AccountSystemNamespace, Name: getUsername(to)}, &toAccount) != nil {
|
||||
// return fmt.Errorf("owner %s account not found", to)
|
||||
// }
|
||||
// balance, _ := crypto.DecryptInt64(*fromAccount.Status.EncryptBalance)
|
||||
// deductionBalance, _ := crypto.DecryptInt64(*fromAccount.Status.EncryptDeductionBalance)
|
||||
// // check balance is enough ( balance - deductionBalance - transferAmount - MinBalance - ActivityBonus) activity give amount not included
|
||||
// if balance < deductionBalance+transfer.Spec.Amount+MinBalance+fromAccount.Status.ActivityBonus {
|
||||
// return fmt.Errorf("balance not enough")
|
||||
// }
|
||||
// if r.Get(ctx, client.ObjectKey{Namespace: r.AccountSystemNamespace, Name: getUsername(to)}, &accountv1.Account{}) != nil {
|
||||
// return fmt.Errorf("user %s account not found", transfer.Spec.To)
|
||||
// }
|
||||
// err := crypto.RechargeBalance(toAccount.Status.EncryptBalance, transfer.Spec.Amount)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// err = crypto.DeductBalance(fromAccount.Status.EncryptBalance, transfer.Spec.Amount)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err = SyncAccountStatus(ctx, r.Client, &toAccount); err != nil {
|
||||
// return fmt.Errorf("sync account status failed: %w", err)
|
||||
// }
|
||||
// if err = SyncAccountStatus(ctx, r.Client, &fromAccount); err != nil {
|
||||
// return fmt.Errorf("sync account status failed: %w", err)
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//const (
|
||||
// TransferInNotification = `You have a new transfer from %s, amount: %d`
|
||||
// TransferOutNotification = `You have a new transfer to %s, amount: %d`
|
||||
//)
|
||||
//
|
||||
//var transferNotification = map[common.Type]string{
|
||||
// accountv1.TransferIn: TransferInNotification,
|
||||
// accountv1.TransferOut: TransferOutNotification,
|
||||
//}
|
||||
//
|
||||
//func (r *TransferReconciler) sendNotice(ctx context.Context, namespace string, user string, amount int64, _type common.Type) error {
|
||||
// now := time.Now().UTC().Unix()
|
||||
// ntf := v1.Notification{
|
||||
// ObjectMeta: metav1.ObjectMeta{
|
||||
// Name: "transfer-notice-" + strconv.FormatInt(now, 10),
|
||||
// Namespace: GetUserNamespace(getUsername(namespace)),
|
||||
// },
|
||||
// Spec: v1.NotificationSpec{
|
||||
// Title: "Transfer Notice",
|
||||
// Message: fmt.Sprintf(transferNotification[_type], GetUserNamespace(getUsername(user)), convertAmount(amount)),
|
||||
// From: "Account-System",
|
||||
// Timestamp: now,
|
||||
// Importance: v1.Low,
|
||||
// },
|
||||
// }
|
||||
// return r.Create(ctx, &ntf)
|
||||
//}
|
||||
//
|
||||
//// Convert amount 1¥:1000000
|
||||
//func convertAmount(amount int64) int64 {
|
||||
// return amount / 1_000_000
|
||||
//}
|
||||
//
|
||||
//func (r *TransferReconciler) check(_ context.Context, transfer *accountv1.Transfer) error {
|
||||
// if transfer.Spec.Amount <= 0 {
|
||||
// return fmt.Errorf("amount must be greater than 0")
|
||||
// }
|
||||
// if transfer.Status.Progress == accountv1.TransferStateFailed {
|
||||
// return fmt.Errorf(transfer.Status.Reason)
|
||||
// }
|
||||
// if transfer.Status.Progress == accountv1.TransferStateCompleted {
|
||||
// return fmt.Errorf("transfer already completed")
|
||||
// }
|
||||
// from := transfer.Namespace
|
||||
// to := transfer.Spec.To
|
||||
// if getUsername(from) == getUsername(to) {
|
||||
// return fmt.Errorf("can not transfer to self")
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
|
||||
@@ -9,8 +9,11 @@ ENV DEFAULT_NAMESPACE account-system
|
||||
ENV cloudDomain="cloud.sealos.io"
|
||||
ENV cloudPort=""
|
||||
ENV MONGO_URI "mongodb://mongo:27017/resources"
|
||||
ENV GLOBAL_COCKROACH_URI ""
|
||||
ENV LOCAL_COCKROACH_URI ""
|
||||
ENV LOCAL_REGION ""
|
||||
ENV OSNamespace="objectstorage-system"
|
||||
ENV OSAdminSecret=""
|
||||
ENV OSInternalEndpoint=""
|
||||
|
||||
CMD ["( kubectl create ns $DEFAULT_NAMESPACE || true ) && ( kubectl create -f manifests/mongo-secret.yaml -n $DEFAULT_NAMESPACE || true ) && kubectl apply -f manifests/deploy.yaml -n $DEFAULT_NAMESPACE"]
|
||||
CMD ["( kubectl create ns $DEFAULT_NAMESPACE || true ) && ( kubectl create -f manifests/mongo-secret.yaml -n $DEFAULT_NAMESPACE || true ) && ( kubectl create -f manifests/account-manager-config.yaml -n $DEFAULT_NAMESPACE || true ) && kubectl apply -f manifests/deploy.yaml -n $DEFAULT_NAMESPACE"]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: account-manager-env
|
||||
data:
|
||||
ApproachingDeletionPeriod: '{{ .ApproachingDeletionPeriod | default "345600" }}'
|
||||
ImminentDeletionPeriod: '{{ .ImminentDeletionPeriod | default "259200" }}'
|
||||
FinalDeletionPeriod: '{{ .FinalDeletionPeriod | default "604800" }}'
|
||||
DebtDetectionCycleSeconds: '{{ .DebtDetectionCycleSeconds | default "30" }}'
|
||||
OSAdminSecret: '{{ .OSAdminSecret }}'
|
||||
OSInternalEndpoint: '{{ .OSInternalEndpoint }}'
|
||||
OSNamespace: '{{ .OSNamespace }}'
|
||||
LOCAL_COCKROACH_URI: '{{ .LOCAL_COCKROACH_URI }}'
|
||||
GLOBAL_COCKROACH_URI: '{{ .GLOBAL_COCKROACH_URI }}'
|
||||
LOCAL_REGION: '{{ .LOCAL_REGION }}'
|
||||
DOMAIN: '{{ .cloudDomain }}'
|
||||
PORT: '{{ .cloudPort }}'
|
||||
BASE_BALANCE: '{{ .BASE_BALANCE | default "ri79LzQiQrs6CVa1ctE308+AseBXbOua0RIMCXAH5hc3irs=" }}'
|
||||
|
||||
|
||||
@@ -1283,16 +1283,10 @@ spec:
|
||||
command:
|
||||
- /manager
|
||||
env:
|
||||
- name: DOMAIN
|
||||
value: '{{ .cloudDomain }}'
|
||||
- name: PORT
|
||||
value: '{{ .cloudPort }}'
|
||||
- name: ACCOUNT_NAMESPACE
|
||||
value: sealos-system
|
||||
- name: NAMESPACE_NAME
|
||||
value: user-system
|
||||
- name: NEW_ACCOUNT_AMOUNT
|
||||
value: ri79LzQiQrs6CVa1ctE308+AseBXbOua0RIMCXAH5hc3irs=
|
||||
- name: WHITELIST
|
||||
value: licenses.License.license.sealos.io/v1,notifications.Notification.notification.sealos.io/v1,payments.Payment.account.sealos.io/v1,billingrecordqueries.BillingRecordQuery.account.sealos.io/v1,billinginfoqueries.BillingInfoQuery.account.sealos.io/v1,pricequeries.PriceQuery.account.sealos.io/v1
|
||||
- name: ACCOUNT_SYSTEM_NAMESPACE
|
||||
@@ -1305,24 +1299,12 @@ spec:
|
||||
secretKeyRef:
|
||||
key: MONGO_URI
|
||||
name: mongo-secret
|
||||
- name: ApproachingDeletionPeriod
|
||||
value: "345600"
|
||||
- name: ImminentDeletionPeriod
|
||||
value: "259200"
|
||||
- name: FinalDeletionPeriod
|
||||
value: "604800"
|
||||
- name: DebtDetectionCycleSeconds
|
||||
value: "30"
|
||||
- name: OSAdminSecret
|
||||
value: '{{ .OSAdminSecret }}'
|
||||
- name: OSInternalEndpoint
|
||||
value: '{{ .OSInternalEndpoint }}'
|
||||
- name: OSNamespace
|
||||
value: '{{ .OSNamespace }}'
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: payment-secret
|
||||
optional: true
|
||||
- configMapRef:
|
||||
name: account-manager-env
|
||||
image: ghcr.io/labring/sealos-account-controller:latest
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
|
||||
@@ -4,4 +4,6 @@ metadata:
|
||||
name: mongo-secret
|
||||
namespace: {{ .DEFAULT_NAMESPACE }}
|
||||
stringData:
|
||||
MONGO_URI: "{{ .MONGO_URI }}"
|
||||
MONGO_URI: "{{ .MONGO_URI }}"
|
||||
COCKROACH_URI: "{{ .COCKROACH_URI }}"
|
||||
LOCAL_REGION: "{{ .LOCAL_REGION }}"
|
||||
@@ -53,6 +53,8 @@ require (
|
||||
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // 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.16.7 // indirect
|
||||
@@ -115,6 +117,7 @@ require (
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/gorm v1.25.5 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.27.2 // indirect
|
||||
k8s.io/component-base v0.27.2 // indirect
|
||||
k8s.io/klog/v2 v2.100.1 // indirect
|
||||
|
||||
@@ -150,6 +150,10 @@ github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
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=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -509,6 +513,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.27.4 h1:0pCo/AN9hONazBKlNUdhQymmnfLRbSZjd5H5H3f0bSs=
|
||||
|
||||
+36
-28
@@ -22,9 +22,14 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database/cockroach"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/labring/sealos/controllers/account/controllers/cache"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database/mongo"
|
||||
@@ -138,21 +143,29 @@ func main() {
|
||||
setupLog.Error(err, "unable to disconnect from mongo")
|
||||
}
|
||||
}()
|
||||
v2Account, err := cockroach.NewCockRoach(os.Getenv(database.GlobalCockroachURI), os.Getenv(database.LocalCockroachURI))
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to connect to cockroach")
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
err := v2Account.Close()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to disconnect from cockroach")
|
||||
}
|
||||
}()
|
||||
accountReconciler := &controllers.AccountReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
DBClient: dbClient,
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
DBClient: dbClient,
|
||||
AccountV2: v2Account,
|
||||
}
|
||||
billingInfoQueryReconciler := &controllers.BillingInfoQueryReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
DBClient: dbClient,
|
||||
Properties: resources.DefaultPropertyTypeLS,
|
||||
}
|
||||
activityReconciler := &controllers.ActivityReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
DBClient: dbClient,
|
||||
AccountV2: v2Account,
|
||||
}
|
||||
activities, discountSteps, discountRatios, err := controllers.RawParseRechargeConfig()
|
||||
if err != nil {
|
||||
@@ -160,20 +173,20 @@ func main() {
|
||||
} else {
|
||||
setupLog.Info("parse recharge config success", "activities", activities, "discountSteps", discountSteps, "discountRatios", discountRatios)
|
||||
accountReconciler.Activities = activities
|
||||
accountReconciler.RechargeStep = discountSteps
|
||||
accountReconciler.RechargeRatio = discountRatios
|
||||
accountReconciler.DefaultDiscount = types.RechargeDiscount{
|
||||
DiscountRates: discountRatios,
|
||||
DiscountSteps: discountSteps,
|
||||
}
|
||||
billingInfoQueryReconciler.Activities = activities
|
||||
billingInfoQueryReconciler.RechargeStep = discountSteps
|
||||
billingInfoQueryReconciler.RechargeRatio = discountRatios
|
||||
activityReconciler.Activity = activities
|
||||
billingInfoQueryReconciler.DefaultDiscount = types.RechargeDiscount{
|
||||
DiscountRates: discountRatios,
|
||||
DiscountSteps: discountSteps,
|
||||
}
|
||||
}
|
||||
setupManagerError := func(err error, controller string) {
|
||||
setupLog.Error(err, "unable to create controller", "controller", controller)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err = (activityReconciler).SetupWithManager(mgr, rateOpts); err != nil {
|
||||
setupManagerError(err, "Activity")
|
||||
}
|
||||
if err = (accountReconciler).SetupWithManager(mgr, rateOpts); err != nil {
|
||||
setupManagerError(err, "Account")
|
||||
}
|
||||
@@ -184,9 +197,10 @@ func main() {
|
||||
setupManagerError(err, "Payment")
|
||||
}
|
||||
if err = (&controllers.DebtReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
DBClient: dbClient,
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
DBClient: dbClient,
|
||||
AccountV2: v2Account,
|
||||
}).SetupWithManager(mgr, rateOpts); err != nil {
|
||||
setupManagerError(err, "Debt")
|
||||
}
|
||||
@@ -198,7 +212,7 @@ func main() {
|
||||
if os.Getenv("DISABLE_WEBHOOKS") == "true" {
|
||||
setupLog.Info("disable all webhooks")
|
||||
} else {
|
||||
mgr.GetWebhookServer().Register("/validate-v1-sealos-cloud", &webhook.Admission{Handler: &accountv1.DebtValidate{Client: mgr.GetClient()}})
|
||||
mgr.GetWebhookServer().Register("/validate-v1-sealos-cloud", &webhook.Admission{Handler: &accountv1.DebtValidate{Client: mgr.GetClient(), AccountV2: v2Account}})
|
||||
}
|
||||
|
||||
err = dbClient.InitDefaultPropertyTypeLS()
|
||||
@@ -218,6 +232,7 @@ func main() {
|
||||
Properties: resources.DefaultPropertyTypeLS,
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
AccountV2: v2Account,
|
||||
}).SetupWithManager(mgr, rateOpts); err != nil {
|
||||
setupManagerError(err, "Billing")
|
||||
}
|
||||
@@ -234,13 +249,6 @@ func main() {
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupManagerError(err, "Namespace")
|
||||
}
|
||||
if err = (&controllers.TransferReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
DBClient: dbClient,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupManagerError(err, "Transfer")
|
||||
}
|
||||
if err = (&controllers.NamespaceBillingHistoryReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
|
||||
+1
-14
@@ -398,6 +398,7 @@ github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHL
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c h1:rwmN+hgiyp8QyBqzdEX43lTjKAxaqCrYHaU5op5P9J8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
||||
@@ -434,11 +435,9 @@ github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
|
||||
github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
|
||||
@@ -540,35 +539,24 @@ golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
@@ -601,7 +589,6 @@ google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwS
|
||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=
|
||||
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
|
||||
@@ -17,30 +17,6 @@ package crypto
|
||||
import "testing"
|
||||
|
||||
func TestRechargeBalance(t *testing.T) {
|
||||
type args struct {
|
||||
rawBalance *string
|
||||
amount int64
|
||||
}
|
||||
a := ""
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
args: args{
|
||||
rawBalance: &a,
|
||||
amount: 100,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := RechargeBalance(tt.args.rawBalance, tt.args.amount); (err != nil) != tt.wantErr {
|
||||
t.Errorf("RechargeBalance() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
data, _ := DecryptInt64("")
|
||||
t.Log(data)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
// Copyright © 2024 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 cockroach
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/crypto"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
)
|
||||
|
||||
type Cockroach struct {
|
||||
DB *gorm.DB
|
||||
Localdb *gorm.DB
|
||||
LocalRegion *types.Region
|
||||
ZeroAccount *types.Account
|
||||
activities types.Activities
|
||||
//TODO need init
|
||||
defaultRechargeDiscount types.RechargeDiscount
|
||||
}
|
||||
|
||||
const (
|
||||
EnvLocalRegion = "LOCAL_REGION"
|
||||
EnvBaseBalance = "BASE_BALANCE"
|
||||
)
|
||||
|
||||
func (g *Cockroach) GetUser(ops *types.UserQueryOpts) (*types.RegionUserCr, error) {
|
||||
if err := checkOps(ops); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := &types.RegionUserCr{
|
||||
CrName: ops.Owner,
|
||||
}
|
||||
if ops.UID != uuid.Nil {
|
||||
query.UserUID = ops.UID
|
||||
}
|
||||
var user types.RegionUserCr
|
||||
if err := g.Localdb.Where(query).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func checkOps(ops *types.UserQueryOpts) error {
|
||||
if ops.Owner == "" && ops.UID == uuid.Nil {
|
||||
return fmt.Errorf("empty query opts")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) GetAccount(ops *types.UserQueryOpts) (*types.Account, error) {
|
||||
return g.getAccount(ops)
|
||||
}
|
||||
|
||||
func (g *Cockroach) getAccount(ops *types.UserQueryOpts) (*types.Account, error) {
|
||||
if ops.UID == uuid.Nil {
|
||||
user, err := g.GetUser(ops)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops.UID = user.UserUID
|
||||
}
|
||||
var account types.Account
|
||||
if err := g.DB.Where(types.Account{UserUID: ops.UID}).First(&account).Error; err != nil {
|
||||
if ops.IgnoreEmpty && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to search account from db: %w", err)
|
||||
}
|
||||
balance, err := crypto.DecryptInt64(account.EncryptBalance)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to descrypt balance: %v", err)
|
||||
}
|
||||
deductionBalance, err := crypto.DecryptInt64(account.EncryptDeductionBalance)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to descrypt deduction balance: %v", err)
|
||||
}
|
||||
account.Balance = balance
|
||||
account.DeductionBalance = deductionBalance
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) GetUserOauthProvider(ops *types.UserQueryOpts) (*types.OauthProvider, error) {
|
||||
if ops.UID == uuid.Nil {
|
||||
user, err := g.GetUser(ops)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %v", err)
|
||||
}
|
||||
ops.UID = user.UserUID
|
||||
}
|
||||
var provider types.OauthProvider
|
||||
if err := g.DB.Where(types.OauthProvider{UserUID: ops.UID}).First(&provider).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user oauth provider: %v", err)
|
||||
}
|
||||
return &provider, nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) updateBalance(tx *gorm.DB, ops *types.UserQueryOpts, amount int64, isDeduction, add bool) error {
|
||||
if ops.UID == uuid.Nil {
|
||||
user, err := g.GetUser(ops)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %v", err)
|
||||
}
|
||||
ops.UID = user.UserUID
|
||||
}
|
||||
var account types.Account
|
||||
//TODO update UserUid = ?
|
||||
if err := tx.Where(&types.Account{UserUID: ops.UID}).First(&account).Error; err != nil {
|
||||
return fmt.Errorf("failed to get account: %w", err)
|
||||
}
|
||||
|
||||
if err := g.updateWithAccount(isDeduction, add, &account, amount); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&account).Error; err != nil {
|
||||
return fmt.Errorf("failed to update account balance: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) updateWithAccount(isDeduction bool, add bool, account *types.Account, amount int64) error {
|
||||
var fieldToUpdate string
|
||||
if isDeduction {
|
||||
fieldToUpdate = account.EncryptDeductionBalance
|
||||
} else {
|
||||
fieldToUpdate = account.EncryptBalance
|
||||
}
|
||||
|
||||
currentBalance, err := crypto.DecryptInt64(fieldToUpdate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt balance: %w", err)
|
||||
}
|
||||
|
||||
if add {
|
||||
currentBalance += amount
|
||||
} else {
|
||||
currentBalance -= amount
|
||||
}
|
||||
|
||||
newEncryptBalance, err := crypto.EncryptInt64(currentBalance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt balance: %v", err)
|
||||
}
|
||||
if isDeduction {
|
||||
account.EncryptDeductionBalance = *newEncryptBalance
|
||||
account.DeductionBalance = currentBalance
|
||||
} else {
|
||||
account.EncryptBalance = *newEncryptBalance
|
||||
account.Balance = currentBalance
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) AddBalance(ops *types.UserQueryOpts, amount int64) error {
|
||||
return g.DB.Transaction(func(tx *gorm.DB) error {
|
||||
return g.updateBalance(tx, ops, amount, false, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Cockroach) ReduceBalance(ops *types.UserQueryOpts, amount int64) error {
|
||||
return g.DB.Transaction(func(tx *gorm.DB) error {
|
||||
return g.updateBalance(tx, ops, amount, false, false)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Cockroach) ReduceDeductionBalance(ops *types.UserQueryOpts, amount int64) error {
|
||||
return g.DB.Transaction(func(tx *gorm.DB) error {
|
||||
return g.updateBalance(tx, ops, amount, false, false)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Cockroach) AddDeductionBalance(ops *types.UserQueryOpts, amount int64) error {
|
||||
return g.DB.Transaction(func(tx *gorm.DB) error {
|
||||
return g.updateBalance(tx, ops, amount, true, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Cockroach) CreateAccount(ops *types.UserQueryOpts, account *types.Account) (*types.Account, error) {
|
||||
if ops.UID == uuid.Nil {
|
||||
user, err := g.GetUser(ops)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %v", err)
|
||||
}
|
||||
ops.UID = user.UserUID
|
||||
}
|
||||
account.UserUID = ops.UID
|
||||
if account.EncryptBalance == "" || account.EncryptDeductionBalance == "" {
|
||||
return nil, fmt.Errorf("empty encrypt balance")
|
||||
}
|
||||
|
||||
if err := g.DB.FirstOrCreate(account).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to create account: %w", err)
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) CreateErrorAccountCreate(account *types.Account, owner, errorMsg string) error {
|
||||
accountErrSave := &types.ErrorAccountCreate{
|
||||
Account: *account,
|
||||
UserCr: owner,
|
||||
ErrorTime: time.Now().UTC(),
|
||||
Message: errorMsg,
|
||||
RegionUserOwner: owner,
|
||||
RegionUID: g.LocalRegion.UID,
|
||||
}
|
||||
if err := g.DB.FirstOrCreate(accountErrSave, types.ErrorAccountCreate{UserCr: owner}).Error; err != nil {
|
||||
return fmt.Errorf("failed to create error account create error msg: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) CreateErrorPaymentCreate(payment types.Payment, errorMsg string) error {
|
||||
if err := g.DB.Create(&types.ErrorPaymentCreate{
|
||||
PaymentRaw: payment.PaymentRaw, Message: errorMsg, CreateTime: time.Now().UTC()}).Error; err != nil {
|
||||
return fmt.Errorf("failed to create error payment create error msg: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TransferAccountV1 account indicates the CRD value of the original account
|
||||
func (g *Cockroach) TransferAccountV1(owner string, account *types.Account) (*types.Account, error) {
|
||||
//transfer := &types.TransferAccountV1{}
|
||||
//// if existed, it indicates that the system has been migrated
|
||||
//err := g.DB.Where(&types.TransferAccountV1{RegionUID: g.LocalRegion.UID, RegionUserOwner: owner}).First(transfer).Error
|
||||
//if err == nil {
|
||||
// return nil, nil
|
||||
//}
|
||||
//if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// return nil, fmt.Errorf("failed to get transfer account: %w", err)
|
||||
//}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(transferAccountV1, g.LocalRegion.UID.String(), owner)); err == nil {
|
||||
return nil, nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("failed to get transfer account: %v", err)
|
||||
}
|
||||
|
||||
// if not existed, it indicates that the system has not been migrated
|
||||
|
||||
query := &types.UserQueryOpts{Owner: owner, IgnoreEmpty: true}
|
||||
accountV2, err := g.GetAccount(query)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err = g.saveNullUserRecord(types.NullUserRecord{
|
||||
CrName: owner,
|
||||
RegionID: g.LocalRegion.UID.String(),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to save null user record: %v", err)
|
||||
}
|
||||
//nullUser := &types.NullUserRecord{
|
||||
// CrName: owner,
|
||||
// RegionID: g.LocalRegion.UID.String(),
|
||||
//}
|
||||
//if err := g.DB.FirstOrCreate(nullUser, types.NullUserRecord{CrName: owner, RegionID: g.LocalRegion.UID.String()}).Error; err != nil {
|
||||
// return nil, fmt.Errorf("failed to create null user record: %v", err)
|
||||
//}
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get account: %v", err)
|
||||
}
|
||||
transfer := types.TransferAccountV1{
|
||||
RegionUID: g.LocalRegion.UID,
|
||||
RegionUserOwner: owner,
|
||||
}
|
||||
if accountV2 == nil {
|
||||
accountV2 = &types.Account{
|
||||
UserUID: query.UID,
|
||||
ActivityBonus: account.ActivityBonus,
|
||||
EncryptDeductionBalance: account.EncryptDeductionBalance,
|
||||
EncryptBalance: account.EncryptBalance,
|
||||
Balance: account.Balance,
|
||||
DeductionBalance: account.DeductionBalance,
|
||||
CreateRegionID: g.LocalRegion.UID.String(),
|
||||
//TODO need init
|
||||
CreatedAt: account.CreatedAt,
|
||||
}
|
||||
if err := g.DB.FirstOrCreate(accountV2).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to create account: %w", err)
|
||||
}
|
||||
} else {
|
||||
if accountV2.CreatedAt.After(account.CreatedAt) {
|
||||
accountV2.CreatedAt = account.CreatedAt
|
||||
}
|
||||
if err := g.updateWithAccount(true, true, accountV2, account.DeductionBalance); err != nil {
|
||||
return nil, fmt.Errorf("failed to update account DeductionBalance: %v", err)
|
||||
}
|
||||
if err := g.updateWithAccount(false, true, accountV2, account.Balance); err != nil {
|
||||
return nil, fmt.Errorf("failed to update account Balance: %v", err)
|
||||
}
|
||||
if err := g.DB.Save(accountV2).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to save account: %v", err)
|
||||
}
|
||||
transfer.Exist = true
|
||||
}
|
||||
|
||||
transfer.Account = *accountV2
|
||||
//if err := g.DB.Save(&transfer).Error; err != nil {
|
||||
// return fmt.Errorf("failed to save transfer account: %v", err)
|
||||
//}
|
||||
if err := g.saveTransferAccountV1(transfer); err != nil {
|
||||
return nil, fmt.Errorf("failed to save transfer account: %v", err)
|
||||
}
|
||||
return accountV2, err
|
||||
}
|
||||
|
||||
var (
|
||||
transferV1toV2 = "transferv1tov2"
|
||||
transferAccountV1 = filepath.Join(transferV1toV2, "transfer_account_v1")
|
||||
transferV1Exist = filepath.Join(transferV1toV2, "transfer_account_v1_exist")
|
||||
nullUserRecord = filepath.Join(transferV1toV2, "null_user_record")
|
||||
)
|
||||
|
||||
func (g *Cockroach) saveTransferAccountV1(transfer types.TransferAccountV1) error {
|
||||
name := transfer.RegionUserOwner
|
||||
savePath := filepath.Join(transferAccountV1, transfer.RegionUID.String(), name)
|
||||
file, err := os.Create(savePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
if !transfer.Exist {
|
||||
return nil
|
||||
}
|
||||
saveExistPath := filepath.Join(transferV1Exist, transfer.RegionUID.String(), name)
|
||||
existFile, err := os.Create(saveExistPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %v", err)
|
||||
}
|
||||
return existFile.Close()
|
||||
}
|
||||
|
||||
func (g *Cockroach) saveNullUserRecord(nullUser types.NullUserRecord) error {
|
||||
savePath := filepath.Join(nullUserRecord, nullUser.RegionID, nullUser.CrName)
|
||||
file, err := os.Create(savePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to create file: %v", err)
|
||||
}
|
||||
return file.Close()
|
||||
}
|
||||
|
||||
func (g *Cockroach) Payment(payment *types.Payment) error {
|
||||
return g.payment(payment, true)
|
||||
}
|
||||
|
||||
func (g *Cockroach) SavePayment(payment *types.Payment) error {
|
||||
return g.payment(payment, false)
|
||||
}
|
||||
|
||||
func (g *Cockroach) payment(payment *types.Payment, updateBalance bool) error {
|
||||
if payment.ID == "" {
|
||||
id, err := gonanoid.New(12)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate payment id: %v", err)
|
||||
}
|
||||
payment.ID = id
|
||||
}
|
||||
if payment.CreatedAt.IsZero() {
|
||||
payment.CreatedAt = time.Now()
|
||||
}
|
||||
if payment.RegionUID == uuid.Nil {
|
||||
payment.RegionUID = g.LocalRegion.UID
|
||||
}
|
||||
if payment.UserUID == uuid.Nil {
|
||||
if payment.RegionUserOwner == "" {
|
||||
return fmt.Errorf("empty payment owner and user")
|
||||
}
|
||||
user, err := g.GetUser(&types.UserQueryOpts{Owner: payment.RegionUserOwner})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %v", err)
|
||||
}
|
||||
payment.UserUID = user.UserUID
|
||||
}
|
||||
|
||||
return g.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := g.DB.First(&types.Payment{ID: payment.ID}).Error; err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := g.DB.Create(payment).Error; err != nil {
|
||||
return fmt.Errorf("failed to save payment: %w", err)
|
||||
}
|
||||
if updateBalance {
|
||||
if err := g.AddBalance(&types.UserQueryOpts{UID: payment.UserUID}, payment.Amount+payment.Gift); err != nil {
|
||||
return fmt.Errorf("failed to add balance: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// NewAccount create a new account
|
||||
func (g *Cockroach) NewAccount(ops *types.UserQueryOpts) (*types.Account, error) {
|
||||
if ops.UID == uuid.Nil {
|
||||
user, err := g.GetUser(ops)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops.UID = user.UserUID
|
||||
}
|
||||
account := &types.Account{
|
||||
UserUID: ops.UID,
|
||||
EncryptDeductionBalance: g.ZeroAccount.EncryptDeductionBalance,
|
||||
EncryptBalance: g.ZeroAccount.EncryptBalance,
|
||||
Balance: g.ZeroAccount.Balance,
|
||||
DeductionBalance: g.ZeroAccount.DeductionBalance,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := g.DB.FirstOrCreate(account).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to create account: %w", err)
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) GetUserAccountRechargeDiscount(ops *types.UserQueryOpts) (*types.RechargeDiscount, error) {
|
||||
userID := ops.UID
|
||||
if userID == uuid.Nil {
|
||||
user, err := g.GetUser(ops)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user %v: %v", ops, err)
|
||||
}
|
||||
userID = user.UserUID
|
||||
}
|
||||
var userActivities []types.UserActivity
|
||||
if !g.DB.Migrator().HasTable("UserActivities") {
|
||||
return &g.defaultRechargeDiscount, nil
|
||||
}
|
||||
if err := g.DB.Table("UserActivities").Where(types.UserActivity{
|
||||
UserID: userID,
|
||||
}).Find(userActivities).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return &g.defaultRechargeDiscount, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user activities: %w", err)
|
||||
}
|
||||
if len(userActivities) == 0 {
|
||||
return &g.defaultRechargeDiscount, nil
|
||||
}
|
||||
for _, activity := range userActivities {
|
||||
currentPhase := activity.CurrentPhase
|
||||
var userPhase types.UserPhase
|
||||
err := g.DB.Table("UserPhase").Where(types.UserPhase{
|
||||
UserActivityID: activity.UserID,
|
||||
Name: currentPhase,
|
||||
}).First(&userPhase).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user %v phase: %v", ops, err)
|
||||
}
|
||||
for _, phase := range g.activities[activity.Name].Phases {
|
||||
if phase.ID == userPhase.ID {
|
||||
limitTime, err := time.ParseDuration(phase.RechargeDiscount.LimitDuration)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get limitTime %s: %v", phase.RechargeDiscount.LimitDuration, err)
|
||||
}
|
||||
if userPhase.RechargeNums >= phase.RechargeDiscount.LimitTimes || userPhase.EndTime.Add(limitTime).After(time.Now()) {
|
||||
return &g.defaultRechargeDiscount, nil
|
||||
}
|
||||
return &phase.RechargeDiscount.RechargeDiscount, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return &g.defaultRechargeDiscount, nil
|
||||
}
|
||||
|
||||
const (
|
||||
BaseUnit = 1_000_000
|
||||
MinBalance = 10 * BaseUnit
|
||||
DefaultBaseBalance = 5 * BaseUnit
|
||||
)
|
||||
|
||||
var (
|
||||
BaseBalance = int64(DefaultBaseBalance)
|
||||
EncryptBaseBalance string
|
||||
)
|
||||
|
||||
func (g *Cockroach) TransferAccount(from, to *types.UserQueryOpts, amount int64) error {
|
||||
if from.UID == uuid.Nil {
|
||||
fromUser, err := g.GetUser(from)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %v", err)
|
||||
}
|
||||
from.UID = fromUser.UserUID
|
||||
}
|
||||
if to.UID == uuid.Nil {
|
||||
toUser, err := g.GetUser(to)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %v", err)
|
||||
}
|
||||
to.UID = toUser.UserUID
|
||||
}
|
||||
err := g.DB.Transaction(func(tx *gorm.DB) error {
|
||||
sender, err := g.GetAccount(&types.UserQueryOpts{UID: from.UID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get sender account: %w", err)
|
||||
}
|
||||
if sender.Balance < sender.DeductionBalance+amount+MinBalance+sender.ActivityBonus {
|
||||
return fmt.Errorf("insufficient balance in sender account, the transferable amount is: %d", sender.Balance-sender.DeductionBalance-MinBalance-sender.ActivityBonus)
|
||||
}
|
||||
|
||||
if err = g.updateBalance(tx, &types.UserQueryOpts{UID: from.UID}, -amount, false, true); err != nil {
|
||||
return fmt.Errorf("failed to update sender balance: %w", err)
|
||||
}
|
||||
if err = g.updateBalance(tx, &types.UserQueryOpts{UID: to.UID}, amount, false, true); err != nil {
|
||||
return fmt.Errorf("failed to update receiver balance: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewCockRoach(globalURI, localURI string) (*Cockroach, error) {
|
||||
dbLogger := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
LogLevel: logger.Error,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
Colorful: true,
|
||||
})
|
||||
db, err := gorm.Open(postgres.Open(globalURI), &gorm.Config{
|
||||
Logger: dbLogger,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open global url %s : %v", globalURI, err)
|
||||
}
|
||||
localdb, err := gorm.Open(postgres.Open(localURI), &gorm.Config{
|
||||
Logger: dbLogger,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open local url %s : %v", localURI, err)
|
||||
}
|
||||
baseBalance, err := crypto.DecryptInt64(os.Getenv(EnvBaseBalance))
|
||||
if err == nil {
|
||||
BaseBalance = baseBalance
|
||||
}
|
||||
newEncryptBalance, err := crypto.EncryptInt64(BaseBalance)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt zero value")
|
||||
}
|
||||
newEncryptDeductionBalance, err := crypto.EncryptInt64(0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt zero value")
|
||||
}
|
||||
if err := CreateTableIfNotExist(db, types.Account{}, types.ErrorAccountCreate{}, types.ErrorPaymentCreate{}, types.Payment{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cockroach := &Cockroach{DB: db, Localdb: localdb, ZeroAccount: &types.Account{EncryptBalance: *newEncryptBalance, EncryptDeductionBalance: *newEncryptDeductionBalance, Balance: baseBalance, DeductionBalance: 0}}
|
||||
//TODO region with local
|
||||
localRegionStr := os.Getenv(EnvLocalRegion)
|
||||
if localRegionStr != "" {
|
||||
cockroach.LocalRegion = &types.Region{
|
||||
UID: uuid.MustParse(localRegionStr),
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("empty local region")
|
||||
}
|
||||
return cockroach, nil
|
||||
}
|
||||
|
||||
func CreateTableIfNotExist(db *gorm.DB, tables ...interface{}) error {
|
||||
for i := range tables {
|
||||
table := tables[i]
|
||||
if !db.Migrator().HasTable(table) {
|
||||
if err := db.AutoMigrate(table); err != nil {
|
||||
return fmt.Errorf("failed to auto migrate table: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close db connection
|
||||
func (g *Cockroach) Close() error {
|
||||
db, err := g.DB.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get db: %w", err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close db: %w", err)
|
||||
}
|
||||
db, err = g.Localdb.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get localdb: %w", err)
|
||||
}
|
||||
return db.Close()
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright © 2024 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 cockroach
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
)
|
||||
|
||||
type TestConfig struct {
|
||||
RegionID string
|
||||
V2GlobalDBURI string
|
||||
V2LocalDBURI string
|
||||
}
|
||||
|
||||
var testConfig = TestConfig{}
|
||||
|
||||
func TestCockroach_GetUserOauthProvider(t *testing.T) {
|
||||
os.Setenv("LOCAL_REGION", testConfig.RegionID)
|
||||
ck, err := NewCockRoach(testConfig.V2GlobalDBURI, testConfig.V2LocalDBURI)
|
||||
if err != nil {
|
||||
t.Errorf("NewCockRoach() error = %v", err)
|
||||
return
|
||||
}
|
||||
defer ck.Close()
|
||||
|
||||
provider, err := ck.GetUserOauthProvider(&types.UserQueryOpts{
|
||||
Owner: "xxx",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("GetUserOauthProvider() error = %v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("provider: %+v", provider)
|
||||
}
|
||||
@@ -18,6 +18,10 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/database/cockroach"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/common"
|
||||
@@ -47,9 +51,11 @@ type Account interface {
|
||||
UpdateBillingStatus(orderID string, status resources.BillingStatus) error
|
||||
GetUpdateTimeForCategoryAndPropertyFromMetering(category string, property string) (time.Time, error)
|
||||
GetAllPricesMap() (map[string]resources.Price, error)
|
||||
GetAllPayment() ([]resources.Billing, error)
|
||||
InitDefaultPropertyTypeLS() error
|
||||
SavePropertyTypes(types []resources.PropertyType) error
|
||||
GetBillingCount(accountType common.Type, startTime, endTime time.Time) (count, amount int64, err error)
|
||||
//GetNodePortAmount(owner string, endTime time.Time) (int64, error)
|
||||
GenerateBillingData(startTime, endTime time.Time, prols *resources.PropertyTypeLS, namespaces []string, owner string) (orderID []string, amount int64, err error)
|
||||
InsertMonitor(ctx context.Context, monitors ...*resources.Monitor) error
|
||||
GetDistinctMonitorCombinations(startTime, endTime time.Time) ([]resources.Monitor, error)
|
||||
@@ -58,6 +64,17 @@ type Account interface {
|
||||
Creator
|
||||
}
|
||||
|
||||
type BillingRecordQuery struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
StartTime v1.Time `json:"startTime"`
|
||||
EndTime v1.Time `json:"endTime"`
|
||||
OrderID string `json:"orderID,omitempty"`
|
||||
Type common.Type `json:"type"`
|
||||
AppType string `json:"appType,omitempty"`
|
||||
}
|
||||
|
||||
type Traffic interface {
|
||||
GetTrafficSentBytes(startTime, endTime time.Time, namespace string, _type uint8, name string) (int64, error)
|
||||
GetTrafficRecvBytes(startTime, endTime time.Time, namespace string, _type uint8, name string) (int64, error)
|
||||
@@ -66,6 +83,26 @@ type Traffic interface {
|
||||
GetPodTrafficRecvBytes(startTime, endTime time.Time, namespace string, name string) (int64, error)
|
||||
}
|
||||
|
||||
type AccountV2 interface {
|
||||
Close() error
|
||||
GetUser(user *types.UserQueryOpts) (*types.RegionUserCr, error)
|
||||
GetAccount(user *types.UserQueryOpts) (*types.Account, error)
|
||||
GetUserOauthProvider(ops *types.UserQueryOpts) (*types.OauthProvider, error)
|
||||
AddBalance(user *types.UserQueryOpts, balance int64) error
|
||||
ReduceBalance(ops *types.UserQueryOpts, amount int64) error
|
||||
ReduceDeductionBalance(ops *types.UserQueryOpts, amount int64) error
|
||||
NewAccount(user *types.UserQueryOpts) (*types.Account, error)
|
||||
Payment(payment *types.Payment) error
|
||||
SavePayment(payment *types.Payment) error
|
||||
CreateErrorPaymentCreate(payment types.Payment, errorMsg string) error
|
||||
CreateAccount(ops *types.UserQueryOpts, account *types.Account) (*types.Account, error)
|
||||
CreateErrorAccountCreate(account *types.Account, owner, errorMsg string) error
|
||||
TransferAccount(from, to *types.UserQueryOpts, amount int64) error
|
||||
TransferAccountV1(owner string, account *types.Account) (*types.Account, error)
|
||||
GetUserAccountRechargeDiscount(user *types.UserQueryOpts) (*types.RechargeDiscount, error)
|
||||
AddDeductionBalance(user *types.UserQueryOpts, balance int64) error
|
||||
}
|
||||
|
||||
type Creator interface {
|
||||
CreateBillingIfNotExist() error
|
||||
//suffix by day, eg: monitor_20200101
|
||||
@@ -84,10 +121,18 @@ type MeteringOwnerTimeResult struct {
|
||||
//}
|
||||
|
||||
const (
|
||||
MongoURI = "MONGO_URI"
|
||||
TrafficMongoURI = "TRAFFIC_MONGO_URI"
|
||||
MongoURI = "MONGO_URI"
|
||||
GlobalCockroachURI = "GLOBAL_COCKROACH_URI"
|
||||
LocalCockroachURI = "LOCAL_COCKROACH_URI"
|
||||
TrafficMongoURI = "TRAFFIC_MONGO_URI"
|
||||
//MongoUsername = "MONGO_USERNAME"
|
||||
//MongoPassword = "MONGO_PASSWORD"
|
||||
//RetentionDay = "RETENTION_DAY"
|
||||
//PermanentRetention = "PERMANENT_RETENTION"
|
||||
)
|
||||
|
||||
var _ = AccountV2(&cockroach.Cockroach{})
|
||||
|
||||
func NewAccountV2(globalURI, localURI string) (AccountV2, error) {
|
||||
return cockroach.NewCockRoach(globalURI, localURI)
|
||||
}
|
||||
|
||||
@@ -326,6 +326,24 @@ func (m *mongoDB) GetAllPricesMap() (map[string]resources.Price, error) {
|
||||
return pricesMap, nil
|
||||
}
|
||||
|
||||
func (m *mongoDB) GetAllPayment() ([]resources.Billing, error) {
|
||||
filter := bson.M{
|
||||
"type": 1,
|
||||
"payment.amount": bson.M{"$gt": 0},
|
||||
}
|
||||
|
||||
cursor, err := m.getBillingCollection().Find(context.Background(), filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get all payment error: %v", err)
|
||||
}
|
||||
|
||||
var payments []resources.Billing
|
||||
if err = cursor.All(context.Background(), &payments); err != nil {
|
||||
return nil, fmt.Errorf("get all payment error: %v", err)
|
||||
}
|
||||
return payments, nil
|
||||
}
|
||||
|
||||
func (m *mongoDB) InitDefaultPropertyTypeLS() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -804,37 +822,78 @@ func (m *mongoDB) QueryBillingRecords(billingRecordQuery *accountv1.BillingRecor
|
||||
return nil
|
||||
}
|
||||
|
||||
//func (m *mongoDB) GetNodePortAmount(owner string, endTime time.Time) (int64, error) {
|
||||
// filter := bson.M{
|
||||
// "owner": owner,
|
||||
// "time": bson.M{
|
||||
// "$lte": endTime,
|
||||
// },
|
||||
// "type": accountv1.Consumption,
|
||||
// "used_amount.4": bson.M{"$ne": 0},
|
||||
// }
|
||||
//
|
||||
// cursor, err := m.getBillingCollection().Find(context.Background(), filter)
|
||||
// if err != nil {
|
||||
// return 0, fmt.Errorf("failed to execute aggregate query: %w", err)
|
||||
// }
|
||||
// defer cursor.Close(context.Background())
|
||||
//
|
||||
// var billings []resources.Billing
|
||||
// if err := cursor.All(context.Background(), &billings); err != nil {
|
||||
// return 0, fmt.Errorf("failed to decode all billing record: %w", err)
|
||||
// }
|
||||
// amountTotal := int64(0)
|
||||
// for i := range billings {
|
||||
// for j := range billings[i].AppCosts {
|
||||
// amount := billings[i].AppCosts[j].UsedAmount[4]
|
||||
// if amount > 0 {
|
||||
// amountTotal += amount
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return amountTotal, nil
|
||||
//
|
||||
//}
|
||||
|
||||
func (m *mongoDB) GetBillingCount(accountType common.Type, startTime, endTime time.Time) (count, amount int64, err error) {
|
||||
filter := bson.M{
|
||||
"type": accountType,
|
||||
"time": bson.M{
|
||||
"$gte": startTime,
|
||||
"$lte": endTime,
|
||||
pipeline := bson.A{
|
||||
bson.M{
|
||||
"$match": bson.M{
|
||||
"type": accountType,
|
||||
"time": bson.M{
|
||||
"$gte": startTime,
|
||||
"$lte": endTime,
|
||||
},
|
||||
},
|
||||
},
|
||||
bson.M{
|
||||
"$group": bson.M{
|
||||
"_id": nil,
|
||||
"count": bson.M{"$sum": 1},
|
||||
"amount": bson.M{"$sum": "$amount"},
|
||||
},
|
||||
},
|
||||
}
|
||||
cursor, err := m.getBillingCollection().Find(context.Background(), filter)
|
||||
|
||||
cursor, err := m.getBillingCollection().Aggregate(context.Background(), pipeline)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer cursor.Close(context.Background())
|
||||
var accountBalanceList []AccountBalanceSpecBSON
|
||||
err = cursor.All(context.Background(), &accountBalanceList)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to decode all billing record: %w", err)
|
||||
|
||||
var result struct {
|
||||
Count int64 `bson:"count"`
|
||||
Amount int64 `bson:"amount"`
|
||||
}
|
||||
for i := range accountBalanceList {
|
||||
count++
|
||||
amount += accountBalanceList[i].Amount
|
||||
|
||||
if cursor.Next(context.Background()) {
|
||||
if err := cursor.Decode(&result); err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to decode aggregation result: %w", err)
|
||||
}
|
||||
}
|
||||
//for cursor.Next(context.Background()) {
|
||||
// var accountBalance AccountBalanceSpecBSON
|
||||
// if err := cursor.Decode(&accountBalance); err != nil {
|
||||
// return 0, 0, err
|
||||
// }
|
||||
// count++
|
||||
// amount += accountBalance.Amount
|
||||
//}
|
||||
return
|
||||
|
||||
return result.Count, result.Amount, nil
|
||||
}
|
||||
|
||||
func (m *mongoDB) getMeteringCollection() *mongo.Collection {
|
||||
|
||||
@@ -22,6 +22,7 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-logr/logr v1.2.4
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/labring/sealos/controllers/account v0.0.0-00010101000000-000000000000
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||
github.com/minio/minio-go/v7 v7.0.64
|
||||
@@ -31,9 +32,10 @@ require (
|
||||
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/sync v0.4.0
|
||||
golang.org/x/time v0.3.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
k8s.io/api v0.27.4
|
||||
k8s.io/apimachinery v0.27.4
|
||||
k8s.io/client-go v0.27.4
|
||||
@@ -70,9 +72,13 @@ require (
|
||||
github.com/google/gnostic v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // 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/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/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.16.7 // indirect
|
||||
@@ -113,6 +119,7 @@ require (
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.8.0 // indirect
|
||||
golang.org/x/sync v0.4.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
|
||||
@@ -129,7 +129,17 @@ 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=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
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=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
@@ -417,6 +427,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=
|
||||
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
||||
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.25.6 h1:LwDY2H6kD/3R8TekJYYaJWOdekNdXDO44eVpX6sNtJA=
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
package pay
|
||||
|
||||
type Interface interface {
|
||||
CreatePayment(amount int64, user string) (string, string, error)
|
||||
CreatePayment(amount int64, user, describe string) (string, string, error)
|
||||
GetPaymentDetails(sessionID string) (string, int64, error)
|
||||
ExpireSession(payment string) error
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func init() {
|
||||
Currency = currency
|
||||
}
|
||||
|
||||
func (s StripePayment) CreatePayment(amount int64, _ string) (string, string, error) {
|
||||
func (s StripePayment) CreatePayment(amount int64, _, _ string) (string, string, error) {
|
||||
session, err := CreateCheckoutSession(amount, Currency, DefaultURL+os.Getenv(stripeSuccessPostfix), DefaultURL+os.Getenv(stripeCancelPostfix))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright © 2024 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 pay
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateCheckoutSession(t *testing.T) {
|
||||
stripe, err := CreateCheckoutSession(2000, "cny", "http://localhost:8080", "http://localhost:8080")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Log(stripe.ID)
|
||||
}
|
||||
@@ -16,9 +16,9 @@ package pay
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (w WechatPayment) CreatePayment(amount int64, user string) (string, string, error) {
|
||||
func (w WechatPayment) CreatePayment(amount int64, user, describe string) (string, string, error) {
|
||||
tradeNO := GetRandomString(32)
|
||||
codeURL, err := WechatPay(amount, user, tradeNO, "", "")
|
||||
codeURL, err := WechatPay(amount, user, tradeNO, describe, "")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
+175
-120
@@ -16,144 +16,199 @@ package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RechargeDiscount struct {
|
||||
LimitTimes int64 `json:"limitTimes,omitempty"`
|
||||
LimitDuration string `json:"limitDuration,omitempty"`
|
||||
DiscountRates []float64 `json:"discountRates,omitempty"`
|
||||
DiscountSteps []int64 `json:"discountSteps,omitempty"`
|
||||
SpecialDiscount map[int64]int64 `json:"specialDiscount,omitempty"`
|
||||
type Activity struct {
|
||||
gorm.Model
|
||||
ActivityType string `gorm:"uniqueIndex"`
|
||||
PhaseOrder string
|
||||
Phases []Phase
|
||||
}
|
||||
|
||||
type Phase struct {
|
||||
Name string `json:"name"`
|
||||
GiveAmount int64 `json:"giveAmount"`
|
||||
RechargeDiscount RechargeDiscount `json:",inline"`
|
||||
gorm.Model
|
||||
ActivityID uint `gorm:"index"`
|
||||
Name string
|
||||
GiveAmount int64
|
||||
RechargeDiscount RechargeDiscountInfo `gorm:"embedded"`
|
||||
}
|
||||
|
||||
type Activity struct {
|
||||
ActivityType string `json:"activityType"`
|
||||
Phases map[string]Phase `json:"phases"`
|
||||
PhaseOrder string `json:"phaseOrder"`
|
||||
type RechargeDiscount struct {
|
||||
DiscountRates []float64 `json:"discountRates"`
|
||||
DiscountSteps []int64 `json:"discountSteps"`
|
||||
SpecialDiscount map[int64]int64 `json:"specialDiscount" gorm:"type:jsonb"`
|
||||
}
|
||||
|
||||
type RechargeDiscountInfo struct {
|
||||
LimitTimes int64 `gorm:"default:0"`
|
||||
LimitDuration string
|
||||
RechargeDiscount
|
||||
}
|
||||
|
||||
type UserActivity struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
UserID uuid.UUID `gorm:"index"`
|
||||
CurrentPhase string
|
||||
ActivityID uint `gorm:"index"`
|
||||
Phases []UserPhase
|
||||
}
|
||||
|
||||
type UserPhase struct {
|
||||
gorm.Model
|
||||
UserActivityID uuid.UUID `gorm:"index"`
|
||||
Name string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
RechargeNums int64
|
||||
GiveAmount int64
|
||||
}
|
||||
|
||||
//type RechargeDiscount struct {
|
||||
// LimitTimes int64 `json:"limitTimes,omitempty"`
|
||||
// LimitDuration string `json:"limitDuration,omitempty"`
|
||||
// DiscountRates []float64 `json:"discountRates,omitempty"`
|
||||
// DiscountSteps []int64 `json:"discountSteps,omitempty"`
|
||||
// SpecialDiscount map[int64]int64 `json:"specialDiscount,omitempty"`
|
||||
//}
|
||||
//
|
||||
//type Phase struct {
|
||||
// Name string `json:"name"`
|
||||
// GiveAmount int64 `json:"giveAmount"`
|
||||
// RechargeDiscount RechargeDiscount `json:",inline"`
|
||||
//}
|
||||
//
|
||||
//type Activity struct {
|
||||
// ActivityType string `json:"activityType"`
|
||||
// Phases map[string]Phase `json:"phases"`
|
||||
// PhaseOrder string `json:"phaseOrder"`
|
||||
//}
|
||||
//
|
||||
//type UserPhase struct {
|
||||
// Name string `json:"name"`
|
||||
// //RFC339 time format
|
||||
// StartTime time.Time `json:"startTime"`
|
||||
// EndTime time.Time `json:"endTime"`
|
||||
// RechargeNums int64 `json:"rechargeNums"`
|
||||
// GiveAmount int64 `json:"giveAmount"`
|
||||
//}
|
||||
//
|
||||
//type UserActivity struct {
|
||||
// CurrentPhase string `json:"currentPhase"`
|
||||
// Phases map[string]*UserPhase
|
||||
//}
|
||||
|
||||
type Activities map[string]*Activity
|
||||
|
||||
type UserActivities map[string]*UserActivity
|
||||
|
||||
type UserPhase struct {
|
||||
Name string `json:"name"`
|
||||
//RFC339 time format
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
RechargeNums int64 `json:"rechargeNums"`
|
||||
GiveAmount int64 `json:"giveAmount"`
|
||||
}
|
||||
//func ParseUserActivities(annotations map[string]string) (UserActivities, error) {
|
||||
// userActivities := make(map[string]*UserActivity)
|
||||
//
|
||||
// for key, value := range annotations {
|
||||
// parts := strings.Split(key, ".")
|
||||
//
|
||||
// if len(parts) == 3 && parts[0] == "activity" && parts[2] == "current-phase" {
|
||||
// if _, exists := userActivities[parts[1]]; !exists {
|
||||
// userActivities[parts[1]] = &UserActivity{
|
||||
// Phases: make(map[string]*UserPhase),
|
||||
// }
|
||||
// }
|
||||
// userActivities[parts[1]].CurrentPhase = value
|
||||
// }
|
||||
//
|
||||
// if len(parts) == 4 && parts[0] == "activity" {
|
||||
// activityType := parts[1]
|
||||
// phase := parts[2]
|
||||
//
|
||||
// if _, exists := userActivities[activityType]; !exists {
|
||||
// userActivities[activityType] = &UserActivity{
|
||||
// Phases: make(map[string]*UserPhase),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if _, exists := userActivities[activityType].Phases[phase]; !exists {
|
||||
// userActivities[activityType].Phases[phase] = &UserPhase{Name: phase}
|
||||
// }
|
||||
// var err error
|
||||
// switch parts[3] {
|
||||
// case "startTime":
|
||||
// fmt.Println(value)
|
||||
// userActivities[activityType].Phases[phase].StartTime, err = time.Parse(time.RFC3339, value)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("parse start time failed: %w", err)
|
||||
// }
|
||||
// case "endTime":
|
||||
// userActivities[activityType].Phases[phase].EndTime, err = time.Parse(time.RFC3339, value)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("parse end time failed: %w", err)
|
||||
// }
|
||||
// case "rechargeNums":
|
||||
// userActivities[activityType].Phases[phase].RechargeNums, err = strconv.ParseInt(value, 10, 64)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("parse %s to recharge nums failed: %w", value, err)
|
||||
// }
|
||||
// case "giveAmount":
|
||||
// userActivities[activityType].Phases[phase].GiveAmount, err = strconv.ParseInt(value, 10, 64)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("parse %s to give amount failed: %w", value, err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return userActivities, nil
|
||||
//}
|
||||
|
||||
type UserActivity struct {
|
||||
CurrentPhase string `json:"currentPhase"`
|
||||
Phases map[string]*UserPhase
|
||||
}
|
||||
|
||||
func ParseUserActivities(annotations map[string]string) (UserActivities, error) {
|
||||
userActivities := make(map[string]*UserActivity)
|
||||
|
||||
for key, value := range annotations {
|
||||
parts := strings.Split(key, ".")
|
||||
|
||||
if len(parts) == 3 && parts[0] == "activity" && parts[2] == "current-phase" {
|
||||
if _, exists := userActivities[parts[1]]; !exists {
|
||||
userActivities[parts[1]] = &UserActivity{
|
||||
Phases: make(map[string]*UserPhase),
|
||||
}
|
||||
}
|
||||
userActivities[parts[1]].CurrentPhase = value
|
||||
}
|
||||
|
||||
if len(parts) == 4 && parts[0] == "activity" {
|
||||
activityType := parts[1]
|
||||
phase := parts[2]
|
||||
|
||||
if _, exists := userActivities[activityType]; !exists {
|
||||
userActivities[activityType] = &UserActivity{
|
||||
Phases: make(map[string]*UserPhase),
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := userActivities[activityType].Phases[phase]; !exists {
|
||||
userActivities[activityType].Phases[phase] = &UserPhase{Name: phase}
|
||||
}
|
||||
var err error
|
||||
switch parts[3] {
|
||||
case "startTime":
|
||||
fmt.Println(value)
|
||||
userActivities[activityType].Phases[phase].StartTime, err = time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse start time failed: %w", err)
|
||||
}
|
||||
case "endTime":
|
||||
userActivities[activityType].Phases[phase].EndTime, err = time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse end time failed: %w", err)
|
||||
}
|
||||
case "rechargeNums":
|
||||
userActivities[activityType].Phases[phase].RechargeNums, err = strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse %s to recharge nums failed: %w", value, err)
|
||||
}
|
||||
case "giveAmount":
|
||||
userActivities[activityType].Phases[phase].GiveAmount, err = strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse %s to give amount failed: %w", value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return userActivities, nil
|
||||
}
|
||||
|
||||
func SetUserPhaseRechargeTimes(annotations map[string]string, activityType string, phase string, rechargeNums int64) map[string]string {
|
||||
annotations[fmt.Sprintf("activity.%s.%s.rechargeNums", activityType, phase)] = fmt.Sprintf("%d", rechargeNums)
|
||||
return annotations
|
||||
}
|
||||
//func SetUserPhaseRechargeTimes(annotations map[string]string, activityType string, phase string, rechargeNums int64) map[string]string {
|
||||
// annotations[fmt.Sprintf("activity.%s.%s.rechargeNums", activityType, phase)] = fmt.Sprintf("%d", rechargeNums)
|
||||
// return annotations
|
||||
//}
|
||||
|
||||
func SetUserPhaseGiveAmount(annotations map[string]string, activityType string, phase string, giveAmount int64) map[string]string {
|
||||
annotations[fmt.Sprintf("activity.%s.%s.giveAmount", activityType, phase)] = fmt.Sprintf("%d", giveAmount)
|
||||
return annotations
|
||||
}
|
||||
|
||||
func GetUserActivityDiscount(activities Activities, userActivities *UserActivities) (activityType string, returnPhase *Phase, returnErr error) {
|
||||
if activities == nil || userActivities == nil {
|
||||
returnErr = fmt.Errorf("activities is nil")
|
||||
return
|
||||
}
|
||||
for aType, userActivity := range *userActivities {
|
||||
activity := activities[aType]
|
||||
phase, exists := activity.Phases[userActivity.CurrentPhase]
|
||||
if !exists {
|
||||
returnErr = fmt.Errorf("phase %s not exist", userActivity.CurrentPhase)
|
||||
return
|
||||
}
|
||||
|
||||
if phase.RechargeDiscount.LimitTimes > 0 && userActivity.Phases[userActivity.CurrentPhase].RechargeNums >= phase.RechargeDiscount.LimitTimes {
|
||||
return
|
||||
}
|
||||
if phase.RechargeDiscount.LimitDuration != "" {
|
||||
duration, err := time.ParseDuration(phase.RechargeDiscount.LimitDuration)
|
||||
if err != nil {
|
||||
returnErr = fmt.Errorf("parse duration failed: %w", err)
|
||||
return
|
||||
}
|
||||
if time.Now().After(userActivity.Phases[userActivity.CurrentPhase].EndTime.Add(duration)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
return aType, &phase, nil
|
||||
}
|
||||
returnErr = fmt.Errorf("user activity not exist")
|
||||
return
|
||||
}
|
||||
//func GetUserActivityDiscount(activities Activities, userActivities *UserActivities) (activityType string, returnPhase *Phase, returnErr error) {
|
||||
// if activities == nil || userActivities == nil {
|
||||
// returnErr = fmt.Errorf("activities is nil")
|
||||
// return
|
||||
// }
|
||||
// for aType, userActivity := range *userActivities {
|
||||
// activity := activities[aType]
|
||||
// for _, phase := range activity.Phases {
|
||||
// if phase.Name == userActivity.CurrentPhase {
|
||||
//
|
||||
// for _, userPhase := range userActivity.Phases {
|
||||
// if userPhase.Name == userActivity.CurrentPhase {
|
||||
//
|
||||
// if phase.RechargeDiscount.LimitTimes > 0 && userPhase.RechargeNums >= phase.RechargeDiscount.LimitTimes {
|
||||
// return
|
||||
// }
|
||||
// if phase.RechargeDiscount.LimitDuration != "" {
|
||||
// duration, err := time.ParseDuration(phase.RechargeDiscount.LimitDuration)
|
||||
// if err != nil {
|
||||
// returnErr = fmt.Errorf("parse duration failed: %w", err)
|
||||
// return
|
||||
// }
|
||||
// if time.Now().After(userActivity.Phases[userActivity.CurrentPhase].EndTime.Add(duration)) {
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
// return aType, &phase, nil
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// returnErr = fmt.Errorf("user activity not exist")
|
||||
// return
|
||||
// }
|
||||
// returnErr = fmt.Errorf("user activity not exist")
|
||||
// return
|
||||
//}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright © 2024 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 "github.com/google/uuid"
|
||||
|
||||
type UserQueryOpts struct {
|
||||
UID uuid.UUID
|
||||
Owner string
|
||||
IgnoreEmpty bool
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright © 2024 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"
|
||||
)
|
||||
|
||||
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"`
|
||||
EncryptBalance string `gorm:"column:encryptBalance;type:text;not null"`
|
||||
EncryptDeductionBalance string `gorm:"column:encryptDeductionBalance;type:text;not null"`
|
||||
CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp();not null"`
|
||||
CreateRegionID string `gorm:"type:text;not null"`
|
||||
Balance int64
|
||||
DeductionBalance int64
|
||||
}
|
||||
|
||||
func (Account) TableName() string {
|
||||
return "Account"
|
||||
}
|
||||
|
||||
type Region struct {
|
||||
UID uuid.UUID `gorm:"type:uid;default:gen_random_uuid();primary_key"`
|
||||
DisplayName string `gorm:"type:text;not null"`
|
||||
Location string `gorm:"type:text;not null"`
|
||||
Domain string `gorm:"type:text;not null"`
|
||||
Description string `gorm:"type:text;not null"`
|
||||
}
|
||||
|
||||
// RegionUserCr is located in the region
|
||||
type RegionUserCr struct {
|
||||
UID uuid.UUID `gorm:"type:uid;default:gen_random_uuid();primary_key"`
|
||||
CrName string `gorm:"type:text;column:crName;not null;unique"`
|
||||
UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null"`
|
||||
CreatedAt time.Time `gorm:"type:timestamp(3);default:current_timestamp();not null"`
|
||||
UpdatedAt time.Time `gorm:"type:timestamp(3);default:current_timestamp();not null"`
|
||||
}
|
||||
|
||||
type OauthProvider struct {
|
||||
UID uuid.UUID `gorm:"type:uid;default:gen_random_uuid();primary_key"`
|
||||
UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null"`
|
||||
ProviderType OauthProviderType `gorm:"column:providerType;type:text;not null"`
|
||||
ProviderID string `gorm:"column:providerId;type:text;not null"`
|
||||
}
|
||||
|
||||
type OauthProviderType string
|
||||
|
||||
const (
|
||||
OauthProviderTypePhone OauthProviderType = "PHONE"
|
||||
OauthProviderTypeGithub OauthProviderType = "GITHUB"
|
||||
OauthProviderTypeWechat OauthProviderType = "WECHAT"
|
||||
)
|
||||
|
||||
func (OauthProvider) TableName() string {
|
||||
return "OauthProvider"
|
||||
}
|
||||
|
||||
func (Region) TableName() string {
|
||||
return "Region"
|
||||
}
|
||||
|
||||
func (RegionUserCr) TableName() string {
|
||||
return "UserCr"
|
||||
}
|
||||
|
||||
type TransferAccountV1 struct {
|
||||
//RealUser RealUser
|
||||
RegionUID uuid.UUID `gorm:"column:regionUid;type:uuid;not null"`
|
||||
RegionUserOwner string `gorm:"column:regionUserOwner;type:text;not null"`
|
||||
Exist bool `gorm:"type:boolean;default:false"`
|
||||
Account
|
||||
}
|
||||
|
||||
func (TransferAccountV1) TableName() string {
|
||||
return "TransferAccountV1"
|
||||
}
|
||||
|
||||
type NullUserRecord struct {
|
||||
CrName string `gorm:"column:crName;type:text;not null;unique"`
|
||||
RegionID string `gorm:"type:text;not null"`
|
||||
}
|
||||
|
||||
func (NullUserRecord) TableName() string {
|
||||
return "NullUserRecord"
|
||||
}
|
||||
|
||||
type ErrorAccountCreate struct {
|
||||
Account
|
||||
UserCr string `gorm:"column:userCr;type:text;not null;unique"`
|
||||
ErrorTime time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp();not null"`
|
||||
RegionUID uuid.UUID `gorm:"column:regionUid;type:uuid;not null"`
|
||||
RegionUserOwner string `gorm:"column:regionUserOwner;type:text;not null"`
|
||||
Message string `gorm:"type:text;not null"`
|
||||
}
|
||||
|
||||
func (ErrorAccountCreate) TableName() string {
|
||||
return "ErrorAccountCreate"
|
||||
}
|
||||
|
||||
type ErrorPaymentCreate struct {
|
||||
PaymentRaw
|
||||
CreateTime time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp();not null"`
|
||||
Message string `gorm:"type:text;not null"`
|
||||
}
|
||||
|
||||
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();not null"`
|
||||
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"`
|
||||
Message string `gorm:"type:text;not null"`
|
||||
}
|
||||
|
||||
func (ErrorPaymentCreate) TableName() string {
|
||||
return "ErrorPaymentCreate"
|
||||
}
|
||||
|
||||
type Payment struct {
|
||||
ID string `gorm:"type:text;primary_key"`
|
||||
PaymentRaw
|
||||
}
|
||||
|
||||
func (Payment) TableName() string {
|
||||
return "Payment"
|
||||
}
|
||||
@@ -43,7 +43,7 @@ clean:
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build service-hub binary.
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) go build $(GO_BUILD_FLAGS) -o bin/manager main.go
|
||||
CGO_ENABLED=0 GOOS=linux go build $(shell [ -n "${CRYPTOKEY}" ] && echo "-ldflags '-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY}'") -o bin/manager main.go
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build: build
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
|
||||
"github.com/labring/sealos/service/account/common"
|
||||
|
||||
"github.com/labring/sealos/service/account/dao"
|
||||
@@ -86,14 +88,14 @@ func GetProperties(c *gin.Context) {
|
||||
// @Tags ConsumptionAmount
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body helper.UserCostsAmountReq true "User consumption amount request"
|
||||
// @Param request body helper.UserBaseReq true "User consumption amount request"
|
||||
// @Success 200 {object} map[string]interface{} "successfully retrieved user consumption amount"
|
||||
// @Failure 400 {object} map[string]interface{} "failed to parse user consumption amount request"
|
||||
// @Failure 401 {object} map[string]interface{} "authenticate error"
|
||||
// @Failure 500 {object} map[string]interface{} "failed to get user consumption amount"
|
||||
// @Router /account/v1alpha1/costs/consumption [post]
|
||||
func GetConsumptionAmount(c *gin.Context) {
|
||||
req, err := helper.ParseUserCostsAmountReq(c)
|
||||
req, err := helper.ParseUserBaseReq(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse user consumption amount request: %v", err)})
|
||||
return
|
||||
@@ -105,26 +107,59 @@ func GetConsumptionAmount(c *gin.Context) {
|
||||
amount, err := dao.DBClient.GetConsumptionAmount(req.Owner, req.TimeRange.StartTime, req.TimeRange.EndTime)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPayment
|
||||
// @Summary Get user payment
|
||||
// @Description Get user payment within a specified time range
|
||||
// @Tags Payment
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body helper.UserBaseReq true "User payment request"
|
||||
// @Success 200 {object} map[string]interface{} "successfully retrieved user payment"
|
||||
// @Failure 400 {object} map[string]interface{} "failed to parse user payment request"
|
||||
// @Failure 401 {object} map[string]interface{} "authenticate error"
|
||||
// @Failure 500 {object} map[string]interface{} "failed to get user payment"
|
||||
// @Router /account/v1alpha1/costs/payment [post]
|
||||
func GetPayment(c *gin.Context) {
|
||||
req, err := helper.ParseUserBaseReq(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse user payment request: %v", err)})
|
||||
return
|
||||
}
|
||||
if err := helper.Authenticate(req.Auth); err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("authenticate error : %v", err)})
|
||||
return
|
||||
}
|
||||
payment, err := dao.DBClient.GetPayment(types.UserQueryOpts{Owner: req.Owner}, req.TimeRange.StartTime, req.TimeRange.EndTime)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get payment : %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"payment": payment,
|
||||
})
|
||||
}
|
||||
|
||||
// GetRechargeAmount
|
||||
// @Summary Get user recharge amount
|
||||
// @Description Get user recharge amount within a specified time range
|
||||
// @Tags RechargeAmount
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body helper.UserCostsAmountReq true "User recharge amount request"
|
||||
// @Param request body helper.UserBaseReq true "User recharge amount request"
|
||||
// @Success 200 {object} map[string]interface{} "successfully retrieved user recharge amount"
|
||||
// @Failure 400 {object} map[string]interface{} "failed to parse user recharge amount request"
|
||||
// @Failure 401 {object} map[string]interface{} "authenticate error"
|
||||
// @Failure 500 {object} map[string]interface{} "failed to get user recharge amount"
|
||||
// @Router /account/v1alpha1/costs/recharge [post]
|
||||
func GetRechargeAmount(c *gin.Context) {
|
||||
req, err := helper.ParseUserCostsAmountReq(c)
|
||||
req, err := helper.ParseUserBaseReq(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse user recharge amount request: %v", err)})
|
||||
return
|
||||
@@ -133,9 +168,10 @@ func GetRechargeAmount(c *gin.Context) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("authenticate error : %v", err)})
|
||||
return
|
||||
}
|
||||
amount, err := dao.DBClient.GetRechargeAmount(req.Owner, req.TimeRange.StartTime, req.TimeRange.EndTime)
|
||||
amount, err := dao.DBClient.GetRechargeAmount(types.UserQueryOpts{Owner: req.Owner}, req.TimeRange.StartTime, req.TimeRange.EndTime)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get recharge amount : %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"amount": amount,
|
||||
@@ -148,14 +184,14 @@ func GetRechargeAmount(c *gin.Context) {
|
||||
// @Tags PropertiesUsedAmount
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body helper.UserCostsAmountReq true "User properties used amount request"
|
||||
// @Param request body helper.UserBaseReq true "User properties used amount request"
|
||||
// @Success 200 {object} map[string]interface{} "successfully retrieved user properties used amount"
|
||||
// @Failure 400 {object} map[string]interface{} "failed to parse user properties used amount request"
|
||||
// @Failure 401 {object} map[string]interface{} "authenticate error"
|
||||
// @Failure 500 {object} map[string]interface{} "failed to get user properties used amount"
|
||||
// @Router /account/v1alpha1/costs/properties [post]
|
||||
func GetPropertiesUsedAmount(c *gin.Context) {
|
||||
req, err := helper.ParseUserCostsAmountReq(c)
|
||||
req, err := helper.ParseUserBaseReq(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse user properties used amount request: %v", err)})
|
||||
return
|
||||
@@ -167,6 +203,7 @@ func GetPropertiesUsedAmount(c *gin.Context) {
|
||||
amount, err := dao.DBClient.GetPropertiesUsedAmount(req.Owner, req.TimeRange.StartTime, req.TimeRange.EndTime)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get properties used amount : %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"amount": amount,
|
||||
@@ -187,14 +224,14 @@ type CostsResultData struct {
|
||||
// @Tags Costs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body helper.UserCostsAmountReq true "User costs amount request"
|
||||
// @Param request body helper.UserBaseReq true "User costs amount request"
|
||||
// @Success 200 {object} map[string]interface{} "successfully retrieved user costs"
|
||||
// @Failure 400 {object} map[string]interface{} "failed to parse user hour costs amount request"
|
||||
// @Failure 401 {object} map[string]interface{} "authenticate error"
|
||||
// @Failure 500 {object} map[string]interface{} "failed to get user costs"
|
||||
// @Router /account/v1alpha1/costs [post]
|
||||
func GetCosts(c *gin.Context) {
|
||||
req, err := helper.ParseUserCostsAmountReq(c)
|
||||
req, err := helper.ParseUserBaseReq(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse user hour costs amount request: %v", err)})
|
||||
return
|
||||
@@ -206,9 +243,41 @@ func GetCosts(c *gin.Context) {
|
||||
costs, err := dao.DBClient.GetCosts(req.Auth.Owner, req.TimeRange.StartTime, req.TimeRange.EndTime)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get cost : %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, CostsResult{
|
||||
Data: CostsResultData{Costs: costs},
|
||||
Message: "successfully retrieved user costs",
|
||||
})
|
||||
}
|
||||
|
||||
// GetAccount
|
||||
// @Summary Get user account
|
||||
// @Description Get user account
|
||||
// @Tags Account
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body helper.Auth true "auth request"
|
||||
// @Success 200 {object} map[string]interface{} "successfully retrieved user account"
|
||||
// @Failure 401 {object} map[string]interface{} "authenticate error"
|
||||
// @Failure 500 {object} map[string]interface{} "failed to get user account"
|
||||
// @Router /account/v1alpha1/account [post]
|
||||
func GetAccount(c *gin.Context) {
|
||||
req, err := helper.ParseUserBaseReq(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse user hour costs amount request: %v", err)})
|
||||
return
|
||||
}
|
||||
if err := helper.Authenticate(req.Auth); err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("authenticate error : %v", err)})
|
||||
return
|
||||
}
|
||||
account, err := dao.DBClient.GetAccount(types.UserQueryOpts{Owner: req.Auth.Owner})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get account : %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"account": account,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/labring/sealos/service/account/helper"
|
||||
@@ -10,7 +11,22 @@ var DBClient Interface
|
||||
|
||||
func InitDB() error {
|
||||
var err error
|
||||
DBClient, err = NewMongoInterface(os.Getenv(helper.EnvMongoURI))
|
||||
globalCockroach := os.Getenv(helper.ENVGlobalCockroach)
|
||||
if globalCockroach == "" {
|
||||
return fmt.Errorf("empty global cockroach uri, please check env: %s", helper.ENVGlobalCockroach)
|
||||
}
|
||||
localCockroach := os.Getenv(helper.ENVLocalCockroach)
|
||||
if localCockroach == "" {
|
||||
return fmt.Errorf("empty local cockroach uri, please check env: %s", helper.ENVLocalCockroach)
|
||||
}
|
||||
mongoURI := os.Getenv(helper.EnvMongoURI)
|
||||
if mongoURI == "" {
|
||||
return fmt.Errorf("empty mongo uri, please check env: %s", helper.EnvMongoURI)
|
||||
}
|
||||
fmt.Println("cockroachStr: ", globalCockroach)
|
||||
fmt.Println("localRegionStr: ", localCockroach)
|
||||
fmt.Println("mongoURI: ", mongoURI)
|
||||
DBClient, err = NewAccountInterface(mongoURI, globalCockroach, localCockroach)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,14 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labring/sealos/controllers/pkg/crypto"
|
||||
"gorm.io/driver/postgres"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
|
||||
"github.com/labring/sealos/service/account/common"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/resources"
|
||||
@@ -22,8 +30,15 @@ type Interface interface {
|
||||
GetProperties() ([]common.PropertyQuery, error)
|
||||
GetCosts(user string, startTime, endTime time.Time) (common.TimeCostsMap, error)
|
||||
GetConsumptionAmount(user string, startTime, endTime time.Time) (int64, error)
|
||||
GetRechargeAmount(user string, startTime, endTime time.Time) (int64, error)
|
||||
GetRechargeAmount(ops types.UserQueryOpts, startTime, endTime time.Time) (int64, error)
|
||||
GetPropertiesUsedAmount(user string, startTime, endTime time.Time) (map[string]int64, error)
|
||||
GetAccount(ops types.UserQueryOpts) (*types.Account, error)
|
||||
GetPayment(ops types.UserQueryOpts, startTime, endTime time.Time) ([]types.Payment, error)
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
*MongoDB
|
||||
*Cockroach
|
||||
}
|
||||
|
||||
type MongoDB struct {
|
||||
@@ -34,6 +49,93 @@ type MongoDB struct {
|
||||
Properties *resources.PropertyTypeLS
|
||||
}
|
||||
|
||||
type Cockroach struct {
|
||||
DB *gorm.DB
|
||||
LocalDB *gorm.DB
|
||||
}
|
||||
|
||||
func (g *Cockroach) GetAccount(ops types.UserQueryOpts) (*types.Account, error) {
|
||||
if ops.UID == uuid.Nil {
|
||||
user, err := g.GetUser(ops)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %v", err)
|
||||
}
|
||||
ops.UID = user.UserUID
|
||||
}
|
||||
var account types.Account
|
||||
if err := g.DB.Where(types.Account{UserUID: ops.UID}).First(&account).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get account: %w", err)
|
||||
}
|
||||
balance, err := crypto.DecryptInt64(account.EncryptBalance)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to descrypt balance: %v", err)
|
||||
}
|
||||
deductionBalance, err := crypto.DecryptInt64(account.EncryptDeductionBalance)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to descrypt deduction balance: %v", err)
|
||||
}
|
||||
account.Balance = balance
|
||||
account.DeductionBalance = deductionBalance
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) GetPayment(ops types.UserQueryOpts, startTime, endTime time.Time) ([]types.Payment, error) {
|
||||
if ops.UID == uuid.Nil {
|
||||
user, err := g.GetUser(ops)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %v", err)
|
||||
}
|
||||
ops.UID = user.UserUID
|
||||
}
|
||||
var payment []types.Payment
|
||||
if startTime != endTime {
|
||||
if err := g.DB.Where(types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}).Where("created_at >= ? AND created_at <= ?", startTime, endTime).Find(&payment).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get payment: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := g.DB.Where(types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}).Find(&payment).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get payment: %w", err)
|
||||
}
|
||||
}
|
||||
return payment, nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) GetRechargeAmount(ops types.UserQueryOpts, startTime, endTime time.Time) (int64, error) {
|
||||
payment, err := g.GetPayment(ops, startTime, endTime)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get payment: %v", err)
|
||||
}
|
||||
paymentAmount := int64(0)
|
||||
for i := range payment {
|
||||
paymentAmount += payment[i].Amount
|
||||
}
|
||||
return paymentAmount, nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) GetUser(ops types.UserQueryOpts) (*types.RegionUserCr, error) {
|
||||
if err := checkOps(ops); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := &types.RegionUserCr{
|
||||
CrName: ops.Owner,
|
||||
}
|
||||
if ops.UID != uuid.Nil {
|
||||
query.UserUID = ops.UID
|
||||
}
|
||||
var user types.RegionUserCr
|
||||
if err := g.LocalDB.Where(query).First(&user).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func checkOps(ops types.UserQueryOpts) error {
|
||||
if ops.Owner == "" && ops.UID == uuid.Nil {
|
||||
return fmt.Errorf("empty query opts")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MongoDB) GetProperties() ([]common.PropertyQuery, error) {
|
||||
propertiesQuery := make([]common.PropertyQuery, 0)
|
||||
if m.Properties == nil {
|
||||
@@ -68,7 +170,7 @@ func (m *MongoDB) GetCosts(user string, startTime, endTime time.Time) (common.Ti
|
||||
},
|
||||
"owner": user,
|
||||
}
|
||||
cursor, err := m.getBillingCollection().Find(context.Background(), filter)
|
||||
cursor, err := m.getBillingCollection().Find(context.Background(), filter, options.Find().SetSort(bson.M{"time": 1}))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get billing collection: %v", err)
|
||||
}
|
||||
@@ -95,10 +197,6 @@ func (m *MongoDB) GetConsumptionAmount(user string, startTime, endTime time.Time
|
||||
return m.getAmountWithType(0, user, startTime, endTime)
|
||||
}
|
||||
|
||||
func (m *MongoDB) GetRechargeAmount(user string, startTime, endTime time.Time) (int64, error) {
|
||||
return m.getAmountWithType(1, user, startTime, endTime)
|
||||
}
|
||||
|
||||
func (m *MongoDB) getAmountWithType(_type int64, user string, startTime, endTime time.Time) (int64, error) {
|
||||
pipeline := bson.A{
|
||||
bson.D{{Key: "$match", Value: bson.M{
|
||||
@@ -172,18 +270,30 @@ func (m *MongoDB) getSumOfUsedAmount(propertyType uint8, user string, startTime,
|
||||
return result.TotalAmount, nil
|
||||
}
|
||||
|
||||
func NewMongoInterface(url string) (Interface, error) {
|
||||
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(url))
|
||||
func NewAccountInterface(mongoURI, cockRoachURI, localCockRoachURI string) (Interface, error) {
|
||||
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(mongoURI))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to connect mongodb: %v", err)
|
||||
}
|
||||
err = client.Ping(context.Background(), nil)
|
||||
return &MongoDB{
|
||||
if err = client.Ping(context.Background(), nil); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping mongodb: %v", err)
|
||||
}
|
||||
mongodb := &MongoDB{
|
||||
Client: client,
|
||||
AccountDBName: "sealos-resources",
|
||||
BillingConn: "billing",
|
||||
PropertiesConn: "properties",
|
||||
}, err
|
||||
}
|
||||
db, err := gorm.Open(postgres.Open(cockRoachURI), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect cockroach uri %s: %v", cockRoachURI, err)
|
||||
}
|
||||
localDB, err := gorm.Open(postgres.Open(localCockRoachURI), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect local cockroach uri %s: %v", localCockRoachURI, err)
|
||||
}
|
||||
account := &Account{MongoDB: mongodb, Cockroach: &Cockroach{DB: db, LocalDB: localDB}}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (m *MongoDB) getProperties() (*resources.PropertyTypeLS, error) {
|
||||
|
||||
@@ -4,49 +4,19 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/resources"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
)
|
||||
|
||||
func TestMongoDB_GetRechargeAmount(t *testing.T) {
|
||||
type fields struct {
|
||||
Client *mongo.Client
|
||||
AccountDBName string
|
||||
BillingConn string
|
||||
PropertiesConn string
|
||||
Properties *resources.PropertyTypeLS
|
||||
func TestCockroach_GetPayment(t *testing.T) {
|
||||
db, err := NewAccountInterface("", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("NewAccountInterface() error = %v", err)
|
||||
return
|
||||
}
|
||||
type args struct {
|
||||
user string
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want int64
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &MongoDB{
|
||||
Client: tt.fields.Client,
|
||||
AccountDBName: tt.fields.AccountDBName,
|
||||
BillingConn: tt.fields.BillingConn,
|
||||
PropertiesConn: tt.fields.PropertiesConn,
|
||||
Properties: tt.fields.Properties,
|
||||
}
|
||||
got, err := m.GetRechargeAmount(tt.args.user, tt.args.startTime, tt.args.endTime)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetRechargeAmount() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("GetRechargeAmount() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
got, err := db.GetPayment(types.UserQueryOpts{Owner: "1fgtm0mn"}, time.Time{}, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("GetPayment() error = %v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("got = %+v", got)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ FROM scratch
|
||||
COPY registry registry
|
||||
COPY manifests manifests
|
||||
|
||||
ENV DEFAULT_NAMESPACE sealos
|
||||
ENV DEFAULT_NAMESPACE account-system
|
||||
ENV MONGO_URI mongodb://mongo:27017
|
||||
|
||||
CMD ["kubectl apply -f manifests/deploy.yaml -n $DEFAULT_NAMESPACE"]
|
||||
@@ -2,6 +2,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: account-service
|
||||
namespace: account-system
|
||||
labels:
|
||||
cloud.sealos.io/app-deploy-manager: account-service
|
||||
spec:
|
||||
@@ -15,6 +16,7 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: account-service
|
||||
namespace: account-system
|
||||
annotations:
|
||||
originImageName: ghcr.io/labring/sealos-account-service:latest
|
||||
deploy.cloud.sealos.io/minReplicas: '1'
|
||||
@@ -43,7 +45,13 @@ spec:
|
||||
image: ghcr.io/labring/sealos-account-service:latest
|
||||
env:
|
||||
- name: MONGO_URI
|
||||
value: {{ .MONGO_URI }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: MONGO_URI
|
||||
name: mongo-secret
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: account-manager-env
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
@@ -55,4 +63,5 @@ spec:
|
||||
- containerPort: 2333
|
||||
imagePullPolicy: Always
|
||||
volumeMounts: []
|
||||
serviceAccountName: account-controller-manager
|
||||
volumes: []
|
||||
|
||||
@@ -18,6 +18,55 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/account/v1alpha1/account": {
|
||||
"post": {
|
||||
"description": "Get user account",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Account"
|
||||
],
|
||||
"summary": "Get user account",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "auth request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.Auth"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successfully retrieved user account",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "authenticate error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "failed to get user account",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/account/v1alpha1/costs": {
|
||||
"post": {
|
||||
"description": "Get user costs within a specified time range",
|
||||
@@ -38,7 +87,7 @@ const docTemplate = `{
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.UserCostsAmountReq"
|
||||
"$ref": "#/definitions/helper.UserBaseReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -94,7 +143,7 @@ const docTemplate = `{
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.UserCostsAmountReq"
|
||||
"$ref": "#/definitions/helper.UserBaseReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -130,6 +179,62 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/account/v1alpha1/costs/payment": {
|
||||
"post": {
|
||||
"description": "Get user payment within a specified time range",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Payment"
|
||||
],
|
||||
"summary": "Get user payment",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User payment request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.UserBaseReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successfully retrieved user payment",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "failed to parse user payment request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "authenticate error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "failed to get user payment",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/account/v1alpha1/costs/properties": {
|
||||
"post": {
|
||||
"description": "Get user properties used amount within a specified time range",
|
||||
@@ -150,7 +255,7 @@ const docTemplate = `{
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.UserCostsAmountReq"
|
||||
"$ref": "#/definitions/helper.UserBaseReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -206,7 +311,7 @@ const docTemplate = `{
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.UserCostsAmountReq"
|
||||
"$ref": "#/definitions/helper.UserBaseReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -454,7 +559,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"helper.UserCostsAmountReq": {
|
||||
"helper.UserBaseReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kubeConfig",
|
||||
|
||||
@@ -11,6 +11,55 @@
|
||||
},
|
||||
"host": "localhost:2333",
|
||||
"paths": {
|
||||
"/account/v1alpha1/account": {
|
||||
"post": {
|
||||
"description": "Get user account",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Account"
|
||||
],
|
||||
"summary": "Get user account",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "auth request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.Auth"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successfully retrieved user account",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "authenticate error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "failed to get user account",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/account/v1alpha1/costs": {
|
||||
"post": {
|
||||
"description": "Get user costs within a specified time range",
|
||||
@@ -31,7 +80,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.UserCostsAmountReq"
|
||||
"$ref": "#/definitions/helper.UserBaseReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -87,7 +136,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.UserCostsAmountReq"
|
||||
"$ref": "#/definitions/helper.UserBaseReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -123,6 +172,62 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/account/v1alpha1/costs/payment": {
|
||||
"post": {
|
||||
"description": "Get user payment within a specified time range",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Payment"
|
||||
],
|
||||
"summary": "Get user payment",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User payment request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.UserBaseReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successfully retrieved user payment",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "failed to parse user payment request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "authenticate error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "failed to get user payment",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/account/v1alpha1/costs/properties": {
|
||||
"post": {
|
||||
"description": "Get user properties used amount within a specified time range",
|
||||
@@ -143,7 +248,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.UserCostsAmountReq"
|
||||
"$ref": "#/definitions/helper.UserBaseReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -199,7 +304,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/helper.UserCostsAmountReq"
|
||||
"$ref": "#/definitions/helper.UserBaseReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -447,7 +552,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"helper.UserCostsAmountReq": {
|
||||
"helper.UserBaseReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kubeConfig",
|
||||
|
||||
@@ -79,7 +79,7 @@ definitions:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
helper.UserCostsAmountReq:
|
||||
helper.UserBaseReq:
|
||||
properties:
|
||||
endTime:
|
||||
example: "2021-12-01T00:00:00Z"
|
||||
@@ -105,6 +105,39 @@ info:
|
||||
title: sealos account service
|
||||
version: v1alpha1
|
||||
paths:
|
||||
/account/v1alpha1/account:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get user account
|
||||
parameters:
|
||||
- description: auth request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/helper.Auth'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: successfully retrieved user account
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: authenticate error
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: failed to get user account
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: Get user account
|
||||
tags:
|
||||
- Account
|
||||
/account/v1alpha1/costs:
|
||||
post:
|
||||
consumes:
|
||||
@@ -116,7 +149,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/helper.UserCostsAmountReq'
|
||||
$ref: '#/definitions/helper.UserBaseReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -154,7 +187,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/helper.UserCostsAmountReq'
|
||||
$ref: '#/definitions/helper.UserBaseReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -181,6 +214,44 @@ paths:
|
||||
summary: Get user consumption amount
|
||||
tags:
|
||||
- ConsumptionAmount
|
||||
/account/v1alpha1/costs/payment:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get user payment within a specified time range
|
||||
parameters:
|
||||
- description: User payment request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/helper.UserBaseReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: successfully retrieved user payment
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: failed to parse user payment request
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: authenticate error
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: failed to get user payment
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: Get user payment
|
||||
tags:
|
||||
- Payment
|
||||
/account/v1alpha1/costs/properties:
|
||||
post:
|
||||
consumes:
|
||||
@@ -192,7 +263,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/helper.UserCostsAmountReq'
|
||||
$ref: '#/definitions/helper.UserBaseReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -230,7 +301,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/helper.UserCostsAmountReq'
|
||||
$ref: '#/definitions/helper.UserBaseReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
||||
@@ -11,12 +11,15 @@ replace (
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/labring/sealos/controllers/pkg v0.0.0-00010101000000-000000000000
|
||||
github.com/labring/sealos/service v0.0.0-00010101000000-000000000000
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.2
|
||||
go.mongodb.org/mongo-driver v1.13.0
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -44,8 +47,12 @@ require (
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.3.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/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.16.7 // indirect
|
||||
|
||||
@@ -83,7 +83,17 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
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=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
@@ -282,6 +292,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=
|
||||
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
||||
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY=
|
||||
k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0=
|
||||
k8s.io/apiextensions-apiserver v0.27.2 h1:iwhyoeS4xj9Y7v8YExhUwbVuBhMr3Q4bd/laClBV6Bo=
|
||||
|
||||
@@ -2,6 +2,8 @@ package helper
|
||||
|
||||
const (
|
||||
GROUP = "/account/v1alpha1"
|
||||
GetAccount = "/account"
|
||||
GetPayment = "/payment"
|
||||
GetHistoryNamespaces = "/namespaces"
|
||||
GetProperties = "/properties"
|
||||
GetRechargeAmount = "/costs/recharge"
|
||||
@@ -12,5 +14,8 @@ const (
|
||||
|
||||
// env
|
||||
const (
|
||||
EnvMongoURI = "MONGO_URI"
|
||||
EnvMongoURI = "MONGO_URI"
|
||||
ENVGlobalCockroach = "GLOBAL_COCKROACH_URI"
|
||||
ENVLocalCockroach = "LOCAL_COCKROACH_URI"
|
||||
EnvLocalRegion = "LOCAL_REGION"
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ func ParseNamespaceBillingHistoryReq(c *gin.Context) (*NamespaceBillingHistoryRe
|
||||
return nsList, nil
|
||||
}
|
||||
|
||||
type UserCostsAmountReq struct {
|
||||
type UserBaseReq struct {
|
||||
TimeRange `json:",inline" bson:",inline"`
|
||||
|
||||
// @Summary Authentication information
|
||||
@@ -79,8 +79,8 @@ type UserCostsAmountReq struct {
|
||||
Auth `json:",inline" bson:",inline"`
|
||||
}
|
||||
|
||||
func ParseUserCostsAmountReq(c *gin.Context) (*UserCostsAmountReq, error) {
|
||||
userCosts := &UserCostsAmountReq{}
|
||||
func ParseUserBaseReq(c *gin.Context) (*UserBaseReq, error) {
|
||||
userCosts := &UserBaseReq{}
|
||||
if err := c.ShouldBindJSON(userCosts); err != nil {
|
||||
return nil, fmt.Errorf("bind json error: %v", err)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ func RegisterPayRouter() {
|
||||
POST(helper.GetHistoryNamespaces, api.GetBillingHistoryNamespaceList).
|
||||
POST(helper.GetProperties, api.GetProperties).
|
||||
POST(helper.GetUserCosts, api.GetCosts).
|
||||
POST(helper.GetAccount, api.GetAccount).
|
||||
POST(helper.GetPayment, api.GetPayment).
|
||||
POST(helper.GetRechargeAmount, api.GetRechargeAmount).
|
||||
POST(helper.GetConsumptionAmount, api.GetConsumptionAmount).
|
||||
POST(helper.GetPropertiesUsed, api.GetPropertiesUsedAmount)
|
||||
|
||||
@@ -173,7 +173,17 @@ github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c h1:rwmN+hg
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
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/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
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=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
@@ -391,6 +401,10 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
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/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
||||
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
|
||||
|
||||
@@ -3,6 +3,7 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@@ -28,7 +29,7 @@ func Authenticate(ns, kc string) error {
|
||||
config, err := clientcmd.RESTConfigFromKubeConfig([]byte(kc))
|
||||
if err != nil {
|
||||
log.Printf("kubeconfig failed (%s)\n", kc)
|
||||
return err
|
||||
return fmt.Errorf("kubeconfig failed %v", err)
|
||||
}
|
||||
|
||||
if k8shost := GetKubernetesHostFromEnv(); k8shost != "" {
|
||||
@@ -39,25 +40,25 @@ func Authenticate(ns, kc string) error {
|
||||
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to new client: %v", err)
|
||||
}
|
||||
discovery, err := discovery.NewDiscoveryClientForConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to new discovery client: %v", err)
|
||||
}
|
||||
res, err := discovery.RESTClient().Get().AbsPath("/readyz").DoRaw(context.Background())
|
||||
if err != nil {
|
||||
log.Println("Authenticate false, ping apiserver error")
|
||||
return err
|
||||
return fmt.Errorf("ping apiserver error: %v", err)
|
||||
}
|
||||
if string(res) != "ok" {
|
||||
log.Println("Authenticate false, response not ok")
|
||||
return err
|
||||
return fmt.Errorf("ping apiserver is no ok: %v", string(res))
|
||||
}
|
||||
|
||||
if err := CheckResourceAccess(client, ns, "get", "pods"); err != nil {
|
||||
// fmt.Println(err.Error())
|
||||
return err
|
||||
return fmt.Errorf("check resource access error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user