Skip to content

Commit 73f661d

Browse files
authored
feat: Implement AppProject watch to retrigger reconciliation (#18)
* feat: Implement AppProject watch to retrigger reconciliation Signed-off-by: Leonardo Luz Almeida <[email protected]> * add integration tests Signed-off-by: Leonardo Luz Almeida <[email protected]> * address review comments Signed-off-by: Leonardo Luz Almeida <[email protected]> --------- Signed-off-by: Leonardo Luz Almeida <[email protected]>
1 parent 8a724be commit 73f661d

File tree

3 files changed

+345
-42
lines changed

3 files changed

+345
-42
lines changed

api/ephemeral-access/v1alpha1/roletemplate_types.go

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ type RoleTemplateStatus struct {
5757
SyncHash string `json:"syncHash"`
5858
}
5959

60+
// Render will return a new RoleTemplate instance with the templates replaced by
61+
// the given projName, appName and appNs. The RoleTemplate fields that accept
62+
// templated values are 'rt.Spec.Description' and 'rt.Spec.Policies'.
6063
func (rt *RoleTemplate) Render(projName, appName, appNs string) (*RoleTemplate, error) {
6164
rendered := rt.DeepCopy()
6265
descTmpl, err := template.New("description").Parse(rt.Spec.Description)

internal/controller/accessrequest_controller.go

+87-7
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const (
5555
// managed by this controller
5656
AccessRequestFinalizerName = "accessrequest.ephemeral-access.argoproj-labs.io/finalizer"
5757
roleTemplateField = ".spec.roleTemplateName"
58+
projectField = ".status.targetProject"
5859
)
5960

6061
// +kubebuilder:rbac:groups=ephemeral-access.argoproj-labs.io,resources=accessrequests,verbs=get;list;watch;create;update;patch;delete
@@ -281,13 +282,18 @@ func (r *AccessRequestReconciler) handleFinalizer(ctx context.Context, ar *api.A
281282
// controller. Only non-concluded AccessRequests will be added to the reconciliation
282283
// list. An AccessRequest is defined as concluded if their status is Expired or Denied.
283284
func (r *AccessRequestReconciler) findObjectsForRoleTemplate(ctx context.Context, roleTemplate client.Object) []reconcile.Request {
285+
logger := log.FromContext(ctx)
286+
logger.Debug(fmt.Sprintf("RoleTemplate %s updated: searching for associated AccessRequests...", roleTemplate.GetName()))
284287
attachedAccessRequests := &api.AccessRequestList{}
285288
listOps := &client.ListOptions{
286289
FieldSelector: fields.OneTermEqualSelector(roleTemplateField, roleTemplate.GetName()),
287-
Namespace: roleTemplate.GetNamespace(),
290+
// This makes a requirement that the AccessRequest has to live in the
291+
// same namespace as the AppProject.
292+
Namespace: roleTemplate.GetNamespace(),
288293
}
289294
err := r.List(ctx, attachedAccessRequests, listOps)
290295
if err != nil {
296+
logger.Error(err, "findObjectsForRoleTemplate error: list k8s resources error")
291297
return []reconcile.Request{}
292298
}
293299

@@ -302,16 +308,74 @@ func (r *AccessRequestReconciler) findObjectsForRoleTemplate(ctx context.Context
302308
}
303309
}
304310
}
305-
if len(requests) == 0 {
311+
totalRequests := len(requests)
312+
if totalRequests == 0 {
306313
return nil
307314
}
315+
logger.Debug(fmt.Sprintf("Found %d associated AccessRequests with RoleTemplate %s. Reconciling...", totalRequests, roleTemplate.GetName()))
308316
return requests
309317
}
310318

311-
// SetupWithManager sets up the controller with the Manager.
312-
func (r *AccessRequestReconciler) SetupWithManager(mgr ctrl.Manager) error {
313-
// create an AccessRequest index by role template name to allow
314-
// fetching all objects referencing a given RoleTemplate
319+
// findObjectsForProject will retrieve all AccessRequest resources referencing
320+
// the given project and build a list of reconcile requests to be sent to the
321+
// controller. Only non-concluded AccessRequests will be added to the reconciliation
322+
// list. An AccessRequest is defined as concluded if their status is Expired or Denied.
323+
func (r *AccessRequestReconciler) findObjectsForProject(ctx context.Context, project client.Object) []reconcile.Request {
324+
logger := log.FromContext(ctx)
325+
logger.Debug(fmt.Sprintf("Project %s updated: searching for associated AccessRequests...", project.GetName()))
326+
associatedAccessRequests := &api.AccessRequestList{}
327+
listOps := &client.ListOptions{
328+
FieldSelector: fields.OneTermEqualSelector(projectField, project.GetName()),
329+
// This makes a requirement that the AccessRequest has to live in the
330+
// same namespace as the AppProject.
331+
Namespace: project.GetNamespace(),
332+
}
333+
err := r.List(ctx, associatedAccessRequests, listOps)
334+
if err != nil {
335+
logger.Error(err, "findObjectsForProject error: list k8s resources error")
336+
return []reconcile.Request{}
337+
}
338+
339+
requests := make([]reconcile.Request, len(associatedAccessRequests.Items))
340+
for i, item := range associatedAccessRequests.Items {
341+
if !isConcluded(&item) {
342+
requests[i] = reconcile.Request{
343+
NamespacedName: types.NamespacedName{
344+
Name: item.GetName(),
345+
Namespace: item.GetNamespace(),
346+
},
347+
}
348+
}
349+
}
350+
totalRequests := len(requests)
351+
if totalRequests == 0 {
352+
return nil
353+
}
354+
logger.Debug(fmt.Sprintf("Found %d associated AccessRequests with project %s. Reconciling...", totalRequests, project.GetName()))
355+
return requests
356+
}
357+
358+
// createProjectIndex will create an AccessRequest index by project to allow
359+
// fetching all objects referencing a given AppProject.
360+
func createProjectIndex(mgr ctrl.Manager) error {
361+
err := mgr.GetFieldIndexer().
362+
IndexField(context.Background(), &api.AccessRequest{}, projectField,
363+
func(rawObj client.Object) []string {
364+
ar := rawObj.(*api.AccessRequest)
365+
if ar.Status.TargetProject == "" {
366+
return nil
367+
}
368+
return []string{ar.Status.TargetProject}
369+
})
370+
if err != nil {
371+
return fmt.Errorf("error creating project field index: %w", err)
372+
}
373+
return nil
374+
}
375+
376+
// createRoleTemplateIndex create an AccessRequest index by role template name
377+
// to allow fetching all objects referencing a given RoleTemplate.
378+
func createRoleTemplateIndex(mgr ctrl.Manager) error {
315379
err := mgr.GetFieldIndexer().
316380
IndexField(context.Background(), &api.AccessRequest{}, roleTemplateField, func(rawObj client.Object) []string {
317381
ar := rawObj.(*api.AccessRequest)
@@ -321,12 +385,28 @@ func (r *AccessRequestReconciler) SetupWithManager(mgr ctrl.Manager) error {
321385
return []string{ar.Spec.RoleTemplateName}
322386
})
323387
if err != nil {
324-
return fmt.Errorf("error creating index field for roleTemplateName: %w", err)
388+
return fmt.Errorf("error creating roleTemplateName field index: %w", err)
389+
}
390+
return nil
391+
}
392+
393+
// SetupWithManager sets up the controller with the Manager.
394+
func (r *AccessRequestReconciler) SetupWithManager(mgr ctrl.Manager) error {
395+
err := createProjectIndex(mgr)
396+
if err != nil {
397+
return fmt.Errorf("create index error: %w", err)
398+
}
399+
err = createRoleTemplateIndex(mgr)
400+
if err != nil {
401+
return fmt.Errorf("create index error: %w", err)
325402
}
326403
return ctrl.NewControllerManagedBy(mgr).
327404
For(&api.AccessRequest{}).
328405
Watches(&api.RoleTemplate{},
329406
handler.EnqueueRequestsFromMapFunc(r.findObjectsForRoleTemplate),
330407
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})).
408+
Watches(&argocd.AppProject{},
409+
handler.EnqueueRequestsFromMapFunc(r.findObjectsForProject),
410+
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})).
331411
Complete(r)
332412
}

0 commit comments

Comments
 (0)