controllers/keycloakrealmcomponent/keycloakrealmcomponent_controller.go (278 lines of code) (raw):
package keycloakrealmcomponent
import (
"context"
"errors"
"fmt"
"reflect"
"time"
"github.com/Nerzal/gocloak/v12"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
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/client/apiutil"
"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.realmcomponent.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 RefClient interface {
MapComponentConfigSecretsRefs(ctx context.Context, config map[string][]string, namespace string) error
}
type Reconcile struct {
client client.Client
helper Helper
secretRefClient RefClient
successReconcileTimeout time.Duration
scheme *runtime.Scheme
}
func NewReconcile(client client.Client, scheme *runtime.Scheme, helper Helper, secretRefClient RefClient) *Reconcile {
return &Reconcile{
client: client,
scheme: scheme,
helper: helper,
secretRefClient: secretRefClient,
}
}
func (r *Reconcile) SetupWithManager(mgr ctrl.Manager, successReconcileTimeout time.Duration) error {
r.successReconcileTimeout = successReconcileTimeout
pred := predicate.Funcs{
UpdateFunc: isSpecUpdated,
}
err := ctrl.NewControllerManagedBy(mgr).
For(&keycloakApi.KeycloakRealmComponent{}, builder.WithPredicates(pred)).
Complete(r)
if err != nil {
return fmt.Errorf("failed to setup keycloakRealmComponent controller: %w", err)
}
return nil
}
func isSpecUpdated(e event.UpdateEvent) bool {
oo, ok := e.ObjectOld.(*keycloakApi.KeycloakRealmComponent)
if !ok {
return false
}
no, ok := e.ObjectNew.(*keycloakApi.KeycloakRealmComponent)
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=keycloakrealmcomponents,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakrealmcomponents/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakrealmcomponents/finalizers,verbs=update
// Reconcile is a loop for reconciling KeycloakRealmComponent object.
// nolint:cyclop
// nolint:funlen
func (r *Reconcile) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Reconciling KeycloakRealmComponent")
keycloakRealmComponent := &keycloakApi.KeycloakRealmComponent{}
if err := r.client.Get(ctx, request.NamespacedName, keycloakRealmComponent); err != nil {
if k8sErrors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return ctrl.Result{}, fmt.Errorf("unable to get KeycloakRealmComponent: %w", err)
}
if updated, err := r.applyDefaults(ctx, keycloakRealmComponent); err != nil {
return ctrl.Result{}, fmt.Errorf("unable to apply defaults: %w", err)
} else if updated {
return ctrl.Result{}, nil
}
err := r.helper.SetRealmOwnerRef(ctx, keycloakRealmComponent)
if err != nil {
return ctrl.Result{}, fmt.Errorf("unable to get realm owner ref: %w", err)
}
if err = r.setComponentOwnerReference(ctx, keycloakRealmComponent); err != nil {
return reconcile.Result{}, err
}
kClient, err := r.helper.CreateKeycloakClientFromRealmRef(ctx, keycloakRealmComponent)
if err != nil {
if errors.Is(err, helper.ErrKeycloakIsNotAvailable) {
return ctrl.Result{
RequeueAfter: helper.RequeueOnKeycloakNotAvailablePeriod,
}, nil
}
return ctrl.Result{}, fmt.Errorf("unable to create keycloak client: %w", err)
}
realm, err := r.helper.GetKeycloakRealmFromRef(ctx, keycloakRealmComponent, kClient)
if err != nil {
return ctrl.Result{}, fmt.Errorf("unable to get realm: %w", err)
}
term := makeTerminator(
gocloak.PString(realm.Realm),
keycloakRealmComponent.Spec.Name,
kClient,
objectmeta.PreserveResourcesOnDeletion(keycloakRealmComponent),
)
if deleted, err := r.helper.TryToDelete(ctx, keycloakRealmComponent, term, finalizerName); err != nil {
return ctrl.Result{}, fmt.Errorf("unable to tryToDelete realm component %w", err)
} else if deleted {
return reconcile.Result{}, nil
}
if err := r.tryReconcile(ctx, keycloakRealmComponent, gocloak.PString(realm.Realm), kClient); err != nil {
keycloakRealmComponent.Status.Value = err.Error()
requeueAfter := r.helper.SetFailureCount(keycloakRealmComponent)
if statusErr := r.client.Status().Update(ctx, keycloakRealmComponent); statusErr != nil {
return ctrl.Result{}, fmt.Errorf("unable to update KeycloakRealmComponent status: %w", statusErr)
}
return ctrl.Result{
RequeueAfter: requeueAfter,
}, fmt.Errorf("unable to reconcile KeycloakRealmComponent: %w", err)
}
helper.SetSuccessStatus(keycloakRealmComponent)
if err := r.client.Status().Update(ctx, keycloakRealmComponent); err != nil {
return ctrl.Result{}, fmt.Errorf("unable to update KeycloakRealmComponent status: %w", err)
}
return reconcile.Result{}, nil
}
func (r *Reconcile) tryReconcile(
ctx context.Context,
keycloakRealmComponent *keycloakApi.KeycloakRealmComponent,
realmName string,
kClient keycloak.Client,
) error {
keycloakComponent, err := r.createKeycloakComponent(ctx, keycloakRealmComponent, realmName, kClient)
if err != nil {
return fmt.Errorf("unable to create keycloak component: %w", err)
}
if err = r.secretRefClient.MapComponentConfigSecretsRefs(ctx, keycloakComponent.Config, keycloakRealmComponent.Namespace); err != nil {
return fmt.Errorf("unable to map config secrets: %w", err)
}
cmp, err := kClient.GetComponent(ctx, realmName, keycloakRealmComponent.Spec.Name)
if err != nil {
if !adapter.IsErrNotFound(err) {
return fmt.Errorf("unable to get component, unexpected error: %w", err)
}
if err := kClient.CreateComponent(ctx, realmName, keycloakComponent); err != nil {
return fmt.Errorf("unable to create component %w", err)
}
return nil
}
keycloakComponent.ID = cmp.ID
if err := kClient.UpdateComponent(ctx, realmName, keycloakComponent); err != nil {
return fmt.Errorf("unable to update component: %w", err)
}
return nil
}
func (r *Reconcile) createKeycloakComponent(
ctx context.Context,
component *keycloakApi.KeycloakRealmComponent,
kcRealmName string,
kClient keycloak.Client,
) (*adapter.Component, error) {
ksComponent := &adapter.Component{
Name: component.Spec.Name,
Config: make(map[string][]string),
ProviderID: component.Spec.ProviderID,
ProviderType: component.Spec.ProviderType,
}
for k, v := range component.Spec.Config {
ksComponent.Config[k] = make([]string, len(v))
copy(ksComponent.Config[k], v)
}
parenID, err := r.getParentID(ctx, component, kcRealmName, kClient)
if err != nil {
return nil, fmt.Errorf("unable to get parent id: %w", err)
}
if parenID != "" {
ksComponent.ParentID = parenID
}
return ksComponent, nil
}
func (r *Reconcile) getParentID(
ctx context.Context,
component *keycloakApi.KeycloakRealmComponent,
kcRealmName string,
kClient keycloak.Client,
) (string, error) {
if component.Spec.ParentRef == nil {
return "", nil
}
if component.Spec.ParentRef.Kind == keycloakApi.KeycloakRealmKind {
parentRealm := &keycloakApi.KeycloakRealm{}
if err := r.client.Get(ctx, types.NamespacedName{Name: component.Spec.ParentRef.Name, Namespace: component.GetNamespace()}, parentRealm); err != nil {
return "", fmt.Errorf("unable to get parent kcRealmName: %w", err)
}
kcParentRealm, err := kClient.GetRealm(ctx, parentRealm.Spec.RealmName)
if err != nil {
return "", fmt.Errorf("unable to get parent kcRealmName: %w", err)
}
if kcParentRealm.ID == nil || *kcParentRealm.ID == "" {
return "", fmt.Errorf("kcRealmName id is empty")
}
return *kcParentRealm.ID, nil
}
if component.Spec.ParentRef.Kind == keycloakApi.KeycloakRealmComponentKind {
parentComponent := &keycloakApi.KeycloakRealmComponent{}
if err := r.client.Get(ctx, types.NamespacedName{Name: component.Spec.ParentRef.Name, Namespace: component.GetNamespace()}, parentComponent); err != nil {
return "", fmt.Errorf("unable to get parent component: %w", err)
}
kcParentComponent, err := kClient.GetComponent(ctx, kcRealmName, parentComponent.Spec.Name)
if err != nil {
return "", fmt.Errorf("unable to get parent component: %w", err)
}
return kcParentComponent.ID, nil
}
return "", fmt.Errorf("parent kind %s is not supported", component.Spec.ParentRef.Kind)
}
// setComponentOwnerReference sets the owner reference for the component.
// In case the component has a parent component, we need to set owner reference to it
// to trigger the deletion of the child KeycloakRealmComponent.
// In the keycloak API side child component is automatically deleted,
// so we need to do the same with the KeycloakRealmComponent resource.
func (r *Reconcile) setComponentOwnerReference(
ctx context.Context,
component *keycloakApi.KeycloakRealmComponent,
) error {
if component.Spec.ParentRef == nil || component.Spec.ParentRef.Kind != keycloakApi.KeycloakRealmComponentKind {
return nil
}
for _, ref := range component.GetOwnerReferences() {
if ref.Kind == keycloakApi.KeycloakRealmComponentKind {
return nil
}
}
parentComponent := &keycloakApi.KeycloakRealmComponent{}
if err := r.client.Get(ctx, types.NamespacedName{Name: component.Spec.ParentRef.Name, Namespace: component.GetNamespace()}, parentComponent); err != nil {
return fmt.Errorf("unable to get parent component: %w", err)
}
gvk, err := apiutil.GVKForObject(parentComponent, r.scheme)
if err != nil {
return fmt.Errorf("unable to get gvk for parent component: %w", err)
}
ref := metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: parentComponent.GetName(),
UID: parentComponent.GetUID(),
BlockOwnerDeletion: pointer.Bool(true),
Controller: pointer.Bool(true),
}
component.SetOwnerReferences([]v1.OwnerReference{ref})
if err := r.client.Update(ctx, component); err != nil {
return fmt.Errorf("failed to set owner reference %s: %w", parentComponent.Name, err)
}
return nil
}
func (r *Reconcile) applyDefaults(ctx context.Context, instance *keycloakApi.KeycloakRealmComponent) (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
}