allow to reference via zitadel too
All checks were successful
Build and Publish / build-release (push) Successful in 2m9s

This commit is contained in:
2026-04-30 15:36:20 -05:00
parent c6aff229ae
commit 319acd90de
8 changed files with 185 additions and 77 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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"`
}

View File

@@ -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")
}
}

View File

@@ -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{

View File

@@ -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") {

View File

@@ -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 {