diff --git a/controllers/user/api/v1/user_types.go b/controllers/user/api/v1/user_types.go index 78ac6cdf4..5b952afce 100644 --- a/controllers/user/api/v1/user_types.go +++ b/controllers/user/api/v1/user_types.go @@ -37,7 +37,8 @@ type UserSpec struct { // The minimum valid value for expirationSeconds is 600, i.e. 10 minutes. // // +optional - CSRExpirationSeconds *int32 `json:"CSRExpirationSeconds,omitempty"` + //+kubebuilder:default:=7200 + CSRExpirationSeconds int32 `json:"csrExpirationSeconds,omitempty"` } type UserPhase string @@ -54,6 +55,8 @@ type UserStatus struct { //+kubebuilder:default:=UserUnknown Phase UserPhase `json:"phase,omitempty"` KubeConfig string `json:"kubeConfig"` + //+kubebuilder:default:=7200 + ObservedCSRExpirationSeconds int32 `json:"observedCSRExpirationSeconds,omitempty"` // The generation observed by the user controller. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` @@ -91,6 +94,7 @@ type Condition struct { // +kubebuilder:resource:scope=Cluster // +kubebuilder:printcolumn:name="DisplayName",type="string",JSONPath=".spec.displayName" // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +// +kubebuilder:printcolumn:name="ExpirationSeconds",type="integer",JSONPath=".status.observedCSRExpirationSeconds" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // User is the Schema for the users API diff --git a/controllers/user/api/v1/usergroup_types.go b/controllers/user/api/v1/usergroup_types.go index 259579175..9496ab504 100644 --- a/controllers/user/api/v1/usergroup_types.go +++ b/controllers/user/api/v1/usergroup_types.go @@ -26,12 +26,26 @@ import ( // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Cluster,shortName=ug +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // UserGroup is the Schema for the usergroups API type UserGroup struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` + Status UserGroupStatus `json:"status,omitempty"` +} + +// UserGroupStatus defines the observed state of UserGroup +type UserGroupStatus struct { + // Phase is the recently observed lifecycle phase of user + //+kubebuilder:default:=UserUnknown + Phase UserPhase `json:"phase,omitempty"` + // The generation observed by the user controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // Conditions contains the different condition statuses for this user group. + Conditions []Condition `json:"conditions,omitempty"` } //+kubebuilder:object:root=true diff --git a/controllers/user/api/v1/zz_generated.deepcopy.go b/controllers/user/api/v1/zz_generated.deepcopy.go index dc01e7116..c3e815169 100644 --- a/controllers/user/api/v1/zz_generated.deepcopy.go +++ b/controllers/user/api/v1/zz_generated.deepcopy.go @@ -247,7 +247,7 @@ func (in *User) DeepCopyInto(out *User) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) } @@ -274,6 +274,7 @@ func (in *UserGroup) DeepCopyInto(out *UserGroup) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserGroup. @@ -389,6 +390,28 @@ func (in *UserGroupList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserGroupStatus) DeepCopyInto(out *UserGroupStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserGroupStatus. +func (in *UserGroupStatus) DeepCopy() *UserGroupStatus { + if in == nil { + return nil + } + out := new(UserGroupStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UserList) DeepCopyInto(out *UserList) { *out = *in @@ -424,11 +447,6 @@ func (in *UserList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UserSpec) DeepCopyInto(out *UserSpec) { *out = *in - if in.CSRExpirationSeconds != nil { - in, out := &in.CSRExpirationSeconds, &out.CSRExpirationSeconds - *out = new(int32) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserSpec. diff --git a/controllers/user/config/crd/bases/user.sealos.io_usergroups.yaml b/controllers/user/config/crd/bases/user.sealos.io_usergroups.yaml index 637d97f31..8c9a62909 100644 --- a/controllers/user/config/crd/bases/user.sealos.io_usergroups.yaml +++ b/controllers/user/config/crd/bases/user.sealos.io_usergroups.yaml @@ -18,6 +18,9 @@ spec: scope: Cluster versions: - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -38,6 +41,52 @@ spec: type: string metadata: type: object + status: + description: UserGroupStatus defines the observed state of UserGroup + properties: + conditions: + description: Conditions contains the different condition statuses + for this user group. + items: + properties: + lastHeartbeatTime: + description: LastHeartbeatTime is the last time this condition + was updated. + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time the condition + changed from one status to another. + format: date-time + type: string + message: + description: Message is a human-readable message indicating + details about the last status change. + type: string + reason: + description: Reason is a (brief) reason for the condition's + last status change. + type: string + status: + description: Status is the status of the condition. One of True, + False, Unknown. + type: string + type: + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the user controller. + format: int64 + type: integer + phase: + default: UserUnknown + description: Phase is the recently observed lifecycle phase of user + type: string + type: object type: object served: true storage: true diff --git a/controllers/user/config/crd/bases/user.sealos.io_users.yaml b/controllers/user/config/crd/bases/user.sealos.io_users.yaml index 1969bedc1..9b29e1e5f 100644 --- a/controllers/user/config/crd/bases/user.sealos.io_users.yaml +++ b/controllers/user/config/crd/bases/user.sealos.io_users.yaml @@ -22,6 +22,9 @@ spec: - jsonPath: .status.phase name: Phase type: string + - jsonPath: .status.observedCSRExpirationSeconds + name: ExpirationSeconds + type: integer - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -45,7 +48,8 @@ spec: spec: description: UserSpec defines the desired state of User properties: - CSRExpirationSeconds: + csrExpirationSeconds: + default: 7200 description: "expirationSeconds is the requested duration of validity of the issued certificate. The certificate signer may issue a certificate with a different validity duration so a client must check the delta @@ -99,6 +103,10 @@ spec: type: array kubeConfig: type: string + observedCSRExpirationSeconds: + default: 7200 + format: int32 + type: integer observedGeneration: description: The generation observed by the user controller. format: int64 diff --git a/controllers/user/config/rbac/role.yaml b/controllers/user/config/rbac/role.yaml index 87dc87b76..d6e099950 100644 --- a/controllers/user/config/rbac/role.yaml +++ b/controllers/user/config/rbac/role.yaml @@ -5,6 +5,26 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests/status + verbs: + - get + - patch + - update - apiGroups: - user.sealos.io resources: diff --git a/controllers/user/config/samples/user_v1_user.yaml b/controllers/user/config/samples/user_v1_user.yaml index c0bd4c477..e41f65b70 100644 --- a/controllers/user/config/samples/user_v1_user.yaml +++ b/controllers/user/config/samples/user_v1_user.yaml @@ -4,3 +4,4 @@ metadata: name: f8699ded-58d3-432b-a9ff-56568b57a38d spec: displayName: cuisongliu + csrExpirationSeconds: 1000000000 diff --git a/controllers/user/controllers/helper/csr.go b/controllers/user/controllers/helper/csr.go index 35ca9f0bc..556fe30d1 100644 --- a/controllers/user/controllers/helper/csr.go +++ b/controllers/user/controllers/helper/csr.go @@ -30,17 +30,18 @@ import ( "net" "time" - "k8s.io/client-go/kubernetes" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/util/retry" csrv1 "k8s.io/api/certificates/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/controller-runtime/pkg/client" ) // newPrivateKey creates an RSA private key @@ -133,28 +134,49 @@ func (csr *CSR) updateCsr(config *rest.Config, cli client.Client) error { csrResource := &csrv1.CertificateSigningRequest{} csrResource.Name = csrName - deleteCSR := csrResource.DeepCopy() - _ = cli.Delete(context.TODO(), deleteCSR, client.PropagationPolicy(v1.DeletePropagationBackground)) + user := &userv1.User{} + err := cli.Get(context.TODO(), client.ObjectKey{Name: csr.User}, user) + if err == nil { + ref := v1.OwnerReference{ + APIVersion: userv1.GroupVersion.String(), + Kind: "User", + UID: user.GetUID(), + Name: user.GetName(), + } + csrResource.OwnerReferences = append(csrResource.OwnerReferences, ref) + csrName = fmt.Sprintf("%s-%d", csrName, user.Generation) + csrResource.Name = csrName + label = map[string]string{ + "csr-name": csrName, + } + } - change, err := controllerutil.CreateOrUpdate(context.TODO(), cli, csrResource, func() error { - csrResource.Labels = label - csrResource.Spec.Request = csr.data.TLSCsr - csrResource.Spec.SignerName = csrv1.KubeAPIServerClientSignerName - csrResource.Spec.ExpirationSeconds = &csr.ExpirationSeconds - csrResource.Spec.Groups = []string{"system:authenticated"} - csrResource.Spec.Usages = []csrv1.KeyUsage{ + if err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + insertCSR := csrResource.DeepCopy() + insertCSR.Labels = label + insertCSR.ResourceVersion = "0" + insertCSR.Spec.Request = csr.data.TLSCsr + insertCSR.Spec.SignerName = csrv1.KubeAPIServerClientSignerName + insertCSR.Spec.ExpirationSeconds = &csr.ExpirationSeconds + insertCSR.Spec.Groups = []string{"system:authenticated"} + insertCSR.Spec.Usages = []csrv1.KeyUsage{ "digital signature", "key encipherment", "client auth", } + + err = cli.Create(context.TODO(), insertCSR) + if err != nil { + if !errors.IsAlreadyExists(err) { + return err + } + } + csrResource = insertCSR.DeepCopy() return nil - }) - if err != nil { + }); err != nil { return err } - if change != controllerutil.OperationResultCreated { - return errors.NewBadRequest("create csr failed,must create new csr for new user") - } + clientset, err := kubernetes.NewForConfig(config) if err != nil { return err diff --git a/controllers/user/controllers/helper/helper.go b/controllers/user/controllers/helper/helper.go index 44a3b7ab2..744bcc75c 100644 --- a/controllers/user/controllers/helper/helper.go +++ b/controllers/user/controllers/helper/helper.go @@ -19,6 +19,7 @@ package helper import ( v1 "github.com/labring/sealos/controllers/user/api/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func IsConditionTrue(conditions []v1.Condition, condition v1.Condition) bool { @@ -78,3 +79,10 @@ func DeleteCondition(conditions []v1.Condition, conditionType v1.ConditionType) conditions = newConditions return conditions } + +func SetConditionError(condition *v1.Condition, reason string, err error) { + condition.LastHeartbeatTime = metav1.Now() + condition.Status = corev1.ConditionFalse + condition.Reason = reason + condition.Message = err.Error() +} diff --git a/controllers/user/controllers/suite_test.go b/controllers/user/controllers/suite_test.go index 0a5e708e7..e758ae1da 100644 --- a/controllers/user/controllers/suite_test.go +++ b/controllers/user/controllers/suite_test.go @@ -134,10 +134,10 @@ users: user := &v1.User{} user.Name = "cuisongliu" defaultExpirationDuration := int32(100000000) - user.Spec.CSRExpirationSeconds = &defaultExpirationDuration + user.Spec.CSRExpirationSeconds = defaultExpirationDuration c := &helper.Config{ User: user.Name, - ExpirationSeconds: *user.Spec.CSRExpirationSeconds, + ExpirationSeconds: user.Spec.CSRExpirationSeconds, } config, err := helper.NewGenerate(c).KubeConfig(cfg, k8sClient) diff --git a/controllers/user/controllers/user_controller.go b/controllers/user/controllers/user_controller.go index ab620fc72..95df13ce9 100644 --- a/controllers/user/controllers/user_controller.go +++ b/controllers/user/controllers/user_controller.go @@ -46,6 +46,10 @@ import ( ) const userAnnotationOwnerKey = "user.sealos.io/creator" +const clusterRoleByCreate = "sealos-user-create-role" + +//const clusterRoleByManager = "sealos-user-manager-role" +//const clusterRoleByUser = "sealos-user-user-role" // UserReconciler reconciles a User object type UserReconciler struct { @@ -60,6 +64,8 @@ type UserReconciler struct { //+kubebuilder:rbac:groups=user.sealos.io,resources=users,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=user.sealos.io,resources=users/status,verbs=get;update;patch //+kubebuilder:rbac:groups=user.sealos.io,resources=users/finalizers,verbs=update +//+kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests/status,verbs=get;update;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -71,7 +77,7 @@ type UserReconciler struct { // 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 *UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Logger.V(4).Info("start reconcile for users") + r.Logger.V(1).Info("start reconcile for users") user := &userv1.User{} ctr := controller.Controller{ Client: r.Client, @@ -103,18 +109,19 @@ func (r *UserReconciler) SetupWithManager(mgr ctrl.Manager) error { r.Scheme = mgr.GetScheme() r.cache = mgr.GetCache() r.config = mgr.GetConfig() - r.Logger.V(4).Info("init reconcile controller user") + r.Logger.V(1).Info("init reconcile controller user") return ctrl.NewControllerManagedBy(mgr). For(&userv1.User{}). Complete(r) } func (r *UserReconciler) Delete(ctx context.Context, req ctrl.Request, gvk schema.GroupVersionKind, obj client.Object) error { + r.Logger.V(1).Info("delete reconcile controller user", "request", req) return nil } func (r *UserReconciler) Update(ctx context.Context, req ctrl.Request, gvk schema.GroupVersionKind, obj client.Object) (ctrl.Result, error) { - r.Logger.V(4).Info("update reconcile controller user", "request", req) + r.Logger.V(1).Info("update reconcile controller user", "request", req) user := &userv1.User{} err := convert.JsonConvert(obj, user) if err != nil { @@ -124,7 +131,6 @@ func (r *UserReconciler) Update(ctx context.Context, req ctrl.Request, gvk schem r.initStatus, r.syncKubeConfig, r.syncOwnerUG, - r.syncOwnerUGUserBinding, r.syncOwnerUGNamespaceBinding, r.syncFinalStatus, } @@ -163,17 +169,11 @@ func (r *UserReconciler) saveCondition(user *userv1.User, condition *userv1.Cond user.Status.Conditions = helper.UpdateCondition(user.Status.Conditions, *condition) } } -func (r *UserReconciler) setConditionError(condition *userv1.Condition, reason string, err error) { - condition.LastHeartbeatTime = metav1.Now() - condition.Status = v1.ConditionFalse - condition.Reason = reason - condition.Message = err.Error() -} func (r *UserReconciler) syncKubeConfig(ctx context.Context, user *userv1.User) { cfg := &helper.Config{ User: user.Name, - ExpirationSeconds: *user.Spec.CSRExpirationSeconds, + ExpirationSeconds: user.Spec.CSRExpirationSeconds, } userConditionType := userv1.ConditionType("KubeConfigSyncReady") condition := &userv1.Condition{ @@ -191,19 +191,28 @@ func (r *UserReconciler) syncKubeConfig(ctx context.Context, user *userv1.User) if event != nil { r.Recorder.Eventf(user, v1.EventTypeWarning, "syncKubeConfig", *event) } + user.Status.ObservedCSRExpirationSeconds = user.Spec.CSRExpirationSeconds if err != nil { r.Recorder.Eventf(user, v1.EventTypeWarning, "syncKubeConfig", "syncReNewConfig %s is error: %v", user.Name, err) + return } if config == nil { config, err = helper.NewGenerate(cfg).KubeConfig(r.config, r.Client) if err != nil { - r.setConditionError(condition, "SyncKubeConfigError", err) + helper.SetConditionError(condition, "SyncKubeConfigError", err) r.Recorder.Eventf(user, v1.EventTypeWarning, "syncKubeConfig", "Sync KubeConfig %s is error: %v", user.Name, err) + return + } + if config == nil { + helper.SetConditionError(condition, "SyncKubeConfigError", errors.New("api.config is nil")) + r.Recorder.Eventf(user, v1.EventTypeWarning, "syncKubeConfig", "Sync KubeConfig %s is error: %v", user.Name, errors.New("api.config is nil")) + return } kubeData, err := clientcmd.Write(*config) if err != nil { - r.setConditionError(condition, "OutputKubeConfigError", err) + helper.SetConditionError(condition, "OutputKubeConfigError", err) r.Recorder.Eventf(user, v1.EventTypeWarning, "syncKubeConfig", "Output KubeConfig %s is error: %v", user.Name, err) + return } user.Status.KubeConfig = string(kubeData) } @@ -213,7 +222,7 @@ func syncReNewConfig(user *userv1.User) (*api.Config, *string, error) { var config *api.Config var err error var event *string - if user.Status.KubeConfig != "" { + if user.Status.KubeConfig != "" && user.Spec.CSRExpirationSeconds == user.Status.ObservedCSRExpirationSeconds { config, err = clientcmd.Load([]byte(user.Status.KubeConfig)) if err != nil { return nil, nil, err @@ -262,60 +271,14 @@ func (r *UserReconciler) syncOwnerUG(ctx context.Context, user *userv1.User) { }); err != nil { return errors.Wrap(err, "unable to create UserGroup") } - r.Logger.V(4).Info("create or update UserGroup ", "OperationResult", change) + r.Logger.V(1).Info("create or update UserGroup ", "OperationResult", change) return nil }); err != nil { - r.setConditionError(condition, "SyncOwnerUGError", err) + helper.SetConditionError(condition, "SyncOwnerUGError", err) r.Recorder.Eventf(user, v1.EventTypeWarning, "syncOwnerUG", "Sync OwnerUG %s is error: %v", ugName, err) } } -func (r *UserReconciler) syncOwnerUGUserBinding(ctx context.Context, user *userv1.User) { - userConditionType := userv1.ConditionType("OwnerUGUserBindingSyncReady") - condition := &userv1.Condition{ - Type: userConditionType, - Status: v1.ConditionTrue, - LastTransitionTime: metav1.Now(), - LastHeartbeatTime: metav1.Now(), - Reason: string(userv1.Ready), - Message: "sync owner ug user binding successfully", - } - defer r.saveCondition(user, condition) - - uguBindingName := fmt.Sprintf("ugu-%s", user.Name) - ugName := fmt.Sprintf("ug-%s", user.Name) - if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - ugBinding := &userv1.UserGroupBinding{} - ugBinding.Name = uguBindingName - var change controllerutil.OperationResult - var err error - if change, err = controllerutil.CreateOrUpdate(ctx, r.Client, ugBinding, func() error { - if err = controllerutil.SetControllerReference(user, ugBinding, r.Scheme); err != nil { - return err - } - ugBinding.Annotations = map[string]string{userAnnotationOwnerKey: user.Name} - ugBinding.UserGroupRef = ugName - ugBinding.Subject = rbacv1.Subject{ - Kind: "User", - APIGroup: userv1.GroupVersion.String(), - Name: user.Name, - } - ugBinding.RoleRef = &rbacv1.RoleRef{ - APIGroup: rbacv1.SchemeGroupVersion.String(), - Kind: "ClusterRole", - Name: "sealos-user-create-role", - } - return nil - }); err != nil { - return errors.Wrap(err, "unable to create user UserGroupBinding") - } - r.Logger.V(4).Info("create or update user UserGroupBinding", "OperationResult", change) - return nil - }); err != nil { - r.setConditionError(condition, "SyncOwnerUGUserBindingError", err) - r.Recorder.Eventf(user, v1.EventTypeWarning, "syncOwnerUGUserBinding", "Sync OwnerUGUserBinding %s is error: %v", uguBindingName, err) - } -} func (r *UserReconciler) syncOwnerUGNamespaceBinding(ctx context.Context, user *userv1.User) { userConditionType := userv1.ConditionType("OwnerUGNamespaceBindingSyncReady") condition := &userv1.Condition{ @@ -350,10 +313,10 @@ func (r *UserReconciler) syncOwnerUGNamespaceBinding(ctx context.Context, user * }); err != nil { return errors.Wrap(err, "unable to create namespace UserGroupBinding") } - r.Logger.V(4).Info("create or update namespace UserGroupBinding", "OperationResult", change) + r.Logger.V(1).Info("create or update namespace UserGroupBinding", "OperationResult", change) return nil }); err != nil { - r.setConditionError(condition, "SyncOwnerUGNamespaceBindingError", err) + helper.SetConditionError(condition, "SyncOwnerUGNamespaceBindingError", err) r.Recorder.Eventf(user, v1.EventTypeWarning, "syncOwnerUGNamespaceBinding", "Sync OwnerUGNamespaceBinding %s is error: %v", ugnBindingName, err) } } diff --git a/controllers/user/controllers/usergroup_controller.go b/controllers/user/controllers/usergroup_controller.go index 76cbddf9f..d850515d9 100644 --- a/controllers/user/controllers/usergroup_controller.go +++ b/controllers/user/controllers/usergroup_controller.go @@ -18,6 +18,17 @@ package controllers import ( "context" + "fmt" + + "github.com/labring/endpoints-operator/library/convert" + "github.com/labring/sealos/controllers/user/controllers/helper" + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/go-logr/logr" "github.com/labring/endpoints-operator/library/controller" @@ -54,7 +65,7 @@ type UserGroupReconciler struct { // 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 *UserGroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Logger.V(4).Info("start reconcile for user groups") + r.Logger.V(1).Info("start reconcile for user groups") userGroup := &userv1.UserGroup{} ctr := controller.Controller{ Client: r.Client, @@ -85,7 +96,7 @@ func (r *UserGroupReconciler) SetupWithManager(mgr ctrl.Manager) error { } r.Scheme = mgr.GetScheme() r.cache = mgr.GetCache() - r.Logger.V(4).Info("init reconcile controller user group") + r.Logger.V(1).Info("init reconcile controller user group") return ctrl.NewControllerManagedBy(mgr). For(&userv1.UserGroup{}). Complete(r) @@ -96,6 +107,136 @@ func (r *UserGroupReconciler) Delete(ctx context.Context, req ctrl.Request, gvk } func (r *UserGroupReconciler) Update(ctx context.Context, req ctrl.Request, gvk schema.GroupVersionKind, obj client.Object) (ctrl.Result, error) { - r.Logger.V(4).Info("update reconcile controller user group", "request", req) + r.Logger.V(1).Info("update reconcile controller user group", "request", req) + ug := &userv1.UserGroup{} + err := convert.JsonConvert(obj, ug) + if err != nil { + return ctrl.Result{Requeue: true}, err + } + pipelines := []func(ctx context.Context, ug *userv1.UserGroup){ + r.initStatus, + r.syncOwnerUGUserBinding, + r.syncFinalStatus, + } + + for _, fn := range pipelines { + fn(ctx, ug) + } + if ug.Status.Phase != userv1.UserUnknown { + ug.Status.Phase = userv1.UserActive + } + err = r.updateStatus(ctx, req.NamespacedName, ug.Status.DeepCopy()) + if err != nil { + r.Recorder.Eventf(ug, v1.EventTypeWarning, "SyncStatus", "Sync status %s is error: %v", ug.Name, err) + return ctrl.Result{}, err + } return ctrl.Result{}, nil } +func (r *UserGroupReconciler) initStatus(ctx context.Context, ug *userv1.UserGroup) { + var initializedCondition = userv1.Condition{ + Type: userv1.Initialized, + Status: v1.ConditionTrue, + Reason: string(userv1.Initialized), + Message: "user group has been initialized", + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + } + ug.Status.Phase = userv1.UserPending + ug.Status.ObservedGeneration = ug.Generation + if !helper.IsConditionTrue(ug.Status.Conditions, initializedCondition) { + ug.Status.Conditions = helper.UpdateCondition(ug.Status.Conditions, initializedCondition) + } +} +func (r *UserGroupReconciler) syncFinalStatus(ctx context.Context, ug *userv1.UserGroup) { + condition := &userv1.Condition{ + Type: userv1.Ready, + Status: v1.ConditionTrue, + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + Reason: string(userv1.Ready), + Message: "UserGroup is available now", + } + defer r.saveCondition(ug, condition) + + if !helper.IsConditionsTrue(ug.Status.Conditions) { + condition.LastHeartbeatTime = metav1.Now() + condition.Status = v1.ConditionFalse + condition.Reason = "Not" + string(userv1.Ready) + condition.Message = "UserGroup is not available now" + ug.Status.Phase = userv1.UserUnknown + } else { + ug.Status.Phase = userv1.UserActive + } +} + +func (r *UserGroupReconciler) updateStatus(ctx context.Context, nn types.NamespacedName, status *userv1.UserGroupStatus) error { + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + original := &userv1.UserGroup{} + if err := r.Get(ctx, nn, original); err != nil { + return err + } + original.Status = *status + if err := r.Client.Status().Update(ctx, original); err != nil { + return err + } + return nil + }); err != nil { + return err + } + return nil +} + +func (r *UserGroupReconciler) saveCondition(ug *userv1.UserGroup, condition *userv1.Condition) { + if !helper.IsConditionTrue(ug.Status.Conditions, *condition) { + ug.Status.Conditions = helper.UpdateCondition(ug.Status.Conditions, *condition) + } +} + +func (r *UserGroupReconciler) syncOwnerUGUserBinding(ctx context.Context, ug *userv1.UserGroup) { + userConditionType := userv1.ConditionType("OwnerUGUserBindingSyncReady") + condition := &userv1.Condition{ + Type: userConditionType, + Status: v1.ConditionTrue, + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + Reason: string(userv1.Ready), + Message: "sync owner ug user binding successfully", + } + defer r.saveCondition(ug, condition) + + userName := ug.Annotations[userAnnotationOwnerKey] + + uguBindingName := fmt.Sprintf("ugu-%s", userName) + ugName := fmt.Sprintf("ug-%s", userName) + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + ugBinding := &userv1.UserGroupBinding{} + ugBinding.Name = uguBindingName + var change controllerutil.OperationResult + var err error + if change, err = controllerutil.CreateOrUpdate(ctx, r.Client, ugBinding, func() error { + if err = controllerutil.SetControllerReference(ug, ugBinding, r.Scheme); err != nil { + return err + } + ugBinding.Annotations = map[string]string{userAnnotationOwnerKey: userName} + ugBinding.UserGroupRef = ugName + ugBinding.Subject = rbacv1.Subject{ + Kind: "User", + APIGroup: userv1.GroupVersion.String(), + Name: userName, + } + ugBinding.RoleRef = &rbacv1.RoleRef{ + APIGroup: rbacv1.SchemeGroupVersion.String(), + Kind: "ClusterRole", + Name: clusterRoleByCreate, + } + return nil + }); err != nil { + return errors.Wrap(err, "unable to create user UserGroupBinding") + } + r.Logger.V(1).Info("create or update user UserGroupBinding", "OperationResult", change) + return nil + }); err != nil { + helper.SetConditionError(condition, "SyncOwnerUGUserBindingError", err) + r.Recorder.Eventf(ug, v1.EventTypeWarning, "syncOwnerUGUserBinding", "Sync OwnerUGUserBinding %s is error: %v", uguBindingName, err) + } +}