diff --git a/controllers/user/api/v1/user_webhook.go b/controllers/user/api/v1/user_webhook.go index 99c7edfe2833..8bd397a3f138 100644 --- a/controllers/user/api/v1/user_webhook.go +++ b/controllers/user/api/v1/user_webhook.go @@ -112,7 +112,11 @@ func (r *User) ValidateCreate(ctx context.Context, obj runtime.Object) (admissio if err := usercount.Init(ctx, userWebhookReader); err != nil { return admission.Warnings{}, err } - if !licensegate.AllowNewUser(usercount.Get()) { + currentCount, err := usercount.CountQuotaUsers(ctx, userWebhookReader) + if err != nil { + return admission.Warnings{}, err + } + if !licensegate.AllowNewUser(currentCount) { message := buildLicenseLimitErrorMessage() if licensegate.HasActiveLicense() { message = buildUserCountLimitErrorMessage() diff --git a/controllers/user/controllers/user_controller.go b/controllers/user/controllers/user_controller.go index 31613a65ad73..c377f4ecc8e1 100644 --- a/controllers/user/controllers/user_controller.go +++ b/controllers/user/controllers/user_controller.go @@ -67,10 +67,11 @@ const ( // UserReconciler reconciles a User object type UserReconciler struct { - Logger logr.Logger - Recorder record.EventRecorder - cache cache.Cache - config *rest.Config + Logger logr.Logger + Recorder record.EventRecorder + cache cache.Cache + apiReader client.Reader + config *rest.Config *runtime.Scheme client.Client finalizer *finalizer.Finalizer @@ -153,6 +154,7 @@ func (r *UserReconciler) SetupWithManager(mgr ctrl.Manager, opts ratelimiter.Rat } r.Scheme = mgr.GetScheme() r.cache = mgr.GetCache() + r.apiReader = mgr.GetAPIReader() r.config = mgr.GetConfig() r.Logger.V(1).Info("init reconcile controller user") r.minRequeueDuration = minRequeueDuration @@ -189,7 +191,6 @@ func (r *UserReconciler) reconcile(ctx context.Context, obj client.Object) (ctrl if !ok { return ctrl.Result{}, errors.New("obj convert user is error") } - isNewUser := r.isNewUser(user) blocked, err := r.handleLicenseLimit(ctx, user) if err != nil { @@ -235,9 +236,6 @@ func (r *UserReconciler) reconcile(ctx context.Context, obj client.Object) (ctrl ) return ctrl.Result{}, err } - if isNewUser { - usercount.Inc() - } return ctrl.Result{ RequeueAfter: RandTimeDurationBetween(r.minRequeueDuration, r.maxRequeueDuration), }, nil @@ -870,22 +868,37 @@ func (r *UserReconciler) updateStatus( } func (r *UserReconciler) handleLicenseLimit(ctx context.Context, user *userv1.User) (bool, error) { - userCount := usercount.Get() - if !usercount.Initialized() { - var err error - userCount, err = r.countExistingUsers(ctx, user.Name) - if err != nil { - return false, err - } + reader := r.apiReader + if reader == nil { + reader = r.Client } - if licensegate.AllowNewUser(userCount) { + + latest := &userv1.User{} + if err := reader.Get(ctx, client.ObjectKeyFromObject(user), latest); err != nil { + return false, err + } + *user = *latest.DeepCopy() + + if !r.isNewUser(user) { user.Status.Conditions = helper.DeleteCondition( user.Status.Conditions, licenseLimitedCondition, ) return false, nil } - if !r.isNewUser(user) { + + if err := usercount.Init(ctx, reader); err != nil { + return false, err + } + userCount, err := usercount.CountQuotaUsersExcluding(ctx, reader, user.Name) + if err != nil { + return false, err + } + if licensegate.AllowNewUser(userCount) { + user.Status.Conditions = helper.DeleteCondition( + user.Status.Conditions, + licenseLimitedCondition, + ) return false, nil } limitCondition := &userv1.Condition{ @@ -916,21 +929,6 @@ func (r *UserReconciler) isNewUser(user *userv1.User) bool { return user.Status.ObservedGeneration == 0 && len(user.Status.Conditions) == 0 } -func (r *UserReconciler) countExistingUsers(ctx context.Context, excludeName string) (int, error) { - userList := &userv1.UserList{} - if err := r.List(ctx, userList); err != nil { - return 0, err - } - count := 0 - for i := range userList.Items { - if userList.Items[i].Name == excludeName { - continue - } - count++ - } - return count, nil -} - func (r *UserReconciler) licenseToUserRequests( ctx context.Context, obj client.Object, diff --git a/controllers/user/main.go b/controllers/user/main.go index d6b81dd14dee..a20bfcc1da69 100644 --- a/controllers/user/main.go +++ b/controllers/user/main.go @@ -214,7 +214,7 @@ func main() { os.Exit(1) } if err := controllers.SetupUserCount(mgr); err != nil { - setupLog.Error(err, "unable to set up user count") + setupLog.Error(err, "unable to set up user count cache") os.Exit(1) } diff --git a/controllers/user/pkg/usercount/count.go b/controllers/user/pkg/usercount/count.go index 61b4ed5b23c8..1083f190a780 100644 --- a/controllers/user/pkg/usercount/count.go +++ b/controllers/user/pkg/usercount/count.go @@ -16,13 +16,18 @@ package usercount import ( "context" + "fmt" "sync/atomic" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" ) +const ( + UserPhaseActive = "Active" +) + var ( userCount int64 initializedFlag uint32 @@ -41,22 +46,116 @@ func Set(count int) { atomic.StoreUint32(&initializedFlag, 1) } -func Inc() { - atomic.AddInt64(&userCount, 1) - atomic.StoreUint32(&initializedFlag, 1) -} - func Init(ctx context.Context, reader client.Reader) error { if Initialized() { return nil } - list := &metav1.PartialObjectMetadataList{} - list.SetGroupVersionKind( - schema.GroupVersion{Group: "user.sealos.io", Version: "v1"}.WithKind("UserList"), - ) - if err := reader.List(ctx, list); err != nil { - return err + count, err := countActiveUsersUnstructured(ctx, reader, &client.ListOptions{}, "") + if err != nil { + return fmt.Errorf("failed to count active users: %w", err) } - Set(len(list.Items)) + Set(count) return nil } + +func CountActiveUsers(ctx context.Context, reader client.Reader) (int, error) { + active, err := countActiveUsersUnstructured(ctx, reader, &client.ListOptions{}, "") + if err != nil { + return 0, fmt.Errorf("unable to get active user count: %w", err) + } + return active, nil +} + +func CountQuotaUsers(ctx context.Context, reader client.Reader) (int, error) { + count, err := countQuotaUsersUnstructured(ctx, reader, &client.ListOptions{}, "") + if err != nil { + return 0, fmt.Errorf("unable to get quota user count: %w", err) + } + return count, nil +} + +func CountQuotaUsersExcluding(ctx context.Context, reader client.Reader, excludeName string) (int, error) { + count, err := countQuotaUsersUnstructured(ctx, reader, &client.ListOptions{}, excludeName) + if err != nil { + return 0, fmt.Errorf("unable to get quota user count excluding %s: %w", excludeName, err) + } + return count, nil +} + +func CountActiveUsersExcluding(ctx context.Context, reader client.Reader, excludeName string) (int, error) { + active, err := countActiveUsersUnstructured(ctx, reader, &client.ListOptions{}, excludeName) + if err != nil { + return 0, fmt.Errorf("unable to get active user count excluding %s: %w", excludeName, err) + } + return active, nil +} + +func countActiveUsersUnstructured( + ctx context.Context, + reader client.Reader, + opts *client.ListOptions, + excludeName string, +) (int, error) { + if reader == nil { + return 0, fmt.Errorf("client reader is nil") + } + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "user.sealos.io", + Version: "v1", + Kind: "UserList", + }) + + if err := reader.List(ctx, list, opts); err != nil { + return 0, fmt.Errorf("failed to list users: %w", err) + } + + var activeCount int + for _, item := range list.Items { + if excludeName != "" && item.GetName() == excludeName { + continue + } + phase, _, _ := unstructured.NestedString(item.Object, "status", "phase") + if phase == UserPhaseActive { + activeCount++ + } + } + + return activeCount, nil +} + +func countQuotaUsersUnstructured( + ctx context.Context, + reader client.Reader, + opts *client.ListOptions, + excludeName string, +) (int, error) { + if reader == nil { + return 0, fmt.Errorf("client reader is nil") + } + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "user.sealos.io", + Version: "v1", + Kind: "UserList", + }) + + if err := reader.List(ctx, list, opts); err != nil { + return 0, fmt.Errorf("failed to list users: %w", err) + } + + var count int + for _, item := range list.Items { + if excludeName != "" && item.GetName() == excludeName { + continue + } + if deletionTimestamp, found, _ := unstructured.NestedString(item.Object, "metadata", "deletionTimestamp"); found && deletionTimestamp != "" { + continue + } + count++ + } + + return count, nil +}