/* Copyright 2024. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "context" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "strings" "time" zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1" builder "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/builder" condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition" "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/configuration" configmap "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/configmap" secret "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/secret" "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/service" "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/deployment" "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/machinekey" "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/masterkey" systemapiaccount "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/systemapi" zitadelClient "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/zitadel" "github.com/hashicorp/go-multierror" "github.com/zitadel/zitadel-go/v2/pkg/client/system" adm "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/admin" authn "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/authn" pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/system" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/log" ) type reconcilePhase struct { Name string Reconcile func(context.Context, *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) } type patcher func(*zitadelv1alpha1.ZitadelClusterStatus) error // ZitadelClusterReconciler reconciles a ZitadelCluster object type ZitadelClusterReconciler struct { client.Client Scheme *runtime.Scheme ConditionReady *condition.Ready Builder *builder.Builder SecretReconciler *secret.SecretReconciler ConfigMapReconciler *configmap.ConfigMapReconciler ServiceReconciler *service.ServiceReconciler RefResolver *zitadelv1alpha1.RefResolver } // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;patch // +kubebuilder:rbac:groups="",resources=services,verbs=list;watch;create;patch // +kubebuilder:rbac:groups="",resources=secrets,verbs=list;watch;create;patch // +kubebuilder:rbac:groups="",resources=endpoints,verbs=create;patch;get;list;watch // +kubebuilder:rbac:groups="",resources=endpoints/restricted,verbs=create;patch;get;list;watch // +kubebuilder:rbac:groups="",resources=pods,verbs=get;delete // +kubebuilder:rbac:groups="",resources=events,verbs=list;watch;create;patch // +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=list;watch;create;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;create;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;create;patch // +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=list;watch;create;patch // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings;clusterrolebindings,verbs=list;watch;create;patch // +kubebuilder:rbac:groups=zitadel.topmanage.com,resources=zitadelclusters,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=zitadel.topmanage.com,resources=zitadelclusters/status,verbs=get;update;patch // +kubebuilder:rbac:groups=zitadel.topmanage.com,resources=zitadelclusters/finalizers,verbs=update // +kubebuilder:rbac:groups=crdb.cockroachlabs.com,resources=crdbclusters,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=crdb.cockroachlabs.com,resources=crdbclusters/status,verbs=get;update;patch // +kubebuilder:rbac:groups=crdb.cockroachlabs.com,resources=crdbclusters/finalizers,verbs=update // +kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests,verbs=get;list;watch;create;patch;delete // +kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests/status,verbs=get;update;patch // +kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests/approval,verbs=update // +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete func (r *ZitadelClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) logger.Info("Starting Reconcile") var zitadel zitadelv1alpha1.ZitadelCluster if err := r.Get(ctx, req.NamespacedName, &zitadel); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } phases := []reconcilePhase{ { Name: "Spec", Reconcile: r.setSpecDefaults, }, { Name: "Status", Reconcile: r.setStatusDefaults, }, { Name: "MasterkeySecret", Reconcile: r.reconcileMasterKeySecret, }, { Name: "ServiceAccount", Reconcile: r.reconcileSystemAPIUser, }, { Name: "Configuration", Reconcile: r.reconcileConfig, }, { Name: "InitJob", Reconcile: r.reconcileInitJob, }, { Name: "SetupJob", Reconcile: r.reconcileSetupJob, }, { Name: "Deployment", Reconcile: r.reconcileDeployment, }, { Name: "Service", Reconcile: r.reconcileService, }, { Name: "DefaultInstance", Reconcile: r.reconcileDefaultInstance, }, { Name: "SMTPConfig", Reconcile: r.reconcileSMTPConfig, }, { Name: "DomainPolicyConfig", Reconcile: r.reconcileDomainPolicy, }, } for _, p := range phases { result, err := p.Reconcile(ctx, &zitadel) if err != nil { if errors.IsNotFound(err) { continue } var errBundle *multierror.Error errBundle = multierror.Append(errBundle, err) msg := fmt.Sprintf("Error reconciling %s: %v", p.Name, err) patchErr := r.patchStatus(ctx, &zitadel, func(s *zitadelv1alpha1.ZitadelClusterStatus) error { patcher := r.ConditionReady.PatcherFailed(msg) patcher(s) return nil }) if errors.IsNotFound(patchErr) { errBundle = multierror.Append(errBundle, patchErr) } if err := errBundle.ErrorOrNil(); err != nil { return ctrl.Result{}, fmt.Errorf("error reconciling %s: %v", p.Name, err) } } if !result.IsZero() { return result, err } } if err := r.patchStatus(ctx, &zitadel, r.patcher(ctx, &zitadel)); err != nil && !errors.IsNotFound(err) { return ctrl.Result{}, err } return ctrl.Result{RequeueAfter: 2 * time.Minute}, nil } func (r *ZitadelClusterReconciler) setSpecDefaults(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { return ctrl.Result{}, r.patch(ctx, zitadel, func(zit *zitadelv1alpha1.ZitadelCluster) { zit.SetDefaults() }) } func (r *ZitadelClusterReconciler) setStatusDefaults(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { return ctrl.Result{}, r.patchStatus(ctx, zitadel, func(status *zitadelv1alpha1.ZitadelClusterStatus) error { status.FillWithDefaults(zitadel) return nil }) } func (r *ZitadelClusterReconciler) reconcileMasterKeySecret(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { secretName := masterkey.MasterKeyName(zitadel) key := types.NamespacedName{ Name: secretName, Namespace: zitadel.Namespace, } _, err := r.SecretReconciler.ReconcileRandomPassword(ctx, key, masterkey.Key, zitadel) if err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil } func (r *ZitadelClusterReconciler) reconcileSystemAPIUser(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { secretName := systemapiaccount.SystemAPIAccountName(zitadel) key := types.NamespacedName{ Name: secretName, Namespace: zitadel.Namespace, } _, err := r.SecretReconciler.ReconcileRandomPrivateRSA(ctx, key, systemapiaccount.Key, zitadel) if err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil } func (r *ZitadelClusterReconciler) reconcileConfig(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { crdb, err := r.RefResolver.CrdbClusterRef(ctx, &zitadel.Spec.CrdbClusterRef, zitadel.Namespace) if err != nil { return ctrl.Result{}, err } configName := configuration.ConfigurationName(zitadel) key := types.NamespacedName{ Name: configName, Namespace: zitadel.Namespace, } privateKeyData, err := r.RefResolver.SecretKeyRef(ctx, corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: systemapiaccount.SystemAPIAccountName(zitadel)}, Key: systemapiaccount.Key}, zitadel.Namespace) if err != nil { return ctrl.Result{}, err } pemBlock, _ := pem.Decode([]byte(privateKeyData)) if pemBlock == nil { return ctrl.Result{}, fmt.Errorf("failed to decode PEM block") } privateKey, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes) publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) if err != nil { return ctrl.Result{}, err } publicKeyPem := pem.EncodeToMemory( &pem.Block{ Type: "RSA PUBLIC KEY", Bytes: publicKeyBytes, }, ) base64key := base64.StdEncoding.EncodeToString(publicKeyPem) err = r.ConfigMapReconciler.ReconcileZitadelConfiguration(ctx, key, zitadel, crdb, base64key) if err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil } func (r *ZitadelClusterReconciler) reconcileInitJob(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { key := client.ObjectKeyFromObject(zitadel) key.Name = "init-job-" + key.Name desiredInitJob, err := r.Builder.BuildInitJob(zitadel, key) if err != nil { return ctrl.Result{}, fmt.Errorf("error building InitJob: %v", err) } var existingJob batchv1.Job if err := r.Get(ctx, key, &existingJob); err != nil { if !errors.IsNotFound(err) { return ctrl.Result{}, fmt.Errorf("error getting InitJob: %v", err) } if err := r.Create(ctx, desiredInitJob); err != nil { return ctrl.Result{}, fmt.Errorf("error creating InitJob: %v", err) } return ctrl.Result{}, nil } patch := client.MergeFrom(existingJob.DeepCopy()) existingJob.Spec.Template.Spec = desiredInitJob.Spec.Template.Spec return ctrl.Result{}, r.Patch(ctx, &existingJob, patch) } func (r *ZitadelClusterReconciler) reconcileSetupJob(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { key := client.ObjectKeyFromObject(zitadel) key.Name = "setup-job-" + key.Name desiredSetupjob, err := r.Builder.BuildSetupJob(zitadel, key) if err != nil { return ctrl.Result{}, fmt.Errorf("error building Setupjob: %v", err) } var existingJob batchv1.Job if err := r.Get(ctx, key, &existingJob); err != nil { if !errors.IsNotFound(err) { return ctrl.Result{}, fmt.Errorf("error getting Setupjob: %v", err) } if err := r.Create(ctx, desiredSetupjob); err != nil { return ctrl.Result{}, fmt.Errorf("error creating Setupjob: %v", err) } return ctrl.Result{}, nil } patch := client.MergeFrom(existingJob.DeepCopy()) existingJob.Spec.Template.Spec = desiredSetupjob.Spec.Template.Spec return ctrl.Result{}, r.Patch(ctx, &existingJob, patch) } func (r *ZitadelClusterReconciler) reconcileDeployment(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { // TODO: Reload on config changed key := client.ObjectKeyFromObject(zitadel) desiredSts, err := r.Builder.BuildDeployment(zitadel, key) if err != nil { return ctrl.Result{}, fmt.Errorf("error building Deployment: %v", err) } var existingDep appsv1.Deployment if err := r.Get(ctx, key, &existingDep); err != nil { if !errors.IsNotFound(err) { return ctrl.Result{}, fmt.Errorf("error getting Deployment: %v", err) } if err := r.Create(ctx, desiredSts); err != nil { return ctrl.Result{}, fmt.Errorf("error creating Deployment: %v", err) } return ctrl.Result{}, nil } patch := client.MergeFrom(existingDep.DeepCopy()) existingDep.Spec.Template = desiredSts.Spec.Template existingDep.Spec.Replicas = desiredSts.Spec.Replicas return ctrl.Result{}, r.Patch(ctx, &existingDep, patch) } func (r *ZitadelClusterReconciler) reconcileService(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { return ctrl.Result{}, r.reconcileDefaultService(ctx, zitadel) } func (r *ZitadelClusterReconciler) reconcileDefaultService(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) error { key := client.ObjectKeyFromObject(zitadel) opts := builder.ServiceOpts{ Ports: []corev1.ServicePort{ { Name: deployment.ZitadelName, Port: deployment.ZitadelPort, }, }, } desiredSvc, err := r.Builder.BuildService(zitadel, key, opts) if err != nil { return fmt.Errorf("error building Service: %v", err) } return r.ServiceReconciler.Reconcile(ctx, desiredSvc) } func (r *ZitadelClusterReconciler) reconcileDefaultInstance(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { // First create systemapi to get, delete and create instances privateKeyData, err := r.RefResolver.SecretKeyRef(ctx, corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: systemapiaccount.SystemAPIAccountName(zitadel)}, Key: systemapiaccount.Key}, zitadel.Namespace) if err != nil { return ctrl.Result{}, err } ztdClient, err := system.NewClient(GetIssuer(zitadel), GetAPI(zitadel), system.JWTProfileFromKey([]byte(privateKeyData), masterkey.OwnerName), system.WithInsecure()) if err != nil { return ctrl.Result{}, fmt.Errorf("Error creating sytem client: %v", err) } defer ztdClient.Connection.Close() // Delete all Instances that isn't the default { resp, err := ztdClient.ListInstances(ctx, &pb.ListInstancesRequest{}) if err != nil { return ctrl.Result{}, fmt.Errorf("Error listing instances: %v", err) } for _, instance := range resp.Result { if instance.Id != zitadel.Status.DefaultInstanceId || instance.Id == "" { fmt.Println("DELETING INSTANCE") _, err := ztdClient.RemoveInstance(ctx, &pb.RemoveInstanceRequest{InstanceId: instance.Id}) if err != nil { return ctrl.Result{}, err } } } } // Check if instance already exists _, err = ztdClient.GetInstance(ctx, &pb.GetInstanceRequest{InstanceId: zitadel.Status.DefaultInstanceId}) if err != nil { if strings.Contains(err.Error(), "Instance not found") { // if Instance doesn't exist, then create and assign secrets resp, err := ztdClient.CreateInstance(ctx, &pb.CreateInstanceRequest{ InstanceName: zitadel.Spec.FirstOrgName, FirstOrgName: zitadel.Spec.FirstOrgName, CustomDomain: zitadel.Spec.Host, Owner: &pb.CreateInstanceRequest_Machine_{Machine: &pb.CreateInstanceRequest_Machine{ Name: "k8s-operator", UserName: "k8s-operator", MachineKey: &pb.CreateInstanceRequest_MachineKey{ Type: authn.KeyType_KEY_TYPE_JSON}, PersonalAccessToken: &pb.CreateInstanceRequest_PersonalAccessToken{}}, }}) if err != nil { return ctrl.Result{}, fmt.Errorf("Error creating default instance: %v", err) } var machineKeyData zitadelClient.MachineKey if err := json.Unmarshal(resp.MachineKey, &machineKeyData); err != nil { return ctrl.Result{}, err } secretName := machinekey.MachineKeySecretName(zitadel) key := types.NamespacedName{ Name: secretName, Namespace: zitadel.Namespace, } secretData := make(map[string][]byte) jsonData, err := json.Marshal(machineKeyData) if err != nil { return ctrl.Result{}, err } secretData[machinekey.Key] = jsonData secret, err := r.Builder.BuildSecret(builder.SecretOpts{Zitadel: zitadel, Key: key, Data: secretData}, zitadel) if err != nil { return ctrl.Result{}, fmt.Errorf("error building machinekey Secret: %v", err) } if err := r.Create(ctx, secret); err != nil { return ctrl.Result{}, fmt.Errorf("error creating machinekey Secret: %v", err) } patch := client.MergeFrom(zitadel.DeepCopy()) zitadel.Status.DefaultInstanceId = resp.InstanceId return ctrl.Result{}, r.Status().Patch(ctx, zitadel, patch) } else { return ctrl.Result{}, fmt.Errorf("Error getting instance with id: %s: %v", zitadel.Status.DefaultInstanceId, err) } } return ctrl.Result{}, nil } func (r *ZitadelClusterReconciler) reconcileSMTPConfig(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { adminClient, err := zitadelClient.NewAdminClient(ctx, zitadel, *r.RefResolver) if err != nil { return ctrl.Result{}, err } resp, err := adminClient.GetSMTPConfig(ctx, &adm.GetSMTPConfigRequest{}) if err != nil { if !strings.Contains(err.Error(), "SMTP configuration not found") { return ctrl.Result{}, fmt.Errorf("Error getting SMTP config: %v", err) } } if resp != nil { adminRequest := &adm.UpdateSMTPConfigRequest{ SenderAddress: zitadel.Spec.SMTPConfig.SenderAddress, SenderName: zitadel.Spec.SMTPConfig.SenderName, Tls: zitadel.Spec.SMTPConfig.TLS, Host: zitadel.Spec.SMTPConfig.Host, } if zitadel.Spec.SMTPConfig.User != nil && zitadel.Spec.SMTPConfig.Password != nil { adminRequest.User = *zitadel.Spec.SMTPConfig.User } if zitadel.Spec.SMTPConfig.ReplyToAddress != nil { adminRequest.ReplyToAddress = *zitadel.Spec.SMTPConfig.ReplyToAddress } passwordSecret, err := r.RefResolver.SecretKeyRef(ctx, zitadel.Spec.SMTPConfig.Password.SecretKeyRef, zitadel.Namespace) if err != nil { return ctrl.Result{}, err } if _, err = adminClient.UpdateSMTPConfig(ctx, adminRequest); err != nil { if !strings.Contains(err.Error(), "No changes") { return ctrl.Result{}, fmt.Errorf("Could not update SMTP config: %v", err) } } if zitadel.Spec.SMTPConfig.Password != nil { if _, err = adminClient.UpdateSMTPConfigPassword(ctx, &adm.UpdateSMTPConfigPasswordRequest{ Password: passwordSecret, }); err != nil { if !strings.Contains(err.Error(), "No changes") { return ctrl.Result{}, fmt.Errorf("Could not update SMTP config: %v", err) } } } } else { adminRequest := &adm.AddSMTPConfigRequest{ SenderAddress: zitadel.Spec.SMTPConfig.SenderAddress, SenderName: zitadel.Spec.SMTPConfig.SenderName, Tls: zitadel.Spec.SMTPConfig.TLS, Host: zitadel.Spec.SMTPConfig.Host, } if zitadel.Spec.SMTPConfig.User != nil && zitadel.Spec.SMTPConfig.Password != nil { passwordSecret, err := r.RefResolver.SecretKeyRef(ctx, zitadel.Spec.SMTPConfig.Password.SecretKeyRef, zitadel.Namespace) if err != nil { return ctrl.Result{}, err } adminRequest.Password = passwordSecret adminRequest.User = *zitadel.Spec.SMTPConfig.User } if zitadel.Spec.SMTPConfig.ReplyToAddress != nil { adminRequest.ReplyToAddress = *zitadel.Spec.SMTPConfig.ReplyToAddress } if _, err = adminClient.AddSMTPConfig(ctx, adminRequest); err != nil { return ctrl.Result{}, fmt.Errorf("Could not add SMTP config: %v", err) } } return ctrl.Result{}, nil } func (r *ZitadelClusterReconciler) reconcileDomainPolicy(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { adminClient, err := zitadelClient.NewAdminClient(ctx, zitadel, *r.RefResolver) if err != nil { return ctrl.Result{}, err } if _, err = adminClient.UpdateDomainPolicy(ctx, &adm.UpdateDomainPolicyRequest{ UserLoginMustBeDomain: zitadel.Spec.DomainSettings.UserLoginMustBeDomain, ValidateOrgDomains: zitadel.Spec.DomainSettings.ValidateOrgDomains, SmtpSenderAddressMatchesInstanceDomain: zitadel.Spec.DomainSettings.SMTPSenderAddressMatchesInstanceDomain, }); err != nil { if !strings.Contains(err.Error(), "not been changed") { return ctrl.Result{}, fmt.Errorf("Could not update domain policy config: %v", err) } } return ctrl.Result{}, nil } func GetIssuer(zitadel *zitadelv1alpha1.ZitadelCluster) string { scheme := "http" if zitadel.Spec.ExternalSecure { scheme = "https" } if zitadel.Spec.ExternalPort == 443 { return fmt.Sprintf("%s://%s", scheme, zitadel.Spec.Host) } return fmt.Sprintf("%s://%s:%d", scheme, zitadel.Spec.Host, zitadel.Spec.ExternalPort) } func GetAPI(zitadel *zitadelv1alpha1.ZitadelCluster) string { return fmt.Sprintf("%s:%d", deployment.ServiceFQDN(zitadel.ObjectMeta), deployment.ZitadelPort) } func (r *ZitadelClusterReconciler) patchStatus(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster, patcher patcher) error { patch := client.MergeFrom(zitadel.DeepCopy()) if err := patcher(&zitadel.Status); err != nil { return err } return r.Status().Patch(ctx, zitadel, patch) } func (r *ZitadelClusterReconciler) patcher(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) patcher { return func(s *zitadelv1alpha1.ZitadelClusterStatus) error { var sts appsv1.Deployment if err := r.Get(ctx, client.ObjectKeyFromObject(zitadel), &sts); err != nil { return err } zitadel.Status.Replicas = sts.Status.ReadyReplicas condition.SetReadyWithDeployment(&zitadel.Status, &sts, zitadel.Status.DefaultInstanceId) return nil } } func (r *ZitadelClusterReconciler) patch(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster, patcher func(*zitadelv1alpha1.ZitadelCluster)) error { patch := client.MergeFrom(zitadel.DeepCopy()) patcher(zitadel) return r.Patch(ctx, zitadel, patch) } // SetupWithManager sets up the controller with the Manager. func (r *ZitadelClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&zitadelv1alpha1.ZitadelCluster{}). Owns(&appsv1.Deployment{}). Owns(&corev1.Service{}). Owns(&corev1.ConfigMap{}). Owns(&corev1.Secret{}). Owns(&zitadelv1alpha1.Organization{}). WithOptions(controller.Options{RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(time.Millisecond*500, time.Minute*3)}). Complete(r) }