From 9f16188207bc0f9bd0c69ee8479a4608aab66791 Mon Sep 17 00:00:00 2001 From: Gianluca Mardente Date: Tue, 14 Jul 2026 14:57:41 +0200 Subject: [PATCH] feat: rework metrics, add matching/conflict visibility Metrics (all `projectsveltos_` prefixed): - **reconcile_duration_seconds** (histogram): cluster_type, cluster_namespace, cluster_name. Duration of programming a Classifier on a workload cluster. - **reconcile_outcome_total** (counter): cluster_type, cluster_namespace, cluster_name, status, classifier_name. Terminal Provisioned/Failed outcomes only. - **reconcile_last_success_timestamp_seconds** (gauge): same labels minus status. Unix timestamp of the last Provisioned outcome. - **matching_clusters** (gauge): classifier_name. Clusters currently matching a Classifier's rules. - **label_conflicts** (gauge): classifier_name. Clusters where this Classifier currently has at least one label key it can't manage because another Classifier already owns it. - **mgmt_cluster_matching_clusters** / **mgmt_cluster_label_conflicts** (gauges): classifier_name. Same concepts as above, kept as separate metrics for ManagementClusterClassifier since it's a distinct CRD/reconciler with its own matching semantics (Lua evaluated against management-cluster resources, no deploy-status state machine to hook a duration/outcome metric into). Also: - Enable auth-proxy RBAC resources - Add manifest/service_monitor.yaml --- .github/workflows/codeql.yaml | 51 ++++ .github/workflows/scorecard.yaml | 45 +++ README.md | 3 +- SECURITY.md | 47 +++ config/rbac/kustomization.yaml | 8 +- controllers/classifier_controller.go | 14 + controllers/classifier_deployer.go | 2 + controllers/metrics.go | 276 +++++++++++++++--- .../mgmtcluster_classifier_controller.go | 8 +- manifest/manifest.yaml | 57 ++++ manifest/service_monitor.yaml | 21 ++ renovate.json | 11 + 12 files changed, 487 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/codeql.yaml create mode 100644 .github/workflows/scorecard.yaml create mode 100644 SECURITY.md create mode 100644 manifest/service_monitor.yaml create mode 100644 renovate.json diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 0000000..3e20059 --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,51 @@ +name: CodeQL +on: + push: + branches: + - 'main' + pull_request: + branches: + - 'main' + schedule: + - cron: '21 3 * * 3' + +permissions: read-all + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: ['go'] + + steps: + - name: checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version: 1.26.5 + + - name: Initialize CodeQL + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + languages: ${{ matrix.language }} + + - name: Build + # Autobuild's heuristic only traced a fraction of the module (79 of 189 files per the + # code-scanning diagnostic). Build the whole module explicitly so CodeQL sees every package. + run: go build ./... + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml new file mode 100644 index 0000000..58dc5be --- /dev/null +++ b/.github/workflows/scorecard.yaml @@ -0,0 +1,45 @@ +name: Scorecard supply-chain security +on: + branch_protection_rule: + schedule: + - cron: '30 2 * * 1' + push: + branches: + - 'main' + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + contents: read + actions: read + + steps: + - name: checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + - name: Upload to code-scanning + uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + sarif_file: results.sarif diff --git a/README.md b/README.md index 45e9873..4802355 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![CI](https://github.com/projectsveltos/classifier/actions/workflows/main.yaml/badge.svg)](https://github.com/projectsveltos/classifier/actions) -[![Go Report Card](https://goreportcard.com/badge/github.com/projectsveltos/classifier)](https://goreportcard.com/report/github.com/projectsveltos/classifier) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/projectsveltos/classifier/badge)](https://scorecard.dev/viewer/?uri=github.com/projectsveltos/classifier) +[![CodeQL](https://github.com/projectsveltos/classifier/actions/workflows/codeql.yaml/badge.svg)](https://github.com/projectsveltos/classifier/actions/workflows/codeql.yaml) [![Release](https://img.shields.io/github/v/release/projectsveltos/classifier)](https://github.com/projectsveltos/classifier/releases) [![License](https://img.shields.io/badge/license-Apache-blue.svg)](LICENSE) [![Slack](https://img.shields.io/badge/join%20slack-%23projectsveltos-brighteen)](https://join.slack.com/t/projectsveltos/shared_invite/zt-1hraownbr-W8NTs6LTimxLPB8Erj8Q6Q) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1e24d31 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,47 @@ +# Security Policy + +## Supported Versions + +We release security fixes for the latest minor version. We encourage all users to stay on the latest release. + +| Version | Supported | +|----------------|--------------------| +| latest release | :white_check_mark: | +| older releases | :x: | + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +If you believe you have found a security vulnerability in any Sveltos repository, please report it responsibly by sending an email to: + +**support@projectsveltos.io** + +Please include as much of the following information as possible to help us understand and resolve the issue quickly: + +- A description of the vulnerability and its potential impact +- The affected component(s) and version(s) +- Step-by-step instructions to reproduce the issue +- Any proof-of-concept or exploit code (if applicable) +- Suggested remediation (if any) + +## Response Process + +- You will receive an acknowledgement within **2 business days** +- We will investigate and keep you informed of our progress +- Once the issue is confirmed, we will work on a fix and coordinate a release +- We will publicly disclose the vulnerability after a fix is available, giving you credit unless you prefer to remain anonymous + +## Scope + +This policy covers all projects under the [projectsveltos](https://github.com/projectsveltos) GitHub organization. + +## Out of Scope + +- Vulnerabilities in dependencies (please report those to the upstream project) +- Issues in non-production branches or unreleased code +- Social engineering attacks + +## Thank You + +We appreciate responsible disclosure and the work of the security community in keeping Sveltos and its users safe. diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 5640336..204c8c7 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -11,7 +11,7 @@ resources: # Comment the following 4 lines if you want to disable # the auth proxy (https://github.com/brancz/kube-rbac-proxy) # which protects your /metrics endpoint. -# - auth_proxy_service.yaml -# - auth_proxy_role.yaml -# - auth_proxy_role_binding.yaml -# - auth_proxy_client_clusterrole.yaml +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/controllers/classifier_controller.go b/controllers/classifier_controller.go index f76ae7e..5dbcfcf 100644 --- a/controllers/classifier_controller.go +++ b/controllers/classifier_controller.go @@ -281,6 +281,7 @@ func (r *ClassifierReconciler) reconcileNormal( logger.V(logs.LogDebug).Error(err, "failed to get matching clusters") return reconcile.Result{Requeue: true, RequeueAfter: normalRequeueAfter}, nil } + trackMatchingClusters(classifierScope.Classifier.Name, len(matchingClusters), logger) // For clusters that no longer match, remove managed labels and clear internal registrations err = r.cleanUpManagedResources(ctx, classifierScope, matchingClusters, logger) @@ -296,6 +297,7 @@ func (r *ClassifierReconciler) reconcileNormal( logger.V(logs.LogDebug).Error(err, "failed to update status/registrations for matching clusters") return reconcile.Result{Requeue: true, RequeueAfter: normalRequeueAfter}, nil } + trackLabelConflicts(classifierScope.Classifier.Name, countUnManagedLabelClusters(matchingStatuses), logger) // Apply the labels to the clusters err = r.updateLabelsOnMatchingClusters(ctx, classifierScope, matchingStatuses, logger) @@ -969,3 +971,15 @@ func hasUnManagedLabels(statuses []libsveltosv1beta1.MachingClusterStatus) bool } return false } + +// countUnManagedLabelClusters returns the number of clusters in statuses with at least one label +// conflict. +func countUnManagedLabelClusters(statuses []libsveltosv1beta1.MachingClusterStatus) int { + count := 0 + for i := range statuses { + if len(statuses[i].UnManagedLabels) > 0 { + count++ + } + } + return count +} diff --git a/controllers/classifier_deployer.go b/controllers/classifier_deployer.go index 5e7079c..263f18a 100644 --- a/controllers/classifier_deployer.go +++ b/controllers/classifier_deployer.go @@ -177,6 +177,8 @@ func (r *ClassifierReconciler) deployClassifier(ctx context.Context, classifierS classifierScope.Classifier.Name, cluster, clusterInfo); updateErr != nil { l.V(logs.LogInfo).Error(updateErr, "failed to update ClassifierReport deployment status") errorSeen = updateErr + } else { + trackClassifierOutcome(classifierScope.Classifier.Name, cluster, clusterInfo, l) } if clusterInfo.Status != libsveltosv1beta1.SveltosStatusProvisioned { allProcessed = false diff --git a/controllers/metrics.go b/controllers/metrics.go index 5edc4cc..15edb3b 100644 --- a/controllers/metrics.go +++ b/controllers/metrics.go @@ -14,87 +14,267 @@ limitations under the License. package controllers import ( - "errors" "fmt" - "strings" "time" "github.com/go-logr/logr" "github.com/prometheus/client_golang/prometheus" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/metrics" + "github.com/projectsveltos/libsveltos/lib/clusterproxy" + libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1" logs "github.com/projectsveltos/libsveltos/lib/logsettings" ) const ( - classifierMetricName = "program_classifier_time_seconds" - classifierMetricHelp = "Program Classifier on a workload cluster duration distribution" + // metricNamespace prefixes every custom metric in this file, purely to avoid name collisions with + // unrelated tools scraped by the same Prometheus instance. Deliberately a fixed literal, not derived + // from getSveltosNamespace() (the Kubernetes namespace this component happens to be deployed into, + // which is user-configurable per install): a metric name prefix should stay stable across every + // install so dashboards/alerts built against it work the same way everywhere, regardless of which + // Kubernetes namespace a given cluster chose to deploy classifier into. + metricNamespace = "projectsveltos" + + metricClusterNameLabel = "cluster_name" + metricClusterNamespaceLabel = "cluster_namespace" + metricClusterTypeLabel = "cluster_type" + metricStatusLabel = "status" + metricClassifierNameLabel = "classifier_name" + + statusSuccess = "success" + statusFailure = "failure" ) var ( - programClassifierDurationHistogram = prometheus.NewHistogram( + // reconcileDurationHistogram tracks how long it takes to program a Classifier (deploy sveltos-agent, + // required CRDs, and the Classifier instance itself) on a workload cluster. Labeled by cluster so it + // can be filtered to a single cluster, aggregated to a namespace, or averaged fleet-wide. + // + // Not labeled by Classifier name: this is observed from programDuration, which is invoked as a + // libsveltos/lib/deployer.MetricHandler callback — a type shared with several other components + // (addon-controller, event-manager, healthcheck-manager, etc.). That callback only carries cluster + // identity and featureID, not the Classifier that requested the work, so adding that label here would + // require changing the shared MetricHandler signature across all of them. reconcileOutcomeCounter and + // reconcileLastSuccessTimestampGauge below get the Classifier name instead, since both are recorded + // from classifier-local code (deployClassifier) that already has it on hand. + // + // Replaces the previous programClassifierDurationHistogram/newClassifierHistogram pair, which had the + // same per-cluster metric-name cardinality bug fixed in addon-controller and event-manager: baking + // cluster identity into the Prometheus metric Namespace (and thus its exported name) instead of using + // a label, producing one distinct, never-unregistered metric per cluster. + reconcileDurationHistogram = prometheus.NewHistogramVec( prometheus.HistogramOpts{ - Namespace: getSveltosNamespace(), - Name: classifierMetricName, - Help: classifierMetricHelp, - Buckets: []float64{0.1, 0.5, 1, 5, 10, 20, 30}, + Namespace: metricNamespace, + Name: "reconcile_duration_seconds", + Help: "Duration distribution of programming a Classifier on a workload cluster", + Buckets: []float64{0.1, 0.5, 1, 2, 5, 10, 20, 30, 60, 120}, }, + []string{metricClusterTypeLabel, metricClusterNamespaceLabel, metricClusterNameLabel}, + ) + + // reconcileOutcomeCounter tracks terminal reconcile outcomes (success/failure) per Classifier and + // cluster. Incremented from deployClassifier, the one place that already knows whether processing a + // cluster ended up Provisioned (success) or Failed (failure). + reconcileOutcomeCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: metricNamespace, + Name: "reconcile_outcome_total", + Help: "Total number of terminal reconcile outcomes for a Classifier on a cluster, by outcome", + }, + []string{metricClusterTypeLabel, metricClusterNamespaceLabel, metricClusterNameLabel, metricStatusLabel, + metricClassifierNameLabel}, + ) + + // reconcileLastSuccessTimestampGauge records when a Classifier last reached a successful terminal + // state (Provisioned) for a cluster. Only ever moves forward on success and is left untouched on + // failure, so it answers "how long has it been since this last worked" even for something that fails + // intermittently rather than in a tight consecutive streak. + reconcileLastSuccessTimestampGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: metricNamespace, + Name: "reconcile_last_success_timestamp_seconds", + Help: "Unix timestamp of the last successful (Provisioned) terminal outcome for a Classifier on a cluster", + }, + []string{metricClusterTypeLabel, metricClusterNamespaceLabel, metricClusterNameLabel, metricClassifierNameLabel}, + ) + + // matchingClustersGauge tracks how many clusters currently match a given Classifier's rules + // (kubernetesVersionConstraints, deployedResourceConstraint) and so receive its classifierLabels. Set + // from ClassifierReconciler right after it computes the matching set, independent of any per-cluster + // deploy activity. + matchingClustersGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: metricNamespace, + Name: "matching_clusters", + Help: "Number of clusters currently matching a Classifier's rules", + }, + []string{metricClassifierNameLabel}, + ) + + // labelConflictsGauge tracks how many clusters currently have at least one label key this Classifier + // wants to manage but cannot, because another Classifier already owns that key on that cluster (see + // controllers/keymanager). A gauge, not a counter: it reflects the current conflict state, not a + // historical count, so it answers "is this Classifier losing label ownership right now." + labelConflictsGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: metricNamespace, + Name: "label_conflicts", + Help: "Number of clusters where this Classifier has at least one label key it cannot manage due to a conflict", + }, + []string{metricClassifierNameLabel}, + ) + + // mgmtClusterMatchingClustersGauge is the ManagementClusterClassifier equivalent of + // matchingClustersGauge. Kept as a separate metric (rather than an added label on + // matchingClustersGauge) because ManagementClusterClassifier is a distinct CRD/reconciler with its own + // matching semantics (a Lua function evaluated over management-cluster resources, not + // kubernetesVersionConstraints/deployedResourceConstraint against managed clusters). + mgmtClusterMatchingClustersGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: metricNamespace, + Name: "mgmt_cluster_matching_clusters", + Help: "Number of clusters currently matching a ManagementClusterClassifier's classificationLua", + }, + []string{metricClassifierNameLabel}, + ) + + // mgmtClusterLabelConflictsGauge is the ManagementClusterClassifier equivalent of + // labelConflictsGauge, kept separate for the same reason as mgmtClusterMatchingClustersGauge above. + mgmtClusterLabelConflictsGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: metricNamespace, + Name: "mgmt_cluster_label_conflicts", + Help: "Number of clusters where this ManagementClusterClassifier has at least one label key it cannot manage due to a conflict", + }, + []string{metricClassifierNameLabel}, ) ) //nolint:gochecknoinits // forced pattern, can't workaround func init() { // Register custom metrics with the global prometheus registry - metrics.Registry.MustRegister(programClassifierDurationHistogram) + metrics.Registry.MustRegister(reconcileDurationHistogram, reconcileOutcomeCounter, + reconcileLastSuccessTimestampGauge, matchingClustersGauge, labelConflictsGauge, + mgmtClusterMatchingClustersGauge, mgmtClusterLabelConflictsGauge) } -func newClassifierHistogram(clusterNamespace, clusterName string, clusterType libsveltosv1beta1.ClusterType, - logger logr.Logger) prometheus.Histogram { +func programDuration(elapsed time.Duration, clusterNamespace, clusterName, featureID string, + clusterType libsveltosv1beta1.ClusterType, logger logr.Logger) { - clusterInfo := strings.ReplaceAll(fmt.Sprintf("%s_%s_%s", clusterType, clusterNamespace, clusterName), "-", "_") - histogram := prometheus.NewHistogram( - prometheus.HistogramOpts{ - Namespace: clusterInfo, - Name: classifierMetricName, - Help: classifierMetricHelp, - Buckets: []float64{0.1, 0.5, 1, 5, 10, 20, 30}, - }, - ) + if featureID != string(libsveltosv1beta1.FeatureClassifier) { + return + } + + reconcileDurationHistogram.With(prometheus.Labels{ + metricClusterTypeLabel: string(clusterType), + metricClusterNamespaceLabel: clusterNamespace, + metricClusterNameLabel: clusterName, + }).Observe(elapsed.Seconds()) + + logger.V(logs.LogVerbose).Info(fmt.Sprintf("recorded duration for %s/%s %s: %s", + clusterNamespace, clusterName, featureID, elapsed)) +} + +// trackReconcileOutcome records a terminal reconcile outcome for a Classifier on a cluster. success is +// true for SveltosStatusProvisioned, false for SveltosStatusFailed. Non-terminal statuses (Provisioning, +// Removing, Removed) must not call this. +func trackReconcileOutcome(clusterNamespace, clusterName string, clusterType libsveltosv1beta1.ClusterType, + success bool, classifierName string, logger logr.Logger) { - err := metrics.Registry.Register(histogram) - if err != nil { - var registrationError *prometheus.AlreadyRegisteredError - ok := errors.As(err, ®istrationError) - if ok { - _, ok = registrationError.ExistingCollector.(prometheus.Histogram) - if ok { - return registrationError.ExistingCollector.(prometheus.Histogram) - } - logCollectorError(err, logger) - return nil - } - logCollectorError(err, logger) - return nil + status := statusFailure + if success { + status = statusSuccess } - return histogram + reconcileOutcomeCounter.With(prometheus.Labels{ + metricClusterTypeLabel: string(clusterType), + metricClusterNamespaceLabel: clusterNamespace, + metricClusterNameLabel: clusterName, + metricStatusLabel: status, + metricClassifierNameLabel: classifierName, + }).Inc() + + logger.V(logs.LogVerbose).Info(fmt.Sprintf("Tracking reconcile outcome for %s %s/%s classifier %s: %s", + clusterType, clusterNamespace, clusterName, classifierName, status)) } -func logCollectorError(err error, logger logr.Logger) { - logger.V(logs.LogInfo).Info(fmt.Sprintf("failed to register collector: %v", err)) +// trackLastSuccess records the timestamp of the most recent successful (Provisioned) terminal outcome for +// a Classifier on a cluster. Only called from the success branch. +func trackLastSuccess(clusterNamespace, clusterName string, clusterType libsveltosv1beta1.ClusterType, + timestamp time.Time, classifierName string, logger logr.Logger) { + + reconcileLastSuccessTimestampGauge.With(prometheus.Labels{ + metricClusterTypeLabel: string(clusterType), + metricClusterNamespaceLabel: clusterNamespace, + metricClusterNameLabel: clusterName, + metricClassifierNameLabel: classifierName, + }).Set(float64(timestamp.Unix())) + + logger.V(logs.LogVerbose).Info(fmt.Sprintf("Tracking last success for %s %s/%s classifier %s: %s", + clusterType, clusterNamespace, clusterName, classifierName, timestamp)) } -func programDuration(elapsed time.Duration, clusterNamespace, clusterName, featureID string, - clusterType libsveltosv1beta1.ClusterType, logger logr.Logger) { +// trackMatchingClusters records how many clusters currently match a Classifier's rules. +func trackMatchingClusters(classifierName string, count int, logger logr.Logger) { + matchingClustersGauge.With(prometheus.Labels{ + metricClassifierNameLabel: classifierName, + }).Set(float64(count)) + + logger.V(logs.LogVerbose).Info(fmt.Sprintf("Tracking matching clusters for classifier %s: %d", + classifierName, count)) +} + +// trackLabelConflicts records how many clusters currently have at least one label key this Classifier +// cannot manage due to a conflict with another Classifier. +func trackLabelConflicts(classifierName string, count int, logger logr.Logger) { + labelConflictsGauge.With(prometheus.Labels{ + metricClassifierNameLabel: classifierName, + }).Set(float64(count)) + + logger.V(logs.LogVerbose).Info(fmt.Sprintf("Tracking label conflicts for classifier %s: %d", + classifierName, count)) +} + +// trackMgmtClusterMatchingClusters records how many clusters currently match a +// ManagementClusterClassifier's classificationLua. +func trackMgmtClusterMatchingClusters(classifierName string, count int, logger logr.Logger) { + mgmtClusterMatchingClustersGauge.With(prometheus.Labels{ + metricClassifierNameLabel: classifierName, + }).Set(float64(count)) + + logger.V(logs.LogVerbose).Info(fmt.Sprintf("Tracking matching clusters for managementclusterclassifier %s: %d", + classifierName, count)) +} + +// trackMgmtClusterLabelConflicts records how many clusters currently have at least one label key this +// ManagementClusterClassifier cannot manage due to a conflict with another Classifier. +func trackMgmtClusterLabelConflicts(classifierName string, count int, logger logr.Logger) { + mgmtClusterLabelConflictsGauge.With(prometheus.Labels{ + metricClassifierNameLabel: classifierName, + }).Set(float64(count)) + + logger.V(logs.LogVerbose).Info(fmt.Sprintf("Tracking label conflicts for managementclusterclassifier %s: %d", + classifierName, count)) +} + +// trackClassifierOutcome records reconcile_outcome_total/reconcile_last_success_timestamp_seconds for +// terminal Classifier statuses (Provisioned/Failed). Non-terminal statuses (Provisioning, Removing, +// Removed) are left untouched: they are not outcomes, they are still in flight. +func trackClassifierOutcome(classifierName string, cluster *corev1.ObjectReference, + clusterInfo *libsveltosv1beta1.ClusterInfo, logger logr.Logger) { + + clusterType := clusterproxy.GetClusterType(cluster) - if featureID == string(libsveltosv1beta1.FeatureClassifier) { - programClassifierDurationHistogram.Observe(elapsed.Seconds()) - clusterHistogram := newClassifierHistogram(clusterNamespace, clusterName, clusterType, logger) - if clusterHistogram != nil { - logger.V(logs.LogVerbose).Info(fmt.Sprintf("register data for %s/%s %s", - clusterNamespace, clusterName, featureID)) - clusterHistogram.Observe(elapsed.Seconds()) - } + switch clusterInfo.Status { + case libsveltosv1beta1.SveltosStatusProvisioned: + trackReconcileOutcome(cluster.Namespace, cluster.Name, clusterType, true, classifierName, logger) + trackLastSuccess(cluster.Namespace, cluster.Name, clusterType, time.Now(), classifierName, logger) + case libsveltosv1beta1.SveltosStatusFailed: + trackReconcileOutcome(cluster.Namespace, cluster.Name, clusterType, false, classifierName, logger) + case libsveltosv1beta1.SveltosStatusProvisioning, libsveltosv1beta1.SveltosStatusFailedNonRetriable, + libsveltosv1beta1.SveltosStatusRemoving, libsveltosv1beta1.SveltosStatusRemoved: + // Not a terminal outcome; nothing to record. } } diff --git a/controllers/mgmtcluster_classifier_controller.go b/controllers/mgmtcluster_classifier_controller.go index c1a4135..8fa4a53 100644 --- a/controllers/mgmtcluster_classifier_controller.go +++ b/controllers/mgmtcluster_classifier_controller.go @@ -116,6 +116,7 @@ func (r *ManagementClusterClassifierReconciler) reconcileNormal( _ = setMgmtClassifierFailureMessage(ctx, r.Client, mcc, err.Error()) return reconcile.Result{Requeue: true, RequeueAfter: normalRequeueAfter}, nil } + trackMgmtClusterMatchingClusters(mcc.Name, len(newClusters), logger) km, err := keymanager.GetKeyManagerInstance(ctx, r.Client) if err != nil { @@ -131,7 +132,7 @@ func (r *ManagementClusterClassifierReconciler) reconcileNormal( return reconcile.Result{Requeue: true, RequeueAfter: normalRequeueAfter}, nil } - hasConflict := false + conflictCount := 0 // Apply labels to newly matched clusters. for key, ref := range newClusters { @@ -152,7 +153,7 @@ func (r *ManagementClusterClassifierReconciler) reconcileNormal( mcc.Spec.ClassifierLabels, km) if len(unmanaged) > 0 { - hasConflict = true + conflictCount++ } if err := updateMgmtClassifierReportStatus(ctx, r.Client, mcc.Name, ref.Namespace, ref.Name, @@ -168,6 +169,7 @@ func (r *ManagementClusterClassifierReconciler) reconcileNormal( } } } + trackMgmtClusterLabelConflicts(mcc.Name, conflictCount, logger) // Update failure message: Lua errors plus any recorded conflicts. failMsg := strings.Join(luaFailures, "; ") @@ -178,7 +180,7 @@ func (r *ManagementClusterClassifierReconciler) reconcileNormal( // Sync GVK map and register dynamic watches for spec.matchResources. r.syncGVKWatches(mcc, logger) - if hasConflict { + if conflictCount > 0 { return reconcile.Result{Requeue: true, RequeueAfter: conflictRequeueAfter}, nil } diff --git a/manifest/manifest.yaml b/manifest/manifest.yaml index 6adf728..1ad7ca6 100644 --- a/manifest/manifest.yaml +++ b/manifest/manifest.yaml @@ -141,6 +141,34 @@ rules: - watch --- apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: classifier-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: classifier-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: classifier-manager-rolebinding @@ -166,6 +194,35 @@ subjects: name: classifier-manager namespace: projectsveltos --- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: classifier-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: classifier-proxy-role +subjects: +- kind: ServiceAccount + name: classifier-manager + namespace: projectsveltos +--- +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: classifier + name: classifier-manager-metrics-service + namespace: projectsveltos +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: classifier +--- apiVersion: apps/v1 kind: Deployment metadata: diff --git a/manifest/service_monitor.yaml b/manifest/service_monitor.yaml new file mode 100644 index 0000000..e39281b --- /dev/null +++ b/manifest/service_monitor.yaml @@ -0,0 +1,21 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: classifier + name: classifier + namespace: projectsveltos +spec: + endpoints: + - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + path: /metrics + port: https + scheme: https + tlsConfig: + insecureSkipVerify: true + namespaceSelector: + matchNames: + - projectsveltos + selector: + matchLabels: + control-plane: classifier diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..a85e67e --- /dev/null +++ b/renovate.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended", ":dependencyDashboard"], + "prHourlyLimit": 0, + "prConcurrentLimit": 0, + "branchConcurrentLimit": 0, + "ignorePaths": ["**/test/**"], + "sveltos": { + "fileMatch": ["^.*\\.yaml$"] + } +}