@@ -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 {
@@ -122,23 +136,52 @@ func (r *PersonalOrganizationController) Reconcile(ctx context.Context, req ctrl
122136 Name : fmt .Sprintf ("personal-project-%s" , personalProjectID ),
123137 },
124138 }
125- _ , err = controllerutil .CreateOrUpdate (ctx , r .Client , personalProject , func () error {
126- logger .Info ("Creating or updating personal project" , "organization" , personalOrg .Name , "project" , personalProject .Name )
127- metav1 .SetMetaDataAnnotation (& personalProject .ObjectMeta , "kubernetes.io/display-name" , "Personal Project" )
128- metav1 .SetMetaDataAnnotation (& personalProject .ObjectMeta , "kubernetes.io/description" , fmt .Sprintf ("%s %s's Personal Project" , user .Spec .GivenName , user .Spec .FamilyName ))
129- if err := controllerutil .SetControllerReference (user , personalProject , r .Scheme ); err != nil {
130- return fmt .Errorf ("failed to set controller reference: %w" , err )
139+
140+ // Use the controller's own client (cluster-scope RBAC) to check whether
141+ // the project already exists. The impersonated user only has org-scoped
142+ // permissions and cannot GET projects at the cluster scope.
143+ existingProject := & resourcemanagerv1alpha1.Project {}
144+ err = r .Client .Get (ctx , client .ObjectKeyFromObject (personalProject ), existingProject )
145+ if err != nil {
146+ if ! apierrors .IsNotFound (err ) {
147+ return ctrl.Result {}, fmt .Errorf ("failed to check for existing personal project: %w" , err )
131148 }
132- personalProject .Spec = resourcemanagerv1alpha1.ProjectSpec {
133- OwnerRef : resourcemanagerv1alpha1.OwnerReference {
134- Kind : "Organization" ,
135- Name : personalOrg .Name ,
149+
150+ // The project webhook requires parent context in UserInfo.Extra fields,
151+ // and also looks up the requesting user by UID to create a PolicyBinding
152+ // granting them ownership. We impersonate the actual user so the webhook
153+ // sees the correct identity and creates the right PolicyBinding.
154+ impersonatedConfig := rest .CopyConfig (r .RestConfig )
155+ impersonatedConfig .Impersonate = rest.ImpersonationConfig {
156+ UserName : user .Name ,
157+ UID : user .Name ,
158+ Groups : []string {"system:authenticated" },
159+ Extra : map [string ][]string {
160+ "iam.miloapis.com/parent-name" : {personalOrg .Name },
161+ "iam.miloapis.com/parent-type" : {"Organization" },
162+ "iam.miloapis.com/parent-api-group" : {"resourcemanager.miloapis.com" },
136163 },
137164 }
138- return nil
139- })
140- if err != nil {
141- return ctrl.Result {}, fmt .Errorf ("failed to create or update personal project: %w" , err )
165+
166+ impersonatedClient , err := client .New (impersonatedConfig , client.Options {Scheme : r .Scheme })
167+ if err != nil {
168+ return ctrl.Result {}, fmt .Errorf ("failed to create impersonated client: %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 )
174+ metav1 .SetMetaDataAnnotation (& personalProject .ObjectMeta , "kubernetes.io/display-name" , "Personal Project" )
175+ metav1 .SetMetaDataAnnotation (& personalProject .ObjectMeta , "kubernetes.io/description" , fmt .Sprintf ("%s %s's Personal Project" , user .Spec .GivenName , user .Spec .FamilyName ))
176+
177+ if err := impersonatedClient .Create (ctx , personalProject ); err != nil {
178+ if apierrors .IsAlreadyExists (err ) {
179+ logger .Info ("Personal project already exists (race)" , "project" , personalProject .Name )
180+ } else {
181+ logger .Error (err , "Failed to create personal project" )
182+ return ctrl.Result {}, fmt .Errorf ("failed to create personal project: %w" , err )
183+ }
184+ }
142185 }
143186
144187 logger .Info ("Successfully created or updated personal organization resources" , "organization" , personalOrg .Name )
0 commit comments