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
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{})
}
5 changes: 3 additions & 2 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,7 +241,8 @@ 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 ||
Comment thread
julius-malcovsky marked this conversation as resolved.
b.Approval.Spec.State == v1.ApprovalStateSuspended

if isDenied {
log.V(1).Info("Approval is rejected or suspended and must not be provisioned")
Expand Down
6 changes: 4 additions & 2 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 @@ -46,7 +49,6 @@ const (
ApprovalStateGranted ApprovalState = "Granted"
ApprovalStateRejected ApprovalState = "Rejected"
ApprovalStateSuspended ApprovalState = "Suspended"
ApprovalStateExpired ApprovalState = "Expired"
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.

Why remove the State?

)

func (s ApprovalState) String() string {
Expand Down Expand Up @@ -136,6 +138,6 @@ type Decision struct {
// ResultingState is the state the resource transitioned to as a result of this decision.
// Automatically set by the defaulting webhook to match Spec.State when not provided.
// +kubebuilder:validation:Required
// +kubebuilder:validation:Enum=Pending;Semigranted;Granted;Rejected;Suspended;Expired
// +kubebuilder:validation:Enum=Pending;Semigranted;Granted;Rejected;Suspended
ResultingState ApprovalState `json:"resultingState"`
}
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