/* 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" "fmt" "time" zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-k8s-operator/api/v1alpha1" condition "gitea.corredorconect.com/software-engineering/zitadel-k8s-operator/pkg/condition" "gitea.corredorconect.com/software-engineering/zitadel-k8s-operator/pkg/controller/service" "gitea.corredorconect.com/software-engineering/zitadel-k8s-operator/pkg/controller/system" "gitea.corredorconect.com/software-engineering/zitadel-k8s-operator/pkg/deployment" zitadelresourcesv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1" corev1 "k8s.io/api/core/v1" ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" systemClient "github.com/zitadel/zitadel-go/v3/pkg/client/system" authn "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/authn" pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/system" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/errors" "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/reconcile" "gitea.corredorconect.com/software-engineering/zitadel-k8s-operator/pkg/builder" ) // InstanceReconciler reconciles a Instance object type InstanceReconciler struct { client.Client RefResolver *zitadelv1alpha1.RefResolver ConditionReady *condition.Ready RequeueInterval time.Duration Builder *builder.Builder ServiceReconciler *service.ServiceReconciler } func NewInstanceReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, serviceReconciler *service.ServiceReconciler, requeueInterval time.Duration) *InstanceReconciler { return &InstanceReconciler{ Client: client, RefResolver: refResolver, ConditionReady: conditionReady, RequeueInterval: requeueInterval, ServiceReconciler: serviceReconciler, Builder: builder, } } //+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=instances,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=instances/status,verbs=get;update;patch //+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=instances/finalizers,verbs=update // +kubebuilder:rbac:groups=zitadel.github.com,resources=machineusers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=zitadel.github.com,resources=machineusers/status,verbs=get;update;patch // +kubebuilder:rbac:groups=zitadel.github.com,resources=machineusers/finalizers,verbs=update // +kubebuilder:rbac:groups=zitadel.github.com,resources=connections,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=zitadel.github.com,resources=connections/status,verbs=get;update;patch // +kubebuilder:rbac:groups=zitadel.github.com,resources=connections/finalizers,verbs=update // +kubebuilder:rbac:groups=zitadel.github.com,resources=organizations,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=zitadel.github.com,resources=organizations/status,verbs=get;update;patch // +kubebuilder:rbac:groups=zitadel.github.com,resources=organizations/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. func (r *InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var instance zitadelv1alpha1.Instance if err := r.Get(ctx, req.NamespacedName, &instance); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } wr := newWrappedInstanceReconciler(r.Client, r.RefResolver, r.Builder, r.ServiceReconciler, &instance) wf := newWrappedInstanceFinalizer(r.Client, &instance) tf := system.NewSystemFinalizer(r.Client, wf) tr := system.NewSystemReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) result, err := tr.Reconcile(ctx, &instance) if err != nil { return result, fmt.Errorf("error reconciling in InstanceReconciler: %v", err) } return result, nil } type wrappedInstanceReconciler struct { client.Client refResolver *zitadelv1alpha1.RefResolver instance *zitadelv1alpha1.Instance Builder *builder.Builder ServiceReconciler *service.ServiceReconciler } func newWrappedInstanceReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, serviceReconciler *service.ServiceReconciler, instance *zitadelv1alpha1.Instance) system.WrappedSystemReconciler { return &wrappedInstanceReconciler{ Client: client, refResolver: refResolver, instance: instance, Builder: builder, ServiceReconciler: serviceReconciler, } } type instanceReconcilePhase struct { Name string Reconcile func(context.Context, *systemClient.Client) error } func (wr *wrappedInstanceReconciler) Reconcile(ctx context.Context, ztdClient *systemClient.Client) error { phases := []instanceReconcilePhase{ { Name: "instance", Reconcile: wr.reconcileInstance, }, { Name: "connection", Reconcile: wr.reconcileConnection, }, { Name: "organization", Reconcile: wr.reconcileFirstOrganization, }, { Name: "loginUIMachineUser", Reconcile: wr.reconcileLoginUIMachineUser, }, { Name: "loginUIDeployment", Reconcile: wr.reconcileLoginUIDeployment, }, { Name: "loginUIService", Reconcile: wr.reconcileLoginUIService, }, } for _, p := range phases { err := p.Reconcile(ctx, ztdClient) if err != nil { return err } } return nil } func (wr *wrappedInstanceReconciler) reconcileInstance(ctx context.Context, ztdClient *systemClient.Client) error { var instanceId *string if wr.instance.Status.InstanceId != nil { getInstanceRes, err := ztdClient.GetInstance(ctx, &pb.GetInstanceRequest{InstanceId: *wr.instance.Status.InstanceId}) if err != nil { return fmt.Errorf("Error getting Instance: %v", err) } if getInstanceRes.Instance != nil { instanceId = &getInstanceRes.Instance.Id } } if instanceId == nil { createInstanceRes, err := ztdClient.CreateInstance(ctx, &pb.CreateInstanceRequest{ InstanceName: wr.instance.Spec.InstanceName, FirstOrgName: wr.instance.Spec.Org.Name, CustomDomain: wr.instance.Spec.CustomDomain, DefaultLanguage: wr.instance.Spec.DefaultLanguage, Owner: &pb.CreateInstanceRequest_Machine_{ Machine: &pb.CreateInstanceRequest_Machine{ UserName: wr.instance.MachineUserName(), Name: wr.instance.MachineName(), PersonalAccessToken: &pb.CreateInstanceRequest_PersonalAccessToken{ ExpirationDate: nil, }, MachineKey: &pb.CreateInstanceRequest_MachineKey{ ExpirationDate: nil, Type: authn.KeyType_KEY_TYPE_JSON, }, }, }, }) if err != nil { return fmt.Errorf("Error creating Instance: %v", err) } key := types.NamespacedName{ Name: wr.instance.MachineSecretName(), Namespace: wr.instance.Namespace, } secretData := map[string][]byte{ "pat": []byte(createInstanceRes.Pat), "machinekey": createInstanceRes.MachineKey, } secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Key: key, Data: secretData}, wr.instance) if err != nil { return fmt.Errorf("error building instance machine Secret: %v", err) } if err := wr.Create(ctx, secret); err != nil { return fmt.Errorf("error creating machinekey Secret: %v", err) } instanceId = &createInstanceRes.InstanceId } patch := ctrlClient.MergeFrom(wr.instance.DeepCopy()) wr.instance.Status.InstanceId = instanceId return wr.Client.Status().Patch(ctx, wr.instance, patch) } func (wr *wrappedInstanceReconciler) reconcileConnection(ctx context.Context, ztdClient *systemClient.Client) error { key := types.NamespacedName{ Name: wr.instance.ConnectionObjectName(), Namespace: wr.instance.Namespace, } desiredConnection, err := wr.Builder.BuildConnection(key, wr.instance) if err != nil { return fmt.Errorf("error building Initial Connectionanization: %v", err) } var existingConnection zitadelresourcesv1alpha1.Connection if err := wr.Get(ctx, key, &existingConnection); err != nil { if !errors.IsNotFound(err) { return fmt.Errorf("error getting Initial Connectionanization: %v", err) } if err := wr.Create(ctx, desiredConnection); err != nil { return fmt.Errorf("error creating Initial Connectionanization: %v", err) } return nil } patch := client.MergeFrom(existingConnection.DeepCopy()) existingConnection.Spec.Host = desiredConnection.Spec.Host existingConnection.Spec.Authentication = desiredConnection.Spec.Authentication return wr.Patch(ctx, &existingConnection, patch) } func (wr *wrappedInstanceReconciler) reconcileFirstOrganization(ctx context.Context, ztdClient *systemClient.Client) error { key := types.NamespacedName{ Name: wr.instance.FirstOrgObjectName(), Namespace: wr.instance.Namespace, } desiredOrg, err := wr.Builder.BuildOrganization(builder.OrganizationOpts{Key: key, Zitadel: wr.instance, OrganizationName: wr.instance.Spec.Org.Name}, wr.instance) if err != nil { return fmt.Errorf("error building Initial Organization: %v", err) } var existingOrg zitadelresourcesv1alpha1.Organization if err := wr.Get(ctx, key, &existingOrg); err != nil { if !errors.IsNotFound(err) { return fmt.Errorf("error getting Initial Organization: %v", err) } if err := wr.Create(ctx, desiredOrg); err != nil { return fmt.Errorf("error creating Initial Organization: %v", err) } return nil } patch := client.MergeFrom(existingOrg.DeepCopy()) existingOrg.Spec.OrganzationName = desiredOrg.Spec.OrganzationName return wr.Patch(ctx, &existingOrg, patch) } func (wr *wrappedInstanceReconciler) reconcileLoginUIMachineUser(ctx context.Context, ztdClient *systemClient.Client) error { key := types.NamespacedName{ Name: wr.instance.LoginMachineUserName(), Namespace: wr.instance.Namespace, } desiredMachineUser, err := wr.Builder.BuildMachineUser(key, builder.MachineUserOpts{Instance: wr.instance, InternalPermissions: []zitadelresourcesv1alpha1.InternalPermissions{ { Resource: zitadelresourcesv1alpha1.Resource{ Instance: &zitadelresourcesv1alpha1.InstanceResource{}, }, Roles: []string{ "IAM_LOGIN_CLIENT", }, }, }, Username: "login-ui", }, wr.instance) if err != nil { return fmt.Errorf("error building LoginUI MachineUser: %v", err) } var existingMachineUser zitadelresourcesv1alpha1.MachineUser if err := wr.Get(ctx, key, &existingMachineUser); err != nil { if !errors.IsNotFound(err) { return fmt.Errorf("error getting MachineUser: %v", err) } if err := wr.Create(ctx, desiredMachineUser); err != nil { return fmt.Errorf("error creating MachineUser: %v", err) } return nil } patch := client.MergeFrom(existingMachineUser.DeepCopy()) existingMachineUser.Spec.Authorizations = desiredMachineUser.Spec.Authorizations existingMachineUser.Spec.InternalPermissions = desiredMachineUser.Spec.InternalPermissions existingMachineUser.Spec.Metadata = desiredMachineUser.Spec.Metadata return wr.Patch(ctx, &existingMachineUser, patch) } func (wr *wrappedInstanceReconciler) reconcileLoginUIDeployment(ctx context.Context, ztdClient *systemClient.Client) error { if wr.instance.Status.InstanceId == nil { return fmt.Errorf("Instance not ready...") } cluster, err := wr.refResolver.Cluster(ctx, &wr.instance.Spec.ClusterRef, wr.instance.Namespace) if err != nil { return err } key := client.ObjectKeyFromObject(wr.instance) key.Name = key.Name + "-login-ui" instanceRes, err := ztdClient.GetInstance(ctx, &pb.GetInstanceRequest{InstanceId: *wr.instance.Status.InstanceId}) if err != nil { return err } var customDomain string for _, d := range instanceRes.Instance.Domains { if d.Primary { customDomain = d.Domain break } } desiredSts, err := wr.Builder.BuildLoginDeployment(cluster, wr.instance, customDomain, key) if err != nil { return fmt.Errorf("error building Login UI Deployment: %v", err) } var existingDep appsv1.Deployment if err := wr.Get(ctx, key, &existingDep); err != nil { if !errors.IsNotFound(err) { return fmt.Errorf("error getting Login UI Deployment: %v", err) } if err := wr.Create(ctx, desiredSts); err != nil { return fmt.Errorf("error creating Login UI Deployment: %v", err) } return nil } patch := client.MergeFrom(existingDep.DeepCopy()) existingDep.Spec.Template = desiredSts.Spec.Template existingDep.Spec.Replicas = desiredSts.Spec.Replicas return wr.Patch(ctx, &existingDep, patch) } func (wr *wrappedInstanceReconciler) reconcileLoginUIService(ctx context.Context, ztdClient *systemClient.Client) error { key := client.ObjectKeyFromObject(wr.instance) key.Name = key.Name + "-login-ui" opts := builder.ServiceOpts{ Ports: []corev1.ServicePort{ { Name: deployment.LoginName, Port: deployment.LoginPort, }, }, } desiredSvc, err := wr.Builder.BuildLoginService(wr.instance, key, opts) if err != nil { return fmt.Errorf("error building Service: %v", err) } return wr.ServiceReconciler.Reconcile(ctx, desiredSvc) } func (wr *wrappedInstanceReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { patch := client.MergeFrom(wr.instance.DeepCopy()) patcher(&wr.instance.Status) if err := wr.Client.Status().Patch(ctx, wr.instance, patch); err != nil { return fmt.Errorf("error patching Instance status: %v", err) } return nil } // SetupWithManager sets up the controller with the Manager. func (r *InstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&zitadelv1alpha1.Instance{}). Owns(&corev1.Secret{}). Owns(&appsv1.Deployment{}). Owns(&corev1.Service{}). Owns(&zitadelresourcesv1alpha1.Connection{}). Owns(&zitadelresourcesv1alpha1.Organization{}). Owns(&zitadelresourcesv1alpha1.MachineUser{}). WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). Complete(r) }