Skip to content

Commit 0e89f2f

Browse files
committed
feat: PersonalOrganizationController now creates personal projects using an impersonated client to correctly attribute ownership, and the build workflow runs on all branches.
1 parent a9d39c5 commit 0e89f2f

File tree

3 files changed

+64
-10
lines changed

3 files changed

+64
-10
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: 59 additions & 7 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{
@@ -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

Comments
 (0)