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 mergepatch to envoyproxy/ratelimit deployment #2374

Merged
merged 7 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Xunzhuo marked this conversation as resolved.
Show resolved Hide resolved
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
arkodg marked this conversation as resolved.
Show resolved Hide resolved
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 @@
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 @@
return errs
}

func validateDeployment(spec *egv1a1.EnvoyProxySpec) []error {
var errs []error
if spec.Provider.Kubernetes != nil && spec.Provider.Kubernetes.EnvoyDeployment != nil {
Xunzhuo marked this conversation as resolved.
Show resolved Hide resolved
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))
}

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

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/validation/envoyproxy_validate.go#L96-L97

Added lines #L96 - L97 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
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
Loading