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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ ARCH ?= $(shell go env GOARCH)
OS ?= $(shell uname -s | tr A-Z a-z)
K8S_LATEST_VER ?= $(shell curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)
export CONTROLLER_IMG ?= $(REGISTRY)/$(IMAGE_NAME)
TAG ?= v1.16.0
TAG ?= main

## Tool Binaries
CONTROLLER_GEN := $(TOOLS_BIN_DIR)/controller-gen
Expand Down
2 changes: 1 addition & 1 deletion config/default/manager_auth_proxy_patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ spec:
- "--shard-key="
- --capi-onboard-annotation=
- "--v=5"
- "--version=v1.16.0"
- "--version=main"
- "--registry="
- "--agent-in-mgmt-cluster=false"
env:
Expand Down
4 changes: 2 additions & 2 deletions config/default/manager_image_patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ spec:
template:
spec:
initContainers:
- image: docker.io/projectsveltos/classifier:v1.16.0
- image: docker.io/projectsveltos/classifier:main
name: migrate
containers:
# Change the value of image field below to your controller image URL
- image: docker.io/projectsveltos/classifier:v1.16.0
- image: docker.io/projectsveltos/classifier:main
name: manager
8 changes: 8 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ rules:
- get
- list
- watch
- apiGroups:
- events.k8s.io
resources:
- events
verbs:
- create
- patch
- update
- apiGroups:
- lib.projectsveltos.io
resources:
Expand Down
25 changes: 25 additions & 0 deletions controllers/classifier_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/events"
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
Expand All @@ -42,6 +43,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"
logs "github.com/projectsveltos/libsveltos/lib/logsettings"
predicates "github.com/projectsveltos/libsveltos/lib/predicates"
libsveltosset "github.com/projectsveltos/libsveltos/lib/set"
Expand Down Expand Up @@ -115,6 +117,9 @@ type ClassifierReconciler struct {
// List of current existing Classifiers
AllClassifierSet libsveltosset.Set

// EventRecorder is used to record events on Classifier instances. No event is recorded when nil.
EventRecorder events.EventRecorder

Logger logr.Logger
}

Expand All @@ -133,6 +138,7 @@ type ClassifierReconciler struct {
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;delete
//+kubebuilder:rbac:groups="apps",resources=deployments,verbs=get;list;watch
//+kubebuilder:rbac:groups="events.k8s.io",resources=events,verbs=create;update;patch

func (r *ClassifierReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
if r.ShardKey != "" {
Expand Down Expand Up @@ -279,6 +285,7 @@ func (r *ClassifierReconciler) reconcileNormal(
matchingClusters, err := r.syncAndGetMatchingClusters(ctx, classifierScope, logger)
if err != nil {
logger.V(logs.LogDebug).Error(err, "failed to get matching clusters")
r.recordReconcileFailure(classifierScope.Classifier, "get matching clusters", err)
return reconcile.Result{RequeueAfter: normalRequeueAfter}, nil
}
trackMatchingClusters(classifierScope.Classifier.Name, len(matchingClusters), logger)
Expand All @@ -288,13 +295,15 @@ func (r *ClassifierReconciler) reconcileNormal(
if err != nil {
// Use Error level because this indicates a failure to clean up resources
logger.V(logs.LogDebug).Error(err, "failed to clean up labels/registrations for non-matching clusters")
r.recordReconcileFailure(classifierScope.Classifier, "clean up labels and registrations for clusters no longer matching", err)
return reconcile.Result{RequeueAfter: normalRequeueAfter}, nil
}

// For every currently matching cluster, update label ownership registrations
matchingStatuses, err := r.updateMatchingClustersAndRegistrations(ctx, classifierScope, matchingClusters, logger)
if err != nil {
logger.V(logs.LogDebug).Error(err, "failed to update status/registrations for matching clusters")
r.recordReconcileFailure(classifierScope.Classifier, "update status and registrations for matching clusters", err)
return reconcile.Result{RequeueAfter: normalRequeueAfter}, nil
}
trackLabelConflicts(classifierScope.Classifier.Name, countUnManagedLabelClusters(matchingStatuses), logger)
Expand All @@ -303,23 +312,28 @@ func (r *ClassifierReconciler) reconcileNormal(
err = r.updateLabelsOnMatchingClusters(ctx, classifierScope, matchingStatuses, logger)
if err != nil {
logger.V(logs.LogDebug).Error(err, "failed to apply labels to matching clusters")
r.recordReconcileFailure(classifierScope.Classifier, "apply labels to matching clusters", err)
return reconcile.Result{RequeueAfter: normalRequeueAfter}, nil
}

err = r.ensureClassifierReports(ctx, classifierScope)
if err != nil {
logger.V(logs.LogDebug).Error(err, "failed to ensure ClassifierReports")
r.recordReconcileFailure(classifierScope.Classifier, "ensure ClassifierReports", err)
return reconcile.Result{RequeueAfter: normalRequeueAfter}, nil
}

if err := r.updateMaps(ctx, classifierScope); err != nil {
logger.V(logs.LogDebug).Error(err, "failed to update internal maps")
r.recordReconcileFailure(classifierScope.Classifier, "update internal maps", err)
return reconcile.Result{RequeueAfter: normalRequeueAfter}, nil
}

f := getHandlersForFeature(libsveltosv1beta1.FeatureClassifier)

if err := r.deployClassifier(ctx, classifierScope, f, logger); err != nil {
// No event here: the error is also returned while the deployment is still in progress.
// Terminal failures are recorded per cluster, in deployClassifier.
logger.V(logs.LogInfo).Error(err, "failed to deploy")
return reconcile.Result{RequeueAfter: normalRequeueAfter}, nil
}
Expand All @@ -332,6 +346,14 @@ func (r *ClassifierReconciler) reconcileNormal(
return reconcile.Result{}, nil
}

// recordReconcileFailure records a Warning event on the Classifier for a reconcile step that failed.
// Those errors are only logged at debug level and the reconciler retries, so without the event
// the failure is not visible to the user.
func (r *ClassifierReconciler) recordReconcileFailure(classifier *libsveltosv1beta1.Classifier, step string, err error) {
recordWarningf(r.EventRecorder, classifier, eventreasons.ClassifierFailed, eventreasons.ActionReconciling,
"failed to %s: %v", step, err)
}

// SetupWithManager sets up the controller with the Manager.
func (r *ClassifierReconciler) SetupWithManager(ctx context.Context,
mgr ctrl.Manager, logger logr.Logger) (controller.Controller, error) {
Expand Down Expand Up @@ -525,6 +547,9 @@ func (r *ClassifierReconciler) updateMatchingClustersAndRegistrations(ctx contex

if len(unmanaged) > 0 {
hasUnmanaged = true
recordWarningf(r.EventRecorder, classifierScope.Classifier, eventreasons.ClassifierLabelConflict,
eventreasons.ActionReconciling, "cluster %s/%s: label keys %s are managed by another Classifier",
c.Namespace, c.Name, unmanagedLabelKeys(unmanaged))
}

if err := updateClassifierReportLabelStatus(ctx, r.Client, classifierScope.Classifier.Name,
Expand Down
10 changes: 10 additions & 0 deletions controllers/classifier_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/events"
"k8s.io/klog/v2/textlogger"
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
ctrl "sigs.k8s.io/controller-runtime"
Expand Down Expand Up @@ -321,12 +322,14 @@ var _ = Describe("Classifier: Reconciler", func() {
// cluster even though cluster is a match
manager.RegisterClassifierForLabels(classifier1, clusterNamespace, clusterName, libsveltosv1beta1.ClusterTypeCapi)

recorder := events.NewFakeRecorder(eventBufferSize)
reconciler := &controllers.ClassifierReconciler{
Client: c,
Scheme: scheme,
ClusterMap: make(map[corev1.ObjectReference]*libsveltosset.Set),
ClassifierMap: make(map[corev1.ObjectReference]*libsveltosset.Set),
Mux: sync.Mutex{},
EventRecorder: recorder,
Logger: logger,
}

Expand Down Expand Up @@ -358,6 +361,13 @@ var _ = Describe("Classifier: Reconciler", func() {
updatedReport)).To(Succeed())
Expect(updatedReport.Status.ManagedLabels).To(BeEmpty())
Expect(updatedReport.Status.UnManagedLabels).To(HaveLen(len(classifier.Spec.ClassifierLabels)))

// The conflict is also reported as an event, so it is visible without reading ClassifierReport.
var recorded string
Expect(recorder.Events).To(Receive(&recorded))
Expect(recorded).To(HavePrefix("Warning ClassifierLabelConflict cluster " + clusterNamespace + "/" + clusterName))
Expect(recorded).To(ContainSubstring("managed by another Classifier"))
Expect(recorded).To(ContainSubstring(classifier.Spec.ClassifierLabels[0].Key))
})

It("updateLabelsOnMatchingClusters updates CAPI Cluster labels", func() {
Expand Down
26 changes: 26 additions & 0 deletions controllers/classifier_deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import (
"github.com/projectsveltos/libsveltos/lib/clusterproxy"
"github.com/projectsveltos/libsveltos/lib/crd"
"github.com/projectsveltos/libsveltos/lib/deployer"
"github.com/projectsveltos/libsveltos/lib/eventreasons"
"github.com/projectsveltos/libsveltos/lib/k8s_utils"
logs "github.com/projectsveltos/libsveltos/lib/logsettings"
"github.com/projectsveltos/libsveltos/lib/patcher"
Expand Down Expand Up @@ -188,6 +189,7 @@ func (r *ClassifierReconciler) deployClassifier(ctx context.Context, classifierS
errorSeen = updateErr
} else {
trackClassifierOutcome(classifierScope.Classifier.Name, cluster, clusterInfo, l)
r.recordDeploymentFailure(classifierScope.Classifier, cluster, clusterInfo)
}
if clusterInfo.Status != libsveltosv1beta1.SveltosStatusProvisioned {
allProcessed = false
Expand All @@ -204,6 +206,30 @@ func (r *ClassifierReconciler) deployClassifier(ctx context.Context, classifierS
return nil
}

// recordDeploymentFailure records a Warning event on the Classifier when the deployment in a cluster
// failed. Only terminal failures are recorded: a deployment that is still in progress, or a cluster
// that is paused or not ready yet, is an expected state and not something the user must act on.
func (r *ClassifierReconciler) recordDeploymentFailure(classifier *libsveltosv1beta1.Classifier,
cluster *corev1.ObjectReference, clusterInfo *libsveltosv1beta1.ClusterInfo) {

if clusterInfo.FailureMessage == nil || *clusterInfo.FailureMessage == "" {
return
}

switch clusterInfo.Status {
case libsveltosv1beta1.SveltosStatusFailed:
recordWarningf(r.EventRecorder, classifier, eventreasons.ClassifierFailed, eventreasons.ActionDeploying,
"cluster %s/%s: %s", cluster.Namespace, cluster.Name, *clusterInfo.FailureMessage)
case libsveltosv1beta1.SveltosStatusFailedNonRetriable:
// The only non retriable failure is the agent of a cluster in pull mode not reporting. That is a
// problem of the cluster, not of this Classifier: it is not reported here, once per Classifier, but
// once per cluster by sveltoscluster-manager.
case libsveltosv1beta1.SveltosStatusProvisioning, libsveltosv1beta1.SveltosStatusProvisioned,
libsveltosv1beta1.SveltosStatusRemoving, libsveltosv1beta1.SveltosStatusRemoved:
// Not a failure: nothing to report
}
}

func (r *ClassifierReconciler) undeployClassifier(ctx context.Context, classifierScope *scope.ClassifierScope,
f feature, logger logr.Logger) error {

Expand Down
53 changes: 53 additions & 0 deletions controllers/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
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 (
"fmt"
"strings"

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

libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1"
"github.com/projectsveltos/libsveltos/lib/eventreasons"
)

// recordWarningf records a Warning event on regarding. It does nothing when no recorder is set.
// The note is capped, so an oversized message is not rejected by the API server. Identical events
// are aggregated by the recorder, so notes must not contain anything that changes between
// reconciliations (timestamps, counters).
func recordWarningf(recorder events.EventRecorder, regarding runtime.Object, reason, action, format string, args ...any) {
if recorder == nil {
return
}

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

// unmanagedLabelKeys returns the keys of the labels the Classifier wants to manage but cannot,
// because they are managed by another instance, as a single comma separated string.
func unmanagedLabelKeys(unmanaged []libsveltosv1beta1.UnManagedLabel) string {
keys := make([]string, len(unmanaged))
for i := range unmanaged {
keys[i] = unmanaged[i].Key
}

return strings.Join(keys, ", ")
}
Loading
Loading