686 lines
26 KiB
Go
686 lines
26 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"
|
|
"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/v3/pkg/client/system"
|
|
adm "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/admin"
|
|
authn "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/authn"
|
|
object "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/object"
|
|
pb "github.com/zitadel/zitadel-go/v3/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: 15 * 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
|
|
|
|
// Build the desired InitJob
|
|
desiredInitJob, err := r.Builder.BuildInitJob(zitadel, key)
|
|
if err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("error building InitJob: %v", err)
|
|
}
|
|
|
|
var existingJob batchv1.Job
|
|
err = r.Get(ctx, key, &existingJob)
|
|
if err != nil {
|
|
if !errors.IsNotFound(err) {
|
|
return ctrl.Result{}, fmt.Errorf("error getting InitJob: %v", err)
|
|
}
|
|
// If job not found, create the InitJob
|
|
if err := r.Create(ctx, desiredInitJob); err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("error creating InitJob: %v", err)
|
|
}
|
|
return ctrl.Result{}, nil
|
|
}
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
func (r *ZitadelClusterReconciler) reconcileSetupJob(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
|
|
key := client.ObjectKeyFromObject(zitadel)
|
|
key.Name = "setup-job-" + key.Name
|
|
|
|
// Build the desired job
|
|
desiredSetupJob, err := r.Builder.BuildSetupJob(zitadel, key)
|
|
if err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("error building SetupJob: %v", err)
|
|
}
|
|
|
|
var existingJob batchv1.Job
|
|
err = r.Get(ctx, key, &existingJob)
|
|
if err != nil {
|
|
if !errors.IsNotFound(err) {
|
|
return ctrl.Result{}, fmt.Errorf("error getting SetupJob: %v", err)
|
|
}
|
|
// If job is not found, create the job
|
|
if err := r.Create(ctx, desiredSetupJob); err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("error creating SetupJob: %v", err)
|
|
}
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
// Compare the image in the existing job with the desired image
|
|
existingImage := existingJob.Spec.Template.Spec.Containers[0].Image
|
|
desiredImage := desiredSetupJob.Spec.Template.Spec.Containers[0].Image
|
|
|
|
// If the images don't match, delete the existing job and wait for deletion
|
|
if existingImage != desiredImage {
|
|
|
|
if err := r.Delete(ctx, &existingJob); err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("error deleting existing SetupJob: %v", err)
|
|
}
|
|
|
|
// Wait for the job to be fully deleted before creating a new one
|
|
for {
|
|
err := r.Get(ctx, key, &existingJob)
|
|
if errors.IsNotFound(err) {
|
|
break // Job has been deleted, we can proceed
|
|
}
|
|
if err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("error checking if SetupJob is deleted: %v", err)
|
|
}
|
|
// Sleep for a short interval to avoid tight loop
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
|
|
// Now create the new job
|
|
if err := r.Create(ctx, desiredSetupJob); err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("error creating new SetupJob: %v", err)
|
|
}
|
|
}
|
|
if err := r.Get(ctx, key, &existingJob); err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("error fetching existing SetupJob status: %v", err)
|
|
}
|
|
|
|
if existingJob.Status.Succeeded != 1 { // Replace with actual success condition
|
|
return ctrl.Result{}, fmt.Errorf("SetupJob is not successful, current status: %v", existingJob.Status)
|
|
}
|
|
// If the job exists and the image matches, no action is needed
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
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(ctx, 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{
|
|
Query: &object.ListQuery{
|
|
Offset: uint64(0),
|
|
Limit: uint32(100),
|
|
Asc: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("Error listing instances: %v", err)
|
|
}
|
|
|
|
for _, instance := range resp.Result {
|
|
if instance != nil {
|
|
if instance.Id != zitadel.Status.DefaultInstanceId || zitadel.Status.DefaultInstanceId == "" {
|
|
for _, domain := range instance.Domains {
|
|
if domain != nil {
|
|
if !domain.Generated {
|
|
if _, err := ztdClient.RemoveDomain(ctx, &pb.RemoveDomainRequest{
|
|
InstanceId: instance.Id,
|
|
Domain: domain.Domain,
|
|
}); err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("Error removing domain in instance: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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) {
|
|
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(ctx, 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()
|
|
|
|
_, err = ztdClient.AddDomain(ctx, &pb.AddDomainRequest{
|
|
Domain: strings.Split(zitadel.Spec.SMTPConfig.SenderAddress, "@")[1],
|
|
InstanceId: zitadel.Status.DefaultInstanceId,
|
|
})
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "AlreadyExists") {
|
|
return ctrl.Result{}, fmt.Errorf("Could add smtp trusted domain: %v", err)
|
|
}
|
|
}
|
|
|
|
adminClient, err := zitadelClient.NewAdminClient(ctx, zitadel, *r.RefResolver)
|
|
if err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
defer adminClient.Connection.Close()
|
|
|
|
resp, err := adminClient.GetEmailProvider(ctx, &adm.GetEmailProviderRequest{})
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "not found") {
|
|
adminRequest := &adm.AddEmailProviderSMTPRequest{
|
|
SenderAddress: zitadel.Spec.SMTPConfig.SenderAddress,
|
|
SenderName: zitadel.Spec.SMTPConfig.SenderName,
|
|
Tls: zitadel.Spec.SMTPConfig.TLS,
|
|
Host: zitadel.Spec.SMTPConfig.Host,
|
|
Description: "autogenerated by k8s-operator",
|
|
Password: "test",
|
|
}
|
|
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
|
|
}
|
|
|
|
addRes, err := adminClient.AddEmailProviderSMTP(ctx, adminRequest)
|
|
if err != nil {
|
|
return ctrl.Result{}, fmt.Errorf("Could not add SMTP config: %v", err)
|
|
}
|
|
if _, err := adminClient.ActivateEmailProvider(ctx, &adm.ActivateEmailProviderRequest{
|
|
Id: addRes.Id,
|
|
}); err != nil {
|
|
if !strings.Contains(err.Error(), "AlreadyActive") {
|
|
return ctrl.Result{}, fmt.Errorf("Error activating SMTP config: %v", err)
|
|
}
|
|
}
|
|
} else {
|
|
return ctrl.Result{}, fmt.Errorf("Error getting SMTP config: %v", err)
|
|
}
|
|
} else {
|
|
if zitadel.Spec.SMTPConfig.SenderAddress != resp.Config.GetSmtp().GetSenderAddress() || zitadel.Spec.SMTPConfig.SenderName != resp.Config.GetSmtp().SenderName || zitadel.Spec.SMTPConfig.TLS != resp.Config.GetSmtp().Tls || zitadel.Spec.SMTPConfig.Host != resp.Config.GetSmtp().Host {
|
|
adminRequest := &adm.UpdateEmailProviderSMTPRequest{
|
|
SenderAddress: zitadel.Spec.SMTPConfig.SenderAddress,
|
|
SenderName: zitadel.Spec.SMTPConfig.SenderName,
|
|
Tls: zitadel.Spec.SMTPConfig.TLS,
|
|
Host: zitadel.Spec.SMTPConfig.Host,
|
|
Id: resp.Config.Id,
|
|
Password: "test",
|
|
Description: "autogenerated by k8s-operator",
|
|
}
|
|
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.UpdateEmailProviderSMTP(ctx, adminRequest); err != nil {
|
|
if !strings.Contains(err.Error(), "No changes") {
|
|
return ctrl.Result{}, fmt.Errorf("Could not update SMTP config: %v", err)
|
|
}
|
|
}
|
|
|
|
// patch := client.MergeFrom(zitadel.DeepCopy())
|
|
// return ctrl.Result{}, r.Status().Patch(ctx, zitadel, patch)
|
|
}
|
|
}
|
|
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)
|
|
}
|