Skip to content

feat(approval-expiration): approvals can now expire and notifications are sent before they do#387

Open
julius-malcovsky wants to merge 4 commits into
mainfrom
feat/approval-expiry
Open

feat(approval-expiration): approvals can now expire and notifications are sent before they do#387
julius-malcovsky wants to merge 4 commits into
mainfrom
feat/approval-expiry

Conversation

@julius-malcovsky
Copy link
Copy Markdown
Contributor

Notification templates will be added in a separate request

… are sent before they do

Co-authored-by: Björn Kottner <BjoernKarma@users.noreply.github.com>
Co-authored-by: Ismael Garba <iagarba@users.noreply.github.com>
Co-authored-by: Stefan Siber <stefan-ctrl@users.noreply.github.com>
Co-authored-by: Ron Gummich <ron96g@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 4, 2026 13:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an approval-expiration feature by adding an Expired approval state, a new ApprovalExpiration CRD/controller to drive expiration, and reminder-notification plumbing so expiring approvals can notify stakeholders ahead of time.

Changes:

  • Add ApprovalExpiration CRD + controller/handler to track expiration dates, trigger expiration, and send reminders.
  • Extend the Approval FSM/API types to support Expired and a system-only Expire action (plus re-approval from Expired).
  • Add notification placeholders + a reminder notification sender, and introduce an env-driven expiration config loader.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
approval/internal/webhook/v1/approval_webhook.go Adds webhook validation intended to block manual transitions to Expired.
approval/internal/webhook/v1/approval_webhook_test.go Adds tests around the new “system-only expire” validation behavior.
approval/internal/handler/util/notification.go Adds reminder-specific placeholders, fields, and a SendReminderNotification helper.
approval/internal/handler/approvalexpiration/handler.go Implements the core expiration/reminder logic for ApprovalExpiration objects.
approval/internal/handler/approvalexpiration/handler_test.go Unit tests for reminder eligibility and next-reconcile scheduling logic.
approval/internal/handler/approval/handler.go Integrates expiration lifecycle management into the Approval handler and hides system-only transitions from status.
approval/internal/handler/approval/fsm.go Extends FSM transitions to include Expire and allow actions from Expired.
approval/internal/controller/suite_test.go Registers the new ApprovalExpirationReconciler in the controller test suite.
approval/internal/controller/approvalexpiration_controller.go Adds the new controller reconciler wiring and RBAC markers.
approval/internal/controller/approvalexpiration_controller_test.go Integration tests for Approval↔ApprovalExpiration lifecycle behavior.
approval/internal/config/expiration.go Adds env-backed expiration/reminder configuration loading.
approval/internal/condition/condition.go Adds an “Expired” condition helper.
approval/config/rbac/role.yaml Grants RBAC permissions for approvalexpirations resources.
approval/config/crd/bases/approval.cp.ei.telekom.de_approvals.yaml Updates the Approval CRD enum to include Expired.
approval/config/crd/bases/approval.cp.ei.telekom.de_approvalexpirations.yaml Introduces the new ApprovalExpiration CRD manifest.
approval/cmd/main.go Loads expiration config and registers the new controller.
approval/api/v1/zz_generated.deepcopy.go Adds autogenerated deepcopy functions for ApprovalExpiration types.
approval/api/v1/common_types.go Adds the Expire action constant (and related type support).
approval/api/v1/builder/builder.go Treats Expired approvals as non-provisionable (denied) during build decisions.
approval/api/v1/approvalexpiration_types.go Adds API types for the new ApprovalExpiration resource.
approval/api/v1/approval_types.go Extends kubebuilder validation enums to include the Expired state.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread approval/internal/handler/approvalexpiration/handler.go
Comment thread approval/internal/handler/approvalexpiration/handler.go Outdated
Comment thread approval/internal/handler/approvalexpiration/handler.go Outdated
Comment thread approval/internal/handler/approvalexpiration/handler.go Outdated
Comment thread approval/internal/handler/approval/handler.go Outdated
Comment on lines +132 to +145
// validateExpireTransition blocks manual transitions to EXPIRED state (system-only action)
func validateExpireTransition(newObj *approvalv1.Approval) error {
if newObj.Spec.State != approvalv1.ApprovalStateExpired {
return nil
}

// Check if this is a legitimate system transition
for _, decision := range newObj.Spec.Decisions {
if decision.Name == "System" && decision.ResultingState == approvalv1.ApprovalStateExpired {
return nil // Valid system transition
}
}

return apierrors.NewBadRequest("Expire action is system-only and cannot be triggered manually")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point by AI ;)

Comment thread approval/internal/handler/util/notification.go Outdated
Comment thread approval/internal/handler/util/notification.go
Comment thread approval/internal/handler/util/notification.go
Comment thread approval/internal/handler/util/notification.go
daysRemaining = 0
case now.After(ae.Spec.DailyReminder.Time):
reminderType = "daily"
daysRemaining = int64(ae.Spec.Expiration.Time.Sub(now).Hours() / 24)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: this looks as something what can be in "utils" package.

Copy link
Copy Markdown
Member

@ron96g ron96g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks solid. Just a few questions/open points.

Add some diagram that explains the logic in more details? Maybe inside of the README similiar to the other digrams there.

if approvalExists {
log.V(2).Info("Approval exists")
isDenied := b.Approval.Spec.State == v1.ApprovalStateRejected || b.Approval.Spec.State == v1.ApprovalStateSuspended
isDenied := b.Approval.Spec.State == v1.ApprovalStateRejected ||
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that you explained it in the Review but is it really the goal to enforce this? Currently its just purely informational

Expiration metav1.Time `json:"expiration"`

// WeeklyReminder is the date from which weekly reminders start
WeeklyReminder metav1.Time `json:"weeklyReminder"`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the reminder logic in common for this? That was implemented for graceful-client-secret-rotation?

Comment thread approval/internal/config/expiration.go Outdated
LastWeeksWithDailyReminder: viper.GetInt("EXPIRATION_DAILY_REMINDER_WEEKS"),
}

if config.ExpirationPeriodMonths <= 0 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done using https://github.com/go-playground/validator, however, the bigger question here is, can we integrate this into the common-config logic? The story to align config-management is still in the backlog, but can we already think about this?

)

// ExpirationConfig holds the configuration for approval expiration
type ExpirationConfig struct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we want to configure via deployment config or rather via Environment? So that these values are set in the Environment CR and we read it from there instead from some configmap?


// SetupWithManager sets up the controller with the Manager.
func (r *ApprovalExpirationReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.Recorder = mgr.GetEventRecorderFor("approvalexpiration-controller")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API is deprecated and we must migrate at some point. Maybe already use the new one for newer controllers? Dont forget about the kubebuilder:rbac annotation


// filterSystemActions removes system-only actions (Expire) from the available transitions
func filterSystemActions(transitions approvalv1.AvailableTransitions) approvalv1.AvailableTransitions {
filtered := make(approvalv1.AvailableTransitions, 0, len(transitions))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could allocate +1 slots if Expire is not added

func NewExpiredCondition() metav1.Condition {
return metav1.Condition{
Type: "Approved",
Status: metav1.ConditionTrue,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct?


// Check if this is a legitimate system transition
for _, decision := range newObj.Spec.Decisions {
if decision.Name == "System" && decision.ResultingState == approvalv1.ApprovalStateExpired {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should "System" be a const somewhere?

Comment on lines +132 to +145
// validateExpireTransition blocks manual transitions to EXPIRED state (system-only action)
func validateExpireTransition(newObj *approvalv1.Approval) error {
if newObj.Spec.State != approvalv1.ApprovalStateExpired {
return nil
}

// Check if this is a legitimate system transition
for _, decision := range newObj.Spec.Decisions {
if decision.Name == "System" && decision.ResultingState == approvalv1.ApprovalStateExpired {
return nil // Valid system transition
}
}

return apierrors.NewBadRequest("Expire action is system-only and cannot be triggered manually")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point by AI ;)

Co-authored-by: Björn Kottner <BjoernKarma@users.noreply.github.com>
Co-authored-by: Ismael Garba <iagarba@users.noreply.github.com>
Co-authored-by: Stefan Siber <stefan-ctrl@users.noreply.github.com>
Co-authored-by: Ron Gummich <ron96g@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants