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
43 changes: 15 additions & 28 deletions controllers/clustersummary_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import (
libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1"
"github.com/projectsveltos/libsveltos/lib/clusterproxy"
"github.com/projectsveltos/libsveltos/lib/deployer"
"github.com/projectsveltos/libsveltos/lib/eventreasons"
license "github.com/projectsveltos/libsveltos/lib/licenses"
logs "github.com/projectsveltos/libsveltos/lib/logsettings"
predicates "github.com/projectsveltos/libsveltos/lib/predicates"
Expand Down Expand Up @@ -607,23 +608,13 @@ func (r *ClusterSummaryReconciler) prepareForDeployment(ctx context.Context,
func (r *ClusterSummaryReconciler) proceedDeployingClusterSummary(ctx context.Context,
clusterSummaryScope *scope.ClusterSummaryScope, logger logr.Logger) (reconcile.Result, error) {

// Snapshot existing failure messages before deploying so we can detect
// new conflicts vs. ongoing ones and avoid re-raising an event every retry.
preDeployFailures := collectFailureMessages(clusterSummaryScope.ClusterSummary)

err := r.deploy(ctx, clusterSummaryScope, logger)
if err != nil {
var conflictErr *deployer.ConflictError
ok := errors.As(err, &conflictErr)
if ok {
// The ProfileConflict event is recorded by updateFeatureStatus, when the conflict is persisted
logger.V(logs.LogInfo).Error(err, "failed to deploy because of conflict")
if _, alreadyKnown := preDeployFailures[conflictErr.Error()]; !alreadyKnown {
clusterSummary := clusterSummaryScope.ClusterSummary
r.eventRecorder.Eventf(clusterSummary, nil, corev1.EventTypeWarning, "Conflict",
configv1beta1.ClusterSummaryKind, "Conflict detected for cluster %s %s/%s: %s",
clusterSummary.Spec.ClusterType, clusterSummary.Spec.ClusterNamespace,
clusterSummary.Spec.ClusterName, conflictErr.Error())
}
r.setNextReconcileTime(clusterSummaryScope, r.ConflictRetryTime)
return reconcile.Result{RequeueAfter: r.ConflictRetryTime}, nil
}
Expand Down Expand Up @@ -2143,14 +2134,23 @@ func grantsPullModeEligibility(result *license.LicenseVerificationResult,
func (r *ClusterSummaryReconciler) updateStatusWithMissingLicenseError(
clusterSummaryScope *scope.ClusterSummaryScope, logger logr.Logger) {

notEligibleError := errors.New("license is required to manage SveltosCluster in pull mode")
// All features fail for the same reason: one event, recorded the first time
_, previousMessage := getFeatureFailure(clusterSummaryScope.ClusterSummary, libsveltosv1beta1.FeatureHelm)

failed := libsveltosv1beta1.FeatureStatusFailed
r.updateFeatureStatus(clusterSummaryScope, libsveltosv1beta1.FeatureHelm, &failed, nil,
notEligibleError, logger)
errPullModeLicenseRequired, logger)
r.updateFeatureStatus(clusterSummaryScope, libsveltosv1beta1.FeatureKustomize, &failed, nil,
notEligibleError, logger)
errPullModeLicenseRequired, logger)
r.updateFeatureStatus(clusterSummaryScope, libsveltosv1beta1.FeatureResources, &failed, nil,
notEligibleError, logger)
errPullModeLicenseRequired, logger)

if previousMessage == nil || *previousMessage != errPullModeLicenseRequired.Error() {
clusterSummary := clusterSummaryScope.ClusterSummary
recordEventf(r.eventRecorder, clusterSummary, nil, corev1.EventTypeWarning, eventreasons.LicenseInvalid,
eventreasons.ActionDeploying, "Cluster %s %s/%s: %s", clusterSummary.Spec.ClusterType,
clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName, errPullModeLicenseRequired.Error())
}
}

// removeFinalizer removes the finalizer
Expand Down Expand Up @@ -2272,16 +2272,3 @@ func getClusterSummaryWithInstantiatedCharts(ctx context.Context, cs *configv1be

return &csCopy, nil
}

// collectFailureMessages returns the set of FailureMessages currently recorded
// across all FeatureSummaries. Used to detect whether a conflict is new (not
// yet in the status) versus ongoing (already reported in a previous reconcile).
func collectFailureMessages(cs *configv1beta1.ClusterSummary) map[string]struct{} {
msgs := make(map[string]struct{}, len(cs.Status.FeatureSummaries))
for i := range cs.Status.FeatureSummaries {
if msg := cs.Status.FeatureSummaries[i].FailureMessage; msg != nil {
msgs[*msg] = struct{}{}
}
}
return msgs
}
31 changes: 21 additions & 10 deletions controllers/clustersummary_deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/projectsveltos/libsveltos/lib/clustercache"
"github.com/projectsveltos/libsveltos/lib/clusterproxy"
"github.com/projectsveltos/libsveltos/lib/deployer"
"github.com/projectsveltos/libsveltos/lib/eventreasons"
logs "github.com/projectsveltos/libsveltos/lib/logsettings"
"github.com/projectsveltos/libsveltos/lib/pullmode"
)
Expand Down Expand Up @@ -179,8 +180,8 @@ func (r *ClusterSummaryReconciler) proceedDeployingFeature(ctx context.Context,
clusterSummary.Spec.ClusterType, clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName)
cluster, _ := clusterproxy.GetCluster(ctx, getManagementClusterClient(),
clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName, clusterSummary.Spec.ClusterType)
r.eventRecorder.Eventf(clusterSummary, cluster, corev1.EventTypeNormal, "Sveltos",
configv1beta1.ClusterSummaryKind, message)
recordEventf(r.eventRecorder, clusterSummary, cluster, corev1.EventTypeNormal, eventreasons.FeatureDeployed,
featureAction(eventreasons.ActionDeploying, f.id), "%s", message)
return nil
}

Expand Down Expand Up @@ -251,8 +252,8 @@ func (r *ClusterSummaryReconciler) handleDeployerError(deployerError error, clus
if errors.As(deployerError, &nonRetriableError) {
nonRetriableStatus := libsveltosv1beta1.FeatureStatusFailedNonRetriable
r.updateFeatureStatus(clusterSummaryScope, f.id, &nonRetriableStatus, currentHash, deployerError, logger)
r.eventRecorder.Eventf(clusterSummary, nil, corev1.EventTypeWarning, "FailedNonRetriable",
configv1beta1.ClusterSummaryKind,
recordEventf(r.eventRecorder, clusterSummary, nil, corev1.EventTypeWarning, eventreasons.FeatureFailedNonRetriable,
featureAction(eventreasons.ActionDeploying, f.id),
"Feature %s for cluster %s %s/%s failed with non-retriable error: %s",
f.id, clusterSummary.Spec.ClusterType, clusterSummary.Spec.ClusterNamespace,
clusterSummary.Spec.ClusterName, deployerError.Error())
Expand All @@ -262,8 +263,8 @@ func (r *ClusterSummaryReconciler) handleDeployerError(deployerError error, clus
if errors.As(deployerError, &templateError) {
nonRetriableStatus := libsveltosv1beta1.FeatureStatusFailedNonRetriable
r.updateFeatureStatus(clusterSummaryScope, f.id, &nonRetriableStatus, currentHash, deployerError, logger)
r.eventRecorder.Eventf(clusterSummary, nil, corev1.EventTypeWarning, "FailedNonRetriable",
configv1beta1.ClusterSummaryKind,
recordEventf(r.eventRecorder, clusterSummary, nil, corev1.EventTypeWarning, eventreasons.FeatureFailedNonRetriable,
featureAction(eventreasons.ActionDeploying, f.id),
"Feature %s for cluster %s %s/%s failed to instantiate template: %s",
f.id, clusterSummary.Spec.ClusterType, clusterSummary.Spec.ClusterNamespace,
clusterSummary.Spec.ClusterName, deployerError.Error())
Expand Down Expand Up @@ -327,8 +328,8 @@ func (r *ClusterSummaryReconciler) handlePullModeProvisioned(ctx context.Context
clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName, clusterSummary.Spec.ClusterType)
message := fmt.Sprintf("Feature: %s deployed to cluster %s %s/%s", f.id,
clusterSummary.Spec.ClusterType, clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName)
r.eventRecorder.Eventf(clusterSummary, cluster, corev1.EventTypeNormal, "Sveltos",
configv1beta1.ClusterSummaryKind, message)
recordEventf(r.eventRecorder, clusterSummary, cluster, corev1.EventTypeNormal, eventreasons.FeatureDeployed,
featureAction(eventreasons.ActionDeploying, f.id), "%s", message)
r.updateFeatureStatus(clusterSummaryScope, f.id, pullmodeProvisionedStatus(), currentHash, nil, logger)
clusterSummaryScope.SetFailureMessage(f.id, nil)
now := metav1.NewTime(time.Now())
Expand Down Expand Up @@ -610,10 +611,15 @@ func (r *ClusterSummaryReconciler) processUndeployResult(ctx context.Context, cl
func (r *ClusterSummaryReconciler) markUndeployFailureRetriable(clusterSummaryScope *scope.ClusterSummaryScope,
featureID libsveltosv1beta1.FeatureID, failureErr error, logger logr.Logger) {

// What the ClusterSummary recorded for this feature before this update tells whether this failure is new
_, previousMessage := getFeatureFailure(clusterSummaryScope.ClusterSummary, featureID)

removing := libsveltosv1beta1.FeatureStatusRemoving
r.updateFeatureStatus(clusterSummaryScope, featureID, &removing, nil, failureErr, logger)
errorMsg := failureErr.Error()
clusterSummaryScope.SetFailureMessage(featureID, &errorMsg)
r.recordFeatureFailure(clusterSummaryScope.ClusterSummary, featureID, eventreasons.ActionRemoving,
previousMessage, failureErr)
}

func cleanPreviousActiveTracking(ctx context.Context, clusterSummary *configv1beta1.ClusterSummary, f feature,
Expand Down Expand Up @@ -956,11 +962,16 @@ func (r *ClusterSummaryReconciler) updateFeatureStatus(clusterSummaryScope *scop
// non-terminal statuses above in case that ever changes.
clusterSummaryScope.SetFeatureStatus(featureID, libsveltosv1beta1.FeatureStatusBlocked, hash, nil)
case libsveltosv1beta1.FeatureStatusFailed, libsveltosv1beta1.FeatureStatusFailedNonRetriable:
// What the ClusterSummary recorded for this feature before this update tells whether this failure is new
previousStatus, previousMessage := getFeatureFailure(clusterSummary, featureID)
failed := true
clusterSummaryScope.SetFeatureStatus(featureID, *status, hash, &failed)
if statusError != nil {
err := statusError.Error()
clusterSummaryScope.SetFailureMessage(featureID, &err)
if *status == libsveltosv1beta1.FeatureStatusFailed {
r.recordFailedStatus(clusterSummary, featureID, previousStatus, previousMessage, statusError)
}
}
clusterSummaryScope.SetLastAppliedTime(featureID, &now)
trackReconcileOutcome(clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName,
Expand Down Expand Up @@ -1087,8 +1098,8 @@ func (r *ClusterSummaryReconciler) markMaxConsecutiveFailuresReached(clusterSumm
nonRetriableStatus := libsveltosv1beta1.FeatureStatusFailedNonRetriable
resultError := errors.New("the maximum number of consecutive errors has been reached")
r.updateFeatureStatus(clusterSummaryScope, f.id, &nonRetriableStatus, currentHash, resultError, logger)
r.eventRecorder.Eventf(clusterSummary, nil, corev1.EventTypeWarning, "FailedNonRetriable",
configv1beta1.ClusterSummaryKind,
recordEventf(r.eventRecorder, clusterSummary, nil, corev1.EventTypeWarning, eventreasons.FeatureFailedNonRetriable,
featureAction(eventreasons.ActionDeploying, f.id),
"Feature %s for cluster %s %s/%s will no longer be retried: maximum consecutive failures reached",
f.id, clusterSummary.Spec.ClusterType, clusterSummary.Spec.ClusterNamespace,
clusterSummary.Spec.ClusterName)
Expand Down
169 changes: 169 additions & 0 deletions controllers/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
Copyright 2026. projectsveltos.io. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
"errors"
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/events"

configv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1"
libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1"
"github.com/projectsveltos/libsveltos/lib/deployer"
"github.com/projectsveltos/libsveltos/lib/eventreasons"
)

// recordEventf records an event on regarding, about the related object (can be nil).
// It does nothing when no recorder is set.
// The note is capped, so an oversized message is not rejected by the API server.
func recordEventf(recorder events.EventRecorder, regarding, related runtime.Object, eventType, reason, action,
format string, args ...any) {

if recorder == nil {
return
}

note := eventreasons.TruncateNote(fmt.Sprintf(format, args...))
recorder.Eventf(regarding, related, eventType, reason, action, "%s", note)
}

// featureAction returns the action of an event about a feature: the verb followed by the feature, for
// instance DeployingHelm. The events recorder merges events with the same reason, action and regarding
// object and keeps only the first note, so an action which is the same for every feature would make
// the failures of two features of the same ClusterSummary a single event.
func featureAction(action string, featureID libsveltosv1beta1.FeatureID) string {
return action + string(featureID)
}

// getActionForFeatureStatus returns the action an event refers to given the status the feature had before:
// a feature being removed is Removing, anything else is Deploying.
func getActionForFeatureStatus(previousStatus *libsveltosv1beta1.FeatureStatus) string {
if previousStatus != nil &&
(*previousStatus == libsveltosv1beta1.FeatureStatusRemoving ||
*previousStatus == libsveltosv1beta1.FeatureStatusAgentRemoving) {

return eventreasons.ActionRemoving
}

return eventreasons.ActionDeploying
}

var (
// errPullModeLicenseRequired is the failure of a ClusterSummary targeting a SveltosCluster in pull mode which
// is not covered by a license.
errPullModeLicenseRequired = errors.New("license is required to manage SveltosCluster in pull mode")
)

// hasOwnEvent returns true for the errors which are already reported by their own event: a failure which
// will not be retried (FeatureFailedNonRetriable) and a missing license (LicenseInvalid).
// FeatureFailed is not recorded for those, or the same failure would be reported twice.
func hasOwnEvent(err error) bool {
var nonRetriableError *configv1beta1.NonRetriableError
var templateError *configv1beta1.TemplateInstantiationError

return errors.As(err, &nonRetriableError) ||
errors.As(err, &templateError) ||
errors.Is(err, errPullModeLicenseRequired)
}

// isConflictError returns true when err is, or wraps, a conflict with another profile.
func isConflictError(err error) bool {
var conflictError *deployer.ConflictError

return errors.As(err, &conflictError)
}

// recordFailedStatus records the event for a feature which has just been set to Failed with failure: a
// ProfileConflict for a conflict with another profile, a FeatureFailed for any other failure which is
// retried. The failures which have their own event (hasOwnEvent) are not recorded here.
// previousStatus and previousMessage are what the ClusterSummary recorded before this update.
func (r *ClusterSummaryReconciler) recordFailedStatus(clusterSummary *configv1beta1.ClusterSummary,
featureID libsveltosv1beta1.FeatureID, previousStatus *libsveltosv1beta1.FeatureStatus, previousMessage *string,
failure error) {

switch {
case isConflictError(failure):
r.recordProfileConflict(clusterSummary, previousMessage, failure)
case !hasOwnEvent(failure):
r.recordFeatureFailure(clusterSummary, featureID, getActionForFeatureStatus(previousStatus),
previousMessage, failure)
}
}

// recordProfileConflict records a Warning ProfileConflict event: a resource this ClusterSummary wants to deploy
// is already deployed by another profile. Nothing is recorded when the ClusterSummary already failed with the
// same message: a retry which hits the same conflict is not news.
func (r *ClusterSummaryReconciler) recordProfileConflict(clusterSummary *configv1beta1.ClusterSummary,
previousMessage *string, conflict error) {

message := conflict.Error()
if previousMessage != nil && *previousMessage == message {
return
}

recordEventf(r.eventRecorder, clusterSummary, nil, corev1.EventTypeWarning, eventreasons.ProfileConflict,
eventreasons.ActionDeploying, "Conflict detected for cluster %s %s/%s: %s",
clusterSummary.Spec.ClusterType, clusterSummary.Spec.ClusterNamespace,
clusterSummary.Spec.ClusterName, message)
}

// getFeatureFailure returns the status and the failure message the ClusterSummary currently records for
// the feature. Both are nil when the feature has no entry yet.
// Call it before updating the feature status: the failure message of the previous attempt is what tells
// whether a failure is new.
func getFeatureFailure(clusterSummary *configv1beta1.ClusterSummary, featureID libsveltosv1beta1.FeatureID,
) (status *libsveltosv1beta1.FeatureStatus, message *string) {

featureSummary := getFeatureSummaryForFeatureID(clusterSummary, featureID)
if featureSummary == nil {
return nil, nil
}

currentStatus := featureSummary.Status
status = &currentStatus
if featureSummary.FailureMessage != nil {
currentMessage := *featureSummary.FailureMessage
message = &currentMessage
}

return status, message
}

// recordFeatureFailure records a Warning FeatureFailed event when a feature fails to be deployed, or removed,
// and the operation is going to be retried. Nothing is recorded when the feature already failed with the same
// message (previousMessage): a retry which fails the same way is not news. A different message is recorded.
// The failure message is cleared when the feature succeeds, so a failure after a success is recorded again.
func (r *ClusterSummaryReconciler) recordFeatureFailure(clusterSummary *configv1beta1.ClusterSummary,
featureID libsveltosv1beta1.FeatureID, action string, previousMessage *string, failure error) {

if failure == nil {
return
}

message := failure.Error()
if previousMessage != nil && *previousMessage == message {
return
}

recordEventf(r.eventRecorder, clusterSummary, nil, corev1.EventTypeWarning, eventreasons.FeatureFailed,
featureAction(action, featureID), "Feature %s for cluster %s %s/%s failed: %s",
featureID, clusterSummary.Spec.ClusterType, clusterSummary.Spec.ClusterNamespace,
clusterSummary.Spec.ClusterName, message)
}
Loading
Loading