Skip to content

Commit e9d1b0a

Browse files
authored
HPA implementation (#1029)
* HPA implementation
1 parent 9e5c971 commit e9d1b0a

File tree

10 files changed

+284
-3
lines changed

10 files changed

+284
-3
lines changed

apis/operator/v1alpha1/authentication_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type AuthenticationSpec struct {
4848
ClientRegistration ClientRegistrationSpec `json:"clientRegistration"`
4949
Config ConfigSpec `json:"config"`
5050
EnableInstanaMetricCollection bool `json:"enableInstanaMetricCollection,omitempty"`
51+
AutoScaleConfig bool `json:"autoScaleConfig,omitempty"`
5152
}
5253

5354
type AuditServiceSpec struct {

bundle/manifests/ibm-iam-operator.clusterserviceversion.yaml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ metadata:
152152
categories: Security
153153
certified: "false"
154154
containerImage: icr.io/cpopen/ibm-iam-operator:4.12.0
155-
createdAt: "2025-05-15T13:06:28Z"
155+
createdAt: "2025-05-15T13:42:55Z"
156156
description: The IAM operator provides a simple Kubernetes CRD-Based API to manage the lifecycle of IAM services. With this operator, you can simply deploy and upgrade the IAM services
157157
features.operators.openshift.io/disconnected: "true"
158158
features.operators.openshift.io/fips-compliant: "true"
@@ -577,6 +577,18 @@ spec:
577577
- patch
578578
- update
579579
- watch
580+
- apiGroups:
581+
- autoscaling
582+
resources:
583+
- horizontalpodautoscalers
584+
verbs:
585+
- create
586+
- delete
587+
- get
588+
- list
589+
- patch
590+
- update
591+
- watch
580592
serviceAccountName: ibm-iam-operator
581593
strategy: deployment
582594
installModes:

bundle/manifests/operator.ibm.com_authentications.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ spec:
125125
- ldapsCACert
126126
- routerCertSecret
127127
type: object
128+
autoScaleConfig:
129+
description: AutoScaleConfig is a boolean to enable or disable HPA
130+
type: boolean
128131
clientRegistration:
129132
properties:
130133
imageName:

config/crd/bases/operator.ibm.com_authentications.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ spec:
125125
- ldapsCACert
126126
- routerCertSecret
127127
type: object
128+
autoScaleConfig:
129+
description: AutoScaleConfig is a boolean to enable or disable HPA
130+
type: boolean
128131
clientRegistration:
129132
properties:
130133
imageName:

config/rbac/role.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,18 @@ rules:
246246
- patch
247247
- update
248248
- watch
249+
- apiGroups:
250+
- autoscaling
251+
resources:
252+
- horizontalpodautoscalers
253+
verbs:
254+
- create
255+
- delete
256+
- get
257+
- list
258+
- patch
259+
- update
260+
- watch
249261
---
250262
apiVersion: rbac.authorization.k8s.io/v1
251263
kind: ClusterRole

controllers/operator/authentication_controller.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
routev1 "github.com/openshift/api/route/v1"
3131
appsv1 "k8s.io/api/apps/v1"
3232
authorizationv1 "k8s.io/api/authorization/v1"
33+
autoscalingv2 "k8s.io/api/autoscaling/v2"
3334
batchv1 "k8s.io/api/batch/v1"
3435
corev1 "k8s.io/api/core/v1"
3536
net "k8s.io/api/networking/v1"
@@ -370,6 +371,10 @@ func (r *AuthenticationReconciler) Reconcile(ctx context.Context, req ctrl.Reque
370371
return subreconciler.Evaluate(subResult, err)
371372
}
372373

374+
if subResult, err := r.handleHPAs(ctx, req); subreconciler.ShouldHaltOrRequeue(subResult, err) {
375+
return subreconciler.Evaluate(subResult, err)
376+
}
377+
373378
return subreconciler.Evaluate(subreconciler.DoNotRequeue())
374379
}
375380

@@ -383,7 +388,8 @@ func (r *AuthenticationReconciler) SetupWithManager(mgr ctrl.Manager) error {
383388
Owns(&batchv1.Job{}).
384389
Owns(&corev1.Service{}).
385390
Owns(&net.Ingress{}).
386-
Owns(&appsv1.Deployment{})
391+
Owns(&appsv1.Deployment{}).
392+
Owns(&autoscalingv2.HorizontalPodAutoscaler{})
387393

388394
//Add routes
389395
if ctrlcommon.ClusterHasOpenShiftConfigGroupVerison(&r.DiscoveryClient) {

controllers/operator/deployment.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,13 @@ func specsDiffer(observed, generated *appsv1.Deployment) (different bool, err er
778778
func modifyDeployment(needsRollout bool) ctrlcommon.ModifyFn[*appsv1.Deployment] {
779779
return func(s ctrlcommon.SecondaryReconciler, ctx context.Context, observed, generated *appsv1.Deployment) (modified bool, err error) {
780780
preserveObservedFields(observed, generated)
781-
781+
authCR, ok := s.GetPrimary().(*operatorv1alpha1.Authentication)
782+
if !ok {
783+
return
784+
}
785+
if authCR.Spec.AutoScaleConfig {
786+
generated.Spec.Replicas = observed.Spec.Replicas
787+
}
782788
if val, ok := observed.Labels["operator.ibm.com/bindinfoRefresh"]; !ok || val != "enabled" {
783789
observed.Labels["operator.ibm.com/bindinfoRefresh"] = "enabled"
784790
modified = true

controllers/operator/hpa.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
//
2+
// Copyright 2025 IBM Corporation
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package operator
17+
18+
import (
19+
"context"
20+
21+
operatorv1alpha1 "github.com/IBM/ibm-iam-operator/apis/operator/v1alpha1"
22+
"github.com/IBM/ibm-iam-operator/controllers/common"
23+
ctrlcommon "github.com/IBM/ibm-iam-operator/controllers/common"
24+
"github.com/opdev/subreconciler"
25+
appsv1 "k8s.io/api/apps/v1"
26+
autoscalingv2 "k8s.io/api/autoscaling/v2"
27+
"k8s.io/apimachinery/pkg/api/errors"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/types"
30+
ctrl "sigs.k8s.io/controller-runtime"
31+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
32+
logf "sigs.k8s.io/controller-runtime/pkg/log"
33+
)
34+
35+
func (r *AuthenticationReconciler) handleHPAs(ctx context.Context, req ctrl.Request) (result *ctrl.Result, err error) {
36+
reqLogger := logf.FromContext(ctx).WithValues("subreconciler", "handleHPAs")
37+
hpaCtx := logf.IntoContext(ctx, reqLogger)
38+
authCR := &operatorv1alpha1.Authentication{}
39+
if result, err = r.getLatestAuthentication(hpaCtx, req, authCR); subreconciler.ShouldHaltOrRequeue(result, err) {
40+
return
41+
}
42+
deployments := []string{"platform-auth-service", "platform-identity-provider", "platform-identity-management"}
43+
44+
authCRNS := authCR.Namespace
45+
autoScaleEnabled := authCR.Spec.AutoScaleConfig
46+
replicas := authCR.Spec.Replicas
47+
if autoScaleEnabled {
48+
subRecs := []ctrlcommon.SecondaryReconciler{}
49+
results := []*ctrl.Result{}
50+
errs := []error{}
51+
52+
for _, deployName := range deployments {
53+
// Build the HPA reconciler
54+
builder := ctrlcommon.NewSecondaryReconcilerBuilder[*autoscalingv2.HorizontalPodAutoscaler]().
55+
WithName(deployName + "-hpa").
56+
WithGenerateFns(generateHPAObject(authCR, deployName))
57+
58+
subRecs = append(subRecs, builder.
59+
WithNamespace(authCRNS).
60+
WithPrimary(authCR).
61+
WithClient(r.Client).
62+
MustBuild())
63+
}
64+
65+
for _, subRec := range subRecs {
66+
subResult, subErr := subRec.Reconcile(hpaCtx)
67+
results = append(results, subResult)
68+
errs = append(errs, subErr)
69+
}
70+
71+
result, err = common.ReduceSubreconcilerResultsAndErrors(results, errs)
72+
if err == nil {
73+
r.needsRollout = false
74+
}
75+
if subreconciler.ShouldRequeue(result, err) {
76+
reqLogger.Info("Cluster state has been modified; requeueing")
77+
return
78+
}
79+
return subreconciler.ContinueReconciling()
80+
} else {
81+
// HPA is disabled - delete any existing HPA and set fixed replicas
82+
for _, deployName := range deployments {
83+
hpa := &autoscalingv2.HorizontalPodAutoscaler{
84+
ObjectMeta: metav1.ObjectMeta{
85+
Name: deployName + "-hpa",
86+
Namespace: authCRNS,
87+
},
88+
}
89+
err := r.Get(ctx, types.NamespacedName{Name: deployName + "-hpa", Namespace: authCRNS}, hpa)
90+
if err == nil {
91+
reqLogger.Info("HPA is disabled, Deleting existing HPAs")
92+
if err := r.Delete(ctx, hpa); err == nil {
93+
r.UpdateDeploymentReplicas(hpaCtx, deployName, authCRNS, replicas)
94+
}
95+
} else if !errors.IsNotFound(err) {
96+
return subreconciler.RequeueWithDelay(defaultLowerWait)
97+
}
98+
}
99+
}
100+
return
101+
}
102+
103+
func generateHPAObject(instance *operatorv1alpha1.Authentication, deploymentName string) ctrlcommon.GenerateFn[*autoscalingv2.HorizontalPodAutoscaler] {
104+
return func(s ctrlcommon.SecondaryReconciler, ctx context.Context, hpa *autoscalingv2.HorizontalPodAutoscaler) (err error) {
105+
106+
// Fetch Deployment
107+
reqLogger := logf.FromContext(ctx)
108+
deploy := &appsv1.Deployment{}
109+
minReplicas := instance.Spec.Replicas
110+
err = s.GetClient().Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: instance.Namespace}, deploy)
111+
if err != nil {
112+
reqLogger.Error(err, "Failed to fetch Deployment", "DeploymentName", deploymentName)
113+
return
114+
}
115+
// Extract memory and CPU requests and limits
116+
if len(deploy.Spec.Template.Spec.Containers) == 0 {
117+
reqLogger.Error(err, "Deployment has no containers")
118+
return
119+
}
120+
121+
container := deploy.Spec.Template.Spec.Containers[0]
122+
currentReplicas := deploy.Spec.Replicas
123+
maxReplicas := 2*(*currentReplicas) + 1
124+
125+
memRequest := container.Resources.Requests.Memory().Value()
126+
memLimit := container.Resources.Limits.Memory().Value()
127+
cpuRequest := container.Resources.Requests.Cpu().MilliValue()
128+
cpuLimit := container.Resources.Limits.Cpu().MilliValue()
129+
130+
// Compute average utilization
131+
avgUtilMem := calculateUtilization(memRequest, memLimit)
132+
avgUtilCPU := calculateUtilization(cpuRequest, cpuLimit)
133+
134+
// Define HPA
135+
*hpa = autoscalingv2.HorizontalPodAutoscaler{
136+
ObjectMeta: metav1.ObjectMeta{
137+
Name: deploymentName + "-hpa",
138+
Namespace: instance.Namespace,
139+
Labels: instance.Labels,
140+
OwnerReferences: []metav1.OwnerReference{
141+
{
142+
APIVersion: instance.APIVersion,
143+
Kind: instance.Kind,
144+
Name: instance.Name,
145+
UID: instance.UID,
146+
},
147+
},
148+
},
149+
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
150+
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
151+
APIVersion: "apps/v1",
152+
Kind: "Deployment",
153+
Name: deploymentName,
154+
},
155+
MinReplicas: &minReplicas,
156+
MaxReplicas: maxReplicas,
157+
Metrics: []autoscalingv2.MetricSpec{
158+
{
159+
Type: autoscalingv2.ResourceMetricSourceType,
160+
Resource: &autoscalingv2.ResourceMetricSource{
161+
Name: "memory",
162+
Target: autoscalingv2.MetricTarget{
163+
Type: autoscalingv2.UtilizationMetricType,
164+
AverageUtilization: &avgUtilMem,
165+
},
166+
},
167+
},
168+
{
169+
Type: autoscalingv2.ResourceMetricSourceType,
170+
Resource: &autoscalingv2.ResourceMetricSource{
171+
Name: "cpu",
172+
Target: autoscalingv2.MetricTarget{
173+
Type: autoscalingv2.UtilizationMetricType,
174+
AverageUtilization: &avgUtilCPU,
175+
},
176+
},
177+
},
178+
},
179+
},
180+
}
181+
182+
// Set owner reference for garbage collection
183+
err = controllerutil.SetControllerReference(instance, hpa, s.GetClient().Scheme())
184+
if err != nil {
185+
reqLogger.Error(err, "Failed to set owner reference for HPA")
186+
return
187+
}
188+
189+
return nil
190+
}
191+
}
192+
193+
func calculateUtilization(request int64, limit int64) int32 {
194+
utilizationRatio := (float64(limit) / float64(request)) * 100
195+
196+
if utilizationRatio < 130 {
197+
return 90
198+
}
199+
return int32((float64(limit) * 0.7 / float64(request)) * 100)
200+
}
201+
202+
func (r *AuthenticationReconciler) UpdateDeploymentReplicas(ctx context.Context, deploymentName, namespace string, fixedReplicas int32) error {
203+
204+
reqLogger := logf.FromContext(ctx)
205+
deploy := &appsv1.Deployment{}
206+
err := r.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, deploy)
207+
if err != nil {
208+
return err
209+
}
210+
// Update only if the existing replica count is different
211+
if deploy.Spec.Replicas == nil || *deploy.Spec.Replicas != fixedReplicas {
212+
replicas := fixedReplicas
213+
deploy.Spec.Replicas = &replicas
214+
if err := r.Update(ctx, deploy); err != nil {
215+
reqLogger.Error(err, "failed to update deployment replicas", "deployment", deploy.Name, "desiredReplicas", replicas)
216+
return err
217+
}
218+
} else {
219+
reqLogger.Info("Deployment already has the correct replicas")
220+
}
221+
222+
return nil
223+
}

helm-cluster-scoped/templates/00_operator.ibm.com_authentications.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ spec:
126126
- ldapsCACert
127127
- routerCertSecret
128128
type: object
129+
autoScaleConfig:
130+
description: AutoScaleConfig is a boolean to enable or disable HPA
131+
type: boolean
129132
clientRegistration:
130133
properties:
131134
imageName:

helm/templates/00-rbac.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,18 @@ rules:
272272
- patch
273273
- update
274274
- watch
275+
- apiGroups:
276+
- autoscaling
277+
resources:
278+
- horizontalpodautoscalers
279+
verbs:
280+
- create
281+
- delete
282+
- get
283+
- list
284+
- patch
285+
- update
286+
- watch
275287
---
276288
apiVersion: rbac.authorization.k8s.io/v1
277289
kind: RoleBinding

0 commit comments

Comments
 (0)