All checks were successful
Build and Publish / build-release (push) Successful in 15m41s
403 lines
15 KiB
Go
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)
|
|
}
|