Skip to content

Commit ec71f58

Browse files
committed
fix: support patch multiple resources in EPP
Signed-off-by: zirain <zirain2009@gmail.com>
1 parent 674f13b commit ec71f58

10 files changed

Lines changed: 586 additions & 98 deletions

internal/xds/translator/jsonpatch.go

Lines changed: 72 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,8 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []*
4848

4949
for _, p := range e.JSONPatches {
5050
var (
51-
resourceJSON []byte
52-
dest cachetypes.Resource
53-
err error
51+
dests []cachetypes.Resource
52+
err error
5453
)
5554

5655
if err := p.Operation.Validate(); err != nil {
@@ -96,61 +95,80 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []*
9695
continue
9796
}
9897

99-
// find the resource to patch and convert it to JSON
100-
dest, err = findXdsResource(tCtx, p)
98+
// find the resources to patch and convert it to JSON
99+
dests, err = findXdsResources(tCtx, p)
101100
if err != nil {
102-
if errors.Is(err, errResourceNotFound) {
103-
tn := typedName{p.Type, p.Name}
104-
notFoundResources = append(notFoundResources, tn.String())
105-
continue
106-
}
107-
108101
tErrs = errors.Join(tErrs, err)
109102
continue
110103
}
111104

112-
resourceJSON, err = jsonMarshalOpts.Marshal(dest)
113-
if err != nil {
114-
tErr := fmt.Errorf("unable to marshal xds resource %s, err: %w", p.Type, err)
115-
tErrs = errors.Join(tErrs, tErr)
105+
if len(dests) == 0 {
106+
tn := typedName{p.Type, p.Name}
107+
notFoundResources = append(notFoundResources, tn.String())
116108
continue
117109
}
118110

119-
modifiedJSON, err := jsonpatch.ApplyJSONPatches(resourceJSON, p.Operation)
120-
if err != nil {
121-
tErrs = errors.Join(tErrs, err)
122-
continue
123-
}
111+
var destsError error
112+
var destsPatched bool
113+
for _, dest := range dests {
114+
var (
115+
resourceJSON []byte
116+
modifiedJSON []byte
117+
)
124118

125-
// Unmarshal back to typed resource
126-
// Use a temp staging variable that can be marshalled
127-
// into and validated before saving it into the xds output resource
128-
temp, err := getXdsResourceType(p.Type)
129-
if err != nil {
130-
tErrs = errors.Join(tErrs, err)
131-
continue
132-
}
119+
resourceJSON, err = jsonMarshalOpts.Marshal(dest)
120+
if err != nil {
121+
tErr := fmt.Errorf("unable to marshal xds resource %s, err: %w", p.Type, err)
122+
destsError = errors.Join(destsError, tErr)
123+
continue
124+
}
133125

134-
if err = protojson.Unmarshal(modifiedJSON, temp); err != nil {
135-
tErr := errors.New(unmarshalErrorMessage(err, string(modifiedJSON)))
136-
tErrs = errors.Join(tErrs, tErr)
137-
continue
138-
}
126+
modifiedJSON, err = jsonpatch.ApplyJSONPatches(resourceJSON, p.Operation)
127+
if err != nil {
128+
destsError = errors.Join(destsError, err)
129+
continue
130+
}
139131

140-
// Validate the patched resource
141-
validator, ok := temp.(interface{ Validate() error })
142-
if ok {
143-
if err = validator.Validate(); err != nil {
144-
tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", p.Type, err.Error())
145-
tErrs = errors.Join(tErrs, tErr)
132+
// Unmarshal back to typed resource
133+
// Use a temp staging variable that can be marshalled
134+
// into and validated before saving it into the xds output resource
135+
temp, err := getXdsResourceType(p.Type)
136+
if err != nil {
137+
destsError = errors.Join(destsError, err)
138+
continue
139+
}
140+
141+
if err = protojson.Unmarshal(modifiedJSON, temp); err != nil {
142+
tErr := errors.New(unmarshalErrorMessage(err, string(modifiedJSON)))
143+
destsError = errors.Join(destsError, tErr)
144+
continue
145+
}
146+
147+
// Validate the patched resource
148+
validator, ok := temp.(interface{ Validate() error })
149+
if ok {
150+
if err = validator.Validate(); err != nil {
151+
tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", p.Type, err.Error())
152+
destsError = errors.Join(destsError, tErr)
153+
continue
154+
}
155+
}
156+
157+
if err = deepCopyPtr(temp, dest); err != nil {
158+
tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", p.Type, err)
159+
destsError = errors.Join(destsError, tErr)
146160
continue
147161
}
162+
163+
// Mark that at least one dest has been patched successfully,
164+
// so that we can report partial success if there are multiple dests and some of them fail
165+
destsPatched = true
148166
}
149167

150-
if err = deepCopyPtr(temp, dest); err != nil {
151-
tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", p.Type, err)
152-
tErrs = errors.Join(tErrs, tErr)
153-
continue
168+
// If there are multiple dests and some of them fail,
169+
// consider it as successful and ignore the failures to patch other dests.
170+
if !destsPatched && destsError != nil {
171+
tErrs = errors.Join(tErrs, destsError)
154172
}
155173
}
156174

@@ -192,42 +210,28 @@ func getXdsResourceType(resourceType string) (cachetypes.Resource, error) {
192210
}
193211
}
194212

195-
var (
196-
errResourceNotFound = errors.New("resource not found")
197-
jsonMarshalOpts = protojson.MarshalOptions{
198-
UseProtoNames: true,
199-
}
200-
)
213+
var jsonMarshalOpts = protojson.MarshalOptions{
214+
UseProtoNames: true,
215+
}
201216

202-
// findXdsResource return the XDS resource to patch
203-
// TODO: return multiple resources
204-
func findXdsResource(tCtx *types.ResourceVersionTable, p *ir.JSONPatchConfig) (cachetypes.Resource, error) {
217+
func findXdsResources(tCtx *types.ResourceVersionTable, p *ir.JSONPatchConfig) ([]cachetypes.Resource, error) {
218+
var resources []cachetypes.Resource
205219
switch p.Type {
206220
case resourcev3.ListenerType:
207-
if r := findXdsListener(tCtx, p.Name); r != nil {
208-
return r, nil
209-
}
221+
resources = findXdsListeners(tCtx, p.Name)
210222
case resourcev3.RouteType:
211-
if r := findXdsRouteConfig(tCtx, p.Name); r != nil {
212-
return r, nil
213-
}
223+
resources = findXdsRouteConfigs(tCtx, p.Name)
214224
case resourcev3.ClusterType:
215-
if r := findXdsCluster(tCtx, p.Name); r != nil {
216-
return r, nil
217-
}
225+
resources = findXdsClusters(tCtx, p.Name)
218226
case resourcev3.EndpointType:
219-
if r := findXdsEndpoint(tCtx, p.Name); r != nil {
220-
return r, nil
221-
}
227+
resources = findXdsEndpoints(tCtx, p.Name)
222228
case resourcev3.SecretType:
223-
if r := findXdsSecret(tCtx, p.Name); r != nil {
224-
return r, nil
225-
}
229+
resources = findXdsSecrets(tCtx, p.Name)
226230
default:
227231
return nil, fmt.Errorf("unsupported patch type %s", p.Type)
228232
}
229233

230-
return nil, errResourceNotFound
234+
return resources, nil
231235
}
232236

233237
var unescaper = strings.NewReplacer(" ", " ")
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
envoyPatchPolicies:
2+
- status:
3+
ancestors:
4+
- ancestorRef:
5+
group: "gateway.networking.k8s.io"
6+
kind: "Gateway"
7+
namespace: "default"
8+
name: "foobar"
9+
name: "first-policy"
10+
namespace: "default"
11+
jsonPatches:
12+
- type: "type.googleapis.com/envoy.config.listener.v3.Listener"
13+
name: ""
14+
operation:
15+
op: add
16+
path: "/filter_chains/0/filters/0/typed_config/preserve_external_request_id"
17+
value: true
18+
- type: "type.googleapis.com/envoy.config.listener.v3.Listener"
19+
name: ""
20+
operation:
21+
op: "add"
22+
path: "/filter_chains/0/filters/0/typed_config/http_filters/0"
23+
value:
24+
name: "envoy.filters.http.ratelimit"
25+
typed_config:
26+
"@type": "type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit"
27+
domain: "eg-ratelimit"
28+
failure_mode_deny: true
29+
timeout: 1s
30+
rate_limit_service:
31+
grpc_service:
32+
envoy_grpc:
33+
cluster_name: rate-limit-cluster
34+
transport_api_version: V3
35+
- type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration"
36+
name: ""
37+
operation:
38+
op: "add"
39+
path: "/virtual_hosts/0/rate_limits"
40+
value:
41+
- actions:
42+
- remote_address: {}
43+
- type: "type.googleapis.com/envoy.config.cluster.v3.Cluster"
44+
name: rate-limit-cluster
45+
operation:
46+
op: add
47+
path: ""
48+
value:
49+
name: rate-limit-cluster
50+
type: STRICT_DNS
51+
connect_timeout: 10s
52+
lb_policy: ROUND_ROBIN
53+
http2_protocol_options: {}
54+
load_assignment:
55+
cluster_name: rate-limit-cluster
56+
endpoints:
57+
- lb_endpoints:
58+
- endpoint:
59+
address:
60+
socket_address:
61+
address: ratelimit.svc.cluster.local
62+
port_value: 8081
63+
- type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"
64+
name: "first-route-dest"
65+
operation:
66+
op: "replace"
67+
path: "/endpoints/0/load_balancing_weight"
68+
value: "50"
69+
- type: "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret"
70+
name: "secret-1"
71+
operation:
72+
op: "replace"
73+
path: "/tls_certificate/certificate_chain/inline_bytes"
74+
value: "a2V5LWRhdGE="
75+
- type: "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret"
76+
name: "test-secret"
77+
operation:
78+
op: "add"
79+
path: ""
80+
value:
81+
name: test_secret
82+
tls_certificate:
83+
certificate_chain:
84+
inline_bytes: Y2VydC1kYXRh
85+
- type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"
86+
name: "first-route-dest"
87+
operation:
88+
op: add
89+
path: "/endpoints/1"
90+
value:
91+
lbEndpoints:
92+
- endpoint:
93+
address:
94+
socketAddress:
95+
address: 1.2.3.4
96+
portValue: 50000
97+
loadBalancingWeight: 1
98+
- type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"
99+
name: "first-route-dest"
100+
operation:
101+
op: "move"
102+
from: "/endpoints/0/load_balancing_weight"
103+
path: "/endpoints/1/load_balancing_weight"
104+
- type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"
105+
name: "first-route-dest"
106+
operation:
107+
op: copy
108+
from: "/endpoints/1/load_balancing_weight"
109+
path: "/endpoints/0/load_balancing_weight"
110+
http:
111+
- name: "first-listener"
112+
address: 0.0.0.0
113+
port: 10443
114+
hostnames:
115+
- "*"
116+
path:
117+
mergeSlashes: true
118+
escapedSlashesAction: UnescapeAndRedirect
119+
tls:
120+
alpnProtocols:
121+
- h2
122+
- http/1.1
123+
certificates:
124+
- name: secret-1
125+
# byte slice representation of "key-data"
126+
certificate: [99, 101, 114, 116, 45, 100, 97, 116, 97]
127+
# byte slice representation of "key-data"
128+
privateKey: [107, 101, 121, 45, 100, 97, 116, 97]
129+
- name: secret-2
130+
certificate: [99, 101, 114, 116, 45, 100, 97, 116, 97]
131+
privateKey: [107, 101, 121, 45, 100, 97, 116, 97]
132+
routes:
133+
- name: "first-route"
134+
hostname: "*"
135+
headerMatches:
136+
- name: user
137+
exact: "jason"
138+
destination:
139+
name: "first-route-dest"
140+
settings:
141+
- endpoints:
142+
- host: "1.2.3.4"
143+
port: 50000
144+
name: "first-route-dest/backend/0"
145+
- name: "second-listener"
146+
address: 0.0.0.0
147+
port: 10080
148+
hostnames:
149+
- "*"
150+
path:
151+
mergeSlashes: true
152+
escapedSlashesAction: UnescapeAndRedirect
153+
routes:
154+
- name: "second-route"
155+
hostname: "*"
156+
headerMatches:
157+
- name: user
158+
exact: "jason"
159+
destination:
160+
name: "second-route-dest"
161+
settings:
162+
- endpoints:
163+
- host: "1.2.3.4"
164+
port: 50000
165+
name: "second-route-dest/backend/0"

0 commit comments

Comments
 (0)