diff --git a/fleetshard/main.go b/fleetshard/main.go index 6c0e7658d6..f73333e883 100644 --- a/fleetshard/main.go +++ b/fleetshard/main.go @@ -6,12 +6,8 @@ import ( "flag" "os" "os/signal" - "time" - "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/central/reconciler" "github.com/stackrox/acs-fleet-manager/internal/certmonitor" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/informers" "github.com/golang/glog" cfg "github.com/stackrox/acs-fleet-manager/fleetshard/config" @@ -59,7 +55,11 @@ func main() { glog.Infof("Image pull secret configured, will be injected into tenant namespaces.") } glog.Info("Creating k8s client...") - k8sClient := k8s.CreateClientOrDie() + restConfig, err := ctrl.GetConfig() + if err != nil { + glog.Fatalf("Failed to get k8s config: %v", err) + } + k8sClient := k8s.CreateClientWithConfigOrDie(restConfig) ctrl.SetLogger(logger.NewKubeAPILogger()) glog.Info("Creating runtime...") runtime, err := runtime.NewRuntime(ctx, config, k8sClient) @@ -76,75 +76,7 @@ func main() { glog.Info("Creating certMonitor") - tenantNamespaceSelector := certmonitor.SelectorConfig{ - LabelSelector: &metav1.LabelSelector{ - MatchExpressions: []metav1.LabelSelectorRequirement{ - { - Key: reconciler.TenantIDLabelKey, - Operator: metav1.LabelSelectorOpExists, - }, - { - Key: reconciler.ProbeLabelKey, - Operator: metav1.LabelSelectorOpDoesNotExist, - }, - }, - }, - } - certmonitorConfig := &certmonitor.Config{ - Monitors: []certmonitor.MonitorConfig{ - { - Namespace: tenantNamespaceSelector, - Secret: certmonitor.SelectorConfig{ // pragma: allowlist secret - Name: "scanner-tls", - }, - }, - { - Namespace: tenantNamespaceSelector, - Secret: certmonitor.SelectorConfig{ // pragma: allowlist secret - Name: "central-tls", - }, - }, - - { - Namespace: tenantNamespaceSelector, - Secret: certmonitor.SelectorConfig{ // pragma: allowlist secret - Name: "scanner-db-tls", - }, - }, - - { - Namespace: tenantNamespaceSelector, - Secret: certmonitor.SelectorConfig{ // pragma: allowlist secret - Name: "scanner-v4-db-tls", - }, - }, - - { - Namespace: tenantNamespaceSelector, - Secret: certmonitor.SelectorConfig{ // pragma: allowlist secret - Name: "scanner-v4-indexer-tls", - }, - }, - { - Namespace: tenantNamespaceSelector, - Secret: certmonitor.SelectorConfig{ // pragma: allowlist secret - Name: "scanner-v4-matcher-tls", - }, - }, - }, - } - - if errs := certmonitor.ValidateConfig(*certmonitorConfig); len(errs) > 0 { - glog.Fatalf("certmonitor validation error: %v", errs) - } - - k8sInterface := k8s.CreateInterfaceOrDie() - informerFactory := informers.NewSharedInformerFactory(k8sInterface, time.Minute) - secretInformer := informerFactory.Core().V1().Secrets().Informer() - namespaceInformer := informerFactory.Core().V1().Namespaces().Informer() - namespaceLister := informerFactory.Core().V1().Namespaces().Lister() - - monitor := certmonitor.NewCertMonitor(certmonitorConfig, informerFactory, secretInformer, namespaceInformer, namespaceLister) + monitor := certmonitor.NewCertMonitor(restConfig) if err := monitor.Start(); err != nil { glog.Fatalf("Error starting certmonitor: %v", err) } diff --git a/fleetshard/pkg/k8s/client.go b/fleetshard/pkg/k8s/client.go index afd78d37ab..a488744d5a 100644 --- a/fleetshard/pkg/k8s/client.go +++ b/fleetshard/pkg/k8s/client.go @@ -9,7 +9,6 @@ import ( "github.com/stackrox/rox/operator/api/v1alpha1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" @@ -75,19 +74,3 @@ func IsRoutesResourceEnabled(client ctrlClient.Client) (bool, error) { } return true, nil } - -// CreateInterfaceOrDie create new kubernetes interface or dies -func CreateInterfaceOrDie() kubernetes.Interface { - - config, err := ctrl.GetConfig() - if err != nil { - glog.Fatal("failed to get k8s client config", err) - } - - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - glog.Fatal("error creating clientset: %s", err.Error()) - } - - return clientset -} diff --git a/internal/certmonitor/certmonitor.go b/internal/certmonitor/certmonitor.go index 78ed4adf35..3c5fe1fef3 100644 --- a/internal/certmonitor/certmonitor.go +++ b/internal/certmonitor/certmonitor.go @@ -1,105 +1,99 @@ package certmonitor import ( + "context" "crypto/x509" "encoding/pem" "fmt" + "time" + + "github.com/golang/glog" "github.com/pkg/errors" "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/fleetshardmetrics" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/client-go/informers" - "k8s.io/client-go/tools/cache" - "time" + "k8s.io/client-go/rest" + toolscache "k8s.io/client-go/tools/cache" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" ) -// SelectorConfig represents a configuration to select a namespace or a secret by name or labelSelector. Only one of Name or LabelSelector can be specified. -type SelectorConfig struct { - Name string `json:"name"` - LabelSelector *metav1.LabelSelector `json:"labelSelector"` -} - -// MonitorConfig represents a configuration for observing certificates contained in kubernetes secrets -type MonitorConfig struct { - Namespace SelectorConfig `json:"namespace"` - Secret SelectorConfig `json:"secret"` -} - -// Config represents the certificate monitor configuration -type Config struct { - Monitors []MonitorConfig `json:"monitors"` - ResyncPeriod *time.Duration `json:"resyncPeriod"` -} - -// NamespaceGetter interface for retrieving namespaces on name. This interface is a subset of `v1.NamespaceLister` -type NamespaceGetter interface { - Get(name string) (*corev1.Namespace, error) -} +const ( + tlsSecretLabel = "rhacs.redhat.com/tls" // pragma: allowlist secret + syncPeriod = 30 * time.Minute +) -// certMonitor is the Certificate Monitor. It watches Kubernetes secrets containing certificates, and populates prometheus metrics with the expiration time of those certificates. -type certMonitor struct { - informerfactory informers.SharedInformerFactory - secretInformer cache.SharedIndexInformer - config *Config - namespaceInformer cache.SharedIndexInformer - namespaceGetter NamespaceGetter - metrics *fleetshardmetrics.Metrics - stopCh chan struct{} +// CertMonitor is the Certificate Monitor. It watches Kubernetes secrets with label rhacs.redhat.com/tls=true +// and populates prometheus metrics with the expiration time of those certificates. +type CertMonitor struct { + cache ctrlcache.Cache + metrics *fleetshardmetrics.Metrics + cancel context.CancelFunc } // Start the certificate monitor -func (c *certMonitor) Start() error { - var err error - if c.stopCh != nil { +func (c *CertMonitor) Start() error { + if c.cancel != nil { return errors.New("already started") } - c.stopCh = make(chan struct{}) - _, err = c.secretInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + ctx, cancel := context.WithCancel(context.Background()) + c.cancel = cancel + + // Add event handler for secret events + informer, err := c.cache.GetInformer(ctx, &corev1.Secret{}) + if err != nil { + return fmt.Errorf("failed to get secret informer: %w", err) + } + + _, err = informer.AddEventHandler(toolscache.ResourceEventHandlerFuncs{ AddFunc: c.handleSecretCreation, UpdateFunc: c.handleSecretUpdate, DeleteFunc: c.handleSecretDeletion, }) if err != nil { - return err - } - _, err = c.namespaceInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ - DeleteFunc: c.handleNamespaceDeletion, - }) - if err != nil { - return err + return fmt.Errorf("failed to add event handler: %w", err) } - c.informerfactory.Start(c.stopCh) - if !cache.WaitForCacheSync(c.stopCh) { - return fmt.Errorf("timed out waiting for caches to sync") + + // Start the cache + go func() { + if err := c.cache.Start(ctx); err != nil { + glog.Errorf("certmonitor cache error: %v", err) + } + }() + + // Wait for cache to sync + if !c.cache.WaitForCacheSync(ctx) { + return fmt.Errorf("timed out waiting for cache to sync") } + + glog.Info("Certificate monitor started successfully") return nil } -func (c *certMonitor) Stop() error { - if c.stopCh == nil { +func (c *CertMonitor) Stop() error { + if c.cancel == nil { return errors.New("not started") } - close(c.stopCh) + c.cancel() + c.cancel = nil return nil } -// NewCertMonitor creates new instance of certMonitor -func NewCertMonitor(config *Config, informerFactory informers.SharedInformerFactory, secretInformer cache.SharedIndexInformer, namespaceInformer cache.SharedIndexInformer, namespaceGetter NamespaceGetter) *certMonitor { - return &certMonitor{ - informerfactory: informerFactory, - secretInformer: secretInformer, // pragma: allowlist secret - config: config, - namespaceInformer: namespaceInformer, // pragma: allowlist secret - namespaceGetter: namespaceGetter, - metrics: fleetshardmetrics.MetricsInstance(), +// NewCertMonitor creates new instance of CertMonitor +// The cache must be configured to only watch tenant tls secrets +func NewCertMonitor(restConfig *rest.Config) *CertMonitor { + cache, err := ctrlcache.New(restConfig, cacheOptions()) + if err != nil { + glog.Fatalf("Failed to create certmonitor cache: %v", err) + } + return &CertMonitor{ + cache: cache, + metrics: fleetshardmetrics.MetricsInstance(), } } // processSecret extracts, decodes, parses certificates from a secret, and populates prometheus metrics -func (c *certMonitor) processSecret(secret *corev1.Secret) { +func (c *CertMonitor) processSecret(secret *corev1.Secret) { for dataKey, dataCert := range secret.Data { pparse, _ := pem.Decode(dataCert) @@ -117,20 +111,15 @@ func (c *certMonitor) processSecret(secret *corev1.Secret) { } } -// handleSecretCreation handles secret creation events -func (c *certMonitor) handleSecretCreation(obj interface{}) { +func (c *CertMonitor) handleSecretCreation(obj interface{}) { secret, ok := obj.(*corev1.Secret) if !ok { return } - if !c.shouldProcessSecret(secret) { - return - } c.processSecret(secret) } -// handleSecretUpdate handles secret updates -func (c *certMonitor) handleSecretUpdate(oldObj, newObj interface{}) { +func (c *CertMonitor) handleSecretUpdate(oldObj, newObj interface{}) { oldSecret, ok := oldObj.(*corev1.Secret) if !ok { return @@ -144,6 +133,7 @@ func (c *certMonitor) handleSecretUpdate(oldObj, newObj interface{}) { if newObj == nil || oldObj == nil { return } + for oldKey := range oldSecret.Data { if _, ok := newSecret.Data[oldKey]; !ok { // secret has been updated, and oldKey does not exist in the new secret - so we delete the metric @@ -151,114 +141,29 @@ func (c *certMonitor) handleSecretUpdate(oldObj, newObj interface{}) { } } - if !c.shouldProcessSecret(newSecret) { - return - } - + // Process the updated secret c.processSecret(newSecret) } -// handleSecretDeletion handles deletion of secrets -func (c *certMonitor) handleSecretDeletion(obj interface{}) { +func (c *CertMonitor) handleSecretDeletion(obj interface{}) { secret, ok := obj.(*corev1.Secret) if !ok { return } - if !c.shouldProcessSecret(secret) { - return - } c.metrics.DeleteCertMetric(secret.Namespace, secret.Name) } -func (c *certMonitor) handleNamespaceDeletion(obj interface{}) { - namespace, ok := obj.(*corev1.Namespace) - if !ok { - return - } - - c.metrics.DeleteCertNamespaceMetric(namespace.Name) -} - -func (c *certMonitor) shouldProcessSecret(s *corev1.Secret) bool { - for _, monitor := range c.config.Monitors { - if c.secretMatches(s, monitor) { - return true - } - } - return false -} - -// secretMatches checks if a secret matches a monitor config -func (c *certMonitor) secretMatches(s *corev1.Secret, monitor MonitorConfig) bool { - if s == nil { - return false - } - if len(monitor.Secret.Name) > 0 && s.Name != monitor.Secret.Name { - return false - } - if len(monitor.Namespace.Name) > 0 && s.Namespace != monitor.Namespace.Name { - return false - } - if monitor.Secret.LabelSelector != nil && !objectMatchesSelector(s, monitor.Secret.LabelSelector) { - return false - } - - if monitor.Namespace.LabelSelector != nil { - ns, err := c.namespaceGetter.Get(s.Namespace) - if err != nil { - return false - } - if !objectMatchesSelector(ns, monitor.Namespace.LabelSelector) { - return false - } - } - return true -} - -// objectMatchesSelector checks if object matches given label selector -func objectMatchesSelector(obj runtime.Object, selector *metav1.LabelSelector) bool { - if selector == nil { - return true - } - labelselector, err := metav1.LabelSelectorAsSelector(selector) - if err != nil { - return false - } - - metaObj, ok := obj.(metav1.Object) - if !ok { - return false - } - - return labelselector.Matches(labels.Set(metaObj.GetLabels())) - -} - -// ValidateConfig checks the validity of Config -func ValidateConfig(config Config) (errs field.ErrorList) { - errs = append(errs, validateMonitors(field.NewPath("monitors"), config.Monitors)...) - return errs -} - -// validateMonitors validates list of Monitor -func validateMonitors(path *field.Path, monitors []MonitorConfig) (errs field.ErrorList) { - for i, monitor := range monitors { - errs = append(errs, validateMonitor(path.Index(i), monitor)...) - } - return errs -} - -// validateMonitor validates a Monitor -func validateMonitor(path *field.Path, monitor MonitorConfig) (errs field.ErrorList) { - errs = append(errs, validateSelectorConfig(path.Child("namespace"), monitor.Namespace)...) - errs = append(errs, validateSelectorConfig(path.Child("secret"), monitor.Secret)...) - return errs -} - -// validateSelectorConfig validates a SelectorConfig -func validateSelectorConfig(path *field.Path, selectorConfig SelectorConfig) (errs field.ErrorList) { - if len(selectorConfig.Name) != 0 && selectorConfig.LabelSelector != nil { - errs = append(errs, field.Invalid(path, selectorConfig, "cannot specify both name and label selector")) +func cacheOptions() ctrlcache.Options { + syncPeriod := syncPeriod + return ctrlcache.Options{ + ByObject: map[client.Object]ctrlcache.ByObject{ + &corev1.Secret{}: { + Label: labels.SelectorFromSet(labels.Set{ + tlsSecretLabel: "true", + }), + }, + }, + DefaultLabelSelector: labels.Nothing(), + SyncPeriod: &syncPeriod, } - return errs } diff --git a/internal/certmonitor/certmonitor_test.go b/internal/certmonitor/certmonitor_test.go index e17cdfd6bf..36ecbf2513 100644 --- a/internal/certmonitor/certmonitor_test.go +++ b/internal/certmonitor/certmonitor_test.go @@ -6,318 +6,25 @@ import ( "crypto/rand" "crypto/x509" "encoding/pem" - "fmt" + "math/big" + "testing" + "time" + "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/fleetshardmetrics" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "math/big" - "testing" - "time" + "k8s.io/apimachinery/pkg/labels" ) -// fakeNameSpaceGetter struct is mock implementation for test -type fakeNamespaceGetter struct { - namespaces map[string]v1.Namespace -} - -// Get func returns/gets a namespace by name -func (f *fakeNamespaceGetter) Get(name string) (*v1.Namespace, error) { - ns, ok := f.namespaces[name] - if !ok { - return nil, fmt.Errorf("namespace %q not found", name) - } - return &ns, nil -} - -// newFakeNamespaceGetter func creates new fakeNameSpaceGetter -func newFakeNamespaceGetter(namespaces []v1.Namespace) *fakeNamespaceGetter { - f := fakeNamespaceGetter{namespaces: make(map[string]v1.Namespace)} - for _, ns := range namespaces { - f.namespaces[ns.Name] = ns - } - return &f -} - -// TestCertMonitor_secretMatches func tests the secretMatches method in certmonitor.go -func TestCertMonitor_secretMatches(t *testing.T) { - tests := []struct { - name string - secret v1.Secret - monitor MonitorConfig - want bool - namespaces []v1.Namespace - }{ - { - name: "should match on namespace and secret name", - - secret: v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "secret-1", Namespace: "namespace-1"}}, // pragma: allowlist secret - monitor: MonitorConfig{ - Namespace: SelectorConfig{Name: "namespace-1"}, - Secret: SelectorConfig{Name: "secret-1"}, // pragma: allowlist secret - }, - want: true, - }, { - name: "mismatch on namespace name", - secret: v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "secret-1", Namespace: "namespace-1"}}, // pragma: allowlist secret - monitor: MonitorConfig{ - Namespace: SelectorConfig{Name: "foo"}, - Secret: SelectorConfig{Name: "secret-1"}, // pragma: allowlist secret - }, // pragma: allowlist secret - want: false, - }, { - name: "mismatch on secret name", - secret: v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "secret-1", Namespace: "namespace-1"}}, // pragma: allowlist secret - monitor: MonitorConfig{ - Namespace: SelectorConfig{Name: "namespace-1"}, - Secret: SelectorConfig{Name: "bar"}, // pragma: allowlist secret - }, - want: false, - }, { - name: "match on namespace name and secret label", - secret: v1.Secret{ // pragma: allowlist secret - ObjectMeta: metav1.ObjectMeta{ // pragma: allowlist secret - Name: "secret-1", // pragma: allowlist secret - Namespace: "namespace-1", - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - monitor: MonitorConfig{ - Namespace: SelectorConfig{Name: "namespace-1"}, // pragma: allowlist secret - Secret: SelectorConfig{ // pragma: allowlist secret - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foo": "bar", // pragma: allowlist secret - }, - }, - }, - }, - namespaces: []v1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "namespace-1", - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - }, - want: true, - }, { - name: "match on namespace label and secret name", - secret: v1.Secret{ // pragma: allowlist secret - ObjectMeta: metav1.ObjectMeta{ - Name: "secret-1", // pragma: allowlist secret - Namespace: "namespace-1", - }, - }, - monitor: MonitorConfig{ - Namespace: SelectorConfig{ - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foo": "bar", - }, - }, - }, - Secret: SelectorConfig{ // pragma: allowlist secret - Name: "secret-1", - }, - }, - namespaces: []v1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "namespace-1", - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - }, - want: true, - }, { - name: "match on both namespace label and secret label", - secret: v1.Secret{ // pragma: allowlist secret - ObjectMeta: metav1.ObjectMeta{ - Name: "secret-1", - Namespace: "namespace-1", - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - monitor: MonitorConfig{ - Namespace: SelectorConfig{ - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foo": "bar", - }, - }, - }, - Secret: SelectorConfig{ // pragma: allowlist secret - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foo": "bar", - }, - }, - }, - }, - namespaces: []v1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "namespace-1", - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - }, - want: true, - }, { - name: "mismatch on both namespace label and secret label", - secret: v1.Secret{ // pragma: allowlist secret - ObjectMeta: metav1.ObjectMeta{ - Name: "secret-1", - Namespace: "namespace-1", - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - monitor: MonitorConfig{ - Namespace: SelectorConfig{ - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foo": "qux", - }, - }, - }, - Secret: SelectorConfig{ // pragma: allowlist secret - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foo": "qux", - }, - }, - }, - }, - namespaces: []v1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "namespace-1", - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - }, - want: false, - }, { - name: "mismatch on namespace name and secret label", - secret: v1.Secret{ // pragma: allowlist secret - ObjectMeta: metav1.ObjectMeta{ - Name: "secret-1", - Namespace: "namespace-1", - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - monitor: MonitorConfig{ - Namespace: SelectorConfig{Name: "namespace-2"}, - Secret: SelectorConfig{ // pragma: allowlist secret - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foo": "qux", - }, - }, - }, - }, - namespaces: []v1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "namespace-1", - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - }, - want: false, - }, { - name: "mismatch on namespace label and secret name", - secret: v1.Secret{ // pragma: allowlist secret - ObjectMeta: metav1.ObjectMeta{ - Name: "secret-1", - Namespace: "namespace-1", - }, - }, - monitor: MonitorConfig{ - Namespace: SelectorConfig{ - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foo": "qux", - }, - }, - }, - Secret: SelectorConfig{ // pragma: allowlist secret - Name: "secret-2", - }, - }, - namespaces: []v1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "namespace-1", - Labels: map[string]string{ - "foo": "bar", - }, - }, - }, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - certMonitor := &certMonitor{ - namespaceGetter: newFakeNamespaceGetter(tt.namespaces), - } - got := certMonitor.secretMatches(&tt.secret, tt.monitor) - assert.Equal(t, tt.want, got) - }) - } -} - // TestCertMonitor_Secret tests that secret event handlers correctly populate prometheus metrics func TestCertMonitor_Secret(t *testing.T) { fleetshardmetrics.MetricsInstance().CertificatesExpiry.Reset() - namespaces := []v1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "namespace-1", - Labels: map[string]string{ - "foo": "bar"}, - }, - }, - } - certMonitor := &certMonitor{ - namespaceGetter: newFakeNamespaceGetter(namespaces), - metrics: fleetshardmetrics.MetricsInstance(), - config: &Config{ - Monitors: []MonitorConfig{ - { - Namespace: SelectorConfig{ - Name: "namespace-1", - }, - Secret: SelectorConfig{ // pragma: allowlist secret - Name: "secret-1", - }, - }, - }, - }, + certMonitor := &CertMonitor{ + metrics: fleetshardmetrics.MetricsInstance(), } now1 := time.Now().UTC() expirytime := now1.Add(1 * time.Hour) @@ -345,54 +52,6 @@ func TestCertMonitor_Secret(t *testing.T) { verifyPrometheusMetricDelete(t, "namespace-1", "secret-1", "tls.crt") } -// TestCertMonitor_Namespace tests that namespace secret handlers remove prometheus metrics when the namespace is deleted -func TestCertMonitor_Namespace(t *testing.T) { - fleetshardmetrics.MetricsInstance().CertificatesExpiry.Reset() - - namespaces := []v1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "namespace-1", - Labels: map[string]string{ - "foo": "bar"}, - }, - }, - } - certMonitor := &certMonitor{ - namespaceGetter: newFakeNamespaceGetter(namespaces), - metrics: fleetshardmetrics.MetricsInstance(), - config: &Config{ - Monitors: []MonitorConfig{ - { - Namespace: SelectorConfig{ - Name: "namespace-1", - }, - Secret: SelectorConfig{ // pragma: allowlist secret - Name: "secret-1", - }, - }, - }, - }, - } - now1 := time.Now().UTC() - expirytime := now1.Add(1 * time.Hour) - - mockNamespace := &v1.Namespace{ // pragma: allowlist secret - ObjectMeta: metav1.ObjectMeta{Name: "namespace-1"}, - } - secret := &v1.Secret{ // pragma: allowlist secret - ObjectMeta: metav1.ObjectMeta{Namespace: mockNamespace.Name, Name: "secret-1"}, - Data: map[string][]byte{"tls.crt": generateCertWithExpiration(t, expirytime)}, - } - expirationUnix := float64(expirytime.Unix()) - certMonitor.handleSecretCreation(secret) - verifyPrometheusMetric(t, "namespace-1", "secret-1", "tls.crt", expirationUnix) - - certMonitor.handleNamespaceDeletion(mockNamespace) - verifyPrometheusMetricDelete(t, "namespace-1", "secret-1", "tls.crt") - -} - // generateCertWithExpiration func generates a pem-encoded certificate func generateCertWithExpiration(t *testing.T, expiry time.Time) []byte { privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) @@ -406,13 +65,12 @@ func generateCertWithExpiration(t *testing.T, expiry time.Time) []byte { } certBytesDER, err := x509.CreateCertificate(rand.Reader, cert, cert, &privateKey.PublicKey, privateKey) require.NoError(t, err) - pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytesDER}) - return []byte(pemCert) + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytesDER}) } // verifyPrometheusMetric func verifies if the promethues metric matches the expected value (create + update handle) -func verifyPrometheusMetric(t *testing.T, namespace, secret, data_key string, expectedValue float64) { - actualValue := testutil.ToFloat64(fleetshardmetrics.MetricsInstance().CertificatesExpiry.WithLabelValues(namespace, secret, data_key)) +func verifyPrometheusMetric(t *testing.T, namespace, secret, dataKey string, expectedValue float64) { + actualValue := testutil.ToFloat64(fleetshardmetrics.MetricsInstance().CertificatesExpiry.WithLabelValues(namespace, secret, dataKey)) assert.Equal(t, expectedValue, actualValue, "Value does not match") } @@ -422,3 +80,62 @@ func verifyPrometheusMetricDelete(t *testing.T, namespace, secret, dataKey strin require.NoError(t, err) require.Equal(t, float64(0), testutil.ToFloat64(metric)) } + +// TestProcessSecret_MultipleCertificates tests processing a secret with multiple certificate keys. +func TestProcessSecret_MultipleCertificates(t *testing.T) { + fleetshardmetrics.MetricsInstance().CertificatesExpiry.Reset() + + certMonitor := &CertMonitor{ + metrics: fleetshardmetrics.MetricsInstance(), + } + + expiry := time.Now().UTC().Add(24 * time.Hour) + certData := generateCertWithExpiration(t, expiry) + + secret := &v1.Secret{ // pragma: allowlist secret + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "multi-cert-secret", + }, + Data: map[string][]byte{ + "tls.crt": certData, + "ca.crt": certData, + "tls.key": []byte("not-a-cert"), // Should be ignored + }, + } + + certMonitor.processSecret(secret) + + expectedValue := float64(expiry.Unix()) + + // Verify both certificate keys have metrics set + verifyPrometheusMetric(t, "test-namespace", "multi-cert-secret", "tls.crt", expectedValue) + verifyPrometheusMetric(t, "test-namespace", "multi-cert-secret", "ca.crt", expectedValue) + + // Non-certificate data should have no metric + verifyPrometheusMetric(t, "test-namespace", "multi-cert-secret", "tls.key", 0) +} + +func TestCacheOptions(t *testing.T) { + opts := cacheOptions() + + // Verify label selector matches labeled secrets + // Need to iterate since &v1.Secret{} creates different pointer instances + var selector labels.Selector + for obj, byObjOpts := range opts.ByObject { + if _, ok := obj.(*v1.Secret); ok { + selector = byObjOpts.Label + break + } + } + require.NotNil(t, selector, "Should have label selector for secrets") + + labeled := labels.Set{"rhacs.redhat.com/tls": "true"} + unlabeled := labels.Set{"other": "label"} + + assert.True(t, selector.Matches(labeled)) + assert.False(t, selector.Matches(unlabeled)) + + // Verify default selector rejects everything + assert.False(t, opts.DefaultLabelSelector.Matches(labels.Set{})) +}