controllers/keycloakrealmidentityprovider/keycloakrealmidentityprovider_controller.go (217 lines of code) (raw):
package keycloakrealmidentityprovider
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.realmidp.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 {
MapConfigSecretsRefs(ctx context.Context, config map[string]string, namespace string) error
}
type Reconcile struct {
client client.Client
helper Helper
secretRefClient RefClient
successReconcileTimeout time.Duration
}
func NewReconcile(client client.Client, helper Helper, secretRefClient RefClient) *Reconcile {
return &Reconcile{
client: client,
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.KeycloakRealmIdentityProvider{}, builder.WithPredicates(pred)).
Complete(r)
if err != nil {
return fmt.Errorf("failed to setup KeycloakRealmIdentityProvider controller: %w", err)
}
return nil
}
func isSpecUpdated(e event.UpdateEvent) bool {
oo, ok := e.ObjectOld.(*keycloakApi.KeycloakRealmIdentityProvider)
if !ok {
return false
}
no, ok := e.ObjectNew.(*keycloakApi.KeycloakRealmIdentityProvider)
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=keycloakrealmidentityproviders,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakrealmidentityproviders/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=v1.edp.epam.com,namespace=placeholder,resources=keycloakrealmidentityproviders/finalizers,verbs=update
// Reconcile is a loop for reconciling KeycloakRealmIdentityProvider object.
func (r *Reconcile) Reconcile(ctx context.Context, request reconcile.Request) (result reconcile.Result, resultErr error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Reconciling KeycloakRealmIdentityProvider")
var instance keycloakApi.KeycloakRealmIdentityProvider
if err := r.client.Get(ctx, request.NamespacedName, &instance); err != nil {
if k8sErrors.IsNotFound(err) {
log.Info("instance not found")
return
}
resultErr = errors.Wrap(err, "unable to get keycloak realm idp 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 {
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 realm idp", "name", request.Name)
} else {
helper.SetSuccessStatus(&instance)
result.RequeueAfter = r.successReconcileTimeout
}
if err := r.client.Status().Update(ctx, &instance); err != nil {
resultErr = errors.Wrap(err, "unable to update status")
}
return
}
func (r *Reconcile) tryReconcile(ctx context.Context, keycloakRealmIDP *keycloakApi.KeycloakRealmIdentityProvider) error {
err := r.helper.SetRealmOwnerRef(ctx, keycloakRealmIDP)
if err != nil {
return fmt.Errorf("unable to set realm owner ref: %w", err)
}
kClient, err := r.helper.CreateKeycloakClientFromRealmRef(ctx, keycloakRealmIDP)
if err != nil {
return fmt.Errorf("unable to create keycloak client from realm ref: %w", err)
}
realm, err := r.helper.GetKeycloakRealmFromRef(ctx, keycloakRealmIDP, kClient)
if err != nil {
return fmt.Errorf("unable to get keycloak realm from ref: %w", err)
}
keycloakIDP := createKeycloakIDPFromSpec(&keycloakRealmIDP.Spec)
if err = r.secretRefClient.MapConfigSecretsRefs(ctx, keycloakIDP.Config, keycloakRealmIDP.Namespace); err != nil {
return fmt.Errorf("unable to map config secrets: %w", err)
}
providerExists, err := kClient.IdentityProviderExists(ctx, gocloak.PString(realm.Realm), keycloakRealmIDP.Spec.Alias)
if err != nil {
return fmt.Errorf("failed to check if the identity provider exists: %w", err)
}
if providerExists {
if err = kClient.UpdateIdentityProvider(ctx, gocloak.PString(realm.Realm), keycloakIDP); err != nil {
return errors.Wrap(err, "unable to update idp")
}
} else {
if err = kClient.CreateIdentityProvider(ctx, gocloak.PString(realm.Realm), keycloakIDP); err != nil {
return errors.Wrap(err, "unable to create idp")
}
}
if err := syncIDPMappers(ctx, &keycloakRealmIDP.Spec, kClient, gocloak.PString(realm.Realm)); err != nil {
return errors.Wrap(err, "unable to sync idp mappers")
}
term := makeTerminator(
gocloak.PString(realm.Realm),
keycloakRealmIDP.Spec.Alias,
kClient,
objectmeta.PreserveResourcesOnDeletion(keycloakRealmIDP),
)
if _, err := r.helper.TryToDelete(ctx, keycloakRealmIDP, term, finalizerName); err != nil {
return errors.Wrap(err, "unable to delete realm idp")
}
return nil
}
func (r *Reconcile) applyDefaults(ctx context.Context, instance *keycloakApi.KeycloakRealmIdentityProvider) (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 syncIDPMappers(ctx context.Context, idpSpec *keycloakApi.KeycloakRealmIdentityProviderSpec,
kClient keycloak.Client, targetRealm string) error {
if len(idpSpec.Mappers) == 0 {
return nil
}
mappers, err := kClient.GetIDPMappers(ctx, targetRealm, idpSpec.Alias)
if err != nil {
return errors.Wrap(err, "unable to get idp mappers")
}
for _, m := range mappers {
if err := kClient.DeleteIDPMapper(ctx, targetRealm, idpSpec.Alias, m.ID); err != nil {
return errors.Wrap(err, "unable to delete idp mapper")
}
}
for _, m := range idpSpec.Mappers {
if m.IdentityProviderAlias == "" {
m.IdentityProviderAlias = idpSpec.Alias
}
if _, err := kClient.CreateIDPMapper(ctx, targetRealm, idpSpec.Alias,
createKeycloakIDPMapperFromSpec(&m)); err != nil {
return errors.Wrap(err, "unable to create idp mapper")
}
}
return nil
}
func createKeycloakIDPMapperFromSpec(spec *keycloakApi.IdentityProviderMapper) *adapter.IdentityProviderMapper {
m := &adapter.IdentityProviderMapper{
IdentityProviderMapper: spec.IdentityProviderMapper,
Name: spec.Name,
Config: make(map[string]string, len(spec.Config)),
IdentityProviderAlias: spec.IdentityProviderAlias,
}
maps.Copy(m.Config, spec.Config)
return m
}
func createKeycloakIDPFromSpec(spec *keycloakApi.KeycloakRealmIdentityProviderSpec) *adapter.IdentityProvider {
p := &adapter.IdentityProvider{
Config: make(map[string]string, len(spec.Config)),
ProviderID: spec.ProviderID,
Alias: spec.Alias,
Enabled: spec.Enabled,
AddReadTokenRoleOnCreate: spec.AddReadTokenRoleOnCreate,
AuthenticateByDefault: spec.AuthenticateByDefault,
DisplayName: spec.DisplayName,
FirstBrokerLoginFlowAlias: spec.FirstBrokerLoginFlowAlias,
LinkOnly: spec.LinkOnly,
StoreToken: spec.StoreToken,
TrustEmail: spec.TrustEmail,
}
maps.Copy(p.Config, spec.Config)
return p
}