Initial commit

[ZITADOPER-1]
This commit is contained in:
Haim Kortovich
2024-04-15 14:44:46 -05:00
parent 95e7d1cb69
commit e4eef2928a
121 changed files with 9053 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
/*
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 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/builder"
condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel"
"github.com/zitadel/zitadel-go/v2/pkg/client/management"
"github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
app "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/app"
pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management"
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"
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"
)
// 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 := zitadel.NewZitadelFinalizer(r.Client, wf)
tr := zitadel.NewZitadelReconciler(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) zitadel.WrappedReconciler {
return &wrappedOIDCAppReconciler{
Client: client,
refResolver: refResolver,
OIDCApp: OIDCApp,
Builder: builder,
}
}
func (wr *wrappedOIDCAppReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error {
org, err := wr.OIDCApp.Organization(ctx, wr.refResolver)
if err != nil {
return err
}
project, err := wr.OIDCApp.Project(ctx, wr.refResolver)
if err != nil {
return err
}
responseTypes := []app.OIDCResponseType{}
for _, r := range wr.OIDCApp.Spec.ResponseTypes {
responseTypes = append(responseTypes, app.OIDCResponseType(app.OIDCResponseType_value[string(r)]))
}
grantTypes := []app.OIDCGrantType{}
for _, r := range wr.OIDCApp.Spec.GrantTypes {
grantTypes = append(grantTypes, app.OIDCGrantType(app.OIDCGrantType_value[string(r)]))
}
if wr.OIDCApp.Status.AppId != "" {
appResp, err := ztdClient.GetAppByID(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppByIDRequest{
ProjectId: project.Status.ProjectId,
AppId: string(wr.OIDCApp.Status.AppId),
})
if err != nil {
return fmt.Errorf("Error getting OIDCApp: %v", err)
}
if appResp.App != nil {
_, err := ztdClient.UpdateOIDCAppConfig(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.UpdateOIDCAppConfigRequest{ProjectId: project.Status.ProjectId, AppId: wr.OIDCApp.Status.AppId,
RedirectUris: wr.OIDCApp.Spec.RedirectUris,
ResponseTypes: responseTypes,
GrantTypes: grantTypes,
AppType: app.OIDCAppType(app.OIDCAppType_value[wr.OIDCApp.Spec.AppType]),
AuthMethodType: app.OIDCAuthMethodType(app.OIDCAuthMethodType_value[wr.OIDCApp.Spec.AuthMethodType]),
PostLogoutRedirectUris: wr.OIDCApp.Spec.PostLogoutRedirectUris,
DevMode: wr.OIDCApp.Spec.DevMode,
AccessTokenType: app.OIDCTokenType(app.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,
})
if err != nil {
if !strings.Contains(err.Error(), "No changes") {
return fmt.Errorf("Error updating OIDCApp: %v", err)
}
}
return nil
}
}
resp, err := ztdClient.AddOIDCApp(middleware.SetOrgID(ctx, org.Status.OrgId),
&pb.AddOIDCAppRequest{
Name: wr.OIDCApp.Name,
ProjectId: project.Status.ProjectId,
RedirectUris: wr.OIDCApp.Spec.RedirectUris,
ResponseTypes: responseTypes,
GrantTypes: grantTypes,
AppType: app.OIDCAppType(app.OIDCAppType_value[wr.OIDCApp.Spec.AppType]),
AuthMethodType: app.OIDCAuthMethodType(app.OIDCAuthMethodType_value[wr.OIDCApp.Spec.AuthMethodType]),
PostLogoutRedirectUris: wr.OIDCApp.Spec.PostLogoutRedirectUris,
Version: app.OIDCVersion_OIDC_VERSION_1_0,
DevMode: wr.OIDCApp.Spec.DevMode,
AccessTokenType: app.OIDCTokenType(app.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,
},
)
if err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
return nil
}
return fmt.Errorf("error creating OIDCApp in Zitadel: %v", err)
}
key := types.NamespacedName{
Name: wr.OIDCApp.Name + "-client-secret",
Namespace: wr.OIDCApp.Namespace,
}
secretData := map[string][]byte{"client-secret": []byte(resp.ClientSecret)}
secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: true, Zitadel: nil, 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)
}
patch := ctrlClient.MergeFrom(wr.OIDCApp.DeepCopy())
wr.OIDCApp.Status.AppId = resp.AppId
wr.OIDCApp.Status.ClientId = resp.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.NewItemExponentialFailureRateLimiter(time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,86 @@
package controller
import (
zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel"
"context"
"fmt"
"github.com/zitadel/zitadel-go/v2/pkg/client/management"
"github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
pb "github.com/zitadel/zitadel-go/v2/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 (
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) zitadel.WrappedFinalizer {
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 *management.Client) error {
if wf.OIDCApp.Status.AppId == "" {
return nil
}
org, err := wf.OIDCApp.Organization(ctx, wf.refresolver)
if err != nil {
return err
}
project, err := wf.OIDCApp.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.OIDCApp.Status.AppId})
if err != 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,122 @@
/*
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 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1"
condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel"
"github.com/zitadel/zitadel-go/v2/pkg/client/management"
pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management"
"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"
)
// 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 := zitadel.NewZitadelFinalizer(r.Client, wf)
tr := zitadel.NewZitadelReconciler(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) zitadel.WrappedReconciler {
return &wrappedOrganizationReconciler{
Client: client,
refResolver: refResolver,
organization: organization,
}
}
func (wr *wrappedOrganizationReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error {
resp, err := ztdClient.AddOrg(ctx, &pb.AddOrgRequest{Name: wr.organization.Name})
if err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
return nil
}
return fmt.Errorf("error creating organization in Zitadel: %v", err)
}
patch := ctrlClient.MergeFrom(wr.organization.DeepCopy())
wr.organization.Status.OrgId = resp.Id
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.NewItemExponentialFailureRateLimiter(time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,87 @@
package controller
import (
"strings"
zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel"
"context"
"fmt"
"github.com/zitadel/zitadel-go/v2/pkg/client/management"
"github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
pb "github.com/zitadel/zitadel-go/v2/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 (
organizationFinalizerName = "organization.zitadel.topmanage.com/organization"
)
type wrappedOrganizationFinalizer struct {
client.Client
organization *zitadelv1alpha1.Organization
}
func newWrappedOrganizationFinalizer(client client.Client, organization *zitadelv1alpha1.Organization) zitadel.WrappedFinalizer {
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 *management.Client) error {
if wf.organization.Status.OrgId == "" {
return nil
}
{
_, err := ztdClient.GetMyOrg(middleware.SetOrgID(ctx, wf.organization.Status.OrgId), &pb.GetMyOrgRequest{})
if err != nil {
if strings.Contains(err.Error(), `Organisation doesn't exist`) {
return nil
}
return err
}
}
_, err := ztdClient.RemoveOrg(middleware.SetOrgID(ctx, wf.organization.Status.OrgId), &pb.RemoveOrgRequest{})
if err != nil {
return 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,157 @@
/*
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 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1"
condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel"
"github.com/zitadel/zitadel-go/v2/pkg/client/management"
"github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management"
"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"
)
// 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 := zitadel.NewZitadelFinalizer(r.Client, wf)
tr := zitadel.NewZitadelReconciler(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) zitadel.WrappedReconciler {
return &wrappedProjectReconciler{
Client: client,
refResolver: refResolver,
project: project,
}
}
func (wr *wrappedProjectReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error {
org, err := wr.refResolver.OrganizationRef(ctx, &wr.project.Spec.OrganizationRef, wr.project.Namespace)
if err != nil {
return err
}
{
_, err := ztdClient.GetProjectByID(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetProjectByIDRequest{Id: wr.project.Status.ProjectId})
if err != nil {
if !strings.Contains(err.Error(), "not found") {
return fmt.Errorf("Error getting Project: %v", err)
}
} else {
_, err := ztdClient.UpdateProject(middleware.SetOrgID(ctx, org.Status.OrgId),
&pb.UpdateProjectRequest{
Id: wr.project.Status.ProjectId,
Name: wr.project.Name,
ProjectRoleAssertion: wr.project.Spec.ProjectRoleAssertion,
ProjectRoleCheck: wr.project.Spec.ProjectRoleAssertion,
HasProjectCheck: wr.project.Spec.HasProjectCheck},
)
if err != nil {
if !strings.Contains(err.Error(), "No changes") {
return fmt.Errorf("Error updating Project: %v", err)
}
}
return nil
}
}
resp, err := ztdClient.AddProject(middleware.SetOrgID(ctx, org.Status.OrgId),
&pb.AddProjectRequest{
Name: wr.project.Name,
ProjectRoleAssertion: wr.project.Spec.ProjectRoleAssertion,
ProjectRoleCheck: wr.project.Spec.ProjectRoleAssertion,
HasProjectCheck: wr.project.Spec.HasProjectCheck},
)
if err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
return nil
}
return fmt.Errorf("error creating project in Zitadel: %v", err)
}
patch := ctrlClient.MergeFrom(wr.project.DeepCopy())
wr.project.Status.ProjectId = resp.Id
return wr.Client.Status().Patch(ctx, wr.project, patch)
}
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.NewItemExponentialFailureRateLimiter(time.Millisecond*500, time.Minute*3)}).
Complete(r)
}

View File

@@ -0,0 +1,94 @@
package controller
import (
"strings"
zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel"
"context"
"fmt"
"github.com/zitadel/zitadel-go/v2/pkg/client/management"
"github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
pb "github.com/zitadel/zitadel-go/v2/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 (
projectFinalizerName = "project.zitadel.topmanage.com/project"
)
type wrappedProjectFinalizer struct {
client.Client
project *zitadelv1alpha1.Project
refresolver *zitadelv1alpha1.RefResolver
}
func newWrappedProjectFinalizer(client client.Client, project *zitadelv1alpha1.Project, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer {
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 *management.Client) error {
if wf.project.Status.ProjectId == "" {
return nil
}
org, err := wf.refresolver.OrganizationRef(ctx, &wf.project.Spec.OrganizationRef, wf.project.Namespace)
if err != nil {
return err
}
{
_, err := ztdClient.GetProjectByID(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetProjectByIDRequest{Id: wf.project.Status.ProjectId})
if err != nil {
if strings.Contains(err.Error(), `doesn't exist`) {
return nil
}
return err
}
}
_, err = ztdClient.RemoveProject(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.RemoveProjectRequest{Id: 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 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/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())
})

View File

@@ -0,0 +1,473 @@
/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"strings"
"time"
zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1"
builder "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/builder"
condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/configuration"
configmap "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/configmap"
secret "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/secret"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/service"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/deployment"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/machinekey"
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/masterkey"
systemapiaccount "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/systemapi"
zitadelClient "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/zitadel"
"github.com/hashicorp/go-multierror"
"github.com/zitadel/zitadel-go/v2/pkg/client/system"
authn "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/authn"
pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/system"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/log"
)
type reconcilePhase struct {
Name string
Reconcile func(context.Context, *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error)
}
type patcher func(*zitadelv1alpha1.ZitadelClusterStatus) error
// ZitadelClusterReconciler reconciles a ZitadelCluster object
type ZitadelClusterReconciler struct {
client.Client
Scheme *runtime.Scheme
ConditionReady *condition.Ready
Builder *builder.Builder
SecretReconciler *secret.SecretReconciler
ConfigMapReconciler *configmap.ConfigMapReconciler
ServiceReconciler *service.ServiceReconciler
RefResolver *zitadelv1alpha1.RefResolver
}
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;patch
// +kubebuilder:rbac:groups="",resources=services,verbs=list;watch;create;patch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=list;watch;create;patch
// +kubebuilder:rbac:groups="",resources=endpoints,verbs=create;patch;get;list;watch
// +kubebuilder:rbac:groups="",resources=endpoints/restricted,verbs=create;patch;get;list;watch
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;delete
// +kubebuilder:rbac:groups="",resources=events,verbs=list;watch;create;patch
// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=list;watch;create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;create;patch
// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=list;watch;create;patch
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings;clusterrolebindings,verbs=list;watch;create;patch
// +kubebuilder:rbac:groups=zitadel.topmanage.com,resources=zitadelclusters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=zitadel.topmanage.com,resources=zitadelclusters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=zitadel.topmanage.com,resources=zitadelclusters/finalizers,verbs=update
// +kubebuilder:rbac:groups=crdb.cockroachlabs.com,resources=crdbclusters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=crdb.cockroachlabs.com,resources=crdbclusters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=crdb.cockroachlabs.com,resources=crdbclusters/finalizers,verbs=update
// +kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests,verbs=get;list;watch;create;patch;delete
// +kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests/approval,verbs=update
func (r *ZitadelClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
logger.Info("Starting Reconcile")
var zitadel zitadelv1alpha1.ZitadelCluster
if err := r.Get(ctx, req.NamespacedName, &zitadel); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if err := r.patchStatus(ctx, &zitadel, r.patcher(ctx, &zitadel)); err != nil && !errors.IsNotFound(err) {
return ctrl.Result{}, err
}
phases := []reconcilePhase{
{
Name: "Spec",
Reconcile: r.setSpecDefaults,
},
{
Name: "Status",
Reconcile: r.setStatusDefaults,
},
{
Name: "MasterkeySecret",
Reconcile: r.reconcileMasterKeySecret,
},
{
Name: "ServiceAccount",
Reconcile: r.reconcileSystemAPIUser,
},
{
Name: "Configuration",
Reconcile: r.reconcileConfig,
},
{
Name: "InitJob",
Reconcile: r.reconcileInitJob,
},
{
Name: "SetupJob",
Reconcile: r.reconcileSetupJob,
},
{
Name: "Deployment",
Reconcile: r.reconcileDeployment,
},
{
Name: "Service",
Reconcile: r.reconcileService,
},
{
Name: "DefaultInstance",
Reconcile: r.reconcileDefaultInstance,
},
}
for _, p := range phases {
result, err := p.Reconcile(ctx, &zitadel)
if err != nil {
if errors.IsNotFound(err) {
continue
}
var errBundle *multierror.Error
errBundle = multierror.Append(errBundle, err)
msg := fmt.Sprintf("Error reconciling %s: %v", p.Name, err)
patchErr := r.patchStatus(ctx, &zitadel, func(s *zitadelv1alpha1.ZitadelClusterStatus) error {
patcher := r.ConditionReady.PatcherFailed(msg)
patcher(s)
return nil
})
if errors.IsNotFound(patchErr) {
errBundle = multierror.Append(errBundle, patchErr)
}
if err := errBundle.ErrorOrNil(); err != nil {
return ctrl.Result{}, fmt.Errorf("error reconciling %s: %v", p.Name, err)
}
}
if !result.IsZero() {
return result, err
}
}
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
}
func (r *ZitadelClusterReconciler) setSpecDefaults(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
return ctrl.Result{}, r.patch(ctx, zitadel, func(zit *zitadelv1alpha1.ZitadelCluster) {
zit.SetDefaults()
})
}
func (r *ZitadelClusterReconciler) setStatusDefaults(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
return ctrl.Result{}, r.patchStatus(ctx, zitadel, func(status *zitadelv1alpha1.ZitadelClusterStatus) error {
status.FillWithDefaults(zitadel)
return nil
})
}
func (r *ZitadelClusterReconciler) reconcileMasterKeySecret(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
secretName := masterkey.MasterKeyName(zitadel)
key := types.NamespacedName{
Name: secretName,
Namespace: zitadel.Namespace,
}
_, err := r.SecretReconciler.ReconcileRandomPassword(ctx, key, masterkey.Key, zitadel)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *ZitadelClusterReconciler) reconcileSystemAPIUser(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
secretName := systemapiaccount.SystemAPIAccountName(zitadel)
key := types.NamespacedName{
Name: secretName,
Namespace: zitadel.Namespace,
}
_, err := r.SecretReconciler.ReconcileRandomPrivateRSA(ctx, key, systemapiaccount.Key, zitadel)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *ZitadelClusterReconciler) reconcileConfig(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
crdb, err := r.RefResolver.CrdbClusterRef(ctx, &zitadel.Spec.CrdbClusterRef, zitadel.Namespace)
if err != nil {
return ctrl.Result{}, err
}
configName := configuration.ConfigurationName(zitadel)
key := types.NamespacedName{
Name: configName,
Namespace: zitadel.Namespace,
}
privateKeyData, err := r.RefResolver.SecretKeyRef(ctx, corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: systemapiaccount.SystemAPIAccountName(zitadel)}, Key: systemapiaccount.Key}, zitadel.Namespace)
if err != nil {
return ctrl.Result{}, err
}
pemBlock, _ := pem.Decode([]byte(privateKeyData))
if pemBlock == nil {
return ctrl.Result{}, fmt.Errorf("failed to decode PEM block")
}
privateKey, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
return ctrl.Result{}, err
}
publicKeyPem := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: publicKeyBytes,
},
)
base64key := base64.StdEncoding.EncodeToString(publicKeyPem)
err = r.ConfigMapReconciler.ReconcileZitadelConfiguration(ctx, key, zitadel, crdb, base64key)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *ZitadelClusterReconciler) reconcileInitJob(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
key := client.ObjectKeyFromObject(zitadel)
key.Name = "init-job-" + key.Name
desiredInitJob, err := r.Builder.BuildInitJob(zitadel, key)
if err != nil {
return ctrl.Result{}, fmt.Errorf("error building InitJob: %v", err)
}
var existingJob batchv1.Job
if err := r.Get(ctx, key, &existingJob); err != nil {
if !errors.IsNotFound(err) {
return ctrl.Result{}, fmt.Errorf("error getting InitJob: %v", err)
}
if err := r.Create(ctx, desiredInitJob); err != nil {
return ctrl.Result{}, fmt.Errorf("error creating InitJob: %v", err)
}
return ctrl.Result{}, nil
}
patch := client.MergeFrom(existingJob.DeepCopy())
existingJob.Spec.Template.Spec = desiredInitJob.Spec.Template.Spec
return ctrl.Result{}, r.Patch(ctx, &existingJob, patch)
}
func (r *ZitadelClusterReconciler) reconcileSetupJob(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
key := client.ObjectKeyFromObject(zitadel)
key.Name = "setup-job-" + key.Name
desiredSetupjob, err := r.Builder.BuildSetupJob(zitadel, key)
if err != nil {
return ctrl.Result{}, fmt.Errorf("error building Setupjob: %v", err)
}
var existingJob batchv1.Job
if err := r.Get(ctx, key, &existingJob); err != nil {
if !errors.IsNotFound(err) {
return ctrl.Result{}, fmt.Errorf("error getting Setupjob: %v", err)
}
if err := r.Create(ctx, desiredSetupjob); err != nil {
return ctrl.Result{}, fmt.Errorf("error creating Setupjob: %v", err)
}
return ctrl.Result{}, nil
}
patch := client.MergeFrom(existingJob.DeepCopy())
existingJob.Spec.Template.Spec = desiredSetupjob.Spec.Template.Spec
return ctrl.Result{}, r.Patch(ctx, &existingJob, patch)
}
func (r *ZitadelClusterReconciler) reconcileDeployment(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
// TODO: Reload on config changed
key := client.ObjectKeyFromObject(zitadel)
desiredSts, err := r.Builder.BuildDeployment(zitadel, key)
if err != nil {
return ctrl.Result{}, fmt.Errorf("error building Deployment: %v", err)
}
var existingDep appsv1.Deployment
if err := r.Get(ctx, key, &existingDep); err != nil {
if !errors.IsNotFound(err) {
return ctrl.Result{}, fmt.Errorf("error getting Deployment: %v", err)
}
if err := r.Create(ctx, desiredSts); err != nil {
return ctrl.Result{}, fmt.Errorf("error creating Deployment: %v", err)
}
return ctrl.Result{}, nil
}
patch := client.MergeFrom(existingDep.DeepCopy())
existingDep.Spec.Template = desiredSts.Spec.Template
existingDep.Spec.Replicas = desiredSts.Spec.Replicas
return ctrl.Result{}, r.Patch(ctx, &existingDep, patch)
}
func (r *ZitadelClusterReconciler) reconcileService(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
return ctrl.Result{}, r.reconcileDefaultService(ctx, zitadel)
}
func (r *ZitadelClusterReconciler) reconcileDefaultService(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) error {
key := client.ObjectKeyFromObject(zitadel)
opts := builder.ServiceOpts{
Ports: []corev1.ServicePort{
{
Name: deployment.ZitadelName,
Port: deployment.ZitadelPort,
},
},
}
desiredSvc, err := r.Builder.BuildService(zitadel, key, opts)
if err != nil {
return fmt.Errorf("error building Service: %v", err)
}
return r.ServiceReconciler.Reconcile(ctx, desiredSvc)
}
func (r *ZitadelClusterReconciler) reconcileDefaultInstance(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) {
// First create systemapi to get, delete and create instances
privateKeyData, err := r.RefResolver.SecretKeyRef(ctx, corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: systemapiaccount.SystemAPIAccountName(zitadel)}, Key: systemapiaccount.Key}, zitadel.Namespace)
if err != nil {
return ctrl.Result{}, err
}
ztdClient, err := system.NewClient(GetIssuer(zitadel), GetAPI(zitadel), system.JWTProfileFromKey([]byte(privateKeyData), masterkey.OwnerName), system.WithInsecure())
if err != nil {
return ctrl.Result{}, err
}
defer ztdClient.Connection.Close()
// Delete all Instances that isn't the default
{
resp, err := ztdClient.ListInstances(ctx, &pb.ListInstancesRequest{})
if err != nil {
return ctrl.Result{}, fmt.Errorf("Error listing instances: %v", err)
}
for _, instance := range resp.Result {
if instance.Id != zitadel.Status.DefaultInstanceId {
fmt.Println("DELETING INSTANCE")
_, err := ztdClient.RemoveInstance(ctx, &pb.RemoveInstanceRequest{InstanceId: instance.Id})
if err != nil {
return ctrl.Result{}, err
}
}
}
}
// Check if instance already exists
_, err = ztdClient.GetInstance(ctx, &pb.GetInstanceRequest{InstanceId: zitadel.Status.DefaultInstanceId})
if err != nil {
if strings.Contains(err.Error(), "Instance not found") {
// if Instance doesn't exist, then create and assign secrets
resp, err := ztdClient.CreateInstance(ctx, &pb.CreateInstanceRequest{InstanceName: "DEFAULT", FirstOrgName: "DEFAULT", CustomDomain: zitadel.Spec.Host, Owner: &pb.CreateInstanceRequest_Machine_{Machine: &pb.CreateInstanceRequest_Machine{Name: "k8s-operator", UserName: "k8s-operator", MachineKey: &pb.CreateInstanceRequest_MachineKey{Type: authn.KeyType_KEY_TYPE_JSON}, PersonalAccessToken: &pb.CreateInstanceRequest_PersonalAccessToken{}}}})
if err != nil {
return ctrl.Result{}, err
}
var machineKeyData zitadelClient.MachineKey
if err := json.Unmarshal(resp.MachineKey, &machineKeyData); err != nil {
return ctrl.Result{}, err
}
secretName := machinekey.MachineKeySecretName(zitadel)
key := types.NamespacedName{
Name: secretName,
Namespace: zitadel.Namespace,
}
secretData := make(map[string][]byte)
jsonData, err := json.Marshal(machineKeyData)
if err != nil {
return ctrl.Result{}, err
}
secretData[machinekey.Key] = jsonData
secret, err := r.Builder.BuildSecret(builder.SecretOpts{Zitadel: zitadel, Key: key, Data: secretData}, zitadel)
if err != nil {
return ctrl.Result{}, fmt.Errorf("error building machinekey Secret: %v", err)
}
if err := r.Create(ctx, secret); err != nil {
return ctrl.Result{}, fmt.Errorf("error creating machinekey Secret: %v", err)
}
patch := client.MergeFrom(zitadel.DeepCopy())
zitadel.Status.DefaultInstanceId = resp.InstanceId
return ctrl.Result{}, r.Status().Patch(ctx, zitadel, patch)
} else {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
func GetIssuer(zitadel *zitadelv1alpha1.ZitadelCluster) string {
return fmt.Sprintf("http://%s:%d", zitadel.Spec.Host, zitadel.Spec.ExternalPort)
}
func GetAPI(zitadel *zitadelv1alpha1.ZitadelCluster) string {
return fmt.Sprintf("%s:%d", deployment.ServiceFQDN(zitadel.ObjectMeta), deployment.ZitadelPort)
}
func (r *ZitadelClusterReconciler) patchStatus(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster,
patcher patcher) error {
patch := client.MergeFrom(zitadel.DeepCopy())
if err := patcher(&zitadel.Status); err != nil {
return err
}
return r.Status().Patch(ctx, zitadel, patch)
}
func (r *ZitadelClusterReconciler) patcher(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) patcher {
return func(s *zitadelv1alpha1.ZitadelClusterStatus) error {
var sts appsv1.Deployment
if err := r.Get(ctx, client.ObjectKeyFromObject(zitadel), &sts); err != nil {
return err
}
zitadel.Status.Replicas = sts.Status.ReadyReplicas
condition.SetReadyWithDeployment(&zitadel.Status, &sts)
return nil
}
}
func (r *ZitadelClusterReconciler) patch(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster,
patcher func(*zitadelv1alpha1.ZitadelCluster)) error {
patch := client.MergeFrom(zitadel.DeepCopy())
patcher(zitadel)
return r.Patch(ctx, zitadel, patch)
}
// SetupWithManager sets up the controller with the Manager.
func (r *ZitadelClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&zitadelv1alpha1.ZitadelCluster{}).
Owns(&appsv1.Deployment{}).
Owns(&corev1.Service{}).
Owns(&corev1.ConfigMap{}).
Owns(&corev1.Secret{}).
WithOptions(controller.Options{RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(time.Millisecond*500, time.Minute*3)}).
Complete(r)
}