diff --git a/.vscode/launch.json b/.vscode/launch.json index d39f06fd..63fdd4e0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,10 +14,10 @@ "RADIX_CONTAINER_REGISTRY":"radixdev.azurecr.io", "PIPELINE_IMG_TAG": "master-latest", "TEKTON_IMG_TAG": "main-latest", - "K8S_API_HOST": "https://weekly-14-clusters-16ede4-97pzjkre.hcp.northeurope.azmk8s.io", + "K8S_API_HOST": "https://weekly-24-clusters-dev-16ede4-uk527vqt.hcp.northeurope.azmk8s.io:443", "RADIX_CLUSTER_TYPE": "development", "RADIX_DNS_ZONE": "dev.radix.equinor.com", - "RADIX_CLUSTERNAME": "weekly-14", + "RADIX_CLUSTERNAME": "weekly-24", "RADIX_ACTIVE_CLUSTER_EGRESS_IPS": "104.45.84.1", "REQUIRE_APP_CONFIGURATION_ITEM": "true", "REQUIRE_APP_AD_GROUPS": "true", diff --git a/api/applications/get_applications_handler.go b/api/applications/get_applications_handler.go index 2b5eb56e..04afd6ce 100644 --- a/api/applications/get_applications_handler.go +++ b/api/applications/get_applications_handler.go @@ -133,7 +133,7 @@ func getComponentsForActiveDeploymentsInEnvironments(ctx context.Context, deploy envName := env.Name g.Go(func() error { - componentModels, err := deploy.GetComponentsForDeployment(ctx, appName, deployment) + componentModels, err := deploy.GetComponentsForDeployment(ctx, appName, deployment.Name, deployment.Environment) if err == nil { chanData <- &ChannelData{key: envName, components: componentModels} } diff --git a/api/deployments/component_controller_test.go b/api/deployments/component_controller_test.go index b101540d..2b5845e1 100644 --- a/api/deployments/component_controller_test.go +++ b/api/deployments/component_controller_test.go @@ -13,14 +13,18 @@ import ( "github.com/equinor/radix-api/api/utils/labelselector" radixhttp "github.com/equinor/radix-common/net/http" radixutils "github.com/equinor/radix-common/utils" + "github.com/equinor/radix-common/utils/pointers" + "github.com/equinor/radix-common/utils/slice" "github.com/equinor/radix-operator/pkg/apis/kube" v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" operatorUtils "github.com/equinor/radix-operator/pkg/apis/utils" "github.com/equinor/radix-operator/pkg/apis/utils/numbers" + "github.com/kedacore/keda/v2/apis/keda/v1alpha1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v2 "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/client-go/kubernetes" ) @@ -78,11 +82,11 @@ func TestGetComponents_active_deployment(t *testing.T) { WithDeploymentName(anyDeployName)) require.NoError(t, err) - err = createComponentPod(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app") + err = createComponentPod(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app") require.NoError(t, err) - err = createComponentPod(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app") + err = createComponentPod(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app") require.NoError(t, err) - err = createComponentPod(kubeclient, "pod3", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "job") + err = createComponentPod(kubeclient, "pod3", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "job") require.NoError(t, err) endpoint := createGetComponentsEndpoint(anyAppName, anyDeployName) @@ -98,9 +102,9 @@ func TestGetComponents_active_deployment(t *testing.T) { assert.Equal(t, 2, len(components)) app := getComponentByName("app", components) - assert.Equal(t, 2, len(app.Replicas)) + assert.Equal(t, 2, len(app.Replicas)) // nolint:staticcheck // SA1019: Ignore linting deprecated fields job := getComponentByName("job", components) - assert.Equal(t, 1, len(job.Replicas)) + assert.Equal(t, 1, len(job.Replicas)) // nolint:staticcheck // SA1019: Ignore linting deprecated fields } func TestGetComponents_WithVolumeMount_ContainsVolumeMountSecrets(t *testing.T) { @@ -300,9 +304,9 @@ func TestGetComponents_inactive_deployment(t *testing.T) { WithActiveFrom(activeDeploymentCreated)) require.NoError(t, err) - err = createComponentPod(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app") + err = createComponentPod(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app") require.NoError(t, err) - err = createComponentPod(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "job") + err = createComponentPod(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "job") require.NoError(t, err) endpoint := createGetComponentsEndpoint(anyAppName, "initial-deployment") @@ -318,23 +322,24 @@ func TestGetComponents_inactive_deployment(t *testing.T) { assert.Equal(t, 2, len(components)) app := getComponentByName("app", components) - assert.Equal(t, 0, len(app.Replicas)) + assert.Equal(t, 0, len(app.Replicas)) // nolint:staticcheck // SA1019: Ignore linting deprecated fields job := getComponentByName("job", components) - assert.Equal(t, 0, len(job.Replicas)) + assert.Equal(t, 0, len(job.Replicas)) // nolint:staticcheck // SA1019: Ignore linting deprecated fields } -func createComponentPod(kubeclient kubernetes.Interface, podName, namespace, radixComponentLabel string) error { - podSpec := getPodSpec(podName, radixComponentLabel) +func createComponentPod(kubeclient kubernetes.Interface, podName, namespace, radixAppLabel, radixComponentLabel string) error { + podSpec := getPodSpec(podName, radixAppLabel, radixComponentLabel) _, err := kubeclient.CoreV1().Pods(namespace).Create(context.Background(), podSpec, metav1.CreateOptions{}) return err } -func getPodSpec(podName, radixComponentLabel string) *corev1.Pod { +func getPodSpec(podName, radixAppLabel, radixComponentLabel string) *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, Labels: map[string]string{ kube.RadixComponentLabel: radixComponentLabel, + kube.RadixAppLabel: radixAppLabel, }, }, } @@ -384,12 +389,12 @@ func TestGetComponents_ReplicaStatus_Failing(t *testing.T) { require.NoError(t, err) message1 := "Couldn't find key TEST_SECRET in Secret radix-demo-hello-nodejs-dev/www" - err = createComponentPodWithContainerState(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app", message1, deploymentModels.Failing, true) + err = createComponentPodWithContainerState(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app", message1, deploymentModels.Failing, true) require.NoError(t, err) - err = createComponentPodWithContainerState(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app", message1, deploymentModels.Failing, true) + err = createComponentPodWithContainerState(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app", message1, deploymentModels.Failing, true) require.NoError(t, err) message2 := "Couldn't find key TEST_SECRET in Secret radix-demo-hello-nodejs-dev/job" - err = createComponentPodWithContainerState(kubeclient, "pod3", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "job", message2, deploymentModels.Failing, true) + err = createComponentPodWithContainerState(kubeclient, "pod3", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "job", message2, deploymentModels.Failing, true) require.NoError(t, err) endpoint := createGetComponentsEndpoint(anyAppName, anyDeployName) @@ -405,12 +410,12 @@ func TestGetComponents_ReplicaStatus_Failing(t *testing.T) { assert.Equal(t, 2, len(components)) app := getComponentByName("app", components) - assert.Equal(t, 2, len(app.ReplicaList)) + require.Equal(t, 2, len(app.ReplicaList)) assert.Equal(t, deploymentModels.Failing.String(), app.ReplicaList[0].Status.Status) assert.Equal(t, message1, app.ReplicaList[0].StatusMessage) job := getComponentByName("job", components) - assert.Equal(t, 1, len(job.ReplicaList)) + require.Equal(t, 1, len(job.ReplicaList)) assert.Equal(t, deploymentModels.Failing.String(), job.ReplicaList[0].Status.Status) assert.Equal(t, message2, job.ReplicaList[0].StatusMessage) } @@ -432,11 +437,11 @@ func TestGetComponents_ReplicaStatus_Running(t *testing.T) { require.NoError(t, err) message := "" - err = createComponentPodWithContainerState(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app", message, deploymentModels.Running, true) + err = createComponentPodWithContainerState(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app", message, deploymentModels.Running, true) require.NoError(t, err) - err = createComponentPodWithContainerState(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app", message, deploymentModels.Running, true) + err = createComponentPodWithContainerState(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app", message, deploymentModels.Running, true) require.NoError(t, err) - err = createComponentPodWithContainerState(kubeclient, "pod3", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "job", message, deploymentModels.Running, true) + err = createComponentPodWithContainerState(kubeclient, "pod3", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "job", message, deploymentModels.Running, true) require.NoError(t, err) endpoint := createGetComponentsEndpoint(anyAppName, anyDeployName) @@ -479,11 +484,11 @@ func TestGetComponents_ReplicaStatus_Starting(t *testing.T) { require.NoError(t, err) message := "" - err = createComponentPodWithContainerState(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app", message, deploymentModels.Running, false) + err = createComponentPodWithContainerState(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app", message, deploymentModels.Running, false) require.NoError(t, err) - err = createComponentPodWithContainerState(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app", message, deploymentModels.Running, false) + err = createComponentPodWithContainerState(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app", message, deploymentModels.Running, false) require.NoError(t, err) - err = createComponentPodWithContainerState(kubeclient, "pod3", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "job", message, deploymentModels.Running, false) + err = createComponentPodWithContainerState(kubeclient, "pod3", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "job", message, deploymentModels.Running, false) require.NoError(t, err) endpoint := createGetComponentsEndpoint(anyAppName, anyDeployName) @@ -526,11 +531,11 @@ func TestGetComponents_ReplicaStatus_Pending(t *testing.T) { require.NoError(t, err) message := "" - err = createComponentPodWithContainerState(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app", message, deploymentModels.Pending, true) + err = createComponentPodWithContainerState(kubeclient, "pod1", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app", message, deploymentModels.Pending, true) require.NoError(t, err) - err = createComponentPodWithContainerState(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "app", message, deploymentModels.Pending, true) + err = createComponentPodWithContainerState(kubeclient, "pod2", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "app", message, deploymentModels.Pending, true) require.NoError(t, err) - err = createComponentPodWithContainerState(kubeclient, "pod3", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), "job", message, deploymentModels.Pending, true) + err = createComponentPodWithContainerState(kubeclient, "pod3", operatorUtils.GetEnvironmentNamespace(anyAppName, "dev"), anyAppName, "job", message, deploymentModels.Pending, true) require.NoError(t, err) endpoint := createGetComponentsEndpoint(anyAppName, anyDeployName) @@ -560,17 +565,21 @@ func TestGetComponents_WithHorizontalScaling(t *testing.T) { // Setup testScenarios := []struct { - name string - deploymentName string - minReplicas int32 - maxReplicas int32 - targetCpu *int32 - targetMemory *int32 + name string + deploymentName string + minReplicas int32 + maxReplicas int32 + targetCpu *int32 + targetMemory *int32 + targetCron *int32 + targetAzureServiceBus *int32 }{ - {"targetCpu and targetMemory are nil", "dep1", 2, 6, nil, nil}, - {"targetCpu is nil, targetMemory is non-nil", "dep2", 2, 6, nil, numbers.Int32Ptr(75)}, - {"targetCpu is non-nil, targetMemory is nil", "dep3", 2, 6, numbers.Int32Ptr(60), nil}, - {"targetCpu and targetMemory are non-nil", "dep4", 2, 6, numbers.Int32Ptr(62), numbers.Int32Ptr(79)}, + {"targetCpu and targetMemory are nil", "dep1", 2, 6, nil, nil, nil, nil}, + {"targetCpu is nil, targetMemory is non-nil", "dep2", 2, 6, nil, pointers.Ptr[int32](75), nil, nil}, + {"targetCpu is non-nil, targetMemory is nil", "dep3", 2, 6, pointers.Ptr[int32](60), nil, nil, nil}, + {"targetCpu and targetMemory are non-nil", "dep4", 2, 6, pointers.Ptr[int32](62), pointers.Ptr[int32](79), nil, nil}, + {"Test CRON trigger is found", "dep5", 2, 6, nil, nil, pointers.Ptr[int32](5), nil}, + {"Test Azure trigger is found", "dep6", 2, 6, nil, nil, nil, pointers.Ptr[int32](15)}, } for _, scenario := range testScenarios { @@ -589,8 +598,10 @@ func TestGetComponents_WithHorizontalScaling(t *testing.T) { require.NoError(t, err) ns := operatorUtils.GetEnvironmentNamespace(anyAppName, "prod") - autoscaler := createAutoscaler("frontend", numbers.Int32Ptr(scenario.minReplicas), scenario.maxReplicas, scenario.targetCpu, scenario.targetMemory) - _, err = client.AutoscalingV2().HorizontalPodAutoscalers(ns).Create(context.Background(), &autoscaler, metav1.CreateOptions{}) + scaler, hpa := createHorizontalScalingObjects("frontend", numbers.Int32Ptr(scenario.minReplicas), scenario.maxReplicas, scenario.targetCpu, scenario.targetMemory, scenario.targetCron, scenario.targetAzureServiceBus) + _, err = kedaClient.KedaV1alpha1().ScaledObjects(ns).Create(context.Background(), &scaler, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = client.AutoscalingV2().HorizontalPodAutoscalers(ns).Create(context.Background(), &hpa, metav1.CreateOptions{}) require.NoError(t, err) // Test @@ -607,18 +618,85 @@ func TestGetComponents_WithHorizontalScaling(t *testing.T) { assert.Equal(t, scenario.minReplicas, components[0].HorizontalScalingSummary.MinReplicas) assert.Equal(t, scenario.maxReplicas, components[0].HorizontalScalingSummary.MaxReplicas) - assert.True(t, nil == components[0].HorizontalScalingSummary.CurrentCPUUtilizationPercentage) // using assert.Equal() fails because simple nil and *int32 typed nil do not pass equality test - assert.Equal(t, scenario.targetCpu, components[0].HorizontalScalingSummary.TargetCPUUtilizationPercentage) - assert.True(t, nil == components[0].HorizontalScalingSummary.CurrentMemoryUtilizationPercentage) - assert.Equal(t, scenario.targetMemory, components[0].HorizontalScalingSummary.TargetMemoryUtilizationPercentage) + assert.Nil(t, components[0].HorizontalScalingSummary.CurrentCPUUtilizationPercentage) // nolint:staticcheck // SA1019: Ignore linting deprecated fields + assert.Equal(t, scenario.targetCpu, components[0].HorizontalScalingSummary.TargetCPUUtilizationPercentage) // nolint:staticcheck // SA1019: Ignore linting deprecated fields + assert.Nil(t, components[0].HorizontalScalingSummary.CurrentMemoryUtilizationPercentage) // nolint:staticcheck // SA1019: Ignore linting deprecated fields + assert.Equal(t, scenario.targetMemory, components[0].HorizontalScalingSummary.TargetMemoryUtilizationPercentage) // nolint:staticcheck // SA1019: Ignore linting deprecated fields + + memoryTrigger, ok := slice.FindFirst(components[0].HorizontalScalingSummary.Triggers, func(s deploymentModels.HorizontalScalingSummaryTriggerStatus) bool { + return s.Name == "memory" + }) + if scenario.targetMemory == nil { + assert.False(t, ok) + } else { + require.True(t, ok) + assert.Equal(t, fmt.Sprintf("%d", *scenario.targetMemory), memoryTrigger.TargetUtilization) + assert.Empty(t, memoryTrigger.CurrentUtilization) + assert.Empty(t, memoryTrigger.Error) + assert.Equal(t, "memory", memoryTrigger.Type) + } + + cpuTrigger, ok := slice.FindFirst(components[0].HorizontalScalingSummary.Triggers, func(s deploymentModels.HorizontalScalingSummaryTriggerStatus) bool { + return s.Name == "cpu" + }) + if scenario.targetCpu == nil { + assert.False(t, ok) + } else { + require.True(t, ok) + assert.Equal(t, fmt.Sprintf("%d", *scenario.targetCpu), cpuTrigger.TargetUtilization) + assert.Empty(t, cpuTrigger.CurrentUtilization) + assert.Empty(t, cpuTrigger.Error) + assert.Equal(t, "cpu", cpuTrigger.Type) + } + + cronTrigger, ok := slice.FindFirst(components[0].HorizontalScalingSummary.Triggers, func(s deploymentModels.HorizontalScalingSummaryTriggerStatus) bool { + return s.Name == "cron" + }) + if scenario.targetCron == nil { + assert.False(t, ok) + } else { + require.True(t, ok) + assert.Equal(t, fmt.Sprintf("%d", *scenario.targetCron), cronTrigger.TargetUtilization) + assert.Equal(t, fmt.Sprintf("%d", *scenario.targetCron), cronTrigger.CurrentUtilization) + assert.Empty(t, cronTrigger.Error) + assert.Equal(t, "cron", cronTrigger.Type) + } + + azureTrigger, ok := slice.FindFirst(components[0].HorizontalScalingSummary.Triggers, func(s deploymentModels.HorizontalScalingSummaryTriggerStatus) bool { + return s.Name == "azure-servicebus" + }) + if scenario.targetAzureServiceBus == nil { + assert.False(t, ok) + } else { + require.True(t, ok) + assert.Equal(t, fmt.Sprintf("%d", *scenario.targetAzureServiceBus), azureTrigger.TargetUtilization) + assert.Equal(t, fmt.Sprintf("%d", *scenario.targetAzureServiceBus), azureTrigger.CurrentUtilization) + assert.Empty(t, azureTrigger.Error) + assert.Equal(t, "azure-servicebus", azureTrigger.Type) + } }) } } -func createAutoscaler(name string, minReplicas *int32, maxReplicas int32, targetCpu *int32, targetMemory *int32) v2.HorizontalPodAutoscaler { +func createHorizontalScalingObjects(name string, minReplicas *int32, maxReplicas int32, targetCpu *int32, targetMemory *int32, targetCron *int32, targetAzureServiceBus *int32) (v1alpha1.ScaledObject, v2.HorizontalPodAutoscaler) { + var triggers []v1alpha1.ScaleTriggers var metrics []v2.MetricSpec + resourceMetricNames := []string{} + externalMetricNames := []string{} + health := map[string]v1alpha1.HealthStatus{} + metricStatus := []v2.MetricStatus{} if targetCpu != nil { + resourceMetricNames = append(resourceMetricNames, "cpu") + triggers = append(triggers, v1alpha1.ScaleTriggers{ + Type: "cpu", + Name: "cpu", + Metadata: map[string]string{ + "value": fmt.Sprintf("%d", *targetCpu), + }, + AuthenticationRef: nil, + MetricType: "Utilization", + }) metrics = append(metrics, v2.MetricSpec{ Resource: &v2.ResourceMetricSource{ Name: "cpu", @@ -631,6 +709,15 @@ func createAutoscaler(name string, minReplicas *int32, maxReplicas int32, target } if targetMemory != nil { + resourceMetricNames = append(resourceMetricNames, "memory") + triggers = append(triggers, v1alpha1.ScaleTriggers{ + Type: "memory", + Name: "memory", + Metadata: map[string]string{ + "value": fmt.Sprintf("%d", *targetMemory), + }, + MetricType: "Utilization", + }) metrics = append(metrics, v2.MetricSpec{ Resource: &v2.ResourceMetricSource{ Name: "memory", @@ -642,18 +729,97 @@ func createAutoscaler(name string, minReplicas *int32, maxReplicas int32, target }) } - autoscaler := v2.HorizontalPodAutoscaler{ + if targetCron != nil { + externalMetricName := fmt.Sprintf("s%d-cron-Europe-Oslo-08xx1-5-016xx1-5", len(triggers)) + externalMetricNames = append(externalMetricNames, externalMetricName) + triggers = append(triggers, v1alpha1.ScaleTriggers{ + Type: "cron", + Name: "cron", + Metadata: map[string]string{ + "end": "0 16 * * 1-5", + "start": "0 8 * * 1-5", + "timezone": "Europe/Oslo", + "desiredReplicas": fmt.Sprintf("%d", *targetCron), + }, + }) + health[externalMetricName] = v1alpha1.HealthStatus{ + NumberOfFailures: pointers.Ptr[int32](0), + Status: "Happy", + } + metricStatus = append(metricStatus, v2.MetricStatus{ + Type: "External", + External: &v2.ExternalMetricStatus{ + Current: v2.MetricValueStatus{ + AverageValue: resource.NewQuantity(int64(*targetCron), resource.DecimalSI), + }, + Metric: v2.MetricIdentifier{ + Name: externalMetricName, + }, + }, + }) + } + + if targetAzureServiceBus != nil { + externalMetricName := fmt.Sprintf("s%d-azure-servicebus-orders", len(triggers)) + externalMetricNames = append(externalMetricNames, externalMetricName) + triggers = append(triggers, v1alpha1.ScaleTriggers{ + Type: "azure-servicebus", + Name: "azure-servicebus", + Metadata: map[string]string{ + "messageCount": fmt.Sprintf("%d", *targetAzureServiceBus), + }, + }) + health[externalMetricName] = v1alpha1.HealthStatus{ + NumberOfFailures: pointers.Ptr[int32](0), + Status: "Happy", + } + metricStatus = append(metricStatus, v2.MetricStatus{ + Type: "External", + External: &v2.ExternalMetricStatus{ + Current: v2.MetricValueStatus{ + AverageValue: resource.NewQuantity(int64(*targetAzureServiceBus), resource.DecimalSI), + }, + Metric: v2.MetricIdentifier{ + Name: externalMetricName, + }, + }, + }) + } + + scaler := v1alpha1.ScaledObject{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labelselector.ForComponent(anyAppName, "frontend"), }, + Spec: v1alpha1.ScaledObjectSpec{ + MinReplicaCount: minReplicas, + MaxReplicaCount: &maxReplicas, + Triggers: triggers, + }, + Status: v1alpha1.ScaledObjectStatus{ + HpaName: fmt.Sprintf("hpa-%s", name), + Health: health, + ResourceMetricNames: resourceMetricNames, + ExternalMetricNames: externalMetricNames, + }, + } + + hpa := v2.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("hpa-%s", name), + Labels: labelselector.ForComponent(anyAppName, "frontend"), + }, Spec: v2.HorizontalPodAutoscalerSpec{ MinReplicas: minReplicas, MaxReplicas: maxReplicas, Metrics: metrics, }, + Status: v2.HorizontalPodAutoscalerStatus{ + CurrentMetrics: metricStatus, + }, } - return autoscaler + + return scaler, hpa } func TestGetComponents_WithIdentity(t *testing.T) { @@ -702,8 +868,8 @@ func TestGetComponents_WithIdentity(t *testing.T) { assert.Nil(t, getComponentByName("comp2", components).Identity) } -func createComponentPodWithContainerState(kubeclient kubernetes.Interface, podName, namespace, radixComponentLabel, message string, status deploymentModels.ContainerStatus, ready bool) error { - podSpec := getPodSpec(podName, radixComponentLabel) +func createComponentPodWithContainerState(kubeclient kubernetes.Interface, podName, namespace, radixAppLabel, radixComponentLabel, message string, status deploymentModels.ContainerStatus, ready bool) error { + podSpec := getPodSpec(podName, radixAppLabel, radixComponentLabel) containerState := getContainerState(message, status) podStatus := corev1.PodStatus{ ContainerStatuses: []corev1.ContainerStatus{ diff --git a/api/deployments/component_handler.go b/api/deployments/component_handler.go index 3c5a0594..0e1d36c3 100644 --- a/api/deployments/component_handler.go +++ b/api/deployments/component_handler.go @@ -2,435 +2,78 @@ package deployments import ( "context" - "fmt" "strings" deploymentModels "github.com/equinor/radix-api/api/deployments/models" "github.com/equinor/radix-api/api/kubequery" - "github.com/equinor/radix-api/api/utils" - "github.com/equinor/radix-api/api/utils/event" - "github.com/equinor/radix-api/api/utils/horizontalscaling" - "github.com/equinor/radix-api/api/utils/labelselector" - radixutils "github.com/equinor/radix-common/utils" - "github.com/equinor/radix-common/utils/slice" - "github.com/equinor/radix-operator/pkg/apis/defaults" - "github.com/equinor/radix-operator/pkg/apis/deployment" + "github.com/equinor/radix-api/api/models" "github.com/equinor/radix-operator/pkg/apis/kube" - v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" - crdUtils "github.com/equinor/radix-operator/pkg/apis/utils" - v2 "k8s.io/api/autoscaling/v2" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" - "k8s.io/client-go/kubernetes" ) // GetComponentsForDeployment Gets a list of components for a given deployment -func (deploy *deployHandler) GetComponentsForDeployment(ctx context.Context, appName string, deployment *deploymentModels.DeploymentSummary) ([]*deploymentModels.Component, error) { - envNs := crdUtils.GetEnvironmentNamespace(appName, deployment.Environment) - rd, err := deploy.accounts.UserAccount.RadixClient.RadixV1().RadixDeployments(envNs).Get(ctx, deployment.Name, metav1.GetOptions{}) +func (deploy *deployHandler) GetComponentsForDeployment(ctx context.Context, appName, deploymentName, envName string) ([]*deploymentModels.Component, error) { + rd, err := kubequery.GetRadixDeploymentByName(ctx, deploy.accounts.UserAccount.RadixClient, appName, envName, deploymentName) if err != nil { return nil, err } - - ra, _ := deploy.accounts.UserAccount.RadixClient.RadixV1().RadixApplications(crdUtils.GetAppNamespace(appName)).Get(ctx, appName, metav1.GetOptions{}) - var components []*deploymentModels.Component - - for _, component := range rd.Spec.Components { - componentModel, err := deploy.getComponent(ctx, &component, ra, rd, deployment) - if err != nil { - return nil, err - } - components = append(components, componentModel) - } - - for _, component := range rd.Spec.Jobs { - componentModel, err := deploy.getComponent(ctx, &component, ra, rd, deployment) - if err != nil { - return nil, err - } - components = append(components, componentModel) - } - - return components, nil -} - -// GetComponentsForDeploymentName handler for GetDeployments -func (deploy *deployHandler) GetComponentsForDeploymentName(ctx context.Context, appName, deploymentName string) ([]*deploymentModels.Component, error) { - deployments, err := deploy.GetDeploymentsForApplication(ctx, appName) + ra, err := kubequery.GetRadixApplication(ctx, deploy.accounts.UserAccount.RadixClient, appName) if err != nil { return nil, err } - - for _, depl := range deployments { - if strings.EqualFold(depl.Name, deploymentName) { - return deploy.GetComponentsForDeployment(ctx, appName, depl) - } - } - - return nil, deploymentModels.NonExistingDeployment(nil, deploymentName) -} - -func (deploy *deployHandler) getComponent(ctx context.Context, component v1.RadixCommonDeployComponent, ra *v1.RadixApplication, rd *v1.RadixDeployment, deployment *deploymentModels.DeploymentSummary) (*deploymentModels.Component, error) { - envNs := crdUtils.GetEnvironmentNamespace(ra.Name, deployment.Environment) - - // TODO: Add interface for RA + EnvConfig - environmentConfig := utils.GetComponentEnvironmentConfig(ra, deployment.Environment, component.GetName()) - - deploymentComponent, err := GetComponentStateFromSpec(ctx, deploy.accounts.UserAccount.Client, ra.Name, deployment, rd.Status, environmentConfig, component) + deploymentList, err := kubequery.GetDeploymentsForEnvironment(ctx, deploy.accounts.UserAccount.Client, appName, envName) if err != nil { return nil, err } - - if component.GetType() == v1.RadixComponentTypeComponent { - hpaSummary, err := deploy.getHpaSummary(ctx, component, ra.Name, envNs) - if err != nil { - return nil, err - } - deploymentComponent.HorizontalScalingSummary = hpaSummary - } - return deploymentComponent, nil -} - -func (deploy *deployHandler) getHpaSummary(ctx context.Context, component v1.RadixCommonDeployComponent, appName, envNs string) (*deploymentModels.HorizontalScalingSummary, error) { - selector := labelselector.ForComponent(appName, component.GetName()).String() - hpas, err := deploy.accounts.UserAccount.Client.AutoscalingV2().HorizontalPodAutoscalers(envNs).List(ctx, metav1.ListOptions{LabelSelector: selector}) + podList, err := kubequery.GetPodsForEnvironmentComponents(ctx, deploy.accounts.UserAccount.Client, appName, envName) if err != nil { return nil, err } - if len(hpas.Items) == 0 { - return nil, nil - } - if len(hpas.Items) > 1 { - return nil, fmt.Errorf("found more than 1 HPA for component %s", component.GetName()) - } - hpa := &hpas.Items[0] - - minReplicas := int32(1) - if hpa.Spec.MinReplicas != nil { - minReplicas = *hpa.Spec.MinReplicas - } - maxReplicas := hpa.Spec.MaxReplicas - - currentCpuUtil, targetCpuUtil := getHpaMetrics(hpa, corev1.ResourceCPU) - currentMemoryUtil, targetMemoryUtil := getHpaMetrics(hpa, corev1.ResourceMemory) - - hpaSummary := deploymentModels.HorizontalScalingSummary{ - MinReplicas: minReplicas, - MaxReplicas: maxReplicas, - CurrentCPUUtilizationPercentage: currentCpuUtil, - TargetCPUUtilizationPercentage: targetCpuUtil, - CurrentMemoryUtilizationPercentage: currentMemoryUtil, - TargetMemoryUtilizationPercentage: targetMemoryUtil, - } - return &hpaSummary, nil -} - -func getHpaMetrics(hpa *v2.HorizontalPodAutoscaler, resourceName corev1.ResourceName) (*int32, *int32) { - currentResourceUtil := getHpaCurrentMetric(hpa, resourceName) - // find resource utilization target - var targetResourceUtil *int32 - targetResourceMetric := horizontalscaling.GetHpaMetric(hpa, resourceName) - if targetResourceMetric != nil { - targetResourceUtil = targetResourceMetric.Resource.Target.AverageUtilization - } - return currentResourceUtil, targetResourceUtil -} - -func getHpaCurrentMetric(hpa *v2.HorizontalPodAutoscaler, resourceName corev1.ResourceName) *int32 { - for _, metric := range hpa.Status.CurrentMetrics { - if metric.Resource != nil && metric.Resource.Name == resourceName { - return metric.Resource.Current.AverageUtilization - } - } - return nil -} - -// GetComponentStateFromSpec Returns a component with the current state -func GetComponentStateFromSpec( - ctx context.Context, - kubeClient kubernetes.Interface, - appName string, - deployment *deploymentModels.DeploymentSummary, - deploymentStatus v1.RadixDeployStatus, - environmentConfig v1.RadixCommonEnvironmentConfig, - component v1.RadixCommonDeployComponent) (*deploymentModels.Component, error) { - - var componentPodNames []string - var environmentVariables map[string]string - var replicaSummaryList []deploymentModels.ReplicaSummary - var auxResource deploymentModels.AuxiliaryResource - - envNs := crdUtils.GetEnvironmentNamespace(appName, deployment.Environment) - status := deploymentModels.ConsistentComponent - - if deployment.ActiveTo == "" { - // current active deployment - we get existing pods - componentPods, err := getComponentPodsByNamespace(ctx, kubeClient, envNs, component.GetName()) - if err != nil { - return nil, err - } - componentPodNames = getPodNames(componentPods) - environmentVariables = getRadixEnvironmentVariables(componentPods) - eventList, err := kubequery.GetEventsForEnvironment(ctx, kubeClient, appName, deployment.Environment) - if err != nil { - return nil, err - } - lastEventWarnings := event.ConvertToEventWarnings(eventList) - replicaSummaryList = getReplicaSummaryList(componentPods, lastEventWarnings) - auxResource, err = getAuxiliaryResources(ctx, kubeClient, appName, component, envNs) - if err != nil { - return nil, err - } - - status, err = getStatusOfActiveDeployment(component, - deploymentStatus, environmentConfig, componentPods) - if err != nil { - return nil, err - } - } - - componentBuilder := deploymentModels.NewComponentBuilder() - if jobComponent, ok := component.(*v1.RadixDeployJobComponent); ok { - componentBuilder.WithSchedulerPort(jobComponent.SchedulerPort) - if jobComponent.Payload != nil { - componentBuilder.WithScheduledJobPayloadPath(jobComponent.Payload.Path) - } - componentBuilder.WithNotifications(jobComponent.Notifications) - } - - return componentBuilder. - WithComponent(component). - WithStatus(status). - WithPodNames(componentPodNames). - WithReplicaSummaryList(replicaSummaryList). - WithRadixEnvironmentVariables(environmentVariables). - WithAuxiliaryResource(auxResource). - BuildComponent() -} - -func getPodNames(pods []corev1.Pod) []string { - var names []string - for _, pod := range pods { - names = append(names, pod.GetName()) - } - return names -} - -func getComponentPodsByNamespace(ctx context.Context, client kubernetes.Interface, envNs, componentName string) ([]corev1.Pod, error) { - var componentPods []corev1.Pod - pods, err := client.CoreV1().Pods(envNs).List(ctx, metav1.ListOptions{ - LabelSelector: getLabelSelectorForComponentPods(componentName).String(), - }) + hpas, err := kubequery.GetHorizontalPodAutoscalersForEnvironment(ctx, deploy.accounts.UserAccount.Client, appName, envName) if err != nil { return nil, err } - - for _, pod := range pods.Items { - pod := pod - - // A previous version of the job-scheduler added the "radix-job-type" label to job pods. - // For backward compatibility, we need to ignore these pods in the list of pods returned for a component - if _, isScheduledJobPod := pod.GetLabels()[kube.RadixJobTypeLabel]; isScheduledJobPod { - continue - } - - // Ignore pods related to jobs created from RadixBatch - if _, isRadixBatchJobPod := pod.GetLabels()[kube.RadixBatchNameLabel]; isRadixBatchJobPod { - continue - } - - componentPods = append(componentPods, pod) - } - - return componentPods, nil -} - -func getLabelSelectorForComponentPods(componentName string) labels.Selector { - componentNameRequirement, _ := labels.NewRequirement(kube.RadixComponentLabel, selection.Equals, []string{componentName}) - notJobAuxRequirement, _ := labels.NewRequirement(kube.RadixPodIsJobAuxObjectLabel, selection.DoesNotExist, []string{}) - return labels.NewSelector().Add(*componentNameRequirement, *notJobAuxRequirement) -} - -func runningReplicaDiffersFromConfig(environmentConfig v1.RadixCommonEnvironmentConfig, actualPods []corev1.Pod) bool { - actualPodsLength := len(actualPods) - if radixutils.IsNil(environmentConfig) { - return actualPodsLength != deployment.DefaultReplicas - } - // No HPA config - if environmentConfig.GetHorizontalScaling() == nil { - if environmentConfig.GetReplicas() != nil { - return actualPodsLength != *environmentConfig.GetReplicas() - } - return actualPodsLength != deployment.DefaultReplicas - } - // With HPA config - if environmentConfig.GetReplicas() != nil && *environmentConfig.GetReplicas() == 0 { - return actualPodsLength != *environmentConfig.GetReplicas() - } - if environmentConfig.GetHorizontalScaling().MinReplicas != nil { - return actualPodsLength < int(*environmentConfig.GetHorizontalScaling().MinReplicas) || - actualPodsLength > int(environmentConfig.GetHorizontalScaling().MaxReplicas) - } - return actualPodsLength < deployment.DefaultReplicas || - actualPodsLength > int(environmentConfig.GetHorizontalScaling().MaxReplicas) -} - -func runningReplicaDiffersFromSpec(component v1.RadixCommonDeployComponent, actualPods []corev1.Pod) bool { - actualPodsLength := len(actualPods) - // No HPA config - if component.GetHorizontalScaling() == nil { - if component.GetReplicas() != nil { - return actualPodsLength != *component.GetReplicas() - } - return actualPodsLength != deployment.DefaultReplicas - } - // With HPA config - if component.GetReplicas() != nil && *component.GetReplicas() == 0 { - return actualPodsLength != *component.GetReplicas() - } - if component.GetHorizontalScaling().MinReplicas != nil { - return actualPodsLength < int(*component.GetHorizontalScaling().MinReplicas) || - actualPodsLength > int(component.GetHorizontalScaling().MaxReplicas) - } - return actualPodsLength < deployment.DefaultReplicas || - actualPodsLength > int(component.GetHorizontalScaling().MaxReplicas) -} - -func getRadixEnvironmentVariables(pods []corev1.Pod) map[string]string { - radixEnvironmentVariables := make(map[string]string) - - for _, pod := range pods { - for _, container := range pod.Spec.Containers { - for _, envVariable := range container.Env { - if crdUtils.IsRadixEnvVar(envVariable.Name) { - radixEnvironmentVariables[envVariable.Name] = envVariable.Value - } - } - } - } - - return radixEnvironmentVariables -} - -func getReplicaSummaryList(pods []corev1.Pod, lastEventWarnings event.LastEventWarnings) []deploymentModels.ReplicaSummary { - return slice.Map(pods, func(pod corev1.Pod) deploymentModels.ReplicaSummary { - return deploymentModels.GetReplicaSummary(pod, lastEventWarnings[pod.GetName()]) - }) -} - -func getAuxiliaryResources(ctx context.Context, kubeClient kubernetes.Interface, appName string, component v1.RadixCommonDeployComponent, envNamespace string) (auxResource deploymentModels.AuxiliaryResource, err error) { - if auth := component.GetAuthentication(); component.IsPublic() && auth != nil && auth.OAuth2 != nil { - auxResource.OAuth2, err = getOAuth2AuxiliaryResource(ctx, kubeClient, appName, component.GetName(), envNamespace) - if err != nil { - return - } + scaledObjects, err := kubequery.GetScaledObjectsForEnvironment(ctx, deploy.accounts.UserAccount.KedaClient, appName, envName) + if err != nil { + return nil, err } - - return -} - -func getOAuth2AuxiliaryResource(ctx context.Context, kubeClient kubernetes.Interface, appName, componentName, envNamespace string) (*deploymentModels.OAuth2AuxiliaryResource, error) { - var oauth2Resource deploymentModels.OAuth2AuxiliaryResource - oauthDeployment, err := getAuxiliaryResourceDeployment(ctx, kubeClient, appName, componentName, envNamespace, defaults.OAuthProxyAuxiliaryComponentType) + noJobPayloadReq, err := labels.NewRequirement(kube.RadixSecretTypeLabel, selection.NotEquals, []string{string(kube.RadixSecretJobPayload)}) if err != nil { return nil, err } - if oauthDeployment != nil { - oauth2Resource.Deployment = *oauthDeployment + secretList, err := kubequery.GetSecretsForEnvironment(ctx, deploy.accounts.UserAccount.Client, appName, envName, *noJobPayloadReq) + if err != nil { + return nil, err } - - return &oauth2Resource, nil -} - -func getAuxiliaryResourceDeployment(ctx context.Context, kubeClient kubernetes.Interface, appName, componentName, envNamespace, auxType string) (*deploymentModels.AuxiliaryResourceDeployment, error) { - var auxResourceDeployment deploymentModels.AuxiliaryResourceDeployment - - selector := labelselector.ForAuxiliaryResource(appName, componentName, auxType).String() - deployments, err := kubeClient.AppsV1().Deployments(envNamespace).List(ctx, metav1.ListOptions{LabelSelector: selector}) + eventList, err := kubequery.GetEventsForEnvironment(ctx, deploy.accounts.UserAccount.Client, appName, envName) if err != nil { return nil, err } - if len(deployments.Items) == 0 { - auxResourceDeployment.Status = deploymentModels.ComponentReconciling.String() - return &auxResourceDeployment, nil + certs, err := kubequery.GetCertificatesForEnvironment(ctx, deploy.accounts.ServiceAccount.CertManagerClient, appName, envName) + if err != nil { + return nil, err } - deployment := deployments.Items[0] - - pods, err := kubeClient.CoreV1().Pods(envNamespace).List(ctx, metav1.ListOptions{LabelSelector: selector}) + certRequests, err := kubequery.GetCertificateRequestsForEnvironment(ctx, deploy.accounts.ServiceAccount.CertManagerClient, appName, envName) if err != nil { return nil, err } - auxResourceDeployment.ReplicaList = getReplicaSummaryList(pods.Items, nil) - auxResourceDeployment.Status = deploymentModels.ComponentStatusFromDeployment(&deployment).String() - return &auxResourceDeployment, nil -} -func runningReplicaIsOutdated(component v1.RadixCommonDeployComponent, actualPods []corev1.Pod) bool { - switch component.GetType() { - case v1.RadixComponentTypeComponent: - return runningComponentReplicaIsOutdated(component, actualPods) - case v1.RadixComponentTypeJob: - return false - default: - return false - } + return models.BuildComponents(ra, rd, deploymentList, podList, hpas, secretList, eventList, certs, certRequests, nil, scaledObjects), nil } -func runningComponentReplicaIsOutdated(component v1.RadixCommonDeployComponent, actualPods []corev1.Pod) bool { - // Check if running component's image is not the same as active deployment image tag and that active rd image is equal to 'starting' component image tag - componentIsInconsistent := false - for _, pod := range actualPods { - if pod.DeletionTimestamp != nil { - // Pod is in termination phase - continue - } - for _, container := range pod.Spec.Containers { - if container.Image != component.GetImage() { - // Container is running an outdated image - componentIsInconsistent = true - } - } +// GetComponentsForDeploymentName handler for GetDeployments +func (deploy *deployHandler) GetComponentsForDeploymentName(ctx context.Context, appName, deploymentName string) ([]*deploymentModels.Component, error) { + deployments, err := deploy.GetDeploymentsForApplication(ctx, appName) + if err != nil { + return nil, err } - return componentIsInconsistent -} - -func getStatusOfActiveDeployment( - component v1.RadixCommonDeployComponent, - deploymentStatus v1.RadixDeployStatus, - environmentConfig v1.RadixCommonEnvironmentConfig, - pods []corev1.Pod) (deploymentModels.ComponentStatus, error) { - - if component.GetType() == v1.RadixComponentTypeComponent { - if runningReplicaDiffersFromConfig(environmentConfig, pods) && - !runningReplicaDiffersFromSpec(component, pods) && - len(pods) == 0 { - return deploymentModels.StoppedComponent, nil - } - if runningReplicaDiffersFromSpec(component, pods) { - return deploymentModels.ComponentReconciling, nil - } - } else if component.GetType() == v1.RadixComponentTypeJob { - if len(pods) == 0 { - return deploymentModels.StoppedComponent, nil + for _, depl := range deployments { + if strings.EqualFold(depl.Name, deploymentName) { + return deploy.GetComponentsForDeployment(ctx, appName, depl.Name, depl.Environment) } } - if runningReplicaIsOutdated(component, pods) { - return deploymentModels.ComponentOutdated, nil - } - restarted := component.GetEnvironmentVariables()[defaults.RadixRestartEnvironmentVariable] - if strings.EqualFold(restarted, "") { - return deploymentModels.ConsistentComponent, nil - } - restartedTime, err := radixutils.ParseTimestamp(restarted) - if err != nil { - return deploymentModels.ConsistentComponent, err - } - reconciledTime := deploymentStatus.Reconciled - if reconciledTime.IsZero() || restartedTime.After(reconciledTime.Time) { - return deploymentModels.ComponentRestarting, nil - } - return deploymentModels.ConsistentComponent, nil + + return nil, deploymentModels.NonExistingDeployment(nil, deploymentName) } diff --git a/api/deployments/deployment_handler.go b/api/deployments/deployment_handler.go index 30028d82..9d4a906d 100644 --- a/api/deployments/deployment_handler.go +++ b/api/deployments/deployment_handler.go @@ -27,7 +27,7 @@ type DeployHandler interface { GetDeploymentWithName(ctx context.Context, appName, deploymentName string) (*deploymentModels.Deployment, error) GetDeploymentsForApplicationEnvironment(ctx context.Context, appName, environment string, latest bool) ([]*deploymentModels.DeploymentSummary, error) GetComponentsForDeploymentName(ctx context.Context, appName, deploymentID string) ([]*deploymentModels.Component, error) - GetComponentsForDeployment(ctx context.Context, appName string, deployment *deploymentModels.DeploymentSummary) ([]*deploymentModels.Component, error) + GetComponentsForDeployment(ctx context.Context, appName, deploymentName, envName string) ([]*deploymentModels.Component, error) GetLatestDeploymentForApplicationEnvironment(ctx context.Context, appName, environment string) (*deploymentModels.DeploymentSummary, error) GetDeploymentsForPipelineJob(context.Context, string, string) ([]*deploymentModels.DeploymentSummary, error) GetJobComponentDeployments(context.Context, string, string, string) ([]*deploymentModels.DeploymentItem, error) @@ -171,7 +171,7 @@ func (deploy *deployHandler) GetDeploymentWithName(ctx context.Context, appName, return nil, err } - components, err := deploy.GetComponentsForDeployment(ctx, appName, deploymentSummary) + components, err := deploy.GetComponentsForDeployment(ctx, appName, deploymentName, deploymentSummary.Environment) if err != nil { return nil, err } diff --git a/api/deployments/mock/deployment_handler_mock.go b/api/deployments/mock/deployment_handler_mock.go index b4cacf14..59e36e79 100644 --- a/api/deployments/mock/deployment_handler_mock.go +++ b/api/deployments/mock/deployment_handler_mock.go @@ -38,18 +38,18 @@ func (m *MockDeployHandler) EXPECT() *MockDeployHandlerMockRecorder { } // GetComponentsForDeployment mocks base method. -func (m *MockDeployHandler) GetComponentsForDeployment(ctx context.Context, appName string, deployment *models.DeploymentSummary) ([]*models.Component, error) { +func (m *MockDeployHandler) GetComponentsForDeployment(ctx context.Context, appName, deploymentName, envName string) ([]*models.Component, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetComponentsForDeployment", ctx, appName, deployment) + ret := m.ctrl.Call(m, "GetComponentsForDeployment", ctx, appName, deploymentName, envName) ret0, _ := ret[0].([]*models.Component) ret1, _ := ret[1].(error) return ret0, ret1 } // GetComponentsForDeployment indicates an expected call of GetComponentsForDeployment. -func (mr *MockDeployHandlerMockRecorder) GetComponentsForDeployment(ctx, appName, deployment interface{}) *gomock.Call { +func (mr *MockDeployHandlerMockRecorder) GetComponentsForDeployment(ctx, appName, deploymentName, envName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetComponentsForDeployment", reflect.TypeOf((*MockDeployHandler)(nil).GetComponentsForDeployment), ctx, appName, deployment) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetComponentsForDeployment", reflect.TypeOf((*MockDeployHandler)(nil).GetComponentsForDeployment), ctx, appName, deploymentName, envName) } // GetComponentsForDeploymentName mocks base method. diff --git a/api/deployments/models/component_deployment.go b/api/deployments/models/component_deployment.go index d6649291..4d71db52 100644 --- a/api/deployments/models/component_deployment.go +++ b/api/deployments/models/component_deployment.go @@ -74,12 +74,10 @@ type Component struct { // required: false Variables map[string]string `json:"variables"` - // Array of pod names + // Deprecated: Array of pod names. Use ReplicaList instead // // required: false - // deprecated: true // example: ["server-78fc8857c4-hm76l", "server-78fc8857c4-asfa2"] - // Deprecated: Use ReplicaList instead. Replicas []string `json:"replicas"` // Array of ReplicaSummary @@ -422,31 +420,66 @@ type HorizontalScalingSummary struct { // example: 5 MaxReplicas int32 `json:"maxReplicas"` - // Component current average CPU utilization over all pods, represented as a percentage of requested CPU + // CooldownPeriod in seconds. From radixconfig.yaml + // + // required: false + // example: 300 + CooldownPeriod int32 `json:"cooldownPeriod"` + + // PollingInterval in seconds. From radixconfig.yaml + // + // required: false + // example: 30 + PollingInterval int32 `json:"pollingInterval"` + + // Triggers lists status of all triggers found in radixconfig.yaml + // + // required: false + // example: 30 + Triggers []HorizontalScalingSummaryTriggerStatus `json:"triggers"` + + // Deprecated: Component current average CPU utilization over all pods, represented as a percentage of requested CPU. Use Triggers instead. Will be removed from Radix API 2025-01-01. // // required: false // example: 70 CurrentCPUUtilizationPercentage *int32 `json:"currentCPUUtilizationPercentage"` - // Component target average CPU utilization over all pods + // Deprecated: Component target average CPU utilization over all pods. Use Triggers instead. Will be removed from Radix API 2025-01-01. // // required: false // example: 80 TargetCPUUtilizationPercentage *int32 `json:"targetCPUUtilizationPercentage"` - // Component current average memory utilization over all pods, represented as a percentage of requested memory + // Deprecated: Component current average memory utilization over all pods, represented as a percentage of requested memory. Use Triggers instead. Will be removed from Radix API 2025-01-01. // // required: false // example: 80 CurrentMemoryUtilizationPercentage *int32 `json:"currentMemoryUtilizationPercentage"` - // Component target average memory utilization over all pods + // Deprecated: Component target average memory utilization over all pods. use Triggers instead. Will be removed from Radix API 2025-01-01. // // required: false // example: 80 TargetMemoryUtilizationPercentage *int32 `json:"targetMemoryUtilizationPercentage"` } +type HorizontalScalingSummaryTriggerStatus struct { + // Name of trigger + Name string `json:"name"` + + // CurrentUtilization is the last measured utilization + CurrentUtilization string `json:"current_utilization"` + + // TargetUtilization is the average target across replicas + TargetUtilization string `json:"target_utilization"` + + // Type of trigger + Type string `json:"type"` + + // Error contains short description if trigger have problems + Error string `json:"error,omitempty"` +} + // Node Defines node attributes, where pod should be scheduled type Node struct { // Gpu Holds lists of node GPU types, with dashed types to exclude diff --git a/api/environments/component_handler.go b/api/environments/component_handler.go index 6a309007..9f89dda2 100644 --- a/api/environments/component_handler.go +++ b/api/environments/component_handler.go @@ -92,7 +92,7 @@ func (eh EnvironmentHandler) RestartComponentAuxiliaryResource(ctx context.Conte return err } - componentsDto, err := eh.deployHandler.GetComponentsForDeployment(ctx, appName, deploySummary) + componentsDto, err := eh.deployHandler.GetComponentsForDeployment(ctx, appName, deploySummary.Name, envName) if err != nil { return err } diff --git a/api/environments/component_spec.go b/api/environments/component_spec.go new file mode 100644 index 00000000..f10488e5 --- /dev/null +++ b/api/environments/component_spec.go @@ -0,0 +1,323 @@ +package environments + +import ( + "context" + "strings" + + deploymentModels "github.com/equinor/radix-api/api/deployments/models" + "github.com/equinor/radix-api/api/kubequery" + "github.com/equinor/radix-api/api/models" + "github.com/equinor/radix-api/api/utils/event" + "github.com/equinor/radix-api/api/utils/labelselector" + radixutils "github.com/equinor/radix-common/utils" + "github.com/equinor/radix-common/utils/slice" + "github.com/equinor/radix-operator/pkg/apis/defaults" + "github.com/equinor/radix-operator/pkg/apis/deployment" + "github.com/equinor/radix-operator/pkg/apis/kube" + v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + crdUtils "github.com/equinor/radix-operator/pkg/apis/utils" + "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/client-go/kubernetes" +) + +// getComponentStateFromSpec Returns a component with the current state +func getComponentStateFromSpec( + ctx context.Context, + kubeClient kubernetes.Interface, + appName string, + deployment *deploymentModels.DeploymentSummary, + deploymentStatus v1.RadixDeployStatus, + environmentConfig v1.RadixCommonEnvironmentConfig, + component v1.RadixCommonDeployComponent, + hpas []autoscalingv2.HorizontalPodAutoscaler, + scaledObjects []v1alpha1.ScaledObject, +) (*deploymentModels.Component, error) { + + var componentPodNames []string + var environmentVariables map[string]string + var replicaSummaryList []deploymentModels.ReplicaSummary + var auxResource deploymentModels.AuxiliaryResource + var horizontalScalingSummary *deploymentModels.HorizontalScalingSummary + + envNs := crdUtils.GetEnvironmentNamespace(appName, deployment.Environment) + status := deploymentModels.ConsistentComponent + + if deployment.ActiveTo == "" { + // current active deployment - we get existing pods + componentPods, err := getComponentPodsByNamespace(ctx, kubeClient, envNs, component.GetName()) + if err != nil { + return nil, err + } + componentPodNames = getPodNames(componentPods) + environmentVariables = getRadixEnvironmentVariables(componentPods) + eventList, err := kubequery.GetEventsForEnvironment(ctx, kubeClient, appName, deployment.Environment) + if err != nil { + return nil, err + } + lastEventWarnings := event.ConvertToEventWarnings(eventList) + replicaSummaryList = getReplicaSummaryList(componentPods, lastEventWarnings) + auxResource, err = getAuxiliaryResources(ctx, kubeClient, appName, component, envNs) + if err != nil { + return nil, err + } + + status, err = getStatusOfActiveDeployment(component, + deploymentStatus, environmentConfig, componentPods) + if err != nil { + return nil, err + } + } + + componentBuilder := deploymentModels.NewComponentBuilder() + if jobComponent, ok := component.(*v1.RadixDeployJobComponent); ok { + componentBuilder.WithSchedulerPort(jobComponent.SchedulerPort) + if jobComponent.Payload != nil { + componentBuilder.WithScheduledJobPayloadPath(jobComponent.Payload.Path) + } + componentBuilder.WithNotifications(jobComponent.Notifications) + } + + if component.GetType() == v1.RadixComponentTypeComponent { + horizontalScalingSummary = models.GetHpaSummary(appName, component.GetName(), hpas, scaledObjects) + } + + return componentBuilder. + WithComponent(component). + WithStatus(status). + WithPodNames(componentPodNames). + WithReplicaSummaryList(replicaSummaryList). + WithRadixEnvironmentVariables(environmentVariables). + WithAuxiliaryResource(auxResource). + WithHorizontalScalingSummary(horizontalScalingSummary). + BuildComponent() +} + +func getPodNames(pods []corev1.Pod) []string { + var names []string + for _, pod := range pods { + names = append(names, pod.GetName()) + } + return names +} + +func getComponentPodsByNamespace(ctx context.Context, client kubernetes.Interface, envNs, componentName string) ([]corev1.Pod, error) { + var componentPods []corev1.Pod + pods, err := client.CoreV1().Pods(envNs).List(ctx, metav1.ListOptions{ + LabelSelector: getLabelSelectorForComponentPods(componentName).String(), + }) + if err != nil { + return nil, err + } + + for _, pod := range pods.Items { + pod := pod + + // A previous version of the job-scheduler added the "radix-job-type" label to job pods. + // For backward compatibility, we need to ignore these pods in the list of pods returned for a component + if _, isScheduledJobPod := pod.GetLabels()[kube.RadixJobTypeLabel]; isScheduledJobPod { + continue + } + + // Ignore pods related to jobs created from RadixBatch + if _, isRadixBatchJobPod := pod.GetLabels()[kube.RadixBatchNameLabel]; isRadixBatchJobPod { + continue + } + + componentPods = append(componentPods, pod) + } + + return componentPods, nil +} + +func getLabelSelectorForComponentPods(componentName string) labels.Selector { + componentNameRequirement, _ := labels.NewRequirement(kube.RadixComponentLabel, selection.Equals, []string{componentName}) + notJobAuxRequirement, _ := labels.NewRequirement(kube.RadixPodIsJobAuxObjectLabel, selection.DoesNotExist, []string{}) + return labels.NewSelector().Add(*componentNameRequirement, *notJobAuxRequirement) +} + +func runningReplicaDiffersFromConfig(environmentConfig v1.RadixCommonEnvironmentConfig, actualPods []corev1.Pod) bool { + actualPodsLength := len(actualPods) + if radixutils.IsNil(environmentConfig) { + return actualPodsLength != deployment.DefaultReplicas + } + // No HPA config + if environmentConfig.GetHorizontalScaling() == nil { + if environmentConfig.GetReplicas() != nil { + return actualPodsLength != *environmentConfig.GetReplicas() + } + return actualPodsLength != deployment.DefaultReplicas + } + // With HPA config + if environmentConfig.GetReplicas() != nil && *environmentConfig.GetReplicas() == 0 { + return actualPodsLength != *environmentConfig.GetReplicas() + } + if environmentConfig.GetHorizontalScaling().MinReplicas != nil { + return actualPodsLength < int(*environmentConfig.GetHorizontalScaling().MinReplicas) || + actualPodsLength > int(environmentConfig.GetHorizontalScaling().MaxReplicas) + } + return actualPodsLength < deployment.DefaultReplicas || + actualPodsLength > int(environmentConfig.GetHorizontalScaling().MaxReplicas) +} + +func runningReplicaDiffersFromSpec(component v1.RadixCommonDeployComponent, actualPods []corev1.Pod) bool { + actualPodsLength := len(actualPods) + // No HPA config + if component.GetHorizontalScaling() == nil { + if component.GetReplicas() != nil { + return actualPodsLength != *component.GetReplicas() + } + return actualPodsLength != deployment.DefaultReplicas + } + // With HPA config + if component.GetReplicas() != nil && *component.GetReplicas() == 0 { + return actualPodsLength != *component.GetReplicas() + } + if component.GetHorizontalScaling().MinReplicas != nil { + return actualPodsLength < int(*component.GetHorizontalScaling().MinReplicas) || + actualPodsLength > int(component.GetHorizontalScaling().MaxReplicas) + } + return actualPodsLength < deployment.DefaultReplicas || + actualPodsLength > int(component.GetHorizontalScaling().MaxReplicas) +} + +func getRadixEnvironmentVariables(pods []corev1.Pod) map[string]string { + radixEnvironmentVariables := make(map[string]string) + + for _, pod := range pods { + for _, container := range pod.Spec.Containers { + for _, envVariable := range container.Env { + if crdUtils.IsRadixEnvVar(envVariable.Name) { + radixEnvironmentVariables[envVariable.Name] = envVariable.Value + } + } + } + } + + return radixEnvironmentVariables +} + +func getReplicaSummaryList(pods []corev1.Pod, lastEventWarnings event.LastEventWarnings) []deploymentModels.ReplicaSummary { + return slice.Map(pods, func(pod corev1.Pod) deploymentModels.ReplicaSummary { + return deploymentModels.GetReplicaSummary(pod, lastEventWarnings[pod.GetName()]) + }) +} + +func getAuxiliaryResources(ctx context.Context, kubeClient kubernetes.Interface, appName string, component v1.RadixCommonDeployComponent, envNamespace string) (auxResource deploymentModels.AuxiliaryResource, err error) { + if auth := component.GetAuthentication(); component.IsPublic() && auth != nil && auth.OAuth2 != nil { + auxResource.OAuth2, err = getOAuth2AuxiliaryResource(ctx, kubeClient, appName, component.GetName(), envNamespace) + if err != nil { + return + } + } + + return +} + +func getOAuth2AuxiliaryResource(ctx context.Context, kubeClient kubernetes.Interface, appName, componentName, envNamespace string) (*deploymentModels.OAuth2AuxiliaryResource, error) { + var oauth2Resource deploymentModels.OAuth2AuxiliaryResource + oauthDeployment, err := getAuxiliaryResourceDeployment(ctx, kubeClient, appName, componentName, envNamespace, defaults.OAuthProxyAuxiliaryComponentType) + if err != nil { + return nil, err + } + if oauthDeployment != nil { + oauth2Resource.Deployment = *oauthDeployment + } + + return &oauth2Resource, nil +} + +func getAuxiliaryResourceDeployment(ctx context.Context, kubeClient kubernetes.Interface, appName, componentName, envNamespace, auxType string) (*deploymentModels.AuxiliaryResourceDeployment, error) { + var auxResourceDeployment deploymentModels.AuxiliaryResourceDeployment + + selector := labelselector.ForAuxiliaryResource(appName, componentName, auxType).String() + deployments, err := kubeClient.AppsV1().Deployments(envNamespace).List(ctx, metav1.ListOptions{LabelSelector: selector}) + if err != nil { + return nil, err + } + if len(deployments.Items) == 0 { + auxResourceDeployment.Status = deploymentModels.ComponentReconciling.String() + return &auxResourceDeployment, nil + } + deployment := deployments.Items[0] + + pods, err := kubeClient.CoreV1().Pods(envNamespace).List(ctx, metav1.ListOptions{LabelSelector: selector}) + if err != nil { + return nil, err + } + auxResourceDeployment.ReplicaList = getReplicaSummaryList(pods.Items, nil) + auxResourceDeployment.Status = deploymentModels.ComponentStatusFromDeployment(&deployment).String() + return &auxResourceDeployment, nil +} + +func runningReplicaIsOutdated(component v1.RadixCommonDeployComponent, actualPods []corev1.Pod) bool { + switch component.GetType() { + case v1.RadixComponentTypeComponent: + return runningComponentReplicaIsOutdated(component, actualPods) + case v1.RadixComponentTypeJob: + return false + default: + return false + } +} + +func runningComponentReplicaIsOutdated(component v1.RadixCommonDeployComponent, actualPods []corev1.Pod) bool { + // Check if running component's image is not the same as active deployment image tag and that active rd image is equal to 'starting' component image tag + componentIsInconsistent := false + for _, pod := range actualPods { + if pod.DeletionTimestamp != nil { + // Pod is in termination phase + continue + } + for _, container := range pod.Spec.Containers { + if container.Image != component.GetImage() { + // Container is running an outdated image + componentIsInconsistent = true + } + } + } + + return componentIsInconsistent +} + +func getStatusOfActiveDeployment( + component v1.RadixCommonDeployComponent, + deploymentStatus v1.RadixDeployStatus, + environmentConfig v1.RadixCommonEnvironmentConfig, + pods []corev1.Pod) (deploymentModels.ComponentStatus, error) { + + if component.GetType() == v1.RadixComponentTypeComponent { + if runningReplicaDiffersFromConfig(environmentConfig, pods) && + !runningReplicaDiffersFromSpec(component, pods) && + len(pods) == 0 { + return deploymentModels.StoppedComponent, nil + } + if runningReplicaDiffersFromSpec(component, pods) { + return deploymentModels.ComponentReconciling, nil + } + } else if component.GetType() == v1.RadixComponentTypeJob { + if len(pods) == 0 { + return deploymentModels.StoppedComponent, nil + } + } + if runningReplicaIsOutdated(component, pods) { + return deploymentModels.ComponentOutdated, nil + } + restarted := component.GetEnvironmentVariables()[defaults.RadixRestartEnvironmentVariable] + if strings.EqualFold(restarted, "") { + return deploymentModels.ConsistentComponent, nil + } + restartedTime, err := radixutils.ParseTimestamp(restarted) + if err != nil { + return deploymentModels.ConsistentComponent, err + } + reconciledTime := deploymentStatus.Reconciled + if reconciledTime.IsZero() || restartedTime.After(reconciledTime.Time) { + return deploymentModels.ComponentRestarting, nil + } + return deploymentModels.ConsistentComponent, nil +} diff --git a/api/deployments/component_handler_test.go b/api/environments/component_spec_test.go similarity index 99% rename from api/deployments/component_handler_test.go rename to api/environments/component_spec_test.go index 08815675..5d8374c6 100644 --- a/api/deployments/component_handler_test.go +++ b/api/environments/component_spec_test.go @@ -1,4 +1,4 @@ -package deployments +package environments import ( "testing" @@ -40,7 +40,6 @@ func TestRunningReplicaDiffersFromConfig_NoHPA(t *testing.T) { isDifferent = runningReplicaDiffersFromConfig(raEnvironmentConfig, actualPods) assert.True(t, isDifferent) } - func TestRunningReplicaDiffersFromConfig_WithHPA(t *testing.T) { // Test replicas 0, pods 3, minReplicas 2, maxReplicas 6 replicas := 0 @@ -184,7 +183,6 @@ func TestRunningReplicaNotOutdatedImage_(t *testing.T) { isOutdated := runningReplicaIsOutdated(&rdComponent, actualPods) assert.False(t, isOutdated) } - func TestRunningReplicaNotOutdatedImage_TerminatingPod(t *testing.T) { // Test replicas 0, pods 1, minReplicas 2, maxReplicas 6 replicas := 0 diff --git a/api/environments/environment_handler.go b/api/environments/environment_handler.go index 4e5aa43e..21c5004a 100644 --- a/api/environments/environment_handler.go +++ b/api/environments/environment_handler.go @@ -46,9 +46,6 @@ func WithAccounts(accounts models.Accounts) EnvironmentHandlerOptions { eh.eventHandler = events.Init(accounts.UserAccount.Client) eh.accounts = accounts kubeUtil, _ := kube.New(accounts.UserAccount.Client, accounts.UserAccount.RadixClient, accounts.UserAccount.KedaClient, accounts.UserAccount.SecretProviderClient) - eh.kubeUtil = kubeUtil - kubeUtilsForServiceAccount, _ := kube.New(accounts.ServiceAccount.Client, accounts.ServiceAccount.RadixClient, accounts.UserAccount.KedaClient, accounts.ServiceAccount.SecretProviderClient) - eh.kubeUtilForServiceAccount = kubeUtilsForServiceAccount eh.jobSchedulerHandlerFactory = jobscheduler.NewFactory(kubeUtil) } } @@ -97,8 +94,6 @@ type EnvironmentHandler struct { deployHandler deployments.DeployHandler eventHandler events.EventHandler accounts models.Accounts - kubeUtil *kube.Kube - kubeUtilForServiceAccount *kube.Kube tlsValidator tlsvalidation.Validator jobSchedulerHandlerFactory jobscheduler.HandlerFactoryInterface } @@ -190,7 +185,10 @@ func (eh EnvironmentHandler) GetEnvironment(ctx context.Context, appName, envNam if err != nil { return nil, err } - + scaledObjects, err := kubequery.GetScaledObjectsForEnvironment(ctx, eh.accounts.UserAccount.KedaClient, appName, envName) + if err != nil { + return nil, err + } noJobPayloadReq, err := labels.NewRequirement(kube.RadixSecretTypeLabel, selection.NotEquals, []string{string(kube.RadixSecretJobPayload)}) if err != nil { return nil, err @@ -216,7 +214,7 @@ func (eh EnvironmentHandler) GetEnvironment(ctx context.Context, appName, envNam return nil, err } - env := apimodels.BuildEnvironment(rr, ra, re, rdList, rjList, deploymentList, componentPodList, hpaList, secretList, secretProviderClassList, eventList, certs, certRequests, eh.tlsValidator) + env := apimodels.BuildEnvironment(rr, ra, re, rdList, rjList, deploymentList, componentPodList, hpaList, secretList, secretProviderClassList, eventList, certs, certRequests, eh.tlsValidator, scaledObjects) return env, nil } @@ -247,8 +245,7 @@ func (eh EnvironmentHandler) CreateEnvironment(ctx context.Context, appName, env // DeleteEnvironment Handler for DeleteEnvironment. Deletes an environment if it is considered orphaned func (eh EnvironmentHandler) DeleteEnvironment(ctx context.Context, appName, envName string) error { - uniqueName := k8sObjectUtils.GetEnvironmentNamespace(appName, envName) - re, err := eh.getRadixEnvironment(ctx, uniqueName) + re, err := kubequery.GetRadixEnvironment(ctx, eh.accounts.ServiceAccount.RadixClient, appName, envName) if err != nil { return err } @@ -259,7 +256,7 @@ func (eh EnvironmentHandler) DeleteEnvironment(ctx context.Context, appName, env } // idempotent removal of RadixEnvironment - err = eh.getServiceAccount().RadixClient.RadixV1().RadixEnvironments().Delete(ctx, uniqueName, metav1.DeleteOptions{}) + err = eh.getServiceAccount().RadixClient.RadixV1().RadixEnvironments().Delete(ctx, re.Name, metav1.DeleteOptions{}) // if an error is anything other than not-found, return it if err != nil && !errors.IsNotFound(err) { return err @@ -270,7 +267,7 @@ func (eh EnvironmentHandler) DeleteEnvironment(ctx context.Context, appName, env // GetEnvironmentEvents Handler for GetEnvironmentEvents func (eh EnvironmentHandler) GetEnvironmentEvents(ctx context.Context, appName, envName string) ([]*eventModels.Event, error) { - radixApplication, err := eh.getRadixApplicationInAppNamespace(ctx, appName) + radixApplication, err := kubequery.GetRadixApplication(ctx, eh.accounts.UserAccount.RadixClient, appName) if err != nil { return nil, err } @@ -449,12 +446,21 @@ func (eh EnvironmentHandler) getRadixCommonComponentUpdater(ctx context.Context, updater = &radixDeployJobComponentUpdater{base: baseUpdater} } + hpas, err := kubequery.GetHorizontalPodAutoscalersForEnvironment(ctx, eh.accounts.UserAccount.Client, appName, envName) + if err != nil { + return nil, err + } + scalers, err := kubequery.GetScaledObjectsForEnvironment(ctx, eh.accounts.UserAccount.KedaClient, appName, envName) + if err != nil { + return nil, err + } + baseUpdater.componentIndex = componentIndex baseUpdater.componentToPatch = componentToPatch - ra, _ := eh.getRadixApplicationInAppNamespace(ctx, appName) + ra, _ := kubequery.GetRadixApplication(ctx, eh.accounts.UserAccount.RadixClient, appName) baseUpdater.environmentConfig = utils.GetComponentEnvironmentConfig(ra, envName, componentName) - baseUpdater.componentState, err = deployments.GetComponentStateFromSpec(ctx, eh.client, appName, deploymentSummary, rd.Status, baseUpdater.environmentConfig, componentToPatch) + baseUpdater.componentState, err = getComponentStateFromSpec(ctx, eh.client, appName, deploymentSummary, rd.Status, baseUpdater.environmentConfig, componentToPatch, hpas, scalers) if err != nil { return nil, err } diff --git a/api/environments/utils.go b/api/environments/utils.go index dc012c6c..9ba0d2bd 100644 --- a/api/environments/utils.go +++ b/api/environments/utils.go @@ -22,11 +22,3 @@ func (eh EnvironmentHandler) getRadixDeployment(ctx context.Context, appName, en } return deploymentSummary, radixDeployment, nil } - -func (eh EnvironmentHandler) getRadixApplicationInAppNamespace(ctx context.Context, appName string) (*v1.RadixApplication, error) { - return eh.radixclient.RadixV1().RadixApplications(operatorutils.GetAppNamespace(appName)).Get(ctx, appName, metav1.GetOptions{}) -} - -func (eh EnvironmentHandler) getRadixEnvironment(ctx context.Context, name string) (*v1.RadixEnvironment, error) { - return eh.getServiceAccount().RadixClient.RadixV1().RadixEnvironments().Get(ctx, name, metav1.GetOptions{}) -} diff --git a/api/kubequery/scaledobject_test.go b/api/kubequery/scaledobject_test.go new file mode 100644 index 00000000..7d47d6a2 --- /dev/null +++ b/api/kubequery/scaledobject_test.go @@ -0,0 +1,27 @@ +package kubequery_test + +import ( + "context" + "testing" + + "github.com/equinor/radix-api/api/kubequery" + "github.com/equinor/radix-operator/pkg/apis/utils/labels" + "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + kedafake "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned/fake" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_GetScaledObjectsForEnvironment(t *testing.T) { + matched1 := v1alpha1.ScaledObject{ObjectMeta: metav1.ObjectMeta{Name: "matched1", Namespace: "app1-env1", Labels: labels.ForApplicationName("app1")}} + matched2 := v1alpha1.ScaledObject{ObjectMeta: metav1.ObjectMeta{Name: "matched2", Namespace: "app1-env1", Labels: labels.ForApplicationName("app1")}} + unmatched1 := v1alpha1.ScaledObject{ObjectMeta: metav1.ObjectMeta{Name: "unmatched1", Namespace: "app1-env1"}} + unmatched2 := v1alpha1.ScaledObject{ObjectMeta: metav1.ObjectMeta{Name: "unmatched2", Namespace: "app1-env1", Labels: labels.ForApplicationName("app2")}} + unmatched3 := v1alpha1.ScaledObject{ObjectMeta: metav1.ObjectMeta{Name: "unmatched3", Namespace: "app1-env2", Labels: labels.ForApplicationName("app1")}} + client := kedafake.NewSimpleClientset(&matched1, &matched2, &unmatched1, &unmatched2, &unmatched3) + expected := []v1alpha1.ScaledObject{matched1, matched2} + actual, err := kubequery.GetScaledObjectsForEnvironment(context.Background(), client, "app1", "env1") + require.NoError(t, err) + assert.ElementsMatch(t, expected, actual) +} diff --git a/api/kubequery/scaledobjects.go b/api/kubequery/scaledobjects.go new file mode 100644 index 00000000..571a1270 --- /dev/null +++ b/api/kubequery/scaledobjects.go @@ -0,0 +1,21 @@ +package kubequery + +import ( + "context" + + operatorUtils "github.com/equinor/radix-operator/pkg/apis/utils" + "github.com/equinor/radix-operator/pkg/apis/utils/labels" + "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GetScaledObjectsForEnvironment returns all ScaledObjects for the specified application and environment. +func GetScaledObjectsForEnvironment(ctx context.Context, kedaClient versioned.Interface, appName, envName string) ([]v1alpha1.ScaledObject, error) { + ns := operatorUtils.GetEnvironmentNamespace(appName, envName) + scaledObjects, err := kedaClient.KedaV1alpha1().ScaledObjects(ns).List(ctx, metav1.ListOptions{LabelSelector: labels.ForApplicationName(appName).String()}) + if err != nil { + return nil, err + } + return scaledObjects.Items, nil +} diff --git a/api/models/component.go b/api/models/component.go index 696a1220..6f2ef9dd 100644 --- a/api/models/component.go +++ b/api/models/component.go @@ -20,6 +20,7 @@ import ( radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" operatorutils "github.com/equinor/radix-operator/pkg/apis/utils" radixlabels "github.com/equinor/radix-operator/pkg/apis/utils/labels" + "github.com/kedacore/keda/v2/apis/keda/v1alpha1" "github.com/rs/zerolog/log" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" @@ -33,29 +34,34 @@ const ( ) // BuildComponents builds a list of Component models. -func BuildComponents(ra *radixv1.RadixApplication, rd *radixv1.RadixDeployment, deploymentList []appsv1.Deployment, podList []corev1.Pod, - hpaList []autoscalingv2.HorizontalPodAutoscaler, secretList []corev1.Secret, eventList []corev1.Event, certs []cmv1.Certificate, certRequests []cmv1.CertificateRequest, - tlsValidator tlsvalidation.Validator) []*deploymentModels.Component { +func BuildComponents( + ra *radixv1.RadixApplication, rd *radixv1.RadixDeployment, deploymentList []appsv1.Deployment, podList []corev1.Pod, + hpaList []autoscalingv2.HorizontalPodAutoscaler, secretList []corev1.Secret, eventList []corev1.Event, certs []cmv1.Certificate, + certRequests []cmv1.CertificateRequest, tlsValidator tlsvalidation.Validator, scaledObjects []v1alpha1.ScaledObject, +) []*deploymentModels.Component { lastEventWarnings := event.ConvertToEventWarnings(eventList) var components []*deploymentModels.Component for _, component := range rd.Spec.Components { - components = append(components, buildComponent(&component, ra, rd, deploymentList, podList, hpaList, secretList, certs, certRequests, lastEventWarnings, tlsValidator)) + components = append(components, buildComponent(&component, ra, rd, deploymentList, podList, hpaList, secretList, certs, certRequests, lastEventWarnings, tlsValidator, scaledObjects)) } for _, job := range rd.Spec.Jobs { - components = append(components, buildComponent(&job, ra, rd, deploymentList, podList, hpaList, secretList, certs, certRequests, lastEventWarnings, tlsValidator)) + components = append(components, buildComponent(&job, ra, rd, deploymentList, podList, hpaList, secretList, certs, certRequests, lastEventWarnings, tlsValidator, scaledObjects)) } return components } -func buildComponent(radixComponent radixv1.RadixCommonDeployComponent, ra *radixv1.RadixApplication, rd *radixv1.RadixDeployment, +func buildComponent( + radixComponent radixv1.RadixCommonDeployComponent, ra *radixv1.RadixApplication, rd *radixv1.RadixDeployment, deploymentList []appsv1.Deployment, podList []corev1.Pod, hpaList []autoscalingv2.HorizontalPodAutoscaler, - secretList []corev1.Secret, certs []cmv1.Certificate, certRequests []cmv1.CertificateRequest, lastEventWarnings map[string]string, tlsValidator tlsvalidation.Validator) *deploymentModels.Component { + secretList []corev1.Secret, certs []cmv1.Certificate, certRequests []cmv1.CertificateRequest, + lastEventWarnings map[string]string, tlsValidator tlsvalidation.Validator, scaledObjects []v1alpha1.ScaledObject, +) *deploymentModels.Component { builder := deploymentModels.NewComponentBuilder(). WithComponent(radixComponent). WithStatus(deploymentModels.ConsistentComponent). - WithHorizontalScalingSummary(getHpaSummary(ra.Name, radixComponent.GetName(), hpaList)). + WithHorizontalScalingSummary(GetHpaSummary(ra.Name, radixComponent.GetName(), hpaList, scaledObjects)). WithExternalDNS(getComponentExternalDNS(ra.Name, radixComponent, secretList, certs, certRequests, tlsValidator)) componentPods := slice.FindAll(podList, predicate.IsPodForComponent(ra.Name, radixComponent.GetName())) diff --git a/api/models/deployment.go b/api/models/deployment.go index 0778969a..832ee0eb 100644 --- a/api/models/deployment.go +++ b/api/models/deployment.go @@ -7,16 +7,20 @@ import ( "github.com/equinor/radix-common/utils/slice" "github.com/equinor/radix-operator/pkg/apis/kube" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + "github.com/kedacore/keda/v2/apis/keda/v1alpha1" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" ) // BuildDeployment builds a Deployment model. -func BuildDeployment(rr *radixv1.RadixRegistration, ra *radixv1.RadixApplication, rd *radixv1.RadixDeployment, deploymentList []appsv1.Deployment, - podList []corev1.Pod, hpaList []autoscalingv2.HorizontalPodAutoscaler, secretList []corev1.Secret, eventList []corev1.Event, rjList []radixv1.RadixJob, - certs []cmv1.Certificate, certRequests []cmv1.CertificateRequest, tlsValidator tlsvalidation.Validator) *deploymentModels.Deployment { - components := BuildComponents(ra, rd, deploymentList, podList, hpaList, secretList, eventList, certs, certRequests, tlsValidator) +func BuildDeployment( + rr *radixv1.RadixRegistration, ra *radixv1.RadixApplication, rd *radixv1.RadixDeployment, deploymentList []appsv1.Deployment, + podList []corev1.Pod, hpaList []autoscalingv2.HorizontalPodAutoscaler, secretList []corev1.Secret, eventList []corev1.Event, + rjList []radixv1.RadixJob, certs []cmv1.Certificate, certRequests []cmv1.CertificateRequest, tlsValidator tlsvalidation.Validator, + scaledObjects []v1alpha1.ScaledObject, +) *deploymentModels.Deployment { + components := BuildComponents(ra, rd, deploymentList, podList, hpaList, secretList, eventList, certs, certRequests, tlsValidator, scaledObjects) // The only error that can be returned from DeploymentBuilder is related to errors from github.com/imdario/mergo // This type of error will only happen if incorrect objects (e.g. incompatible structs) are sent as arguments to mergo, diff --git a/api/models/environment.go b/api/models/environment.go index 5e883af8..f15d0a51 100644 --- a/api/models/environment.go +++ b/api/models/environment.go @@ -8,6 +8,7 @@ import ( "github.com/equinor/radix-api/api/utils/tlsvalidation" "github.com/equinor/radix-common/utils/slice" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + "github.com/kedacore/keda/v2/apis/keda/v1alpha1" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" @@ -18,7 +19,8 @@ import ( func BuildEnvironment(rr *radixv1.RadixRegistration, ra *radixv1.RadixApplication, re *radixv1.RadixEnvironment, rdList []radixv1.RadixDeployment, rjList []radixv1.RadixJob, deploymentList []appsv1.Deployment, podList []corev1.Pod, hpaList []autoscalingv2.HorizontalPodAutoscaler, secretList []corev1.Secret, secretProviderClassList []secretsstorev1.SecretProviderClass, eventList []corev1.Event, - certs []cmv1.Certificate, certRequests []cmv1.CertificateRequest, tlsValidator tlsvalidation.Validator) *environmentModels.Environment { + certs []cmv1.Certificate, certRequests []cmv1.CertificateRequest, tlsValidator tlsvalidation.Validator, scaledObjects []v1alpha1.ScaledObject, +) *environmentModels.Environment { var buildFromBranch string var activeDeployment *deploymentModels.Deployment var secrets []secretModels.Secret @@ -28,7 +30,7 @@ func BuildEnvironment(rr *radixv1.RadixRegistration, ra *radixv1.RadixApplicatio } if activeRd, ok := slice.FindFirst(rdList, isActiveDeploymentForAppAndEnv(ra.Name, re.Spec.EnvName)); ok { - activeDeployment = BuildDeployment(rr, ra, &activeRd, deploymentList, podList, hpaList, secretList, eventList, rjList, certs, certRequests, tlsValidator) + activeDeployment = BuildDeployment(rr, ra, &activeRd, deploymentList, podList, hpaList, secretList, eventList, rjList, certs, certRequests, tlsValidator, scaledObjects) secrets = BuildSecrets(secretList, secretProviderClassList, &activeRd) } diff --git a/api/models/horizontal_scaling_summary.go b/api/models/horizontal_scaling_summary.go index d273e258..acf71282 100644 --- a/api/models/horizontal_scaling_summary.go +++ b/api/models/horizontal_scaling_summary.go @@ -1,40 +1,141 @@ package models import ( + "fmt" + "regexp" + "strconv" + deploymentModels "github.com/equinor/radix-api/api/deployments/models" "github.com/equinor/radix-api/api/utils/horizontalscaling" "github.com/equinor/radix-api/api/utils/predicate" "github.com/equinor/radix-common/utils/slice" + "github.com/kedacore/keda/v2/apis/keda/v1alpha1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" ) -func getHpaSummary(appName, componentName string, hpaList []autoscalingv2.HorizontalPodAutoscaler) *deploymentModels.HorizontalScalingSummary { - hpa, ok := slice.FindFirst(hpaList, predicate.IsHpaForComponent(appName, componentName)) +var triggerIndexRegex = regexp.MustCompile(`^s(\d+)-`) + +func GetHpaSummary(appName, componentName string, hpaList []autoscalingv2.HorizontalPodAutoscaler, scalerList []v1alpha1.ScaledObject) *deploymentModels.HorizontalScalingSummary { + scaler, ok := slice.FindFirst(scalerList, predicate.IsScaledObjectForComponent(appName, componentName)) + if !ok { + return nil + } + hpa, ok := slice.FindFirst(hpaList, func(s autoscalingv2.HorizontalPodAutoscaler) bool { + return s.Name == scaler.Status.HpaName + }) if !ok { return nil } - minReplicas := int32(1) - if hpa.Spec.MinReplicas != nil { - minReplicas = *hpa.Spec.MinReplicas + var minReplicas, maxReplicas, cooldownPeriod, pollingInterval int32 + if scaler.Spec.MinReplicaCount != nil { + minReplicas = *scaler.Spec.MinReplicaCount + } + if scaler.Spec.MaxReplicaCount != nil { + maxReplicas = *scaler.Spec.MaxReplicaCount + } + if scaler.Spec.CooldownPeriod != nil { + cooldownPeriod = *scaler.Spec.CooldownPeriod + } + if scaler.Spec.PollingInterval != nil { + pollingInterval = *scaler.Spec.PollingInterval } - maxReplicas := hpa.Spec.MaxReplicas currentCpuUtil, targetCpuUtil := getHpaMetrics(&hpa, corev1.ResourceCPU) currentMemoryUtil, targetMemoryUtil := getHpaMetrics(&hpa, corev1.ResourceMemory) + var triggers []deploymentModels.HorizontalScalingSummaryTriggerStatus + + // ResourceMetricNames lists resource types, not metric names + for _, resourceType := range scaler.Status.ResourceMetricNames { + var trigger v1alpha1.ScaleTriggers + + if trigger, ok = slice.FindFirst(scaler.Spec.Triggers, func(t v1alpha1.ScaleTriggers) bool { + return t.Type == resourceType + }); !ok { + continue + } + + triggers = append(triggers, getResourceMetricStatus(hpa, trigger)) + } + + for _, triggerName := range scaler.Status.ExternalMetricNames { + match := triggerIndexRegex.FindStringSubmatch(triggerName) + if len(match) != 2 { + continue + } + index, err := strconv.Atoi(match[1]) + if err != nil { + continue + } + + trigger := scaler.Spec.Triggers[index] + triggers = append(triggers, getExternalMetricStatus(hpa, triggerName, scaler, trigger)) + } + hpaSummary := deploymentModels.HorizontalScalingSummary{ MinReplicas: minReplicas, MaxReplicas: maxReplicas, + CooldownPeriod: cooldownPeriod, + PollingInterval: pollingInterval, CurrentCPUUtilizationPercentage: currentCpuUtil, TargetCPUUtilizationPercentage: targetCpuUtil, CurrentMemoryUtilizationPercentage: currentMemoryUtil, TargetMemoryUtilizationPercentage: targetMemoryUtil, + Triggers: triggers, } return &hpaSummary } +func getResourceMetricStatus(hpa autoscalingv2.HorizontalPodAutoscaler, trigger v1alpha1.ScaleTriggers) deploymentModels.HorizontalScalingSummaryTriggerStatus { + var current string + if metricStatus, ok := slice.FindFirst(hpa.Status.CurrentMetrics, func(s autoscalingv2.MetricStatus) bool { + return s.Resource != nil && s.Resource.Name.String() == trigger.Type + }); ok && metricStatus.Resource != nil { + current = fmt.Sprintf("%d", *metricStatus.Resource.Current.AverageUtilization) + } + + status := deploymentModels.HorizontalScalingSummaryTriggerStatus{ + Name: trigger.Name, + CurrentUtilization: current, + TargetUtilization: trigger.Metadata["value"], + Type: trigger.Type, + Error: "", + } + return status +} + +func getExternalMetricStatus(hpa autoscalingv2.HorizontalPodAutoscaler, triggerName string, scaler v1alpha1.ScaledObject, trigger v1alpha1.ScaleTriggers) deploymentModels.HorizontalScalingSummaryTriggerStatus { + var current, target, errStr string + + if metricStatus, ok := slice.FindFirst(hpa.Status.CurrentMetrics, func(s autoscalingv2.MetricStatus) bool { + return s.External != nil && s.External.Metric.Name == triggerName + }); ok && metricStatus.External != nil { + current = metricStatus.External.Current.AverageValue.String() + } + + if health, ok := scaler.Status.Health[triggerName]; ok && health.Status != "Happy" { + errStr = fmt.Sprintf("%s: number of failurs: %d", health.Status, *health.NumberOfFailures) + } + + switch trigger.Type { + case "cron": + target = trigger.Metadata["desiredReplicas"] + case "azure-servicebus": + target = trigger.Metadata["messageCount"] + } + + status := deploymentModels.HorizontalScalingSummaryTriggerStatus{ + Name: trigger.Name, + CurrentUtilization: current, + TargetUtilization: target, + Type: trigger.Type, + Error: errStr, + } + return status +} + func getHpaMetrics(hpa *autoscalingv2.HorizontalPodAutoscaler, resourceName corev1.ResourceName) (*int32, *int32) { currentResourceUtil := getHpaCurrentMetric(hpa, resourceName) diff --git a/api/utils/kubernetes.go b/api/utils/kubernetes.go index bb64e90b..c4083cae 100644 --- a/api/utils/kubernetes.go +++ b/api/utils/kubernetes.go @@ -6,12 +6,14 @@ import ( certclient "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned" "github.com/equinor/radix-api/api/metrics" + "github.com/equinor/radix-api/api/utils/logs" radixmodels "github.com/equinor/radix-common/models" radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" kedav2 "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" tektonclient "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" "k8s.io/client-go/kubernetes" @@ -108,6 +110,9 @@ func getOutClusterClientConfig(token string, impersonation radixmodels.Impersona kubeConfig.Impersonate = impersonationConfig } + kubeConfig.Wrap(logs.Logger(func(e *zerolog.Event) { + e.Str("client", "out-cluster") + })) return addCommonConfigs(kubeConfig, options) } @@ -121,6 +126,9 @@ func getInClusterClientConfig(options []RestClientConfigOption) *restclient.Conf log.Fatal().Err(err).Msg("getClusterConfig InClusterConfig") } } + config.Wrap(logs.Logger(func(e *zerolog.Event) { + e.Str("client", "in-cluster") + })) return addCommonConfigs(config, options) } diff --git a/api/utils/logs/roundtrip_logger.go b/api/utils/logs/roundtrip_logger.go new file mode 100644 index 00000000..ae1bcd17 --- /dev/null +++ b/api/utils/logs/roundtrip_logger.go @@ -0,0 +1,65 @@ +package logs + +import ( + "net/http" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// RoundTripperFunc implements http.RoundTripper for convenient usage. +type RoundTripperFunc func(*http.Request) (*http.Response, error) + +// RoundTrip satisfies http.RoundTripper and calls fn. +func (fn RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return fn(req) +} + +type WithFunc func(e *zerolog.Event) + +// Logger returns a http.RoundTripper that logs failed requests, and add traces for successfull requests +// +// nolint Zerolog complains about potential unsent event, but we send the event on the end of the function +func Logger(fns ...WithFunc) func(t http.RoundTripper) http.RoundTripper { + return func(t http.RoundTripper) http.RoundTripper { + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + logger := log.Ctx(r.Context()).With(). + Str("method", r.Method). + Stringer("path", r.URL). + Logger() + + start := time.Now() + resp, err := t.RoundTrip(r) + elapsedMs := time.Since(start).Milliseconds() + + if err != nil { + errEvent := logger.Error().Err(err) + for _, fn := range fns { + errEvent.Func(fn) + } + errEvent. + Int64("elapsed_ms", elapsedMs). + Msg("Failed to send request") + + return resp, err + } + + var ev *zerolog.Event + switch { + case resp.StatusCode >= 400 && resp.StatusCode <= 499: + ev = logger.Warn() + case resp.StatusCode >= 500: + ev = logger.Error() + default: + ev = logger.Trace() + } + + for _, fn := range fns { + ev.Func(fn) + } + ev.Int64("elapsed_ms", elapsedMs).Int("status", resp.StatusCode).Msg(http.StatusText(resp.StatusCode)) + return resp, err + }) + } +} diff --git a/api/utils/predicate/kubernetes.go b/api/utils/predicate/kubernetes.go index 16d56731..cc3bbb3c 100644 --- a/api/utils/predicate/kubernetes.go +++ b/api/utils/predicate/kubernetes.go @@ -3,6 +3,7 @@ package predicate import ( "github.com/equinor/radix-api/api/utils/labelselector" radixlabels "github.com/equinor/radix-operator/pkg/apis/utils/labels" + "github.com/kedacore/keda/v2/apis/keda/v1alpha1" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" @@ -45,6 +46,12 @@ func IsHpaForComponent(appName, componentName string) func(autoscalingv2.Horizon return selector.Matches(labels.Set(hpa.Labels)) } } +func IsScaledObjectForComponent(appName, componentName string) func(object v1alpha1.ScaledObject) bool { + selector := labels.SelectorFromSet(radixlabels.Merge(radixlabels.ForApplicationName(appName), radixlabels.ForComponentName(componentName))) + return func(object v1alpha1.ScaledObject) bool { + return selector.Matches(labels.Set(object.Labels)) + } +} func IsSecretForSecretStoreProviderClass(secret corev1.Secret) bool { return labels.Set{secretStoreCsiManagedLabel: "true"}.AsSelector().Matches(labels.Set(secret.Labels)) diff --git a/swaggerui/html/swagger.json b/swaggerui/html/swagger.json index 011aefc8..5b84cb8c 100644 --- a/swaggerui/html/swagger.json +++ b/swaggerui/html/swagger.json @@ -5623,7 +5623,7 @@ "x-go-name": "ReplicaList" }, "replicas": { - "description": "Array of pod names", + "description": "Deprecated: Array of pod names. Use ReplicaList instead", "type": "array", "items": { "type": "string" @@ -6301,15 +6301,22 @@ "description": "HorizontalScalingSummary describe the summary of horizontal scaling of a component", "type": "object", "properties": { + "cooldownPeriod": { + "description": "CooldownPeriod in seconds. From radixconfig.yaml", + "type": "integer", + "format": "int32", + "x-go-name": "CooldownPeriod", + "example": 300 + }, "currentCPUUtilizationPercentage": { - "description": "Component current average CPU utilization over all pods, represented as a percentage of requested CPU", + "description": "Deprecated: Component current average CPU utilization over all pods, represented as a percentage of requested CPU. Use Triggers instead. Will be removed from Radix API 2025-01-01.", "type": "integer", "format": "int32", "x-go-name": "CurrentCPUUtilizationPercentage", "example": 70 }, "currentMemoryUtilizationPercentage": { - "description": "Component current average memory utilization over all pods, represented as a percentage of requested memory", + "description": "Deprecated: Component current average memory utilization over all pods, represented as a percentage of requested memory. Use Triggers instead. Will be removed from Radix API 2025-01-01.", "type": "integer", "format": "int32", "x-go-name": "CurrentMemoryUtilizationPercentage", @@ -6329,19 +6336,66 @@ "x-go-name": "MinReplicas", "example": 2 }, + "pollingInterval": { + "description": "PollingInterval in seconds. From radixconfig.yaml", + "type": "integer", + "format": "int32", + "x-go-name": "PollingInterval", + "example": 30 + }, "targetCPUUtilizationPercentage": { - "description": "Component target average CPU utilization over all pods", + "description": "Deprecated: Component target average CPU utilization over all pods. Use Triggers instead. Will be removed from Radix API 2025-01-01.", "type": "integer", "format": "int32", "x-go-name": "TargetCPUUtilizationPercentage", "example": 80 }, "targetMemoryUtilizationPercentage": { - "description": "Component target average memory utilization over all pods", + "description": "Deprecated: Component target average memory utilization over all pods. use Triggers instead. Will be removed from Radix API 2025-01-01.", "type": "integer", "format": "int32", "x-go-name": "TargetMemoryUtilizationPercentage", "example": 80 + }, + "triggers": { + "description": "Triggers lists status of all triggers found in radixconfig.yaml", + "type": "array", + "items": { + "$ref": "#/definitions/HorizontalScalingSummaryTriggerStatus" + }, + "x-go-name": "Triggers", + "example": "30" + } + }, + "x-go-package": "github.com/equinor/radix-api/api/deployments/models" + }, + "HorizontalScalingSummaryTriggerStatus": { + "type": "object", + "properties": { + "current_utilization": { + "description": "CurrentUtilization is the last measured utilization", + "type": "string", + "x-go-name": "CurrentUtilization" + }, + "error": { + "description": "Error contains short description if trigger have problems", + "type": "string", + "x-go-name": "Error" + }, + "name": { + "description": "Name of trigger", + "type": "string", + "x-go-name": "Name" + }, + "target_utilization": { + "description": "TargetUtilization is the average target across replicas", + "type": "string", + "x-go-name": "TargetUtilization" + }, + "type": { + "description": "Type of trigger", + "type": "string", + "x-go-name": "Type" } }, "x-go-package": "github.com/equinor/radix-api/api/deployments/models"