Skip to content

Commit befdf23

Browse files
committed
fix: Use autoscalingv2.HorizontalPodAutoscaler
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent 101dd55 commit befdf23

5 files changed

Lines changed: 136 additions & 72 deletions

File tree

infra/feast-operator/config/rbac/role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ rules:
3030
- delete
3131
- get
3232
- list
33+
- patch
3334
- update
3435
- watch
3536
- apiGroups:

infra/feast-operator/dist/install.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18585,6 +18585,7 @@ rules:
1858518585
- delete
1858618586
- get
1858718587
- list
18588+
- patch
1858818589
- update
1858918590
- watch
1859018591
- apiGroups:

infra/feast-operator/internal/controller/featurestore_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ type FeatureStoreReconciler struct {
6666
// +kubebuilder:rbac:groups=authentication.k8s.io,resources=tokenreviews,verbs=create
6767
// +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;create;update;watch;delete
6868
// +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
69-
// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;delete
69+
// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete
7070

7171
// Reconcile is part of the main kubernetes reconciliation loop which aims to
7272
// move the current state of the cluster closer to the desired state.

infra/feast-operator/internal/controller/services/scaling.go

Lines changed: 91 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ limitations under the License.
1717
package services
1818

1919
import (
20+
"encoding/json"
21+
2022
feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1"
2123
appsv1 "k8s.io/api/apps/v1"
2224
autoscalingv2 "k8s.io/api/autoscaling/v2"
2325
corev1 "k8s.io/api/core/v1"
2426
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/types"
28+
hpaac "k8s.io/client-go/applyconfigurations/autoscaling/v2"
29+
metaac "k8s.io/client-go/applyconfigurations/meta/v1"
2530
"sigs.k8s.io/controller-runtime/pkg/client"
2631
"sigs.k8s.io/controller-runtime/pkg/log"
2732
)
@@ -50,8 +55,8 @@ func (feast *FeastServices) getDesiredReplicas() *int32 {
5055
}
5156

5257
// createOrDeleteHPA reconciles the HorizontalPodAutoscaler for the FeatureStore
53-
// deployment using Server-Side Apply. If autoscaling is not configured, any
54-
// existing HPA is deleted.
58+
// deployment using Server-Side Apply with typed apply configurations. If
59+
// autoscaling is not configured, any existing HPA is deleted.
5560
func (feast *FeastServices) createOrDeleteHPA() error {
5661
cr := feast.Handler.FeatureStore
5762

@@ -64,84 +69,114 @@ func (feast *FeastServices) createOrDeleteHPA() error {
6469
return feast.Handler.DeleteOwnedFeastObj(hpa)
6570
}
6671

67-
hpa := feast.buildHPA()
72+
hpaAC := feast.buildHPAApplyConfig()
73+
data, err := json.Marshal(hpaAC)
74+
if err != nil {
75+
return err
76+
}
77+
78+
hpa := &autoscalingv2.HorizontalPodAutoscaler{ObjectMeta: feast.GetObjectMeta()}
6879
logger := log.FromContext(feast.Handler.Context)
6980
if err := feast.Handler.Client.Patch(feast.Handler.Context, hpa,
70-
client.Apply, client.FieldOwner(fieldManager), client.ForceOwnership); err != nil {
81+
client.RawPatch(types.ApplyPatchType, data),
82+
client.FieldOwner(fieldManager), client.ForceOwnership); err != nil {
7183
return err
7284
}
7385
logger.Info("Successfully applied", "HorizontalPodAutoscaler", hpa.Name)
7486

7587
return nil
7688
}
7789

78-
// buildHPA constructs the fully desired HPA state for Server-Side Apply.
79-
func (feast *FeastServices) buildHPA() *autoscalingv2.HorizontalPodAutoscaler {
90+
// buildHPAApplyConfig constructs the fully desired HPA state as a typed apply
91+
// configuration for Server-Side Apply.
92+
func (feast *FeastServices) buildHPAApplyConfig() *hpaac.HorizontalPodAutoscalerApplyConfiguration {
8093
cr := feast.Handler.FeatureStore
8194
autoscaling := cr.Status.Applied.Services.Scaling.Autoscaling
82-
95+
objMeta := feast.GetObjectMeta()
8396
deploy := feast.initFeastDeploy()
97+
8498
minReplicas := defaultHPAMinReplicas
8599
if autoscaling.MinReplicas != nil {
86100
minReplicas = *autoscaling.MinReplicas
87101
}
88102

89-
metrics := defaultHPAMetrics()
103+
hpa := hpaac.HorizontalPodAutoscaler(objMeta.Name, objMeta.Namespace).
104+
WithLabels(feast.getLabels()).
105+
WithOwnerReferences(
106+
metaac.OwnerReference().
107+
WithAPIVersion(feastdevv1.GroupVersion.String()).
108+
WithKind("FeatureStore").
109+
WithName(cr.Name).
110+
WithUID(cr.UID).
111+
WithController(true).
112+
WithBlockOwnerDeletion(true),
113+
).
114+
WithSpec(hpaac.HorizontalPodAutoscalerSpec().
115+
WithScaleTargetRef(
116+
hpaac.CrossVersionObjectReference().
117+
WithAPIVersion(appsv1.SchemeGroupVersion.String()).
118+
WithKind("Deployment").
119+
WithName(deploy.Name),
120+
).
121+
WithMinReplicas(minReplicas).
122+
WithMaxReplicas(autoscaling.MaxReplicas),
123+
)
124+
90125
if len(autoscaling.Metrics) > 0 {
91-
metrics = autoscaling.Metrics
92-
}
93-
94-
isController := true
95-
hpa := &autoscalingv2.HorizontalPodAutoscaler{
96-
TypeMeta: metav1.TypeMeta{
97-
APIVersion: autoscalingv2.SchemeGroupVersion.String(),
98-
Kind: "HorizontalPodAutoscaler",
99-
},
100-
ObjectMeta: metav1.ObjectMeta{
101-
Name: feast.GetObjectMeta().Name,
102-
Namespace: feast.GetObjectMeta().Namespace,
103-
Labels: feast.getLabels(),
104-
OwnerReferences: []metav1.OwnerReference{
105-
{
106-
APIVersion: feastdevv1.GroupVersion.String(),
107-
Kind: "FeatureStore",
108-
Name: cr.Name,
109-
UID: cr.UID,
110-
Controller: &isController,
111-
BlockOwnerDeletion: &isController,
112-
},
113-
},
114-
},
115-
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
116-
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
117-
APIVersion: appsv1.SchemeGroupVersion.String(),
118-
Kind: "Deployment",
119-
Name: deploy.Name,
120-
},
121-
MinReplicas: &minReplicas,
122-
MaxReplicas: autoscaling.MaxReplicas,
123-
Metrics: metrics,
124-
Behavior: autoscaling.Behavior,
125-
},
126+
hpa.Spec.Metrics = convertMetrics(autoscaling.Metrics)
127+
} else {
128+
hpa.Spec.Metrics = defaultHPAMetrics()
129+
}
130+
131+
if autoscaling.Behavior != nil {
132+
hpa.Spec.Behavior = convertBehavior(autoscaling.Behavior)
126133
}
127134

128135
return hpa
129136
}
130137

131-
func defaultHPAMetrics() []autoscalingv2.MetricSpec {
132-
utilization := defaultHPACPUUtilization
133-
return []autoscalingv2.MetricSpec{
134-
{
135-
Type: autoscalingv2.ResourceMetricSourceType,
136-
Resource: &autoscalingv2.ResourceMetricSource{
137-
Name: corev1.ResourceCPU,
138-
Target: autoscalingv2.MetricTarget{
139-
Type: autoscalingv2.UtilizationMetricType,
140-
AverageUtilization: &utilization,
141-
},
142-
},
143-
},
138+
func defaultHPAMetrics() []hpaac.MetricSpecApplyConfiguration {
139+
return []hpaac.MetricSpecApplyConfiguration{
140+
*hpaac.MetricSpec().
141+
WithType(autoscalingv2.ResourceMetricSourceType).
142+
WithResource(
143+
hpaac.ResourceMetricSource().
144+
WithName(corev1.ResourceCPU).
145+
WithTarget(
146+
hpaac.MetricTarget().
147+
WithType(autoscalingv2.UtilizationMetricType).
148+
WithAverageUtilization(defaultHPACPUUtilization),
149+
),
150+
),
151+
}
152+
}
153+
154+
// convertMetrics converts standard API metric specs to their apply configuration
155+
// equivalents via JSON round-trip (the types share identical JSON schemas).
156+
func convertMetrics(metrics []autoscalingv2.MetricSpec) []hpaac.MetricSpecApplyConfiguration {
157+
data, err := json.Marshal(metrics)
158+
if err != nil {
159+
return nil
160+
}
161+
var result []hpaac.MetricSpecApplyConfiguration
162+
if err := json.Unmarshal(data, &result); err != nil {
163+
return nil
164+
}
165+
return result
166+
}
167+
168+
// convertBehavior converts a standard API behavior spec to its apply configuration
169+
// equivalent via JSON round-trip.
170+
func convertBehavior(behavior *autoscalingv2.HorizontalPodAutoscalerBehavior) *hpaac.HorizontalPodAutoscalerBehaviorApplyConfiguration {
171+
data, err := json.Marshal(behavior)
172+
if err != nil {
173+
return nil
174+
}
175+
result := &hpaac.HorizontalPodAutoscalerBehaviorApplyConfiguration{}
176+
if err := json.Unmarshal(data, result); err != nil {
177+
return nil
144178
}
179+
return result
145180
}
146181

147182
// updateScalingStatus updates the scaling status fields using the deployment

infra/feast-operator/internal/controller/services/scaling_test.go

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
. "github.com/onsi/gomega"
2626
appsv1 "k8s.io/api/apps/v1"
2727
autoscalingv1 "k8s.io/api/autoscaling/v1"
28+
autoscalingv2 "k8s.io/api/autoscaling/v2"
2829
corev1 "k8s.io/api/core/v1"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031
"k8s.io/apimachinery/pkg/types"
@@ -550,31 +551,31 @@ var _ = Describe("Horizontal Scaling", func() {
550551
})
551552

552553
Describe("HPA Configuration", func() {
553-
It("should build an HPA with default CPU metrics", func() {
554+
It("should build an HPA apply config with default CPU metrics", func() {
554555
featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{
555556
Autoscaling: &feastdevv1.AutoscalingConfig{
556557
MaxReplicas: 10,
557558
},
558559
}
559560

560-
hpa := feast.buildHPA()
561-
Expect(hpa.Spec.MaxReplicas).To(Equal(int32(10)))
561+
hpa := feast.buildHPAApplyConfig()
562+
Expect(*hpa.Spec.MaxReplicas).To(Equal(int32(10)))
562563
Expect(*hpa.Spec.MinReplicas).To(Equal(int32(1)))
563564
Expect(hpa.Spec.Metrics).To(HaveLen(1))
564-
Expect(hpa.Spec.Metrics[0].Resource.Name).To(Equal(corev1.ResourceCPU))
565+
Expect(*hpa.Spec.Metrics[0].Resource.Name).To(Equal(corev1.ResourceCPU))
565566
})
566567

567-
It("should build an HPA with custom min replicas", func() {
568+
It("should build an HPA apply config with custom min replicas", func() {
568569
featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{
569570
Autoscaling: &feastdevv1.AutoscalingConfig{
570571
MinReplicas: ptr(int32(2)),
571572
MaxReplicas: 10,
572573
},
573574
}
574575

575-
hpa := feast.buildHPA()
576+
hpa := feast.buildHPAApplyConfig()
576577
Expect(*hpa.Spec.MinReplicas).To(Equal(int32(2)))
577-
Expect(hpa.Spec.MaxReplicas).To(Equal(int32(10)))
578+
Expect(*hpa.Spec.MaxReplicas).To(Equal(int32(10)))
578579
})
579580

580581
It("should set correct scale target reference", func() {
@@ -584,10 +585,10 @@ var _ = Describe("Horizontal Scaling", func() {
584585
},
585586
}
586587

587-
hpa := feast.buildHPA()
588-
Expect(hpa.Spec.ScaleTargetRef.APIVersion).To(Equal("apps/v1"))
589-
Expect(hpa.Spec.ScaleTargetRef.Kind).To(Equal("Deployment"))
590-
Expect(hpa.Spec.ScaleTargetRef.Name).To(Equal(GetFeastName(featureStore)))
588+
hpa := feast.buildHPAApplyConfig()
589+
Expect(*hpa.Spec.ScaleTargetRef.APIVersion).To(Equal("apps/v1"))
590+
Expect(*hpa.Spec.ScaleTargetRef.Kind).To(Equal("Deployment"))
591+
Expect(*hpa.Spec.ScaleTargetRef.Name).To(Equal(GetFeastName(featureStore)))
591592
})
592593

593594
It("should set TypeMeta and owner reference for SSA", func() {
@@ -597,13 +598,39 @@ var _ = Describe("Horizontal Scaling", func() {
597598
},
598599
}
599600

600-
hpa := feast.buildHPA()
601-
Expect(hpa.TypeMeta.APIVersion).To(Equal("autoscaling/v2"))
602-
Expect(hpa.TypeMeta.Kind).To(Equal("HorizontalPodAutoscaler"))
601+
hpa := feast.buildHPAApplyConfig()
602+
Expect(*hpa.Kind).To(Equal("HorizontalPodAutoscaler"))
603+
Expect(*hpa.APIVersion).To(Equal("autoscaling/v2"))
603604
Expect(hpa.OwnerReferences).To(HaveLen(1))
604-
Expect(hpa.OwnerReferences[0].Name).To(Equal(featureStore.Name))
605+
Expect(*hpa.OwnerReferences[0].Name).To(Equal(featureStore.Name))
605606
Expect(*hpa.OwnerReferences[0].Controller).To(BeTrue())
606607
})
608+
609+
It("should convert custom metrics via JSON round-trip", func() {
610+
customMetrics := []autoscalingv2.MetricSpec{
611+
{
612+
Type: autoscalingv2.ResourceMetricSourceType,
613+
Resource: &autoscalingv2.ResourceMetricSource{
614+
Name: corev1.ResourceMemory,
615+
Target: autoscalingv2.MetricTarget{
616+
Type: autoscalingv2.UtilizationMetricType,
617+
AverageUtilization: ptr(int32(75)),
618+
},
619+
},
620+
},
621+
}
622+
featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{
623+
Autoscaling: &feastdevv1.AutoscalingConfig{
624+
MaxReplicas: 10,
625+
Metrics: customMetrics,
626+
},
627+
}
628+
629+
hpa := feast.buildHPAApplyConfig()
630+
Expect(hpa.Spec.Metrics).To(HaveLen(1))
631+
Expect(*hpa.Spec.Metrics[0].Resource.Name).To(Equal(corev1.ResourceMemory))
632+
Expect(*hpa.Spec.Metrics[0].Resource.Target.AverageUtilization).To(Equal(int32(75)))
633+
})
607634
})
608635

609636
Describe("Scale sub-resource", func() {

0 commit comments

Comments
 (0)