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:
Jiahui
2024-03-11 15:13:50 +08:00
committed by GitHub
parent 295e6a543f
commit 0350700b55
52 changed files with 2839 additions and 1172 deletions
+1
View File
@@ -46,6 +46,7 @@ env:
# Common versions
GO_VERSION: "1.20"
DEFAULT_OWNER: "labring"
CRYPTOKEY: ${{ secrets.CONTROLLER_BUILD_CRYPTOKEY }}
jobs:
resolve-modules:
+10 -2
View File
@@ -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
+17 -12
View File
@@ -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
//}
+4 -1
View File
@@ -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 }}"
+3
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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=
+2 -26
View File
@@ -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)
}
+47 -2
View File
@@ -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)
}
+81 -22
View File
@@ -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 {
+9 -2
View File
@@ -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
+14
View File
@@ -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=
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
+2 -2
View File
@@ -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
View File
@@ -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
//}
+23
View File
@@ -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
}
+149
View File
@@ -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"
}
+1 -1
View File
@@ -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
+78 -9
View File
@@ -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,
})
}
+17 -1
View File
@@ -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
}
+122 -12
View File
@@ -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) {
+11 -41
View File
@@ -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)
}
+1 -1
View File
@@ -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: []
+110 -5
View File
@@ -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",
+110 -5
View File
@@ -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",
+76 -5
View File
@@ -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:
+8 -1
View File
@@ -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
+14
View File
@@ -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=
+6 -1
View File
@@ -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"
)
+3 -3
View File
@@ -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)
}
+2
View File
@@ -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)
+14
View File
@@ -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=
+7 -6
View File
@@ -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