Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion controllers/user/api/v1/user_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
62 changes: 30 additions & 32 deletions controllers/user/controllers/user_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion controllers/user/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
125 changes: 112 additions & 13 deletions controllers/user/pkg/usercount/count.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Loading