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"):