@@ -8,8 +8,10 @@ import (
88 "fmt"
99 "hash/fnv"
1010
11+ apierrors "k8s.io/apimachinery/pkg/api/errors"
1112 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1213 "k8s.io/apimachinery/pkg/runtime"
14+ "k8s.io/client-go/rest"
1315 ctrl "sigs.k8s.io/controller-runtime"
1416 "sigs.k8s.io/controller-runtime/pkg/client"
1517 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -40,6 +42,9 @@ type PersonalOrganizationController struct {
4042 // The scheme is used to set the controller reference on the personal
4143 // organization.
4244 Scheme * runtime.Scheme
45+
46+ // RestConfig is used to create an impersonated client for project creation.
47+ RestConfig * rest.Config
4348}
4449
4550// +kubebuilder:rbac:groups=iam.datumapis.com,resources=users,verbs=get;list;watch
@@ -57,9 +62,18 @@ func (r *PersonalOrganizationController) Reconcile(ctx context.Context, req ctrl
5762 // Get the user.
5863 user := & iamv1alpha1.User {}
5964 if err := r .Client .Get (ctx , req .NamespacedName , user ); err != nil {
65+ if apierrors .IsNotFound (err ) {
66+ logger .Info ("User not found, skipping reconciliation" , "user" , req .NamespacedName )
67+ return ctrl.Result {}, nil
68+ }
6069 return ctrl.Result {}, fmt .Errorf ("failed to get user: %w" , err )
6170 }
6271
72+ if ! user .DeletionTimestamp .IsZero () {
73+ logger .Info ("User is being deleted, skipping reconciliation" , "user" , user .Name )
74+ return ctrl.Result {}, nil
75+ }
76+
6377 // Automatically create a personal organization for the user. They should not
6478 // be able to modify or delete the organization.
6579 personalOrg := & resourcemanagerv1alpha1.Organization {
@@ -116,29 +130,67 @@ func (r *PersonalOrganizationController) Reconcile(ctx context.Context, req ctrl
116130 }
117131
118132 // Create a default personal project in the personal organization.
133+ // The project webhook requires parent context in UserInfo.Extra fields,
134+ // and also looks up the requesting user by UID to create a PolicyBinding
135+ // granting them ownership. We impersonate the actual user so the webhook
136+ // sees the correct identity and creates the right PolicyBinding.
119137 personalProjectID := hashPersonalOrgName (string (user .UID ))
120138 personalProject := & resourcemanagerv1alpha1.Project {
121139 ObjectMeta : metav1.ObjectMeta {
122140 Name : fmt .Sprintf ("personal-project-%s" , personalProjectID ),
123141 },
124142 }
125- _ , err = controllerutil .CreateOrUpdate (ctx , r .Client , personalProject , func () error {
126- logger .Info ("Creating or updating personal project" , "organization" , personalOrg .Name , "project" , personalProject .Name )
143+
144+ impersonatedConfig := rest .CopyConfig (r .RestConfig )
145+ impersonatedConfig .Impersonate = rest.ImpersonationConfig {
146+ UserName : user .Name ,
147+ UID : user .Name ,
148+ Groups : []string {"system:authenticated" },
149+ Extra : map [string ][]string {
150+ "iam.miloapis.com/parent-name" : {personalOrg .Name },
151+ "iam.miloapis.com/parent-type" : {"Organization" },
152+ "iam.miloapis.com/parent-api-group" : {"resourcemanager.miloapis.com" },
153+ },
154+ }
155+
156+ impersonatedClient , err := client .New (impersonatedConfig , client.Options {Scheme : r .Scheme })
157+ if err != nil {
158+ return ctrl.Result {}, fmt .Errorf ("failed to create impersonated client: %w" , err )
159+ }
160+
161+ // Use the controller's own client (cluster-scope RBAC) to check whether
162+ // the project already exists. The impersonated user only has org-scoped
163+ // permissions and cannot GET projects at the cluster scope.
164+ existingProject := & resourcemanagerv1alpha1.Project {}
165+ err = r .Client .Get (ctx , client .ObjectKeyFromObject (personalProject ), existingProject )
166+ if err != nil {
167+ if ! apierrors .IsNotFound (err ) {
168+ return ctrl.Result {}, fmt .Errorf ("failed to check for existing personal project: %w" , err )
169+ }
170+
171+ // Project does not exist — create it via the impersonated client so
172+ // the webhook sees the actual user's identity.
173+ logger .Info ("Creating personal project" , "organization" , personalOrg .Name , "project" , personalProject .Name )
127174 metav1 .SetMetaDataAnnotation (& personalProject .ObjectMeta , "kubernetes.io/display-name" , "Personal Project" )
128175 metav1 .SetMetaDataAnnotation (& personalProject .ObjectMeta , "kubernetes.io/description" , fmt .Sprintf ("%s %s's Personal Project" , user .Spec .GivenName , user .Spec .FamilyName ))
129176 if err := controllerutil .SetControllerReference (user , personalProject , r .Scheme ); err != nil {
130- return fmt .Errorf ("failed to set controller reference: %w" , err )
177+ return ctrl. Result {}, fmt .Errorf ("failed to set controller reference: %w" , err )
131178 }
132179 personalProject .Spec = resourcemanagerv1alpha1.ProjectSpec {
133180 OwnerRef : resourcemanagerv1alpha1.OwnerReference {
134181 Kind : "Organization" ,
135182 Name : personalOrg .Name ,
136183 },
137184 }
138- return nil
139- })
140- if err != nil {
141- return ctrl.Result {}, fmt .Errorf ("failed to create or update personal project: %w" , err )
185+
186+ if err := impersonatedClient .Create (ctx , personalProject ); err != nil {
187+ if apierrors .IsAlreadyExists (err ) {
188+ logger .Info ("Personal project already exists (race)" , "project" , personalProject .Name )
189+ } else {
190+ logger .Error (err , "Failed to create personal project" )
191+ return ctrl.Result {}, fmt .Errorf ("failed to create personal project: %w" , err )
192+ }
193+ }
142194 }
143195
144196 logger .Info ("Successfully created or updated personal organization resources" , "organization" , personalOrg .Name )
0 commit comments