move everything to src
All checks were successful
Build and Publish / build-release (push) Successful in 8m29s

This commit is contained in:
2026-04-07 12:33:54 -05:00
parent 080a33f76f
commit d5c3485fd2
98 changed files with 75 additions and 76 deletions

View File

@@ -0,0 +1,192 @@
/*
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"
"strings"
"time"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1"
"github.com/HaimKortovich/zitadel-k8s-operator/pkg/builder"
condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition"
"github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel"
"github.com/zitadel/zitadel-go/v3/pkg/client/management"
"github.com/zitadel/zitadel-go/v3/pkg/client/middleware"
pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management"
durationpb "google.golang.org/protobuf/types/known/durationpb"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// ActionReconciler reconciles a Action object
type ActionReconciler struct {
client.Client
RefResolver *zitadelv1alpha1.RefResolver
ConditionReady *condition.Ready
RequeueInterval time.Duration
Builder *builder.Builder
}
func NewActionReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready,
requeueInterval time.Duration) *ActionReconciler {
return &ActionReconciler{
Client: client,
RefResolver: refResolver,
ConditionReady: conditionReady,
RequeueInterval: requeueInterval,
Builder: builder,
}
}
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=actions,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=actions/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=actions/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 *ActionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var Action zitadelv1alpha1.Action
if err := r.Get(ctx, req.NamespacedName, &Action); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
wr := newWrappedActionReconciler(r.Client, r.RefResolver, r.Builder, &Action)
wf := newWrappedActionFinalizer(r.Client, &Action, r.RefResolver)
tf := zitadel.NewZitadelFinalizer(r.Client, wf)
tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval)
result, err := tr.Reconcile(ctx, &Action)
if err != nil {
return result, fmt.Errorf("error reconciling in ActionReconciler: %v", err)
}
return result, nil
}
type wrappedActionReconciler struct {
client.Client
refResolver *zitadelv1alpha1.RefResolver
Action *zitadelv1alpha1.Action
Builder *builder.Builder
}
func newWrappedActionReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder,
Action *zitadelv1alpha1.Action) zitadel.WrappedReconciler {
return &wrappedActionReconciler{
Client: client,
refResolver: refResolver,
Action: Action,
Builder: builder,
}
}
type actionReoncilePhase struct {
Name string
Reconcile func(context.Context, *management.Client) error
}
func (wr *wrappedActionReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error {
phases := []actionReoncilePhase{
{
Name: "action",
Reconcile: wr.reconcileAction,
},
}
for _, p := range phases {
err := p.Reconcile(ctx, ztdClient)
if err != nil {
return err
}
}
return nil
}
func (wr *wrappedActionReconciler) reconcileAction(ctx context.Context, ztdClient *management.Client) error {
org, err := wr.refResolver.OrganizationRef(ctx, &wr.Action.Spec.OrganizationRef, wr.Action.Namespace)
if err != nil {
return err
}
ctx = middleware.SetOrgID(ctx, org.Status.OrgId)
if wr.Action.Status.ActionId != "" {
p, err := ztdClient.GetAction(ctx, &pb.GetActionRequest{Id: wr.Action.Status.ActionId})
if p != nil {
_, err := ztdClient.UpdateAction(ctx,
&pb.UpdateActionRequest{
Id: p.Action.Id,
Name: wr.Action.Name,
Script: wr.Action.Spec.Script,
Timeout: durationpb.New(wr.Action.Spec.Timeout.Duration),
AllowedToFail: wr.Action.Spec.AllowedToFail,
},
)
if err != nil {
if !strings.Contains(err.Error(), "No changes") {
return fmt.Errorf("Error updating Action: %v", err)
}
}
return nil
}
if err != nil {
if !strings.Contains(err.Error(), "not found") {
return fmt.Errorf("Error getting Action: %v", err)
}
}
}
resp, err := ztdClient.CreateAction(ctx,
&pb.CreateActionRequest{
Name: wr.Action.Name,
Script: wr.Action.Spec.Script,
Timeout: durationpb.New(wr.Action.Spec.Timeout.Duration),
AllowedToFail: wr.Action.Spec.AllowedToFail,
},
)
if err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
return nil
}
return fmt.Errorf("error creating action in Zitadel: %v", err)
}
patch := ctrlClient.MergeFrom(wr.Action.DeepCopy())
wr.Action.Status.ActionId = resp.Id
return wr.Client.Status().Patch(ctx, wr.Action, patch)
}
func (wr *wrappedActionReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error {
patch := client.MergeFrom(wr.Action.DeepCopy())
patcher(&wr.Action.Status)
if err := wr.Client.Status().Patch(ctx, wr.Action, patch); err != nil {
return fmt.Errorf("error patching Action status: %v", err)
}
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *ActionReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&zitadelv1alpha1.Action{}).
WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,94 @@
package controller
import (
"strings"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1"
"github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel"
"context"
"fmt"
"github.com/zitadel/zitadel-go/v3/pkg/client/management"
"github.com/zitadel/zitadel-go/v3/pkg/client/middleware"
pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
const (
actionFinalizerName = "action.zitadel.topmanage.com/action"
)
type wrappedActionFinalizer struct {
client.Client
action *zitadelv1alpha1.Action
refresolver *zitadelv1alpha1.RefResolver
}
func newWrappedActionFinalizer(client client.Client, action *zitadelv1alpha1.Action, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer {
return &wrappedActionFinalizer{
Client: client,
action: action,
refresolver: refresolver,
}
}
func (wf *wrappedActionFinalizer) AddFinalizer(ctx context.Context) error {
if wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.action, func(action *zitadelv1alpha1.Action) {
controllerutil.AddFinalizer(action, actionFinalizerName)
})
}
func (wf *wrappedActionFinalizer) RemoveFinalizer(ctx context.Context) error {
if !wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.action, func(action *zitadelv1alpha1.Action) {
controllerutil.RemoveFinalizer(wf.action, actionFinalizerName)
})
}
func (wr *wrappedActionFinalizer) ContainsFinalizer() bool {
return controllerutil.ContainsFinalizer(wr.action, actionFinalizerName)
}
func (wf *wrappedActionFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error {
if wf.action.Status.ActionId == "" {
return nil
}
org, err := wf.refresolver.OrganizationRef(ctx, &wf.action.Spec.OrganizationRef, wf.action.Namespace)
if err != nil {
return err
}
{
_, err := ztdClient.GetAction(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetActionRequest{Id: wf.action.Status.ActionId})
if err != nil {
if strings.Contains(err.Error(), `not found`) {
return nil
}
return err
}
}
_, err = ztdClient.DeleteAction(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.DeleteActionRequest{Id: wf.action.Status.ActionId})
if err != nil {
return err
}
return nil
}
func (wr *wrappedActionFinalizer) patch(ctx context.Context, action *zitadelv1alpha1.Action,
patchFn func(*zitadelv1alpha1.Action)) error {
patch := ctrlClient.MergeFrom(action.DeepCopy())
patchFn(action)
if err := wr.Client.Patch(ctx, action, patch); err != nil {
return fmt.Errorf("error patching Action finalizer: %v", err)
}
return nil
}

View File

@@ -0,0 +1,282 @@
/*
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"
"encoding/json"
"fmt"
"strings"
"time"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1"
"github.com/HaimKortovich/zitadel-k8s-operator/pkg/builder"
condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition"
"github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel"
"github.com/zitadel/zitadel-go/v3/pkg/client/management"
"github.com/zitadel/zitadel-go/v3/pkg/client/middleware"
app "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/app"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/authn"
pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// APIAppReconciler reconciles a APIApp object
type APIAppReconciler struct {
client.Client
RefResolver *zitadelv1alpha1.RefResolver
ConditionReady *condition.Ready
RequeueInterval time.Duration
Builder *builder.Builder
}
func NewAPIAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready,
requeueInterval time.Duration) *APIAppReconciler {
return &APIAppReconciler{
Client: client,
RefResolver: refResolver,
ConditionReady: conditionReady,
RequeueInterval: requeueInterval,
Builder: builder,
}
}
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps/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 *APIAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var APIApp zitadelv1alpha1.APIApp
if err := r.Get(ctx, req.NamespacedName, &APIApp); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
wr := newWrappedAPIAppReconciler(r.Client, r.RefResolver, r.Builder, &APIApp)
wf := newWrappedAPIAppFinalizer(r.Client, &APIApp, r.RefResolver)
tf := zitadel.NewZitadelFinalizer(r.Client, wf)
tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval)
result, err := tr.Reconcile(ctx, &APIApp)
if err != nil {
return result, fmt.Errorf("error reconciling in APIAppReconciler: %v", err)
}
return result, nil
}
type wrappedAPIAppReconciler struct {
client.Client
refResolver *zitadelv1alpha1.RefResolver
APIApp *zitadelv1alpha1.APIApp
Builder *builder.Builder
}
func newWrappedAPIAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder,
APIApp *zitadelv1alpha1.APIApp) zitadel.WrappedReconciler {
return &wrappedAPIAppReconciler{
Client: client,
refResolver: refResolver,
APIApp: APIApp,
Builder: builder,
}
}
type apiAppReoncilePhase struct {
Name string
Reconcile func(context.Context, *management.Client) error
}
func (wr *wrappedAPIAppReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error {
phases := []projectReconcilePhase{
{
Name: "apiapp",
Reconcile: wr.reconcileApp,
},
{
Name: "keys",
Reconcile: wr.reconcileKeys,
},
}
for _, p := range phases {
err := p.Reconcile(ctx, ztdClient)
if err != nil {
return err
}
}
return nil
}
func (wr *wrappedAPIAppReconciler) reconcileApp(ctx context.Context, ztdClient *management.Client) error {
org, err := wr.APIApp.Organization(ctx, wr.refResolver)
if err != nil {
return err
}
project, err := wr.APIApp.Project(ctx, wr.refResolver)
if err != nil {
return err
}
if wr.APIApp.Status.AppId != "" {
appResp, err := ztdClient.GetAppByID(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppByIDRequest{
ProjectId: project.Status.ProjectId,
AppId: string(wr.APIApp.Status.AppId),
})
if err != nil {
return fmt.Errorf("Error getting APIApp: %v", err)
}
if appResp.App != nil {
_, err := ztdClient.UpdateAPIAppConfig(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.UpdateAPIAppConfigRequest{ProjectId: project.Status.ProjectId, AppId: wr.APIApp.Status.AppId,
AuthMethodType: app.APIAuthMethodType(app.APIAuthMethodType_value[wr.APIApp.Spec.AuthMethodType]),
})
if err != nil {
if !strings.Contains(err.Error(), "No changes") {
return fmt.Errorf("Error updating APIApp: %v", err)
}
}
return nil
}
}
resp, err := ztdClient.AddAPIApp(middleware.SetOrgID(ctx, org.Status.OrgId),
&pb.AddAPIAppRequest{
Name: wr.APIApp.Name,
ProjectId: project.Status.ProjectId,
AuthMethodType: app.APIAuthMethodType(app.APIAuthMethodType_value[wr.APIApp.Spec.AuthMethodType]),
},
)
if err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
return nil
}
return fmt.Errorf("error creating APIApp in Zitadel: %v", err)
}
key := types.NamespacedName{
Name: wr.APIApp.Name + "-client-secret",
Namespace: wr.APIApp.Namespace,
}
secretData := map[string][]byte{"client-secret": []byte(resp.ClientSecret)}
secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: false, Zitadel: nil, Key: key, Data: secretData}, wr.APIApp)
if err != nil {
return fmt.Errorf("error building Secret: %v", err)
}
if err := wr.Create(ctx, secret); err != nil {
return fmt.Errorf("error creating Client-secret Secret: %v", err)
}
patch := ctrlClient.MergeFrom(wr.APIApp.DeepCopy())
wr.APIApp.Status.AppId = resp.AppId
wr.APIApp.Status.ClientId = resp.ClientId
return wr.Client.Status().Patch(ctx, wr.APIApp, patch)
}
type Key struct {
Type string `json:"type"`
KeyID string `json:"keyId"`
Key string `json:"key"`
AppID string `json:"appId"`
ClientID string `json:"clientId"`
}
func (wr *wrappedAPIAppReconciler) reconcileKeys(ctx context.Context, ztdClient *management.Client) error {
if wr.APIApp.Spec.AuthMethodType == "API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT" {
org, err := wr.APIApp.Organization(ctx, wr.refResolver)
if err != nil {
return err
}
project, err := wr.APIApp.Project(ctx, wr.refResolver)
if err != nil {
return err
}
if wr.APIApp.Status.KeyId != "" {
appKey, err := ztdClient.GetAppKey(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppKeyRequest{
ProjectId: project.Status.ProjectId,
AppId: wr.APIApp.Status.AppId,
KeyId: wr.APIApp.Status.KeyId,
})
if err != nil {
if !strings.Contains(err.Error(), "not found") {
return fmt.Errorf("Could not get key: %v", err)
}
}
if appKey.Key != nil {
return nil
}
}
resp, err := ztdClient.AddAppKey(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.AddAppKeyRequest{
ProjectId: project.Status.ProjectId,
AppId: wr.APIApp.Status.AppId,
Type: authn.KeyType_KEY_TYPE_JSON,
ExpirationDate: nil,
})
if err != nil {
return fmt.Errorf("Error adding Key to app: %v", err)
}
key := types.NamespacedName{
Name: wr.APIApp.Name + "-privatekey-secret",
Namespace: wr.APIApp.Namespace,
}
var jsonKey Key
if err = json.Unmarshal(resp.KeyDetails, &jsonKey); err != nil {
return fmt.Errorf("Could not unmarshal key details: %v", err)
}
secretData := map[string][]byte{
"clientId": []byte(jsonKey.ClientID),
"type": []byte(jsonKey.Type),
"keyId": []byte(jsonKey.KeyID),
"appId": []byte(jsonKey.AppID),
"key": []byte(jsonKey.Key),
}
secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: false, Zitadel: nil, Key: key, Data: secretData}, wr.APIApp)
if err != nil {
return fmt.Errorf("error building Secret: %v", err)
}
if err := wr.Create(ctx, secret); err != nil {
return fmt.Errorf("error creating private-key Secret: %v", err)
}
patch := ctrlClient.MergeFrom(wr.APIApp.DeepCopy())
wr.APIApp.Status.KeyId = resp.Id
return wr.Client.Status().Patch(ctx, wr.APIApp, patch)
}
return nil
}
func (wr *wrappedAPIAppReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error {
patch := client.MergeFrom(wr.APIApp.DeepCopy())
patcher(&wr.APIApp.Status)
if err := wr.Client.Status().Patch(ctx, wr.APIApp, patch); err != nil {
return fmt.Errorf("error patching APIApp status: %v", err)
}
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *APIAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&zitadelv1alpha1.APIApp{}).
Owns(&corev1.Secret{}).
WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,91 @@
package controller
import (
"strings"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1"
"github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel"
"context"
"fmt"
"github.com/zitadel/zitadel-go/v3/pkg/client/management"
"github.com/zitadel/zitadel-go/v3/pkg/client/middleware"
pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
const (
APIAppFinalizerName = "apiapp.zitadel.topmanage.com/apiapp"
)
type wrappedAPIAppFinalizer struct {
client.Client
APIApp *zitadelv1alpha1.APIApp
refresolver *zitadelv1alpha1.RefResolver
}
func newWrappedAPIAppFinalizer(client client.Client, APIApp *zitadelv1alpha1.APIApp, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer {
return &wrappedAPIAppFinalizer{
Client: client,
APIApp: APIApp,
refresolver: refresolver,
}
}
func (wf *wrappedAPIAppFinalizer) AddFinalizer(ctx context.Context) error {
if wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.APIApp, func(APIApp *zitadelv1alpha1.APIApp) {
controllerutil.AddFinalizer(APIApp, APIAppFinalizerName)
})
}
func (wf *wrappedAPIAppFinalizer) RemoveFinalizer(ctx context.Context) error {
if !wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.APIApp, func(APIApp *zitadelv1alpha1.APIApp) {
controllerutil.RemoveFinalizer(wf.APIApp, APIAppFinalizerName)
})
}
func (wr *wrappedAPIAppFinalizer) ContainsFinalizer() bool {
return controllerutil.ContainsFinalizer(wr.APIApp, APIAppFinalizerName)
}
func (wf *wrappedAPIAppFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error {
if wf.APIApp.Status.AppId == "" {
return nil
}
org, err := wf.APIApp.Organization(ctx, wf.refresolver)
if err != nil {
return err
}
project, err := wf.APIApp.Project(ctx, wf.refresolver)
if err != nil {
return err
}
_, err = ztdClient.RemoveApp(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.RemoveAppRequest{ProjectId: project.Status.ProjectId, AppId: wf.APIApp.Status.AppId})
if err != nil {
if strings.Contains(err.Error(), "doesn't exist") {
return nil
}
return err
}
return nil
}
func (wr *wrappedAPIAppFinalizer) patch(ctx context.Context, APIApp *zitadelv1alpha1.APIApp,
patchFn func(*zitadelv1alpha1.APIApp)) error {
patch := ctrlClient.MergeFrom(APIApp.DeepCopy())
patchFn(APIApp)
if err := wr.Client.Patch(ctx, APIApp, patch); err != nil {
return fmt.Errorf("error patching APIApp finalizer: %v", err)
}
return nil
}

View File

@@ -0,0 +1,118 @@
/*
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-resources-operator/api/v1alpha1"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/builder"
condition "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/condition"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
"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"
)
// ConnectionReconciler reconciles a Connection object
type ConnectionReconciler struct {
client.Client
RefResolver *zitadelv1alpha1.RefResolver
ConditionReady *condition.Ready
RequeueInterval time.Duration
Builder *builder.Builder
}
func NewConnectionReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready,
requeueInterval time.Duration) *ConnectionReconciler {
return &ConnectionReconciler{
Client: client,
RefResolver: refResolver,
ConditionReady: conditionReady,
RequeueInterval: requeueInterval,
Builder: builder,
}
}
//+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="",resources=secrets,verbs=list;watch;create;patch
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch;delete
// 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 *ConnectionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var Connection zitadelv1alpha1.Connection
if err := r.Get(ctx, req.NamespacedName, &Connection); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
wr := newWrappedConnectionReconciler(r.Client, r.RefResolver, r.Builder, &Connection)
wf := newWrappedConnectionFinalizer(r.Client, &Connection, r.RefResolver)
tf := core.NewCoreFinalizer(r.Client, wf)
tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval)
result, err := tr.Reconcile(ctx, &Connection)
if err != nil {
return result, fmt.Errorf("error reconciling in ConnectionReconciler: %v", err)
}
return result, nil
}
type wrappedConnectionReconciler struct {
client.Client
refResolver *zitadelv1alpha1.RefResolver
Connection *zitadelv1alpha1.Connection
Builder *builder.Builder
}
func newWrappedConnectionReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder,
Connection *zitadelv1alpha1.Connection) core.WrappedCoreReconciler {
return &wrappedConnectionReconciler{
Client: client,
refResolver: refResolver,
Connection: Connection,
Builder: builder,
}
}
func (wr *wrappedConnectionReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error {
return nil
}
func (wr *wrappedConnectionReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error {
patch := client.MergeFrom(wr.Connection.DeepCopy())
patcher(&wr.Connection.Status)
if err := wr.Client.Status().Patch(ctx, wr.Connection, patch); err != nil {
return fmt.Errorf("error patching Connection status: %v", err)
}
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *ConnectionReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&zitadelv1alpha1.Connection{}).
WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,69 @@
package controller
import (
zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core"
"context"
"fmt"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
const (
ConnectionFinalizerName = "connection.zitadel.github.com/connection"
)
type wrappedConnectionFinalizer struct {
client.Client
Connection *zitadelv1alpha1.Connection
refresolver *zitadelv1alpha1.RefResolver
}
func newWrappedConnectionFinalizer(client client.Client, Connection *zitadelv1alpha1.Connection, refresolver *zitadelv1alpha1.RefResolver) core.WrappedCoreFinalizer {
return &wrappedConnectionFinalizer{
Client: client,
Connection: Connection,
refresolver: refresolver,
}
}
func (wf *wrappedConnectionFinalizer) AddFinalizer(ctx context.Context) error {
if wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.Connection, func(Connection *zitadelv1alpha1.Connection) {
controllerutil.AddFinalizer(Connection, ConnectionFinalizerName)
})
}
func (wf *wrappedConnectionFinalizer) RemoveFinalizer(ctx context.Context) error {
if !wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.Connection, func(Connection *zitadelv1alpha1.Connection) {
controllerutil.RemoveFinalizer(wf.Connection, ConnectionFinalizerName)
})
}
func (wr *wrappedConnectionFinalizer) ContainsFinalizer() bool {
return controllerutil.ContainsFinalizer(wr.Connection, ConnectionFinalizerName)
}
func (wf *wrappedConnectionFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error {
return nil
}
func (wr *wrappedConnectionFinalizer) patch(ctx context.Context, Connection *zitadelv1alpha1.Connection,
patchFn func(*zitadelv1alpha1.Connection)) error {
patch := ctrlClient.MergeFrom(Connection.DeepCopy())
patchFn(Connection)
if err := wr.Client.Patch(ctx, Connection, patch); err != nil {
return fmt.Errorf("error patching Connection finalizer: %v", err)
}
return nil
}

View File

@@ -0,0 +1,168 @@
/*
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"
"strings"
"time"
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1"
"github.com/HaimKortovich/zitadel-k8s-operator/pkg/builder"
condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition"
"github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel"
"github.com/zitadel/zitadel-go/v3/pkg/client/management"
"github.com/zitadel/zitadel-go/v3/pkg/client/middleware"
pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management"
"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"
)
// FlowReconciler reconciles a Flow object
type FlowReconciler struct {
client.Client
RefResolver *zitadelv1alpha1.RefResolver
ConditionReady *condition.Ready
RequeueInterval time.Duration
Builder *builder.Builder
}
func NewFlowReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready,
requeueInterval time.Duration) *FlowReconciler {
return &FlowReconciler{
Client: client,
RefResolver: refResolver,
ConditionReady: conditionReady,
RequeueInterval: requeueInterval,
Builder: builder,
}
}
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=flows,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=flows/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=flows/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 *FlowReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var Flow zitadelv1alpha1.Flow
if err := r.Get(ctx, req.NamespacedName, &Flow); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
wr := newWrappedFlowReconciler(r.Client, r.RefResolver, r.Builder, &Flow)
wf := newWrappedFlowFinalizer(r.Client, &Flow, r.RefResolver)
tf := zitadel.NewZitadelFinalizer(r.Client, wf)
tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval)
result, err := tr.Reconcile(ctx, &Flow)
if err != nil {
return result, fmt.Errorf("error reconciling in FlowReconciler: %v", err)
}
return result, nil
}
type wrappedFlowReconciler struct {
client.Client
refResolver *zitadelv1alpha1.RefResolver
Flow *zitadelv1alpha1.Flow
Builder *builder.Builder
}
func newWrappedFlowReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder,
Flow *zitadelv1alpha1.Flow) zitadel.WrappedReconciler {
return &wrappedFlowReconciler{
Client: client,
refResolver: refResolver,
Flow: Flow,
Builder: builder,
}
}
type flowReoncilePhase struct {
Name string
Reconcile func(context.Context, *management.Client) error
}
func (wr *wrappedFlowReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error {
phases := []flowReoncilePhase{
{
Name: "flow",
Reconcile: wr.reconcileFlow,
},
}
for _, p := range phases {
err := p.Reconcile(ctx, ztdClient)
if err != nil {
return err
}
}
return nil
}
func (wr *wrappedFlowReconciler) reconcileFlow(ctx context.Context, ztdClient *management.Client) error {
org, err := wr.refResolver.OrganizationRef(ctx, &wr.Flow.Spec.OrganizationRef, wr.Flow.Namespace)
if err != nil {
return err
}
ctx = middleware.SetOrgID(ctx, org.Status.OrgId)
actionIds := []string{}
for _, actionRef := range wr.Flow.Spec.ActionRefs {
action, err := wr.refResolver.ActionRef(ctx, &actionRef, wr.Flow.Namespace)
if err != nil {
return fmt.Errorf("Error resolving action reference: %v", err)
}
if action.Status.ActionId == "" {
return fmt.Errorf("Action with name: %s not ready for trigger", action.Name)
}
actionIds = append(actionIds, action.Status.ActionId)
}
_, err = ztdClient.SetTriggerActions(ctx, &pb.SetTriggerActionsRequest{
FlowType: wr.Flow.Spec.FlowType,
TriggerType: wr.Flow.Spec.TriggerType,
ActionIds: actionIds,
})
if err != nil {
if !strings.Contains(err.Error(), "No Changes") {
return fmt.Errorf("Error triggering action flow: %v", err)
}
}
return nil
}
func (wr *wrappedFlowReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error {
patch := client.MergeFrom(wr.Flow.DeepCopy())
patcher(&wr.Flow.Status)
if err := wr.Client.Status().Patch(ctx, wr.Flow, patch); err != nil {
return fmt.Errorf("error patching Flow status: %v", err)
}
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *FlowReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&zitadelv1alpha1.Flow{}).
WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,82 @@
package controller
import (
zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1"
"github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel"
"context"
"fmt"
"github.com/zitadel/zitadel-go/v3/pkg/client/management"
"github.com/zitadel/zitadel-go/v3/pkg/client/middleware"
pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
const (
flowFinalizerName = "flow.zitadel.topmanage.com/flow"
)
type wrappedFlowFinalizer struct {
client.Client
flow *zitadelv1alpha1.Flow
refresolver *zitadelv1alpha1.RefResolver
}
func newWrappedFlowFinalizer(client client.Client, flow *zitadelv1alpha1.Flow, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer {
return &wrappedFlowFinalizer{
Client: client,
flow: flow,
refresolver: refresolver,
}
}
func (wf *wrappedFlowFinalizer) AddFinalizer(ctx context.Context) error {
if wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.flow, func(flow *zitadelv1alpha1.Flow) {
controllerutil.AddFinalizer(flow, flowFinalizerName)
})
}
func (wf *wrappedFlowFinalizer) RemoveFinalizer(ctx context.Context) error {
if !wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.flow, func(flow *zitadelv1alpha1.Flow) {
controllerutil.RemoveFinalizer(wf.flow, flowFinalizerName)
})
}
func (wr *wrappedFlowFinalizer) ContainsFinalizer() bool {
return controllerutil.ContainsFinalizer(wr.flow, flowFinalizerName)
}
func (wf *wrappedFlowFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error {
org, err := wf.refresolver.OrganizationRef(ctx, &wf.flow.Spec.OrganizationRef, wf.flow.Namespace)
if err != nil {
return err
}
_, err = ztdClient.ClearFlow(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.ClearFlowRequest{
Type: wf.flow.Spec.FlowType,
})
if err != nil {
return err
}
return nil
}
func (wr *wrappedFlowFinalizer) patch(ctx context.Context, flow *zitadelv1alpha1.Flow,
patchFn func(*zitadelv1alpha1.Flow)) error {
patch := ctrlClient.MergeFrom(flow.DeepCopy())
patchFn(flow)
if err := wr.Client.Patch(ctx, flow, patch); err != nil {
return fmt.Errorf("error patching Flow finalizer: %v", err)
}
return nil
}

View File

@@ -0,0 +1,417 @@
package controller
import (
"context"
"fmt"
"slices"
"time"
zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/builder"
condition "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/condition"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/filter/v2"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/internal_permission/v2"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/object/v2"
"google.golang.org/protobuf/types/known/timestamppb"
user "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/user/v2"
corev1 "k8s.io/api/core/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"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// MachineUserReconciler reconciles a MachineUser object
type MachineUserReconciler struct {
client.Client
RefResolver *zitadelv1alpha1.RefResolver
ConditionReady *condition.Ready
RequeueInterval time.Duration
Builder *builder.Builder
}
func NewMachineUserReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready,
requeueInterval time.Duration) *MachineUserReconciler {
return &MachineUserReconciler{
Client: client,
RefResolver: refResolver,
ConditionReady: conditionReady,
RequeueInterval: requeueInterval,
Builder: builder,
}
}
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=machineusers,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=machineusers/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=machineusers/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 *MachineUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var MachineUser zitadelv1alpha1.MachineUser
if err := r.Get(ctx, req.NamespacedName, &MachineUser); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
wr := newWrappedMachineUserReconciler(r.Client, r.RefResolver, r.Builder, &MachineUser)
wf := newWrappedMachineUserFinalizer(r.Client, &MachineUser)
tf := core.NewCoreFinalizer(r.Client, wf)
tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval)
result, err := tr.Reconcile(ctx, &MachineUser)
if err != nil {
return result, fmt.Errorf("error reconciling in MachineUserReconciler: %v", err)
}
return result, nil
}
type wrappedMachineUserReconciler struct {
client.Client
refResolver *zitadelv1alpha1.RefResolver
MachineUser *zitadelv1alpha1.MachineUser
Builder *builder.Builder
}
func newWrappedMachineUserReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder,
MachineUser *zitadelv1alpha1.MachineUser) core.WrappedCoreReconciler {
return &wrappedMachineUserReconciler{
Client: client,
refResolver: refResolver,
MachineUser: MachineUser,
Builder: builder,
}
}
type machineUserReconcilePhase struct {
Name string
Reconcile func(context.Context, *clientv2.Client) error
}
func (wr *wrappedMachineUserReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error {
phases := []machineUserReconcilePhase{
{
Name: "machineUser",
Reconcile: wr.reconcileMachineUser,
},
{
Name: "internalPermissions",
Reconcile: wr.reconcileInternalPermissions,
},
{
Name: "pat",
Reconcile: wr.reconcilePAT,
},
// {
// Name: "jwt",
// Reconcile: wr.reconcileJWT,
// },
}
for _, p := range phases {
err := p.Reconcile(ctx, ztdClient)
if err != nil {
return err
}
}
return nil
}
func (wr *wrappedMachineUserReconciler) reconcileMachineUser(ctx context.Context, ztdClient *clientv2.Client) error {
org, err := wr.refResolver.OrganizationRef(ctx, &wr.MachineUser.Spec.OrganizationRef, wr.MachineUser.Namespace)
if err != nil {
return err
}
if org.Status.OrganizationId == nil {
return fmt.Errorf("Organization not created yet")
}
var userId *string
userList, err := ztdClient.UserServiceV2().ListUsers(ctx, &user.ListUsersRequest{
Queries: []*user.SearchQuery{{
Query: &user.SearchQuery_AndQuery{
AndQuery: &user.AndQuery{
Queries: []*user.SearchQuery{
&user.SearchQuery{
Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: wr.MachineUser.Spec.Username,
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
&user.SearchQuery{
Query: &user.SearchQuery_OrganizationIdQuery{
OrganizationIdQuery: &user.OrganizationIdQuery{
OrganizationId: *org.Status.OrganizationId,
},
},
},
},
},
},
}}})
if err != nil {
return fmt.Errorf("error listing users: %v", err)
}
if len(userList.Result) > 0 {
userId = &userList.Result[0].UserId
}
if userId == nil {
accesTokenType := user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER
switch wr.MachineUser.Spec.AccessTokenType {
case "ACCESS_TOKEN_TYPE_BEARER":
accesTokenType = user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER
case "ACCESS_TOKEN_TYPE_JWT":
accesTokenType = user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT
}
ztdClient.UserServiceV2().CreateUser(ctx, &user.CreateUserRequest{
OrganizationId: *org.Status.OrganizationId,
Username: &wr.MachineUser.Spec.Username,
UserType: &user.CreateUserRequest_Machine_{
Machine: &user.CreateUserRequest_Machine{
Name: wr.MachineUser.Spec.Username,
AccessTokenType: accesTokenType,
},
},
})
}
patch := ctrlClient.MergeFrom(wr.MachineUser.DeepCopy())
wr.MachineUser.Status.UserId = userId
return wr.Client.Status().Patch(ctx, wr.MachineUser, patch)
}
func (wr *wrappedMachineUserReconciler) reconcilePAT(ctx context.Context, ztdClient *clientv2.Client) error {
pats, err := ztdClient.UserServiceV2().ListPersonalAccessTokens(ctx, &user.ListPersonalAccessTokensRequest{
Filters: []*user.PersonalAccessTokensSearchFilter{
{
Filter: &user.PersonalAccessTokensSearchFilter_UserIdFilter{
UserIdFilter: &filter.IDFilter{
Id: *wr.MachineUser.Status.UserId,
},
},
},
}})
if err != nil {
return fmt.Errorf("Error getting PAT: %v", err)
}
if pats.Result == nil || !wr.MachineUser.Status.GetConditionStatus(zitadelv1alpha1.ConditionTypePATUpToDate) {
resp, err := ztdClient.UserServiceV2().AddPersonalAccessToken(ctx, &user.AddPersonalAccessTokenRequest{
UserId: *wr.MachineUser.Status.UserId,
ExpirationDate: timestamppb.New(time.Now().AddDate(999, 1, 1)),
})
if err != nil {
return fmt.Errorf("Error adding PAT: %v", err)
}
key := types.NamespacedName{
Name: wr.MachineUser.PatSecretName(),
Namespace: wr.MachineUser.Namespace,
}
desiredPatSecret, err := wr.Builder.BuildSecret(builder.SecretOpts{
Key: key,
Immutable: false,
Data: map[string][]byte{
"pat": []byte(resp.Token),
},
}, wr.MachineUser)
if err != nil {
return fmt.Errorf("error building PAT Secret: %v", err)
}
{
var existingPatSecret corev1.Secret
if err := wr.Get(ctx, key, &existingPatSecret); err != nil {
if errors.IsNotFound(err) {
if err := wr.Create(ctx, desiredPatSecret); err != nil {
return fmt.Errorf("error creating PAT Secret: %v", err)
}
} else {
return fmt.Errorf("error getting PAT Secret: %v", err)
}
} else {
patch := client.MergeFrom(existingPatSecret.DeepCopy())
existingPatSecret.Data = desiredPatSecret.Data
if err = wr.Patch(ctx, &existingPatSecret, patch); err != nil {
return err
}
}
}
if err = wr.PatchStatus(ctx, condition.SetPatUpToDate); err != nil {
return err
}
patch := ctrlClient.MergeFrom(wr.MachineUser.DeepCopy())
wr.MachineUser.Status.PATId = &resp.TokenId
return wr.Client.Status().Patch(ctx, wr.MachineUser, patch)
}
return nil
}
// func (wr *wrappedMachineUserReconciler) reconcileJWT(ctx context.Context, ztdClient *management.Client) error {
// org, err := wr.refResolver.OrganizationRef(ctx, &wr.MachineUser.Spec.OrganizationRef, wr.MachineUser.Namespace)
// if err != nil {
// return err
// }
// ctx = middleware.SetOrgID(ctx, org.Status.OrgId)
// token, err := ztdClient.GetMachineKeyByIDs(ctx, &pb.GetMachineKeyByIDsRequest{
// UserId: wr.MachineUser.Status.UserId,
// KeyId: wr.MachineUser.Status.KeyId,
// })
// if err != nil {
// if !(strings.Contains(err.Error(), "NotFound") || strings.Contains(err.Error(), "length must be between 1 and 200 runes")) {
// return fmt.Errorf("Error getting JWT: %v", err)
// }
// }
// if token == nil {
// resp, err := ztdClient.AddMachineKey(ctx, &pb.AddMachineKeyRequest{
// UserId: wr.MachineUser.Status.UserId,
// Type: authn.KeyType_KEY_TYPE_JSON,
// })
// if err != nil {
// return fmt.Errorf("Error adding JWT: %v", err)
// }
// key := types.NamespacedName{
// Name: wr.MachineUser.JWTSecretName(),
// Namespace: wr.MachineUser.Namespace,
// }
// var jsonKey Key
// if err = json.Unmarshal(resp.KeyDetails, &jsonKey); err != nil {
// return fmt.Errorf("Could not unmarshal key details: %v", err)
// }
// secretData := map[string][]byte{
// "clientId": []byte(jsonKey.ClientID),
// "type": []byte(jsonKey.Type),
// "keyId": []byte(jsonKey.KeyID),
// "appId": []byte(jsonKey.AppID),
// "key": []byte(jsonKey.Key),
// }
// jwtSecret, err := wr.Builder.BuildSecret(builder.SecretOpts{
// Key: key,
// Immutable: false,
// Data: secretData,
// }, wr.MachineUser)
// if err != nil {
// return fmt.Errorf("error building machine key Secret: %v", err)
// }
// if err := wr.Create(ctx, jwtSecret); err != nil {
// return fmt.Errorf("error creating machine key Secret: %v", err)
// }
// patch := ctrlClient.MergeFrom(wr.MachineUser.DeepCopy())
// wr.MachineUser.Status.KeyId = resp.KeyId
// return wr.Client.Status().Patch(ctx, wr.MachineUser, patch)
// }
// return nil
// }
func (wr *wrappedMachineUserReconciler) reconcileInternalPermissions(ctx context.Context, ztdClient *clientv2.Client) error {
for _, permission := range wr.MachineUser.Spec.InternalPermissions {
permissionPbFilter :=
&internal_permission.AdministratorSearchFilter_Resource{
Resource: &internal_permission.ResourceFilter{},
}
permissionPb := &internal_permission.ResourceType{}
if permission.Resource.Instance != nil {
permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_Instance{Instance: true}
permissionPb.Resource = &internal_permission.ResourceType_Instance{Instance: true}
} else if permission.Resource.Organization != nil {
permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_OrganizationId{OrganizationId: permission.Resource.Organization.OrgID}
permissionPb.Resource = &internal_permission.ResourceType_OrganizationId{OrganizationId: permission.Resource.Organization.OrgID}
} else if permission.Resource.ProjectGrant != nil {
permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_ProjectGrant_{ProjectGrant: &internal_permission.ResourceFilter_ProjectGrant{
ProjectId: permission.Resource.ProjectGrant.ProjectID, OrganizationId: permission.Resource.ProjectGrant.ProjectID,
}}
permissionPb.Resource = &internal_permission.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission.ResourceType_ProjectGrant{
ProjectId: permission.Resource.ProjectGrant.ProjectID, OrganizationId: permission.Resource.ProjectGrant.ProjectID,
}}
} else if permission.Resource.Project != nil {
permissionPbFilter.Resource.Resource = &internal_permission.ResourceFilter_ProjectId{ProjectId: permission.Resource.Project.ProjectID}
permissionPb.Resource = &internal_permission.ResourceType_ProjectId{ProjectId: permission.Resource.Project.ProjectID}
}
var admin *internal_permission.Administrator
adminRoleList, err := ztdClient.InternalPermissionServiceV2().ListAdministrators(ctx, &internal_permission.ListAdministratorsRequest{
Filters: []*internal_permission.AdministratorSearchFilter{
{
Filter: permissionPbFilter,
},
{
Filter: &internal_permission.AdministratorSearchFilter_And{And: &internal_permission.AndFilter{Queries: []*internal_permission.AdministratorSearchFilter{
{
Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{
InUserIdsFilter: &filter.InIDsFilter{
Ids: []string{
*wr.MachineUser.Status.UserId,
},
},
},
}},
},
},
},
},
})
if err != nil {
return fmt.Errorf("error listing admin list: %v", err)
}
if len(adminRoleList.Administrators) > 0 {
admin = adminRoleList.Administrators[0]
}
if admin == nil {
_, err := ztdClient.InternalPermissionServiceV2().CreateAdministrator(ctx, &internal_permission.CreateAdministratorRequest{
UserId: *wr.MachineUser.Status.UserId,
Roles: permission.Roles,
Resource: permissionPb,
})
if err != nil {
return fmt.Errorf("error creating admin: %v", err)
}
} else {
uniqueRoles := permission.Roles
uniqueRoles = append(uniqueRoles, admin.Roles...)
uniqueRoles = slices.Compact(uniqueRoles)
_, err := ztdClient.InternalPermissionServiceV2().UpdateAdministrator(ctx, &internal_permission.UpdateAdministratorRequest{
UserId: *wr.MachineUser.Status.UserId,
Roles: uniqueRoles,
Resource: permissionPb,
})
if err != nil {
return fmt.Errorf("error updating admin: %v", err)
}
}
}
return nil
}
func (wr *wrappedMachineUserReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error {
patch := client.MergeFrom(wr.MachineUser.DeepCopy())
patcher(&wr.MachineUser.Status)
if err := wr.Client.Status().Patch(ctx, wr.MachineUser, patch); err != nil {
return fmt.Errorf("error patching MachineUser status: %v", err)
}
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *MachineUserReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&zitadelv1alpha1.MachineUser{}).
Owns(&corev1.Secret{}).
WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,78 @@
package controller
import (
zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core"
"context"
"fmt"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/user/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
const (
machineuserFinalizerName = "machineuser.zitadel.topmanage.com/machineuser"
)
type wrappedMachineUserFinalizer struct {
client.Client
machineuser *zitadelv1alpha1.MachineUser
}
func newWrappedMachineUserFinalizer(client client.Client, machineuser *zitadelv1alpha1.MachineUser) core.WrappedCoreFinalizer {
return &wrappedMachineUserFinalizer{
Client: client,
machineuser: machineuser,
}
}
func (wf *wrappedMachineUserFinalizer) AddFinalizer(ctx context.Context) error {
if wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.machineuser, func(machineuser *zitadelv1alpha1.MachineUser) {
controllerutil.AddFinalizer(machineuser, machineuserFinalizerName)
})
}
func (wf *wrappedMachineUserFinalizer) RemoveFinalizer(ctx context.Context) error {
if !wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.machineuser, func(machineuser *zitadelv1alpha1.MachineUser) {
controllerutil.RemoveFinalizer(wf.machineuser, machineuserFinalizerName)
})
}
func (wr *wrappedMachineUserFinalizer) ContainsFinalizer() bool {
return controllerutil.ContainsFinalizer(wr.machineuser, machineuserFinalizerName)
}
func (wf *wrappedMachineUserFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error {
if wf.machineuser.Status.UserId != nil {
if _, err := ztdClient.UserServiceV2().DeleteUser(ctx,
&user.DeleteUserRequest{
UserId: *wf.machineuser.Status.UserId,
},
); err != nil {
return fmt.Errorf("Error deleting organization: %v", err)
}
}
return nil
}
func (wr *wrappedMachineUserFinalizer) patch(ctx context.Context, machineuser *zitadelv1alpha1.MachineUser,
patchFn func(*zitadelv1alpha1.MachineUser)) error {
patch := ctrlClient.MergeFrom(machineuser.DeepCopy())
patchFn(machineuser)
if err := wr.Client.Patch(ctx, machineuser, patch); err != nil {
return fmt.Errorf("error patching MachineUser finalizer: %v", err)
}
return nil
}

View File

@@ -0,0 +1,263 @@
/*
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-resources-operator/api/v1alpha1"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/builder"
condition "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/condition"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/application/v2"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/filter/v2"
durationpb "google.golang.org/protobuf/types/known/durationpb"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// OIDCAppReconciler reconciles a OIDCApp object
type OIDCAppReconciler struct {
client.Client
RefResolver *zitadelv1alpha1.RefResolver
ConditionReady *condition.Ready
RequeueInterval time.Duration
Builder *builder.Builder
}
func NewOIDCAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready,
requeueInterval time.Duration) *OIDCAppReconciler {
return &OIDCAppReconciler{
Client: client,
RefResolver: refResolver,
ConditionReady: conditionReady,
RequeueInterval: requeueInterval,
Builder: builder,
}
}
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=oidcapps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=oidcapps/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=oidcapps/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 *OIDCAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var OIDCApp zitadelv1alpha1.OIDCApp
if err := r.Get(ctx, req.NamespacedName, &OIDCApp); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
wr := newWrappedOIDCAppReconciler(r.Client, r.RefResolver, r.Builder, &OIDCApp)
wf := newWrappedOIDCAppFinalizer(r.Client, &OIDCApp, r.RefResolver)
tf := core.NewCoreFinalizer(r.Client, wf)
tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval)
result, err := tr.Reconcile(ctx, &OIDCApp)
if err != nil {
return result, fmt.Errorf("error reconciling in OIDCAppReconciler: %v", err)
}
return result, nil
}
type wrappedOIDCAppReconciler struct {
client.Client
refResolver *zitadelv1alpha1.RefResolver
OIDCApp *zitadelv1alpha1.OIDCApp
Builder *builder.Builder
}
func newWrappedOIDCAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder,
OIDCApp *zitadelv1alpha1.OIDCApp) core.WrappedCoreReconciler {
return &wrappedOIDCAppReconciler{
Client: client,
refResolver: refResolver,
OIDCApp: OIDCApp,
Builder: builder,
}
}
func (wr *wrappedOIDCAppReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error {
project, err := wr.OIDCApp.Project(ctx, wr.refResolver)
if err != nil {
return err
}
if project.Status.ProjectId == nil {
return fmt.Errorf("Project has not been created yet...")
}
responseTypes := []application.OIDCResponseType{}
for _, r := range wr.OIDCApp.Spec.ResponseTypes {
responseTypes = append(responseTypes, application.OIDCResponseType(application.OIDCApplicationType_value[string(r)]))
}
grantTypes := []application.OIDCGrantType{}
for _, r := range wr.OIDCApp.Spec.GrantTypes {
grantTypes = append(grantTypes, application.OIDCGrantType(application.OIDCGrantType_value[string(r)]))
}
var appid *string
var clientid *string
appList, err := ztdClient.ApplicationServiceV2().ListApplications(ctx,
&application.ListApplicationsRequest{
Filters: []*application.ApplicationSearchFilter{
{
Filter: &application.ApplicationSearchFilter_NameFilter{
NameFilter: &application.ApplicationNameFilter{
Name: wr.OIDCApp.Spec.OIDCAppName,
Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
{
Filter: &application.ApplicationSearchFilter_ProjectIdFilter{
ProjectIdFilter: &application.ProjectIDFilter{
ProjectId: *project.Status.ProjectId,
},
},
},
},
},
)
if err != nil {
return fmt.Errorf("Error listing OIDCApps: %v", err)
}
if len(appList.Applications) > 0 {
appid = &appList.Applications[0].ApplicationId
clientid = &appList.Applications[0].GetApiConfiguration().ClientId
}
if appid == nil {
resp, err := ztdClient.ApplicationServiceV2().CreateApplication(ctx,
&application.CreateApplicationRequest{
Name: wr.OIDCApp.Name,
ProjectId: *project.Status.ProjectId,
ApplicationType: &application.CreateApplicationRequest_OidcConfiguration{
OidcConfiguration: &application.CreateOIDCApplicationRequest{
RedirectUris: wr.OIDCApp.Spec.RedirectUris,
ResponseTypes: responseTypes,
GrantTypes: grantTypes,
AuthMethodType: application.OIDCAuthMethodType(application.OIDCTokenType_value[wr.OIDCApp.Spec.AuthMethodType]),
PostLogoutRedirectUris: wr.OIDCApp.Spec.PostLogoutRedirectUris,
Version: application.OIDCVersion_OIDC_VERSION_1_0,
DevelopmentMode: wr.OIDCApp.Spec.DevMode,
AccessTokenType: application.OIDCTokenType(application.OIDCTokenType_value[wr.OIDCApp.Spec.AccessTokenType]),
AccessTokenRoleAssertion: wr.OIDCApp.Spec.AccessTokenRoleAssertion,
IdTokenRoleAssertion: wr.OIDCApp.Spec.IdTokenRoleAssertion,
IdTokenUserinfoAssertion: wr.OIDCApp.Spec.IdTokenUserinfoAssertion,
ClockSkew: durationpb.New(wr.OIDCApp.Spec.ClockSkew.Duration),
AdditionalOrigins: wr.OIDCApp.Spec.AdditionalOrigins,
SkipNativeAppSuccessPage: wr.OIDCApp.Spec.SkipNativeAppSuccessPage,
LoginVersion: &application.LoginVersion{
Version: &application.LoginVersion_LoginV2{
LoginV2: &application.LoginV2{
BaseUri: nil,
},
},
},
BackChannelLogoutUri: wr.OIDCApp.Spec.BackChannelLogoutUri,
},
},
},
)
if err != nil {
return fmt.Errorf("error creating OIDCApp in Zitadel: %v", err)
}
key := types.NamespacedName{
Name: wr.OIDCApp.ClientSecretName(),
Namespace: wr.OIDCApp.Namespace,
}
secretData := map[string][]byte{"clientSecret": []byte(resp.GetApiConfiguration().ClientSecret), "appId": []byte(resp.ApplicationId), "clientId": []byte(resp.GetApiConfiguration().ClientId)}
secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: false, Key: key, Data: secretData}, wr.OIDCApp)
if err != nil {
return fmt.Errorf("error building Secret: %v", err)
}
if err := wr.Create(ctx, secret); err != nil {
return fmt.Errorf("error creating Client-secret Secret: %v", err)
}
appid = &resp.ApplicationId
clientid = &resp.GetApiConfiguration().ClientId
} else {
_, err := ztdClient.ApplicationServiceV2().UpdateApplication(ctx,
&application.UpdateApplicationRequest{
Name: wr.OIDCApp.Name,
ProjectId: *project.Status.ProjectId,
ApplicationType: &application.UpdateApplicationRequest_OidcConfiguration{
OidcConfiguration: &application.UpdateOIDCApplicationConfigurationRequest{
RedirectUris: wr.OIDCApp.Spec.RedirectUris,
ResponseTypes: responseTypes,
GrantTypes: grantTypes,
AuthMethodType: ptr.To(application.OIDCAuthMethodType(application.OIDCTokenType_value[wr.OIDCApp.Spec.AuthMethodType])),
PostLogoutRedirectUris: wr.OIDCApp.Spec.PostLogoutRedirectUris,
Version: ptr.To(application.OIDCVersion_OIDC_VERSION_1_0),
DevelopmentMode: &wr.OIDCApp.Spec.DevMode,
AccessTokenType: ptr.To(application.OIDCTokenType(application.OIDCTokenType_value[wr.OIDCApp.Spec.AccessTokenType])),
AccessTokenRoleAssertion: &wr.OIDCApp.Spec.AccessTokenRoleAssertion,
IdTokenRoleAssertion: &wr.OIDCApp.Spec.IdTokenRoleAssertion,
IdTokenUserinfoAssertion: &wr.OIDCApp.Spec.IdTokenUserinfoAssertion,
ClockSkew: durationpb.New(wr.OIDCApp.Spec.ClockSkew.Duration),
AdditionalOrigins: wr.OIDCApp.Spec.AdditionalOrigins,
SkipNativeAppSuccessPage: &wr.OIDCApp.Spec.SkipNativeAppSuccessPage,
LoginVersion: &application.LoginVersion{
Version: &application.LoginVersion_LoginV2{
LoginV2: &application.LoginV2{
BaseUri: nil,
},
},
},
BackChannelLogoutUri: &wr.OIDCApp.Spec.BackChannelLogoutUri,
},
},
},
)
if err != nil {
return fmt.Errorf("error updating OIDCApp in Zitadel: %v", err)
}
}
patch := ctrlClient.MergeFrom(wr.OIDCApp.DeepCopy())
wr.OIDCApp.Status.AppId = appid
wr.OIDCApp.Status.ClientId = clientid
return wr.Client.Status().Patch(ctx, wr.OIDCApp, patch)
}
func (wr *wrappedOIDCAppReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error {
patch := client.MergeFrom(wr.OIDCApp.DeepCopy())
patcher(&wr.OIDCApp.Status)
if err := wr.Client.Status().Patch(ctx, wr.OIDCApp, patch); err != nil {
return fmt.Errorf("error patching OIDCApp status: %v", err)
}
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *OIDCAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&zitadelv1alpha1.OIDCApp{}).
Owns(&corev1.Secret{}).
WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,88 @@
package controller
import (
"strings"
zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core"
"context"
"fmt"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/application/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
const (
OIDCAppFinalizerName = "oidcapp.zitadel.topmanage.com/oidcapp"
)
type wrappedOIDCAppFinalizer struct {
client.Client
OIDCApp *zitadelv1alpha1.OIDCApp
refresolver *zitadelv1alpha1.RefResolver
}
func newWrappedOIDCAppFinalizer(client client.Client, OIDCApp *zitadelv1alpha1.OIDCApp, refresolver *zitadelv1alpha1.RefResolver) core.WrappedCoreFinalizer {
return &wrappedOIDCAppFinalizer{
Client: client,
OIDCApp: OIDCApp,
refresolver: refresolver,
}
}
func (wf *wrappedOIDCAppFinalizer) AddFinalizer(ctx context.Context) error {
if wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.OIDCApp, func(OIDCApp *zitadelv1alpha1.OIDCApp) {
controllerutil.AddFinalizer(OIDCApp, OIDCAppFinalizerName)
})
}
func (wf *wrappedOIDCAppFinalizer) RemoveFinalizer(ctx context.Context) error {
if !wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.OIDCApp, func(OIDCApp *zitadelv1alpha1.OIDCApp) {
controllerutil.RemoveFinalizer(wf.OIDCApp, OIDCAppFinalizerName)
})
}
func (wr *wrappedOIDCAppFinalizer) ContainsFinalizer() bool {
return controllerutil.ContainsFinalizer(wr.OIDCApp, OIDCAppFinalizerName)
}
func (wf *wrappedOIDCAppFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error {
if wf.OIDCApp.Status.AppId != nil {
project, err := wf.OIDCApp.Project(ctx, wf.refresolver)
if err != nil {
return err
}
_, err = ztdClient.ApplicationServiceV2().DeleteApplication(ctx, &application.DeleteApplicationRequest{
ApplicationId: *wf.OIDCApp.Status.AppId,
ProjectId: *project.Status.ProjectId,
})
if err != nil {
if strings.Contains(err.Error(), "doesn't exist") {
return nil
}
return err
}
}
return nil
}
func (wr *wrappedOIDCAppFinalizer) patch(ctx context.Context, OIDCApp *zitadelv1alpha1.OIDCApp,
patchFn func(*zitadelv1alpha1.OIDCApp)) error {
patch := ctrlClient.MergeFrom(OIDCApp.DeepCopy())
patchFn(OIDCApp)
if err := wr.Client.Patch(ctx, OIDCApp, patch); err != nil {
return fmt.Errorf("error patching OIDCApp finalizer: %v", err)
}
return nil
}

View File

@@ -0,0 +1,167 @@
/*
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-resources-operator/api/v1alpha1"
condition "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/condition"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/object/v2"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/org/v2"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// OrganizationReconciler reconciles a Organization object
type OrganizationReconciler struct {
client.Client
RefResolver *zitadelv1alpha1.RefResolver
ConditionReady *condition.Ready
RequeueInterval time.Duration
}
func NewOrganizationReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, conditionReady *condition.Ready,
requeueInterval time.Duration) *OrganizationReconciler {
return &OrganizationReconciler{
Client: client,
RefResolver: refResolver,
ConditionReady: conditionReady,
RequeueInterval: requeueInterval,
}
}
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=organizations,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=organizations/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=zitadel.topmanage.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 *OrganizationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var organization zitadelv1alpha1.Organization
if err := r.Get(ctx, req.NamespacedName, &organization); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
wr := newWrappedOrganizationReconciler(r.Client, r.RefResolver, &organization)
wf := newWrappedOrganizationFinalizer(r.Client, &organization)
tf := core.NewCoreFinalizer(r.Client, wf)
tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval)
result, err := tr.Reconcile(ctx, &organization)
if err != nil {
return result, fmt.Errorf("error reconciling in OrganizationReconciler: %v", err)
}
return result, nil
}
type wrappedOrganizationReconciler struct {
client.Client
refResolver *zitadelv1alpha1.RefResolver
organization *zitadelv1alpha1.Organization
}
func newWrappedOrganizationReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver,
organization *zitadelv1alpha1.Organization) core.WrappedCoreReconciler {
return &wrappedOrganizationReconciler{
Client: client,
refResolver: refResolver,
organization: organization,
}
}
type orgReconcilePhase struct {
Name string
Reconcile func(context.Context, *clientv2.Client) error
}
func (wr *wrappedOrganizationReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error {
phases := []orgReconcilePhase{
{
Name: "organization",
Reconcile: wr.reconcileOrg,
},
}
for _, p := range phases {
err := p.Reconcile(ctx, ztdClient)
if err != nil {
return err
}
}
return nil
}
func (wr *wrappedOrganizationReconciler) reconcileOrg(ctx context.Context, ztdClient *clientv2.Client) error {
var organizationId *string
orgList, err := ztdClient.OrganizationServiceV2().ListOrganizations(ctx, &org.ListOrganizationsRequest{
Queries: []*org.SearchQuery{
{
Query: &org.SearchQuery_NameQuery{
NameQuery: &org.OrganizationNameQuery{
Name: wr.organization.Spec.OrganzationName,
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
})
if err != nil {
return fmt.Errorf("error listing organization: %v", err)
}
if len(orgList.Result) > 0 {
organizationId = &orgList.Result[0].Id
}
if organizationId == nil {
resp, err :=
ztdClient.OrganizationServiceV2().AddOrganization(ctx, &org.AddOrganizationRequest{
Name: wr.organization.Spec.OrganzationName,
})
if err != nil {
return fmt.Errorf("error creating organization: %v", err)
}
organizationId = &resp.OrganizationId
}
patch := ctrlClient.MergeFrom(wr.organization.DeepCopy())
wr.organization.Status.OrganizationId = organizationId
return wr.Client.Status().Patch(ctx, wr.organization, patch)
}
func (wr *wrappedOrganizationReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error {
patch := client.MergeFrom(wr.organization.DeepCopy())
patcher(&wr.organization.Status)
if err := wr.Client.Status().Patch(ctx, wr.organization, patch); err != nil {
return fmt.Errorf("error patching Organization status: %v", err)
}
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *OrganizationReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&zitadelv1alpha1.Organization{}).
WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,77 @@
package controller
import (
zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core"
"context"
"fmt"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/org/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
const (
organizationFinalizerName = "organization.zitadel.github.com/organization"
)
type wrappedOrganizationFinalizer struct {
client.Client
organization *zitadelv1alpha1.Organization
}
func newWrappedOrganizationFinalizer(client client.Client, organization *zitadelv1alpha1.Organization) core.WrappedCoreFinalizer {
return &wrappedOrganizationFinalizer{
Client: client,
organization: organization,
}
}
func (wf *wrappedOrganizationFinalizer) AddFinalizer(ctx context.Context) error {
if wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.organization, func(organization *zitadelv1alpha1.Organization) {
controllerutil.AddFinalizer(organization, organizationFinalizerName)
})
}
func (wf *wrappedOrganizationFinalizer) RemoveFinalizer(ctx context.Context) error {
if !wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.organization, func(organization *zitadelv1alpha1.Organization) {
controllerutil.RemoveFinalizer(wf.organization, organizationFinalizerName)
})
}
func (wr *wrappedOrganizationFinalizer) ContainsFinalizer() bool {
return controllerutil.ContainsFinalizer(wr.organization, organizationFinalizerName)
}
func (wf *wrappedOrganizationFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error {
if wf.organization.Status.OrganizationId != nil {
if _, err := ztdClient.OrganizationServiceV2().DeleteOrganization(ctx,
&org.DeleteOrganizationRequest{
OrganizationId: *wf.organization.Status.OrganizationId,
},
); err != nil {
return fmt.Errorf("Error deleting organization: %v", err)
}
}
return nil
}
func (wr *wrappedOrganizationFinalizer) patch(ctx context.Context, organization *zitadelv1alpha1.Organization,
patchFn func(*zitadelv1alpha1.Organization)) error {
patch := ctrlClient.MergeFrom(organization.DeepCopy())
patchFn(organization)
if err := wr.Client.Patch(ctx, organization, patch); err != nil {
return fmt.Errorf("error patching Organization finalizer: %v", err)
}
return nil
}

View File

@@ -0,0 +1,314 @@
/*
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"
"reflect"
"sort"
"time"
zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1"
condition "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/condition"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/filter/v2"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/project/v2"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// ProjectReconciler reconciles a Project object
type ProjectReconciler struct {
client.Client
RefResolver *zitadelv1alpha1.RefResolver
ConditionReady *condition.Ready
RequeueInterval time.Duration
}
func NewProjectReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, conditionReady *condition.Ready,
requeueInterval time.Duration) *ProjectReconciler {
return &ProjectReconciler{
Client: client,
RefResolver: refResolver,
ConditionReady: conditionReady,
RequeueInterval: requeueInterval,
}
}
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=projects,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=projects/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=projects/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 *ProjectReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var project zitadelv1alpha1.Project
if err := r.Get(ctx, req.NamespacedName, &project); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
wr := newWrappedProjectReconciler(r.Client, r.RefResolver, &project)
wf := newWrappedProjectFinalizer(r.Client, &project, r.RefResolver)
tf := core.NewCoreFinalizer(r.Client, wf)
tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval)
result, err := tr.Reconcile(ctx, &project)
if err != nil {
return result, fmt.Errorf("error reconciling in ProjectReconciler: %v", err)
}
return result, nil
}
type wrappedProjectReconciler struct {
client.Client
refResolver *zitadelv1alpha1.RefResolver
project *zitadelv1alpha1.Project
}
func newWrappedProjectReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver,
project *zitadelv1alpha1.Project) core.WrappedCoreReconciler {
return &wrappedProjectReconciler{
Client: client,
refResolver: refResolver,
project: project,
}
}
type projectReconcilePhase struct {
Name string
Reconcile func(context.Context, *clientv2.Client) error
}
func (wr *wrappedProjectReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error {
phases := []projectReconcilePhase{
{
Name: "project",
Reconcile: wr.reconcileProject,
},
{
Name: "roles",
Reconcile: wr.reconcileRoles,
},
{
Name: "grants",
Reconcile: wr.reconcileGrants,
},
}
for _, p := range phases {
err := p.Reconcile(ctx, ztdClient)
if err != nil {
return err
}
}
return nil
}
func (wr *wrappedProjectReconciler) reconcileProject(ctx context.Context, ztdClient *clientv2.Client) error {
org, err := wr.refResolver.OrganizationRef(ctx, &wr.project.Spec.OrganizationRef, wr.project.Namespace)
if err != nil {
return err
}
if org.Status.OrganizationId == nil {
return fmt.Errorf("Organization not created yet")
}
var projectId *string
projectList, err := ztdClient.ProjectServiceV2().ListProjects(ctx, &project.ListProjectsRequest{
Filters: []*project.ProjectSearchFilter{
&project.ProjectSearchFilter{
Filter: &project.ProjectSearchFilter_ProjectNameFilter{
ProjectNameFilter: &project.ProjectNameFilter{
ProjectName: wr.project.Spec.ProjectName,
Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
&project.ProjectSearchFilter{
Filter: &project.ProjectSearchFilter_OrganizationIdFilter{
OrganizationIdFilter: &project.ProjectOrganizationIDFilter{
OrganizationId: *org.Status.OrganizationId,
Type: project.ProjectOrganizationIDFilter_OWNED,
},
},
},
},
})
if err != nil {
return fmt.Errorf("error listing project: %v", err)
}
if len(projectList.Projects) > 0 {
projectId = &projectList.Projects[0].ProjectId
}
if projectId == nil {
resp, err :=
ztdClient.ProjectServiceV2().CreateProject(ctx,
&project.CreateProjectRequest{
OrganizationId: *org.Status.OrganizationId,
Name: wr.project.Spec.ProjectName,
ProjectRoleAssertion: wr.project.Spec.ProjectRoleAssertion,
AuthorizationRequired: wr.project.Spec.ProjectRoleCheck,
ProjectAccessRequired: wr.project.Spec.HasProjectCheck,
},
)
if err != nil {
return fmt.Errorf("error creating project: %v", err)
}
projectId = &resp.ProjectId
}
patch := ctrlClient.MergeFrom(wr.project.DeepCopy())
wr.project.Status.ProjectId = projectId
return wr.Client.Status().Patch(ctx, wr.project, patch)
}
func (wr *wrappedProjectReconciler) reconcileRoles(ctx context.Context, ztdClient *clientv2.Client) error {
resp, err := ztdClient.ProjectServiceV2().ListProjectRoles(ctx, &project.ListProjectRolesRequest{
ProjectId: *wr.project.Status.ProjectId,
})
if err != nil {
return fmt.Errorf("Could not list project roles: %v", err)
}
roles := map[string]*project.ProjectRole{}
deleteRolesKeys := []string{}
for _, role := range wr.project.Spec.Roles {
roles[role.Key] = &project.ProjectRole{
Key: role.Key,
DisplayName: role.DisplayName,
Group: role.Group,
ProjectId: *wr.project.Status.ProjectId,
}
}
for _, role := range resp.ProjectRoles {
if r, ok := roles[role.Key]; ok {
if r.DisplayName != role.DisplayName || r.Group != role.Group {
deleteRolesKeys = append(deleteRolesKeys, role.Key)
} else {
delete(roles, role.Key)
}
} else {
deleteRolesKeys = append(deleteRolesKeys, role.Key)
}
}
if len(deleteRolesKeys) > 0 {
for _, key := range deleteRolesKeys {
if _, err = ztdClient.ProjectServiceV2().RemoveProjectRole(ctx, &project.RemoveProjectRoleRequest{
ProjectId: *wr.project.Status.ProjectId,
RoleKey: key,
}); err != nil {
return fmt.Errorf("Error removing project role: %v", err)
}
}
}
if len(roles) > 0 {
for _, value := range roles {
if _, err = ztdClient.ProjectServiceV2().AddProjectRole(ctx, &project.AddProjectRoleRequest{
ProjectId: *wr.project.Status.ProjectId,
RoleKey: value.Key,
DisplayName: value.DisplayName,
Group: &value.Group,
}); err != nil {
return fmt.Errorf("Error adding project role: %v", err)
}
}
}
return nil
}
func (wr *wrappedProjectReconciler) reconcileGrants(ctx context.Context, ztdClient *clientv2.Client) error {
existingGrants, err := ztdClient.ProjectServiceV2().ListProjectGrants(ctx, &project.ListProjectGrantsRequest{
Filters: []*project.ProjectGrantSearchFilter{
{
Filter: &project.ProjectGrantSearchFilter_InProjectIdsFilter{
InProjectIdsFilter: &filter.InIDsFilter{
Ids: []string{
*wr.project.Status.ProjectId,
},
},
},
},
},
})
if err != nil {
return fmt.Errorf("Error listing project grants: %v", err)
}
for _, grant := range wr.project.DeepCopy().Spec.Grants {
grantedOrg, err := wr.refResolver.OrganizationRef(ctx, &grant.OrganizationRef, wr.project.Namespace)
if err != nil {
return err
}
if grantedOrg.Status.OrganizationId == nil {
continue
}
var existingGrant *project.ProjectGrant
for _, eGrant := range existingGrants.ProjectGrants {
if eGrant.GrantedOrganizationId == *grantedOrg.Status.OrganizationId {
existingGrant = eGrant
break
}
}
if existingGrant == nil {
_, err := ztdClient.ProjectServiceV2().CreateProjectGrant(ctx, &project.CreateProjectGrantRequest{
ProjectId: *wr.project.Status.ProjectId,
GrantedOrganizationId: *grantedOrg.Status.OrganizationId,
RoleKeys: grant.RoleKeys,
})
if err != nil {
return fmt.Errorf("Error Adding project grant: %v", err)
}
} else {
sort.Strings(existingGrant.GrantedRoleKeys)
sort.Strings(grant.RoleKeys)
if !reflect.DeepEqual(existingGrant.GrantedRoleKeys, grant.RoleKeys) {
_, err := ztdClient.ProjectServiceV2().UpdateProjectGrant(ctx, &project.UpdateProjectGrantRequest{
ProjectId: *wr.project.Status.ProjectId,
GrantedOrganizationId: existingGrant.GrantedOrganizationId,
RoleKeys: grant.RoleKeys,
})
if err != nil {
return fmt.Errorf("Error Updating project grant: %v", err)
}
}
}
}
return nil
}
func (wr *wrappedProjectReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error {
patch := client.MergeFrom(wr.project.DeepCopy())
patcher(&wr.project.Status)
if err := wr.Client.Status().Patch(ctx, wr.project, patch); err != nil {
return fmt.Errorf("error patching Project status: %v", err)
}
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *ProjectReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&zitadelv1alpha1.Project{}).
WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,76 @@
package controller
import (
zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1"
"gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core"
"context"
"fmt"
clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/project/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
const (
projectFinalizerName = "project.zitadel.github.com/project"
)
type wrappedProjectFinalizer struct {
client.Client
project *zitadelv1alpha1.Project
refresolver *zitadelv1alpha1.RefResolver
}
func newWrappedProjectFinalizer(client client.Client, project *zitadelv1alpha1.Project, refresolver *zitadelv1alpha1.RefResolver) core.WrappedCoreFinalizer {
return &wrappedProjectFinalizer{
Client: client,
project: project,
refresolver: refresolver,
}
}
func (wf *wrappedProjectFinalizer) AddFinalizer(ctx context.Context) error {
if wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.project, func(project *zitadelv1alpha1.Project) {
controllerutil.AddFinalizer(project, projectFinalizerName)
})
}
func (wf *wrappedProjectFinalizer) RemoveFinalizer(ctx context.Context) error {
if !wf.ContainsFinalizer() {
return nil
}
return wf.patch(ctx, wf.project, func(project *zitadelv1alpha1.Project) {
controllerutil.RemoveFinalizer(wf.project, projectFinalizerName)
})
}
func (wr *wrappedProjectFinalizer) ContainsFinalizer() bool {
return controllerutil.ContainsFinalizer(wr.project, projectFinalizerName)
}
func (wf *wrappedProjectFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error {
if wf.project.Status.ProjectId != nil {
_, err := ztdClient.ProjectServiceV2().DeleteProject(ctx, &project.DeleteProjectRequest{ProjectId: *wf.project.Status.ProjectId})
if err != nil {
return err
}
}
return nil
}
func (wr *wrappedProjectFinalizer) patch(ctx context.Context, project *zitadelv1alpha1.Project,
patchFn func(*zitadelv1alpha1.Project)) error {
patch := ctrlClient.MergeFrom(project.DeepCopy())
patchFn(project)
if err := wr.Client.Patch(ctx, project, patch); err != nil {
return fmt.Errorf("error patching Project finalizer: %v", err)
}
return nil
}

View File

@@ -0,0 +1,80 @@
/*
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 (
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1"
//+kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}
var err error
// cfg is defined in this file globally.
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
err = zitadelv1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
//+kubebuilder:scaffold:scheme
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
})
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})