diff --git a/api/v1alpha1/kubernetes_helpers.go b/api/v1alpha1/kubernetes_helpers.go index 3dc553dea33..cd9ec84257c 100644 --- a/api/v1alpha1/kubernetes_helpers.go +++ b/api/v1alpha1/kubernetes_helpers.go @@ -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" ) @@ -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 +} diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go index 970c62ba6a1..23cc2a1ef09 100644 --- a/api/v1alpha1/shared_types.go +++ b/api/v1alpha1/shared_types.go @@ -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 ( @@ -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 @@ -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"` +} diff --git a/api/v1alpha1/validation/envoyproxy_validate.go b/api/v1alpha1/validation/envoyproxy_validate.go index a61973dcaaf..6dc7e116c89 100644 --- a/api/v1alpha1/validation/envoyproxy_validate.go +++ b/api/v1alpha1/validation/envoyproxy_validate.go @@ -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...) @@ -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 diff --git a/api/v1alpha1/validation/envoyproxy_validate_test.go b/api/v1alpha1/validation/envoyproxy_validate_test.go index f13e8a921fb..f476a5767af 100644 --- a/api/v1alpha1/validation/envoyproxy_validate_test.go +++ b/api/v1alpha1/validation/envoyproxy_validate_test.go @@ -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" @@ -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, }, } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 750a39b5915..ad649b03118 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2110,6 +2110,11 @@ func (in *KubernetesDeployMode) DeepCopy() *KubernetesDeployMode { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubernetesDeploymentSpec) DeepCopyInto(out *KubernetesDeploymentSpec) { *out = *in + if in.Patch != nil { + in, out := &in.Patch, &out.Patch + *out = new(KubernetesPatchSpec) + (*in).DeepCopyInto(*out) + } if in.Replicas != nil { in, out := &in.Replicas, &out.Replicas *out = new(int32) @@ -2186,6 +2191,27 @@ func (in *KubernetesHorizontalPodAutoscalerSpec) DeepCopy() *KubernetesHorizonta return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesPatchSpec) DeepCopyInto(out *KubernetesPatchSpec) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(MergeType) + **out = **in + } + in.Value.DeepCopyInto(&out.Value) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesPatchSpec. +func (in *KubernetesPatchSpec) DeepCopy() *KubernetesPatchSpec { + if in == nil { + return nil + } + out := new(KubernetesPatchSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubernetesPodSpec) DeepCopyInto(out *KubernetesPodSpec) { *out = *in diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index abb86ce60dd..5a0e0305866 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -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. diff --git a/examples/kubernetes/mergepatch.yaml b/examples/kubernetes/mergepatch.yaml new file mode 100644 index 00000000000..87b34eb4b16 --- /dev/null +++ b/examples/kubernetes/mergepatch.yaml @@ -0,0 +1,115 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: mergepatch + namespace: envoy-gateway-system +spec: + provider: + type: Kubernetes + kubernetes: + envoyDeployment: + merge: + type: StrategicMerge + object: + spec: + template: + spec: + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: eg +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: mergepatch + namespace: envoy-gateway-system +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: eg +spec: + gatewayClassName: eg + listeners: + - name: http + protocol: HTTP + port: 80 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: backend +--- +apiVersion: v1 +kind: Service +metadata: + name: backend + labels: + app: backend + service: backend +spec: + ports: + - name: http + port: 3000 + targetPort: 3000 + selector: + app: backend +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend +spec: + replicas: 1 + selector: + matchLabels: + app: backend + version: v1 + template: + metadata: + labels: + app: backend + version: v1 + spec: + serviceAccountName: backend + containers: + - image: gcr.io/k8s-staging-ingressconformance/echoserver:v20221109-7ee2f3e + imagePullPolicy: IfNotPresent + name: backend + ports: + - containerPort: 3000 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: backend +spec: + parentRefs: + - name: eg + hostnames: + - "www.example.com" + rules: + - backendRefs: + - group: "" + kind: Service + name: backend + port: 3000 + weight: 1 + matches: + - path: + type: PathPrefix + value: / diff --git a/go.mod b/go.mod index 03c3b870e4f..ac5e67758fe 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/chai2010/gettext-go v1.0.2 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect - github.com/evanphx/json-patch v5.7.0+incompatible // indirect + github.com/evanphx/json-patch v5.7.0+incompatible github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect diff --git a/internal/infrastructure/kubernetes/proxy/resource_provider.go b/internal/infrastructure/kubernetes/proxy/resource_provider.go index 7ed28fb75b8..725a1202b45 100644 --- a/internal/infrastructure/kubernetes/proxy/resource_provider.go +++ b/internal/infrastructure/kubernetes/proxy/resource_provider.go @@ -265,6 +265,11 @@ func (r *ResourceRender) Deployment() (*appsv1.Deployment, error) { deployment.Spec.Replicas = nil } + // apply merge patch to deployment + if merged, err := deploymentConfig.ApplyMergePatch(deployment); err == nil { + deployment = merged + } + return deployment, nil } diff --git a/internal/infrastructure/kubernetes/proxy/resource_provider_test.go b/internal/infrastructure/kubernetes/proxy/resource_provider_test.go index 8a41fba535c..686e5dd3d0a 100644 --- a/internal/infrastructure/kubernetes/proxy/resource_provider_test.go +++ b/internal/infrastructure/kubernetes/proxy/resource_provider_test.go @@ -16,6 +16,7 @@ import ( appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -133,6 +134,18 @@ func TestDeployment(t *testing.T) { }, }, }, + { + caseName: "patch-deployment", + infra: newTestInfra(), + deploy: &egv1a1.KubernetesDeploymentSpec{ + Patch: &egv1a1.KubernetesPatchSpec{ + Type: ptr.To(egv1a1.StrategicMerge), + Value: v1.JSON{ + Raw: []byte("{\"spec\":{\"template\":{\"spec\":{\"hostNetwork\":true,\"dnsPolicy\":\"ClusterFirstWithHostNet\"}}}}"), + }, + }, + }, + }, { caseName: "bootstrap", infra: newTestInfra(), diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/patch-deployment.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/patch-deployment.yaml new file mode 100644 index 00000000000..e4c9a822c87 --- /dev/null +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/patch-deployment.yaml @@ -0,0 +1,207 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/name: envoy + app.kubernetes.io/component: proxy + app.kubernetes.io/managed-by: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: default + gateway.envoyproxy.io/owning-gateway-namespace: default + name: envoy-default-37a8eec1 + namespace: envoy-gateway-system +spec: + replicas: 1 + strategy: + type: RollingUpdate + selector: + matchLabels: + app.kubernetes.io/name: envoy + app.kubernetes.io/component: proxy + app.kubernetes.io/managed-by: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: default + gateway.envoyproxy.io/owning-gateway-namespace: default + template: + metadata: + labels: + app.kubernetes.io/name: envoy + app.kubernetes.io/component: proxy + app.kubernetes.io/managed-by: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: default + gateway.envoyproxy.io/owning-gateway-namespace: default + spec: + automountServiceAccountToken: false + containers: + - args: + - --service-cluster default + - --service-node $(ENVOY_POD_NAME) + - | + --config-yaml admin: + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/null + address: + socket_address: + address: 127.0.0.1 + port_value: 19000 + layered_runtime: + layers: + - name: global_config + static_layer: + envoy.restart_features.use_eds_cache_for_ads: true + re2.max_program_size.error_level: 4294967295 + re2.max_program_size.warn_level: 1000 + dynamic_resources: + ads_config: + api_type: DELTA_GRPC + transport_api_version: V3 + grpc_services: + - envoy_grpc: + cluster_name: xds_cluster + set_node_on_first_message_only: true + lds_config: + ads: {} + resource_api_version: V3 + cds_config: + ads: {} + resource_api_version: V3 + static_resources: + listeners: + - name: envoy-gateway-proxy-ready-0.0.0.0-19001 + address: + socket_address: + address: 0.0.0.0 + port_value: 19001 + protocol: TCP + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: eg-ready-http + route_config: + name: local_route + http_filters: + - name: envoy.filters.http.health_check + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.health_check.v3.HealthCheck + pass_through_mode: false + headers: + - name: ":path" + string_match: + exact: /ready + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + clusters: + - connect_timeout: 10s + load_assignment: + cluster_name: xds_cluster + endpoints: + - load_balancing_weight: 1 + lb_endpoints: + - load_balancing_weight: 1 + endpoint: + address: + socket_address: + address: envoy-gateway + port_value: 18000 + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions" + explicit_http_config: + http2_protocol_options: + connection_keepalive: + interval: 30s + timeout: 5s + name: xds_cluster + type: STRICT_DNS + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + tls_params: + tls_maximum_protocol_version: TLSv1_3 + tls_certificate_sds_secret_configs: + - name: xds_certificate + sds_config: + path_config_source: + path: "/sds/xds-certificate.json" + resource_api_version: V3 + validation_context_sds_secret_config: + name: xds_trusted_ca + sds_config: + path_config_source: + path: "/sds/xds-trusted-ca.json" + resource_api_version: V3 + - --log-level warn + - --cpuset-threads + command: + - envoy + env: + - name: ENVOY_GATEWAY_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: ENVOY_POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + image: envoyproxy/envoy:distroless-dev + imagePullPolicy: IfNotPresent + name: envoy + ports: + - containerPort: 8080 + name: EnvoyH-d76a15e2 + protocol: TCP + - containerPort: 8443 + name: EnvoyH-6658f727 + protocol: TCP + resources: + requests: + cpu: 100m + memory: 512Mi + readinessProbe: + httpGet: + path: /ready + port: 19001 + scheme: HTTP + timeoutSeconds: 1 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /certs + name: certs + readOnly: true + - mountPath: /sds + name: sds + dnsPolicy: ClusterFirstWithHostNet + hostNetwork: true + restartPolicy: Always + schedulerName: default-scheduler + serviceAccountName: envoy-default-37a8eec1 + terminationGracePeriodSeconds: 300 + volumes: + - name: certs + secret: + secretName: envoy + defaultMode: 420 + - configMap: + defaultMode: 420 + items: + - key: xds-trusted-ca.json + path: xds-trusted-ca.json + - key: xds-certificate.json + path: xds-certificate.json + name: envoy-default-37a8eec1 + optional: false + name: sds + revisionHistoryLimit: 10 + progressDeadlineSeconds: 600 diff --git a/internal/infrastructure/kubernetes/ratelimit/resource_provider.go b/internal/infrastructure/kubernetes/ratelimit/resource_provider.go index 6549a430412..5e4a2a2639b 100644 --- a/internal/infrastructure/kubernetes/ratelimit/resource_provider.go +++ b/internal/infrastructure/kubernetes/ratelimit/resource_provider.go @@ -205,6 +205,11 @@ func (r *ResourceRender) Deployment() (*appsv1.Deployment, error) { } } + // apply merge patch to deployment + if merged, err := r.rateLimitDeployment.ApplyMergePatch(deployment); err == nil { + deployment = merged + } + return deployment, nil } diff --git a/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go b/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go index b6dc4f71c51..13621851a32 100644 --- a/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go +++ b/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -146,6 +147,18 @@ func TestDeployment(t *testing.T) { rateLimit: rateLimit, deploy: cfg.EnvoyGateway.GetEnvoyGatewayProvider().GetEnvoyGatewayKubeProvider().RateLimitDeployment, }, + { + caseName: "patch-deployment", + rateLimit: rateLimit, + deploy: &egv1a1.KubernetesDeploymentSpec{ + Patch: &egv1a1.KubernetesPatchSpec{ + Type: ptr.To(egv1a1.StrategicMerge), + Value: v1.JSON{ + Raw: []byte("{\"spec\":{\"template\":{\"spec\":{\"hostNetwork\":true,\"dnsPolicy\":\"ClusterFirstWithHostNet\"}}}}"), + }, + }, + }, + }, { caseName: "custom", rateLimit: rateLimit, diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/patch-deployment.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/patch-deployment.yaml new file mode 100644 index 00000000000..c9e915ef9b0 --- /dev/null +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/patch-deployment.yaml @@ -0,0 +1,114 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/name: envoy-ratelimit + app.kubernetes.io/component: ratelimit + app.kubernetes.io/managed-by: envoy-gateway + name: envoy-ratelimit + namespace: envoy-gateway-system + ownerReferences: + - apiVersion: apps/v1 + kind: Deployment + name: envoy-gateway + uid: test-owner-reference-uid-for-deployment +spec: + replicas: 1 + strategy: + type: RollingUpdate + selector: + matchLabels: + app.kubernetes.io/name: envoy-ratelimit + app.kubernetes.io/component: ratelimit + app.kubernetes.io/managed-by: envoy-gateway + template: + metadata: + labels: + app.kubernetes.io/name: envoy-ratelimit + app.kubernetes.io/component: ratelimit + app.kubernetes.io/managed-by: envoy-gateway + spec: + automountServiceAccountToken: false + containers: + - command: + - /bin/ratelimit + env: + - name: REDIS_SOCKET_TYPE + value: tcp + - name: REDIS_URL + value: redis.redis.svc:6379 + - name: RUNTIME_ROOT + value: /data + - name: RUNTIME_SUBDIRECTORY + value: ratelimit + - name: RUNTIME_IGNOREDOTFILES + value: "true" + - name: RUNTIME_WATCH_ROOT + value: "false" + - name: LOG_LEVEL + value: info + - name: USE_STATSD + value: "false" + - name: CONFIG_TYPE + value: GRPC_XDS_SOTW + - name: CONFIG_GRPC_XDS_SERVER_URL + value: envoy-gateway:18001 + - name: CONFIG_GRPC_XDS_NODE_ID + value: envoy-ratelimit + - name: GRPC_SERVER_USE_TLS + value: "true" + - name: GRPC_SERVER_TLS_CERT + value: "/certs/tls.crt" + - name: GRPC_SERVER_TLS_KEY + value: "/certs/tls.key" + - name: GRPC_SERVER_TLS_CA_CERT + value: "/certs/ca.crt" + - name: CONFIG_GRPC_XDS_SERVER_USE_TLS + value: "true" + - name: CONFIG_GRPC_XDS_CLIENT_TLS_CERT + value: "/certs/tls.crt" + - name: CONFIG_GRPC_XDS_CLIENT_TLS_KEY + value: "/certs/tls.key" + - name: CONFIG_GRPC_XDS_SERVER_TLS_CACERT + value: "/certs/ca.crt" + - name: FORCE_START_WITHOUT_INITIAL_CONFIG + value: "true" + image: envoyproxy/ratelimit:master + imagePullPolicy: IfNotPresent + name: envoy-ratelimit + ports: + - containerPort: 8081 + name: grpc + protocol: TCP + resources: + requests: + cpu: 100m + memory: 512Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /certs + name: certs + readOnly: true + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + dnsPolicy: ClusterFirstWithHostNet + hostNetwork: true + restartPolicy: Always + schedulerName: default-scheduler + serviceAccountName: envoy-ratelimit + terminationGracePeriodSeconds: 300 + volumes: + - name: certs + secret: + secretName: envoy-rate-limit + defaultMode: 420 + revisionHistoryLimit: 10 + progressDeadlineSeconds: 600 diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index d508226493f..3b6ab330900 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -1420,6 +1420,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | +| `patch` | _[KubernetesPatchSpec](#kubernetespatchspec)_ | false | Patch defines how to perform the patch operation to deployment | | `replicas` | _integer_ | false | Replicas is the number of desired pods. Defaults to 1. | | `strategy` | _[DeploymentStrategy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#deploymentstrategy-v1-apps)_ | false | The deployment strategy to use to replace existing pods with new ones. | | `pod` | _[KubernetesPodSpec](#kubernetespodspec)_ | false | Pod defines the desired specification of pod. | @@ -1444,6 +1445,21 @@ _Appears in:_ | `behavior` | _[HorizontalPodAutoscalerBehavior](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#horizontalpodautoscalerbehavior-v2-autoscaling)_ | false | behavior configures the scaling behavior of the target in both Up and Down directions (scaleUp and scaleDown fields respectively). If not set, the default HPAScalingRules for scale up and scale down are used. See k8s.io.autoscaling.v2.HorizontalPodAutoScalerBehavior. | +#### KubernetesPatchSpec + + + +KubernetesPatchSpec defines how to perform the patch operation + +_Appears in:_ +- [KubernetesDeploymentSpec](#kubernetesdeploymentspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[MergeType](#mergetype)_ | false | Type is the type of merge operation to perform

By default, StrategicMerge is used as the patch type. | +| `value` | _[JSON](#json)_ | true | Object contains the raw configuration for merged object | + + #### KubernetesPodSpec @@ -1580,6 +1596,8 @@ _Appears in:_ + + #### MetricSinkType _Underlying type:_ _string_