controllers/keycloakclientscope/keycloakclientscope_controller.go (199 lines of code) (raw):

package keycloakclientscope import ( "context" "fmt" "reflect" "time" "github.com/Nerzal/gocloak/v12" "github.com/pkg/errors" "golang.org/x/exp/maps" k8sErrors "k8s.io/apimachinery/pkg/api/errors" 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 finalizerName = "keycloak.clientscope.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{ UpdateFunc: isSpecUpdated, } err := ctrl.NewControllerManagedBy(mgr). For(&keycloakApi.KeycloakClientScope{}, builder.WithPredicates(pred)). Complete(r) if err != nil { return fmt.Errorf("failed to setup KeycloakClientScope controller: %w", err) } return nil } func isSpecUpdated(e event.UpdateEvent) bool { oo, ok := e.ObjectOld.(*keycloakApi.KeycloakClientScope) if !ok { return false } no, ok := e.ObjectNew.(*keycloakApi.KeycloakClientScope) if !ok { return false } return !reflect.DeepEqual(oo.Spec, no.Spec) || (oo.GetDeletionTimestamp().IsZero() && !no.GetDeletionTimestamp().IsZero()) } //+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakclientscopes,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakclientscopes/status,verbs=get;update;patch //+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakclientscopes/finalizers,verbs=update // Reconcile is a loop for reconciling KeycloakClientScope object. func (r *Reconcile) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { log := ctrl.LoggerFrom(ctx) log.Info("Reconciling KeycloakClientScope") scope := &keycloakApi.KeycloakClientScope{} if err := r.client.Get(ctx, request.NamespacedName, scope); err != nil { if k8sErrors.IsNotFound(err) { return reconcile.Result{}, nil } return reconcile.Result{}, fmt.Errorf("unable to get keycloak client scope from k8s: %w", err) } if updated, err := r.applyDefaults(ctx, scope); err != nil { return reconcile.Result{}, fmt.Errorf("unable to apply default values: %w", err) } else if updated { return reconcile.Result{}, nil } oldStatus := scope.Status scopeID, err := r.tryReconcile(ctx, scope) if err != nil { if errors.Is(err, helper.ErrKeycloakIsNotAvailable) { return helper.RequeueOnKeycloakNotAvailable, nil } scope.Status.Value = err.Error() if statusErr := r.updateKeycloakClientScopeStatus(ctx, scope, oldStatus); statusErr != nil { return reconcile.Result{}, statusErr } return reconcile.Result{}, err } scope.Status.Value = helper.StatusOK scope.Status.ID = scopeID if statusErr := r.updateKeycloakClientScopeStatus(ctx, scope, oldStatus); statusErr != nil { return reconcile.Result{}, statusErr } log.Info("Reconciling KeycloakClientScope done") return reconcile.Result{}, nil } func (r *Reconcile) tryReconcile(ctx context.Context, instance *keycloakApi.KeycloakClientScope) (string, error) { err := r.helper.SetRealmOwnerRef(ctx, instance) if err != nil { return "", fmt.Errorf("unable to set realm owner ref: %w", err) } cl, err := r.helper.CreateKeycloakClientFromRealmRef(ctx, instance) if err != nil { return "", fmt.Errorf("unable to create keycloak client from realm ref: %w", err) } realm, err := r.helper.GetKeycloakRealmFromRef(ctx, instance, cl) if err != nil { return "", fmt.Errorf("unable to get keycloak realm from ref: %w", err) } scopeID, err := syncClientScope(ctx, instance, gocloak.PString(realm.Realm), cl) if err != nil { return "", errors.Wrap(err, "unable to sync client scope") } if _, err = r.helper.TryToDelete(ctx, instance, makeTerminator( cl, gocloak.PString(realm.Realm), instance.Status.ID, objectmeta.PreserveResourcesOnDeletion(instance), ), finalizerName, ); err != nil { return "", fmt.Errorf("unable to delete client scope: %w", err) } return scopeID, nil } func (r *Reconcile) applyDefaults(ctx context.Context, instance *keycloakApi.KeycloakClientScope) (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) updateKeycloakClientScopeStatus( ctx context.Context, scope *keycloakApi.KeycloakClientScope, oldStatus keycloakApi.KeycloakClientScopeStatus, ) error { if scope.Status == oldStatus { return nil } if err := r.client.Status().Update(ctx, scope); err != nil { return fmt.Errorf("failed to update KeycloakClientScope status: %w", err) } return nil } func syncClientScope(ctx context.Context, instance *keycloakApi.KeycloakClientScope, realmName string, cl keycloak.Client) (string, error) { clientScope, err := cl.GetClientScope(instance.Spec.Name, realmName) if err != nil && !adapter.IsErrNotFound(err) { return "", errors.Wrap(err, "unable to get client scope") } cScope := adapter.ClientScope{ Name: instance.Spec.Name, Attributes: instance.Spec.Attributes, Protocol: instance.Spec.Protocol, ProtocolMappers: convertProtocolMappers(instance.Spec.ProtocolMappers), Description: instance.Spec.Description, Default: instance.Spec.Default, } if err == nil { if instance.Status.ID == "" { instance.Status.ID = clientScope.ID } if err = cl.UpdateClientScope(ctx, realmName, instance.Status.ID, &cScope); err != nil { return "", errors.Wrap(err, "unable to update client scope") } return instance.Status.ID, nil } id, err := cl.CreateClientScope(ctx, realmName, &cScope) if err != nil { return "", errors.Wrap(err, "unable to create client scope") } instance.Status.ID = id return instance.Status.ID, nil } func convertProtocolMappers(mappers []keycloakApi.ProtocolMapper) []adapter.ProtocolMapper { aMappers := make([]adapter.ProtocolMapper, 0, len(mappers)) for _, m := range mappers { pm := adapter.ProtocolMapper{ Name: m.Name, Config: make(map[string]string, len(m.Config)), ProtocolMapper: m.ProtocolMapper, Protocol: m.Protocol, } maps.Copy(pm.Config, m.Config) aMappers = append(aMappers, pm) } return aMappers }