Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support patching on EnvoyProxy.spec.provider.kubernetes.envoyHpa and EnvoyProxy.spec.provider.kubernetes.envoyPDB #4910

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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 @@

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 @@

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)
}

Check warning on line 282 in api/v1alpha1/kubernetes_helpers.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/kubernetes_helpers.go#L281-L282

Added lines #L281 - L282 were not covered by tests

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)

Check warning on line 290 in api/v1alpha1/kubernetes_helpers.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/kubernetes_helpers.go#L289-L290

Added lines #L289 - L290 were not covered by tests
}
if err != nil {
return nil, fmt.Errorf("error applying merge patch: %w", err)
}

Check warning on line 294 in api/v1alpha1/kubernetes_helpers.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/kubernetes_helpers.go#L293-L294

Added lines #L293 - L294 were not covered by tests

// 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)
}

Check warning on line 300 in api/v1alpha1/kubernetes_helpers.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/kubernetes_helpers.go#L299-L300

Added lines #L299 - L300 were not covered by tests

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)
}

Check warning on line 318 in api/v1alpha1/kubernetes_helpers.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/kubernetes_helpers.go#L317-L318

Added lines #L317 - L318 were not covered by tests

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)

Check warning on line 326 in api/v1alpha1/kubernetes_helpers.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/kubernetes_helpers.go#L325-L326

Added lines #L325 - L326 were not covered by tests
}
if err != nil {
return nil, fmt.Errorf("error applying merge patch: %w", err)
}

Check warning on line 330 in api/v1alpha1/kubernetes_helpers.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/kubernetes_helpers.go#L329-L330

Added lines #L329 - L330 were not covered by tests

// 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)
}

Check warning on line 336 in api/v1alpha1/kubernetes_helpers.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/kubernetes_helpers.go#L335-L336

Added lines #L335 - L336 were not covered by tests

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 @@
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 @@
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))
}

Check warning on line 115 in api/v1alpha1/validation/envoyproxy_validate.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/validation/envoyproxy_validate.go#L114-L115

Added lines #L114 - L115 were not covered by tests
}
}
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))
}

Check warning on line 130 in api/v1alpha1/validation/envoyproxy_validate.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/validation/envoyproxy_validate.go#L129-L130

Added lines #L129 - L130 were not covered by tests
}
}
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
Loading