Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/web-hooks/internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
346 changes: 346 additions & 0 deletions crds/sme.sap.com_capapplicationversions.yaml

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion internal/controller/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
55 changes: 55 additions & 0 deletions internal/controller/reconcile-capapplicationversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions internal/controller/reconcile-capapplicationversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
)
}
86 changes: 86 additions & 0 deletions internal/controller/testdata/capapplicationversion/cav-hpa.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading