Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions approval/api/v1/approval_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type ApprovalSpec struct {
Strategy ApprovalStrategy `json:"strategy"`

// State defines the state of the approval
// +kubebuilder:validation:Enum=Pending;Semigranted;Granted;Rejected;Suspended
// +kubebuilder:validation:Enum=Pending;Semigranted;Granted;Rejected;Suspended;Expired
// +kubebuilder:default=Pending
State ApprovalState `json:"state"`

Expand All @@ -57,7 +57,7 @@ type ApprovalStatus struct {
AvailableTransitions AvailableTransitions `json:"availableTransitions,omitempty"`

// LastState defines the last state of the approval
// +kubebuilder:validation:Enum=Pending;Semigranted;Granted;Rejected;Suspended
// +kubebuilder:validation:Enum=Pending;Semigranted;Granted;Rejected;Suspended;Expired
// +kubebuilder:default=Pending
LastState ApprovalState `json:"lastState,omitempty"`

Expand Down
78 changes: 78 additions & 0 deletions approval/api/v1/approvalexpiration_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2025 Deutsche Telekom IT GmbH
//
// SPDX-License-Identifier: Apache-2.0

package v1

import (
"github.com/telekom/controlplane/common/pkg/reminder"
"github.com/telekom/controlplane/common/pkg/types"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ApprovalExpirationSpec defines the desired state of ApprovalExpiration
type ApprovalExpirationSpec struct {
// Approval is a reference to the parent Approval resource
Approval types.ObjectRef `json:"approval"`

// Expiration is the absolute date when the approval expires
Expiration metav1.Time `json:"expiration"`

// Thresholds defines when reminders should be sent relative to the expiration deadline.
// For example: [{Before: "720h"}, {Before: "168h", Repeat: "24h"}] sends one reminder
// 30 days before expiration, then daily reminders starting 7 days before.
// +optional
Thresholds []reminder.Threshold `json:"thresholds,omitempty"`
}

// ApprovalExpirationStatus defines the observed state of ApprovalExpiration
type ApprovalExpirationStatus struct {
// +listType=map
// +listMapKey=type
// +patchStrategy=merge
// +patchMergeKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`

// SentReminders tracks which reminder thresholds have been triggered and when
// +optional
SentReminders []reminder.SentReminder `json:"sentReminders,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// ApprovalExpiration is the Schema for the approvalexpirations API
// +kubebuilder:printcolumn:name="Approval",type="string",JSONPath=".spec.approval.name",description="The parent Approval"
// +kubebuilder:printcolumn:name="Expiration",type="date",JSONPath=".spec.expiration",description="When the approval expires"
type ApprovalExpiration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec ApprovalExpirationSpec `json:"spec,omitempty"`
Status ApprovalExpirationStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// ApprovalExpirationList contains a list of ApprovalExpiration
type ApprovalExpirationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ApprovalExpiration `json:"items"`
}

// GetConditions returns the conditions of the ApprovalExpiration
func (ae *ApprovalExpiration) GetConditions() []metav1.Condition {
return ae.Status.Conditions
}

// SetCondition sets the condition of the ApprovalExpiration
func (ae *ApprovalExpiration) SetCondition(condition metav1.Condition) bool {
return meta.SetStatusCondition(&ae.Status.Conditions, condition)
}

func init() {
SchemeBuilder.Register(&ApprovalExpiration{}, &ApprovalExpirationList{})
}
10 changes: 6 additions & 4 deletions approval/api/v1/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func (b *approvalBuilder) Build(ctx context.Context) (finalResult ApprovalResult
approvalReq.Spec.State = v1.ApprovalStateGranted
if len(approvalReq.Spec.Decisions) == 0 {
approvalReq.Spec.Decisions = append(approvalReq.Spec.Decisions, v1.Decision{
Name: "System",
Name: v1.SystemDecisionName,
Comment: v1.AutoApprovedComment,
ResultingState: v1.ApprovalStateGranted,
})
Expand Down Expand Up @@ -241,11 +241,13 @@ func (b *approvalBuilder) Build(ctx context.Context) (finalResult ApprovalResult
// Approval was found
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

b.Approval.Spec.State == v1.ApprovalStateSuspended ||
b.Approval.Spec.State == v1.ApprovalStateExpired

if isDenied {
log.V(1).Info("Approval is rejected or suspended and must not be provisioned")
b.Owner.SetCondition(newApprovalGrantedCondition(b.Approval.Spec.State, "Approval has been rejected or suspended"))
log.V(1).Info("Approval is rejected, suspended, or expired and must not be provisioned")
b.Owner.SetCondition(newApprovalGrantedCondition(b.Approval.Spec.State, "Approval has been rejected, suspended, or expired"))
return ApprovalResultDenied, nil
}
}
Expand Down
4 changes: 4 additions & 0 deletions approval/api/v1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const (
ApprovalStrategyFourEyes ApprovalStrategy = "FourEyes"
)

// SystemDecisionName is the decision name used for system-generated decisions (auto-approval, expiration).
const SystemDecisionName = "System"

// AutoApprovedComment is the comment added to auto-approved ApprovalRequests.
const AutoApprovedComment = "Auto-approved: The approval strategy does not require manual review."

Expand All @@ -32,6 +35,7 @@ const (
ApprovalActionDeny ApprovalAction = "Deny"
ApprovalActionSuspend ApprovalAction = "Suspend"
ApprovalActionResume ApprovalAction = "Resume"
ApprovalActionExpire ApprovalAction = "Expire"
)

func (a ApprovalAction) String() string {
Expand Down
113 changes: 113 additions & 0 deletions approval/api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 21 additions & 2 deletions approval/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook"

approvalv1 "github.com/telekom/controlplane/approval/api/v1"
"github.com/telekom/controlplane/approval/internal/config"
"github.com/telekom/controlplane/approval/internal/controller"
webhookv1 "github.com/telekom/controlplane/approval/internal/webhook/v1"
notificationv1 "github.com/telekom/controlplane/notification/api/v1"
Expand Down Expand Up @@ -79,6 +80,16 @@ func main() {

ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

// Load expiration configuration
expirationConfig, err := config.LoadExpirationConfig()
if err != nil {
setupLog.Error(err, "unable to load expiration config")
os.Exit(1)
}
setupLog.Info("loaded expiration config",
"expirationDuration", expirationConfig.ExpirationDuration,
"thresholds", len(expirationConfig.DefaultThresholds))

// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
Expand Down Expand Up @@ -122,8 +133,9 @@ func main() {
}

if err = (&controller.ApprovalReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
ExpirationConfig: expirationConfig,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Approval")
os.Exit(1)
Expand All @@ -135,6 +147,13 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "ApprovalRequest")
os.Exit(1)
}
if err = (&controller.ApprovalExpirationReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ApprovalExpiration")
os.Exit(1)
}
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err = webhookv1.SetupApprovalRequestWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "ApprovalRequest")
Expand Down
Loading
Loading