package apps import ( "context" "fmt" "time" "github.com/Nerzal/gocloak/v11" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" appsv1alpha1 "libre.sh/controller/apis/apps/v1alpha1" "libre.sh/controller/internal" ) // OIDCClientReconciler reconciles a OIDCClient object type OIDCClientReconciler struct { client.Client Scheme *runtime.Scheme } //+kubebuilder:rbac:groups=apps.libre.sh,resources=oidcclients,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=apps.libre.sh,resources=oidcclients/status,verbs=get;update;patch //+kubebuilder:rbac:groups=apps.libre.sh,resources=oidcclients/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // TODO(user): Modify the Reconcile function to compare the state specified by // the OIDCClient object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.1/pkg/reconcile func (r *OIDCClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { log := log.FromContext(ctx) log.Info("reconciling") var oidcClient appsv1alpha1.OIDCClient err = r.Client.Get(ctx, req.NamespacedName, &oidcClient) if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } if oidcClient.Spec.Suspend { return ctrl.Result{}, nil } var kcErr error defer func() { if err != nil { return } if kcErr != nil { log.Error(kcErr, "Failed to provision oidcClient") result.RequeueAfter = 10 * time.Minute conditions.MarkStalled(&oidcClient, meta.FailedReason, kcErr.Error()) } else { conditions.Delete(&oidcClient, meta.StalledCondition) } if conditions.IsStalled(&oidcClient) { conditions.MarkFalse(&oidcClient, meta.ReadyCondition, meta.FailedReason, "Is stalled") } else if conditions.IsReconciling(&oidcClient) { conditions.MarkUnknown(&oidcClient, meta.ReadyCondition, meta.ProgressingReason, "Is reconciling") } else { conditions.MarkTrue(&oidcClient, meta.ReadyCondition, meta.SucceededReason, "Is provisionned") } err = r.Client.Status().Update(ctx, &oidcClient) }() if oidcClient.Generation != conditions.GetObservedGeneration(&oidcClient, meta.ReadyCondition) { conditions.MarkReconciling(&oidcClient, meta.ProgressingReason, "Reconciliation in progress") err = r.Client.Status().Update(ctx, &oidcClient) if err != nil { return } } config, err := internal.GetConfig(ctx, r.Client, oidcClient.Namespace) if err != nil { return } provider, err := config.GetKeycloak(ctx, r.Client) if err != nil { return } oidcClient.SetDefaults() // TODO this only works for new keycloak version that do not use the auth subpath // TODO use system user not master keycloakClient := gocloak.NewClient(provider.Endpoint, gocloak.SetAuthAdminRealms("admin/realms"), gocloak.SetAuthRealms("realms")) token, kcErr := keycloakClient.LoginAdmin(ctx, provider.Username, provider.Password, "master") if kcErr != nil { return } getClientsParams := gocloak.GetClientsParams{ ClientID: &oidcClient.Spec.ClientID, } var oidClientExists bool clientRepresentation := &gocloak.Client{} clientRepresentations, kcErr := keycloakClient.GetClients(ctx, token.AccessToken, oidcClient.Spec.Realm, getClientsParams) if kcErr != nil { return } // TODO manage it if returns multiple clients ? if len(clientRepresentations) == 1 { oidClientExists = true clientRepresentation = clientRepresentations[0] } internal.MutateOIDCClientRepresentation(oidcClient, clientRepresentation) if !oidClientExists { // Create Realm log.Info("creating oidc client") clientID, kcErr := keycloakClient.CreateClient(ctx, token.AccessToken, oidcClient.Spec.Realm, *clientRepresentation) if kcErr != nil { return } clientRepresentation.ID = &clientID } else { kcErr = keycloakClient.UpdateClient(ctx, token.AccessToken, oidcClient.Spec.Realm, *clientRepresentation) if kcErr != nil { return } } cred, kcErr := keycloakClient.GetClientSecret(ctx, token.AccessToken, oidcClient.Spec.Realm, *clientRepresentation.ID) if err != nil { return } secret := &corev1.Secret{} secret.Name = fmt.Sprintf("%s-%s-oidc", oidcClient.Name, oidcClient.Spec.Realm) secret.Namespace = oidcClient.Namespace _, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, secret, func() error { data := map[string][]byte{ "client-id": []byte(oidcClient.Spec.ClientID), "client-secret": []byte(*cred.Value), } secret.Data = data return controllerutil.SetOwnerReference(&oidcClient, secret, r.Scheme) }) if err != nil { panic(err) } conditions.Delete(&oidcClient, meta.ReconcilingCondition) return } // SetupWithManager sets up the controller with the Manager. func (r *OIDCClientReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&appsv1alpha1.OIDCClient{}). Complete(r) }