Init commit

This commit is contained in:
2026-03-25 16:44:42 -05:00
commit 25c940cfd3
101 changed files with 9907 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
package builder
import (
"k8s.io/apimachinery/pkg/runtime"
)
type Builder struct {
scheme *runtime.Scheme
}
func NewBuilder(scheme *runtime.Scheme) *Builder {
return &Builder{
scheme: scheme,
}
}

View File

@@ -0,0 +1,21 @@
package builder
type LabelsBuilder struct {
labels map[string]string
}
func NewLabelsBuilder() *LabelsBuilder {
return &LabelsBuilder{
labels: map[string]string{},
}
}
func (b *LabelsBuilder) WithLabels(labels map[string]string) *LabelsBuilder {
for k, v := range labels {
b.labels[k] = v
}
return b
}
func (b *LabelsBuilder) Build() map[string]string {
return b.labels
}

View File

@@ -0,0 +1,39 @@
package metadata
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
type MetadataBuilder struct {
objMeta metav1.ObjectMeta
}
func NewMetadataBuilder(key types.NamespacedName) *MetadataBuilder {
return &MetadataBuilder{
objMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
Labels: map[string]string{},
Annotations: map[string]string{},
},
}
}
func (b *MetadataBuilder) WithLabels(labels map[string]string) *MetadataBuilder {
for k, v := range labels {
b.objMeta.Labels[k] = v
}
return b
}
func (b *MetadataBuilder) WithAnnotations(annotations map[string]string) *MetadataBuilder {
for k, v := range annotations {
b.objMeta.Annotations[k] = v
}
return b
}
func (b *MetadataBuilder) Build() metav1.ObjectMeta {
return b.objMeta
}

View File

@@ -0,0 +1,35 @@
package builder
import (
"fmt"
metadata "github.com/HaimKortovich/zitadel-resources-operator/pkg/builder/metadata"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
type SecretOpts struct {
Key types.NamespacedName
Data map[string][]byte
Labels map[string]string
Annotations map[string]string
Immutable bool
}
func (b *Builder) BuildSecret(opts SecretOpts, owner metav1.Object) (*corev1.Secret, error) {
objMeta :=
metadata.NewMetadataBuilder(opts.Key).
WithLabels(opts.Labels).
WithAnnotations(opts.Annotations).
Build()
secret := &corev1.Secret{
ObjectMeta: objMeta,
Data: opts.Data,
Immutable: &opts.Immutable,
}
if err := controllerutil.SetControllerReference(owner, secret, b.scheme); err != nil {
return nil, fmt.Errorf("error setting controller reference in Secret manifest: %v", err)
}
return secret, nil
}

View File

@@ -0,0 +1,68 @@
package conditions
import (
"fmt"
"reflect"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Conditioner interface {
SetCondition(condition metav1.Condition)
}
type Patcher func(Conditioner)
type Ready struct{}
func NewReady() *Ready {
return &Ready{}
}
func (p *Ready) PatcherFailed(msg string) Patcher {
return func(c Conditioner) {
SetReadyFailedWithMessage(c, msg)
}
}
func (p *Ready) PatcherWithError(err error) Patcher {
return func(c Conditioner) {
if err == nil {
SetReadyCreated(c)
} else {
SetReadyFailed(c)
}
}
}
func (p *Ready) PatcherRefResolver(err error, obj interface{}) Patcher {
return func(c Conditioner) {
if err == nil {
return
}
if apierrors.IsNotFound(err) {
SetReadyFailedWithMessage(c, fmt.Sprintf("%s not found", getType(obj)))
return
}
SetReadyFailedWithMessage(c, fmt.Sprintf("Error getting %s", getType(obj)))
}
}
func (p *Ready) PatcherHealthy(err error) Patcher {
return func(c Conditioner) {
if err == nil {
SetReadyHealthty(c)
} else {
SetReadyUnhealthtyWithError(c, err)
}
}
}
func getType(obj interface{}) string {
if t := reflect.TypeOf(obj); t.Kind() == reflect.Ptr {
return t.Elem().Name()
} else {
return t.Name()
}
}

25
src/pkg/condition/pat.go Normal file
View File

@@ -0,0 +1,25 @@
package conditions
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1"
)
func SetPatOutOfDate(c Conditioner) {
c.SetCondition(metav1.Condition{
Type: zitadelv1alpha1.ConditionTypePATUpToDate,
Status: metav1.ConditionFalse,
Reason: zitadelv1alpha1.ConditionReasonRolesChanged,
Message: "PAT out of date",
})
}
func SetPatUpToDate(c Conditioner) {
c.SetCondition(metav1.Condition{
Type: zitadelv1alpha1.ConditionTypePATUpToDate,
Status: metav1.ConditionTrue,
Reason: zitadelv1alpha1.ConditionReasonPATUpToDate,
Message: "PAT up to date",
})
}

View File

@@ -0,0 +1,70 @@
package conditions
import (
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1"
)
func SetReadyHealthty(c Conditioner) {
c.SetCondition(metav1.Condition{
Type: zitadelv1alpha1.ConditionTypeReady,
Status: metav1.ConditionTrue,
Reason: zitadelv1alpha1.ConditionReasonHealthy,
Message: "Healthy",
})
}
func SetReadyUnhealthtyWithError(c Conditioner, err error) {
c.SetCondition(metav1.Condition{
Type: zitadelv1alpha1.ConditionTypeReady,
Status: metav1.ConditionFalse,
Reason: zitadelv1alpha1.ConditionReasonHealthy,
Message: err.Error(),
})
}
func SetReadyCreatedWithMessage(c Conditioner, message string) {
c.SetCondition(metav1.Condition{
Type: zitadelv1alpha1.ConditionTypeReady,
Status: metav1.ConditionTrue,
Reason: zitadelv1alpha1.ConditionReasonCreated,
Message: message,
})
}
func SetReadyCreated(c Conditioner) {
SetReadyCreatedWithMessage(c, "Created")
}
func SetReadyFailedWithMessage(c Conditioner, message string) {
c.SetCondition(metav1.Condition{
Type: zitadelv1alpha1.ConditionTypeReady,
Status: metav1.ConditionFalse,
Reason: zitadelv1alpha1.ConditionReasonFailed,
Message: message,
})
}
func SetReadyFailed(c Conditioner) {
SetReadyFailedWithMessage(c, "Failed")
}
func SetReadyWithDeployment(c Conditioner, sts *appsv1.Deployment) {
if sts.Status.Replicas == 0 || sts.Status.ReadyReplicas != sts.Status.Replicas {
c.SetCondition(metav1.Condition{
Type: zitadelv1alpha1.ConditionTypeReady,
Status: metav1.ConditionFalse,
Reason: zitadelv1alpha1.ConditionReasonDeploymentNotReady,
Message: "Not ready",
})
return
}
c.SetCondition(metav1.Condition{
Type: zitadelv1alpha1.ConditionTypeReady,
Status: metav1.ConditionTrue,
Reason: zitadelv1alpha1.ConditionReasonDeploymentReady,
Message: "Running",
})
}

View File

@@ -0,0 +1,111 @@
package core
import (
"context"
"fmt"
"time"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1"
condition "github.com/HaimKortovich/zitadel-resources-operator/pkg/condition"
zitadelClient "github.com/HaimKortovich/zitadel-resources-operator/pkg/zitadel"
"github.com/hashicorp/go-multierror"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
type CoreReconciler struct {
Client client.Client
RefResolver *zitadelv1alpha1.RefResolver
ConditionReady *condition.Ready
WrappedReconciler WrappedCoreReconciler
Finalizer Finalizer
RequeueInterval time.Duration
}
func NewCoreReconciler(client client.Client, cr *condition.Ready, wr WrappedCoreReconciler, f Finalizer,
requeueInterval time.Duration) Reconciler {
return &CoreReconciler{
Client: client,
RefResolver: zitadelv1alpha1.NewRefResolver(client),
ConditionReady: cr,
WrappedReconciler: wr,
Finalizer: f,
RequeueInterval: requeueInterval,
}
}
func (r *CoreReconciler) Reconcile(ctx context.Context, resource Resource) (ctrl.Result, error) {
if resource.IsBeingDeleted() {
if err := r.Finalizer.Finalize(ctx, resource); err != nil {
return ctrl.Result{}, fmt.Errorf("error finalizing %s: %v", resource.GetName(), err)
}
return ctrl.Result{}, nil
}
connectionRef, err := resource.ConnectionRef(ctx, r.RefResolver)
if err != nil {
return ctrl.Result{}, err
}
connection, err := r.RefResolver.ConnectionRef(ctx, connectionRef, resource.GetNamespace())
if err != nil {
var errBundle *multierror.Error
errBundle = multierror.Append(errBundle, err)
err = r.WrappedReconciler.PatchStatus(ctx, r.ConditionReady.PatcherRefResolver(err, connection))
errBundle = multierror.Append(errBundle, err)
return ctrl.Result{}, fmt.Errorf("error getting Connection: %v", errBundle)
}
ztdClient, err := zitadelClient.NewV2Client(ctx, connection, *r.RefResolver)
if err != nil {
var errBundle *multierror.Error
errBundle = multierror.Append(errBundle, err)
msg := fmt.Sprintf("Error connecting to Zitadel: %v", err)
err = r.WrappedReconciler.PatchStatus(ctx, r.ConditionReady.PatcherFailed(msg))
errBundle = multierror.Append(errBundle, err)
return r.retryResult(ctx, resource, errBundle)
}
defer ztdClient.Close()
err = r.WrappedReconciler.Reconcile(ctx, ztdClient)
var errBundle *multierror.Error
errBundle = multierror.Append(errBundle, err)
if err := errBundle.ErrorOrNil(); err != nil {
msg := fmt.Sprintf("Error creating %s: %v", resource.GetName(), err)
err = r.WrappedReconciler.PatchStatus(ctx, r.ConditionReady.PatcherFailed(msg))
errBundle = multierror.Append(errBundle, err)
return r.retryResult(ctx, resource, errBundle)
}
if err = r.Finalizer.AddFinalizer(ctx); err != nil {
errBundle = multierror.Append(errBundle, fmt.Errorf("error adding finalizer to %s: %v", resource.GetName(), err))
}
err = r.WrappedReconciler.PatchStatus(ctx, r.ConditionReady.PatcherWithError(errBundle.ErrorOrNil()))
errBundle = multierror.Append(errBundle, err)
if err := errBundle.ErrorOrNil(); err != nil {
return ctrl.Result{}, err
}
return r.requeueResult(ctx, resource)
}
func (r *CoreReconciler) retryResult(ctx context.Context, resource Resource, err error) (ctrl.Result, error) {
return ctrl.Result{}, err
}
func (r *CoreReconciler) requeueResult(ctx context.Context, resource Resource) (ctrl.Result, error) {
if r.RequeueInterval > 0 {
log.FromContext(ctx).V(1).Info("Requeuing CORE resource")
return ctrl.Result{RequeueAfter: r.RequeueInterval}, nil
}
return ctrl.Result{}, nil
}

View File

@@ -0,0 +1,73 @@
package core
import (
"context"
"fmt"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1"
zitadelClient "github.com/HaimKortovich/zitadel-resources-operator/pkg/zitadel"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type CoreFinalizer struct {
Client client.Client
RefResolver *zitadelv1alpha1.RefResolver
WrappedFinalizer WrappedCoreFinalizer
}
func NewCoreFinalizer(client client.Client, wf WrappedCoreFinalizer) Finalizer {
return &CoreFinalizer{
Client: client,
RefResolver: zitadelv1alpha1.NewRefResolver(client),
WrappedFinalizer: wf,
}
}
func (tf *CoreFinalizer) AddFinalizer(ctx context.Context) error {
if tf.WrappedFinalizer.ContainsFinalizer() {
return nil
}
if err := tf.WrappedFinalizer.AddFinalizer(ctx); err != nil {
return fmt.Errorf("error adding finalizer in TemplateFinalizer: %v", err)
}
return nil
}
func (tf *CoreFinalizer) Finalize(ctx context.Context, resource Resource) error {
if !tf.WrappedFinalizer.ContainsFinalizer() {
return nil
}
connectionRef, err := resource.ConnectionRef(ctx, tf.RefResolver)
if err != nil {
return err
}
connection, err := tf.RefResolver.ConnectionRef(ctx, connectionRef, resource.GetNamespace())
if err != nil {
if apierrors.IsNotFound(err) {
if err := tf.WrappedFinalizer.RemoveFinalizer(ctx); err != nil {
return fmt.Errorf("error removing %s finalizer: %v", resource.GetName(), err)
}
return nil
}
return fmt.Errorf("error getting Cluster: %v", err)
}
ztdClient, err := zitadelClient.NewV2Client(ctx, connection, *tf.RefResolver)
if err != nil {
return fmt.Errorf("error connecting to Zitadel: %v", err)
}
defer ztdClient.Close()
if err := tf.WrappedFinalizer.Reconcile(ctx, ztdClient); err != nil {
return fmt.Errorf("error reconciling in TemplateFinalizer: %v", err)
}
if err := tf.WrappedFinalizer.RemoveFinalizer(ctx); err != nil {
return fmt.Errorf("error removing finalizer in TemplateFinalizer: %v", err)
}
return nil
}

View File

@@ -0,0 +1,39 @@
package core
import (
"context"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1"
"github.com/zitadel/zitadel-go/v3/pkg/client"
condition "github.com/HaimKortovich/zitadel-resources-operator/pkg/condition"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
)
type Resource interface {
v1.Object
ConnectionRef(context.Context, *zitadelv1alpha1.RefResolver) (*zitadelv1alpha1.ConnectionRef, error)
IsBeingDeleted() bool
}
type Reconciler interface {
Reconcile(ctx context.Context, resource Resource) (ctrl.Result, error)
}
type WrappedCoreReconciler interface {
Reconcile(context.Context, *client.Client) error
PatchStatus(context.Context, condition.Patcher) error
}
type Finalizer interface {
AddFinalizer(context.Context) error
Finalize(context.Context, Resource) error
}
type WrappedCoreFinalizer interface {
AddFinalizer(context.Context) error
RemoveFinalizer(context.Context) error
ContainsFinalizer() bool
Reconcile(context.Context, *client.Client) error
}

View File

@@ -0,0 +1,63 @@
package zitadel
import (
"context"
"fmt"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-resources-operator/api/v1alpha1"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
z "github.com/zitadel/zitadel-go/v3/pkg/zitadel"
)
func NewV2Client(ctx context.Context, connection *zitadelv1alpha1.Connection, refresolver zitadelv1alpha1.RefResolver) (*clientv2.Client, error) {
zOpts := []z.Option{}
if connection.Spec.Port != nil {
zOpts = append(zOpts, z.WithPort(*connection.Spec.Port))
}
if connection.Spec.InsecureSkipVerifyTLS {
zOpts = append(zOpts, z.WithInsecureSkipVerifyTLS())
}
if !connection.Spec.Secure {
port := uint16(443)
if connection.Spec.Port != nil {
port = *connection.Spec.Port
}
zOpts = append(zOpts, z.WithInsecure(fmt.Sprintf("%d", port)))
}
var auth clientv2.TokenSourceInitializer
if connection.Spec.Authentication.PAT != nil {
pat, err := refresolver.SecretKeyRef(ctx, connection.Spec.Authentication.PAT.TokenSecretKey, connection.Namespace)
if err != nil {
return nil, err
}
auth = clientv2.PAT(pat)
}
if connection.Spec.Authentication.JWT != nil {
jwt, err := refresolver.SecretKeyRef(ctx, connection.Spec.Authentication.JWT.JWTSecretKey, connection.Namespace)
if err != nil {
return nil, err
}
keyfile, err := clientv2.ConfigFromKeyFileData([]byte(jwt))
if err != nil {
return nil, err
}
auth = clientv2.AuthenticationJWTProfile(keyfile, connection.Spec.Authentication.JWT.Scopes...)
}
if connection.Spec.Authentication.Password != nil {
password, err := refresolver.SecretKeyRef(ctx, connection.Spec.Authentication.Password.PasswordSecretKey, connection.Namespace)
if err != nil {
return nil, err
}
auth = clientv2.PasswordAuthentication(connection.Spec.Authentication.Password.Username, password, connection.Spec.Authentication.Password.Scopes...)
}
client, err := clientv2.New(ctx, z.New(connection.Spec.Host, zOpts...), clientv2.WithAuth(auth))
if err != nil {
return nil, fmt.Errorf("Error creating V2Client: %v", err)
}
return client, nil
}