diff --git a/pkg/canary/deployment_controller_test.go b/pkg/canary/deployment_controller_test.go index 0191b140..c12a3379 100644 --- a/pkg/canary/deployment_controller_test.go +++ b/pkg/canary/deployment_controller_test.go @@ -50,10 +50,6 @@ func TestDeploymentController_Sync_ConsistentNaming(t *testing.T) { annotation := depPrimary.Annotations["kustomize.toolkit.fluxcd.io/checksum"] assert.Equal(t, "", annotation) - - hpaPrimary, err := mocks.kubeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{}) - require.NoError(t, err) - assert.Equal(t, depPrimary.Name, hpaPrimary.Spec.ScaleTargetRef.Name) } func TestDeploymentController_Sync_InconsistentNaming(t *testing.T) { @@ -71,10 +67,6 @@ func TestDeploymentController_Sync_InconsistentNaming(t *testing.T) { primarySelectorValue := depPrimary.Spec.Selector.MatchLabels[dc.label] assert.Equal(t, primarySelectorValue, fmt.Sprintf("%s-primary", dc.labelValue)) - - hpaPrimary, err := mocks.kubeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{}) - require.NoError(t, err) - assert.Equal(t, depPrimary.Name, hpaPrimary.Spec.ScaleTargetRef.Name) } func TestDeploymentController_Promote(t *testing.T) { @@ -90,15 +82,6 @@ func TestDeploymentController_Promote(t *testing.T) { _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Update(context.TODO(), config2, metav1.UpdateOptions{}) require.NoError(t, err) - hpa, err := mocks.kubeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo", metav1.GetOptions{}) - require.NoError(t, err) - - hpaClone := hpa.DeepCopy() - hpaClone.Spec.MaxReplicas = 2 - - _, err = mocks.kubeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Update(context.TODO(), hpaClone, metav1.UpdateOptions{}) - require.NoError(t, err) - err = mocks.controller.Promote(mocks.canary) require.NoError(t, err) @@ -121,18 +104,6 @@ func TestDeploymentController_Promote(t *testing.T) { require.NoError(t, err) assert.Equal(t, config2.Data["color"], configPrimary.Data["color"]) - hpaPrimary, err := mocks.kubeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{}) - require.NoError(t, err) - assert.Equal(t, int32(2), hpaPrimary.Spec.MaxReplicas) - - hpaPrimaryLabels := hpaPrimary.ObjectMeta.Labels - hpaSourceLabels := hpa.ObjectMeta.Labels - assert.Equal(t, hpaSourceLabels["app.kubernetes.io/test-label-1"], hpaPrimaryLabels["app.kubernetes.io/test-label-1"]) - - hpaPrimaryAnnotations := hpaPrimary.ObjectMeta.Annotations - hpaSourceAnnotations := hpa.ObjectMeta.Annotations - assert.Equal(t, hpaSourceAnnotations["app.kubernetes.io/test-annotation-1"], hpaPrimaryAnnotations["app.kubernetes.io/test-annotation-1"]) - value := depPrimary.Spec.Template.Spec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].PodAffinityTerm.LabelSelector.MatchExpressions[0].Values[0] assert.Equal(t, "podinfo-primary", value) diff --git a/pkg/canary/deployment_fixture_test.go b/pkg/canary/deployment_fixture_test.go index cf08695f..c14bc5b8 100644 --- a/pkg/canary/deployment_fixture_test.go +++ b/pkg/canary/deployment_fixture_test.go @@ -24,7 +24,6 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" appsv1 "k8s.io/api/apps/v1" - hpav2 "k8s.io/api/autoscaling/v2beta2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -91,7 +90,6 @@ func newCustomizableFixture(dc deploymentConfigs) (deploymentControllerFixture, // init kube clientset and register mock objects kubeClient := fake.NewSimpleClientset( newDeploymentControllerTest(dc), - newDeploymentControllerTestHPA(), newDeploymentControllerTestConfigMap(), newDeploymentControllerTestConfigMapEnv(), newDeploymentControllerTestConfigMapVol(), @@ -1000,33 +998,3 @@ func newDeploymentControllerTestV2() *appsv1.Deployment { return d } - -func newDeploymentControllerTestHPA() *hpav2.HorizontalPodAutoscaler { - h := &hpav2.HorizontalPodAutoscaler{ - TypeMeta: metav1.TypeMeta{APIVersion: hpav2.SchemeGroupVersion.String()}, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "podinfo", - }, - Spec: hpav2.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: hpav2.CrossVersionObjectReference{ - Name: "podinfo", - APIVersion: "apps/v1", - Kind: "Deployment", - }, - Metrics: []hpav2.MetricSpec{ - { - Type: "Resource", - Resource: &hpav2.ResourceMetricSource{ - Name: "cpu", - Target: hpav2.MetricTarget{ - AverageUtilization: int32p(99), - }, - }, - }, - }, - }, - } - - return h -} diff --git a/pkg/canary/hpa_reconciler_test.go b/pkg/canary/hpa_reconciler_test.go new file mode 100644 index 00000000..a72ff9ca --- /dev/null +++ b/pkg/canary/hpa_reconciler_test.go @@ -0,0 +1,106 @@ +package canary + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + hpav2 "k8s.io/api/autoscaling/v2" + hpav2beta2 "k8s.io/api/autoscaling/v2beta2" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_reconcilePrimaryHpa(t *testing.T) { + mocks := newScalerReconcilerFixture(scalerConfig{ + targetName: "podinfo", + scaler: "HorizontalPodAutoscaler", + // avoid creating a v2 HPA. + excludeObjs: []string{"HPAV2"}, + }) + hpaReconciler := mocks.scalerReconciler.(*HPAReconciler) + + err := hpaReconciler.reconcilePrimaryHpa(mocks.canary, true) + require.NoError(t, err) + + // assert that we fallback to v2beta2, when HPAv2 fails. + _, err = mocks.kubeClient.AutoscalingV2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{}) + assert.True(t, errors.IsNotFound(err)) + + hpa, err := mocks.kubeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, hpa) + + mocks = newScalerReconcilerFixture(scalerConfig{ + targetName: "podinfo", + scaler: "HorizontalPodAutoscaler", + // avoid creating _any_ HPAs. + excludeObjs: []string{"HPAV2", "HPAV2Beta2"}, + }) + hpaReconciler = mocks.scalerReconciler.(*HPAReconciler) + // assert that we return an error if no HPAs are found. + err = hpaReconciler.reconcilePrimaryHpa(mocks.canary, true) + require.Error(t, err) +} + +func Test_reconcilePrimaryHpaV2(t *testing.T) { + mocks := newScalerReconcilerFixture(scalerConfig{ + targetName: "podinfo", + scaler: "HorizontalPodAutoscaler", + }) + hpaReconciler := mocks.scalerReconciler.(*HPAReconciler) + + hpa, err := mocks.kubeClient.AutoscalingV2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo", metav1.GetOptions{}) + require.NoError(t, err) + + err = hpaReconciler.reconcilePrimaryHpaV2(mocks.canary, hpa, true) + require.NoError(t, err) + + primaryHPA, err := mocks.kubeClient.AutoscalingV2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, primaryHPA.Spec.ScaleTargetRef.Name, "podinfo-primary") + assert.Equal(t, int(*primaryHPA.Spec.Metrics[0].Resource.Target.AverageUtilization), 99) + + hpa.Spec.Metrics[0].Resource.Target = hpav2.MetricTarget{AverageUtilization: int32p(50)} + _, err = mocks.kubeClient.AutoscalingV2().HorizontalPodAutoscalers("default").Update(context.TODO(), hpa, metav1.UpdateOptions{}) + require.NoError(t, err) + + err = hpaReconciler.reconcilePrimaryHpaV2(mocks.canary, hpa, false) + require.NoError(t, err) + + primaryHPA, err = mocks.kubeClient.AutoscalingV2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, int(*primaryHPA.Spec.Metrics[0].Resource.Target.AverageUtilization), 50) +} + +func Test_reconcilePrimaryHpaV2Beta2(t *testing.T) { + mocks := newScalerReconcilerFixture(scalerConfig{ + targetName: "podinfo", + scaler: "HorizontalPodAutoscaler", + }) + + hpaReconciler := mocks.scalerReconciler.(*HPAReconciler) + + hpa, err := mocks.kubeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo", metav1.GetOptions{}) + require.NoError(t, err) + + err = hpaReconciler.reconcilePrimaryHpaV2Beta2(mocks.canary, hpa, true) + require.NoError(t, err) + + primaryHPA, err := mocks.kubeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, primaryHPA.Spec.ScaleTargetRef.Name, "podinfo-primary") + assert.Equal(t, int(*primaryHPA.Spec.Metrics[0].Resource.Target.AverageUtilization), 99) + + hpa.Spec.Metrics[0].Resource.Target = hpav2beta2.MetricTarget{AverageUtilization: int32p(50)} + _, err = mocks.kubeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Update(context.TODO(), hpa, metav1.UpdateOptions{}) + require.NoError(t, err) + + err = hpaReconciler.reconcilePrimaryHpaV2Beta2(mocks.canary, hpa, false) + require.NoError(t, err) + + primaryHPA, err = mocks.kubeClient.AutoscalingV2beta2().HorizontalPodAutoscalers("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, int(*primaryHPA.Spec.Metrics[0].Resource.Target.AverageUtilization), 50) +} diff --git a/pkg/canary/scaler_reconciler_fixture_test.go b/pkg/canary/scaler_reconciler_fixture_test.go new file mode 100644 index 00000000..8f5fc0ed --- /dev/null +++ b/pkg/canary/scaler_reconciler_fixture_test.go @@ -0,0 +1,136 @@ +package canary + +import ( + "go.uber.org/zap" + hpav2 "k8s.io/api/autoscaling/v2" + hpav2beta2 "k8s.io/api/autoscaling/v2beta2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + + flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1" + clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned" + fakeFlagger "github.com/fluxcd/flagger/pkg/client/clientset/versioned/fake" + "github.com/fluxcd/flagger/pkg/logger" +) + +type scalerReconcilerFixture struct { + canary *flaggerv1.Canary + kubeClient kubernetes.Interface + flaggerClient clientset.Interface + scalerReconciler ScalerReconciler + logger *zap.SugaredLogger +} + +type scalerConfig struct { + targetName string + excludeObjs []string + scaler string +} + +func newScalerReconcilerFixture(cfg scalerConfig) scalerReconcilerFixture { + canary := newDeploymentControllerTestCanary(canaryConfigs{targetName: cfg.targetName}) + flaggerClient := fakeFlagger.NewSimpleClientset(canary) + + kubeClient := fake.NewSimpleClientset( + newScalerReconcilerTestHPAV2(), + newScalerReconcilerTestHPAV2Beta2(), + ) + for _, obj := range cfg.excludeObjs { + if obj == "HPAV2" { + kubeClient.Tracker().Delete(schema.GroupVersionResource{ + Group: "autoscaling", + Version: "v2", + Resource: "horizontalpodautoscalers", + }, "default", "podinfo") + } + if obj == "HPAV2Beta2" { + kubeClient.Tracker().Delete(schema.GroupVersionResource{ + Group: "autoscaling", + Version: "v2beta2", + Resource: "horizontalpodautoscalers", + }, "default", "podinfo") + } + } + + logger, _ := logger.NewLogger("debug") + var hpaReconciler HPAReconciler + + if cfg.scaler == "HorizontalPodAutoscaler" { + hpaReconciler = HPAReconciler{ + kubeClient: kubeClient, + flaggerClient: flaggerClient, + logger: logger, + includeLabelPrefix: []string{"app.kubernetes.io"}, + } + } + + return scalerReconcilerFixture{ + canary: canary, + kubeClient: kubeClient, + flaggerClient: flaggerClient, + scalerReconciler: &hpaReconciler, + logger: logger, + } +} + +func newScalerReconcilerTestHPAV2Beta2() *hpav2beta2.HorizontalPodAutoscaler { + h := &hpav2beta2.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{APIVersion: hpav2beta2.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo", + }, + Spec: hpav2beta2.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpav2beta2.CrossVersionObjectReference{ + Name: "podinfo", + APIVersion: "apps/v1", + Kind: "Deployment", + }, + Metrics: []hpav2beta2.MetricSpec{ + { + Type: "Resource", + Resource: &hpav2beta2.ResourceMetricSource{ + Name: "cpu", + Target: hpav2beta2.MetricTarget{ + AverageUtilization: int32p(99), + }, + }, + }, + }, + }, + } + + return h +} + +func newScalerReconcilerTestHPAV2() *hpav2.HorizontalPodAutoscaler { + h := &hpav2.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{APIVersion: hpav2.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo", + }, + Spec: hpav2.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpav2.CrossVersionObjectReference{ + Name: "podinfo", + APIVersion: "apps/v1", + Kind: "Deployment", + }, + Metrics: []hpav2.MetricSpec{ + { + Type: "Resource", + Resource: &hpav2.ResourceMetricSource{ + Name: "cpu", + Target: hpav2.MetricTarget{ + AverageUtilization: int32p(99), + }, + }, + }, + }, + }, + } + + return h +}