controllers/keycloakclient/keycloakclient_controller.go (186 lines of code) (raw):

package keycloakclient import ( "context" "errors" "fmt" "time" "github.com/Nerzal/gocloak/v12" pkgErrors "github.com/pkg/errors" 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/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/controllers/keycloakclient/chain" "github.com/epam/edp-keycloak-operator/pkg/client/keycloak" "github.com/epam/edp-keycloak-operator/pkg/objectmeta" ) 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 CreateKeycloakClientFromRealmRef(ctx context.Context, object helper.ObjectWithRealmRef) (keycloak.Client, error) GetKeycloakRealmFromRef(ctx context.Context, object helper.ObjectWithRealmRef, kcClient keycloak.Client) (*gocloak.RealmRepresentation, error) } const ( keyCloakClientOperatorFinalizerName = "keycloak.client.operator.finalizer.name" clientAttributeLogoutRedirectUris = "post.logout.redirect.uris" clientAttributeLogoutRedirectUrisDefValue = "+" ) func NewReconcileKeycloakClient(client client.Client, helper Helper) *ReconcileKeycloakClient { return &ReconcileKeycloakClient{ client: client, helper: helper, } } // ReconcileKeycloakClient reconciles a KeycloakClient object. type ReconcileKeycloakClient struct { client client.Client helper Helper successReconcileTimeout time.Duration } func (r *ReconcileKeycloakClient) SetupWithManager(mgr ctrl.Manager, successReconcileTimeout time.Duration) error { r.successReconcileTimeout = successReconcileTimeout pred := predicate.Funcs{ UpdateFunc: helper.IsFailuresUpdated, } err := ctrl.NewControllerManagedBy(mgr). For(&keycloakApi.KeycloakClient{}, builder.WithPredicates(pred)). Complete(r) if err != nil { return fmt.Errorf("failed to setup KeycloakClient controller: %w", err) } return nil } //+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakclients,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakclients/status,verbs=get;update;patch //+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakclients/finalizers,verbs=update // Reconcile is a loop for reconciling KeycloakClient object. func (r *ReconcileKeycloakClient) Reconcile(ctx context.Context, request reconcile.Request) (result reconcile.Result, resultErr error) { log := ctrl.LoggerFrom(ctx) log.Info("Reconciling KeycloakClient") var instance keycloakApi.KeycloakClient if err := r.client.Get(ctx, request.NamespacedName, &instance); err != nil { if k8sErrors.IsNotFound(err) { return } resultErr = err return } if updated, err := r.applyDefaults(ctx, &instance); err != nil { return reconcile.Result{}, err } else if updated { return reconcile.Result{}, nil } if err := r.tryReconcile(ctx, &instance); err != nil { if errors.Is(err, helper.ErrKeycloakIsNotAvailable) { return ctrl.Result{ RequeueAfter: helper.RequeueOnKeycloakNotAvailablePeriod, }, nil } instance.Status.Value = err.Error() result.RequeueAfter = r.helper.SetFailureCount(&instance) log.Error(err, "an error has occurred while handling keycloak client", "name", request.Name) } else { helper.SetSuccessStatus(&instance) result.RequeueAfter = r.successReconcileTimeout } if err := r.client.Status().Update(ctx, &instance); err != nil { resultErr = pkgErrors.Wrap(err, "unable to update status") } return } func (r *ReconcileKeycloakClient) tryReconcile(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient) error { err := r.helper.SetRealmOwnerRef(ctx, keycloakClient) if err != nil { return fmt.Errorf("unable to set realm owner ref: %w", err) } kClient, err := r.helper.CreateKeycloakClientFromRealmRef(ctx, keycloakClient) if err != nil { return fmt.Errorf("unable to create keycloak client from realm ref: %w", err) } realm, err := r.getKeycloakRealm(ctx, keycloakClient, kClient) if err != nil { return fmt.Errorf("unable to get keycloak realm: %w", err) } deleted, err := r.helper.TryToDelete( ctx, keycloakClient, makeTerminator(keycloakClient.Status.ClientID, realm, kClient, objectmeta.PreserveResourcesOnDeletion(keycloakClient)), keyCloakClientOperatorFinalizerName, ) if err != nil { return fmt.Errorf("deliting keycloak client: %w", err) } if deleted { return nil } if err = chain.MakeChain(kClient, r.client).Serve(ctx, keycloakClient, realm); err != nil { return fmt.Errorf("unable to serve keycloak client: %w", err) } return nil } // applyDefaults applies default values to KeycloakClient. func (r *ReconcileKeycloakClient) applyDefaults(ctx context.Context, keycloakClient *keycloakApi.KeycloakClient) (bool, error) { if keycloakClient.Spec.Attributes == nil { keycloakClient.Spec.Attributes = make(map[string]string) } updated := false if _, ok := keycloakClient.Spec.Attributes[clientAttributeLogoutRedirectUris]; !ok { // set default value for logout redirect uris to "+" is required for correct logout from keycloak keycloakClient.Spec.Attributes[clientAttributeLogoutRedirectUris] = clientAttributeLogoutRedirectUrisDefValue updated = true } if keycloakClient.Spec.RealmRef.Name == "" { realmName, err := r.getKeycloakCRName(ctx, keycloakClient.Spec.TargetRealm, keycloakClient.Namespace) if err != nil { return false, fmt.Errorf("unable to get keycloak cr name: %w", err) } keycloakClient.Spec.RealmRef = common.RealmRef{ Kind: keycloakApi.KeycloakRealmKind, Name: realmName, } updated = true } if keycloakClient.Spec.WebOrigins == nil { keycloakClient.Spec.WebOrigins = []string{ keycloakClient.Spec.WebUrl, } updated = true } if updated { if err := r.client.Update(ctx, keycloakClient); err != nil { return false, fmt.Errorf("failed to update keycloak client default values: %w", err) } return true, nil } return false, nil } func (r *ReconcileKeycloakClient) getKeycloakCRName(ctx context.Context, targetRealm, namespace string) (string, error) { realmList := &keycloakApi.KeycloakRealmList{} if err := r.client.List(ctx, realmList, client.InNamespace(namespace)); err != nil { return "", fmt.Errorf("unable to get realms: %w", err) } for i := 0; i < len(realmList.Items); i++ { if realmList.Items[i].Spec.RealmName == targetRealm { return realmList.Items[i].Name, nil } } // Add this for backward compatibility because in old versions KeycloakRealm CR name was hardcoded to "main". // We can remove this in the future release as RealmRef will be set for all KeycloakClient CRs. for i := 0; i < len(realmList.Items); i++ { if realmList.Items[i].Name == "main" { return realmList.Items[i].Name, nil } } return "", fmt.Errorf("realm %s not found", targetRealm) } func (r *ReconcileKeycloakClient) getKeycloakRealm( ctx context.Context, keycloakClient *keycloakApi.KeycloakClient, adapterClient keycloak.Client, ) (string, error) { if keycloakClient.Spec.TargetRealm == "" { realm, err := r.helper.GetKeycloakRealmFromRef(ctx, keycloakClient, adapterClient) if err != nil { return "", fmt.Errorf("unable to get keycloak realm from ref: %w", err) } return gocloak.PString(realm.Realm), nil } // If TargetRealm is set, use it instead of RealmRef. This is for backward compatibility. return keycloakClient.Spec.TargetRealm, nil }