Skip to content

Commit

Permalink
feat: support mergepatch to envoyproxy/ratelimit deployment (envoypro…
Browse files Browse the repository at this point in the history
…xy#2374)

* resolve conflicts

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

* update

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

* update: change apiextensionsv1.JSON

Signed-off-by: bitliu <[email protected]>
  • Loading branch information
Xunzhuo authored Feb 23, 2024
1 parent 620053e commit b94211c
Show file tree
Hide file tree
Showing 15 changed files with 713 additions and 1 deletion.
41 changes: 41 additions & 0 deletions api/v1alpha1/kubernetes_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
package v1alpha1

import (
"encoding/json"
"fmt"

jsonpatch "github.com/evanphx/json-patch"
appv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/utils/ptr"
)

Expand Down Expand Up @@ -121,3 +126,39 @@ func (hpa *KubernetesHorizontalPodAutoscalerSpec) setDefault() {
hpa.Metrics = DefaultEnvoyProxyHpaMetrics()
}
}

// ApplyMergePatch applies a merge patch to a deployment based on the merge type
func (deployment *KubernetesDeploymentSpec) ApplyMergePatch(old *appv1.Deployment) (*appv1.Deployment, error) {
if deployment.Patch == nil {
return old, nil
}

var patchedJSON []byte
var err error

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

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

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

return &patchedDeployment, nil
}
28 changes: 28 additions & 0 deletions api/v1alpha1/shared_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
appv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)

const (
Expand Down Expand Up @@ -52,6 +53,11 @@ const (

// KubernetesDeploymentSpec defines the desired state of the Kubernetes deployment resource.
type KubernetesDeploymentSpec struct {
// Patch defines how to perform the patch operation to deployment
//
// +optional
Patch *KubernetesPatchSpec `json:"patch,omitempty"`

// Replicas is the number of desired pods. Defaults to 1.
//
// +optional
Expand Down Expand Up @@ -370,3 +376,25 @@ type KubernetesHorizontalPodAutoscalerSpec struct {
// +kubebuilder:validation:Maximum=600
// +kubebuilder:validation:ExclusiveMaximum=true
type HTTPStatus int

// MergeType defines the type of merge operation
type MergeType string

const (
// StrategicMerge indicates a strategic merge patch type
StrategicMerge MergeType = "StrategicMerge"
// JSONMerge indicates a JSON merge patch type
JSONMerge MergeType = "JSONMerge"
)

// KubernetesPatchSpec defines how to perform the patch operation
type KubernetesPatchSpec struct {
// Type is the type of merge operation to perform
//
// By default, StrategicMerge is used as the patch type.
// +optional
Type *MergeType `json:"type,omitempty"`

// Object contains the raw configuration for merged object
Value apiextensionsv1.JSON `json:"value"`
}
19 changes: 19 additions & 0 deletions api/v1alpha1/validation/envoyproxy_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ func validateProvider(spec *egv1a1.EnvoyProxySpec) []error {
if spec.Provider.Type != egv1a1.ProviderTypeKubernetes {
errs = append(errs, fmt.Errorf("unsupported provider type %v", spec.Provider.Type))
}
validateDeploymentErrs := validateDeployment(spec)
if len(validateDeploymentErrs) != 0 {
errs = append(errs, validateDeploymentErrs...)
}
validateServiceErrs := validateService(spec)
if len(validateServiceErrs) != 0 {
errs = append(errs, validateServiceErrs...)
Expand All @@ -81,6 +85,21 @@ func validateProvider(spec *egv1a1.EnvoyProxySpec) []error {
return errs
}

func validateDeployment(spec *egv1a1.EnvoyProxySpec) []error {
var errs []error
if spec.Provider.Kubernetes != nil && spec.Provider.Kubernetes.EnvoyDeployment != nil {
if patch := spec.Provider.Kubernetes.EnvoyDeployment.Patch; patch != nil {
if patch.Value.Raw == nil {
errs = append(errs, fmt.Errorf("envoy deployment patch object cannot be empty"))
}
if patch.Type != nil && *patch.Type != egv1a1.JSONMerge && *patch.Type != egv1a1.StrategicMerge {
errs = append(errs, fmt.Errorf("unsupported envoy deployment 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
92 changes: 92 additions & 0 deletions api/v1alpha1/validation/envoyproxy_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"

Expand Down Expand Up @@ -447,6 +448,97 @@ func TestValidateEnvoyProxy(t *testing.T) {
},
},
expected: true,
}, {
name: "should invalid when patch type is empty",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyDeployment: &egv1a1.KubernetesDeploymentSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Value: v1.JSON{
Raw: []byte{},
},
},
},
},
},
},
},
expected: true,
}, {
name: "should invalid when patch object is empty",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyDeployment: &egv1a1.KubernetesDeploymentSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Type: ptr.To(egv1a1.StrategicMerge),
},
},
},
},
},
},
expected: false,
}, {
name: "should valid when patch type and object are both not empty",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyDeployment: &egv1a1.KubernetesDeploymentSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Type: ptr.To(egv1a1.StrategicMerge),
Value: v1.JSON{
Raw: []byte("{}"),
},
},
},
},
},
},
},
expected: true,
}, {
name: "should valid when patch type is empty and object is not empty",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.ProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyDeployment: &egv1a1.KubernetesDeploymentSpec{
Patch: &egv1a1.KubernetesPatchSpec{
Value: v1.JSON{
Raw: []byte("{}"),
},
},
},
},
},
},
},
expected: true,
},
}

Expand Down
26 changes: 26 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.

Original file line number Diff line number Diff line change
Expand Up @@ -2012,6 +2012,22 @@ spec:
- name
type: object
type: array
patch:
description: Patch defines how to perform the patch operation
to deployment
properties:
type:
description: "Type is the type of merge operation
to perform \n By default, StrategicMerge is used
as the patch type."
type: string
value:
description: Object contains the raw configuration
for merged object
x-kubernetes-preserve-unknown-fields: true
required:
- value
type: object
pod:
description: Pod defines the desired specification of
pod.
Expand Down
Loading

0 comments on commit b94211c

Please sign in to comment.