diff --git a/api/v1alpha1/apiapp_types.go b/api/v1alpha1/apiapp_types.go index b72bea8..b337f4f 100644 --- a/api/v1alpha1/apiapp_types.go +++ b/api/v1alpha1/apiapp_types.go @@ -32,6 +32,7 @@ type APIAppSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file ProjectRef ProjectRef `json:"projectRef"` + APIAppName string `json:"apiAppName"` // +kubebuilder:validation:Enum=API_AUTH_METHOD_TYPE_BASIC;API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT AuthMethodType string `json:"authMethodType"` } @@ -43,12 +44,9 @@ type APIAppStatus struct { // +optional // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} Conditions []metav1.Condition `json:"conditions,omitempty"` - // +kubebuilder:default="" - AppId string `json:"appId"` - // +kubebuilder:default="" - KeyId string `json:"keyId"` - // +kubebuilder:default="" - ClientId string `json:"clientId"` + AppId *string `json:"appId,omitempty"` + ClientId *string `json:"clientId,omitempty"` + KeyId *string `json:"keyId,omitempty"` } func (d *APIAppStatus) SetCondition(condition metav1.Condition) { @@ -120,6 +118,14 @@ func (d *APIApp) Project(ctx context.Context, refresolver *RefResolver) (*Resolv return refresolver.ResolveProject(ctx, &d.Spec.ProjectRef, d.Namespace) } +func (d *APIApp) ClientSecretName() string { + return d.Name + "-client-secret" +} + +func (d *APIApp) PrivateKeySecretName() string { + return d.Name + "-privatekey-secret" +} + //+kubebuilder:object:root=true // APIAppList contains a list of APIApp diff --git a/api/v1alpha1/project_types.go b/api/v1alpha1/project_types.go index dffe004..43f8f27 100644 --- a/api/v1alpha1/project_types.go +++ b/api/v1alpha1/project_types.go @@ -18,7 +18,6 @@ package v1alpha1 import ( "context" - "fmt" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -97,7 +96,6 @@ func (d *Project) IsReady() bool { } func (d *Project) ConnectionRef(ctx context.Context, refresolver *RefResolver) (*ConnectionRef, error) { - fmt.Println(d.Spec.OrganizationRef) // Check if using direct Zitadel ID reference if d.Spec.OrganizationRef.ID != "" { return &d.Spec.OrganizationRef.ConnectionRef, nil diff --git a/api/v1alpha1/ref_types.go b/api/v1alpha1/ref_types.go index 1fc436a..0ff6a5e 100644 --- a/api/v1alpha1/ref_types.go +++ b/api/v1alpha1/ref_types.go @@ -27,11 +27,11 @@ type OrganizationRef struct { // +kubebuilder:validation:XValidation:rule="has(self.name) || has(self.id)",message="must provide either k8s object reference (name) or zitadel ID reference (id)" // +kubebuilder:validation:XValidation:rule="!has(self.id) || has(self.connectionRef.name)",message="zitadel ID reference requires connectionRef.name" type ProjectRef struct { - ObjectReference corev1.ObjectReference `json:",inline"` - ID string `json:"id,omitempty"` - ConnectionRef ConnectionRef `json:"connectionRef,omitempty"` + corev1.ObjectReference `json:",inline"` + ID string `json:"id,omitempty"` + ConnectionRef ConnectionRef `json:"connectionRef,omitempty"` } type ActionRef struct { - ObjectReference corev1.ObjectReference `json:",inline"` + corev1.ObjectReference `json:",inline"` } diff --git a/cmd/main.go b/cmd/main.go index 9250ef8..ab43cae 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -116,14 +116,14 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "OIDCApp") os.Exit(1) } + if err = controller.NewAPIAppReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "APIApp") + os.Exit(1) + } if err = controller.NewMachineUserReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MachineUser") os.Exit(1) } - // if err = controller.NewAPIAppReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { - // setupLog.Error(err, "unable to create controller", "controller", "APIApp") - // os.Exit(1) - // } // if err = controller.NewActionReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { // setupLog.Error(err, "unable to create controller", "controller", "Action") // os.Exit(1) diff --git a/config/crd/bases/zitadel.github.com_apiapps.yaml b/config/crd/bases/zitadel.github.com_apiapps.yaml index 0e5e4dc..2777d8a 100644 --- a/config/crd/bases/zitadel.github.com_apiapps.yaml +++ b/config/crd/bases/zitadel.github.com_apiapps.yaml @@ -39,6 +39,8 @@ spec: spec: description: APIAppSpec defines the desired state of APIApp properties: + apiAppName: + type: string authMethodType: enum: - API_AUTH_METHOD_TYPE_BASIC @@ -143,6 +145,7 @@ spec: - message: zitadel ID reference requires connectionRef.name rule: '!has(self.id) || has(self.connectionRef.name)' required: + - apiAppName - authMethodType - projectRef type: object @@ -150,10 +153,8 @@ spec: description: APIAppStatus defines the observed state of APIApp properties: appId: - default: "" type: string clientId: - default: "" type: string conditions: description: |- @@ -215,12 +216,7 @@ spec: type: object type: array keyId: - default: "" type: string - required: - - appId - - clientId - - keyId type: object type: object served: true diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c8d69b3..30340bb 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -28,6 +28,7 @@ rules: - apiGroups: - zitadel.github.com resources: + - apiapps - connections - machineusers - oidcapps @@ -44,6 +45,7 @@ rules: - apiGroups: - zitadel.github.com resources: + - apiapps/finalizers - connections/finalizers - machineusers/finalizers - oidcapps/finalizers @@ -54,6 +56,7 @@ rules: - apiGroups: - zitadel.github.com resources: + - apiapps/status - connections/status - machineusers/status - oidcapps/status diff --git a/internal/controller/apiapp_controller.go b/internal/controller/apiapp_controller.go new file mode 100644 index 0000000..b40cf7d --- /dev/null +++ b/internal/controller/apiapp_controller.go @@ -0,0 +1,328 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "encoding/json" + "fmt" + "time" + + zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1" + "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/builder" + condition "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/condition" + "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core" + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/application/v2" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/filter/v2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// APIAppReconciler reconciles a APIApp object +type APIAppReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration + Builder *builder.Builder +} + +func NewAPIAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, + requeueInterval time.Duration) *APIAppReconciler { + return &APIAppReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + Builder: builder, + } +} + +//+kubebuilder:rbac:groups=zitadel.github.com,resources=apiapps,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.github.com,resources=apiapps/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.github.com,resources=apiapps/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *APIAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var APIApp zitadelv1alpha1.APIApp + if err := r.Get(ctx, req.NamespacedName, &APIApp); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedAPIAppReconciler(r.Client, r.RefResolver, r.Builder, &APIApp) + wf := newWrappedAPIAppFinalizer(r.Client, &APIApp, r.RefResolver) + tf := core.NewCoreFinalizer(r.Client, wf) + tr := core.NewCoreReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &APIApp) + if err != nil { + return result, fmt.Errorf("error reconciling in APIAppReconciler: %v", err) + } + return result, nil +} + +type wrappedAPIAppReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + APIApp *zitadelv1alpha1.APIApp + Builder *builder.Builder +} + +func newWrappedAPIAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, + APIApp *zitadelv1alpha1.APIApp) core.WrappedCoreReconciler { + return &wrappedAPIAppReconciler{ + Client: client, + refResolver: refResolver, + APIApp: APIApp, + Builder: builder, + } +} + +type apiAppReconcilePhase struct { + Name string + Reconcile func(context.Context, *clientv2.Client) error +} + +func (wr *wrappedAPIAppReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + phases := []apiAppReconcilePhase{ + { + Name: "apiapp", + Reconcile: wr.reconcileApp, + }, + { + Name: "keys", + Reconcile: wr.reconcileKeys, + }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedAPIAppReconciler) reconcileApp(ctx context.Context, ztdClient *clientv2.Client) error { + projectRef, err := wr.APIApp.Project(ctx, wr.refResolver) + if err != nil { + return err + } + if projectRef.ID == "" { + return fmt.Errorf("Project has not been created yet...") + } + + var appid *string + var clientid *string + appList, err := ztdClient.ApplicationServiceV2().ListApplications(ctx, + &application.ListApplicationsRequest{ + Filters: []*application.ApplicationSearchFilter{ + { + Filter: &application.ApplicationSearchFilter_NameFilter{ + NameFilter: &application.ApplicationNameFilter{ + Name: wr.APIApp.Spec.APIAppName, + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + { + Filter: &application.ApplicationSearchFilter_ProjectIdFilter{ + ProjectIdFilter: &application.ProjectIDFilter{ + ProjectId: projectRef.ID, + }, + }, + }, + }, + }, + ) + if err != nil { + return fmt.Errorf("Error listing APIApps: %v", err) + } + + if len(appList.Applications) > 0 { + appid = &appList.Applications[0].ApplicationId + clientid = &appList.Applications[0].GetApiConfiguration().ClientId + } + + if appid == nil { + resp, err := ztdClient.ApplicationServiceV2().CreateApplication(ctx, + &application.CreateApplicationRequest{ + Name: wr.APIApp.Spec.APIAppName, + ProjectId: projectRef.ID, + ApplicationType: &application.CreateApplicationRequest_ApiConfiguration{ + ApiConfiguration: &application.CreateAPIApplicationRequest{ + AuthMethodType: application.APIAuthMethodType(application.APIAuthMethodType_value[wr.APIApp.Spec.AuthMethodType]), + }, + }, + }, + ) + if err != nil { + return fmt.Errorf("error creating APIApp in Zitadel: %v", err) + } + key := types.NamespacedName{ + Name: wr.APIApp.ClientSecretName(), + Namespace: wr.APIApp.Namespace, + } + + secretData := map[string][]byte{ + "clientSecret": []byte(resp.GetApiConfiguration().ClientSecret), + "appId": []byte(resp.ApplicationId), + "clientId": []byte(resp.GetApiConfiguration().ClientId), + } + secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: false, Key: key, Data: secretData}, wr.APIApp) + if err != nil { + return fmt.Errorf("error building Secret: %v", err) + } + if err := wr.Create(ctx, secret); err != nil { + return fmt.Errorf("error creating Client-secret Secret: %v", err) + } + appid = &resp.ApplicationId + clientid = &resp.GetApiConfiguration().ClientId + } else { + _, err := ztdClient.ApplicationServiceV2().UpdateApplication(ctx, + &application.UpdateApplicationRequest{ + Name: wr.APIApp.Name, + ProjectId: projectRef.ID, + ApplicationId: *appid, + ApplicationType: &application.UpdateApplicationRequest_ApiConfiguration{ + ApiConfiguration: &application.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: application.APIAuthMethodType(application.APIAuthMethodType_value[wr.APIApp.Spec.AuthMethodType]), + }, + }, + }, + ) + if err != nil { + return fmt.Errorf("error updating APIApp in Zitadel: %v", err) + } + } + patch := ctrlClient.MergeFrom(wr.APIApp.DeepCopy()) + wr.APIApp.Status.AppId = appid + wr.APIApp.Status.ClientId = clientid + return wr.Client.Status().Patch(ctx, wr.APIApp, patch) +} + +type Key struct { + Type string `json:"type"` + KeyID string `json:"keyId"` + Key string `json:"key"` + AppID string `json:"appId"` + ClientID string `json:"clientId"` +} + +func (wr *wrappedAPIAppReconciler) reconcileKeys(ctx context.Context, ztdClient *clientv2.Client) error { + if wr.APIApp.Spec.AuthMethodType == "API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT" { + projectRef, err := wr.APIApp.Project(ctx, wr.refResolver) + if err != nil { + return err + } + if wr.APIApp.Status.AppId == nil { + return fmt.Errorf("APIApp has not been created yet...") + } + + if wr.APIApp.Status.KeyId != nil { + keyList, err := ztdClient.ApplicationServiceV2().ListApplicationKeys(ctx, &application.ListApplicationKeysRequest{ + Filters: []*application.ApplicationKeySearchFilter{ + { + Filter: &application.ApplicationKeySearchFilter_ApplicationIdFilter{ + ApplicationIdFilter: &application.ApplicationKeyApplicationIDFilter{ + ApplicationId: *wr.APIApp.Status.AppId, + }, + }, + }, + { + Filter: &application.ApplicationKeySearchFilter_ProjectIdFilter{ + ProjectIdFilter: &application.ApplicationKeyProjectIDFilter{ + ProjectId: projectRef.ID, + }, + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("Error listing application keys: %v", err) + } + + for _, key := range keyList.Keys { + if key.KeyId == *wr.APIApp.Status.KeyId { + return nil + } + } + } + + resp, err := ztdClient.ApplicationServiceV2().CreateApplicationKey(ctx, &application.CreateApplicationKeyRequest{ + ApplicationId: *wr.APIApp.Status.AppId, + ProjectId: projectRef.ID, + }) + + if err != nil { + return fmt.Errorf("Error adding Key to app: %v", err) + } + + key := types.NamespacedName{ + Name: wr.APIApp.PrivateKeySecretName(), + Namespace: wr.APIApp.Namespace, + } + var jsonKey Key + if err = json.Unmarshal(resp.KeyDetails, &jsonKey); err != nil { + return fmt.Errorf("Could not unmarshal key details: %v", err) + } + secretData := map[string][]byte{ + "clientId": []byte(jsonKey.ClientID), + "type": []byte(jsonKey.Type), + "keyId": []byte(jsonKey.KeyID), + "appId": []byte(jsonKey.AppID), + "key": []byte(jsonKey.Key), + } + secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: false, Key: key, Data: secretData}, wr.APIApp) + if err != nil { + return fmt.Errorf("error building Secret: %v", err) + } + if err := wr.Create(ctx, secret); err != nil { + return fmt.Errorf("error creating private-key Secret: %v", err) + } + patch := ctrlClient.MergeFrom(wr.APIApp.DeepCopy()) + wr.APIApp.Status.KeyId = &resp.KeyId + return wr.Client.Status().Patch(ctx, wr.APIApp, patch) + } + return nil +} + +func (wr *wrappedAPIAppReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.APIApp.DeepCopy()) + patcher(&wr.APIApp.Status) + + if err := wr.Client.Status().Patch(ctx, wr.APIApp, patch); err != nil { + return fmt.Errorf("error patching APIApp status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *APIAppReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.APIApp{}). + Owns(&corev1.Secret{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/internal/controller/apiapp_controller.gold b/internal/controller/apiapp_controller.gold deleted file mode 100644 index b0ad1ef..0000000 --- a/internal/controller/apiapp_controller.gold +++ /dev/null @@ -1,282 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controller - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" - "github.com/HaimKortovich/zitadel-k8s-operator/pkg/builder" - condition "github.com/HaimKortovich/zitadel-k8s-operator/pkg/condition" - "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel" - "github.com/zitadel/zitadel-go/v3/pkg/client/management" - "github.com/zitadel/zitadel-go/v3/pkg/client/middleware" - app "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/app" - "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/authn" - pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/workqueue" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -// APIAppReconciler reconciles a APIApp object -type APIAppReconciler struct { - client.Client - RefResolver *zitadelv1alpha1.RefResolver - ConditionReady *condition.Ready - RequeueInterval time.Duration - Builder *builder.Builder -} - -func NewAPIAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, - requeueInterval time.Duration) *APIAppReconciler { - return &APIAppReconciler{ - Client: client, - RefResolver: refResolver, - ConditionReady: conditionReady, - RequeueInterval: requeueInterval, - Builder: builder, - } -} - -//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -func (r *APIAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - var APIApp zitadelv1alpha1.APIApp - if err := r.Get(ctx, req.NamespacedName, &APIApp); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - wr := newWrappedAPIAppReconciler(r.Client, r.RefResolver, r.Builder, &APIApp) - wf := newWrappedAPIAppFinalizer(r.Client, &APIApp, r.RefResolver) - tf := zitadel.NewZitadelFinalizer(r.Client, wf) - tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) - - result, err := tr.Reconcile(ctx, &APIApp) - if err != nil { - return result, fmt.Errorf("error reconciling in APIAppReconciler: %v", err) - } - return result, nil -} - -type wrappedAPIAppReconciler struct { - client.Client - refResolver *zitadelv1alpha1.RefResolver - APIApp *zitadelv1alpha1.APIApp - Builder *builder.Builder -} - -func newWrappedAPIAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, - APIApp *zitadelv1alpha1.APIApp) zitadel.WrappedReconciler { - return &wrappedAPIAppReconciler{ - Client: client, - refResolver: refResolver, - APIApp: APIApp, - Builder: builder, - } -} - -type apiAppReoncilePhase struct { - Name string - Reconcile func(context.Context, *management.Client) error -} - -func (wr *wrappedAPIAppReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { - phases := []projectReconcilePhase{ - { - Name: "apiapp", - Reconcile: wr.reconcileApp, - }, - { - Name: "keys", - Reconcile: wr.reconcileKeys, - }, - } - for _, p := range phases { - err := p.Reconcile(ctx, ztdClient) - if err != nil { - return err - } - } - return nil -} - -func (wr *wrappedAPIAppReconciler) reconcileApp(ctx context.Context, ztdClient *management.Client) error { - org, err := wr.APIApp.Organization(ctx, wr.refResolver) - if err != nil { - return err - } - project, err := wr.APIApp.Project(ctx, wr.refResolver) - if err != nil { - return err - } - if wr.APIApp.Status.AppId != "" { - appResp, err := ztdClient.GetAppByID(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppByIDRequest{ - ProjectId: project.Status.ProjectId, - AppId: string(wr.APIApp.Status.AppId), - }) - if err != nil { - return fmt.Errorf("Error getting APIApp: %v", err) - } - if appResp.App != nil { - _, err := ztdClient.UpdateAPIAppConfig(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.UpdateAPIAppConfigRequest{ProjectId: project.Status.ProjectId, AppId: wr.APIApp.Status.AppId, - AuthMethodType: app.APIAuthMethodType(app.APIAuthMethodType_value[wr.APIApp.Spec.AuthMethodType]), - }) - if err != nil { - if !strings.Contains(err.Error(), "No changes") { - return fmt.Errorf("Error updating APIApp: %v", err) - } - } - return nil - } - } - - resp, err := ztdClient.AddAPIApp(middleware.SetOrgID(ctx, org.Status.OrgId), - &pb.AddAPIAppRequest{ - Name: wr.APIApp.Name, - ProjectId: project.Status.ProjectId, - AuthMethodType: app.APIAuthMethodType(app.APIAuthMethodType_value[wr.APIApp.Spec.AuthMethodType]), - }, - ) - if err != nil { - if strings.Contains(err.Error(), "AlreadyExists") { - return nil - } - return fmt.Errorf("error creating APIApp in Zitadel: %v", err) - } - key := types.NamespacedName{ - Name: wr.APIApp.Name + "-client-secret", - Namespace: wr.APIApp.Namespace, - } - - secretData := map[string][]byte{"client-secret": []byte(resp.ClientSecret)} - secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: false, Zitadel: nil, Key: key, Data: secretData}, wr.APIApp) - if err != nil { - return fmt.Errorf("error building Secret: %v", err) - } - if err := wr.Create(ctx, secret); err != nil { - return fmt.Errorf("error creating Client-secret Secret: %v", err) - } - patch := ctrlClient.MergeFrom(wr.APIApp.DeepCopy()) - wr.APIApp.Status.AppId = resp.AppId - wr.APIApp.Status.ClientId = resp.ClientId - return wr.Client.Status().Patch(ctx, wr.APIApp, patch) -} - -type Key struct { - Type string `json:"type"` - KeyID string `json:"keyId"` - Key string `json:"key"` - AppID string `json:"appId"` - ClientID string `json:"clientId"` -} - -func (wr *wrappedAPIAppReconciler) reconcileKeys(ctx context.Context, ztdClient *management.Client) error { - if wr.APIApp.Spec.AuthMethodType == "API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT" { - org, err := wr.APIApp.Organization(ctx, wr.refResolver) - if err != nil { - return err - } - project, err := wr.APIApp.Project(ctx, wr.refResolver) - if err != nil { - return err - } - if wr.APIApp.Status.KeyId != "" { - appKey, err := ztdClient.GetAppKey(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppKeyRequest{ - ProjectId: project.Status.ProjectId, - AppId: wr.APIApp.Status.AppId, - KeyId: wr.APIApp.Status.KeyId, - }) - if err != nil { - if !strings.Contains(err.Error(), "not found") { - return fmt.Errorf("Could not get key: %v", err) - } - } - if appKey.Key != nil { - return nil - } - } - resp, err := ztdClient.AddAppKey(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.AddAppKeyRequest{ - ProjectId: project.Status.ProjectId, - AppId: wr.APIApp.Status.AppId, - Type: authn.KeyType_KEY_TYPE_JSON, - ExpirationDate: nil, - }) - - if err != nil { - return fmt.Errorf("Error adding Key to app: %v", err) - } - - key := types.NamespacedName{ - Name: wr.APIApp.Name + "-privatekey-secret", - Namespace: wr.APIApp.Namespace, - } - var jsonKey Key - if err = json.Unmarshal(resp.KeyDetails, &jsonKey); err != nil { - return fmt.Errorf("Could not unmarshal key details: %v", err) - } - secretData := map[string][]byte{ - "clientId": []byte(jsonKey.ClientID), - "type": []byte(jsonKey.Type), - "keyId": []byte(jsonKey.KeyID), - "appId": []byte(jsonKey.AppID), - "key": []byte(jsonKey.Key), - } - secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: false, Zitadel: nil, Key: key, Data: secretData}, wr.APIApp) - if err != nil { - return fmt.Errorf("error building Secret: %v", err) - } - if err := wr.Create(ctx, secret); err != nil { - return fmt.Errorf("error creating private-key Secret: %v", err) - } - patch := ctrlClient.MergeFrom(wr.APIApp.DeepCopy()) - wr.APIApp.Status.KeyId = resp.Id - return wr.Client.Status().Patch(ctx, wr.APIApp, patch) - } - return nil -} - -func (wr *wrappedAPIAppReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { - patch := client.MergeFrom(wr.APIApp.DeepCopy()) - patcher(&wr.APIApp.Status) - - if err := wr.Client.Status().Patch(ctx, wr.APIApp, patch); err != nil { - return fmt.Errorf("error patching APIApp status: %v", err) - } - return nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *APIAppReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&zitadelv1alpha1.APIApp{}). - Owns(&corev1.Secret{}). - WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](time.Millisecond*500, time.Minute*3)}). - Complete(r) -} diff --git a/internal/controller/apiapp_controller_finalizer.gold b/internal/controller/apiapp_controller_finalizer.go similarity index 61% rename from internal/controller/apiapp_controller_finalizer.gold rename to internal/controller/apiapp_controller_finalizer.go index 88ca265..c4284d5 100644 --- a/internal/controller/apiapp_controller_finalizer.gold +++ b/internal/controller/apiapp_controller_finalizer.go @@ -3,22 +3,21 @@ package controller import ( "strings" - zitadelv1alpha1 "github.com/HaimKortovich/zitadel-k8s-operator/api/v1alpha1" - "github.com/HaimKortovich/zitadel-k8s-operator/pkg/controller/zitadel" + zitadelv1alpha1 "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/api/v1alpha1" + "gitea.corredorconect.com/software-engineering/zitadel-resources-operator/pkg/controller/core" "context" "fmt" - "github.com/zitadel/zitadel-go/v3/pkg/client/management" - "github.com/zitadel/zitadel-go/v3/pkg/client/middleware" - pb "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/management" + clientv2 "github.com/zitadel/zitadel-go/v3/pkg/client" + "github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/application/v2" "sigs.k8s.io/controller-runtime/pkg/client" ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) const ( - APIAppFinalizerName = "apiapp.zitadel.topmanage.com/apiapp" + APIAppFinalizerName = "apiapp.zitadel.github.com/apiapp" ) type wrappedAPIAppFinalizer struct { @@ -27,7 +26,7 @@ type wrappedAPIAppFinalizer struct { refresolver *zitadelv1alpha1.RefResolver } -func newWrappedAPIAppFinalizer(client client.Client, APIApp *zitadelv1alpha1.APIApp, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer { +func newWrappedAPIAppFinalizer(client client.Client, APIApp *zitadelv1alpha1.APIApp, refresolver *zitadelv1alpha1.RefResolver) core.WrappedCoreFinalizer { return &wrappedAPIAppFinalizer{ Client: client, APIApp: APIApp, @@ -57,24 +56,22 @@ func (wr *wrappedAPIAppFinalizer) ContainsFinalizer() bool { return controllerutil.ContainsFinalizer(wr.APIApp, APIAppFinalizerName) } -func (wf *wrappedAPIAppFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error { - if wf.APIApp.Status.AppId == "" { - return nil - } - org, err := wf.APIApp.Organization(ctx, wf.refresolver) - if err != nil { - return err - } - project, err := wf.APIApp.Project(ctx, wf.refresolver) - if err != nil { - return err - } - _, err = ztdClient.RemoveApp(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.RemoveAppRequest{ProjectId: project.Status.ProjectId, AppId: wf.APIApp.Status.AppId}) - if err != nil { - if strings.Contains(err.Error(), "doesn't exist") { - return nil +func (wf *wrappedAPIAppFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { + if wf.APIApp.Status.AppId != nil { + projectRef, err := wf.APIApp.Project(ctx, wf.refresolver) + if err != nil { + return err + } + _, err = ztdClient.ApplicationServiceV2().DeleteApplication(ctx, &application.DeleteApplicationRequest{ + ApplicationId: *wf.APIApp.Status.AppId, + ProjectId: projectRef.ID, + }) + if err != nil { + if strings.Contains(err.Error(), "doesn't exist") { + return nil + } + return err } - return err } return nil } diff --git a/ops/chart/crds/apiapp-crd.yaml b/ops/chart/crds/apiapp-crd.yaml index 4cf57ae..359a978 100644 --- a/ops/chart/crds/apiapp-crd.yaml +++ b/ops/chart/crds/apiapp-crd.yaml @@ -38,6 +38,8 @@ spec: spec: description: APIAppSpec defines the desired state of APIApp properties: + apiAppName: + type: string authMethodType: enum: - API_AUTH_METHOD_TYPE_BASIC @@ -142,6 +144,7 @@ spec: - message: zitadel ID reference requires connectionRef.name rule: '!has(self.id) || has(self.connectionRef.name)' required: + - apiAppName - authMethodType - projectRef type: object @@ -149,10 +152,8 @@ spec: description: APIAppStatus defines the observed state of APIApp properties: appId: - default: "" type: string clientId: - default: "" type: string conditions: description: |- @@ -214,12 +215,7 @@ spec: type: object type: array keyId: - default: "" type: string - required: - - appId - - clientId - - keyId type: object type: object served: true diff --git a/ops/chart/templates/manager-rbac.yaml b/ops/chart/templates/manager-rbac.yaml index af5f76c..de47fc8 100644 --- a/ops/chart/templates/manager-rbac.yaml +++ b/ops/chart/templates/manager-rbac.yaml @@ -29,6 +29,7 @@ rules: - apiGroups: - zitadel.github.com resources: + - apiapps - connections - machineusers - oidcapps @@ -45,6 +46,7 @@ rules: - apiGroups: - zitadel.github.com resources: + - apiapps/finalizers - connections/finalizers - machineusers/finalizers - oidcapps/finalizers @@ -55,6 +57,7 @@ rules: - apiGroups: - zitadel.github.com resources: + - apiapps/status - connections/status - machineusers/status - oidcapps/status diff --git a/pkg/controller/core/controller.go b/pkg/controller/core/controller.go index 45f780e..bb130fc 100644 --- a/pkg/controller/core/controller.go +++ b/pkg/controller/core/controller.go @@ -47,7 +47,6 @@ func (r *CoreReconciler) Reconcile(ctx context.Context, resource Resource) (ctrl if err != nil { return ctrl.Result{}, err } - fmt.Println(connectionRef.ObjectReference.Name) connection, err := r.RefResolver.ConnectionRef(ctx, connectionRef, resource.GetNamespace()) if err != nil {