Skip to content

Commit

Permalink
feat: support patching on EnvoyProxy.spec.provider.kubernetes.envoyHp…
Browse files Browse the repository at this point in the history
…a and EnvoyProxy.spec.provider.kubernetes.envoyPDB (envoyproxy#4910)

* Add patch field for envoyHPA and envoyPDB in EnvoyGateway API

Signed-off-by: keithfz <[email protected]>

* It's actually currently the envoyPDB field, not envoyPdb

Signed-off-by: keithfz <[email protected]>

* fix comment

Signed-off-by: keithfz <[email protected]>

* fix error messages

Signed-off-by: keithfz <[email protected]>

* Add validation for hpa and pdb

Signed-off-by: keithfz <[email protected]>

* lint and gen-check

Signed-off-by: keithfz <[email protected]>

* adding test coverage

Signed-off-by: keithfz <[email protected]>

* lint

Signed-off-by: keithfz <[email protected]>

---------

Signed-off-by: keithfz <[email protected]>
  • Loading branch information
keithfz authored Dec 14, 2024
1 parent 920a13c commit 172cbb2
Show file tree
Hide file tree
Showing 15 changed files with 497 additions and 6 deletions.
74 changes: 74 additions & 0 deletions api/v1alpha1/kubernetes_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (

jsonpatch "github.com/evanphx/json-patch"
appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/utils/ptr"
Expand Down Expand Up @@ -263,3 +265,75 @@ func (service *KubernetesServiceSpec) ApplyMergePatch(old *corev1.Service) (*cor

return &patchedService, nil
}

// ApplyMergePatch applies a merge patch to a HorizontalPodAutoscaler based on the merge type
func (hpa *KubernetesHorizontalPodAutoscalerSpec) ApplyMergePatch(old *autoscalingv2.HorizontalPodAutoscaler) (*autoscalingv2.HorizontalPodAutoscaler, error) {
if hpa.Patch == nil {
return old, nil
}

var patchedJSON []byte
var err error

// Serialize the current HPA to JSON
originalJSON, err := json.Marshal(old)
if err != nil {
return nil, fmt.Errorf("error marshaling original HorizontalPodAutoscaler: %w", err)
}

switch {
case hpa.Patch.Type == nil || *hpa.Patch.Type == StrategicMerge:
patchedJSON, err = strategicpatch.StrategicMergePatch(originalJSON, hpa.Patch.Value.Raw, autoscalingv2.HorizontalPodAutoscaler{})
case *hpa.Patch.Type == JSONMerge:
patchedJSON, err = jsonpatch.MergePatch(originalJSON, hpa.Patch.Value.Raw)
default:
return nil, fmt.Errorf("unsupported merge type: %s", *hpa.Patch.Type)
}
if err != nil {
return nil, fmt.Errorf("error applying merge patch: %w", err)
}

// Deserialize the patched JSON into a new HorizontalPodAutoscaler object
var patchedHpa autoscalingv2.HorizontalPodAutoscaler
if err := json.Unmarshal(patchedJSON, &patchedHpa); err != nil {
return nil, fmt.Errorf("error unmarshaling patched HorizontalPodAutoscaler: %w", err)
}

return &patchedHpa, nil
}

// ApplyMergePatch applies a merge patch to a PodDisruptionBudget based on the merge type
func (pdb *KubernetesPodDisruptionBudgetSpec) ApplyMergePatch(old *policyv1.PodDisruptionBudget) (*policyv1.PodDisruptionBudget, error) {
if pdb.Patch == nil {
return old, nil
}

var patchedJSON []byte
var err error

// Serialize the PDB deployment to JSON
originalJSON, err := json.Marshal(old)
if err != nil {
return nil, fmt.Errorf("error marshaling original PodDisruptionBudget: %w", err)
}

switch {
case pdb.Patch.Type == nil || *pdb.Patch.Type == StrategicMerge:
patchedJSON, err = strategicpatch.StrategicMergePatch(originalJSON, pdb.Patch.Value.Raw, policyv1.PodDisruptionBudget{})
case *pdb.Patch.Type == JSONMerge:
patchedJSON, err = jsonpatch.MergePatch(originalJSON, pdb.Patch.Value.Raw)
default:
return nil, fmt.Errorf("unsupported merge type: %s", *pdb.Patch.Type)
}
if err != nil {
return nil, fmt.Errorf("error applying merge patch: %w", err)
}

// Deserialize the patched JSON into a new HorizontalPodAutoscaler object
var patchedPdb policyv1.PodDisruptionBudget
if err := json.Unmarshal(patchedJSON, &patchedPdb); err != nil {
return nil, fmt.Errorf("error unmarshaling patched PodDisruptionBudget: %w", err)
}

return &patchedPdb, nil
}
10 changes: 10 additions & 0 deletions api/v1alpha1/shared_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,11 @@ type KubernetesPodDisruptionBudgetSpec struct {
// and resilience during maintenance operations.
// +optional
MinAvailable *int32 `json:"minAvailable,omitempty"`

// Patch defines how to perform the patch operation to the PodDisruptionBudget
//
// +optional
Patch *KubernetesPatchSpec `json:"patch,omitempty"`
}

// KubernetesHorizontalPodAutoscalerSpec defines Kubernetes Horizontal Pod Autoscaler settings of Envoy Proxy Deployment.
Expand Down Expand Up @@ -443,6 +448,11 @@ type KubernetesHorizontalPodAutoscalerSpec struct {
//
// +optional
Behavior *autoscalingv2.HorizontalPodAutoscalerBehavior `json:"behavior,omitempty"`

// Patch defines how to perform the patch operation to the HorizontalPodAutoscaler
//
// +optional
Patch *KubernetesPatchSpec `json:"patch,omitempty"`
}

// HTTPStatus defines the http status code.
Expand Down
38 changes: 38 additions & 0 deletions api/v1alpha1/validation/envoyproxy_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ func validateProvider(spec *egv1a1.EnvoyProxySpec) []error {
if len(validateDeploymentErrs) != 0 {
errs = append(errs, validateDeploymentErrs...)
}
validateHpaErrors := validateHpa(spec)
if len(validateHpaErrors) != 0 {
errs = append(errs, validateHpaErrors...)
}
validatePdbErrors := validatePdb(spec)
if len(validatePdbErrors) != 0 {
errs = append(errs, validatePdbErrors...)
}
validateServiceErrs := validateService(spec)
if len(validateServiceErrs) != 0 {
errs = append(errs, validateServiceErrs...)
Expand All @@ -95,6 +103,36 @@ func validateDeployment(spec *egv1a1.EnvoyProxySpec) []error {
return errs
}

func validateHpa(spec *egv1a1.EnvoyProxySpec) []error {
var errs []error
if spec.Provider.Kubernetes != nil && spec.Provider.Kubernetes.EnvoyHpa != nil {
if patch := spec.Provider.Kubernetes.EnvoyHpa.Patch; patch != nil {
if patch.Value.Raw == nil {
errs = append(errs, fmt.Errorf("envoy hpa patch object cannot be empty"))
}
if patch.Type != nil && *patch.Type != egv1a1.JSONMerge && *patch.Type != egv1a1.StrategicMerge {
errs = append(errs, fmt.Errorf("unsupported envoy hpa patch type %s", *patch.Type))
}
}
}
return errs
}

func validatePdb(spec *egv1a1.EnvoyProxySpec) []error {
var errs []error
if spec.Provider.Kubernetes != nil && spec.Provider.Kubernetes.EnvoyPDB != nil {
if patch := spec.Provider.Kubernetes.EnvoyPDB.Patch; patch != nil {
if patch.Value.Raw == nil {
errs = append(errs, fmt.Errorf("envoy pdb patch object cannot be empty"))
}
if patch.Type != nil && *patch.Type != egv1a1.JSONMerge && *patch.Type != egv1a1.StrategicMerge {
errs = append(errs, fmt.Errorf("unsupported envoy pdb patch type %s", *patch.Type))
}
}
}
return errs
}

// TODO: remove this function if CEL validation became stable
func validateService(spec *egv1a1.EnvoyProxySpec) []error {
var errs []error
Expand Down
186 changes: 186 additions & 0 deletions api/v1alpha1/validation/envoyproxy_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,192 @@ func TestValidateEnvoyProxy(t *testing.T) {
},
expected: true,
},
{
name: "should be valid when pdb patch type and patch are empty",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyPDB: &egv1a1.KubernetesPodDisruptionBudgetSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Value: apiextensionsv1.JSON{
Raw: []byte{},
},
},
},
},
},
},
},
expected: true,
},
{
name: "should be valid when pdb patch and type are set",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyPDB: &egv1a1.KubernetesPodDisruptionBudgetSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Type: ptr.To(egv1a1.StrategicMerge),
Value: apiextensionsv1.JSON{
Raw: []byte("{}"),
},
},
},
},
},
},
},
expected: true,
},
{
name: "should be invalid when pdb patch not set",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyPDB: &egv1a1.KubernetesPodDisruptionBudgetSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Type: ptr.To(egv1a1.StrategicMerge),
},
},
},
},
},
},
expected: false,
},
{
name: "should be invalid when pdb type not set",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyPDB: &egv1a1.KubernetesPodDisruptionBudgetSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Type: ptr.To(egv1a1.StrategicMerge),
},
},
},
},
},
},
expected: false,
},
{
name: "should be valid when hpa patch and type are empty",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyHpa: &egv1a1.KubernetesHorizontalPodAutoscalerSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Value: apiextensionsv1.JSON{
Raw: []byte{},
},
},
},
},
},
},
},
expected: true,
},
{
name: "should be valid when hpa patch and type are set",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyHpa: &egv1a1.KubernetesHorizontalPodAutoscalerSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Type: ptr.To(egv1a1.StrategicMerge),
Value: apiextensionsv1.JSON{
Raw: []byte("{}"),
},
},
},
},
},
},
},
expected: true,
},
{
name: "should be invalid when hpa patch not set",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyHpa: &egv1a1.KubernetesHorizontalPodAutoscalerSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Type: ptr.To(egv1a1.StrategicMerge),
},
},
},
},
},
},
expected: false,
},
{
name: "should be invalid when hpa type not set",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyHpa: &egv1a1.KubernetesHorizontalPodAutoscalerSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Type: ptr.To(egv1a1.StrategicMerge),
},
},
},
},
},
},
expected: false,
},
{
name: "should invalid when patch object is empty",
proxy: &egv1a1.EnvoyProxy{
Expand Down
10 changes: 10 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 172cbb2

Please sign in to comment.