optimize transfer controller, add encryption to account and adapt (#3463)

* optimize transfer controller, add encryption to account and adapt to existing environment

* fix semgrep ci
This commit is contained in:
Jiahui
2023-07-05 11:30:12 +08:00
committed by GitHub
parent 59bd06800e
commit 4945ee2a38
13 changed files with 350 additions and 78 deletions
+2 -2
View File
@@ -63,13 +63,13 @@ type AccountSpec struct{}
// AccountStatus defines the observed state of Account
type AccountStatus struct {
// EncryptBalance is to encrypt balance
EncryptBalance string `json:"encryptBalance,omitempty"`
EncryptBalance *string `json:"encryptBalance,omitempty"`
// Recharge amount
Balance int64 `json:"balance,omitempty"`
//Deduction amount
DeductionBalance int64 `json:"deductionBalance,omitempty"`
// EncryptDeductionBalance is to encrypt DeductionBalance
EncryptDeductionBalance string `json:"encryptDeductionBalance,omitempty"`
EncryptDeductionBalance *string `json:"encryptDeductionBalance,omitempty"`
// delete in the future
ChargeList []Charge `json:"chargeList,omitempty"`
}
+18 -1
View File
@@ -17,6 +17,8 @@ limitations under the License.
package v1
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -34,7 +36,8 @@ const (
// TransferSpec defines the desired state of Transfer
type TransferSpec struct {
To string `json:"to"`
From string `json:"from"`
To string `json:"to"`
// +kubebuilder:validation:Minimum=1000000
Amount int64 `json:"amount"`
}
@@ -69,3 +72,17 @@ type TransferList struct {
func init() {
SchemeBuilder.Register(&Transfer{}, &TransferList{})
}
func (t *Transfer) ToJSON() string {
return `{
"spec": {
"from": "` + t.Spec.From + `",
"to": "` + t.Spec.To + `",
"amount": ` + fmt.Sprint(t.Spec.Amount) + `
},
"status": {
"reason": "` + t.Status.Reason + `",
"progress": "` + string(rune(t.Status.Progress)) + `"
}
}`
}
@@ -70,6 +70,12 @@ spec:
description: Deduction amount
format: int64
type: integer
encryptBalance:
description: EncryptBalance is to encrypt balance
type: string
encryptDeductionBalance:
description: EncryptDeductionBalance is to encrypt DeductionBalance
type: string
type: object
type: object
served: true
@@ -0,0 +1,68 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.8.0
creationTimestamp: null
name: transfers.account.sealos.io
spec:
group: account.sealos.io
names:
kind: Transfer
listKind: TransferList
plural: transfers
singular: transfer
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
description: Transfer is the Schema for the transfers API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: TransferSpec defines the desired state of Transfer
properties:
amount:
format: int64
minimum: 1000000
type: integer
from:
type: string
to:
type: string
required:
- amount
- to
type: object
status:
description: TransferStatus defines the observed state of Transfer
properties:
progress:
type: integer
reason:
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
@@ -0,0 +1,24 @@
# permissions for end users to edit transfers.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: transfer-editor-role
rules:
- apiGroups:
- account.sealos.io
resources:
- transfers
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- account.sealos.io
resources:
- transfers/status
verbs:
- get
@@ -0,0 +1,20 @@
# permissions for end users to view transfers.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: transfer-viewer-role
rules:
- apiGroups:
- account.sealos.io
resources:
- transfers
verbs:
- get
- list
- watch
- apiGroups:
- account.sealos.io
resources:
- transfers/status
verbs:
- get
@@ -0,0 +1,6 @@
apiVersion: account.sealos.io/v1
kind: Transfer
metadata:
name: transfer-sample
spec:
# TODO(user): Add fields here
@@ -23,6 +23,11 @@ import (
"strconv"
"time"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/builder"
"github.com/labring/sealos/controllers/pkg/crypto"
retry2 "k8s.io/client-go/util/retry"
@@ -58,7 +63,6 @@ import (
const (
ACCOUNTNAMESPACEENV = "ACCOUNT_NAMESPACE"
PrivateDeployEnv = "PRIVATE_DEPLOY"
DEFAULTACCOUNTNAMESPACE = "sealos-system"
AccountAnnotationNewAccount = "account.sealos.io/new-account"
NEWACCOUNTAMOUNTENV = "NEW_ACCOUNT_AMOUNT"
@@ -66,7 +70,6 @@ const (
// AccountReconciler reconciles a Account object
type AccountReconciler struct {
PrivateDeploy bool
client.Client
Scheme *runtime.Scheme
Logger logr.Logger
@@ -99,8 +102,10 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
accountBalance := accountv1.AccountBalance{}
if err := r.Get(ctx, req.NamespacedName, &accountBalance); err == nil {
if err := r.updateDeductionBalance(ctx, &accountBalance); err != nil {
r.Logger.Error(err, err.Error())
err = retry2.RetryOnConflict(retry2.DefaultBackoff, func() error {
return r.updateDeductionBalance(ctx, &accountBalance)
})
if err != nil {
return ctrl.Result{}, err
}
} else if client.IgnoreNotFound(err) != nil {
@@ -126,21 +131,6 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
return ctrl.Result{}, fmt.Errorf("get account failed: %v", err)
}
if r.PrivateDeploy {
encryptBalance := account.Status.EncryptBalance
encryptDeductionBalance := account.Status.EncryptDeductionBalance
balance, err := crypto.DecryptInt64(encryptBalance)
if err != nil {
return ctrl.Result{}, fmt.Errorf("decrypt balance failed: %v", err)
}
account.Status.Balance = balance
deductionBalance, err := crypto.DecryptInt64(encryptDeductionBalance)
if err != nil {
return ctrl.Result{}, fmt.Errorf("decrypt deduction balance failed: %v", err)
}
account.Status.DeductionBalance = deductionBalance
}
orderResp, err := pay.QueryOrder(payment.Status.TradeNO)
if err != nil {
return ctrl.Result{}, fmt.Errorf("query order failed: %v", err)
@@ -163,17 +153,11 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
now := time.Now().UTC()
payAmount := *orderResp.Amount.Total * 10000
//1¥ = 100WechatPayAmount; 1 WechatPayAmount = 10000 SealosAmount
var gift = giveGift(payAmount)
if r.PrivateDeploy {
encryptBalance := account.Status.EncryptBalance
encryptBalance, err = crypto.RechargeBalance(encryptBalance, payAmount+gift)
if err != nil {
return ctrl.Result{}, fmt.Errorf("recharge encrypt balance failed: %v", err)
}
account.Status.EncryptBalance = encryptBalance
err = crypto.RechargeBalance(account.Status.EncryptBalance, giveGift(payAmount))
if err != nil {
return ctrl.Result{}, fmt.Errorf("recharge encrypt balance failed: %v", err)
}
account.Status.Balance += payAmount + gift
if err := r.Status().Update(ctx, account); err != nil {
if err := r.updateAccountStatus(ctx, account); err != nil {
return ctrl.Result{}, fmt.Errorf("update account failed: %v", err)
}
payment.Status.Status = pay.StatusSuccess
@@ -255,17 +239,16 @@ func (r *AccountReconciler) syncAccount(ctx context.Context, name, accountNamesp
}); err != nil {
return nil, err
}
if r.PrivateDeploy {
encryptBalance := account.Status.EncryptBalance
encryptBalance, err = crypto.RechargeBalance(encryptBalance, int64(amount))
if err != nil {
return nil, fmt.Errorf("recharge balance failed: %v", err)
}
account.Status.EncryptBalance = encryptBalance
err = r.initBalance(&account)
if err != nil {
return nil, fmt.Errorf("sync init balance failed: %v", err)
}
account.Status.Balance += int64(amount)
if err := r.Status().Update(ctx, &account); err != nil {
return nil, err
err = crypto.RechargeBalance(account.Status.EncryptBalance, int64(amount))
if err != nil {
return nil, fmt.Errorf("recharge balance failed: %v", err)
}
if err := r.updateAccountStatus(ctx, &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)
@@ -376,33 +359,34 @@ func (r *AccountReconciler) updateDeductionBalance(ctx context.Context, accountB
r.Logger.Error(err, err.Error())
return err
}
if accountBalance.Spec.Type == accountv1.TransferIn {
account.Status.Balance += accountBalance.Spec.Amount
} else {
account.Status.DeductionBalance += accountBalance.Spec.Amount
err = r.initBalance(account)
if err != nil {
return fmt.Errorf("sync balance failed: %v", err)
}
if r.PrivateDeploy {
if accountBalance.Spec.Type == accountv1.TransferIn {
encryptBalance := account.Status.EncryptBalance
encryptBalance, err = crypto.RechargeBalance(encryptBalance, accountBalance.Spec.Amount)
if err != nil {
r.Logger.Error(err, err.Error())
return err
}
account.Status.EncryptBalance = encryptBalance
} else {
encryptDeductionBalance := account.Status.EncryptDeductionBalance
encryptDeductionBalance, err = crypto.RechargeBalance(encryptDeductionBalance, accountBalance.Spec.Amount)
if err != nil {
r.Logger.Error(err, err.Error())
return err
}
account.Status.EncryptDeductionBalance = encryptDeductionBalance
switch accountBalance.Spec.Type {
case accountv1.TransferIn:
err = crypto.RechargeBalance(account.Status.EncryptBalance, accountBalance.Spec.Amount)
if err != nil {
r.Logger.Error(err, err.Error())
return err
}
case accountv1.TransferOut:
err = crypto.DeductBalance(account.Status.EncryptBalance, accountBalance.Spec.Amount)
if err != nil {
r.Logger.Error(err, err.Error())
return err
}
case accountv1.Consumption:
err = crypto.RechargeBalance(account.Status.EncryptDeductionBalance, accountBalance.Spec.Amount)
if err != nil {
r.Logger.Error(err, err.Error())
return err
}
default:
return fmt.Errorf("unknown accountbalance type: %v", accountBalance.Spec.Type)
}
if err := r.Status().Update(ctx, account); err != nil {
if err := r.updateAccountStatus(ctx, account); err != nil {
r.Logger.Error(err, err.Error())
return err
}
@@ -431,6 +415,38 @@ func (r *AccountReconciler) updateDeductionBalance(ctx context.Context, accountB
return nil
}
func (r *AccountReconciler) updateAccountStatus(ctx context.Context, 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 r.Status().Update(ctx, account)
}
func (r *AccountReconciler) 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 {
const controllerName = "account_controller"
@@ -447,12 +463,33 @@ func (r *AccountReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controll
return ctrl.NewControllerManagedBy(mgr).
For(&accountv1.Account{}).
Watches(&source.Kind{Type: &accountv1.Payment{}}, &handler.EnqueueRequestForObject{}).
Watches(&source.Kind{Type: &accountv1.AccountBalance{}}, &handler.EnqueueRequestForObject{}).
Watches(&source.Kind{Type: &accountv1.AccountBalance{}}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(&NamespaceFilterPredicate{Namespace: r.AccountSystemNamespace})).
Watches(&source.Kind{Type: &userV1.User{}}, &handler.EnqueueRequestForObject{}).
WithOptions(rateOpts).
Complete(r)
}
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
Threshold1 = 299 * BaseUnit
@@ -483,5 +520,5 @@ func giveGift(amount int64) int64 {
default:
ratio = Ratio5
}
return amount * ratio / 100
return (amount * ratio / 100) + amount
}
@@ -67,10 +67,10 @@ func (r *TransferReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
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)
}
transfer.Status.Progress = accountv1.TransferStateCompleted
pipeLine := []func(ctx context.Context, transfer *accountv1.Transfer) error{
r.check,
r.TransferOutSaver,
@@ -83,6 +83,9 @@ func (r *TransferReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
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)
}
@@ -124,6 +127,7 @@ func (r *TransferReconciler) TransferOutSaver(ctx context.Context, transfer *acc
Owner: getUsername(transfer.Namespace),
Time: metav1.Time{Time: time.Now().UTC()},
Type: accountv1.TransferOut,
Details: transfer.ToJSON(),
}
from := accountv1.AccountBalance{
ObjectMeta: objMeta,
@@ -150,6 +154,7 @@ func (r *TransferReconciler) TransferInSaver(ctx context.Context, transfer *acco
Owner: getUsername(transfer.Spec.To),
Time: metav1.Time{Time: time.Now().UTC()},
Type: accountv1.TransferIn,
Details: transfer.ToJSON(),
}
to := accountv1.AccountBalance{
ObjectMeta: objMeta,
@@ -168,6 +173,9 @@ func (r *TransferReconciler) check(ctx context.Context, transfer *accountv1.Tran
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
fromAccount := accountv1.Account{}
@@ -160,6 +160,12 @@ spec:
description: Deduction amount
format: int64
type: integer
encryptBalance:
description: EncryptBalance is to encrypt balance
type: string
encryptDeductionBalance:
description: EncryptDeductionBalance is to encrypt DeductionBalance
type: string
type: object
type: object
served: true
@@ -551,6 +557,8 @@ spec:
format: int64
minimum: 1000000
type: integer
from:
type: string
to:
type: string
required:
@@ -181,16 +181,13 @@ func (r *LicenseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}
charge := amount * cloud.BaseCount
account.Status.Balance += charge
encryptBalance := account.Status.EncryptBalance
encryptBalance, err = crypto.RechargeBalance(encryptBalance, charge)
err = crypto.RechargeBalance(account.Status.EncryptBalance, charge)
if err != nil {
r.logger.Error(err, "Recharge Failed, failed to encrypt the balance", "namespace:", req.Namespace, "name:", req.Name)
pack := cloud.NewNotificationPackage(cloud.RechargeFailedTitle, cloud.SEALOS, cloud.RechargeFailedContent)
util.SubmitNotificationWithUser(ctx, r.Client, r.logger, req.Namespace, pack)
return ctrl.Result{RequeueAfter: time.Second * 5}, nil
}
account.Status.EncryptBalance = encryptBalance
err = r.Client.Status().Update(ctx, &account)
if err != nil {
r.logger.Error(err, "Recharge Failed, failed to modify the status", "namespace:", req.Namespace, "name:", req.Name)
+26 -6
View File
@@ -56,8 +56,9 @@ func Encrypt(plaintext []byte) (string, error) {
return base64.StdEncoding.EncodeToString(append(nonce, ciphertext...)), nil
}
func EncryptInt64(in int64) (string, error) {
return Encrypt([]byte(strconv.FormatInt(in, 10)))
func EncryptInt64(in int64) (*string, error) {
out, err := Encrypt([]byte(strconv.FormatInt(in, 10)))
return &out, err
}
func DecryptInt64(in string) (int64, error) {
@@ -68,13 +69,32 @@ func DecryptInt64(in string) (int64, error) {
return strconv.ParseInt(string(out), 10, 64)
}
func RechargeBalance(balance string, amount int64) (string, error) {
balanceInt, err := DecryptInt64(balance)
func RechargeBalance(rawBalance *string, amount int64) error {
balanceInt, err := DecryptInt64(*rawBalance)
if err != nil {
return "", fmt.Errorf("failed to recharge balance: %w", err)
return fmt.Errorf("failed to recharge balance: %w", err)
}
balanceInt += amount
return EncryptInt64(balanceInt)
encryptBalance, err := EncryptInt64(balanceInt)
if err != nil {
return fmt.Errorf("failed to recharge balance: %w", err)
}
*rawBalance = *encryptBalance
return nil
}
func DeductBalance(balance *string, amount int64) error {
balanceInt, err := DecryptInt64(*balance)
if err != nil {
return fmt.Errorf("failed to deduct balance: %w", err)
}
balanceInt -= amount
encryptBalance, err := EncryptInt64(balanceInt)
if err != nil {
return fmt.Errorf("failed to deduct balance: %w", err)
}
*balance = *encryptBalance
return nil
}
// Decrypt decrypts the given ciphertext using AES-GCM.
+61
View File
@@ -0,0 +1,61 @@
package crypto
import (
"testing"
)
func TestRechargeBalance(t *testing.T) {
rawInt := int64(100_000_000)
addInt := int64(200_000_000)
expectedInt := int64(300_000_000)
account := createEncryptedAccount(t, rawInt)
rechargeAccount(t, account, addInt)
validateAccount(t, account, expectedInt, "recharge balance failed")
}
func TestDeductBalance(t *testing.T) {
rawInt := int64(300_000_000)
deductInt := int64(200_000_000)
expectedInt := int64(100_000_000)
account := createEncryptedAccount(t, rawInt)
deductAccount(t, account, deductInt)
validateAccount(t, account, expectedInt, "deduct balance failed")
}
func createEncryptedAccount(t *testing.T, rawInt int64) *string {
account, err := EncryptInt64(rawInt)
if err != nil {
t.Fatal(err)
}
return account
}
func rechargeAccount(t *testing.T, account *string, amount int64) {
err := RechargeBalance(account, amount)
if err != nil {
t.Fatal(err)
}
}
func deductAccount(t *testing.T, account *string, amount int64) {
err := DeductBalance(account, amount)
if err != nil {
t.Fatal(err)
}
}
func validateAccount(t *testing.T, account *string, expectedInt int64, errMsg string) {
decryptedInt, err := DecryptInt64(*account)
if err != nil {
t.Fatal(err)
}
if decryptedInt != expectedInt {
t.Fatalf("%s: expected %d, got %d", errMsg, expectedInt, decryptedInt)
}
}