453 lines
13 KiB
Go
453 lines
13 KiB
Go
/*
|
|
Copyright 2024 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package metrics
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
customapi "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2"
|
|
externalapi "k8s.io/metrics/pkg/apis/external_metrics/v1beta1"
|
|
"k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
|
resourceclient "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1"
|
|
customclient "k8s.io/metrics/pkg/client/custom_metrics"
|
|
externalclient "k8s.io/metrics/pkg/client/external_metrics"
|
|
)
|
|
|
|
// Mock clients and interfaces
|
|
type mockResourceClient struct {
|
|
resourceclient.PodMetricsesGetter
|
|
}
|
|
|
|
type mockCustomClient struct {
|
|
customclient.CustomMetricsClient
|
|
}
|
|
|
|
type mockExternalClient struct {
|
|
externalclient.ExternalMetricsClient
|
|
}
|
|
|
|
type mockExternalMetricsClient struct {
|
|
externalclient.ExternalMetricsClient
|
|
metrics *externalapi.ExternalMetricValueList
|
|
err error
|
|
}
|
|
|
|
type mockExternalMetricsInterface struct {
|
|
externalclient.MetricsInterface
|
|
metrics *externalapi.ExternalMetricValueList
|
|
err error
|
|
}
|
|
|
|
type mockCustomMetricsClient struct {
|
|
customclient.CustomMetricsClient
|
|
metrics *customapi.MetricValueList
|
|
err error
|
|
}
|
|
|
|
type mockCustomMetricsInterface struct {
|
|
customclient.MetricsInterface
|
|
metrics *customapi.MetricValueList
|
|
err error
|
|
}
|
|
|
|
type mockPodMetricsGetter struct {
|
|
metrics *v1beta1.PodMetricsList
|
|
err error
|
|
}
|
|
|
|
type mockPodMetricsInterface struct {
|
|
resourceclient.PodMetricsInterface
|
|
metrics *v1beta1.PodMetricsList
|
|
err error
|
|
}
|
|
|
|
func (m *mockExternalMetricsClient) NamespacedMetrics(_ string) externalclient.MetricsInterface {
|
|
return &mockExternalMetricsInterface{metrics: m.metrics, err: m.err}
|
|
}
|
|
|
|
func (m *mockExternalMetricsInterface) List(_ string, _ labels.Selector) (*externalapi.ExternalMetricValueList, error) {
|
|
return m.metrics, m.err
|
|
}
|
|
|
|
func (m *mockCustomMetricsClient) NamespacedMetrics(_ string) customclient.MetricsInterface {
|
|
return &mockCustomMetricsInterface{metrics: m.metrics, err: m.err}
|
|
}
|
|
|
|
func (m *mockCustomMetricsInterface) GetForObjects(_ schema.GroupKind, _ labels.Selector, _ string, _ labels.Selector) (*customapi.MetricValueList, error) {
|
|
return m.metrics, m.err
|
|
}
|
|
|
|
func (m *mockCustomMetricsInterface) GetForObject(_ schema.GroupKind, _ string, _ string, _ labels.Selector) (*customapi.MetricValue, error) {
|
|
if len(m.metrics.Items) > 0 {
|
|
return &m.metrics.Items[0], m.err
|
|
}
|
|
return nil, m.err
|
|
}
|
|
|
|
func (m *mockPodMetricsGetter) PodMetricses(_ string) resourceclient.PodMetricsInterface {
|
|
return &mockPodMetricsInterface{metrics: m.metrics, err: m.err}
|
|
}
|
|
|
|
func (m *mockPodMetricsInterface) List(_ context.Context, _ metav1.ListOptions) (*v1beta1.PodMetricsList, error) {
|
|
return m.metrics, m.err
|
|
}
|
|
|
|
// Test functions
|
|
|
|
// NewRESTMetricsClient creates a new REST metrics client with the given clients.
|
|
func TestNewRESTMetricsClient(t *testing.T) {
|
|
resourceClient := &mockResourceClient{}
|
|
customClient := &mockCustomClient{}
|
|
externalClient := &mockExternalClient{}
|
|
|
|
client := NewRESTMetricsClient(resourceClient, customClient, externalClient)
|
|
|
|
if client == nil {
|
|
t.Error("Expected non-nil client, got nil")
|
|
}
|
|
}
|
|
|
|
// TestGetResourceMetric tests the GetResourceMetric function with various scenarios.
|
|
func TestGetResourceMetric(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mockMetrics *v1beta1.PodMetricsList
|
|
mockError error
|
|
container string
|
|
expectedError string
|
|
expectedResult PodMetricsInfo
|
|
}{
|
|
{
|
|
name: "Successful retrieval",
|
|
mockMetrics: &v1beta1.PodMetricsList{
|
|
Items: []v1beta1.PodMetrics{
|
|
createPodMetrics("pod1", "container1", 100),
|
|
},
|
|
},
|
|
expectedResult: PodMetricsInfo{
|
|
"pod1": {Value: 100},
|
|
},
|
|
},
|
|
{
|
|
name: "API error",
|
|
mockError: errors.New("API error"),
|
|
expectedError: "unable to fetch metrics from resource metrics API: API error",
|
|
},
|
|
{
|
|
name: "Empty metrics",
|
|
mockMetrics: &v1beta1.PodMetricsList{},
|
|
expectedError: "no metrics returned from resource metrics API",
|
|
},
|
|
{
|
|
name: "Container-specific metrics",
|
|
mockMetrics: &v1beta1.PodMetricsList{
|
|
Items: []v1beta1.PodMetrics{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"},
|
|
Containers: []v1beta1.ContainerMetrics{
|
|
createPodMetrics("pod1", "container1", 100).Containers[0],
|
|
createPodMetrics("pod1", "container2", 200).Containers[0],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
container: "container2",
|
|
expectedResult: PodMetricsInfo{
|
|
"pod1": {Value: 200},
|
|
},
|
|
},
|
|
{
|
|
name: "Container not found",
|
|
mockMetrics: &v1beta1.PodMetricsList{
|
|
Items: []v1beta1.PodMetrics{
|
|
createPodMetrics("pod1", "container1", 100),
|
|
},
|
|
},
|
|
container: "nonexistent",
|
|
expectedError: "failed to get container metrics: container nonexistent not present in metrics for pod default/pod1",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client := setupMockResourceClient(tt.mockMetrics, tt.mockError)
|
|
result, _, err := client.GetResourceMetric(context.Background(), corev1.ResourceCPU, "default", labels.Everything(), tt.container)
|
|
|
|
assertError(t, err, tt.expectedError)
|
|
assertPodMetricsInfoEqual(t, result, tt.expectedResult)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetExternalMetric tests the retrieval of external metrics.
|
|
func TestGetExternalMetric(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mockMetrics *externalapi.ExternalMetricValueList
|
|
mockError error
|
|
expectedValues []int64
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "Successful retrieval",
|
|
mockMetrics: &externalapi.ExternalMetricValueList{
|
|
Items: []externalapi.ExternalMetricValue{
|
|
{Value: *resource.NewQuantity(100, resource.DecimalSI)},
|
|
{Value: *resource.NewQuantity(200, resource.DecimalSI)},
|
|
},
|
|
},
|
|
expectedValues: []int64{100000, 200000},
|
|
},
|
|
{
|
|
name: "API error",
|
|
mockError: errors.New("API error"),
|
|
expectedError: "unable to fetch metrics from external metrics API: API error",
|
|
},
|
|
{
|
|
name: "Empty metrics",
|
|
mockMetrics: &externalapi.ExternalMetricValueList{},
|
|
expectedError: "no metrics returned from external metrics API",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client := setupMockExternalClient(tt.mockMetrics, tt.mockError)
|
|
values, _, err := client.GetExternalMetric("test-metric", "default", labels.Everything())
|
|
|
|
assertError(t, err, tt.expectedError)
|
|
assertInt64SliceEqual(t, values, tt.expectedValues)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetRawMetric tests the retrieval of raw custom metrics.
|
|
func TestGetRawMetric(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mockMetrics *customapi.MetricValueList
|
|
mockError error
|
|
expectedResult PodMetricsInfo
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "Successful retrieval",
|
|
mockMetrics: &customapi.MetricValueList{
|
|
Items: []customapi.MetricValue{
|
|
{
|
|
DescribedObject: corev1.ObjectReference{
|
|
Kind: "Pod",
|
|
Name: "pod1",
|
|
APIVersion: "v1",
|
|
},
|
|
Metric: customapi.MetricIdentifier{
|
|
Name: "test-metric",
|
|
},
|
|
Timestamp: metav1.Time{Time: time.Now()},
|
|
Value: *resource.NewQuantity(100, resource.DecimalSI),
|
|
},
|
|
},
|
|
},
|
|
expectedResult: PodMetricsInfo{
|
|
"pod1": {Value: 100000},
|
|
},
|
|
},
|
|
{
|
|
name: "API error",
|
|
mockError: errors.New("API error"),
|
|
expectedError: "unable to fetch metrics from custom metrics API: API error",
|
|
},
|
|
{
|
|
name: "Empty metrics",
|
|
mockMetrics: &customapi.MetricValueList{},
|
|
expectedError: "no metrics returned from custom metrics API",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client := setupMockCustomClient(tt.mockMetrics, tt.mockError)
|
|
result, _, err := client.GetRawMetric("test-metric", "default", labels.Everything(), labels.Everything())
|
|
|
|
assertError(t, err, tt.expectedError)
|
|
assertPodMetricsInfoEqual(t, result, tt.expectedResult)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetObjectMetric tests the retrieval of object-specific custom metrics.
|
|
func TestGetObjectMetric(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mockMetrics *customapi.MetricValueList
|
|
mockError error
|
|
objectRef *autoscalingv2.CrossVersionObjectReference
|
|
expectedValue int64
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "Successful retrieval",
|
|
mockMetrics: &customapi.MetricValueList{
|
|
Items: []customapi.MetricValue{
|
|
{
|
|
DescribedObject: corev1.ObjectReference{
|
|
Kind: "Deployment",
|
|
Name: "test-deployment",
|
|
APIVersion: "apps/v1",
|
|
},
|
|
Metric: customapi.MetricIdentifier{
|
|
Name: "test-metric",
|
|
},
|
|
Timestamp: metav1.Time{Time: time.Now()},
|
|
Value: *resource.NewQuantity(100, resource.DecimalSI),
|
|
},
|
|
},
|
|
},
|
|
objectRef: &autoscalingv2.CrossVersionObjectReference{
|
|
Kind: "Deployment",
|
|
Name: "test-deployment",
|
|
APIVersion: "apps/v1",
|
|
},
|
|
expectedValue: 100000,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client := setupMockCustomClient(tt.mockMetrics, tt.mockError)
|
|
value, _, err := client.GetObjectMetric("test-metric", "default", tt.objectRef, labels.Everything())
|
|
|
|
assertError(t, err, tt.expectedError)
|
|
assertInt64Equal(t, value, tt.expectedValue)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// createPodMetrics creates a PodMetrics object with specified name, container name, and CPU value
|
|
func createPodMetrics(name string, containerName string, cpuValue int64) v1beta1.PodMetrics {
|
|
return v1beta1.PodMetrics{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default"},
|
|
Timestamp: metav1.Time{Time: time.Now()},
|
|
Window: metav1.Duration{Duration: time.Minute},
|
|
Containers: []v1beta1.ContainerMetrics{
|
|
{
|
|
Name: containerName,
|
|
Usage: corev1.ResourceList{
|
|
corev1.ResourceCPU: *resource.NewMilliQuantity(cpuValue, resource.DecimalSI),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// setupMockResourceClient creates a mock resource metrics client for testing
|
|
func setupMockResourceClient(mockMetrics *v1beta1.PodMetricsList, mockError error) *resourceMetricsClient {
|
|
mockClient := &mockResourceClient{}
|
|
mockClient.PodMetricsesGetter = &mockPodMetricsGetter{
|
|
metrics: mockMetrics,
|
|
err: mockError,
|
|
}
|
|
return &resourceMetricsClient{client: mockClient}
|
|
}
|
|
|
|
// setupMockExternalClient creates a mock external metrics client for testing
|
|
func setupMockExternalClient(mockMetrics *externalapi.ExternalMetricValueList, mockError error) *externalMetricsClient {
|
|
mockClient := &mockExternalMetricsClient{
|
|
metrics: mockMetrics,
|
|
err: mockError,
|
|
}
|
|
return &externalMetricsClient{client: mockClient}
|
|
}
|
|
|
|
// setupMockCustomClient creates a mock custom metrics client for testing
|
|
func setupMockCustomClient(mockMetrics *customapi.MetricValueList, mockError error) *customMetricsClient {
|
|
mockClient := &mockCustomMetricsClient{
|
|
metrics: mockMetrics,
|
|
err: mockError,
|
|
}
|
|
return &customMetricsClient{client: mockClient}
|
|
}
|
|
|
|
// assertError checks if the error matches the expected error string
|
|
func assertError(t *testing.T, got error, want string) {
|
|
if want == "" {
|
|
if got != nil {
|
|
t.Errorf("Unexpected error: %v", got)
|
|
}
|
|
} else if got == nil || got.Error() != want {
|
|
t.Errorf("Expected error '%s', got '%v'", want, got)
|
|
}
|
|
}
|
|
|
|
// assertPodMetricsInfoEqual compares two PodMetricsInfo objects for equality
|
|
func assertPodMetricsInfoEqual(t *testing.T, got, want PodMetricsInfo) {
|
|
if !podMetricsInfoEqual(got, want) {
|
|
t.Errorf("Expected result %v, got %v", want, got)
|
|
}
|
|
}
|
|
|
|
// assertInt64SliceEqual compares two int64 slices for equality
|
|
func assertInt64SliceEqual(t *testing.T, got, want []int64) {
|
|
if !int64SliceEqual(got, want) {
|
|
t.Errorf("Expected values %v, got %v", want, got)
|
|
}
|
|
}
|
|
|
|
// assertInt64Equal compares two int64 values for equality
|
|
func assertInt64Equal(t *testing.T, got, want int64) {
|
|
if got != want {
|
|
t.Errorf("Expected value %d, got %d", want, got)
|
|
}
|
|
}
|
|
|
|
// int64SliceEqual checks if two int64 slices are equal
|
|
func int64SliceEqual(a, b []int64) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i, v := range a {
|
|
if v != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// podMetricsInfoEqual checks if two PodMetricsInfo objects are equal
|
|
func podMetricsInfoEqual(a, b PodMetricsInfo) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for k, v := range a {
|
|
if bv, ok := b[k]; !ok || v.Value != bv.Value {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|