From 5c2a6e5e9f91d4e2eddcf4213e130bc880cf8269 Mon Sep 17 00:00:00 2001 From: jiahui Date: Tue, 4 Jul 2023 18:11:06 +0800 Subject: [PATCH] optimize transfer controller, add encryption to account and adapt to existing environment --- controllers/account/api/v1/account_types.go | 4 +- controllers/account/api/v1/transfer_types.go | 19 ++- .../crd/bases/account.sealos.io_accounts.yaml | 6 + .../bases/account.sealos.io_transfers.yaml | 68 ++++++++ .../config/rbac/transfer_editor_role.yaml | 24 +++ .../config/rbac/transfer_viewer_role.yaml | 20 +++ .../config/samples/account_v1_transfer.yaml | 6 + .../account/controllers/account_controller.go | 161 +++++++++++------- .../controllers/transfer_controller.go | 10 +- .../account/deploy/manifests/deploy.yaml | 8 + controllers/pkg/crypto/crypto.go | 20 ++- 11 files changed, 273 insertions(+), 73 deletions(-) create mode 100644 controllers/account/config/crd/bases/account.sealos.io_transfers.yaml create mode 100644 controllers/account/config/rbac/transfer_editor_role.yaml create mode 100644 controllers/account/config/rbac/transfer_viewer_role.yaml create mode 100644 controllers/account/config/samples/account_v1_transfer.yaml diff --git a/controllers/account/api/v1/account_types.go b/controllers/account/api/v1/account_types.go index b6143238d00..bd646684138 100644 --- a/controllers/account/api/v1/account_types.go +++ b/controllers/account/api/v1/account_types.go @@ -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"` } diff --git a/controllers/account/api/v1/transfer_types.go b/controllers/account/api/v1/transfer_types.go index 823f5bc515f..ead609e1079 100644 --- a/controllers/account/api/v1/transfer_types.go +++ b/controllers/account/api/v1/transfer_types.go @@ -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)) + `" + } +}` +} diff --git a/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml b/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml index ec28fe5a512..7c95f390cc2 100644 --- a/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml +++ b/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml @@ -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 diff --git a/controllers/account/config/crd/bases/account.sealos.io_transfers.yaml b/controllers/account/config/crd/bases/account.sealos.io_transfers.yaml new file mode 100644 index 00000000000..da28f973f1e --- /dev/null +++ b/controllers/account/config/crd/bases/account.sealos.io_transfers.yaml @@ -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: [] diff --git a/controllers/account/config/rbac/transfer_editor_role.yaml b/controllers/account/config/rbac/transfer_editor_role.yaml new file mode 100644 index 00000000000..25ec128cf60 --- /dev/null +++ b/controllers/account/config/rbac/transfer_editor_role.yaml @@ -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 diff --git a/controllers/account/config/rbac/transfer_viewer_role.yaml b/controllers/account/config/rbac/transfer_viewer_role.yaml new file mode 100644 index 00000000000..a73a9e13538 --- /dev/null +++ b/controllers/account/config/rbac/transfer_viewer_role.yaml @@ -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 diff --git a/controllers/account/config/samples/account_v1_transfer.yaml b/controllers/account/config/samples/account_v1_transfer.yaml new file mode 100644 index 00000000000..0de98376130 --- /dev/null +++ b/controllers/account/config/samples/account_v1_transfer.yaml @@ -0,0 +1,6 @@ +apiVersion: account.sealos.io/v1 +kind: Transfer +metadata: + name: transfer-sample +spec: + # TODO(user): Add fields here diff --git a/controllers/account/controllers/account_controller.go b/controllers/account/controllers/account_controller.go index d91e0b29a3f..2563e72fca2 100644 --- a/controllers/account/controllers/account_controller.go +++ b/controllers/account/controllers/account_controller.go @@ -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 + account.Status.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 + account.Status.EncryptBalance, 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: + account.Status.EncryptBalance, err = crypto.RechargeBalance(account.Status.EncryptBalance, accountBalance.Spec.Amount) + if err != nil { + r.Logger.Error(err, err.Error()) + return err + } + case accountv1.TransferOut: + account.Status.EncryptBalance, err = crypto.DeductBalance(account.Status.EncryptBalance, accountBalance.Spec.Amount) + if err != nil { + r.Logger.Error(err, err.Error()) + return err + } + case accountv1.Consumption: + account.Status.EncryptDeductionBalance, 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,34 @@ func (r *AccountReconciler) updateDeductionBalance(ctx context.Context, accountB return nil } +func (r *AccountReconciler) updateAccountStatus(ctx context.Context, account *accountv1.Account) (err error) { + account.Status.Balance, err = crypto.DecryptInt64(*account.Status.EncryptBalance) + if err != nil { + return fmt.Errorf("update decrypt balance failed: %v", err) + } + account.Status.DeductionBalance, err = crypto.DecryptInt64(*account.Status.EncryptDeductionBalance) + if err != nil { + return fmt.Errorf("update decrypt deduction balance failed: %v", err) + } + return r.Status().Update(ctx, account) +} + +func (r *AccountReconciler) initBalance(account *accountv1.Account) (err error) { + if account.Status.EncryptBalance == nil { + account.Status.EncryptBalance, err = crypto.EncryptInt64(account.Status.Balance) + if err != nil { + return fmt.Errorf("sync encrypt balance failed: %v", err) + } + } + if account.Status.EncryptDeductionBalance == nil { + account.Status.EncryptDeductionBalance, err = crypto.EncryptInt64(account.Status.DeductionBalance) + if err != nil { + return fmt.Errorf("sync encrypt deduction balance failed: %v", err) + } + } + 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 +459,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 +516,5 @@ func giveGift(amount int64) int64 { default: ratio = Ratio5 } - return amount * ratio / 100 + return (amount * ratio / 100) + amount } diff --git a/controllers/account/controllers/transfer_controller.go b/controllers/account/controllers/transfer_controller.go index b754933887c..7518fcffe24 100644 --- a/controllers/account/controllers/transfer_controller.go +++ b/controllers/account/controllers/transfer_controller.go @@ -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{} diff --git a/controllers/account/deploy/manifests/deploy.yaml b/controllers/account/deploy/manifests/deploy.yaml index 33633deee93..60fb7430136 100644 --- a/controllers/account/deploy/manifests/deploy.yaml +++ b/controllers/account/deploy/manifests/deploy.yaml @@ -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: diff --git a/controllers/pkg/crypto/crypto.go b/controllers/pkg/crypto/crypto.go index 1861c76b474..a1fdc918bb2 100644 --- a/controllers/pkg/crypto/crypto.go +++ b/controllers/pkg/crypto/crypto.go @@ -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,15 +69,24 @@ 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(balance *string, amount int64) (*string, error) { + balanceInt, err := DecryptInt64(*balance) if err != nil { - return "", fmt.Errorf("failed to recharge balance: %w", err) + return nil, fmt.Errorf("failed to recharge balance: %w", err) } balanceInt += amount return EncryptInt64(balanceInt) } +func DeductBalance(balance *string, amount int64) (*string, error) { + balanceInt, err := DecryptInt64(*balance) + if err != nil { + return nil, fmt.Errorf("failed to deduct balance: %w", err) + } + balanceInt -= amount + return EncryptInt64(balanceInt) +} + // Decrypt decrypts the given ciphertext using AES-GCM. func Decrypt(ciphertextBase64 string) ([]byte, error) { ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64)