diff --git a/api/v1alpha1/apiapp_types.go b/api/v1alpha1/apiapp_types.go index a0d2d5f..b72bea8 100644 --- a/api/v1alpha1/apiapp_types.go +++ b/api/v1alpha1/apiapp_types.go @@ -18,6 +18,7 @@ package v1alpha1 import ( "context" + "fmt" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -78,6 +79,12 @@ func (d *APIApp) IsReady() bool { } func (d *APIApp) ConnectionRef(ctx context.Context, refresolver *RefResolver) (*ConnectionRef, error) { + // Check if using direct Zitadel ID reference + if d.Spec.ProjectRef.ID != "" { + return &d.Spec.ProjectRef.ConnectionRef, nil + } + + // Fall back to K8s reference resolution project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) if err != nil { return nil, err @@ -88,24 +95,29 @@ func (d *APIApp) ConnectionRef(ctx context.Context, refresolver *RefResolver) (* } return &org.Spec.ConnectionRef, nil } -func (d *APIApp) Organization(ctx context.Context, refresolver *RefResolver) (*Organization, error) { +func (d *APIApp) Organization(ctx context.Context, refresolver *RefResolver) (*ResolvedReference, error) { + // Check if using direct Zitadel ID reference for project + if d.Spec.ProjectRef.ID != "" { + // For cross-cluster references, we need to get the organization ID from the project reference + // Since we don't have the full project object, we'll need to resolve the organization differently + // For now, return an error as this requires more complex handling + return nil, fmt.Errorf("cross-cluster organization resolution not yet implemented") + } + + // Fall back to K8s reference resolution project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) if err != nil { return nil, err } - org, err := refresolver.OrganizationRef(ctx, &project.Spec.OrganizationRef, d.Namespace) + orgRef, err := refresolver.ResolveOrganization(ctx, &project.Spec.OrganizationRef, d.Namespace) if err != nil { return nil, err } - return org, nil + return orgRef, nil } -func (d *APIApp) Project(ctx context.Context, refresolver *RefResolver) (*Project, error) { - project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) - if err != nil { - return nil, err - } - return project, nil +func (d *APIApp) Project(ctx context.Context, refresolver *RefResolver) (*ResolvedReference, error) { + return refresolver.ResolveProject(ctx, &d.Spec.ProjectRef, d.Namespace) } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/oidcapp_types.go b/api/v1alpha1/oidcapp_types.go index 7f682dc..daf4be3 100644 --- a/api/v1alpha1/oidcapp_types.go +++ b/api/v1alpha1/oidcapp_types.go @@ -18,6 +18,7 @@ package v1alpha1 import ( "context" + "fmt" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -99,6 +100,12 @@ func (d *OIDCApp) IsReady() bool { } func (d *OIDCApp) ConnectionRef(ctx context.Context, refresolver *RefResolver) (*ConnectionRef, error) { + // Check if using direct Zitadel ID reference + if d.Spec.ProjectRef.ID != "" { + return &d.Spec.ProjectRef.ConnectionRef, nil + } + + // Fall back to K8s reference resolution project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) if err != nil { return nil, err @@ -111,24 +118,29 @@ func (d *OIDCApp) ConnectionRef(ctx context.Context, refresolver *RefResolver) ( return &org.Spec.ConnectionRef, nil } -func (d *OIDCApp) Organization(ctx context.Context, refresolver *RefResolver) (*Organization, error) { +func (d *OIDCApp) Organization(ctx context.Context, refresolver *RefResolver) (*ResolvedReference, error) { + // Check if using direct Zitadel ID reference for project + if d.Spec.ProjectRef.ID != "" { + // For cross-cluster references, we need to get the organization ID from the project reference + // Since we don't have the full project object, we'll need to resolve the organization differently + // For now, return an error as this requires more complex handling + return nil, fmt.Errorf("cross-cluster organization resolution not yet implemented") + } + + // Fall back to K8s reference resolution project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) if err != nil { return nil, err } - org, err := refresolver.OrganizationRef(ctx, &project.Spec.OrganizationRef, d.Namespace) + orgRef, err := refresolver.ResolveOrganization(ctx, &project.Spec.OrganizationRef, d.Namespace) if err != nil { return nil, err } - return org, nil + return orgRef, nil } -func (d *OIDCApp) Project(ctx context.Context, refresolver *RefResolver) (*Project, error) { - project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace) - if err != nil { - return nil, err - } - return project, nil +func (d *OIDCApp) Project(ctx context.Context, refresolver *RefResolver) (*ResolvedReference, error) { + return refresolver.ResolveProject(ctx, &d.Spec.ProjectRef, d.Namespace) } func (d *OIDCApp) ClientSecretName() string { diff --git a/api/v1alpha1/project_types.go b/api/v1alpha1/project_types.go index 7025936..43f8f27 100644 --- a/api/v1alpha1/project_types.go +++ b/api/v1alpha1/project_types.go @@ -96,6 +96,12 @@ func (d *Project) IsReady() bool { } func (d *Project) ConnectionRef(ctx context.Context, refresolver *RefResolver) (*ConnectionRef, error) { + // Check if using direct Zitadel ID reference + if d.Spec.OrganizationRef.ID != "" { + return &d.Spec.OrganizationRef.ConnectionRef, nil + } + + // Fall back to K8s reference resolution org, err := refresolver.OrganizationRef(ctx, &d.Spec.OrganizationRef, d.Namespace) if err != nil { return nil, err diff --git a/api/v1alpha1/ref_types.go b/api/v1alpha1/ref_types.go index 6c7b6e1..828fe51 100644 --- a/api/v1alpha1/ref_types.go +++ b/api/v1alpha1/ref_types.go @@ -5,31 +5,31 @@ import ( ) type ConnectionRef struct { - // ObjectReference is a reference to a object. - // +operator-sdk:csv:customresourcedefinitions:type=spec - corev1.ObjectReference `json:",inline"` + ObjectReference corev1.ObjectReference `json:",inline"` } type OIDCAppRef struct { - // ObjectReference is a reference to a object. - // +operator-sdk:csv:customresourcedefinitions:type=spec - corev1.ObjectReference `json:",inline"` + ObjectReference corev1.ObjectReference `json:",inline"` } +// OrganizationRef can reference an organization via K8s object or direct Zitadel ID +// +kubebuilder:validation:XValidation:rule="has(self.name) == has(self.id)",message="must provide either k8s object reference (name) or zitadel ID reference (id), but not both" +// +kubebuilder:validation:XValidation:rule="!has(self.id) || has(self.connectionRef.name)",message="zitadel ID reference requires connectionRef.name" type OrganizationRef struct { - // ObjectReference is a reference to a object. - // +operator-sdk:csv:customresourcedefinitions:type=spec - corev1.ObjectReference `json:",inline"` + ObjectReference corev1.ObjectReference `json:",inline"` + ID string `json:"id,omitempty"` + ConnectionRef ConnectionRef `json:"connectionRef,omitempty"` } +// ProjectRef can reference a project via K8s object or direct Zitadel ID +// +kubebuilder:validation:XValidation:rule="has(self.name) == has(self.id)",message="must provide either k8s object reference (name) or zitadel ID reference (id), but not both" +// +kubebuilder:validation:XValidation:rule="!has(self.id) || has(self.connectionRef.name)",message="zitadel ID reference requires connectionRef.name" type ProjectRef struct { - // ObjectReference is a reference to a object. - // +operator-sdk:csv:customresourcedefinitions:type=spec - corev1.ObjectReference `json:",inline"` + ObjectReference corev1.ObjectReference `json:",inline"` + ID string `json:"id,omitempty"` + ConnectionRef ConnectionRef `json:"connectionRef,omitempty"` } type ActionRef struct { - // ObjectReference is a reference to a object. - // +operator-sdk:csv:customresourcedefinitions:type=spec - corev1.ObjectReference `json:",inline"` + ObjectReference corev1.ObjectReference `json:",inline"` } diff --git a/api/v1alpha1/refresolver.go b/api/v1alpha1/refresolver.go index ca889b0..613856c 100644 --- a/api/v1alpha1/refresolver.go +++ b/api/v1alpha1/refresolver.go @@ -9,6 +9,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +// ResolvedReference is the common abstraction for both reference types +type ResolvedReference struct { + ID string + Namespace string + Name string +} + // +kubebuilder:object:generate=false type RefResolver struct { client client.Client @@ -22,16 +29,16 @@ func NewRefResolver(client client.Client) *RefResolver { func (r *RefResolver) OIDCAppRef(ctx context.Context, ref *OIDCAppRef, namespace string) (*OIDCApp, error) { - if ref.Kind != "" && ref.Kind != "OIDCApp" { - return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + if ref.ObjectReference.Kind != "" && ref.ObjectReference.Kind != "OIDCApp" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.ObjectReference.Kind) } key := types.NamespacedName{ - Name: ref.Name, + Name: ref.ObjectReference.Name, Namespace: namespace, } - if ref.Namespace != "" { - key.Namespace = ref.Namespace + if ref.ObjectReference.Namespace != "" { + key.Namespace = ref.ObjectReference.Namespace } var zitadel OIDCApp @@ -43,16 +50,16 @@ func (r *RefResolver) OIDCAppRef(ctx context.Context, ref *OIDCAppRef, func (r *RefResolver) ActionRef(ctx context.Context, ref *ActionRef, namespace string) (*Action, error) { - if ref.Kind != "" && ref.Kind != "Action" { - return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + if ref.ObjectReference.Kind != "" && ref.ObjectReference.Kind != "Action" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.ObjectReference.Kind) } key := types.NamespacedName{ - Name: ref.Name, + Name: ref.ObjectReference.Name, Namespace: namespace, } - if ref.Namespace != "" { - key.Namespace = ref.Namespace + if ref.ObjectReference.Namespace != "" { + key.Namespace = ref.ObjectReference.Namespace } var zitadel Action @@ -64,16 +71,16 @@ func (r *RefResolver) ActionRef(ctx context.Context, ref *ActionRef, func (r *RefResolver) ProjectRef(ctx context.Context, ref *ProjectRef, namespace string) (*Project, error) { - if ref.Kind != "" && ref.Kind != "Project" { - return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + if ref.ObjectReference.Kind != "" && ref.ObjectReference.Kind != "Project" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.ObjectReference.Kind) } key := types.NamespacedName{ - Name: ref.Name, + Name: ref.ObjectReference.Name, Namespace: namespace, } - if ref.Namespace != "" { - key.Namespace = ref.Namespace + if ref.ObjectReference.Namespace != "" { + key.Namespace = ref.ObjectReference.Namespace } var zitadel Project @@ -85,16 +92,16 @@ func (r *RefResolver) ProjectRef(ctx context.Context, ref *ProjectRef, func (r *RefResolver) OrganizationRef(ctx context.Context, ref *OrganizationRef, namespace string) (*Organization, error) { - if ref.Kind != "" && ref.Kind != "Organization" { - return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + if ref.ObjectReference.Kind != "" && ref.ObjectReference.Kind != "Organization" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.ObjectReference.Kind) } key := types.NamespacedName{ - Name: ref.Name, + Name: ref.ObjectReference.Name, Namespace: namespace, } - if ref.Namespace != "" { - key.Namespace = ref.Namespace + if ref.ObjectReference.Namespace != "" { + key.Namespace = ref.ObjectReference.Namespace } var zitadel Organization @@ -105,15 +112,15 @@ func (r *RefResolver) OrganizationRef(ctx context.Context, ref *OrganizationRef, } func (r *RefResolver) ConnectionRef(ctx context.Context, ref *ConnectionRef, namespace string) (*Connection, error) { - if ref.Kind != "" && ref.Kind != "Connection" { - return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + if ref.ObjectReference.Kind != "" && ref.ObjectReference.Kind != "Connection" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.ObjectReference.Kind) } key := types.NamespacedName{ - Name: ref.Name, + Name: ref.ObjectReference.Name, Namespace: namespace, } - if ref.Namespace != "" { - key.Namespace = ref.Namespace + if ref.ObjectReference.Namespace != "" { + key.Namespace = ref.ObjectReference.Namespace } var connection Connection @@ -141,3 +148,79 @@ func (r *RefResolver) SecretKeyRef(ctx context.Context, selector corev1.SecretKe return string(data), nil } + +// ResolveOrganization resolves an organization reference to a common abstraction +func (r *RefResolver) ResolveOrganization(ctx context.Context, ref *OrganizationRef, namespace string) (*ResolvedReference, error) { + // Priority 1: Direct Zitadel ID reference (cross-cluster) + if ref.ID != "" { + return &ResolvedReference{ + ID: ref.ID, + }, nil + } + + // Priority 2: K8s object reference (same-cluster, backward compatible) + if ref.ObjectReference.Name != "" { + org, err := r.OrganizationRef(ctx, ref, namespace) + if err != nil { + return nil, err + } + if org.Status.OrganizationId == nil { + return nil, fmt.Errorf("organization not ready") + } + return &ResolvedReference{ + ID: *org.Status.OrganizationId, + Namespace: org.Namespace, + Name: org.Name, + }, nil + } + + return nil, fmt.Errorf("no valid organization reference provided") +} + +// ResolveProject resolves a project reference to a common abstraction +func (r *RefResolver) ResolveProject(ctx context.Context, ref *ProjectRef, namespace string) (*ResolvedReference, error) { + // Priority 1: Direct Zitadel ID reference (cross-cluster) + if ref.ID != "" { + return &ResolvedReference{ + ID: ref.ID, + }, nil + } + + // Priority 2: K8s object reference (same-cluster, backward compatible) + if ref.ObjectReference.Name != "" { + project, err := r.ProjectRef(ctx, ref, namespace) + if err != nil { + return nil, err + } + if project.Status.ProjectId == nil { + return nil, fmt.Errorf("project not ready") + } + return &ResolvedReference{ + ID: *project.Status.ProjectId, + Namespace: project.Namespace, + Name: project.Name, + }, nil + } + + return nil, fmt.Errorf("no valid project reference provided") +} + +// ResolveConnectionForRef resolves connection from either a ConnectionRef or enhanced reference with embedded ConnectionRef +func (r *RefResolver) ResolveConnectionForRef(ctx context.Context, ref interface{}, namespace string) (*Connection, error) { + switch v := ref.(type) { + case *ConnectionRef: + return r.ConnectionRef(ctx, v, namespace) + case *OrganizationRef: + if v.ID != "" { + return r.ConnectionRef(ctx, &v.ConnectionRef, namespace) + } + return nil, fmt.Errorf("organization reference does not contain connection info") + case *ProjectRef: + if v.ID != "" { + return r.ConnectionRef(ctx, &v.ConnectionRef, namespace) + } + return nil, fmt.Errorf("project reference does not contain connection info") + default: + return nil, fmt.Errorf("unsupported reference type for connection resolution") + } +} diff --git a/internal/controller/oidcapp_controller.go b/internal/controller/oidcapp_controller.go index c2b2061..0448742 100644 --- a/internal/controller/oidcapp_controller.go +++ b/internal/controller/oidcapp_controller.go @@ -101,11 +101,11 @@ func newWrappedOIDCAppReconciler(client client.Client, refResolver *zitadelv1alp } func (wr *wrappedOIDCAppReconciler) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { - project, err := wr.OIDCApp.Project(ctx, wr.refResolver) + projectRef, err := wr.OIDCApp.Project(ctx, wr.refResolver) if err != nil { return err } - if project.Status.ProjectId == nil { + if projectRef.ID == "" { return fmt.Errorf("Project has not been created yet...") } responseTypes := []application.OIDCResponseType{} @@ -133,7 +133,7 @@ func (wr *wrappedOIDCAppReconciler) Reconcile(ctx context.Context, ztdClient *cl { Filter: &application.ApplicationSearchFilter_ProjectIdFilter{ ProjectIdFilter: &application.ProjectIDFilter{ - ProjectId: *project.Status.ProjectId, + ProjectId: projectRef.ID, }, }, }, @@ -153,7 +153,7 @@ func (wr *wrappedOIDCAppReconciler) Reconcile(ctx context.Context, ztdClient *cl resp, err := ztdClient.ApplicationServiceV2().CreateApplication(ctx, &application.CreateApplicationRequest{ Name: wr.OIDCApp.Spec.OIDCAppName, - ProjectId: *project.Status.ProjectId, + ProjectId: projectRef.ID, ApplicationType: &application.CreateApplicationRequest_OidcConfiguration{ OidcConfiguration: &application.CreateOIDCApplicationRequest{ ApplicationType: application.OIDCApplicationType(application.OIDCApplicationType_value[wr.OIDCApp.Spec.AppType]), @@ -205,7 +205,7 @@ func (wr *wrappedOIDCAppReconciler) Reconcile(ctx context.Context, ztdClient *cl _, err := ztdClient.ApplicationServiceV2().UpdateApplication(ctx, &application.UpdateApplicationRequest{ Name: wr.OIDCApp.Name, - ProjectId: *project.Status.ProjectId, + ProjectId: projectRef.ID, ApplicationId: *appid, ApplicationType: &application.UpdateApplicationRequest_OidcConfiguration{ OidcConfiguration: &application.UpdateOIDCApplicationConfigurationRequest{ diff --git a/internal/controller/oidcapp_controller_finalizer.go b/internal/controller/oidcapp_controller_finalizer.go index 9a789ba..36752fb 100644 --- a/internal/controller/oidcapp_controller_finalizer.go +++ b/internal/controller/oidcapp_controller_finalizer.go @@ -58,13 +58,13 @@ func (wr *wrappedOIDCAppFinalizer) ContainsFinalizer() bool { func (wf *wrappedOIDCAppFinalizer) Reconcile(ctx context.Context, ztdClient *clientv2.Client) error { if wf.OIDCApp.Status.AppId != nil { - project, err := wf.OIDCApp.Project(ctx, wf.refresolver) + projectRef, err := wf.OIDCApp.Project(ctx, wf.refresolver) if err != nil { return err } _, err = ztdClient.ApplicationServiceV2().DeleteApplication(ctx, &application.DeleteApplicationRequest{ ApplicationId: *wf.OIDCApp.Status.AppId, - ProjectId: *project.Status.ProjectId, + ProjectId: projectRef.ID, }) if err != nil { if strings.Contains(err.Error(), "doesn't exist") { diff --git a/internal/controller/project_controller.go b/internal/controller/project_controller.go index 348a2e7..50009d7 100644 --- a/internal/controller/project_controller.go +++ b/internal/controller/project_controller.go @@ -124,13 +124,11 @@ func (wr *wrappedProjectReconciler) Reconcile(ctx context.Context, ztdClient *cl } func (wr *wrappedProjectReconciler) reconcileProject(ctx context.Context, ztdClient *clientv2.Client) error { - org, err := wr.refResolver.OrganizationRef(ctx, &wr.project.Spec.OrganizationRef, wr.project.Namespace) + orgRef, err := wr.refResolver.ResolveOrganization(ctx, &wr.project.Spec.OrganizationRef, wr.project.Namespace) if err != nil { return err } - if org.Status.OrganizationId == nil { - return fmt.Errorf("Organization not created yet") - } + var projectId *string projectList, err := ztdClient.ProjectServiceV2().ListProjects(ctx, &project.ListProjectsRequest{ Filters: []*project.ProjectSearchFilter{ @@ -145,7 +143,7 @@ func (wr *wrappedProjectReconciler) reconcileProject(ctx context.Context, ztdCli &project.ProjectSearchFilter{ Filter: &project.ProjectSearchFilter_OrganizationIdFilter{ OrganizationIdFilter: &project.ProjectOrganizationIDFilter{ - OrganizationId: *org.Status.OrganizationId, + OrganizationId: orgRef.ID, Type: project.ProjectOrganizationIDFilter_OWNED, }, }, @@ -163,7 +161,7 @@ func (wr *wrappedProjectReconciler) reconcileProject(ctx context.Context, ztdCli resp, err := ztdClient.ProjectServiceV2().CreateProject(ctx, &project.CreateProjectRequest{ - OrganizationId: *org.Status.OrganizationId, + OrganizationId: orgRef.ID, Name: wr.project.Spec.ProjectName, ProjectRoleAssertion: wr.project.Spec.ProjectRoleAssertion, AuthorizationRequired: wr.project.Spec.ProjectRoleCheck, @@ -254,16 +252,13 @@ func (wr *wrappedProjectReconciler) reconcileGrants(ctx context.Context, ztdClie return fmt.Errorf("Error listing project grants: %v", err) } for _, grant := range wr.project.DeepCopy().Spec.Grants { - grantedOrg, err := wr.refResolver.OrganizationRef(ctx, &grant.OrganizationRef, wr.project.Namespace) + grantedOrgRef, err := wr.refResolver.ResolveOrganization(ctx, &grant.OrganizationRef, wr.project.Namespace) if err != nil { return err } - if grantedOrg.Status.OrganizationId == nil { - continue - } var existingGrant *project.ProjectGrant for _, eGrant := range existingGrants.ProjectGrants { - if eGrant.GrantedOrganizationId == *grantedOrg.Status.OrganizationId { + if eGrant.GrantedOrganizationId == grantedOrgRef.ID { existingGrant = eGrant break } @@ -271,7 +266,7 @@ func (wr *wrappedProjectReconciler) reconcileGrants(ctx context.Context, ztdClie if existingGrant == nil { _, err := ztdClient.ProjectServiceV2().CreateProjectGrant(ctx, &project.CreateProjectGrantRequest{ ProjectId: *wr.project.Status.ProjectId, - GrantedOrganizationId: *grantedOrg.Status.OrganizationId, + GrantedOrganizationId: grantedOrgRef.ID, RoleKeys: grant.RoleKeys, }) if err != nil {