controllers/keycloakrealmuser/keycloakrealmuser_controller.go (191 lines of code) (raw):

package keycloakrealmuser import ( "context" "fmt" "time" "github.com/Nerzal/gocloak/v12" "github.com/pkg/errors" coreV1 "k8s.io/api/core/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/epam/edp-keycloak-operator/api/common" keycloakApi "github.com/epam/edp-keycloak-operator/api/v1" "github.com/epam/edp-keycloak-operator/controllers/helper" "github.com/epam/edp-keycloak-operator/pkg/client/keycloak" "github.com/epam/edp-keycloak-operator/pkg/client/keycloak/adapter" "github.com/epam/edp-keycloak-operator/pkg/objectmeta" ) const finalizer = "keycloak.realmuser.operator.finalizer.name" type Helper interface { SetFailureCount(fc helper.FailureCountable) time.Duration TryToDelete(ctx context.Context, obj client.Object, terminator helper.Terminator, finalizer string) (isDeleted bool, resultErr error) SetRealmOwnerRef(ctx context.Context, object helper.ObjectWithRealmRef) error GetKeycloakRealmFromRef(ctx context.Context, object helper.ObjectWithRealmRef, kcClient keycloak.Client) (*gocloak.RealmRepresentation, error) CreateKeycloakClientFromRealmRef(ctx context.Context, object helper.ObjectWithRealmRef) (keycloak.Client, error) } type Reconcile struct { client client.Client helper Helper } func NewReconcile(client client.Client, helper Helper) *Reconcile { return &Reconcile{ client: client, helper: helper, } } func (r *Reconcile) SetupWithManager(mgr ctrl.Manager) error { pred := predicate.Funcs{ DeleteFunc: func(deleteEvent event.DeleteEvent) bool { return false }, } err := ctrl.NewControllerManagedBy(mgr). For(&keycloakApi.KeycloakRealmUser{}, builder.WithPredicates(pred)). Complete(r) if err != nil { return fmt.Errorf("failed to setup KeycloakRealmUser controller: %w", err) } return nil } //+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakrealmusers,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakrealmusers/status,verbs=get;update;patch //+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakrealmusers/finalizers,verbs=update // Reconcile is a loop for reconciling KeycloakRealmUser object. func (r *Reconcile) Reconcile(ctx context.Context, request reconcile.Request) (ctrl.Result, error) { log := ctrl.LoggerFrom(ctx) log.Info("Reconciling KeycloakRealmUser") var instance keycloakApi.KeycloakRealmUser if err := r.client.Get(ctx, request.NamespacedName, &instance); err != nil { if k8sErrors.IsNotFound(err) { return ctrl.Result{}, nil } return ctrl.Result{}, errors.Wrap(err, "unable to get keycloak realm user from k8s") } if updated, err := r.applyDefaults(ctx, &instance); err != nil { return ctrl.Result{}, fmt.Errorf("unable to apply default values: %w", err) } else if updated { return ctrl.Result{}, nil } oldStatus := instance.Status if err := r.tryReconcile(ctx, &instance); err != nil { log.Error(err, "An error has occurred while handling KeycloakRealmUser") if errors.Is(err, helper.ErrKeycloakIsNotAvailable) { return helper.RequeueOnKeycloakNotAvailable, nil } instance.Status.Value = err.Error() if statusErr := r.updateKeycloakRealmUserStatus(ctx, &instance, oldStatus); statusErr != nil { return ctrl.Result{}, statusErr } return ctrl.Result{}, err } instance.Status.Value = helper.StatusOK if statusErr := r.updateKeycloakRealmUserStatus(ctx, &instance, oldStatus); statusErr != nil { return ctrl.Result{}, statusErr } log.Info("Reconciling KeycloakRealmUser done") return ctrl.Result{}, nil } func (r *Reconcile) tryReconcile(ctx context.Context, instance *keycloakApi.KeycloakRealmUser) error { err := r.helper.SetRealmOwnerRef(ctx, instance) if err != nil { return fmt.Errorf("unable to set realm owner ref: %w", err) } kClient, err := r.helper.CreateKeycloakClientFromRealmRef(ctx, instance) if err != nil { return fmt.Errorf("unable to create keycloak client from ref: %w", err) } realm, err := r.helper.GetKeycloakRealmFromRef(ctx, instance, kClient) if err != nil { return fmt.Errorf("unable to get keycloak realm from ref: %w", err) } if instance.Spec.KeepResource { deleted, err := r.helper.TryToDelete(ctx, instance, makeTerminator( gocloak.PString(realm.Realm), instance.Spec.Username, kClient, objectmeta.PreserveResourcesOnDeletion(instance), ), finalizer, ) if err != nil { return fmt.Errorf("failed to delete keycloak realm user: %w", err) } if deleted { return nil } } password, getPasswordErr := r.getPassword(ctx, instance) if getPasswordErr != nil { return fmt.Errorf("unable to get password: %w", getPasswordErr) } if err := kClient.SyncRealmUser(ctx, gocloak.PString(realm.Realm), &adapter.KeycloakUser{ Username: instance.Spec.Username, Groups: instance.Spec.Groups, Roles: instance.Spec.Roles, RequiredUserActions: instance.Spec.RequiredUserActions, LastName: instance.Spec.LastName, FirstName: instance.Spec.FirstName, EmailVerified: instance.Spec.EmailVerified, Enabled: instance.Spec.Enabled, Email: instance.Spec.Email, Attributes: instance.Spec.Attributes, Password: password, }, instance.GetReconciliationStrategy() == keycloakApi.ReconciliationStrategyAddOnly); err != nil { return errors.Wrap(err, "unable to sync realm user") } if !instance.Spec.KeepResource { if err := r.client.Delete(ctx, instance); err != nil { return errors.Wrap(err, "unable to delete instance of keycloak realm user") } } return nil } func (r *Reconcile) getPassword(ctx context.Context, instance *keycloakApi.KeycloakRealmUser) (string, error) { log := ctrl.LoggerFrom(ctx) if instance.Spec.PasswordSecret.Name != "" && instance.Spec.PasswordSecret.Key != "" { secret := &coreV1.Secret{} if err := r.client.Get(ctx, types.NamespacedName{Name: instance.Spec.PasswordSecret.Name, Namespace: instance.Namespace}, secret); err != nil { if k8sErrors.IsNotFound(err) { return "", errors.Wrapf(err, "secret %s not found", instance.Spec.PasswordSecret.Name) } return "", errors.Wrapf(err, "unable to get secret %s", instance.Spec.PasswordSecret.Name) } passwordBytes, ok := secret.Data[instance.Spec.PasswordSecret.Key] if !ok { return "", errors.Errorf("key %s not found in secret %s", instance.Spec.PasswordSecret.Key, instance.Spec.PasswordSecret.Name) } log.Info("Using password from secret", "secret", instance.Spec.PasswordSecret.Name) return string(passwordBytes), nil } log.Info("Using password from instance Spec.password") return instance.Spec.Password, nil } func (r *Reconcile) applyDefaults(ctx context.Context, instance *keycloakApi.KeycloakRealmUser) (bool, error) { if instance.Spec.RealmRef.Name == "" { instance.Spec.RealmRef = common.RealmRef{ Kind: keycloakApi.KeycloakRealmKind, Name: instance.Spec.Realm, } if err := r.client.Update(ctx, instance); err != nil { return false, fmt.Errorf("failed to update default values: %w", err) } return true, nil } return false, nil } func (r *Reconcile) updateKeycloakRealmUserStatus( ctx context.Context, user *keycloakApi.KeycloakRealmUser, oldStatus keycloakApi.KeycloakRealmUserStatus, ) error { if user.Status == oldStatus { return nil } if err := r.client.Status().Update(ctx, user); err != nil { return fmt.Errorf("failed to update KeycloakRealmUser status: %w", err) } return nil }