controllers/keycloakrealmrolebatch/keycloakrealmrolebatch_controller.go (193 lines of code) (raw):
package keycloakrealmrolebatch
import (
"context"
"fmt"
"time"
"github.com/Nerzal/gocloak/v12"
"github.com/pkg/errors"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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/controller/controllerutil"
"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/objectmeta"
)
const keyCloakRealmRoleBatchOperatorFinalizerName = "keycloak.realmrolebatch.operator.finalizer.name"
type Helper interface {
TryToDelete(ctx context.Context, obj client.Object, terminator helper.Terminator, finalizer string) (isDeleted bool, resultErr error)
SetRealmOwnerRef(ctx context.Context, object helper.ObjectWithRealmRef) error
SetFailureCount(fc helper.FailureCountable) time.Duration
}
func NewReconcileKeycloakRealmRoleBatch(client client.Client, helper Helper) *ReconcileKeycloakRealmRoleBatch {
return &ReconcileKeycloakRealmRoleBatch{
client: client,
helper: helper,
}
}
type ReconcileKeycloakRealmRoleBatch struct {
client client.Client
helper Helper
successReconcileTimeout time.Duration
}
func (r *ReconcileKeycloakRealmRoleBatch) SetupWithManager(mgr ctrl.Manager, successReconcileTimeout time.Duration) error {
r.successReconcileTimeout = successReconcileTimeout
pred := predicate.Funcs{
UpdateFunc: helper.IsFailuresUpdated,
}
err := ctrl.NewControllerManagedBy(mgr).
For(&keycloakApi.KeycloakRealmRoleBatch{}, builder.WithPredicates(pred)).
Complete(r)
if err != nil {
return fmt.Errorf("failed to setup KeycloakRealmRoleBatch controller: %w", err)
}
return nil
}
//+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakrealmrolebatches,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakrealmrolebatches/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakrealmrolebatches/finalizers,verbs=update
// Reconcile is a loop for reconciling KeycloakRealmRoleBatch object.
func (r *ReconcileKeycloakRealmRoleBatch) Reconcile(ctx context.Context, request reconcile.Request) (result reconcile.Result, resultErr error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Reconciling KeycloakRealmRoleBatch")
var instance keycloakApi.KeycloakRealmRoleBatch
if err := r.client.Get(ctx, request.NamespacedName, &instance); err != nil {
if k8sErrors.IsNotFound(err) {
return
}
resultErr = errors.Wrap(err, "unable to get keycloak realm role batch from k8s")
return
}
if updated, err := r.applyDefaults(ctx, &instance); err != nil {
resultErr = fmt.Errorf("unable to apply default values: %w", err)
return
} else if updated {
return
}
if err := r.tryReconcile(ctx, &instance); err != nil {
instance.Status.Value = err.Error()
result.RequeueAfter = r.helper.SetFailureCount(&instance)
log.Error(err, "an error has occurred while handling keycloak realm role batch")
} else {
helper.SetSuccessStatus(&instance)
result.RequeueAfter = r.successReconcileTimeout
}
instanceDeleted := !controllerutil.ContainsFinalizer(&instance, keyCloakRealmRoleBatchOperatorFinalizerName) &&
instance.GetDeletionTimestamp() != nil
if !instanceDeleted {
if err := r.client.Status().Update(ctx, &instance); err != nil {
resultErr = err
}
}
log.Info("Reconciling done")
return
}
func (r *ReconcileKeycloakRealmRoleBatch) isOwner(batch *keycloakApi.KeycloakRealmRoleBatch, role *keycloakApi.KeycloakRealmRole) bool {
for _, owner := range role.GetOwnerReferences() {
if owner.Kind == batch.Kind && owner.Name == batch.Name && owner.UID == batch.UID {
return true
}
}
return false
}
func (r *ReconcileKeycloakRealmRoleBatch) removeRoles(ctx context.Context, batch *keycloakApi.KeycloakRealmRoleBatch) error {
var (
namespaceRoles keycloakApi.KeycloakRealmRoleList
specRoles = make(map[string]struct{})
)
if err := r.client.List(ctx, &namespaceRoles); err != nil {
return errors.Wrap(err, "unable to get keycloak realm roles")
}
for _, r := range batch.Spec.Roles {
specRoles[batch.FormattedRoleName(r.Name)] = struct{}{}
}
for i := range namespaceRoles.Items {
if _, ok := specRoles[namespaceRoles.Items[i].Name]; !ok && r.isOwner(batch, &namespaceRoles.Items[i]) {
if err := r.client.Delete(ctx, &namespaceRoles.Items[i]); err != nil {
return errors.Wrap(err, "unable to delete keycloak realm role")
}
}
}
return nil
}
func (r *ReconcileKeycloakRealmRoleBatch) putRoles(
ctx context.Context,
batch *keycloakApi.KeycloakRealmRoleBatch,
) (roles []keycloakApi.KeycloakRealmRole, resultErr error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Start putting keycloak cr role batch")
for _, role := range batch.Spec.Roles {
roleName := batch.FormattedRoleName(role.Name)
var crRole keycloakApi.KeycloakRealmRole
err := r.client.Get(ctx, types.NamespacedName{Namespace: batch.Namespace, Name: roleName}, &crRole)
if err != nil && !k8sErrors.IsNotFound(err) {
return nil, errors.Wrap(err, "unable to check batch role")
} else if err == nil {
if r.isOwner(batch, &crRole) {
log.Info("Role already created")
roles = append(roles, crRole)
continue
}
return nil, errors.New("one of batch role already exists")
}
newRole := keycloakApi.KeycloakRealmRole{
ObjectMeta: metav1.ObjectMeta{Name: roleName,
Namespace: batch.Namespace,
OwnerReferences: []metav1.OwnerReference{
{Name: batch.Name, Kind: batch.Kind, BlockOwnerDeletion: gocloak.BoolP(true), UID: batch.UID,
APIVersion: batch.APIVersion},
}},
Spec: keycloakApi.KeycloakRealmRoleSpec{
Name: role.Name,
RealmRef: batch.GetRealmRef(),
Composite: role.Composite,
Composites: role.Composites,
Description: role.Description,
Attributes: role.Attributes,
IsDefault: role.IsDefault,
}}
if err := r.client.Create(ctx, &newRole); err != nil {
return nil, errors.Wrap(err, "unable to create child role from batch")
}
roles = append(roles, newRole)
}
log.Info("Realm role batch put successfully")
return
}
func (r *ReconcileKeycloakRealmRoleBatch) tryReconcile(ctx context.Context, batch *keycloakApi.KeycloakRealmRoleBatch) error {
err := r.helper.SetRealmOwnerRef(ctx, batch)
if err != nil {
return fmt.Errorf("unable to set realm owner ref: %w", err)
}
createdRoles, err := r.putRoles(ctx, batch)
if err != nil {
return errors.Wrap(err, "unable to put roles batch")
}
if err := r.removeRoles(ctx, batch); err != nil {
return errors.Wrap(err, "unable to delete roles")
}
if _, err := r.helper.TryToDelete(
ctx,
batch,
makeTerminator(r.client, createdRoles, objectmeta.PreserveResourcesOnDeletion(batch)),
keyCloakRealmRoleBatchOperatorFinalizerName,
); err != nil {
return fmt.Errorf("unable to delete keycloak realm role batch: %w", err)
}
return nil
}
func (r *ReconcileKeycloakRealmRoleBatch) applyDefaults(ctx context.Context, instance *keycloakApi.KeycloakRealmRoleBatch) (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
}