Skip to content
Merged
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
25 changes: 25 additions & 0 deletions api/v1alpha1/rollout_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,23 @@ type FailedHealthCheck struct {
Message *string `json:"message,omitempty"`
}

// TriggeredByInfo indicates what triggered a deployment.
type TriggeredByInfo struct {
// Kind indicates the type of trigger: "User" for manual deployments triggered by a user,
// or "System" for automatic deployments triggered by the rollout controller.
// +kubebuilder:validation:Enum=User;System
// +kubebuilder:validation:Required
// +required
Kind string `json:"kind"`

// Name contains the name of the user or system that triggered the deployment.
// For user-triggered deployments, this is extracted from the "rollout.kuberik.com/deploy-user" annotation.
// For system-triggered deployments, this is typically "rollout-controller".
// +kubebuilder:validation:Required
// +required
Name string `json:"name"`
}

// DeploymentHistoryEntry represents a single entry in the deployment history.
type DeploymentHistoryEntry struct {
// ID is a unique auto-incrementing identifier for this history entry.
Expand All @@ -267,6 +284,14 @@ type DeploymentHistoryEntry struct {
// +optional
Message *string `json:"message,omitempty"`

// TriggeredBy indicates what triggered this deployment.
// Kind can be "User" for manual deployments triggered by a user, or "System" for automatic deployments.
// Name contains the name of the user or system that triggered the deployment.
// For user-triggered deployments, this is extracted from the "rollout.kuberik.com/deploy-user" annotation.
// For system-triggered deployments, this is typically "rollout-controller".
// +optional
TriggeredBy *TriggeredByInfo `json:"triggeredBy,omitempty"`

// BakeStatus tracks the bake state for this deployment (e.g., None, InProgress, Succeeded, Failed, Cancelled)
// The bake process ensures that the deployment is stable and healthy before marking as successful.
// +optional
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

26 changes: 26 additions & 0 deletions config/crd/bases/kuberik.com_rollouts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,32 @@ spec:
description: Timestamp is the time when the deployment occurred.
format: date-time
type: string
triggeredBy:
description: |-
TriggeredBy indicates what triggered this deployment.
Kind can be "User" for manual deployments triggered by a user, or "System" for automatic deployments.
Name contains the name of the user or system that triggered the deployment.
For user-triggered deployments, this is extracted from the "rollout.kuberik.com/deploy-user" annotation.
For system-triggered deployments, this is typically "rollout-controller".
properties:
kind:
description: |-
Kind indicates the type of trigger: "User" for manual deployments triggered by a user,
or "System" for automatic deployments triggered by the rollout controller.
enum:
- User
- System
type: string
name:
description: |-
Name contains the name of the user or system that triggered the deployment.
For user-triggered deployments, this is extracted from the "rollout.kuberik.com/deploy-user" annotation.
For system-triggered deployments, this is typically "rollout-controller".
type: string
required:
- kind
- name
type: object
version:
description: Version is the version information that was deployed.
properties:
Expand Down
32 changes: 29 additions & 3 deletions internal/controller/rollout_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *RolloutReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

Check failure on line 105 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

cyclomatic complexity 65 of func `(*RolloutReconciler).Reconcile` is high (> 30) (gocyclo)

Check failure on line 105 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

cyclomatic complexity 65 of func `(*RolloutReconciler).Reconcile` is high (> 30) (gocyclo)
log := logf.FromContext(ctx)

rollout := rolloutv1alpha1.Rollout{}
Expand Down Expand Up @@ -195,7 +195,7 @@
// Check if user has requested to unblock failed deployment via annotation
unblockRequested := false
if rollout.Annotations != nil {
if unblock, exists := rollout.Annotations["rollout.kuberik.com/unblock-failed"]; exists && unblock == "true" {

Check failure on line 198 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

string `true` has 4 occurrences, make it a constant (goconst)

Check failure on line 198 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

string `true` has 4 occurrences, make it a constant (goconst)
unblockRequested = true
log.Info("User requested to unblock failed deployment via annotation")
}
Expand Down Expand Up @@ -480,7 +480,7 @@
}

// parseOCIManifest extracts all metadata from OCI image manifest including version, revision, artifact type, source, title, and description.
func (r *RolloutReconciler) parseOCIManifest(ctx context.Context, imageRef string, imagePolicy *imagev1beta2.ImagePolicy) (version, revision, artifactType, source, title, description *string, created *metav1.Time, err error) {

Check failure on line 483 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

cyclomatic complexity 48 of func `(*RolloutReconciler).parseOCIManifest` is high (> 30) (gocyclo)

Check failure on line 483 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

cyclomatic complexity 48 of func `(*RolloutReconciler).parseOCIManifest` is high (> 30) (gocyclo)
log := logf.FromContext(ctx)

// Get authentication keychain from ImageRepository
Expand Down Expand Up @@ -719,7 +719,7 @@
}

// evaluateGates lists and evaluates gates, updates rollout status, and returns filtered candidates and gatesPassing.
func (r *RolloutReconciler) evaluateGates(ctx context.Context, namespace string, rollout *rolloutv1alpha1.Rollout, releaseCandidates []rolloutv1alpha1.VersionInfo) ([]rolloutv1alpha1.VersionInfo, bool, error) {

Check failure on line 722 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

cyclomatic complexity 32 of func `(*RolloutReconciler).evaluateGates` is high (> 30) (gocyclo)

Check failure on line 722 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

cyclomatic complexity 32 of func `(*RolloutReconciler).evaluateGates` is high (> 30) (gocyclo)
// Check for gate bypass annotation
bypassVersion := ""
if rollout.Annotations != nil {
Expand Down Expand Up @@ -1071,7 +1071,7 @@
}

// deployRelease finds and patches Flux resources with the wanted version.
func (r *RolloutReconciler) deployRelease(ctx context.Context, rollout *rolloutv1alpha1.Rollout, wantedRelease string) error {

Check failure on line 1074 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

cyclomatic complexity 45 of func `(*RolloutReconciler).deployRelease` is high (> 30) (gocyclo)

Check failure on line 1074 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

cyclomatic complexity 45 of func `(*RolloutReconciler).deployRelease` is high (> 30) (gocyclo)
log := logf.FromContext(ctx)

// Check if this deployment was done with gate bypass
Expand Down Expand Up @@ -1167,6 +1167,9 @@
// Generate deployment message
deploymentMessage := r.generateDeploymentMessage(rollout, wantedRelease, bypassUsed, forceDeployUsed, unblockUsed)

// Extract triggered by information
triggeredBy := r.extractTriggeredByInfo(rollout, r.hasManualDeployment(rollout))

// Find the version info for the wanted release
var versionInfo rolloutv1alpha1.VersionInfo
versionInfo.Tag = wantedRelease
Expand Down Expand Up @@ -1200,6 +1203,7 @@
Version: versionInfo,
Timestamp: now,
Message: &deploymentMessage,
TriggeredBy: triggeredBy,
BakeStatus: bakeStatus,
BakeStatusMessage: bakeStatusMsg,
BakeStartTime: nil, // Will be set when healthchecks become healthy
Expand Down Expand Up @@ -1288,22 +1292,24 @@

// Clear annotations based on deployment type
if forceDeployUsed {
// Clear both force-deploy and deploy-message annotations when force deploy was used
// Clear both force-deploy, deploy-message, and deploy-user annotations when force deploy was used
patch := client.MergeFrom(rollout.DeepCopy())
delete(rollout.Annotations, "rollout.kuberik.com/force-deploy")
delete(rollout.Annotations, "rollout.kuberik.com/deploy-message")
delete(rollout.Annotations, "rollout.kuberik.com/deploy-user")

if err := r.Client.Patch(ctx, rollout, patch); err != nil {
log.Error(err, "Failed to patch rollout to clear force deploy annotations")
return err
}
} else if r.hasManualDeployment(rollout) {
// Clear only deploy-message annotation when WantedVersion was used
// Clear deploy-message and deploy-user annotations when WantedVersion was used
patch := client.MergeFrom(rollout.DeepCopy())
delete(rollout.Annotations, "rollout.kuberik.com/deploy-message")
delete(rollout.Annotations, "rollout.kuberik.com/deploy-user")

if err := r.Client.Patch(ctx, rollout, patch); err != nil {
log.Error(err, "Failed to patch rollout to clear deploy-message annotation")
log.Error(err, "Failed to patch rollout to clear deploy-message and deploy-user annotations")
return err
}
}
Expand Down Expand Up @@ -1459,7 +1465,7 @@
return nil
}

func (r *RolloutReconciler) handleBakeTime(ctx context.Context, namespace string, rollout *rolloutv1alpha1.Rollout) (ctrl.Result, error) {

Check failure on line 1468 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

cyclomatic complexity 41 of func `(*RolloutReconciler).handleBakeTime` is high (> 30) (gocyclo)

Check failure on line 1468 in internal/controller/rollout_controller.go

View workflow job for this annotation

GitHub Actions / Run on Ubuntu

cyclomatic complexity 41 of func `(*RolloutReconciler).handleBakeTime` is high (> 30) (gocyclo)
log := logf.FromContext(ctx)
now := r.now()

Expand Down Expand Up @@ -1777,6 +1783,26 @@
return 1
}

// extractTriggeredByInfo extracts triggered by information from annotations.
// Returns nil if no user annotation is found (indicating a system-triggered deployment).
func (r *RolloutReconciler) extractTriggeredByInfo(rollout *rolloutv1alpha1.Rollout, isManualDeployment bool) *rolloutv1alpha1.TriggeredByInfo {
// Check for user annotation (similar to how we check deploy-message annotation)
if rollout.Annotations != nil {
if userName, exists := rollout.Annotations["rollout.kuberik.com/deploy-user"]; exists && userName != "" {
return &rolloutv1alpha1.TriggeredByInfo{
Kind: "User",
Name: userName,
}
}
}

// If no user annotation found, it's a system-triggered deployment
return &rolloutv1alpha1.TriggeredByInfo{
Kind: "System",
Name: "rollout-controller",
}
}
Copy link

Choose a reason for hiding this comment

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

Unused parameter causes incorrect deployment trigger attribution

The extractTriggeredByInfo function accepts an isManualDeployment parameter that is explicitly passed at the call site but never used in the function body. When a user triggers a manual deployment via WantedVersion or force-deploy without setting the deploy-user annotation, the deployment is incorrectly recorded as Kind: "System" instead of properly indicating it was user-initiated. The function comment also incorrectly states it "Returns nil if no user annotation is found" but the function never returns nil.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

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

Stale deploy-user annotation persists on automatic deployments

The deploy-user annotation is read and used for any deployment regardless of whether it's a manual deployment, but it's only cleared for manual deployments (forceDeployUsed or hasManualDeployment). This differs from deploy-message which is both used and cleared only for manual deployments. If deploy-user is set on a rollout during an automatic deployment, the annotation persists, causing all subsequent automatic deployments to be incorrectly attributed to that user until the annotation is manually removed.

Additional Locations (1)

Fix in Cursor Fix in Web


// generateDeploymentMessage creates a descriptive message for a deployment history entry
func (r *RolloutReconciler) generateDeploymentMessage(rollout *rolloutv1alpha1.Rollout, wantedRelease string, bypassUsed, forceDeployUsed, unblockUsed bool) string {
var messageParts []string
Expand Down
Loading
Loading