...

Source file src/k8s.io/kubernetes/pkg/controller/podautoscaler/metrics/client_test.go

Documentation: k8s.io/kubernetes/pkg/controller/podautoscaler/metrics

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package metrics
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"testing"
    23  	"time"
    24  
    25  	autoscalingapi "k8s.io/api/autoscaling/v2"
    26  	v1 "k8s.io/api/core/v1"
    27  	"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
    28  	"k8s.io/apimachinery/pkg/api/resource"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	core "k8s.io/client-go/testing"
    34  	"k8s.io/kubernetes/pkg/api/legacyscheme"
    35  	_ "k8s.io/kubernetes/pkg/apis/apps/install"
    36  	cmapi "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2"
    37  	emapi "k8s.io/metrics/pkg/apis/external_metrics/v1beta1"
    38  	metricsapi "k8s.io/metrics/pkg/apis/metrics/v1beta1"
    39  	metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake"
    40  	cmfake "k8s.io/metrics/pkg/client/custom_metrics/fake"
    41  	emfake "k8s.io/metrics/pkg/client/external_metrics/fake"
    42  
    43  	"github.com/stretchr/testify/assert"
    44  )
    45  
    46  var fixedTimestamp = time.Date(2015, time.November, 10, 12, 30, 0, 0, time.UTC)
    47  
    48  // timestamp is used for establishing order on metricPoints
    49  type metricPoint struct {
    50  	level     uint64
    51  	timestamp int
    52  }
    53  
    54  type restClientTestCase struct {
    55  	desiredMetricValues PodMetricsInfo
    56  	desiredError        error
    57  
    58  	// "timestamps" here are actually the offset in minutes from a base timestamp
    59  	targetTimestamp      int
    60  	window               time.Duration
    61  	reportedMetricPoints []metricPoint
    62  	reportedPodMetrics   []map[string]int64
    63  	singleObject         *autoscalingapi.CrossVersionObjectReference
    64  
    65  	namespace           string
    66  	selector            labels.Selector
    67  	resourceName        v1.ResourceName
    68  	container           string
    69  	metricName          string
    70  	metricSelector      *metav1.LabelSelector
    71  	metricLabelSelector labels.Selector
    72  }
    73  
    74  func (tc *restClientTestCase) prepareTestClient(t *testing.T) (*metricsfake.Clientset, *cmfake.FakeCustomMetricsClient, *emfake.FakeExternalMetricsClient) {
    75  	namespace := "test-namespace"
    76  	tc.namespace = namespace
    77  	podNamePrefix := "test-pod"
    78  	podLabels := map[string]string{"name": podNamePrefix}
    79  	tc.selector = labels.SelectorFromSet(podLabels)
    80  
    81  	// it's a resource test if we have a resource name
    82  	isResource := len(tc.resourceName) > 0
    83  	// it's an external test if we have a metric selector
    84  	isExternal := tc.metricSelector != nil
    85  
    86  	fakeMetricsClient := &metricsfake.Clientset{}
    87  	fakeCMClient := &cmfake.FakeCustomMetricsClient{}
    88  	fakeEMClient := &emfake.FakeExternalMetricsClient{}
    89  
    90  	if isResource {
    91  		fakeMetricsClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
    92  			metrics := &metricsapi.PodMetricsList{}
    93  			for i, containers := range tc.reportedPodMetrics {
    94  				metric := metricsapi.PodMetrics{
    95  					ObjectMeta: metav1.ObjectMeta{
    96  						Name:      fmt.Sprintf("%s-%d", podNamePrefix, i),
    97  						Namespace: namespace,
    98  						Labels:    podLabels,
    99  					},
   100  					Timestamp:  metav1.Time{Time: offsetTimestampBy(tc.targetTimestamp)},
   101  					Window:     metav1.Duration{Duration: tc.window},
   102  					Containers: []metricsapi.ContainerMetrics{},
   103  				}
   104  				for containerName, cpu := range containers {
   105  					cm := metricsapi.ContainerMetrics{
   106  						Name: containerName,
   107  						Usage: v1.ResourceList{
   108  							v1.ResourceCPU: *resource.NewMilliQuantity(
   109  								cpu,
   110  								resource.DecimalSI),
   111  							v1.ResourceMemory: *resource.NewQuantity(
   112  								int64(1024*1024),
   113  								resource.BinarySI),
   114  						},
   115  					}
   116  					metric.Containers = append(metric.Containers, cm)
   117  				}
   118  				metrics.Items = append(metrics.Items, metric)
   119  			}
   120  			return true, metrics, nil
   121  		})
   122  	} else if isExternal {
   123  		fakeEMClient.AddReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
   124  			listAction := action.(core.ListAction)
   125  			assert.Equal(t, tc.metricName, listAction.GetResource().Resource, "the metric requested should have matched the one specified.")
   126  			assert.Equal(t, tc.metricLabelSelector, listAction.GetListRestrictions().Labels, "the metric selector should have matched the one specified")
   127  
   128  			metrics := emapi.ExternalMetricValueList{}
   129  			for _, metricPoint := range tc.reportedMetricPoints {
   130  				timestamp := offsetTimestampBy(metricPoint.timestamp)
   131  				metric := emapi.ExternalMetricValue{
   132  					Value:      *resource.NewMilliQuantity(int64(metricPoint.level), resource.DecimalSI),
   133  					Timestamp:  metav1.Time{Time: timestamp},
   134  					MetricName: tc.metricName,
   135  				}
   136  				metrics.Items = append(metrics.Items, metric)
   137  			}
   138  			return true, &metrics, nil
   139  		})
   140  	} else {
   141  		fakeCMClient.AddReactor("get", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
   142  			getForAction := action.(cmfake.GetForAction)
   143  			assert.Equal(t, tc.metricName, getForAction.GetMetricName(), "the metric requested should have matched the one specified")
   144  
   145  			if getForAction.GetName() == "*" {
   146  				// multiple objects
   147  				metrics := cmapi.MetricValueList{}
   148  				assert.Equal(t, "pods", getForAction.GetResource().Resource, "type of object that we requested multiple metrics for should have been pods")
   149  
   150  				for i, metricPoint := range tc.reportedMetricPoints {
   151  					timestamp := offsetTimestampBy(metricPoint.timestamp)
   152  					metric := cmapi.MetricValue{
   153  						DescribedObject: v1.ObjectReference{
   154  							Kind:       "Pod",
   155  							APIVersion: "v1",
   156  							Name:       fmt.Sprintf("%s-%d", podNamePrefix, i),
   157  						},
   158  						Value:     *resource.NewMilliQuantity(int64(metricPoint.level), resource.DecimalSI),
   159  						Timestamp: metav1.Time{Time: timestamp},
   160  						Metric: cmapi.MetricIdentifier{
   161  							Name: tc.metricName,
   162  						},
   163  					}
   164  
   165  					metrics.Items = append(metrics.Items, metric)
   166  				}
   167  
   168  				return true, &metrics, nil
   169  			} else {
   170  				name := getForAction.GetName()
   171  				mapper := testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme)
   172  				assert.NotNil(t, tc.singleObject, "should have only requested a single-object metric when we asked for metrics for a single object")
   173  				gk := schema.FromAPIVersionAndKind(tc.singleObject.APIVersion, tc.singleObject.Kind).GroupKind()
   174  				mapping, err := mapper.RESTMapping(gk)
   175  				if err != nil {
   176  					return true, nil, fmt.Errorf("unable to get mapping for %s: %v", gk.String(), err)
   177  				}
   178  				groupResource := mapping.Resource.GroupResource()
   179  
   180  				assert.Equal(t, groupResource.String(), getForAction.GetResource().Resource, "should have requested metrics for the resource matching the GroupKind passed in")
   181  				assert.Equal(t, tc.singleObject.Name, name, "should have requested metrics for the object matching the name passed in")
   182  				metricPoint := tc.reportedMetricPoints[0]
   183  				timestamp := offsetTimestampBy(metricPoint.timestamp)
   184  
   185  				metrics := &cmapi.MetricValueList{
   186  					Items: []cmapi.MetricValue{
   187  						{
   188  							DescribedObject: v1.ObjectReference{
   189  								Kind:       tc.singleObject.Kind,
   190  								APIVersion: tc.singleObject.APIVersion,
   191  								Name:       tc.singleObject.Name,
   192  							},
   193  							Timestamp: metav1.Time{Time: timestamp},
   194  							Metric: cmapi.MetricIdentifier{
   195  								Name: tc.metricName,
   196  							},
   197  							Value: *resource.NewMilliQuantity(int64(metricPoint.level), resource.DecimalSI),
   198  						},
   199  					},
   200  				}
   201  
   202  				return true, metrics, nil
   203  			}
   204  		})
   205  	}
   206  
   207  	return fakeMetricsClient, fakeCMClient, fakeEMClient
   208  }
   209  
   210  func (tc *restClientTestCase) verifyResults(t *testing.T, metrics PodMetricsInfo, timestamp time.Time, err error) {
   211  	if tc.desiredError != nil {
   212  		assert.Error(t, err, "there should be an error retrieving the metrics")
   213  		assert.Contains(t, fmt.Sprintf("%v", err), fmt.Sprintf("%v", tc.desiredError), "the error message should be as expected")
   214  		return
   215  	}
   216  	assert.NoError(t, err, "there should be no error retrieving the metrics")
   217  	assert.NotNil(t, metrics, "there should be metrics returned")
   218  
   219  	if len(metrics) != len(tc.desiredMetricValues) {
   220  		t.Errorf("Not equal:\nexpected: %v\nactual: %v", tc.desiredMetricValues, metrics)
   221  	} else {
   222  		for k, m := range metrics {
   223  			if !m.Timestamp.Equal(tc.desiredMetricValues[k].Timestamp) ||
   224  				m.Window != tc.desiredMetricValues[k].Window ||
   225  				m.Value != tc.desiredMetricValues[k].Value {
   226  				t.Errorf("Not equal:\nexpected: %v\nactual: %v", tc.desiredMetricValues, metrics)
   227  				break
   228  			}
   229  		}
   230  	}
   231  
   232  	targetTimestamp := offsetTimestampBy(tc.targetTimestamp)
   233  	assert.True(t, targetTimestamp.Equal(timestamp), fmt.Sprintf("the timestamp should be as expected (%s) but was %s", targetTimestamp, timestamp))
   234  }
   235  
   236  func (tc *restClientTestCase) runTest(t *testing.T) {
   237  	var err error
   238  	testMetricsClient, testCMClient, testEMClient := tc.prepareTestClient(t)
   239  	metricsClient := NewRESTMetricsClient(testMetricsClient.MetricsV1beta1(), testCMClient, testEMClient)
   240  	isResource := len(tc.resourceName) > 0
   241  	isExternal := tc.metricSelector != nil
   242  	if isResource {
   243  		info, timestamp, err := metricsClient.GetResourceMetric(context.TODO(), v1.ResourceName(tc.resourceName), tc.namespace, tc.selector, tc.container)
   244  		tc.verifyResults(t, info, timestamp, err)
   245  	} else if isExternal {
   246  		tc.metricLabelSelector, err = metav1.LabelSelectorAsSelector(tc.metricSelector)
   247  		if err != nil {
   248  			t.Errorf("invalid metric selector: %+v", tc.metricSelector)
   249  		}
   250  		val, timestamp, err := metricsClient.GetExternalMetric(tc.metricName, tc.namespace, tc.metricLabelSelector)
   251  		info := make(PodMetricsInfo, len(val))
   252  		for i, metricVal := range val {
   253  			info[fmt.Sprintf("%v-val-%v", tc.metricName, i)] = PodMetric{Value: metricVal}
   254  		}
   255  		tc.verifyResults(t, info, timestamp, err)
   256  	} else if tc.singleObject == nil {
   257  		info, timestamp, err := metricsClient.GetRawMetric(tc.metricName, tc.namespace, tc.selector, tc.metricLabelSelector)
   258  		tc.verifyResults(t, info, timestamp, err)
   259  	} else {
   260  		val, timestamp, err := metricsClient.GetObjectMetric(tc.metricName, tc.namespace, tc.singleObject, tc.metricLabelSelector)
   261  		info := PodMetricsInfo{tc.singleObject.Name: {Value: val}}
   262  		tc.verifyResults(t, info, timestamp, err)
   263  	}
   264  }
   265  
   266  func TestRESTClientPodCPU(t *testing.T) {
   267  	targetTimestamp := 1
   268  	window := 30 * time.Second
   269  	tc := restClientTestCase{
   270  		desiredMetricValues: PodMetricsInfo{
   271  			"test-pod-0": {Value: 5000, Timestamp: offsetTimestampBy(targetTimestamp), Window: window},
   272  			"test-pod-1": {Value: 5000, Timestamp: offsetTimestampBy(targetTimestamp), Window: window},
   273  			"test-pod-2": {Value: 5000, Timestamp: offsetTimestampBy(targetTimestamp), Window: window},
   274  		},
   275  		resourceName:       v1.ResourceCPU,
   276  		targetTimestamp:    targetTimestamp,
   277  		window:             window,
   278  		reportedPodMetrics: []map[string]int64{{"test": 5000}, {"test": 5000}, {"test": 5000}},
   279  	}
   280  	tc.runTest(t)
   281  }
   282  
   283  func TestRESTClientContainerCPU(t *testing.T) {
   284  	targetTimestamp := 1
   285  	window := 30 * time.Second
   286  	tc := restClientTestCase{
   287  		desiredMetricValues: PodMetricsInfo{
   288  			"test-pod-0": {Value: 5000, Timestamp: offsetTimestampBy(targetTimestamp), Window: window},
   289  			"test-pod-1": {Value: 5000, Timestamp: offsetTimestampBy(targetTimestamp), Window: window},
   290  			"test-pod-2": {Value: 5000, Timestamp: offsetTimestampBy(targetTimestamp), Window: window},
   291  		},
   292  		container:          "test-1",
   293  		resourceName:       v1.ResourceCPU,
   294  		targetTimestamp:    targetTimestamp,
   295  		window:             window,
   296  		reportedPodMetrics: []map[string]int64{{"test-1": 5000, "test-2": 500}, {"test-1": 5000, "test-2": 500}, {"test-1": 5000, "test-2": 500}},
   297  	}
   298  	tc.runTest(t)
   299  }
   300  
   301  func TestRESTClientExternal(t *testing.T) {
   302  	tc := restClientTestCase{
   303  		desiredMetricValues: PodMetricsInfo{
   304  			"external-val-0": {Value: 10000}, "external-val-1": {Value: 20000}, "external-val-2": {Value: 10000},
   305  		},
   306  		metricSelector:       &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
   307  		metricName:           "external",
   308  		targetTimestamp:      1,
   309  		reportedMetricPoints: []metricPoint{{10000, 1}, {20000, 1}, {10000, 1}},
   310  	}
   311  	tc.runTest(t)
   312  }
   313  
   314  func TestRESTClientQPS(t *testing.T) {
   315  	targetTimestamp := 1
   316  	tc := restClientTestCase{
   317  		desiredMetricValues: PodMetricsInfo{
   318  			"test-pod-0": {Value: 10000, Timestamp: offsetTimestampBy(targetTimestamp), Window: metricServerDefaultMetricWindow},
   319  			"test-pod-1": {Value: 20000, Timestamp: offsetTimestampBy(targetTimestamp), Window: metricServerDefaultMetricWindow},
   320  			"test-pod-2": {Value: 10000, Timestamp: offsetTimestampBy(targetTimestamp), Window: metricServerDefaultMetricWindow},
   321  		},
   322  		metricName:           "qps",
   323  		targetTimestamp:      targetTimestamp,
   324  		reportedMetricPoints: []metricPoint{{10000, 1}, {20000, 1}, {10000, 1}},
   325  	}
   326  	tc.runTest(t)
   327  }
   328  
   329  func TestRESTClientSingleObject(t *testing.T) {
   330  	tc := restClientTestCase{
   331  		desiredMetricValues:  PodMetricsInfo{"some-dep": {Value: 10}},
   332  		metricName:           "queue-length",
   333  		targetTimestamp:      1,
   334  		reportedMetricPoints: []metricPoint{{10, 1}},
   335  		singleObject: &autoscalingapi.CrossVersionObjectReference{
   336  			APIVersion: "apps/v1",
   337  			Kind:       "Deployment",
   338  			Name:       "some-dep",
   339  		},
   340  	}
   341  	tc.runTest(t)
   342  }
   343  
   344  func TestRESTClientQpsSumEqualZero(t *testing.T) {
   345  	targetTimestamp := 0
   346  	tc := restClientTestCase{
   347  		desiredMetricValues: PodMetricsInfo{
   348  			"test-pod-0": {Value: 0, Timestamp: offsetTimestampBy(targetTimestamp), Window: metricServerDefaultMetricWindow},
   349  			"test-pod-1": {Value: 0, Timestamp: offsetTimestampBy(targetTimestamp), Window: metricServerDefaultMetricWindow},
   350  			"test-pod-2": {Value: 0, Timestamp: offsetTimestampBy(targetTimestamp), Window: metricServerDefaultMetricWindow},
   351  		},
   352  		metricName:           "qps",
   353  		targetTimestamp:      targetTimestamp,
   354  		reportedMetricPoints: []metricPoint{{0, 0}, {0, 0}, {0, 0}},
   355  	}
   356  	tc.runTest(t)
   357  }
   358  
   359  func TestRESTClientExternalSumEqualZero(t *testing.T) {
   360  	tc := restClientTestCase{
   361  		desiredMetricValues: PodMetricsInfo{
   362  			"external-val-0": {Value: 0}, "external-val-1": {Value: 0}, "external-val-2": {Value: 0},
   363  		},
   364  		metricSelector:       &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
   365  		metricName:           "external",
   366  		targetTimestamp:      0,
   367  		reportedMetricPoints: []metricPoint{{0, 0}, {0, 0}, {0, 0}},
   368  	}
   369  	tc.runTest(t)
   370  }
   371  
   372  func TestRESTClientQpsEmptyMetrics(t *testing.T) {
   373  	tc := restClientTestCase{
   374  		metricName:           "qps",
   375  		desiredError:         fmt.Errorf("no metrics returned from custom metrics API"),
   376  		reportedMetricPoints: []metricPoint{},
   377  	}
   378  
   379  	tc.runTest(t)
   380  }
   381  
   382  func TestRESTClientExternalEmptyMetrics(t *testing.T) {
   383  	tc := restClientTestCase{
   384  		metricName:           "external",
   385  		metricSelector:       &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
   386  		desiredError:         fmt.Errorf("no metrics returned from external metrics API"),
   387  		reportedMetricPoints: []metricPoint{},
   388  	}
   389  
   390  	tc.runTest(t)
   391  }
   392  
   393  func TestRESTClientPodCPUEmptyMetrics(t *testing.T) {
   394  	tc := restClientTestCase{
   395  		resourceName:         v1.ResourceCPU,
   396  		desiredError:         fmt.Errorf("no metrics returned from resource metrics API"),
   397  		reportedMetricPoints: []metricPoint{},
   398  		reportedPodMetrics:   []map[string]int64{},
   399  	}
   400  	tc.runTest(t)
   401  }
   402  
   403  func TestRESTClientPodCPUEmptyMetricsForOnePod(t *testing.T) {
   404  	targetTimestamp := 1
   405  	window := 30 * time.Second
   406  	tc := restClientTestCase{
   407  		resourceName: v1.ResourceCPU,
   408  		desiredMetricValues: PodMetricsInfo{
   409  			"test-pod-0": {Value: 100, Timestamp: offsetTimestampBy(targetTimestamp), Window: window},
   410  			"test-pod-1": {Value: 700, Timestamp: offsetTimestampBy(targetTimestamp), Window: window},
   411  		},
   412  		targetTimestamp:    targetTimestamp,
   413  		window:             window,
   414  		reportedPodMetrics: []map[string]int64{{"test-1": 100}, {"test-1": 300, "test-2": 400}, {}},
   415  	}
   416  	tc.runTest(t)
   417  }
   418  
   419  func TestRESTClientContainerCPUEmptyMetricsForOnePod(t *testing.T) {
   420  	targetTimestamp := 1
   421  	window := 30 * time.Second
   422  	tc := restClientTestCase{
   423  		resourceName: v1.ResourceCPU,
   424  		desiredMetricValues: PodMetricsInfo{
   425  			"test-pod-0": {Value: 100, Timestamp: offsetTimestampBy(targetTimestamp), Window: window},
   426  			"test-pod-1": {Value: 300, Timestamp: offsetTimestampBy(targetTimestamp), Window: window},
   427  		},
   428  		container:          "test-1",
   429  		targetTimestamp:    targetTimestamp,
   430  		window:             window,
   431  		desiredError:       fmt.Errorf("failed to get container metrics"),
   432  		reportedPodMetrics: []map[string]int64{{"test-1": 100}, {"test-1": 300, "test-2": 400}, {}},
   433  	}
   434  	tc.runTest(t)
   435  }
   436  
   437  func offsetTimestampBy(t int) time.Time {
   438  	return fixedTimestamp.Add(time.Duration(t) * time.Minute)
   439  }
   440  

View as plain text