github.com/verrazzano/verrazzano@v1.7.1/tests/e2e/pkg/jaeger.go (about)

     1  // Copyright (c) 2022, 2023, 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 pkg
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"github.com/verrazzano/verrazzano/pkg/vzcr"
    11  	"github.com/verrazzano/verrazzano/platform-operator/controllers/verrazzano/component/opensearch"
    12  	"net/http"
    13  	"net/url"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/hashicorp/go-retryablehttp"
    19  	globalconst "github.com/verrazzano/verrazzano/pkg/constants"
    20  	"github.com/verrazzano/verrazzano/pkg/k8sutil"
    21  	"github.com/verrazzano/verrazzano/platform-operator/constants"
    22  	appsv1 "k8s.io/api/apps/v1"
    23  	v1 "k8s.io/api/batch/v1"
    24  	"k8s.io/api/batch/v1beta1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/labels"
    27  	"k8s.io/apimachinery/pkg/selection"
    28  	"k8s.io/client-go/kubernetes"
    29  )
    30  
    31  const (
    32  	jaegerSpanIndexPrefix       = "verrazzano-jaeger-span"
    33  	jaegerClusterNameLabel      = "verrazzano_cluster"
    34  	adminClusterName            = "local"
    35  	jaegerOperatorSampleMetric  = "jaeger_operator_instances_managed"
    36  	jaegerAgentSampleMetric     = "jaeger_agent_collector_proxy_total"
    37  	jaegerQuerySampleMetric     = "jaeger_query_requests_total"
    38  	jaegerCollectorSampleMetric = "jaeger_collector_queue_capacity"
    39  	jaegerESIndexCleanerJob     = "jaeger-operator-jaeger-es-index-cleaner"
    40  	componentLabelKey           = "app.kubernetes.io/component"
    41  	instanceLabelKey            = "app.kubernetes.io/instance"
    42  )
    43  
    44  const (
    45  	jaegerListServicesErrFmt = "Error listing services in Jaeger: url=%s, error=%v"
    46  	jaegerListTracesErrFmt   = "Error listing traces in Jaeger: url=%s, error=%v"
    47  )
    48  
    49  var (
    50  	// common services running in both admin and managed cluster
    51  	managedClusterSystemServiceNames = []string{
    52  		"fluentd.verrazzano-system",
    53  		"verrazzano-authproxy.verrazzano-system",
    54  	}
    55  
    56  	// services that are common plus the ones unique to admin cluster
    57  	adminClusterSystemServiceNames = append(managedClusterSystemServiceNames,
    58  		"jaeger.verrazzano-monitoring")
    59  )
    60  
    61  type JaegerTraceData struct {
    62  	TraceID string `json:"traceID"`
    63  	Spans   []struct {
    64  		TraceID       string `json:"traceID"`
    65  		SpanID        string `json:"spanID"`
    66  		Flags         int    `json:"flags"`
    67  		OperationName string `json:"operationName"`
    68  		References    []struct {
    69  			RefType string `json:"refType"`
    70  			TraceID string `json:"traceID"`
    71  			SpanID  string `json:"spanID"`
    72  		} `json:"references"`
    73  		StartTime int64 `json:"startTime"`
    74  		Duration  int   `json:"duration"`
    75  		Tags      []struct {
    76  			Key   string      `json:"key"`
    77  			Type  string      `json:"type"`
    78  			Value interface{} `json:"value"`
    79  		} `json:"tags"`
    80  		Logs []struct {
    81  			Timestamp int64 `json:"timestamp"`
    82  			Fields    []struct {
    83  				Key   string `json:"key"`
    84  				Type  string `json:"type"`
    85  				Value string `json:"value"`
    86  			} `json:"fields"`
    87  		} `json:"logs"`
    88  		ProcessID string      `json:"processID"`
    89  		Warnings  interface{} `json:"warnings"`
    90  	} `json:"spans"`
    91  	Processes struct {
    92  		P1 struct {
    93  			ServiceName string `json:"serviceName"`
    94  			Tags        []struct {
    95  				Key   string `json:"key"`
    96  				Type  string `json:"type"`
    97  				Value string `json:"value"`
    98  			} `json:"tags"`
    99  		} `json:"p1"`
   100  	} `json:"processes"`
   101  	Warnings interface{} `json:"warnings"`
   102  }
   103  
   104  type JaegerTraceDataWrapper struct {
   105  	Data   []JaegerTraceData `json:"data"`
   106  	Total  int               `json:"total"`
   107  	Limit  int               `json:"limit"`
   108  	Offset int               `json:"offset"`
   109  	Errors interface{}       `json:"errors"`
   110  }
   111  
   112  // IsJaegerInstanceCreated checks whether the default Jaeger CR is created
   113  func IsJaegerInstanceCreated(kubeconfigPath string) (bool, error) {
   114  	collectorDeployments, err := GetJaegerCollectorDeployments(kubeconfigPath, globalconst.JaegerInstanceName)
   115  	if err != nil {
   116  		return false, err
   117  	}
   118  	Log(Info, fmt.Sprintf("cluster has %d jaeger-collector deployments", len(collectorDeployments)))
   119  	queryDeployments, err := GetJaegerQueryDeployments(kubeconfigPath, globalconst.JaegerInstanceName)
   120  	if err != nil {
   121  		return false, err
   122  	}
   123  	Log(Info, fmt.Sprintf("cluster has %d jaeger-query deployments", len(queryDeployments)))
   124  	return len(collectorDeployments) > 0 && len(queryDeployments) > 0, nil
   125  }
   126  
   127  // GetJaegerCollectorDeployments returns the deployment object of the Jaeger collector corresponding to the given
   128  //
   129  //	Jaeger instance. If no instance name is provided, then it returns all Jaeger collector pods in the
   130  //
   131  // //		verrazzano-monitoring namespace.
   132  func GetJaegerCollectorDeployments(kubeconfigPath, jaegerCRName string) ([]appsv1.Deployment, error) {
   133  	labels := map[string]string{
   134  		componentLabelKey: globalconst.JaegerCollectorComponentName,
   135  	}
   136  	if jaegerCRName != "" {
   137  		labels[instanceLabelKey] = jaegerCRName
   138  	}
   139  	Log(Info, fmt.Sprintf("Checking for collector deployments with labels %v", labels))
   140  	deployments, err := ListDeploymentsMatchingLabelsInCluster(kubeconfigPath, constants.VerrazzanoMonitoringNamespace, labels)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	return deployments.Items, err
   145  }
   146  
   147  // GetJaegerQueryDeployments returns the deployment object of the Jaeger query corresponding to the given
   148  //
   149  //	Jaeger instance. If no Jaeger instance name is provided, then it returns all Jaeger query pods in the
   150  //	verrazzano-monitoring namespace
   151  func GetJaegerQueryDeployments(kubeconfigPath, jaegerCRName string) ([]appsv1.Deployment, error) {
   152  	labels := map[string]string{
   153  		componentLabelKey: globalconst.JaegerQueryComponentName,
   154  	}
   155  	if jaegerCRName != "" {
   156  		labels[instanceLabelKey] = jaegerCRName
   157  	}
   158  	Log(Info, fmt.Sprintf("Checking for query deployments with labels %v", labels))
   159  	deployments, err := ListDeploymentsMatchingLabelsInCluster(kubeconfigPath, constants.VerrazzanoMonitoringNamespace, labels)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  	return deployments.Items, err
   164  }
   165  
   166  // JaegerSpanRecordFoundInOpenSearch checks if jaeger span records are found in OpenSearch storage
   167  func JaegerSpanRecordFoundInOpenSearch(kubeconfigPath string, after time.Time, serviceName string) bool {
   168  	indexName, err := GetJaegerSpanIndexName(kubeconfigPath)
   169  	if err != nil {
   170  		return false
   171  	}
   172  	fields := map[string]string{
   173  		"process.serviceName": serviceName,
   174  	}
   175  	searchResult := querySystemOpenSearch(indexName, fields, kubeconfigPath, false)
   176  	if len(searchResult) == 0 {
   177  		Log(Info, fmt.Sprintf("Expected to find log record matching fields %v", fields))
   178  		return false
   179  	}
   180  	found := findJaegerSpanHits(searchResult, &after)
   181  	if !found {
   182  		Log(Error, fmt.Sprintf("Failed to find recent jaeger span record for service %s", serviceName))
   183  	}
   184  	return found
   185  }
   186  
   187  // GetJaegerSpanIndexName returns the index name used in OpenSearch used for storage
   188  func GetJaegerSpanIndexName(kubeconfigPath string) (string, error) {
   189  	var jaegerIndices []string
   190  	for _, indexName := range listSystemOpenSearchIndices(kubeconfigPath) {
   191  		if strings.HasPrefix(indexName, jaegerSpanIndexPrefix) {
   192  			jaegerIndices = append(jaegerIndices, indexName)
   193  			break
   194  		}
   195  	}
   196  	if len(jaegerIndices) > 0 {
   197  		return jaegerIndices[0], nil
   198  	}
   199  	return "", fmt.Errorf("Jaeger Span index not found")
   200  }
   201  
   202  // ListJaegerTracesWithTags lists all trace ids for a given service with the given tags
   203  func ListJaegerTracesWithTags(kubeconfigPath string, start time.Time, serviceName string, tags map[string]string) []string {
   204  	var traces []string
   205  	params := url.Values{}
   206  	params.Add("service", serviceName)
   207  	params.Add("start", strconv.FormatInt(start.UnixMicro(), 10))
   208  	params.Add("end", strconv.FormatInt(time.Now().UnixMicro(), 10))
   209  	jsonStr, err := json.Marshal(tags)
   210  	if err != nil {
   211  		Log(Error, fmt.Sprintf("Error parsing tags %v to JSON string", tags))
   212  		return traces
   213  	}
   214  	params.Add("tags", string(jsonStr))
   215  	url := fmt.Sprintf("%s/api/traces?%s", getJaegerURL(kubeconfigPath), params.Encode())
   216  	username, password, err := getJaegerUsernamePassword(kubeconfigPath)
   217  	if err != nil {
   218  		return traces
   219  	}
   220  	resp, err := getJaegerWithBasicAuth(url, "", username, password, kubeconfigPath)
   221  	if err != nil {
   222  		Log(Error, fmt.Sprintf(jaegerListTracesErrFmt, url, err))
   223  		return traces
   224  	}
   225  	if resp.StatusCode != http.StatusOK {
   226  		Log(Error, fmt.Sprintf(jaegerListTracesErrFmt, url, resp.StatusCode))
   227  		return traces
   228  	}
   229  	var jaegerTraceDataWrapper JaegerTraceDataWrapper
   230  	json.Unmarshal(resp.Body, &jaegerTraceDataWrapper)
   231  	for _, traceObj := range jaegerTraceDataWrapper.Data {
   232  		traces = append(traces, traceObj.TraceID)
   233  	}
   234  	Log(Info, fmt.Sprintf("Found %d traces for service %s", len(traces), serviceName))
   235  	return traces
   236  }
   237  
   238  // ListServicesInJaeger lists the services whose traces are available in Jaeger
   239  func ListServicesInJaeger(kubeconfigPath string) []string {
   240  	var services []string
   241  	url := fmt.Sprintf("%s/api/services", getJaegerURL(kubeconfigPath))
   242  	username, password, err := getJaegerUsernamePassword(kubeconfigPath)
   243  	if err != nil {
   244  		return services
   245  	}
   246  	resp, err := getJaegerWithBasicAuth(url, "", username, password, kubeconfigPath)
   247  	if err != nil {
   248  		Log(Error, fmt.Sprintf(jaegerListServicesErrFmt, url, err))
   249  		return services
   250  	}
   251  	if resp.StatusCode != http.StatusOK {
   252  		Log(Error, fmt.Sprintf(jaegerListServicesErrFmt, url, resp.StatusCode))
   253  		return services
   254  	}
   255  	var serviceMap map[string][]string
   256  	json.Unmarshal(resp.Body, &serviceMap)
   257  	services = append(services, serviceMap["data"]...)
   258  	return services
   259  }
   260  
   261  // DoesCronJobExist returns whether a cronjob with the given name and namespace exists for the cluster
   262  func DoesCronJobExist(kubeconfigPath, namespace string, name string) (bool, error) {
   263  	cronjobs, err := ListCronJobNamesMatchingLabels(kubeconfigPath, namespace, nil)
   264  	if err != nil {
   265  		Log(Error, fmt.Sprintf("Failed listing deployments in cluster for namespace %s: %v", namespace, err))
   266  		return false, err
   267  	}
   268  	for _, cronJobName := range cronjobs {
   269  		if strings.HasPrefix(cronJobName, name) {
   270  			return true, nil
   271  		}
   272  	}
   273  	return false, nil
   274  }
   275  
   276  // ListDeploymentsMatchingLabelsInCluster returns the list of deployments in a given namespace matching the given labels for the cluster
   277  func ListDeploymentsMatchingLabelsInCluster(kubeconfigPath, namespace string, matchLabels map[string]string) (*appsv1.DeploymentList, error) {
   278  	// Get the Kubernetes clientset
   279  	clientset, err := GetKubernetesClientsetForCluster(kubeconfigPath)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	listOptions := metav1.ListOptions{}
   284  	if matchLabels != nil {
   285  		selector := labels.NewSelector()
   286  		for k, v := range matchLabels {
   287  			selectorLabel, _ := labels.NewRequirement(k, selection.Equals, []string{v})
   288  			selector = selector.Add(*selectorLabel)
   289  		}
   290  		listOptions.LabelSelector = selector.String()
   291  	}
   292  	deployments, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), listOptions)
   293  	if err != nil {
   294  		Log(Error, fmt.Sprintf("Failed to list deployments in namespace %s: %v", namespace, err))
   295  		return nil, err
   296  	}
   297  	return deployments, nil
   298  }
   299  
   300  // ListCronJobNamesMatchingLabels returns the list of cronjobs in a given namespace matching the given labels for the cluster
   301  func ListCronJobNamesMatchingLabels(kubeconfigPath, namespace string, matchLabels map[string]string) ([]string, error) {
   302  	var cronjobNames []string
   303  	// Get the Kubernetes clientset
   304  	clientset, err := GetKubernetesClientsetForCluster(kubeconfigPath)
   305  	if err != nil {
   306  		return nil, err
   307  	}
   308  	info, err := clientset.ServerVersion()
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  	majorVersion, err := strconv.Atoi(info.Major)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  	if majorVersion > 1 {
   317  		return nil, fmt.Errorf("Unknown major version %d", majorVersion)
   318  	}
   319  	minorVersion, err := strconv.Atoi(info.Minor)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  	// For k8s version 1.20 and lesser, cronjobs are created under version batch/v1beta1
   324  	// For k8s version greater than 1.20, cronjobs are created under version batch/v1
   325  	if minorVersion <= 20 {
   326  		cronJobs, err := listV1Beta1CronJobNames(clientset, namespace, fillLabelSelectors(matchLabels))
   327  		if err != nil {
   328  			return nil, err
   329  		}
   330  		for _, cronjob := range cronJobs {
   331  			cronjobNames = append(cronjobNames, cronjob.Name)
   332  		}
   333  	} else {
   334  		cronJobs, err := listV1CronJobNames(clientset, namespace, fillLabelSelectors(matchLabels))
   335  		if err != nil {
   336  			return nil, err
   337  		}
   338  		for _, cronjob := range cronJobs {
   339  			cronjobNames = append(cronjobNames, cronjob.Name)
   340  		}
   341  	}
   342  	return cronjobNames, nil
   343  }
   344  
   345  // GetJaegerSystemServicesInManagedCluster returns the system services that needs to be running in a managed cluster
   346  func GetJaegerSystemServicesInManagedCluster() []string {
   347  	return managedClusterSystemServiceNames
   348  }
   349  
   350  // GetJaegerSystemServicesInAdminCluster returns the system services that needs to be running in a admin cluster
   351  func GetJaegerSystemServicesInAdminCluster() []string {
   352  	return adminClusterSystemServiceNames
   353  }
   354  
   355  // ValidateJaegerOperatorMetricFunc returns a function that validates if metrics of Jaeger operator is scraped by prometheus.
   356  func ValidateJaegerOperatorMetricFunc(metricsTest MetricsTest) func() bool {
   357  	return func() bool {
   358  		return metricsTest.MetricsExist(jaegerOperatorSampleMetric, map[string]string{})
   359  	}
   360  }
   361  
   362  // ValidateJaegerCollectorMetricFunc returns a function that validates if metrics of Jaeger collector is scraped by prometheus.
   363  func ValidateJaegerCollectorMetricFunc(metricsTest MetricsTest) func() bool {
   364  	return func() bool {
   365  		return metricsTest.MetricsExist(jaegerCollectorSampleMetric, map[string]string{})
   366  	}
   367  }
   368  
   369  // ValidateJaegerQueryMetricFunc returns a function that validates if metrics of Jaeger query is scraped by prometheus.
   370  func ValidateJaegerQueryMetricFunc(metricsTest MetricsTest) func() bool {
   371  	return func() bool {
   372  		return metricsTest.MetricsExist(jaegerQuerySampleMetric, map[string]string{})
   373  	}
   374  }
   375  
   376  // ValidateJaegerAgentMetricFunc returns a function that validates if metrics of Jaeger agent is scraped by prometheus.
   377  func ValidateJaegerAgentMetricFunc(metricsTest MetricsTest) func() bool {
   378  	return func() bool {
   379  		return metricsTest.MetricsExist(jaegerAgentSampleMetric, map[string]string{})
   380  	}
   381  }
   382  
   383  // ValidateEsIndexCleanerCronJobFunc returns a function that validates if cron job for periodically cleaning the OS indices are created.
   384  func ValidateEsIndexCleanerCronJobFunc() func() (bool, error) {
   385  	return func() (bool, error) {
   386  		kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
   387  		if err != nil {
   388  			return false, err
   389  		}
   390  		vz, err := GetVerrazzanoInstallResourceInCluster(kubeconfigPath)
   391  		if err != nil {
   392  			return false, err
   393  		}
   394  		create := vzcr.IsComponentStatusEnabled(vz, opensearch.ComponentName)
   395  		if create {
   396  			return DoesCronJobExist(kubeconfigPath, constants.VerrazzanoMonitoringNamespace, jaegerESIndexCleanerJob)
   397  		}
   398  		return false, nil
   399  	}
   400  }
   401  
   402  // ValidateSystemTracesFuncInCluster returns a function that validates if system traces for the given cluster can be successfully queried from Jaeger
   403  func ValidateSystemTracesFuncInCluster(kubeconfigPath string, start time.Time, clusterName string) func() (bool, error) {
   404  	return func() (bool, error) {
   405  		// Check if the service name is registered in Jaeger and traces are present for that service
   406  		systemServices := GetJaegerSystemServicesInManagedCluster()
   407  		if clusterName == "admin" || clusterName == "local" {
   408  			systemServices = GetJaegerSystemServicesInAdminCluster()
   409  		}
   410  		tracesFound := true
   411  		for i := 0; i < len(systemServices); i++ {
   412  			Log(Info, fmt.Sprintf("Inspecting traces for service: %s", systemServices[i]))
   413  			if i == 0 {
   414  				tracesFound =
   415  					len(ListJaegerTracesWithTags(kubeconfigPath, start, systemServices[i],
   416  						map[string]string{"verrazzano_cluster": clusterName})) > 0
   417  			} else {
   418  				tracesFound = tracesFound && len(ListJaegerTracesWithTags(kubeconfigPath, start, systemServices[i],
   419  					map[string]string{"verrazzano_cluster": clusterName})) > 0
   420  			}
   421  			Log(Info, fmt.Sprintf("Trace found flag for service: %s is %v", systemServices[i], tracesFound))
   422  			// return early and retry later
   423  			if !tracesFound {
   424  				return false, nil
   425  			}
   426  		}
   427  		return tracesFound, nil
   428  	}
   429  }
   430  
   431  // ValidateSystemTracesInOSFunc returns a function that validates if system traces are stored successfully in OS backend storage
   432  func ValidateSystemTracesInOSFunc(start time.Time) func() bool {
   433  	return func() bool {
   434  		kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
   435  		if err != nil {
   436  			return false
   437  		}
   438  		tracesFound := true
   439  		systemServices := GetJaegerSystemServicesInAdminCluster()
   440  		for i := 0; i < len(systemServices); i++ {
   441  			Log(Info, fmt.Sprintf("Finding traces for service %s after %s", systemServices[i], start.String()))
   442  			if i == 0 {
   443  				tracesFound = JaegerSpanRecordFoundInOpenSearch(kubeconfigPath, start, systemServices[i])
   444  			} else {
   445  				tracesFound = tracesFound && JaegerSpanRecordFoundInOpenSearch(kubeconfigPath, start, systemServices[i])
   446  			}
   447  			// return early and retry later
   448  			if !tracesFound {
   449  				return false
   450  			}
   451  		}
   452  		return tracesFound
   453  	}
   454  }
   455  
   456  // ValidateApplicationTracesInCluster returns a function that validates if application traces can be successfully queried from Jaeger
   457  func ValidateApplicationTracesInCluster(kubeconfigPath string, start time.Time, appServiceName, clusterName string) func() (bool, error) {
   458  	return func() (bool, error) {
   459  		tracesFound := false
   460  		servicesWithJaegerTraces := ListServicesInJaeger(kubeconfigPath)
   461  		for _, serviceName := range servicesWithJaegerTraces {
   462  			Log(Info, fmt.Sprintf("Checking if service name %s matches the expected app service %s", serviceName, appServiceName))
   463  			if strings.HasPrefix(serviceName, appServiceName) {
   464  				Log(Info, fmt.Sprintf("Finding traces for service %s after %s", serviceName, start.String()))
   465  				traceIds := ListJaegerTracesWithTags(kubeconfigPath, start, appServiceName,
   466  					map[string]string{"verrazzano_cluster": clusterName})
   467  				tracesFound = len(traceIds) > 0
   468  				if !tracesFound {
   469  					errMsg := fmt.Sprintf("traces not found for service: %s", serviceName)
   470  					Log(Error, errMsg)
   471  					return false, fmt.Errorf(errMsg)
   472  				}
   473  				break
   474  			}
   475  		}
   476  		return tracesFound, nil
   477  	}
   478  }
   479  
   480  // ValidateApplicationTracesInOS returns a function that validates if application traces are stored successfully in OS backend storage
   481  func ValidateApplicationTracesInOS(start time.Time, appServiceName string) func() bool {
   482  	return func() bool {
   483  		kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
   484  		if err != nil {
   485  			return false
   486  		}
   487  		return JaegerSpanRecordFoundInOpenSearch(kubeconfigPath, start, appServiceName)
   488  	}
   489  }
   490  
   491  // GenerateTrafficForTraces creates some HTTP requests to the application so that the corresponding traces would be generated for them.
   492  func GenerateTrafficForTraces(namespace, appConfigName, urlPath, kubeconfigPath string) error {
   493  	// Get the host URL from the gateway and send 10 test requests to generate traces
   494  	host, err := k8sutil.GetHostnameFromGatewayInCluster(namespace, appConfigName, kubeconfigPath)
   495  	if err != nil {
   496  		Log(Error, err.Error())
   497  		return err
   498  	}
   499  	Log(Info, fmt.Sprintf("Found hostname %s from gateway", host))
   500  	for i := 0; i < 10; i++ {
   501  		url := fmt.Sprintf("https://%s/%s", host, urlPath)
   502  		resp, err := GetWebPageInCluster(url, host, kubeconfigPath)
   503  		if err != nil {
   504  			Log(Error, fmt.Sprintf("Error sending request to %s app: %v", appConfigName, err.Error()))
   505  			return err
   506  		}
   507  		if resp.StatusCode == http.StatusOK {
   508  			Log(Info, fmt.Sprintf("Successfully sent request to %s app: %v", appConfigName, resp.StatusCode))
   509  		} else {
   510  			err = fmt.Errorf("got error response code %v", resp.StatusCode)
   511  			Log(Error, err.Error())
   512  		}
   513  	}
   514  	return nil
   515  }
   516  
   517  // fillLabelSelectors fills the match labels from map to be passed in list options
   518  func fillLabelSelectors(matchLabels map[string]string) metav1.ListOptions {
   519  	listOptions := metav1.ListOptions{}
   520  	if matchLabels != nil {
   521  		var selector labels.Selector
   522  		for k, v := range matchLabels {
   523  			selectorLabel, _ := labels.NewRequirement(k, selection.Equals, []string{v})
   524  			selector = labels.NewSelector()
   525  			selector = selector.Add(*selectorLabel)
   526  		}
   527  		listOptions.LabelSelector = selector.String()
   528  	}
   529  	return listOptions
   530  }
   531  
   532  // listV1CronJobNames lists the cronjob under batch/v1 api version for k8s version > 1.20
   533  func listV1CronJobNames(clientset *kubernetes.Clientset, namespace string, listOptions metav1.ListOptions) ([]v1.CronJob, error) {
   534  	var cronJobs []v1.CronJob
   535  	cronJobList, err := clientset.BatchV1().CronJobs(namespace).List(context.TODO(), listOptions)
   536  	if err != nil {
   537  		Log(Error, fmt.Sprintf("Failed to list v1/cronjobs in namespace %s: %v", namespace, err))
   538  		return cronJobs, err
   539  	}
   540  	return cronJobList.Items, nil
   541  }
   542  
   543  // listV1Beta1CronJobNames lists the cronjob under batch/v1beta1 api version for k8s version <= 1.20
   544  func listV1Beta1CronJobNames(clientset *kubernetes.Clientset, namespace string, listOptions metav1.ListOptions) ([]v1beta1.CronJob, error) {
   545  	var cronJobs []v1beta1.CronJob
   546  	cronJobList, err := clientset.BatchV1beta1().CronJobs(namespace).List(context.TODO(), listOptions)
   547  	if err != nil {
   548  		Log(Error, fmt.Sprintf("Failed to list v1beta1/cronjobs in namespace %s: %v", namespace, err))
   549  		return cronJobs, err
   550  	}
   551  	return cronJobList.Items, nil
   552  }
   553  
   554  // getJaegerWithBasicAuth access Jaeger with GET using basic auth, using a given kubeconfig
   555  func getJaegerWithBasicAuth(url string, hostHeader string, username string, password string, kubeconfigPath string) (*HTTPResponse, error) {
   556  	retryableClient, err := getJaegerClient(kubeconfigPath)
   557  	if err != nil {
   558  		return nil, err
   559  	}
   560  	return doReq(url, "GET", "", hostHeader, username, password, nil, retryableClient)
   561  }
   562  
   563  // getJaegerClient returns the Jaeger client which can be used for GET/POST operations using a given kubeconfig
   564  func getJaegerClient(kubeconfigPath string) (*retryablehttp.Client, error) {
   565  	client, err := GetVerrazzanoHTTPClient(kubeconfigPath)
   566  	if err != nil {
   567  		return nil, err
   568  	}
   569  	return client, err
   570  }
   571  
   572  // getJaegerURL returns Jaeger URL from the corresponding ingress resource using the given kubeconfig
   573  func getJaegerURL(kubeconfigPath string) string {
   574  	clientset, err := GetKubernetesClientsetForCluster(kubeconfigPath)
   575  	if err != nil {
   576  		Log(Error, fmt.Sprintf("Failed to get clientset for cluster %v", err))
   577  		return ""
   578  	}
   579  	ingressList, _ := clientset.NetworkingV1().Ingresses(VerrazzanoNamespace).List(context.TODO(), metav1.ListOptions{})
   580  	for _, ingress := range ingressList.Items {
   581  		if ingress.Name == "verrazzano-jaeger" {
   582  			Log(Info, fmt.Sprintf("Found Jaeger Ingress %v, host %s", ingress.Name, ingress.Spec.Rules[0].Host))
   583  			return fmt.Sprintf("https://%s", ingress.Spec.Rules[0].Host)
   584  		}
   585  	}
   586  	return ""
   587  }
   588  
   589  // getJaegerUsernamePassword returns the username/password for connecting to Jaeger
   590  func getJaegerUsernamePassword(kubeconfigPath string) (username, password string, err error) {
   591  	password, err = GetVerrazzanoPasswordInCluster(kubeconfigPath)
   592  	if err != nil {
   593  		return "", "", err
   594  	}
   595  	return "verrazzano", password, err
   596  }
   597  
   598  // findJaegerSpanHits returns the number of span hits that are older than the given time
   599  func findJaegerSpanHits(searchResult map[string]interface{}, after *time.Time) bool {
   600  	hits := Jq(searchResult, "hits", "hits")
   601  	if hits == nil {
   602  		Log(Info, "Expected to find hits in span record query results")
   603  		return false
   604  	}
   605  	Log(Info, fmt.Sprintf("Found %d records", len(hits.([]interface{}))))
   606  	if len(hits.([]interface{})) == 0 {
   607  		Log(Info, "Expected span record query results to contain at least one hit")
   608  		return false
   609  	}
   610  	if after == nil {
   611  		return true
   612  	}
   613  	for _, hit := range hits.([]interface{}) {
   614  		timestamp := Jq(hit, "_source", "startTimeMillis")
   615  		t := time.UnixMilli(int64(timestamp.(float64)))
   616  		if t.After(*after) {
   617  			Log(Info, fmt.Sprintf("Found recent record: %f", timestamp))
   618  			return true
   619  		}
   620  		Log(Info, fmt.Sprintf("Found old record: %f", timestamp))
   621  	}
   622  	return true
   623  }