Files
zitadel-k8s-operator/internal/controller/instance_controller.go
HaimKortovich da5d944430
Some checks failed
Build and Publish / build-release (push) Failing after 26s
divide operators
2026-04-07 13:41:25 -05:00

403 lines
15 KiB
Go

/*
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)
}