Initial commit
[ZITADOPER-1]
This commit is contained in:
217
src/internal/controller/oidcapp_controller.go
Normal file
217
src/internal/controller/oidcapp_controller.go
Normal 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)
|
||||
}
|
||||
86
src/internal/controller/oidcapp_controller_finalizer.go
Normal file
86
src/internal/controller/oidcapp_controller_finalizer.go
Normal 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
|
||||
}
|
||||
122
src/internal/controller/organization_controller.go
Normal file
122
src/internal/controller/organization_controller.go
Normal 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)
|
||||
}
|
||||
87
src/internal/controller/organization_controller_finalizer.go
Normal file
87
src/internal/controller/organization_controller_finalizer.go
Normal 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
|
||||
}
|
||||
157
src/internal/controller/project_controller.go
Normal file
157
src/internal/controller/project_controller.go
Normal 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)
|
||||
}
|
||||
94
src/internal/controller/project_controller_finalizer.go
Normal file
94
src/internal/controller/project_controller_finalizer.go
Normal 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
|
||||
}
|
||||
80
src/internal/controller/suite_test.go
Normal file
80
src/internal/controller/suite_test.go
Normal 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())
|
||||
})
|
||||
473
src/internal/controller/zitadelcluster_controller.go
Normal file
473
src/internal/controller/zitadelcluster_controller.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user