github.com/verrazzano/verrazzano@v1.7.1/application-operator/controllers/metricsbinding/metricsbinding_update_test.go (about)

     1  // Copyright (c) 2022, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package metricsbinding
     5  
     6  import (
     7  	"context"
     8  	"strings"
     9  	"testing"
    10  
    11  	promoperapi "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
    12  	asserts "github.com/stretchr/testify/assert"
    13  	vzapi "github.com/verrazzano/verrazzano/application-operator/apis/app/v1alpha1"
    14  	"github.com/verrazzano/verrazzano/application-operator/constants"
    15  	vzconst "github.com/verrazzano/verrazzano/pkg/constants"
    16  	"github.com/verrazzano/verrazzano/pkg/log/vzlog"
    17  	"github.com/verrazzano/verrazzano/pkg/metricsutils"
    18  	appsv1 "k8s.io/api/apps/v1"
    19  	corev1 "k8s.io/api/core/v1"
    20  	"k8s.io/apimachinery/pkg/runtime"
    21  	"k8s.io/apimachinery/pkg/types"
    22  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    23  )
    24  
    25  // TestGetMetricsTemplate tests the retrieval process of the metrics template
    26  // GIVEN a metrics binding
    27  // WHEN the function receives the binding
    28  // THEN return the metrics template without error
    29  func TestGetMetricsTemplate(t *testing.T) {
    30  	assert := asserts.New(t)
    31  
    32  	scheme := runtime.NewScheme()
    33  	_ = vzapi.AddToScheme(scheme)
    34  	c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(metricsTemplate).Build()
    35  
    36  	localMetricsBinding := metricsBinding.DeepCopy()
    37  
    38  	log := vzlog.DefaultLogger()
    39  	r := newReconciler(c)
    40  	template, err := r.getMetricsTemplate(context.Background(), localMetricsBinding, log)
    41  	assert.NoError(err, "Expected no error getting the MetricsTemplate from the MetricsBinding")
    42  	assert.NotNil(template)
    43  }
    44  
    45  // TestHandleDefaultMetricsTemplate tests the retrieval process of the metrics template
    46  // GIVEN a metrics binding
    47  // WHEN the function receives the binding
    48  // THEN a scrape config gets generated for the target workload
    49  func TestHandleDefaultMetricsTemplate(t *testing.T) {
    50  	assert := asserts.New(t)
    51  
    52  	scheme := runtime.NewScheme()
    53  	_ = vzapi.AddToScheme(scheme)
    54  	_ = corev1.AddToScheme(scheme)
    55  	_ = appsv1.AddToScheme(scheme)
    56  	_ = promoperapi.AddToScheme(scheme)
    57  
    58  	labeledNs := plainNs.DeepCopy()
    59  	labeledNs.Labels = map[string]string{constants.LabelIstioInjection: "enabled"}
    60  
    61  	labeledWorkload := plainWorkload.DeepCopy()
    62  	labeledWorkload.Labels = map[string]string{constants.MetricsWorkloadLabel: testDeploymentName}
    63  
    64  	serviceMonitorNSN := types.NamespacedName{Namespace: testMetricsBindingNamespace, Name: testMetricsBindingName}
    65  
    66  	tests := []struct {
    67  		name        string
    68  		workload    *appsv1.Deployment
    69  		namespace   *corev1.Namespace
    70  		expectError bool
    71  	}{
    72  		{
    73  			name:        "test no workload",
    74  			workload:    &appsv1.Deployment{},
    75  			namespace:   labeledNs,
    76  			expectError: true,
    77  		},
    78  		{
    79  			name:        "test no namespace",
    80  			workload:    labeledWorkload,
    81  			namespace:   &corev1.Namespace{},
    82  			expectError: true,
    83  		},
    84  		{
    85  			name:        "test workload no label",
    86  			workload:    plainWorkload,
    87  			namespace:   labeledNs,
    88  			expectError: true,
    89  		},
    90  		{
    91  			name:        "test workload and namespace label",
    92  			workload:    labeledWorkload,
    93  			namespace:   labeledNs,
    94  			expectError: false,
    95  		},
    96  		{
    97  			name:        "test workload label only",
    98  			workload:    labeledWorkload,
    99  			namespace:   plainNs,
   100  			expectError: false,
   101  		},
   102  	}
   103  	for _, tt := range tests {
   104  		t.Run(tt.name, func(t *testing.T) {
   105  			c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects([]runtime.Object{
   106  				metricsTemplate,
   107  				tt.workload,
   108  				tt.namespace,
   109  			}...).Build()
   110  
   111  			localMetricsBinding := metricsBinding.DeepCopy()
   112  
   113  			log := vzlog.DefaultLogger()
   114  			r := newReconciler(c)
   115  			err := r.handleDefaultMetricsTemplate(context.Background(), localMetricsBinding, log)
   116  			if tt.expectError {
   117  				assert.Error(err, "Expected error handling the default MetricsTemplate")
   118  				return
   119  			}
   120  			assert.NoError(err, "Expected no error handling the default MetricsTemplate")
   121  
   122  			// Get the service monitor for analysis
   123  			serviceMonitor := promoperapi.ServiceMonitor{}
   124  			err = c.Get(context.TODO(), serviceMonitorNSN, &serviceMonitor)
   125  			assert.NoError(err, "Expected no error getting the Service Monitor")
   126  
   127  			assert.Equal(1, len(serviceMonitor.Spec.Endpoints))
   128  			assert.Contains(serviceMonitor.Spec.Endpoints[0].RelabelConfigs[1].SourceLabels, promoperapi.LabelName(workloadSourceLabel))
   129  			assert.Equal("local", serviceMonitor.Spec.Endpoints[0].RelabelConfigs[0].Replacement)
   130  			if _, ok := tt.namespace.Labels[constants.LabelIstioInjection]; ok {
   131  				assert.Equal("https", serviceMonitor.Spec.Endpoints[0].Scheme)
   132  			} else {
   133  				assert.Equal("http", serviceMonitor.Spec.Endpoints[0].Scheme)
   134  			}
   135  		})
   136  	}
   137  }
   138  
   139  // TestHandleCustomMetricsTemplate tests the custom metrics path implementation
   140  // GIVEN a metrics binding
   141  // WHEN the function receives the binding
   142  // THEN a scrape config gets generated for the target workload
   143  func TestHandleCustomMetricsTemplate(t *testing.T) {
   144  	assert := asserts.New(t)
   145  
   146  	scheme := runtime.NewScheme()
   147  	_ = vzapi.AddToScheme(scheme)
   148  	_ = corev1.AddToScheme(scheme)
   149  	_ = appsv1.AddToScheme(scheme)
   150  	_ = promoperapi.AddToScheme(scheme)
   151  
   152  	labeledNs := plainNs.DeepCopy()
   153  	labeledNs.Labels = map[string]string{constants.LabelIstioInjection: "enabled"}
   154  
   155  	labeledWorkload := plainWorkload.DeepCopy()
   156  	labeledWorkload.Labels = map[string]string{constants.MetricsWorkloadLabel: testDeploymentName}
   157  
   158  	populatedTemplate, err := getTemplateTestFile()
   159  	assert.NoError(err)
   160  	testFileCMEmpty, err := getConfigMapFromTestFile(true)
   161  	assert.NoError(err)
   162  	testFileCMFilled, err := getConfigMapFromTestFile(false)
   163  	assert.NoError(err)
   164  	testFileCMOtherScrapeConfigs, err := readConfigMapData("testdata/cmDataHasOtherScrapeConfigs.yaml")
   165  	assert.NoError(err)
   166  	testFileSec, err := getSecretFromTestFile(true)
   167  	assert.NoError(err)
   168  
   169  	tests := []struct {
   170  		name               string
   171  		workload           *appsv1.Deployment
   172  		namespace          *corev1.Namespace
   173  		configMap          *corev1.ConfigMap
   174  		expectConfigMapAdd bool
   175  		secret             *corev1.Secret
   176  		expectError        bool
   177  	}{
   178  		{
   179  			name:               "test configmap empty",
   180  			workload:           labeledWorkload,
   181  			namespace:          labeledNs,
   182  			configMap:          testFileCMEmpty,
   183  			expectError:        false,
   184  			expectConfigMapAdd: true,
   185  		},
   186  		{
   187  			name:               "test configmap with other scrape configs",
   188  			workload:           labeledWorkload,
   189  			namespace:          labeledNs,
   190  			configMap:          testFileCMOtherScrapeConfigs,
   191  			expectError:        false,
   192  			expectConfigMapAdd: true,
   193  		},
   194  		{
   195  			name:               "test configmap filled",
   196  			workload:           labeledWorkload,
   197  			namespace:          labeledNs,
   198  			configMap:          testFileCMFilled,
   199  			expectError:        false,
   200  			expectConfigMapAdd: false,
   201  		},
   202  		{
   203  			name:        "test secret",
   204  			workload:    labeledWorkload,
   205  			namespace:   labeledNs,
   206  			secret:      testFileSec,
   207  			expectError: false,
   208  		},
   209  		{
   210  			name:               "test configmap no Istio",
   211  			workload:           labeledWorkload,
   212  			namespace:          plainNs,
   213  			configMap:          testFileCMEmpty,
   214  			expectError:        false,
   215  			expectConfigMapAdd: true,
   216  		},
   217  		{
   218  			name:        "test secret no Istio",
   219  			workload:    labeledWorkload,
   220  			namespace:   plainNs,
   221  			secret:      testFileSec,
   222  			expectError: false,
   223  		},
   224  	}
   225  	for _, tt := range tests {
   226  		t.Run(tt.name, func(t *testing.T) {
   227  			c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects([]runtime.Object{
   228  				populatedTemplate,
   229  				tt.workload,
   230  				tt.namespace,
   231  			}...)
   232  
   233  			localMetricsBinding := metricsBinding.DeepCopy()
   234  			configMapNumScrapeConfigs := 0
   235  			if tt.configMap != nil {
   236  				c = c.WithRuntimeObjects(tt.configMap)
   237  				parsedConfigMap, err := getConfigData(tt.configMap)
   238  				assert.NoError(err, "Could not parse test config map")
   239  				configMapNumScrapeConfigs = len(parsedConfigMap.Search(prometheusScrapeConfigsLabel).Children())
   240  			}
   241  			if tt.secret != nil {
   242  				c = c.WithRuntimeObjects(tt.secret)
   243  				localMetricsBinding.Spec.PrometheusConfigMap = vzapi.NamespaceName{}
   244  				localMetricsBinding.Spec.PrometheusConfigSecret = vzapi.SecretKey{
   245  					Namespace: vzconst.PrometheusOperatorNamespace,
   246  					Name:      vzconst.PromAdditionalScrapeConfigsSecretName,
   247  					Key:       vzconst.PromAdditionalScrapeConfigsSecretKey,
   248  				}
   249  			}
   250  
   251  			client := c.Build()
   252  
   253  			log := vzlog.DefaultLogger()
   254  			r := newReconciler(client)
   255  			err = r.handleCustomMetricsTemplate(context.TODO(), localMetricsBinding, log)
   256  			if tt.expectError {
   257  				assert.Error(err, "Expected error handling the custom MetricsTemplate")
   258  				return
   259  			}
   260  			assert.NoError(err, "Expected no error handling the custom MetricsTemplate")
   261  
   262  			if tt.configMap != nil {
   263  				var newCM corev1.ConfigMap
   264  				err := client.Get(context.TODO(), types.NamespacedName{Namespace: vzconst.VerrazzanoSystemNamespace, Name: testConfigMapName}, &newCM)
   265  				assert.NoError(err)
   266  				assert.True(strings.Contains(newCM.Data[prometheusConfigKey], createJobName(localMetricsBinding)))
   267  				parsedPrometheusConfig, err := getConfigData(&newCM)
   268  				assert.NoError(err)
   269  				newScrapeConfigs := parsedPrometheusConfig.Search(prometheusScrapeConfigsLabel)
   270  				assert.NotNil(newScrapeConfigs)
   271  				if tt.expectConfigMapAdd {
   272  					assert.Equal(configMapNumScrapeConfigs+1, len(newScrapeConfigs.Children()))
   273  				} else {
   274  					assert.Equal(configMapNumScrapeConfigs, len(newScrapeConfigs.Children()))
   275  				}
   276  				foundJob := metricsutils.FindScrapeJob(newScrapeConfigs, createJobName(localMetricsBinding))
   277  				assert.NotNil(foundJob)
   278  			}
   279  			if tt.secret != nil {
   280  				var newSecret corev1.Secret
   281  				err := client.Get(context.TODO(), types.NamespacedName{Namespace: vzconst.PrometheusOperatorNamespace, Name: vzconst.PromAdditionalScrapeConfigsSecretName}, &newSecret)
   282  				assert.NoError(err)
   283  				assert.True(strings.Contains(string(newSecret.Data[vzconst.PromAdditionalScrapeConfigsSecretKey]), createJobName(localMetricsBinding)))
   284  			}
   285  		})
   286  	}
   287  }
   288  
   289  // TestReconcileCreateOrUpdate tests the create or update process of the reconiler
   290  // GIVEN a metrics binding
   291  // WHEN the function receives the binding
   292  // THEN the binding gets updated accordingly
   293  func TestReconcileCreateOrUpdate(t *testing.T) {
   294  	assert := asserts.New(t)
   295  
   296  	scheme := runtime.NewScheme()
   297  	_ = vzapi.AddToScheme(scheme)
   298  	_ = corev1.AddToScheme(scheme)
   299  	_ = appsv1.AddToScheme(scheme)
   300  	_ = promoperapi.AddToScheme(scheme)
   301  
   302  	labeledNs := plainNs.DeepCopy()
   303  	labeledNs.Labels = map[string]string{constants.LabelIstioInjection: "enabled"}
   304  
   305  	labeledWorkload := plainWorkload.DeepCopy()
   306  	labeledWorkload.Labels = map[string]string{constants.MetricsWorkloadLabel: testDeploymentName}
   307  
   308  	populatedTemplate, err := getTemplateTestFile()
   309  	assert.NoError(err)
   310  	testFileCM, err := getConfigMapFromTestFile(true)
   311  	assert.NoError(err)
   312  	testFileSec, err := getSecretFromTestFile(true)
   313  	assert.NoError(err)
   314  
   315  	CMMetricsBinding := metricsBinding.DeepCopy()
   316  	secMetricsBinding := metricsBinding.DeepCopy()
   317  	secMetricsBinding.Spec.PrometheusConfigMap = vzapi.NamespaceName{}
   318  	secMetricsBinding.Spec.PrometheusConfigSecret = vzapi.SecretKey{
   319  		Namespace: vzconst.PrometheusOperatorNamespace,
   320  		Name:      vzconst.PromAdditionalScrapeConfigsSecretName,
   321  		Key:       vzconst.PromAdditionalScrapeConfigsSecretKey,
   322  	}
   323  	legacyBinding := metricsBinding.DeepCopy()
   324  	legacyBinding.Spec.MetricsTemplate.Namespace = constants.LegacyDefaultMetricsTemplateNamespace
   325  	legacyBinding.Spec.MetricsTemplate.Name = constants.LegacyDefaultMetricsTemplateName
   326  	legacyBinding.Spec.PrometheusConfigMap.Namespace = vzconst.VerrazzanoSystemNamespace
   327  	legacyBinding.Spec.PrometheusConfigMap.Name = vzconst.VmiPromConfigName
   328  
   329  	legacyBindingCustomTemplateDefaultCM := metricsBinding.DeepCopy()
   330  	legacyBindingCustomTemplateDefaultCM.Spec.PrometheusConfigMap.Namespace = vzconst.VerrazzanoSystemNamespace
   331  	legacyBindingCustomTemplateDefaultCM.Spec.PrometheusConfigMap.Name = vzconst.VmiPromConfigName
   332  
   333  	tests := []struct {
   334  		name           string
   335  		metricsBinding *vzapi.MetricsBinding
   336  		workload       *appsv1.Deployment
   337  		namespace      *corev1.Namespace
   338  		configMap      *corev1.ConfigMap
   339  		secret         *corev1.Secret
   340  		requeue        bool
   341  		expectError    bool
   342  	}{
   343  		{
   344  			name:           "test configmap",
   345  			metricsBinding: CMMetricsBinding,
   346  			workload:       labeledWorkload,
   347  			namespace:      labeledNs,
   348  			configMap:      testFileCM,
   349  			requeue:        true,
   350  			expectError:    false,
   351  		},
   352  		{
   353  			name:           "test secret",
   354  			metricsBinding: secMetricsBinding,
   355  			workload:       labeledWorkload,
   356  			namespace:      labeledNs,
   357  			secret:         testFileSec,
   358  			requeue:        true,
   359  			expectError:    false,
   360  		},
   361  		{
   362  			name:           "test legacy",
   363  			metricsBinding: legacyBinding,
   364  			workload:       labeledWorkload,
   365  			namespace:      labeledNs,
   366  			secret:         testFileSec,
   367  			requeue:        true,
   368  			expectError:    false,
   369  		},
   370  		{
   371  			name:           "test legacy with custom template default CM",
   372  			metricsBinding: legacyBindingCustomTemplateDefaultCM,
   373  			workload:       labeledWorkload,
   374  			namespace:      labeledNs,
   375  			secret:         testFileSec,
   376  			requeue:        true,
   377  			expectError:    false,
   378  		},
   379  	}
   380  	for _, tt := range tests {
   381  		t.Run(tt.name, func(t *testing.T) {
   382  			c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects([]runtime.Object{
   383  				populatedTemplate,
   384  				tt.workload,
   385  				tt.namespace,
   386  				tt.metricsBinding,
   387  			}...)
   388  
   389  			if tt.configMap != nil {
   390  				c = c.WithRuntimeObjects(tt.configMap)
   391  			}
   392  			if tt.secret != nil {
   393  				c = c.WithRuntimeObjects(tt.secret)
   394  			}
   395  
   396  			client := c.Build()
   397  			log := vzlog.DefaultLogger()
   398  			r := newReconciler(client)
   399  			reconcileMetricsBinding := tt.metricsBinding.DeepCopy()
   400  			result, err := r.reconcileBindingCreateOrUpdate(context.TODO(), reconcileMetricsBinding, log)
   401  			if tt.expectError {
   402  				assert.Error(err, "Expected error reconciling the Metrics Binding")
   403  				return
   404  			}
   405  			assert.NoError(err, "Expected no error reconciling the Metrics Binding")
   406  			assert.Equal(tt.requeue, result.Requeue)
   407  
   408  			if isLegacyDefaultMetricsBinding(tt.metricsBinding) {
   409  				newMB := vzapi.MetricsBinding{}
   410  				err := client.Get(context.TODO(), types.NamespacedName{Namespace: tt.metricsBinding.Namespace, Name: tt.metricsBinding.Name}, &newMB)
   411  				assert.Error(err, "Expected not to find the Metrics Binding in the cluster")
   412  				return
   413  			}
   414  
   415  			if isLegacyVmiPrometheusConfigMapName(tt.metricsBinding.Spec.PrometheusConfigMap) {
   416  				newMB := vzapi.MetricsBinding{}
   417  				err := client.Get(context.TODO(), types.NamespacedName{Namespace: tt.metricsBinding.Namespace, Name: tt.metricsBinding.Name}, &newMB)
   418  				assert.NoError(err)
   419  
   420  				// for legacy VMI config map in metrics binding, it should be updated to have
   421  				// the additional scrape configs secret, and the config map should be removed from
   422  				// the metrics binding
   423  				assert.Empty(newMB.Spec.PrometheusConfigMap.Namespace)
   424  				assert.Empty(newMB.Spec.PrometheusConfigMap.Name)
   425  				assert.Equal(vzconst.PrometheusOperatorNamespace, newMB.Spec.PrometheusConfigSecret.Namespace)
   426  				assert.Equal(vzconst.PromAdditionalScrapeConfigsSecretName, newMB.Spec.PrometheusConfigSecret.Name)
   427  				assert.Equal(vzconst.PromAdditionalScrapeConfigsSecretKey, newMB.Spec.PrometheusConfigSecret.Key)
   428  
   429  				updatedSecret := corev1.Secret{}
   430  				err = client.Get(context.TODO(), types.NamespacedName{Namespace: vzconst.PrometheusOperatorNamespace, Name: vzconst.PromAdditionalScrapeConfigsSecretName}, &updatedSecret)
   431  				assert.NoError(err)
   432  				scrapeConfigs := updatedSecret.Data[vzconst.PromAdditionalScrapeConfigsSecretKey]
   433  				assert.NotNil(scrapeConfigs, "Expected additional scrape config secret to contain the scrape config")
   434  				assert.Contains(string(scrapeConfigs), tt.workload.Name)
   435  			}
   436  			newMB := vzapi.MetricsBinding{}
   437  			err = client.Get(context.TODO(), types.NamespacedName{Namespace: tt.metricsBinding.Namespace, Name: tt.metricsBinding.Name}, &newMB)
   438  			assert.NoError(err)
   439  			assert.Equal(1, len(newMB.Finalizers))
   440  			assert.Equal(finalizerName, newMB.Finalizers[0])
   441  			assert.Equal(1, len(newMB.OwnerReferences))
   442  			assert.Equal(tt.workload.Name, newMB.OwnerReferences[0].Name)
   443  		})
   444  	}
   445  }