Skip to content

Commit 96b81f8

Browse files
authored
fix: Error when creating personal personal project (#181)
PersonalOrganizationController now generates personal projects using an impersonated client to accurately attribute them with the required Extra fields. This ensures that the `Projects` server filter can identify the parent resource, and the `Project` validation webhook can correctly set the policy binding.
2 parents 5ed651f + 8d9acee commit 96b81f8

File tree

3 files changed

+62
-17
lines changed

3 files changed

+62
-17
lines changed

.github/workflows/build-and-test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
branches:
66
- main
7+
- '**'
78
pull_request:
89
release:
910
types:

cmd/controller/manager.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,10 @@ func runControllerManager(
287287
}
288288

289289
if err = (&resourcemanagercontroller.PersonalOrganizationController{
290-
Client: mgr.GetClient(),
291-
Config: serverConfig.PersonalOrganizationController,
292-
Scheme: mgr.GetScheme(),
290+
Client: mgr.GetClient(),
291+
Config: serverConfig.PersonalOrganizationController,
292+
Scheme: mgr.GetScheme(),
293+
RestConfig: mgr.GetConfig(),
293294
}).SetupWithManager(mgr); err != nil {
294295
setupLog.Error(err, "unable to create controller", "controller", "PersonalOrganization")
295296
return err

internal/controller/resourcemanager/personal_organization_controller.go

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)