diff --git a/controllers/account/api/v1/debt_types.go b/controllers/account/api/v1/debt_types.go index 5a23d350ea0..563ccde6a58 100644 --- a/controllers/account/api/v1/debt_types.go +++ b/controllers/account/api/v1/debt_types.go @@ -59,6 +59,7 @@ const ( // DebtSpec defines the desired state of Debt type DebtSpec struct { UserName string `json:"userName,omitempty"` + UserID string `json:"userID,omitempty"` } // DebtStatus defines the observed state of Debt diff --git a/controllers/account/api/v1/payment_types.go b/controllers/account/api/v1/payment_types.go index a1fe9d1410c..bd287b4b571 100644 --- a/controllers/account/api/v1/payment_types.go +++ b/controllers/account/api/v1/payment_types.go @@ -45,6 +45,8 @@ type PaymentSpec struct { // UserID is the user id who want to recharge UserID string `json:"userID,omitempty"` + // UserCr is the user cr name who want to recharge + UserCR string `json:"userCR,omitempty"` // Amount is the amount of recharge Amount int64 `json:"amount,omitempty"` // e.g. wechat, alipay, creditcard, etc. diff --git a/controllers/account/config/crd/bases/account.sealos.io_debts.yaml b/controllers/account/config/crd/bases/account.sealos.io_debts.yaml index 28508b2ff9f..ae0db6f8040 100644 --- a/controllers/account/config/crd/bases/account.sealos.io_debts.yaml +++ b/controllers/account/config/crd/bases/account.sealos.io_debts.yaml @@ -55,6 +55,8 @@ spec: properties: userName: type: string + userID: + type: string type: object status: description: DebtStatus defines the observed state of Debt diff --git a/controllers/account/config/crd/bases/account.sealos.io_payments.yaml b/controllers/account/config/crd/bases/account.sealos.io_payments.yaml index 410b07ba695..d5e0ad03a3a 100644 --- a/controllers/account/config/crd/bases/account.sealos.io_payments.yaml +++ b/controllers/account/config/crd/bases/account.sealos.io_payments.yaml @@ -60,6 +60,9 @@ spec: userID: description: UserID is the user id who want to recharge type: string + userCR: + description: UserCr is the user cr name who want to recharge + type: string type: object status: description: PaymentStatus defines the observed state of Payment diff --git a/controllers/account/controllers/account_controller.go b/controllers/account/controllers/account_controller.go index 707c2d9268c..b20dbbaf664 100644 --- a/controllers/account/controllers/account_controller.go +++ b/controllers/account/controllers/account_controller.go @@ -27,8 +27,6 @@ import ( "strings" "time" - "sigs.k8s.io/controller-runtime/pkg/event" - "go.mongodb.org/mongo-driver/bson/primitive" "github.com/google/uuid" @@ -46,7 +44,6 @@ import ( accountv1 "github.com/labring/sealos/controllers/account/api/v1" "github.com/labring/sealos/controllers/pkg/database" - "github.com/labring/sealos/controllers/pkg/pay" "github.com/labring/sealos/controllers/pkg/resources" pkgtypes "github.com/labring/sealos/controllers/pkg/types" "github.com/labring/sealos/controllers/pkg/utils/env" @@ -60,7 +57,6 @@ import ( "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/handler" ) const ( @@ -99,11 +95,6 @@ type AccountReconciler struct { //+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - //It should not stop the normal process for the failure to delete the payment - // delete payments that exist for more than 5 minutes - if err := r.DeletePayment(ctx); err != nil { - r.Logger.Error(err, "delete payment failed") - } user := &userv1.User{} owner := "" if err := r.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, user); err == nil { @@ -122,77 +113,6 @@ func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, err } - payment := &accountv1.Payment{} - if err := r.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, payment); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - if payment.Spec.UserID == "" || payment.Spec.Amount == 0 { - return ctrl.Result{}, fmt.Errorf("payment is invalid: %v", payment) - } - if payment.Status.TradeNO == "" { - return ctrl.Result{Requeue: true, RequeueAfter: time.Millisecond * 300}, nil - } - if payment.Status.Status == pay.PaymentSuccess { - return ctrl.Result{}, nil - } - - account, err := r.syncAccount(ctx, getUsername(payment.Spec.UserID), payment.Namespace) - if err != nil { - return ctrl.Result{}, fmt.Errorf("get account failed: %v", err) - } - - // get payment handler - payHandler, err := pay.NewPayHandler(payment.Spec.PaymentMethod) - if err != nil { - r.Logger.Error(err, "get payment handler failed") - return ctrl.Result{}, err - } - // get payment details(status, amount) - // TODO The GetPaymentDetails may cause issues when using Stripe - status, orderAmount, err := payHandler.GetPaymentDetails(payment.Status.TradeNO) - if err != nil { - return ctrl.Result{}, fmt.Errorf("query order failed: %v", err) - } - r.Logger.V(1).Info("query order details", "orderStatus", status, "orderAmount", orderAmount) - switch status { - case pay.PaymentSuccess: - //1¥ = 100WechatPayAmount; 1 WechatPayAmount = 10000 SealosAmount - payAmount := orderAmount * 10000 - gift, err := r.getAmountWithRates(payAmount, account) - if err != nil { - r.Logger.Error(err, "get gift error") - } - 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) - } - - case pay.PaymentProcessing, pay.PaymentNotPaid: - return ctrl.Result{Requeue: true, RequeueAfter: time.Second}, nil - case pay.PaymentFailed, pay.PaymentExpired: - if err := r.Delete(ctx, payment); err != nil { - return ctrl.Result{}, fmt.Errorf("delete payment failed: %v", err) - } - return ctrl.Result{}, nil - default: - return ctrl.Result{}, fmt.Errorf("unknown status: %v", err) - } - return ctrl.Result{}, nil } @@ -246,82 +166,16 @@ func (r *AccountReconciler) adaptEphemeralStorageLimitRange(ctx context.Context, }) } -// DeletePayment delete payments that exist for more than 5 minutes -func (r *AccountReconciler) DeletePayment(ctx context.Context) error { - payments := &accountv1.PaymentList{} - err := r.List(ctx, payments) - if err != nil { - return err - } - for _, payment := range payments.Items { - //get payment handler - payHandler, err := pay.NewPayHandler(payment.Spec.PaymentMethod) - if err != nil { - r.Logger.Error(err, "get payment handler failed") - return err - } - //delete payment if it is exist for more than 5 minutes - if time.Since(payment.CreationTimestamp.Time) > time.Minute*5 { - if payment.Status.TradeNO != "" { - status, amount, err := payHandler.GetPaymentDetails(payment.Status.TradeNO) - if err != nil { - r.Logger.Error(err, "get payment details failed") - } - if status == pay.PaymentSuccess { - if payment.Status.Status != pay.PaymentSuccess { - continue - } - r.Logger.Info("payment success, post delete payment cr", "payment", payment, "amount", amount) - } - // expire session - if err = payHandler.ExpireSession(payment.Status.TradeNO); err != nil { - r.Logger.Error(err, "cancel payment failed") - } - } - if err := r.Delete(ctx, &payment); err != nil { - return err - } - } - } - 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(OnlyCreatePredicate{})). - Watches(&accountv1.Payment{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(PaymentPredicate{})). WithOptions(rateOpts). Complete(r) } -type PaymentPredicate struct{} - -func (PaymentPredicate) Create(e event.CreateEvent) bool { - if payment, ok := e.Object.(*accountv1.Payment); ok { - fmt.Println("payment create", payment.Status.TradeNO, payment.Status.Status) - return payment.Status.TradeNO != "" && payment.Status.Status != pay.PaymentSuccess - } - return false -} - -func (PaymentPredicate) Update(e event.UpdateEvent) bool { - if payment, ok := e.ObjectNew.(*accountv1.Payment); ok { - return payment.Status.TradeNO != "" && payment.Status.Status != pay.PaymentSuccess - } - return false -} - -func (PaymentPredicate) Delete(_ event.DeleteEvent) bool { - return false -} - -func (PaymentPredicate) Generic(_ event.GenericEvent) bool { - return false -} - func RawParseRechargeConfig() (activities pkgtypes.Activities, discountsteps []int64, discountratios []float64, returnErr error) { // local test //config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG")) @@ -378,49 +232,49 @@ func parseConfigList(s string, list interface{}, configName string) error { return nil } -func GetUserOwner(user *userv1.User) string { - own := user.Annotations[userv1.UserAnnotationOwnerKey] - if own == "" { - return user.Name - } - return own -} +//func GetUserOwner(user *userv1.User) string { +// own := user.Annotations[userv1.UserAnnotationOwnerKey] +// if own == "" { +// return user.Name +// } +// return own +//} const BaseUnit = 1_000_000 -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 - - 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 discount == nil || discount.DiscountSteps == nil || discount.DiscountRates == nil { - return getAmountWithDiscount(amount, r.DefaultDiscount), nil - } - return getAmountWithDiscount(amount, *discount), nil -} +//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 +// +// 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 discount == nil || discount.DiscountSteps == nil || discount.DiscountRates == nil { +// return getAmountWithDiscount(amount, r.DefaultDiscount), nil +// } +// return getAmountWithDiscount(amount, *discount), nil +//} func getAmountWithDiscount(amount int64, discount pkgtypes.RechargeDiscount) int64 { if discount.SpecialDiscount != nil && discount.SpecialDiscount[amount/BaseUnit] != 0 { diff --git a/controllers/account/controllers/debt_controller.go b/controllers/account/controllers/debt_controller.go index d9494f59d01..bb350268660 100644 --- a/controllers/account/controllers/debt_controller.go +++ b/controllers/account/controllers/debt_controller.go @@ -134,11 +134,27 @@ func (r *DebtReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. if payment.Status.Status != pay.PaymentSuccess { return ctrl.Result{RequeueAfter: 10 * time.Second}, nil } - reconcileErr = r.reconcile(ctx, payment.Spec.UserID) + reconcileErr = r.reconcile(ctx, payment.Spec.UserCR, payment.Spec.UserID) } else if client.IgnoreNotFound(err) != nil { return ctrl.Result{}, fmt.Errorf("failed to get payment %s: %v", req.Name, err) } else { - reconcileErr = r.reconcile(ctx, req.NamespacedName.Name) + cr, err := r.AccountV2.GetUserCr(&pkgtypes.UserQueryOpts{Owner: req.NamespacedName.Name}) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + r.Logger.Info("user cr not exist, skip", "user", req.NamespacedName.Name) + return ctrl.Result{}, nil + } + return ctrl.Result{RequeueAfter: 10 * time.Minute}, fmt.Errorf("failed to get user cr %s: %v", req.NamespacedName.Name, err) + } + user, err := r.AccountV2.GetUser(&pkgtypes.UserQueryOpts{Owner: req.NamespacedName.Name, UID: cr.UserUID}) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + r.Logger.Info("user not exist, skip", "user", req.NamespacedName.Name) + return ctrl.Result{}, nil + } + return ctrl.Result{RequeueAfter: 10 * time.Minute}, fmt.Errorf("failed to get user %s: %v", req.NamespacedName.Name, err) + } + reconcileErr = r.reconcile(ctx, req.NamespacedName.Name, user.ID) } if reconcileErr != nil { if reconcileErr == ErrAccountNotExist { @@ -150,22 +166,38 @@ func (r *DebtReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. return ctrl.Result{RequeueAfter: r.DebtDetectionCycle}, nil } -func (r *DebtReconciler) reconcile(ctx context.Context, owner string) error { +//func (r *DebtReconciler) getNamespaceOwner(namespace string) (string, error) { +// ns := &corev1.Namespace{} +// if err := r.Get(context.Background(), client.ObjectKey{Name: namespace}, ns); err != nil { +// return "", fmt.Errorf("failed to get namespace %s: %v", namespace, err) +// } +// if ns.Labels == nil { +// return "", fmt.Errorf("namespace %s labels is nil", namespace) +// } +// owner, ok := ns.Labels[userv1.UserAnnotationOwnerKey] +// if !ok { +// return "", fmt.Errorf("namespace %s owner is not exist", namespace) +// } +// return owner, nil +//} + +func (r *DebtReconciler) reconcile(ctx context.Context, userCr, userID string) error { debt := &accountv1.Debt{} - account, err := r.AccountV2.GetAccount(&pkgtypes.UserQueryOpts{Owner: owner}) + userQueryOpts := &pkgtypes.UserQueryOpts{Owner: userCr, ID: userID} + account, err := r.AccountV2.GetAccount(userQueryOpts) if account == nil { if errors.Is(err, gorm.ErrRecordNotFound) { - _, err = r.AccountV2.NewAccount(&pkgtypes.UserQueryOpts{Owner: owner}) + _, err = r.AccountV2.NewAccount(userQueryOpts) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("failed to create account %s: %v", owner, err) + return fmt.Errorf("failed to create account %v: %v", userQueryOpts, err) } userOwner := &userv1.User{} - if err := r.Get(ctx, types.NamespacedName{Name: owner, Namespace: r.accountSystemNamespace}, userOwner); err != nil { + if err := r.Get(ctx, types.NamespacedName{Name: userCr, Namespace: r.accountSystemNamespace}, userOwner); err != nil { // if user not exist, skip if client.IgnoreNotFound(err) == nil { return nil } - return fmt.Errorf("failed to get user %s: %v", owner, err) + return fmt.Errorf("failed to get usercr %s: %v", userCr, err) } // if user not exist, skip if userOwner.CreationTimestamp.Add(20 * 24 * time.Hour).Before(time.Now()) { @@ -173,29 +205,36 @@ func (r *DebtReconciler) reconcile(ctx context.Context, owner string) error { } } if err != nil { - r.Logger.Error(fmt.Errorf("account %s not exist", owner), err.Error()) + r.Logger.Error(fmt.Errorf("account %v not exist", userQueryOpts), err.Error()) } return ErrAccountNotExist } if account.CreateRegionID == "" { if err = r.AccountV2.SetAccountCreateLocalRegion(account, r.LocalRegionID); err != nil { - return fmt.Errorf("failed to set account %s create region: %v", owner, err) + return fmt.Errorf("failed to set account %v create region: %v", userQueryOpts, err) } } // In a multi-region scenario, select the region where the account is created for SMS notification smsEnable := account.CreateRegionID == r.LocalRegionID //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 { + if err := r.Get(ctx, client.ObjectKey{Name: GetDebtName(userCr), Namespace: r.accountSystemNamespace}, debt); client.IgnoreNotFound(err) != nil { return err } else if err != nil { - if err := r.syncDebt(ctx, owner, debt); err != nil { + if err := r.syncDebt(ctx, userCr, userID, debt); err != nil { return err } //r.Logger.Info("create or update debt success", "debt", debt) } + // backward compatibility + if debt.Spec.UserID == "" { + debt.Spec.UserID = userID + if err := r.Update(ctx, debt); err != nil { + return fmt.Errorf("update debt %s failed: %v", debt.Name, err) + } + } - nsList, err := getOwnNsList(r.Client, getUsername(owner)) + nsList, err := getOwnNsList(r.Client, getUsername(userCr)) if err != nil { r.Logger.Error(err, "get own ns list error") return fmt.Errorf("get own ns list error: %v", err) @@ -363,11 +402,12 @@ func (r *DebtReconciler) reconcileDebtStatus(ctx context.Context, debt *accountv return nil } -func (r *DebtReconciler) syncDebt(ctx context.Context, owner string, debt *accountv1.Debt) error { +func (r *DebtReconciler) syncDebt(ctx context.Context, owner, userID 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 = owner + debt.Spec.UserID = userID return nil }); err != nil { return err diff --git a/controllers/account/controllers/namespace_controller.go b/controllers/account/controllers/namespace_controller.go index 4b05a627af5..b874def3688 100644 --- a/controllers/account/controllers/namespace_controller.go +++ b/controllers/account/controllers/namespace_controller.go @@ -23,6 +23,8 @@ import ( "strings" "time" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "github.com/minio/madmin-go/v3" v1 "github.com/labring/sealos/controllers/account/api/v1" @@ -205,9 +207,13 @@ func (r *NamespaceReconciler) suspendKBCluster(ctx context.Context, namespace st ops.Namespace = kbCluster.Namespace ops.ObjectMeta.Name = "stop-" + kbCluster.Name + "-" + time.Now().Format("2006-01-02-15") ops.Spec.TTLSecondsAfterSucceed = 1 + abort := int32(60 * 60) + ops.Spec.TTLSecondsBeforeAbort = &abort ops.Spec.ClusterRef = kbCluster.Name ops.Spec.Type = "Stop" - err := r.Client.Create(ctx, &ops) + _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &ops, func() error { + return nil + }) if err != nil { r.Log.Error(err, "create ops request failed", "ops", ops.Name, "namespace", ops.Namespace) } @@ -317,7 +323,7 @@ func (r *NamespaceReconciler) resumePod(ctx context.Context, namespace string) e func (r *NamespaceReconciler) recreatePod(ctx context.Context, oldPod corev1.Pod, newPod *corev1.Pod) error { list := corev1.PodList{} - watcher, err := r.Client.Watch(ctx, &list) + watcher, err := r.Client.Watch(ctx, &list, client.InNamespace(oldPod.Namespace)) if err != nil { return fmt.Errorf("failed to start watch stream for pod %s: %w", oldPod.Name, err) } diff --git a/controllers/account/controllers/payment_controller.go b/controllers/account/controllers/payment_controller.go index 0fc3bcbbe2e..5f0ab3223b9 100644 --- a/controllers/account/controllers/payment_controller.go +++ b/controllers/account/controllers/payment_controller.go @@ -18,21 +18,22 @@ package controllers import ( "context" + "fmt" "os" + "sync" "time" - "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/labring/sealos/controllers/pkg/pay" + pkgtypes "github.com/labring/sealos/controllers/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/builder" + "github.com/labring/sealos/controllers/pkg/pay" "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" ) @@ -40,75 +41,282 @@ import ( // PaymentReconciler reconciles a Payment object type PaymentReconciler struct { client.Client - Scheme *runtime.Scheme - Logger logr.Logger - domain string + Account *AccountReconciler + WatchClient client.WithWatch + Scheme *runtime.Scheme + Logger logr.Logger + reconcileDuration time.Duration + createDuration time.Duration + domain string } +var ( + // Ensure PaymentReconciler implements the LeaderElectionRunnable and Runnable interface + _ manager.LeaderElectionRunnable = &PaymentReconciler{} + _ manager.Runnable = &PaymentReconciler{} +) + +const ( + EnvPaymentReconcileDuration = "PAYMENT_RECONCILE_DURATION" + EnvPaymentCreateDuration = "PAYMENT_CREATE_DURATION" + + defaultReconcileDuration = 10 * time.Second + defaultCreateDuration = 5 * time.Second +) + //+kubebuilder:rbac:groups=account.sealos.io,resources=payments,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=account.sealos.io,resources=payments/status,verbs=get;update;patch //+kubebuilder:rbac:groups=account.sealos.io,resources=payments/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 *PaymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Logger = log.FromContext(ctx) +// SetupWithManager sets up the controller with the Manager. +func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) error { + const controllerName = "payment_controller" + r.Logger = ctrl.Log.WithName(controllerName) + r.Logger.V(1).Info("init reconcile controller payment") + r.domain = os.Getenv("DOMAIN") + r.reconcileDuration = defaultReconcileDuration + r.createDuration = defaultCreateDuration + if duration := os.Getenv(EnvPaymentReconcileDuration); duration != "" { + reconcileDuration, err := time.ParseDuration(duration) + if err == nil { + r.reconcileDuration = reconcileDuration + } + } + if duration := os.Getenv(EnvPaymentCreateDuration); duration != "" { + createDuration, err := time.ParseDuration(duration) + if err == nil { + r.createDuration = createDuration + } + } + r.Logger.V(1).Info("reconcile duration", "reconcileDuration", r.reconcileDuration, "createDuration", r.createDuration) + if err := mgr.Add(r); err != nil { + return fmt.Errorf("add payment controller failed: %w", err) + } + return nil +} + +// LeaderElectionRunnable knows if a Runnable needs to be run in the leader election mode. +func (r *PaymentReconciler) NeedLeaderElection() bool { + return true +} - p := &accountv1.Payment{} - if err := r.Get(ctx, req.NamespacedName, p); err != nil { - r.Logger.Error(err, "get payment failed") - return ctrl.Result{}, client.IgnoreNotFound(err) +func (r *PaymentReconciler) Start(ctx context.Context) error { + var wg sync.WaitGroup + defer wg.Wait() + fc := func(wg *sync.WaitGroup, t *time.Ticker, reconcileFunc func(ctx context.Context) []error) { + wg.Add(1) + defer wg.Done() + for { + select { + case <-t.C: + if errs := reconcileFunc(ctx); len(errs) > 0 { + for _, err := range errs { + r.Logger.Error(err, "reconcile payments failed") + } + } + case <-ctx.Done(): + return + } + } + } + tickerReconcilePayment := time.NewTicker(r.reconcileDuration) + tickerNewPayment := time.NewTicker(r.createDuration) + go fc(&wg, tickerReconcilePayment, r.reconcilePayments) + go fc(&wg, tickerNewPayment, r.reconcileCreatePayments) + return nil +} + +func (r *PaymentReconciler) reconcilePayments(_ context.Context) (errs []error) { + paymentList := &accountv1.PaymentList{} + err := r.Client.List(context.Background(), paymentList, &client.ListOptions{}) + if err != nil { + errs = append(errs, fmt.Errorf("watch payment failed: %w", err)) + return + } + for _, payment := range paymentList.Items { + if err := r.reconcilePayment(&payment); err != nil { + errs = append(errs, fmt.Errorf("reconcile payment failed: payment: %s, user: %s, err: %w", payment.Name, payment.Spec.UserID, err)) + } } - if p.Status.TradeNO != "" { - return ctrl.Result{}, nil + return +} + +func (r *PaymentReconciler) reconcileCreatePayments(ctx context.Context) (errs []error) { + //paymentList := &accountv1.PaymentList{} + //listOpts := &client.ListOptions{ + // FieldSelector: fields.OneTermEqualSelector("status.tradeNO", ""), + //} + //// handler old payment + //err := r.Client.List(context.Background(), paymentList, listOpts) + //if err != nil { + // errs = append(errs, fmt.Errorf("watch payment failed: %w", err)) + // return + //} + //for _, payment := range paymentList.Items { + // if err := r.reconcileNewPayment(&payment); err != nil { + // errs = append(errs, fmt.Errorf("reconcile payment failed: payment: %s, user: %s, err: %w", payment.Name, payment.Spec.UserID, err)) + // } + //} + // watch new payment + watcher, err := r.WatchClient.Watch(context.Background(), &accountv1.PaymentList{}, &client.ListOptions{}) + if err != nil { + errs = append(errs, fmt.Errorf("watch payment failed: %w", err)) + return } - if p.Status.Status == "" { - p.Status.Status = "Created" - if err := r.Status().Update(ctx, p); err != nil { - r.Logger.Error(err, "update payment failed: %v", "payment", *p) - return ctrl.Result{Requeue: true}, err + select { + case <-ctx.Done(): + return + case event := <-watcher.ResultChan(): + if event.Object == nil { + break + } + payment, ok := event.Object.(*accountv1.Payment) + if !ok { + errs = append(errs, fmt.Errorf("convert payment failed: %v", event.Object)) + break + } + if err := r.reconcileNewPayment(payment); err != nil { + errs = append(errs, fmt.Errorf("reconcile payment failed: payment: %s, user: %s, err: %w", payment.Name, payment.Spec.UserID, err)) } } + return +} +func (r *PaymentReconciler) reconcilePayment(payment *accountv1.Payment) error { + if payment.Status.TradeNO == "" { + if err := r.reconcileNewPayment(payment); err != nil { + return fmt.Errorf("reconcile new payment failed: %w", err) + } + return nil + } + if payment.Status.Status == pay.PaymentSuccess { + if err := r.expiredOvertimePayment(payment); err != nil { + return fmt.Errorf("expired payment failed: %w", err) + } + return nil + } // get payment handler - payHandler, err := pay.NewPayHandler(p.Spec.PaymentMethod) + payHandler, err := pay.NewPayHandler(payment.Spec.PaymentMethod) if err != nil { - r.Logger.Error(err, "get payment Interface failed") - return ctrl.Result{}, err + return fmt.Errorf("get payment Interface failed: %w", err) } - // get tradeNO and codeURL - tradeNO, codeURL, err := payHandler.CreatePayment(p.Spec.Amount/10000, p.Spec.UserID, "sealos cloud pay [domain="+r.domain+"]") + // TODO The GetPaymentDetails may cause issues when using Stripe + status, orderAmount, err := payHandler.GetPaymentDetails(payment.Status.TradeNO) if err != nil { - r.Logger.Error(err, "get tradeNO and codeURL failed") - return ctrl.Result{Requeue: true, RequeueAfter: time.Second}, err + return fmt.Errorf("get payment details failed: %w", err) } - p.Status.CodeURL = codeURL - p.Status.TradeNO = tradeNO + switch status { + case pay.PaymentSuccess: + user, err := r.Account.AccountV2.GetUser(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID}) + if err != nil { + return fmt.Errorf("get user failed: %w", err) + } + //1¥ = 100WechatPayAmount; 1 WechatPayAmount = 10000 SealosAmount + payAmount := orderAmount * 10000 + gift := getAmountWithDiscount(payAmount, r.Account.DefaultDiscount) + if err = r.Account.AccountV2.Payment(&pkgtypes.Payment{ + PaymentRaw: pkgtypes.PaymentRaw{ + UserUID: user.UID, + Amount: payAmount, + Gift: gift, + CreatedAt: payment.CreationTimestamp.Time, + RegionUserOwner: getUsername(payment.Namespace), + Method: payment.Spec.PaymentMethod, + TradeNO: payment.Status.TradeNO, + CodeURL: payment.Status.CodeURL, + }, + }); err != nil { + return fmt.Errorf("payment failed: %w", err) + } + payment.Status.Status = pay.PaymentSuccess + if err := r.Status().Update(context.Background(), payment); err != nil { + return fmt.Errorf("update payment failed: %w", err) + } + //case pay.PaymentFailed, pay.PaymentExpired: + default: + if err := r.expiredOvertimePayment(payment); err != nil { + return fmt.Errorf("expired payment failed: %w", err) + } + } + return nil +} - if err := r.Status().Update(ctx, p); err != nil { - r.Logger.Error(err, "update payment failed: %v", "payment", *p) - return ctrl.Result{}, err +func (r *PaymentReconciler) expiredOvertimePayment(payment *accountv1.Payment) error { + if payment.CreationTimestamp.Time.Add(10 * time.Minute).After(time.Now()) { + return nil + } + payHandler, err := pay.NewPayHandler(payment.Spec.PaymentMethod) + if err != nil { + return fmt.Errorf("get payment Interface failed: %w", err) + } + currentStatus, _, err := payHandler.GetPaymentDetails(payment.Status.TradeNO) + if err != nil { + return fmt.Errorf("get payment details failed: %w", err) } - //qrterminal.Generate(codeURL, qrterminal.L, os.Stdout) - return ctrl.Result{}, nil + if payment.Status.Status != pay.PaymentSuccess { + // skip if payment is success paid + if currentStatus == pay.PaymentSuccess { + return nil + } + if err = payHandler.ExpireSession(payment.Status.TradeNO); err != nil { + r.Logger.Error(err, "cancel payment failed") + } + } + if err = r.Delete(context.Background(), payment); err != nil { + r.Logger.Error(err, "delete payment failed") + } + r.Logger.Info("payment expired", "payment", payment) + return nil } -// SetupWithManager sets up the controller with the Manager. -func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error { - 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). - Complete(r) +func (r *PaymentReconciler) reconcileNewPayment(payment *accountv1.Payment) error { + if payment.Status.TradeNO != "" { + return nil + } + // backward compatibility + if payment.Spec.UserCR == "" { + if payment.Spec.UserID == "" { + return fmt.Errorf("user ID is empty") + } + payment.Spec.UserCR = payment.Spec.UserID + user, err := r.Account.AccountV2.GetUser(&pkgtypes.UserQueryOpts{Owner: payment.Spec.UserCR}) + if err != nil { + return fmt.Errorf("get user failed: %w", err) + } + if user == nil { + return fmt.Errorf("user not found") + } + payment.Spec.UserID = user.ID + } + if err := r.Update(context.Background(), payment); err != nil { + return fmt.Errorf("create payment failed: %w", err) + } + // get user ID + account, err := r.Account.AccountV2.GetAccount(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID, IgnoreEmpty: true}) + if err != nil { + return fmt.Errorf("get account failed: %w", err) + } + if account == nil { + _, err := r.Account.AccountV2.NewAccount(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID}) + if err != nil { + return fmt.Errorf("create account failed: %w", err) + } + } + // get payment handler + payHandler, err := pay.NewPayHandler(payment.Spec.PaymentMethod) + if err != nil { + return fmt.Errorf("get payment Interface failed: %w", err) + } + tradeNO, codeURL, err := payHandler.CreatePayment(payment.Spec.Amount/10000, payment.Spec.UserID, "sealos cloud pay [domain="+r.domain+"]") + if err != nil { + return fmt.Errorf("get tradeNO and codeURL failed: %w", err) + } + payment.Status.CodeURL = codeURL + payment.Status.TradeNO = tradeNO + payment.Status.Status = pay.PaymentProcessing + if err = r.Status().Update(context.Background(), payment); err != nil { + return fmt.Errorf("update payment failed: %w", err) + } + return nil } diff --git a/controllers/account/deploy/manifests/deploy.yaml.tmpl b/controllers/account/deploy/manifests/deploy.yaml similarity index 97% rename from controllers/account/deploy/manifests/deploy.yaml.tmpl rename to controllers/account/deploy/manifests/deploy.yaml index bbf424605b4..3ed178e2e76 100644 --- a/controllers/account/deploy/manifests/deploy.yaml.tmpl +++ b/controllers/account/deploy/manifests/deploy.yaml @@ -1,3 +1,17 @@ +# 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. + apiVersion: v1 kind: Namespace metadata: @@ -362,6 +376,8 @@ spec: properties: userName: type: string + userID: + type: string type: object status: description: DebtStatus defines the observed state of Debt @@ -521,6 +537,9 @@ spec: userID: description: UserID is the user id who want to recharge type: string + userCR: + description: UserCr is the user cr name who want to recharge + type: string type: object status: description: PaymentStatus defines the observed state of Payment diff --git a/controllers/account/main.go b/controllers/account/main.go index 00121fae0c6..291aa96c392 100644 --- a/controllers/account/main.go +++ b/controllers/account/main.go @@ -81,7 +81,7 @@ func main() { flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&development, "development", false, "Enable development mode.") - flag.BoolVar(&enableLeaderElection, "leader-elect", false, + flag.BoolVar(&enableLeaderElection, "leader-elect", true, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.IntVar(&concurrent, "concurrent", 5, "The number of concurrent cluster reconciles.") @@ -207,12 +207,6 @@ func main() { if err = (accountReconciler).SetupWithManager(mgr, rateOpts); err != nil { setupManagerError(err, "Account") } - if err = (&controllers.PaymentReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr, rateOpts); err != nil { - setupManagerError(err, "Payment") - } if err = (&controllers.DebtReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -275,6 +269,16 @@ func main() { if err = (billingInfoQueryReconciler).SetupWithManager(mgr); err != nil { setupManagerError(err, "BillingInfoQuery") } + + if err = (&controllers.PaymentReconciler{ + Account: accountReconciler, + WatchClient: watchClient, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupManagerError(err, "Payment") + } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/controllers/devbox/internal/controller/devbox_controller.go b/controllers/devbox/internal/controller/devbox_controller.go index 015167947b9..5a110a8b58b 100644 --- a/controllers/devbox/internal/controller/devbox_controller.go +++ b/controllers/devbox/internal/controller/devbox_controller.go @@ -352,12 +352,14 @@ func (r *DevboxReconciler) generateDevboxPod(ctx context.Context, devbox *devbox }, } terminationGracePeriodSeconds := 300 + automountServiceAccountToken := false expectPod := &corev1.Pod{ ObjectMeta: objectMeta, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyNever, Containers: containers, TerminationGracePeriodSeconds: ptr.To(int64(terminationGracePeriodSeconds)), + AutomountServiceAccountToken: ptr.To(automountServiceAccountToken), }, } if err = controllerutil.SetControllerReference(devbox, expectPod, r.Scheme); err != nil { diff --git a/controllers/go.work.sum b/controllers/go.work.sum index 5bf887d0c3a..4d3a7d0b581 100644 --- a/controllers/go.work.sum +++ b/controllers/go.work.sum @@ -1235,6 +1235,7 @@ go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzox go.opentelemetry.io/otel v1.2.0 h1:YOQDvxO1FayUcT9MIhJhgMyNO1WqoduiyvQHzGN0kUQ= go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI= go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc= go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= @@ -1254,6 +1255,7 @@ go.opentelemetry.io/otel/internal/metric v0.25.0 h1:w/7RXe16WdPylaIXDgcYM6t/q0K5 go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= go.opentelemetry.io/otel/metric v0.25.0 h1:7cXOnCADUsR3+EOqxPaSKwhEuNu0gz/56dRN1hpIdKw= +go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA= go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= @@ -1265,6 +1267,7 @@ go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m go.opentelemetry.io/otel/sdk v1.2.0 h1:wKN260u4DesJYhyjxDa7LRFkuhH7ncEVKU37LWcyNIo= go.opentelemetry.io/otel/sdk v1.11.2/go.mod h1:wZ1WxImwpq+lVRo4vsmSOxdd+xwoUJ6rqyLc3SyX9aU= go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= +go.opentelemetry.io/otel/sdk v1.20.0 h1:5Jf6imeFZlZtKv9Qbo6qt2ZkmWtdWx/wzcCbNUlAWGM= go.opentelemetry.io/otel/sdk v1.20.0/go.mod h1:rmkSx1cZCm/tn16iWDn1GQbLtsW/LvsdEEFzCSRM6V0= go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= @@ -1277,6 +1280,7 @@ go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16g go.opentelemetry.io/otel/trace v1.2.0 h1:Ys3iqbqZhcf28hHzrm5WAquMkDHNZTUkw7KHbuNjej0= go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA= go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ= go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= @@ -1357,6 +1361,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1: google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM= google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0= google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8= diff --git a/controllers/pkg/database/cockroach/accountv2.go b/controllers/pkg/database/cockroach/accountv2.go index 8b8ba95da5e..dd5f98eca8e 100644 --- a/controllers/pkg/database/cockroach/accountv2.go +++ b/controllers/pkg/database/cockroach/accountv2.go @@ -113,7 +113,7 @@ func (c *Cockroach) GetUser(ops *types.UserQueryOpts) (*types.User, error) { } var user types.User if err := c.DB.Where(queryUser).First(&user).Error; err != nil { - return nil, fmt.Errorf("failed to get user: %v", err) + return nil, err } return &user, nil } diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 268652949b6..3fd2cfa279e 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -82,7 +82,7 @@ RUN if [ "$name" = "template" ]; then \ apk add --no-cache git openssh-client; \ fi RUN if [ "$name" = "desktop" ]; then \ - npm install -g prisma; \ + npm install -g prisma@5.10.2; \ fi USER nextjs @@ -100,4 +100,4 @@ ENV PORT 3000 ENV launchpath=./${path}/server.js -ENTRYPOINT ["dumb-init", "sh", "-c", "node ${launchpath}"] \ No newline at end of file +ENTRYPOINT ["dumb-init", "sh", "-c", "node ${launchpath}"] diff --git a/frontend/desktop/src/components/AppDock/index.tsx b/frontend/desktop/src/components/AppDock/index.tsx index 398b0c52a4f..4292d6f34b2 100644 --- a/frontend/desktop/src/components/AppDock/index.tsx +++ b/frontend/desktop/src/components/AppDock/index.tsx @@ -3,14 +3,14 @@ import useAppStore, { AppInfo } from '@/stores/app'; import { useConfigStore } from '@/stores/config'; import { useDesktopConfigStore } from '@/stores/desktopConfig'; import { APPTYPE, TApp } from '@/types'; +import { I18nCommonKey } from '@/types/i18next'; import { Box, Center, Flex, Image } from '@chakra-ui/react'; -import { MouseEvent, useContext, useMemo } from 'react'; +import { useTranslation } from 'next-i18next'; +import { MouseEvent, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Menu, useContextMenu } from 'react-contexify'; import { ChevronDownIcon } from '../icons'; -import styles from './index.module.css'; -import { useTranslation } from 'next-i18next'; import CustomTooltip from './CustomTooltip'; -import { I18nCommonKey } from '@/types/i18next'; +import styles from './index.module.css'; const APP_DOCK_MENU_ID = 'APP_DOCK_MENU_ID'; @@ -19,7 +19,6 @@ export default function AppDock() { const { installedApps: apps, runningInfo, - setToHighestLayerById, currentAppPid, openApp, switchAppById, @@ -28,7 +27,9 @@ export default function AppDock() { } = useAppStore(); const logo = useConfigStore().layoutConfig?.logo; const moreAppsContent = useContext(MoreAppsContext); - const { isNavbarVisible, toggleNavbarVisibility } = useDesktopConfigStore(); + const { isNavbarVisible, toggleNavbarVisibility, getTransitionValue } = useDesktopConfigStore(); + const [isMouseOverDock, setIsMouseOverDock] = useState(false); + const timeoutRef = useRef(null); const { show } = useContextMenu({ id: APP_DOCK_MENU_ID @@ -109,40 +110,76 @@ export default function AppDock() { event: e, position: { // @ts-ignore - x: '60px', + x: '244px', // @ts-ignore - y: '-114px' + y: '-34px' } }); }; - const transitionValue = 'transform 200ms ease-in-out, opacity 200ms ease-in-out'; + useEffect(() => { + if (!isMouseOverDock) { + const hasMaximizedApp = runningInfo.some((app) => app.size === 'maximize'); + toggleNavbarVisibility(!hasMaximizedApp); + } + }, [isMouseOverDock, runningInfo, toggleNavbarVisibility]); + + const handleMouseEnter = () => { + if (timeoutRef.current !== null) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + setIsMouseOverDock(true); + }; + + const handleMouseLeave = () => { + timeoutRef.current = window.setTimeout(() => { + setIsMouseOverDock(false); + }, 500); + }; return ( - -
- -
+ + {runningInfo.length > 0 && runningInfo.some((app) => app.size === 'maximize') && ( +
{ + toggleNavbarVisibility(); + }} + > + +
+ )} + displayMenu(e)} borderRadius="12px" @@ -157,13 +194,6 @@ export default function AppDock() { gap={'12px'} userSelect={'none'} px={'12px'} - transition={transitionValue} - opacity={isNavbarVisible ? 1 : 0} - position="absolute" - top={'-64px'} - transform={isNavbarVisible ? 'translate(-50%, 0)' : 'translate(-50%, 68px)'} - will-change="transform, opacity" - overflow="hidden" > {AppMenuLists.map((item: AppInfo, index: number) => { return ( @@ -199,6 +229,7 @@ export default function AppDock() { alt={item?.name} w="32px" h="32px" + draggable={false} /> + <> - + ); } diff --git a/frontend/desktop/src/stores/app.ts b/frontend/desktop/src/stores/app.ts index 4e447ceebd6..e2ab853a452 100644 --- a/frontend/desktop/src/stores/app.ts +++ b/frontend/desktop/src/stores/app.ts @@ -6,6 +6,7 @@ import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import AppStateManager from '../utils/ProcessManager'; +import { useDesktopConfigStore } from './desktopConfig'; export class AppInfo { pid: number; @@ -75,6 +76,7 @@ const useAppStore = create()( }, // should use pid to close app, but it don't support multi same app process now closeAppById: (pid: number) => { + useDesktopConfigStore.getState().temporarilyDisableAnimation(); set((state) => { state.runner.closeApp(pid); // make sure the process is killed @@ -123,6 +125,7 @@ const useAppStore = create()( }, openApp: async (app: TApp, { query, raw, pathname = '/', appSize = 'maximize' } = {}) => { + useDesktopConfigStore.getState().temporarilyDisableAnimation(); const zIndex = get().maxZIndex + 1; // debugger // 未支持多实例 diff --git a/frontend/desktop/src/stores/desktopConfig.ts b/frontend/desktop/src/stores/desktopConfig.ts index aa0235f11fe..46d86518265 100644 --- a/frontend/desktop/src/stores/desktopConfig.ts +++ b/frontend/desktop/src/stores/desktopConfig.ts @@ -5,24 +5,43 @@ import { immer } from 'zustand/middleware/immer'; type State = { isAppBar: boolean; isNavbarVisible: boolean; + isAnimationEnabled: boolean; toggleShape: () => void; - toggleNavbarVisibility: () => void; + toggleNavbarVisibility: (forceState?: boolean) => void; + temporarilyDisableAnimation: () => void; + getTransitionValue: () => string; }; export const useDesktopConfigStore = create()( persist( - immer((set) => ({ + immer((set, get) => ({ isAppBar: true, isNavbarVisible: true, + isAnimationEnabled: true, toggleShape() { set((state) => { state.isAppBar = !state.isAppBar; }); }, - toggleNavbarVisibility() { + toggleNavbarVisibility(forceState) { set((state) => { - state.isNavbarVisible = !state.isNavbarVisible; + state.isNavbarVisible = forceState !== undefined ? forceState : !state.isNavbarVisible; }); + }, + temporarilyDisableAnimation() { + set((state) => { + state.isAnimationEnabled = false; + }); + requestAnimationFrame(() => { + set((state) => { + state.isAnimationEnabled = true; + }); + }); + }, + getTransitionValue() { + return get().isAnimationEnabled + ? 'transform 200ms ease-in-out, opacity 200ms ease-in-out' + : 'none'; } })), { diff --git a/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx b/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx index 468490f31b3..5d09a9fe88b 100644 --- a/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx +++ b/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx @@ -258,7 +258,8 @@ const Form = ({ height={'80px'} border={'1px solid'} borderRadius={'6px'} - cursor={'pointer'} + cursor={isEdit ? 'not-allowed' : 'pointer'} + opacity={isEdit && getValues('dbType') !== item.id ? '0.4' : '1'} fontWeight={'bold'} color={'grayModern.900'} {...(getValues('dbType') === item.id @@ -275,6 +276,7 @@ const Form = ({ } })} onClick={() => { + if (isEdit) return; setValue('dbType', item.id); setValue('dbVersion', DBVersionMap[getValues('dbType')][0].id); }} @@ -303,6 +305,7 @@ const Form = ({