From 433bb688bccb03057a6d7f7068676911452de36c Mon Sep 17 00:00:00 2001 From: Pavan <25031267+Pavan-SAP@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:57:57 +0100 Subject: [PATCH] [Feat] Operator: HPA config enabled This change enables explicit support for HPA. Doing this within the operator allows us to also cleanup HPAs for older versions (as this is owned by the version). We had to create our own HPA spec as scaleTargerRef is required and the one from the k8s client does not allow to omit it. We cannnot let users specify it as this needs to be set by the operator to the correct deployment values. --- cmd/web-hooks/internal/handler/handler.go | 4 +- crds/sme.sap.com_capapplicationversions.yaml | 346 ++++++++++++++++++ internal/controller/common_test.go | 5 +- .../reconcile-capapplicationversion.go | 55 +++ .../reconcile-capapplicationversion_test.go | 46 +++ .../capapplicationversion/cav-hpa.yaml | 86 +++++ .../expected/cav-hpa-ready.yaml | 158 ++++++++ pkg/apis/sme.sap.com/v1alpha1/types.go | 19 + .../v1alpha1/zz_generated.deepcopy.go | 39 ++ .../sme.sap.com/v1alpha1/deploymentdetails.go | 10 + .../v1alpha1/horizontalpodautoscalerspec.go | 68 ++++ pkg/client/applyconfiguration/utils.go | 2 + 12 files changed, 835 insertions(+), 3 deletions(-) create mode 100644 internal/controller/testdata/capapplicationversion/cav-hpa.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-hpa-ready.yaml create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/horizontalpodautoscalerspec.go diff --git a/cmd/web-hooks/internal/handler/handler.go b/cmd/web-hooks/internal/handler/handler.go index 03d0940a..ffc0b492 100644 --- a/cmd/web-hooks/internal/handler/handler.go +++ b/cmd/web-hooks/internal/handler/handler.go @@ -377,8 +377,8 @@ func validateWorkloads(ca *v1alpha1.CAPApplication, cavObjNew *v1alpha1.CAPAppli return workloadPortValidate } - if workloadPortValidate := checkWorkloadPodDistruptionBudget(&workload); !workloadPortValidate.allowed { - return workloadPortValidate + if workloadPDBValidate := checkWorkloadPodDistruptionBudget(&workload); !workloadPDBValidate.allowed { + return workloadPDBValidate } // get count of workload names diff --git a/crds/sme.sap.com_capapplicationversions.yaml b/crds/sme.sap.com_capapplicationversions.yaml index 3cd2302c..1bc28217 100644 --- a/crds/sme.sap.com_capapplicationversions.yaml +++ b/crds/sme.sap.com_capapplicationversions.yaml @@ -659,6 +659,352 @@ spec: - name type: object type: array + horizontalPodAutoscaler: + properties: + behavior: + properties: + scaleDown: + properties: + policies: + items: + properties: + periodSeconds: + format: int32 + type: integer + type: + type: string + value: + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + type: string + stabilizationWindowSeconds: + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + properties: + policies: + items: + properties: + periodSeconds: + format: int32 + type: integer + type: + type: string + value: + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + type: string + stabilizationWindowSeconds: + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + format: int32 + type: integer + metrics: + items: + properties: + containerResource: + properties: + container: + type: string + name: + type: string + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + properties: + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + properties: + describedObject: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + properties: + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + properties: + name: + type: string + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + type: string + required: + - type + type: object + type: array + minReplicas: + format: int32 + type: integer + required: + - maxReplicas + type: object image: type: string imagePullPolicy: diff --git a/internal/controller/common_test.go b/internal/controller/common_test.go index f77cdfcc..3868f7d5 100644 --- a/internal/controller/common_test.go +++ b/internal/controller/common_test.go @@ -43,6 +43,7 @@ import ( istiofake "istio.io/client-go/pkg/clientset/versioned/fake" istioscheme "istio.io/client-go/pkg/clientset/versioned/scheme" appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" @@ -671,8 +672,10 @@ func compareExpectedWithStore(t *testing.T, resource []byte, c *Controller) erro actual, err = c.kubeClient.(*k8sfake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("endpointslices"), mo.GetNamespace(), mo.GetName()) case *policyv1.PodDisruptionBudget: actual, err = c.kubeClient.(*k8sfake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("poddisruptionbudgets"), mo.GetNamespace(), mo.GetName()) + case *autoscalingv2.HorizontalPodAutoscaler: + actual, err = c.kubeClient.(*k8sfake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("horizontalpodautoscalers"), mo.GetNamespace(), mo.GetName()) default: - return fmt.Errorf("unknown expected object type") + return fmt.Errorf("unknown expected object type (common_test might need to be extended): %T", expected) } if err == nil { diff --git a/internal/controller/reconcile-capapplicationversion.go b/internal/controller/reconcile-capapplicationversion.go index b6e55c8d..718204ea 100644 --- a/internal/controller/reconcile-capapplicationversion.go +++ b/internal/controller/reconcile-capapplicationversion.go @@ -18,6 +18,7 @@ import ( "github.com/sap/cap-operator/internal/util" "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" @@ -713,9 +714,63 @@ func (c *Controller) updateDeployment(ca *v1alpha1.CAPApplication, cav *v1alpha1 } } + // Create HPA for the deployment if configured + if err == nil && workload.DeploymentDefinition.HorizontalPodAutoscaler != nil { + err = c.createOrUpdateHorizontalPodAutoscaler(deploymentName, workload, cav, ca) + if err != nil { + return nil, err + } + } + return workloadDeployment, doChecks(err, workloadDeployment, cav, workload.Name) } +func (c *Controller) createOrUpdateHorizontalPodAutoscaler(deploymentName string, workload *v1alpha1.WorkloadDetails, cav *v1alpha1.CAPApplicationVersion, ca *v1alpha1.CAPApplication) error { + hpaName := deploymentName + // Get the HPA which should exist for this deployment + hpa, err := c.kubeClient.AutoscalingV2().HorizontalPodAutoscalers(cav.Namespace).Get(context.TODO(), hpaName, metav1.GetOptions{}) + // If the resource doesn't exist, we'll create it + if k8sErrors.IsNotFound(err) { + hpa, err = c.kubeClient.AutoscalingV2().HorizontalPodAutoscalers(cav.Namespace).Create(context.TODO(), newHorizontalPodAutoscaler(deploymentName, ca, cav, workload), metav1.CreateOptions{}) + if err == nil { + util.LogInfo("Horizontal Pod Autoscaler created successfully", string(Processing), cav, hpa, "version", cav.Spec.Version) + } + } + return doChecks(err, hpa, cav, workload.Name) +} + +func newHorizontalPodAutoscaler(deploymentName string, ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion, workload *v1alpha1.WorkloadDetails) *autoscalingv2.HorizontalPodAutoscaler { + hpaName := deploymentName + labels := copyMaps(workload.Labels, getLabels(ca, cav, CategoryWorkload, string(workload.DeploymentDefinition.Type), getWorkloadName(cav.Name, workload.Name), true)) + + // Copy the HPA spec defined in the CRD and set the scale target ref to the deployment created for this workload. + // As scaleTargetRef from k8s client is required, we used our own copy without that field and set it here to avoid users having to set it. + hpaSpec := autoscalingv2.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: deploymentName, + APIVersion: "apps/v1", + }, + MinReplicas: workload.DeploymentDefinition.HorizontalPodAutoscaler.MinReplicas, + MaxReplicas: workload.DeploymentDefinition.HorizontalPodAutoscaler.MaxReplicas, + Metrics: workload.DeploymentDefinition.HorizontalPodAutoscaler.Metrics, + Behavior: workload.DeploymentDefinition.HorizontalPodAutoscaler.Behavior, + } + + return &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Name: hpaName, + Namespace: cav.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(cav, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationVersionKind)), + }, + Labels: labels, + Annotations: copyMaps(workload.Annotations, getAnnotations(ca, cav, true)), + }, + Spec: hpaSpec, + } +} + func (c *Controller) createOrUpdatePodDisruptionBudget(workload *v1alpha1.WorkloadDetails, cav *v1alpha1.CAPApplicationVersion, ca *v1alpha1.CAPApplication) error { pdbName := getWorkloadName(cav.Name, workload.Name) // Get the PDB which should exist for this deployment diff --git a/internal/controller/reconcile-capapplicationversion_test.go b/internal/controller/reconcile-capapplicationversion_test.go index 4ebb1c73..62d8ccdc 100644 --- a/internal/controller/reconcile-capapplicationversion_test.go +++ b/internal/controller/reconcile-capapplicationversion_test.go @@ -1015,3 +1015,49 @@ func TestCAV_PodDisruptionBudget(t *testing.T) { }, ) } + +func TestCAV_HorizontalPodAutoscalerError(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-ca-01-cav-v1"}}, + TestData{ + description: "capapplication version - horizontal pod autoscaler error scenario", + initialResources: []string{ + "testdata/common/domain-ready.yaml", + "testdata/common/cluster-domain-ready.yaml", + "testdata/common/ca-services.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-hpa.yaml", + "testdata/capapplicationversion/services-ready.yaml", + "testdata/capapplicationversion/service-content-job-completed.yaml", + }, + backlogItems: []string{}, + expectError: true, + mockErrorForResources: []ResourceAction{ + {Verb: "get", Group: "autoscaling", Version: "v2", Resource: "horizontalpodautoscalers", Namespace: "default", Name: "*"}, + }, + }, + ) +} + +func TestCAV_HorizontalPodAutoscaler(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-ca-01-cav-v1"}}, + TestData{ + description: "capapplication version - horizontal pod autoscaler", + initialResources: []string{ + "testdata/common/domain-ready.yaml", + "testdata/common/cluster-domain-ready.yaml", + "testdata/common/ca-services.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-hpa.yaml", + "testdata/capapplicationversion/services-ready.yaml", + "testdata/capapplicationversion/service-content-job-completed.yaml", + }, + backlogItems: []string{}, + expectError: false, + expectedResources: "testdata/capapplicationversion/expected/cav-hpa-ready.yaml", + }, + ) +} diff --git a/internal/controller/testdata/capapplicationversion/cav-hpa.yaml b/internal/controller/testdata/capapplicationversion/cav-hpa.yaml new file mode 100644 index 00000000..94c732b1 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-hpa.yaml @@ -0,0 +1,86 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 66ee9c21adb3f78f19a2c376acc179437a96b5cb + finalizers: + - sme.sap.com/capapplicationversion + name: test-ca-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-ca-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-ca-01 + registrySecrets: + - regcred + version: 0.0.1 + workloads: + - name: cap-backend-service + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: Service + image: docker.image.repo/srv/server:latest + ports: + - name: server + port: 4004 + appProtocol: http + - name: api + port: 8000 + appProtocol: http + - name: metrics + port: 4005 + appProtocol: http + replicas: 3 + horizontalPodAutoscaler: + minReplicas: 1 + maxReplicas: 5 + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Service + image: docker.image.repo/approuter/approuter:latest + ports: + - name: server + port: 5000 + appProtocol: http + replicas: 3 + horizontalPodAutoscaler: + minReplicas: 2 + maxReplicas: 4 + serviceExposures: + - subDomain: router + routes: + - workloadName: app-router + port: 5000 + - subDomain: api + routes: + - workloadName: cap-backend-service + port: 8000 + path: /api +status: + state: Processing \ No newline at end of file diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-hpa-ready.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-hpa-ready.yaml new file mode 100644 index 00000000..8f72cbe6 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-hpa-ready.yaml @@ -0,0 +1,158 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 66ee9c21adb3f78f19a2c376acc179437a96b5cb + finalizers: + - sme.sap.com/capapplicationversion + name: test-ca-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-ca-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-ca-01 + registrySecrets: + - regcred + version: 0.0.1 + workloads: + - name: cap-backend-service + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: Service + image: docker.image.repo/srv/server:latest + ports: + - name: server + port: 4004 + appProtocol: http + - name: api + port: 8000 + appProtocol: http + - name: metrics + port: 4005 + appProtocol: http + replicas: 3 + horizontalPodAutoscaler: + minReplicas: 1 + maxReplicas: 5 + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Service + image: docker.image.repo/approuter/approuter:latest + ports: + - name: server + port: 5000 + appProtocol: http + replicas: 3 + horizontalPodAutoscaler: + minReplicas: 2 + maxReplicas: 4 + serviceExposures: + - subDomain: router + routes: + - workloadName: app-router + port: 5000 + - subDomain: api + routes: + - workloadName: cap-backend-service + port: 8000 + path: /api +status: + conditions: + - reason: WorkloadsReady + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - test-ca-01-cav-v1-content + state: Ready +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-ca-01-cav-v1 + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-ca-01-cav-v1-cap-backend-service + sme.sap.com/workload-type: Service + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "0.0.1" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: 00a7bd3cb89af3010bf21dcdc056f176adf3a862 + name: test-ca-01-cav-v1-cap-backend-service + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-ca-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + minReplicas: 1 + maxReplicas: 5 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: test-ca-01-cav-v1-cap-backend-service +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-ca-01-cav-v1 + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-ca-01-cav-v1-app-router + sme.sap.com/workload-type: Service + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "0.0.1" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: 00a7bd3cb89af3010bf21dcdc056f176adf3a862 + name: test-ca-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-ca-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + minReplicas: 2 + maxReplicas: 4 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: test-ca-01-cav-v1-app-router \ No newline at end of file diff --git a/pkg/apis/sme.sap.com/v1alpha1/types.go b/pkg/apis/sme.sap.com/v1alpha1/types.go index 013c245a..3cd5f9d4 100644 --- a/pkg/apis/sme.sap.com/v1alpha1/types.go +++ b/pkg/apis/sme.sap.com/v1alpha1/types.go @@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0 package v1alpha1 import ( + autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -285,6 +286,24 @@ type DeploymentDetails struct { Monitoring *WorkloadMonitoring `json:"monitoring,omitempty"` // Pod Disruption Budget may be used to specify the minimum number of available pods for this workload PodDisruptionBudget *policyv1.PodDisruptionBudgetSpec `json:"podDisruptionBudget,omitempty"` + // Horizontal Pod Autoscaler may be used to specify the scaling behavior for this workload + HorizontalPodAutoscaler *HorizontalPodAutoscalerSpec `json:"horizontalPodAutoscaler,omitempty"` +} + +// HorizontalPodAutoscalerSpec wraps autoscalingv2.HorizontalPodAutoscalerSpec but gets rid of scaleTargetRef, +// as the operator always sets it to the deployment created for the workload. +type HorizontalPodAutoscalerSpec struct { + // minReplicas is the lower limit for the number of replicas to which the autoscaler can scale down. + // +optional + MinReplicas *int32 `json:"minReplicas,omitempty"` + // maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up. + MaxReplicas int32 `json:"maxReplicas"` + // metrics contains the specifications for which to use to calculate the desired replica count. + // +optional + Metrics []autoscalingv2.MetricSpec `json:"metrics,omitempty"` + // behavior configures the scaling behavior of the target in both Up and Down directions. + // +optional + Behavior *autoscalingv2.HorizontalPodAutoscalerBehavior `json:"behavior,omitempty"` } // ServiceExposure specifies the details of the VirtualService to be exposed for `Service` type workload(s) diff --git a/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go index e09462fa..f012abcc 100644 --- a/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go @@ -11,6 +11,7 @@ SPDX-License-Identifier: Apache-2.0 package v1alpha1 import ( + v2 "k8s.io/api/autoscaling/v2" v1 "k8s.io/api/core/v1" policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -874,6 +875,11 @@ func (in *DeploymentDetails) DeepCopyInto(out *DeploymentDetails) { *out = new(policyv1.PodDisruptionBudgetSpec) (*in).DeepCopyInto(*out) } + if in.HorizontalPodAutoscaler != nil { + in, out := &in.HorizontalPodAutoscaler, &out.HorizontalPodAutoscaler + *out = new(HorizontalPodAutoscalerSpec) + (*in).DeepCopyInto(*out) + } return } @@ -1037,6 +1043,39 @@ func (in *GenericStatus) DeepCopy() *GenericStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HorizontalPodAutoscalerSpec) DeepCopyInto(out *HorizontalPodAutoscalerSpec) { + *out = *in + if in.MinReplicas != nil { + in, out := &in.MinReplicas, &out.MinReplicas + *out = new(int32) + **out = **in + } + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make([]v2.MetricSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Behavior != nil { + in, out := &in.Behavior, &out.Behavior + *out = new(v2.HorizontalPodAutoscalerBehavior) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizontalPodAutoscalerSpec. +func (in *HorizontalPodAutoscalerSpec) DeepCopy() *HorizontalPodAutoscalerSpec { + if in == nil { + return nil + } + out := new(HorizontalPodAutoscalerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JobDetails) DeepCopyInto(out *JobDetails) { *out = *in diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go index 95d44ba1..2de53025 100644 --- a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go @@ -33,6 +33,8 @@ type DeploymentDetailsApplyConfiguration struct { Monitoring *WorkloadMonitoringApplyConfiguration `json:"monitoring,omitempty"` // Pod Disruption Budget may be used to specify the minimum number of available pods for this workload PodDisruptionBudget *policyv1.PodDisruptionBudgetSpec `json:"podDisruptionBudget,omitempty"` + // Horizontal Pod Autoscaler may be used to specify the scaling behavior for this workload + HorizontalPodAutoscaler *HorizontalPodAutoscalerSpecApplyConfiguration `json:"horizontalPodAutoscaler,omitempty"` } // DeploymentDetailsApplyConfiguration constructs a declarative configuration of the DeploymentDetails type for use with @@ -275,3 +277,11 @@ func (b *DeploymentDetailsApplyConfiguration) WithPodDisruptionBudget(value poli b.PodDisruptionBudget = &value return b } + +// WithHorizontalPodAutoscaler sets the HorizontalPodAutoscaler field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the HorizontalPodAutoscaler field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithHorizontalPodAutoscaler(value *HorizontalPodAutoscalerSpecApplyConfiguration) *DeploymentDetailsApplyConfiguration { + b.HorizontalPodAutoscaler = value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/horizontalpodautoscalerspec.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/horizontalpodautoscalerspec.go new file mode 100644 index 00000000..f31fe144 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/horizontalpodautoscalerspec.go @@ -0,0 +1,68 @@ +/* +SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v2 "k8s.io/api/autoscaling/v2" +) + +// HorizontalPodAutoscalerSpecApplyConfiguration represents a declarative configuration of the HorizontalPodAutoscalerSpec type for use +// with apply. +// +// HorizontalPodAutoscalerSpec wraps autoscalingv2.HorizontalPodAutoscalerSpec but makes scaleTargetRef optional, +// as the operator always sets it to the deployment created for the workload. +type HorizontalPodAutoscalerSpecApplyConfiguration struct { + // minReplicas is the lower limit for the number of replicas to which the autoscaler can scale down. + MinReplicas *int32 `json:"minReplicas,omitempty"` + // maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up. + MaxReplicas *int32 `json:"maxReplicas,omitempty"` + // metrics contains the specifications for which to use to calculate the desired replica count. + Metrics []v2.MetricSpec `json:"metrics,omitempty"` + // behavior configures the scaling behavior of the target in both Up and Down directions. + Behavior *v2.HorizontalPodAutoscalerBehavior `json:"behavior,omitempty"` +} + +// HorizontalPodAutoscalerSpecApplyConfiguration constructs a declarative configuration of the HorizontalPodAutoscalerSpec type for use with +// apply. +func HorizontalPodAutoscalerSpec() *HorizontalPodAutoscalerSpecApplyConfiguration { + return &HorizontalPodAutoscalerSpecApplyConfiguration{} +} + +// WithMinReplicas sets the MinReplicas field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the MinReplicas field is set to the value of the last call. +func (b *HorizontalPodAutoscalerSpecApplyConfiguration) WithMinReplicas(value int32) *HorizontalPodAutoscalerSpecApplyConfiguration { + b.MinReplicas = &value + return b +} + +// WithMaxReplicas sets the MaxReplicas field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the MaxReplicas field is set to the value of the last call. +func (b *HorizontalPodAutoscalerSpecApplyConfiguration) WithMaxReplicas(value int32) *HorizontalPodAutoscalerSpecApplyConfiguration { + b.MaxReplicas = &value + return b +} + +// WithMetrics adds the given value to the Metrics field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Metrics field. +func (b *HorizontalPodAutoscalerSpecApplyConfiguration) WithMetrics(values ...v2.MetricSpec) *HorizontalPodAutoscalerSpecApplyConfiguration { + for i := range values { + b.Metrics = append(b.Metrics, values[i]) + } + return b +} + +// WithBehavior sets the Behavior field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Behavior field is set to the value of the last call. +func (b *HorizontalPodAutoscalerSpecApplyConfiguration) WithBehavior(value v2.HorizontalPodAutoscalerBehavior) *HorizontalPodAutoscalerSpecApplyConfiguration { + b.Behavior = &value + return b +} diff --git a/pkg/client/applyconfiguration/utils.go b/pkg/client/applyconfiguration/utils.go index 8ce6e050..2a089b5b 100644 --- a/pkg/client/applyconfiguration/utils.go +++ b/pkg/client/applyconfiguration/utils.go @@ -79,6 +79,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &smesapcomv1alpha1.DomainStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("GenericStatus"): return &smesapcomv1alpha1.GenericStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("HorizontalPodAutoscalerSpec"): + return &smesapcomv1alpha1.HorizontalPodAutoscalerSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("JobDetails"): return &smesapcomv1alpha1.JobDetailsApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("MetricRule"):