From 9cbfb17024b589c0fb009c8548157a4da2e1805c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20D=C3=B6ll?= Date: Sat, 7 Dec 2024 20:50:56 +0000 Subject: [PATCH] wip: user conditions --- api/v1alpha1/common_types.go | 1 + api/v1alpha1/nats_user_types.go | 12 +- api/v1alpha1/zz_generated.deepcopy.go | 10 +- controllers/natsoperator_controller.go | 6 +- controllers/natsuser_controller.go | 114 ++++++++++++------ .../templates/crds/natsusers.yaml | 76 +++++++++++- .../crd/bases/natz.zeiss.com_natsusers.yaml | 76 +++++++++++- pkg/status/status.go | 33 ++++- 8 files changed, 273 insertions(+), 55 deletions(-) diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go index b194349..6a986c6 100644 --- a/api/v1alpha1/common_types.go +++ b/api/v1alpha1/common_types.go @@ -13,6 +13,7 @@ const ( const ( ConditionReasonCreated = "Created" ConditionReasonSynchronized = "Synchronized" + ConditionReasonFailed = "Failed" ) const ( diff --git a/api/v1alpha1/nats_user_types.go b/api/v1alpha1/nats_user_types.go index 9fbd305..f919fcb 100644 --- a/api/v1alpha1/nats_user_types.go +++ b/api/v1alpha1/nats_user_types.go @@ -94,10 +94,16 @@ type NatsUserStatus struct { UserSecretName string `json:"userSecretName,omitempty"` PublicKey string `json:"publicKey,omitempty"` JWT string `json:"jwt,omitempty"` - // Phase is the current state of the user + // Conditions is an array of conditions that the operator is currently in. + Conditions []metav1.Condition `json:"conditions,omitempty" optional:"true"` + // Phase is the current phase of the operator. + // + // +kubebuilder:validation:Enum={None,Pending,Creating,Synchronized,Failed} Phase UserPhase `json:"phase"` - // ControlerPaused is used to pause the operator for this user - ControlerPaused bool `json:"controlerPaused,omitempty"` + // ControlPaused is a flag that indicates if the operator is paused. + ControlPaused bool `json:"controlPaused,omitempty" optional:"true"` + // LastUpdate is the timestamp of the last update. + LastUpdate metav1.Time `json:"lastUpdate,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2c2f75f..6bf2562 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -391,7 +391,7 @@ func (in *NatsUser) DeepCopyInto(out *NatsUser) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NatsUser. @@ -470,6 +470,14 @@ func (in *NatsUserSpec) DeepCopy() *NatsUserSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NatsUserStatus) DeepCopyInto(out *NatsUserStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.LastUpdate.DeepCopyInto(&out.LastUpdate) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NatsUserStatus. diff --git a/controllers/natsoperator_controller.go b/controllers/natsoperator_controller.go index 658e510..339c60c 100644 --- a/controllers/natsoperator_controller.go +++ b/controllers/natsoperator_controller.go @@ -396,7 +396,11 @@ func (r *NatsOperatorReconciler) ManageSuccess(ctx context.Context, obj *natsv1a return ctrl.Result{}, nil } - status.SetNatzOperatorCondition(obj, status.NewOperatorSynchronizingCondition(obj)) + status.SetNatzOperatorCondition(obj, status.NewOperatorSychronizedCondition(obj)) + + if r.IsCreating(obj) { + return ctrl.Result{Requeue: true}, nil + } if err := r.Client.Status().Update(ctx, obj); err != nil { return ctrl.Result{}, err diff --git a/controllers/natsuser_controller.go b/controllers/natsuser_controller.go index 5c713df..ce17a5c 100644 --- a/controllers/natsuser_controller.go +++ b/controllers/natsuser_controller.go @@ -3,6 +3,8 @@ package controllers import ( "context" "fmt" + "math" + "time" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -17,9 +19,9 @@ import ( "github.com/nats-io/jwt/v2" "github.com/nats-io/nkeys" natsv1alpha1 "github.com/zeiss/natz-operator/api/v1alpha1" - "github.com/zeiss/pkg/cast" + "github.com/zeiss/natz-operator/pkg/status" "github.com/zeiss/pkg/conv" - "github.com/zeiss/pkg/k8s/finalizers" + "github.com/zeiss/pkg/slices" "github.com/zeiss/pkg/utilx" ) @@ -36,6 +38,7 @@ const ( EventReasonUserSecretCreateSucceeded EventReason = "UserSecretCreateSucceeded" EventReasonUserSecretCreateFailed EventReason = "UserSecretCreateFailed" EventReasonUserSynchronizeFailed EventReason = "UserSynchronizeFailed" + EventReasonUserSynchronized EventReason = "UserSynchronized" ) // NatsUserReconciler reconciles a NatsUser object @@ -73,17 +76,7 @@ func (r *NatsUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } if !user.ObjectMeta.DeletionTimestamp.IsZero() { - log.Info("processing deletion of user") - - if finalizers.HasFinalizer(user, natsv1alpha1.FinalizerName) { - err := r.reconcileDelete(ctx, user) - if err != nil { - return ctrl.Result{}, err - } - } - - // Delete - return reconcile.Result{}, nil + return r.reconcileDelete(ctx, user) } // get latest version of the account @@ -93,50 +86,38 @@ func (r *NatsUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return reconcile.Result{}, err } - err := r.reconcileResources(ctx, req, user) - if err != nil { - r.Recorder.Event(user, corev1.EventTypeWarning, cast.String(EventReasonUserSynchronizeFailed), "user resources reconciliation failed") - return reconcile.Result{}, err - } - - return reconcile.Result{}, nil + return r.reconcileResources(ctx, req, user) } -func (r *NatsUserReconciler) reconcileDelete(ctx context.Context, user *natsv1alpha1.NatsUser) error { - log := log.FromContext(ctx) +func (r *NatsUserReconciler) reconcileDelete(ctx context.Context, obj *natsv1alpha1.NatsUser) (ctrl.Result, error) { + // Remove our finalizer from the list. + controllerutil.RemoveFinalizer(obj, natsv1alpha1.FinalizerName) - log.Info("reconcile delete user", "name", user.Name, "namespace", user.Namespace) + if !obj.DeletionTimestamp.IsZero() { + // Remove our finalizer from the list. + controllerutil.RemoveFinalizer(obj, natsv1alpha1.FinalizerName) - user.SetFinalizers(finalizers.RemoveFinalizer(user, natsv1alpha1.FinalizerName)) - err := r.Update(ctx, user) - if err != nil && !errors.IsNotFound(err) { - return err + // Stop reconciliation as the object is being deleted. + return ctrl.Result{}, r.Update(ctx, obj) } - return nil + return ctrl.Result{Requeue: true}, nil } -func (r *NatsUserReconciler) reconcileResources(ctx context.Context, req ctrl.Request, user *natsv1alpha1.NatsUser) error { - log := log.FromContext(ctx) - - log.Info("reconcile resources", "name", user.Name, "namespace", user.Namespace) - +func (r *NatsUserReconciler) reconcileResources(ctx context.Context, req ctrl.Request, user *natsv1alpha1.NatsUser) (ctrl.Result, error) { if err := r.reconcileStatus(ctx, user); err != nil { - log.Error(err, "failed to reconcile status", "name", user.Name, "namespace", user.Namespace) - return err + return r.ManageError(ctx, user, err) } if err := r.reconcileUser(ctx, req, user); err != nil { - log.Error(err, "failed to reconcile user", "name", user.Name, "namespace", user.Namespace) - return err + return r.ManageError(ctx, user, err) } if err := r.reconcileSecret(ctx, user); err != nil { - log.Error(err, "failed to reconcile secret", "name", user.Name, "namespace", user.Namespace) - return err + return r.ManageError(ctx, user, err) } - return nil + return r.ManageSuccess(ctx, user) } func (r *NatsUserReconciler) reconcileUser(ctx context.Context, req ctrl.Request, user *natsv1alpha1.NatsUser) error { @@ -290,6 +271,59 @@ func (r *NatsUserReconciler) reconcileSecret(ctx context.Context, user *natsv1al return nil } +// IsCreating ... +func (r *NatsUserReconciler) IsCreating(obj *natsv1alpha1.NatsUser) bool { + return utilx.Or(obj.Status.Conditions == nil, slices.Len(obj.Status.Conditions) == 0) +} + +// IsSynchronized ... +func (r *NatsUserReconciler) IsSynchronized(obj *natsv1alpha1.NatsUser) bool { + return obj.Status.Phase == natsv1alpha1.UserPhaseSynchronized +} + +// ManageError ... +func (r *NatsUserReconciler) ManageError(ctx context.Context, obj *natsv1alpha1.NatsUser, err error) (ctrl.Result, error) { + status.SetNatzUserCondition(obj, status.NewUserFailedCondition(obj, err)) + + if err := r.Client.Status().Update(ctx, obj); err != nil { + return ctrl.Result{Requeue: true, RequeueAfter: time.Second}, err + } + + r.Recorder.Event(obj, corev1.EventTypeWarning, conv.String(EventReasonUserSynchronizeFailed), "user synchronization failed") + + var retryInterval time.Duration + + return reconcile.Result{ + RequeueAfter: time.Duration(math.Min(float64(retryInterval.Nanoseconds()*2), float64(time.Hour.Nanoseconds()*6))), + Requeue: true, + }, nil +} + +// ManageSuccess ... +func (r *NatsUserReconciler) ManageSuccess(ctx context.Context, obj *natsv1alpha1.NatsUser) (ctrl.Result, error) { + if r.IsSynchronized(obj) { + return ctrl.Result{}, nil + } + + status.SetNatzUserCondition(obj, status.NewUserSychronizedCondition(obj)) + + if r.IsCreating(obj) { + return ctrl.Result{Requeue: true}, nil + } + + if err := r.Client.Status().Update(ctx, obj); err != nil { + return ctrl.Result{}, err + } + + if !obj.ObjectMeta.DeletionTimestamp.IsZero() { + return ctrl.Result{Requeue: true}, nil + } + + r.Recorder.Event(obj, corev1.EventTypeNormal, conv.String(EventReasonUserSynchronized), "user synchronized") + + return ctrl.Result{}, nil +} + // SetupWithManager sets up the controller with the Manager. func (r *NatsUserReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/helm/charts/natz-operator/templates/crds/natsusers.yaml b/helm/charts/natz-operator/templates/crds/natsusers.yaml index 726ccd3..77cc433 100644 --- a/helm/charts/natz-operator/templates/crds/natsusers.yaml +++ b/helm/charts/natz-operator/templates/crds/natsusers.yaml @@ -175,14 +175,82 @@ spec: status: description: NatsUserStatus defines the observed state of NatsUser properties: - controlerPaused: - description: ControlerPaused is used to pause the operator for this - user + conditions: + description: Conditions is an array of conditions that the operator + is currently in. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + controlPaused: + description: ControlPaused is a flag that indicates if the operator + is paused. type: boolean jwt: type: string + lastUpdate: + description: LastUpdate is the timestamp of the last update. + format: date-time + type: string phase: - description: Phase is the current state of the user + description: Phase is the current phase of the operator. + enum: + - None + - Pending + - Creating + - Synchronized + - Failed type: string publicKey: type: string diff --git a/manifests/crd/bases/natz.zeiss.com_natsusers.yaml b/manifests/crd/bases/natz.zeiss.com_natsusers.yaml index 726ccd3..77cc433 100644 --- a/manifests/crd/bases/natz.zeiss.com_natsusers.yaml +++ b/manifests/crd/bases/natz.zeiss.com_natsusers.yaml @@ -175,14 +175,82 @@ spec: status: description: NatsUserStatus defines the observed state of NatsUser properties: - controlerPaused: - description: ControlerPaused is used to pause the operator for this - user + conditions: + description: Conditions is an array of conditions that the operator + is currently in. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + controlPaused: + description: ControlPaused is a flag that indicates if the operator + is paused. type: boolean jwt: type: string + lastUpdate: + description: LastUpdate is the timestamp of the last update. + format: date-time + type: string phase: - description: Phase is the current state of the user + description: Phase is the current phase of the operator. + enum: + - None + - Pending + - Creating + - Synchronized + - Failed type: string publicKey: type: string diff --git a/pkg/status/status.go b/pkg/status/status.go index 1423400..0cc5dbc 100644 --- a/pkg/status/status.go +++ b/pkg/status/status.go @@ -37,8 +37,13 @@ func SetNatzOperatorCondition(obj *natsv1alpha1.NatsOperator, condition metav1.C obj.Status.Conditions = SetCondition(condition, obj.Status.Conditions...) } -// NewOperatorSynchronizingCondition creates the provisioning started condition in cluster conditions. -func NewOperatorSynchronizingCondition(obj *natsv1alpha1.NatsOperator) metav1.Condition { +// SetNatzUserCondition ... +func SetNatzUserCondition(obj *natsv1alpha1.NatsUser, condition metav1.Condition) { + obj.Status.Conditions = SetCondition(condition, obj.Status.Conditions...) +} + +// NewOperatorSychronizedCondition creates the provisioning started condition in cluster conditions. +func NewOperatorSychronizedCondition(obj *natsv1alpha1.NatsOperator) metav1.Condition { return metav1.Condition{ Type: natsv1alpha1.ConditionTypeSynchronized, ObservedGeneration: obj.Generation, @@ -48,3 +53,27 @@ func NewOperatorSynchronizingCondition(obj *natsv1alpha1.NatsOperator) metav1.Co Reason: natsv1alpha1.ConditionReasonSynchronized, } } + +// NewUserSychronizedCondition creates the provisioning started condition in cluster conditions. +func NewUserSychronizedCondition(obj *natsv1alpha1.NatsUser) metav1.Condition { + return metav1.Condition{ + Type: natsv1alpha1.ConditionTypeSynchronized, + ObservedGeneration: obj.Generation, + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Message: fmt.Sprintf("the user has successfully created: %s", obj.Name), + Reason: natsv1alpha1.ConditionReasonSynchronized, + } +} + +// NewUserFailedCondition creates the provisioning started condition in cluster conditions. +func NewUserFailedCondition(obj *natsv1alpha1.NatsUser, err error) metav1.Condition { + return metav1.Condition{ + Type: natsv1alpha1.ConditionTypeFailed, + ObservedGeneration: obj.Generation, + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Message: err.Error(), + Reason: natsv1alpha1.ConditionReasonFailed, + } +}