karmada/pkg/controllers/federatedhpa/metrics/client_test.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
}