This commit is contained in:
402
internal/controller/instance_controller.go
Normal file
402
internal/controller/instance_controller.go
Normal file
@@ -0,0 +1,402 @@
|
||||
/*
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user