Skip to content
Draft
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: 2 additions & 0 deletions internal/controller/catalog/catalog_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ func (r *CatalogReconciler) suspendArtifactGenerator(ctx context.Context, name,

func (r *CatalogReconciler) EnsureCreated(ctx context.Context, obj lifecycle.RuntimeObject) (ctrl.Result, lifecycle.ReconcileResult, error) {
catalog := obj.(*greenhousev1alpha1.Catalog) //nolint:errcheck
defer UpdateCatalogReadyMetric(catalog)
catalog.SetUnknownCondition()

if len(catalog.Spec.Sources) == 0 {
Expand Down Expand Up @@ -375,6 +376,7 @@ func (r *CatalogReconciler) deleteOrphanedResource(ctx context.Context, obj clie
return nil
}

//nolint:unparam
func (r *CatalogReconciler) verifyStatus(ctx context.Context, catalog *greenhousev1alpha1.Catalog, allErrors []catalogError) (ctrl.Result, lifecycle.ReconcileResult, error) {
existingNotReady := catalog.Status.GetConditionByType(greenhousemetav1alpha1.ReadyCondition)
if existingNotReady != nil && existingNotReady.Status == metav1.ConditionFalse {
Expand Down
101 changes: 101 additions & 0 deletions internal/controller/catalog/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package catalog

import (
"github.com/prometheus/client_golang/prometheus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
crmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"

greenhouseapis "github.com/cloudoperators/greenhouse/api"
greenhousemetav1alpha1 "github.com/cloudoperators/greenhouse/api/meta/v1alpha1"
greenhousev1alpha1 "github.com/cloudoperators/greenhouse/api/v1alpha1"
)

var (
// ReadyGauge indicates whether the catalog is ready
// A catalog is considered ready when all its sources and their resources are ready
ReadyGauge = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "greenhouse_catalog_ready",
Help: "Indicates whether the catalog is ready (1 = ready, 0 = not ready)",
},
[]string{"catalog", "namespace", "owned_by"})

// MissingSecretGauge indicates whether a specific catalog source is missing its secret
// This metric is per-source, allowing you to identify which repository is affected
MissingSecretGauge = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "greenhouse_catalog_missing_secret",
Help: "Indicates whether a catalog source is missing its secret (1 = missing, 0 = present)",
},
[]string{"catalog", "namespace", "owned_by", "repository_url", "git_repo_name", "ref", "secret_name"})

// AuthErrorGauge indicates whether a catalog source has an authentication error
// This metric is per-source, allowing you to identify which repository has auth issues
AuthErrorGauge = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "greenhouse_catalog_auth_error",
Help: "Indicates whether a catalog source has an authentication error (1 = error, 0 = no error)",
},
[]string{"catalog", "namespace", "owned_by", "repository_url", "git_repo_name", "ref"})
)

func init() {
crmetrics.Registry.MustRegister(ReadyGauge)
crmetrics.Registry.MustRegister(MissingSecretGauge)
crmetrics.Registry.MustRegister(AuthErrorGauge)
}

// UpdateCatalogReadyMetric updates the catalog ready metric based on the catalog's ready condition
func UpdateCatalogReadyMetric(catalog *greenhousev1alpha1.Catalog) {
catalogReadyLabels := prometheus.Labels{
"catalog": catalog.Name,
"namespace": catalog.Namespace,
"owned_by": catalog.Labels[greenhouseapis.LabelKeyOwnedBy],
}

// Check if the catalog has a Ready condition set to True
readyCondition := catalog.Status.GetConditionByType(greenhousemetav1alpha1.ReadyCondition)
if readyCondition != nil && readyCondition.Status == metav1.ConditionTrue {
ReadyGauge.With(catalogReadyLabels).Set(1)
} else {
ReadyGauge.With(catalogReadyLabels).Set(0)
}
}

// UpdateCatalogMissingSecretMetric updates the missing secret metric for a specific catalog source
func UpdateCatalogMissingSecretMetric(catalog *greenhousev1alpha1.Catalog, repositoryURL, gitRepoName, ref, secretName string, isMissing bool) {
catalogLabels := prometheus.Labels{
"catalog": catalog.Name,
"namespace": catalog.Namespace,
"owned_by": catalog.Labels[greenhouseapis.LabelKeyOwnedBy],
"repository_url": repositoryURL,
"git_repo_name": gitRepoName,
"ref": ref,
"secret_name": secretName,
}
if isMissing {
MissingSecretGauge.With(catalogLabels).Set(1)
} else {
MissingSecretGauge.With(catalogLabels).Set(0)
}
}

// UpdateCatalogAuthErrorMetric updates the authentication error metric for a specific catalog source
func UpdateCatalogAuthErrorMetric(catalog *greenhousev1alpha1.Catalog, repositoryURL, gitRepoName, ref string, hasError bool) {
catalogLabels := prometheus.Labels{
"catalog": catalog.Name,
"namespace": catalog.Namespace,
"owned_by": catalog.Labels[greenhouseapis.LabelKeyOwnedBy],
"repository_url": repositoryURL,
"git_repo_name": gitRepoName,
"ref": ref,
}
if hasError {
AuthErrorGauge.With(catalogLabels).Set(1)
} else {
AuthErrorGauge.With(catalogLabels).Set(0)
}
}
61 changes: 60 additions & 1 deletion internal/controller/catalog/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
sourcev2 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -191,8 +192,28 @@ func (s *source) getSourceSecret(ctx context.Context) (*corev1.Secret, error) {
secret.SetNamespace(s.catalog.Namespace)
err := s.Get(ctx, client.ObjectKeyFromObject(secret), secret)
if err != nil {
// Track missing secret in metrics if it's a NotFound error
if apierrors.IsNotFound(err) {
UpdateCatalogMissingSecretMetric(
s.catalog,
s.source.Repository,
s.getGitRepoName(),
s.source.GetRefValue(),
*s.source.SecretName,
true,
)
}
return nil, fmt.Errorf("failed to get secret %s/%s: %w", s.catalog.Namespace, *s.source.SecretName, err)
}
// Secret found, update metric to show it's present
UpdateCatalogMissingSecretMetric(
s.catalog,
s.source.Repository,
s.getGitRepoName(),
s.source.GetRefValue(),
*s.source.SecretName,
false,
)
return secret, nil
}

Expand Down Expand Up @@ -544,7 +565,6 @@ func (s *source) objectReadiness(ctx context.Context, obj client.Object) (ready
msg = err.Error()
return
}

conditions := cObj.GetConditions()
readyCondition := meta.FindStatusCondition(conditions, fluxmeta.ReadyCondition)

Expand All @@ -555,5 +575,44 @@ func (s *source) objectReadiness(ctx context.Context, obj client.Object) (ready
}
ready = readyCondition.Status
msg = readyCondition.Message

// Update metrics based on object conditions
s.updateCatalogObjectMetrics(cObj)
return
}

// updateCatalogObjectMetrics updates metrics based on the object's conditions
// This is a helper method to track specific error conditions for different Flux resource types
func (s *source) updateCatalogObjectMetrics(obj lifecycle.CatalogObject) {
kind := obj.GetObjectKind().GroupVersionKind().Kind
conditions := obj.GetConditions()

// TODO: use switch for future object type metrics
if kind == sourcev1.GitRepositoryKind {
// Check for authentication errors in GitRepository
s.checkGitRepositoryAuthError(conditions)
}
}

// checkGitRepositoryAuthError checks if a GitRepository has an authentication error
// and updates the authentication error metric accordingly
func (s *source) checkGitRepositoryAuthError(conditions []metav1.Condition) {
hasAuthError := false

// Check for FetchFailed condition with AuthenticationFailed reason
fetchFailedCondition := meta.FindStatusCondition(conditions, sourcev1.FetchFailedCondition)
if fetchFailedCondition != nil &&
fetchFailedCondition.Status == metav1.ConditionTrue &&
fetchFailedCondition.Reason == sourcev1.AuthenticationFailedReason {
hasAuthError = true
}

// Update the authentication error metric
UpdateCatalogAuthErrorMetric(
s.catalog,
s.source.Repository,
s.getGitRepoName(),
s.source.GetRefValue(),
hasAuthError,
)
}
Loading