github.com/argoproj/argo-cd/v3@v3.2.1/applicationset/metrics/metrics_test.go (about)

     1  package metrics
     2  
     3  import (
     4  	"net/http"
     5  	"net/http/httptest"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/prometheus/client_golang/prometheus"
    11  	"github.com/prometheus/client_golang/prometheus/promhttp"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  	"k8s.io/apimachinery/pkg/runtime"
    15  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    16  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    17  	"sigs.k8s.io/controller-runtime/pkg/metrics"
    18  	"sigs.k8s.io/yaml"
    19  
    20  	"github.com/argoproj/argo-cd/v3/applicationset/utils"
    21  	argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    22  	metricsutil "github.com/argoproj/argo-cd/v3/util/metrics"
    23  )
    24  
    25  var (
    26  	applicationsetNamespaces = []string{"argocd", "test-namespace1"}
    27  
    28  	filter = func(appset *argoappv1.ApplicationSet) bool {
    29  		return utils.IsNamespaceAllowed(applicationsetNamespaces, appset.Namespace)
    30  	}
    31  
    32  	collectedLabels = []string{"included/test"}
    33  )
    34  
    35  const fakeAppsetList = `
    36  apiVersion: argoproj.io/v1alpha1
    37  kind: ApplicationSet
    38  metadata:
    39    name: test1
    40    namespace: argocd
    41    labels:
    42      included/test: test
    43      not-included.label/test: test
    44  spec:
    45    generators:
    46    - git:
    47        directories:
    48        - path: test/*
    49        repoURL: https://github.com/test/test.git
    50        revision: HEAD
    51    template:
    52      metadata:
    53        name: '{{.path.basename}}'
    54      spec:
    55        destination:
    56          namespace: '{{.path.basename}}'
    57          server: https://kubernetes.default.svc
    58        project: default
    59        source:
    60          path: '{{.path.path}}'
    61          repoURL: https://github.com/test/test.git
    62          targetRevision: HEAD
    63  status:
    64    resources:
    65    - group: argoproj.io
    66      health:
    67        status: Missing
    68      kind: Application
    69      name: test-app1
    70      namespace: argocd
    71      status: OutOfSync
    72      version: v1alpha1
    73    - group: argoproj.io
    74      health:
    75        status: Missing
    76      kind: Application
    77      name: test-app2
    78      namespace: argocd
    79      status: OutOfSync
    80      version: v1alpha1
    81    conditions:
    82    - lastTransitionTime: "2024-01-01T00:00:00Z"
    83      message: Successfully generated parameters for all Applications
    84      reason: ApplicationSetUpToDate
    85      status: "False"
    86      type: ErrorOccurred
    87    - lastTransitionTime: "2024-01-01T00:00:00Z"
    88      message: Successfully generated parameters for all Applications
    89      reason: ParametersGenerated
    90      status: "True"
    91      type: ParametersGenerated
    92    - lastTransitionTime: "2024-01-01T00:00:00Z"
    93      message: ApplicationSet up to date
    94      reason: ApplicationSetUpToDate
    95      status: "True"
    96      type: ResourcesUpToDate
    97  ---
    98  apiVersion: argoproj.io/v1alpha1
    99  kind: ApplicationSet
   100  metadata:
   101    name: test2
   102    namespace: argocd
   103    labels:
   104      not-included.label/test: test
   105  spec:
   106    generators:
   107    - git:
   108        directories:
   109        - path: test/*
   110        repoURL: https://github.com/test/test.git
   111        revision: HEAD
   112    template:
   113      metadata:
   114        name: '{{.path.basename}}'
   115      spec:
   116        destination:
   117          namespace: '{{.path.basename}}'
   118          server: https://kubernetes.default.svc
   119        project: default
   120        source:
   121          path: '{{.path.path}}'
   122          repoURL: https://github.com/test/test.git
   123          targetRevision: HEAD
   124  ---
   125  apiVersion: argoproj.io/v1alpha1
   126  kind: ApplicationSet
   127  metadata:
   128    name: should-be-filtered-out
   129    namespace: not-allowed
   130  spec:
   131    generators:
   132    - git:
   133        directories:
   134        - path: test/*
   135        repoURL: https://github.com/test/test.git
   136        revision: HEAD
   137    template:
   138      metadata:
   139        name: '{{.path.basename}}'
   140      spec:
   141        destination:
   142          namespace: '{{.path.basename}}'
   143          server: https://kubernetes.default.svc
   144        project: default
   145        source:
   146          path: '{{.path.path}}'
   147          repoURL: https://github.com/test/test.git
   148          targetRevision: HEAD
   149  `
   150  
   151  func newFakeAppsets(fakeAppsetYAML string) []argoappv1.ApplicationSet {
   152  	var results []argoappv1.ApplicationSet
   153  
   154  	appsetRawYamls := strings.Split(fakeAppsetYAML, "---")
   155  
   156  	for _, appsetRawYaml := range appsetRawYamls {
   157  		var appset argoappv1.ApplicationSet
   158  		err := yaml.Unmarshal([]byte(appsetRawYaml), &appset)
   159  		if err != nil {
   160  			panic(err)
   161  		}
   162  
   163  		results = append(results, appset)
   164  	}
   165  
   166  	return results
   167  }
   168  
   169  func TestApplicationsetCollector(t *testing.T) {
   170  	appsetList := newFakeAppsets(fakeAppsetList)
   171  	client := initializeClient(appsetList)
   172  	metrics.Registry = prometheus.NewRegistry()
   173  
   174  	appsetCollector := newAppsetCollector(utils.NewAppsetLister(client), collectedLabels, filter)
   175  
   176  	metrics.Registry.MustRegister(appsetCollector)
   177  	req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   178  	require.NoError(t, err)
   179  	rr := httptest.NewRecorder()
   180  	handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{})
   181  	handler.ServeHTTP(rr, req)
   182  
   183  	assert.Equal(t, http.StatusOK, rr.Code)
   184  	// Test correct appset_info and owned applications
   185  	assert.Contains(t, rr.Body.String(), `
   186  argocd_appset_info{name="test1",namespace="argocd",resource_update_status="ApplicationSetUpToDate"} 1
   187  `)
   188  	assert.Contains(t, rr.Body.String(), `
   189  argocd_appset_owned_applications{name="test1",namespace="argocd"} 2
   190  `)
   191  	// Test labels collection - should not include labels not included in the list of collected labels and include the ones that do.
   192  	assert.Contains(t, rr.Body.String(), `
   193  argocd_appset_labels{label_included_test="test",name="test1",namespace="argocd"} 1
   194  `)
   195  	assert.NotContains(t, rr.Body.String(), normalizeLabel("not-included.label/test"))
   196  	// If collected label is not present on the applicationset the value should be empty
   197  	assert.Contains(t, rr.Body.String(), `
   198  argocd_appset_labels{label_included_test="",name="test2",namespace="argocd"} 1
   199  `)
   200  	// If ResourcesUpToDate condition is not present on the applicationset the status should be reported as 'Unknown'
   201  	assert.Contains(t, rr.Body.String(), `
   202  argocd_appset_info{name="test2",namespace="argocd",resource_update_status="Unknown"} 1
   203  `)
   204  	// If there are no resources on the applicationset the owned application gague should return 0
   205  	assert.Contains(t, rr.Body.String(), `
   206  argocd_appset_owned_applications{name="test2",namespace="argocd"} 0
   207  `)
   208  	// Test that filter is working
   209  	assert.NotContains(t, rr.Body.String(), `name="should-be-filtered-out"`)
   210  }
   211  
   212  func TestObserveReconcile(t *testing.T) {
   213  	appsetList := newFakeAppsets(fakeAppsetList)
   214  	client := initializeClient(appsetList)
   215  	metrics.Registry = prometheus.NewRegistry()
   216  
   217  	appsetMetrics := NewApplicationsetMetrics(utils.NewAppsetLister(client), collectedLabels, filter)
   218  
   219  	req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   220  	require.NoError(t, err)
   221  	rr := httptest.NewRecorder()
   222  	handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{})
   223  	appsetMetrics.ObserveReconcile(&appsetList[0], 5*time.Second)
   224  	handler.ServeHTTP(rr, req)
   225  	assert.Contains(t, rr.Body.String(), `
   226  argocd_appset_reconcile_sum{name="test1",namespace="argocd"} 5
   227  `)
   228  	// If there are no resources on the applicationset the owned application gague should return 0
   229  	assert.Contains(t, rr.Body.String(), `
   230  argocd_appset_reconcile_count{name="test1",namespace="argocd"} 1
   231  `)
   232  }
   233  
   234  func initializeClient(appsets []argoappv1.ApplicationSet) ctrlclient.WithWatch {
   235  	scheme := runtime.NewScheme()
   236  	err := argoappv1.AddToScheme(scheme)
   237  	if err != nil {
   238  		panic(err)
   239  	}
   240  
   241  	var clientObjects []ctrlclient.Object
   242  
   243  	for _, appset := range appsets {
   244  		clientObjects = append(clientObjects, appset.DeepCopy())
   245  	}
   246  
   247  	return fake.NewClientBuilder().WithScheme(scheme).WithObjects(clientObjects...).Build()
   248  }
   249  
   250  func normalizeLabel(label string) string {
   251  	return metricsutil.NormalizeLabels("label", []string{label})[0]
   252  }