xref: /aosp_15_r20/external/grpc-grpc-java/xds/src/main/java/io/grpc/xds/XdsResourceType.java (revision e07d83d3ffcef9ecfc9f7f475418ec639ff0e5fe)
1 /*
2  * Copyright 2022 The gRPC Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.grpc.xds;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 import static io.grpc.xds.Bootstrapper.ServerInfo;
21 import static io.grpc.xds.XdsClient.ResourceUpdate;
22 import static io.grpc.xds.XdsClient.canonifyResourceName;
23 import static io.grpc.xds.XdsClient.isResourceNameValid;
24 import static io.grpc.xds.XdsClientImpl.ResourceInvalidException;
25 
26 import com.google.common.annotations.VisibleForTesting;
27 import com.google.common.base.Strings;
28 import com.google.protobuf.Any;
29 import com.google.protobuf.InvalidProtocolBufferException;
30 import com.google.protobuf.Message;
31 import io.envoyproxy.envoy.service.discovery.v3.Resource;
32 import io.grpc.LoadBalancerRegistry;
33 import java.util.ArrayList;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 import javax.annotation.Nullable;
40 
41 abstract class XdsResourceType<T extends ResourceUpdate> {
42   static final String TYPE_URL_RESOURCE =
43       "type.googleapis.com/envoy.service.discovery.v3.Resource";
44   static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls";
45   @VisibleForTesting
46   static final String AGGREGATE_CLUSTER_TYPE_NAME = "envoy.clusters.aggregate";
47   @VisibleForTesting
48   static final String HASH_POLICY_FILTER_STATE_KEY = "io.grpc.channel_id";
49   @VisibleForTesting
50   static boolean enableRouteLookup = getFlag("GRPC_EXPERIMENTAL_XDS_RLS_LB", true);
51   @VisibleForTesting
52   static boolean enableLeastRequest =
53       !Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST"))
54           ? Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST"))
55           : Boolean.parseBoolean(System.getProperty("io.grpc.xds.experimentalEnableLeastRequest"));
56 
57   @VisibleForTesting
58   static boolean enableWrr = getFlag("GRPC_EXPERIMENTAL_XDS_WRR_LB", true);
59 
60   @VisibleForTesting
61   static boolean enablePickFirst = getFlag("GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG", false);
62 
63   static final String TYPE_URL_CLUSTER_CONFIG =
64       "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig";
65   static final String TYPE_URL_TYPED_STRUCT_UDPA =
66       "type.googleapis.com/udpa.type.v1.TypedStruct";
67   static final String TYPE_URL_TYPED_STRUCT =
68       "type.googleapis.com/xds.type.v3.TypedStruct";
69 
70   @Nullable
extractResourceName(Message unpackedResource)71   abstract String extractResourceName(Message unpackedResource);
72 
unpackedClassName()73   abstract Class<? extends com.google.protobuf.Message> unpackedClassName();
74 
typeName()75   abstract String typeName();
76 
typeUrl()77   abstract String typeUrl();
78 
79   // Do not confuse with the SotW approach: it is the mechanism in which the client must specify all
80   // resource names it is interested in with each request. Different resource types may behave
81   // differently in this approach. For LDS and CDS resources, the server must return all resources
82   // that the client has subscribed to in each request. For RDS and EDS, the server may only return
83   // the resources that need an update.
isFullStateOfTheWorld()84   abstract boolean isFullStateOfTheWorld();
85 
86   static class Args {
87     final ServerInfo serverInfo;
88     final String versionInfo;
89     final String nonce;
90     final Bootstrapper.BootstrapInfo bootstrapInfo;
91     final FilterRegistry filterRegistry;
92     final LoadBalancerRegistry loadBalancerRegistry;
93     final TlsContextManager tlsContextManager;
94     // Management server is required to always send newly requested resources, even if they
95     // may have been sent previously (proactively). Thus, client does not need to cache
96     // unrequested resources.
97     // Only resources in the set needs to be parsed. Null means parse everything.
98     final @Nullable Set<String> subscribedResources;
99 
Args(ServerInfo serverInfo, String versionInfo, String nonce, Bootstrapper.BootstrapInfo bootstrapInfo, FilterRegistry filterRegistry, LoadBalancerRegistry loadBalancerRegistry, TlsContextManager tlsContextManager, @Nullable Set<String> subscribedResources)100     public Args(ServerInfo serverInfo, String versionInfo, String nonce,
101                 Bootstrapper.BootstrapInfo bootstrapInfo,
102                 FilterRegistry filterRegistry,
103                 LoadBalancerRegistry loadBalancerRegistry,
104                 TlsContextManager tlsContextManager,
105                 @Nullable Set<String> subscribedResources) {
106       this.serverInfo = serverInfo;
107       this.versionInfo = versionInfo;
108       this.nonce = nonce;
109       this.bootstrapInfo = bootstrapInfo;
110       this.filterRegistry = filterRegistry;
111       this.loadBalancerRegistry = loadBalancerRegistry;
112       this.tlsContextManager = tlsContextManager;
113       this.subscribedResources = subscribedResources;
114     }
115   }
116 
parse(Args args, List<Any> resources)117   ValidatedResourceUpdate<T> parse(Args args, List<Any> resources) {
118     Map<String, ParsedResource<T>> parsedResources = new HashMap<>(resources.size());
119     Set<String> unpackedResources = new HashSet<>(resources.size());
120     Set<String> invalidResources = new HashSet<>();
121     List<String> errors = new ArrayList<>();
122 
123     for (int i = 0; i < resources.size(); i++) {
124       Any resource = resources.get(i);
125 
126       Message unpackedMessage;
127       try {
128         resource = maybeUnwrapResources(resource);
129         unpackedMessage = unpackCompatibleType(resource, unpackedClassName(), typeUrl(), null);
130       } catch (InvalidProtocolBufferException e) {
131         errors.add(String.format("%s response Resource index %d - can't decode %s: %s",
132                 typeName(), i, unpackedClassName().getSimpleName(), e.getMessage()));
133         continue;
134       }
135       String name = extractResourceName(unpackedMessage);
136       if (name == null || !isResourceNameValid(name, resource.getTypeUrl())) {
137         errors.add(
138             "Unsupported resource name: " + name + " for type: " + typeName());
139         continue;
140       }
141       String cname = canonifyResourceName(name);
142       if (args.subscribedResources != null && !args.subscribedResources.contains(name)) {
143         continue;
144       }
145       unpackedResources.add(cname);
146 
147       T resourceUpdate;
148       try {
149         resourceUpdate = doParse(args, unpackedMessage);
150       } catch (XdsClientImpl.ResourceInvalidException e) {
151         errors.add(String.format("%s response %s '%s' validation error: %s",
152                 typeName(), unpackedClassName().getSimpleName(), cname, e.getMessage()));
153         invalidResources.add(cname);
154         continue;
155       }
156 
157       // Resource parsed successfully.
158       parsedResources.put(cname, new ParsedResource<T>(resourceUpdate, resource));
159     }
160     return new ValidatedResourceUpdate<T>(parsedResources, unpackedResources, invalidResources,
161         errors);
162 
163   }
164 
doParse(Args args, Message unpackedMessage)165   abstract T doParse(Args args, Message unpackedMessage) throws ResourceInvalidException;
166 
167   /**
168    * Helper method to unpack serialized {@link com.google.protobuf.Any} message, while replacing
169    * Type URL {@code compatibleTypeUrl} with {@code typeUrl}.
170    *
171    * @param <T> The type of unpacked message
172    * @param any serialized message to unpack
173    * @param clazz the class to unpack the message to
174    * @param typeUrl type URL to replace message Type URL, when it's compatible
175    * @param compatibleTypeUrl compatible Type URL to be replaced with {@code typeUrl}
176    * @return Unpacked message
177    * @throws InvalidProtocolBufferException if the message couldn't be unpacked
178    */
unpackCompatibleType( Any any, Class<T> clazz, String typeUrl, String compatibleTypeUrl)179   static <T extends com.google.protobuf.Message> T unpackCompatibleType(
180       Any any, Class<T> clazz, String typeUrl, String compatibleTypeUrl)
181       throws InvalidProtocolBufferException {
182     if (any.getTypeUrl().equals(compatibleTypeUrl)) {
183       any = any.toBuilder().setTypeUrl(typeUrl).build();
184     }
185     return any.unpack(clazz);
186   }
187 
maybeUnwrapResources(Any resource)188   private Any maybeUnwrapResources(Any resource)
189       throws InvalidProtocolBufferException {
190     if (resource.getTypeUrl().equals(TYPE_URL_RESOURCE)) {
191       return unpackCompatibleType(resource, Resource.class, TYPE_URL_RESOURCE,
192           null).getResource();
193     } else {
194       return resource;
195     }
196   }
197 
198   static final class ParsedResource<T extends ResourceUpdate> {
199     private final T resourceUpdate;
200     private final Any rawResource;
201 
ParsedResource(T resourceUpdate, Any rawResource)202     public ParsedResource(T resourceUpdate, Any rawResource) {
203       this.resourceUpdate = checkNotNull(resourceUpdate, "resourceUpdate");
204       this.rawResource = checkNotNull(rawResource, "rawResource");
205     }
206 
getResourceUpdate()207     T getResourceUpdate() {
208       return resourceUpdate;
209     }
210 
getRawResource()211     Any getRawResource() {
212       return rawResource;
213     }
214   }
215 
216   static final class ValidatedResourceUpdate<T extends ResourceUpdate> {
217     Map<String, ParsedResource<T>> parsedResources;
218     Set<String> unpackedResources;
219     Set<String> invalidResources;
220     List<String> errors;
221 
222     // validated resource update
ValidatedResourceUpdate(Map<String, ParsedResource<T>> parsedResources, Set<String> unpackedResources, Set<String> invalidResources, List<String> errors)223     public ValidatedResourceUpdate(Map<String, ParsedResource<T>> parsedResources,
224                                    Set<String> unpackedResources,
225                                    Set<String> invalidResources,
226                                    List<String> errors) {
227       this.parsedResources = parsedResources;
228       this.unpackedResources = unpackedResources;
229       this.invalidResources = invalidResources;
230       this.errors = errors;
231     }
232   }
233 
getFlag(String envVarName, boolean enableByDefault)234   private static boolean getFlag(String envVarName, boolean enableByDefault) {
235     String envVar = System.getenv(envVarName);
236     if (enableByDefault) {
237       return Strings.isNullOrEmpty(envVar) || Boolean.parseBoolean(envVar);
238     } else {
239       return !Strings.isNullOrEmpty(envVar) && Boolean.parseBoolean(envVar);
240     }
241   }
242 
243   @VisibleForTesting
244   static final class StructOrError<T> {
245 
246     /**
247     * Returns a {@link StructOrError} for the successfully converted data object.
248     */
fromStruct(T struct)249     static <T> StructOrError<T> fromStruct(T struct) {
250       return new StructOrError<>(struct);
251     }
252 
253     /**
254      * Returns a {@link StructOrError} for the failure to convert the data object.
255      */
fromError(String errorDetail)256     static <T> StructOrError<T> fromError(String errorDetail) {
257       return new StructOrError<>(errorDetail);
258     }
259 
260     private final String errorDetail;
261     private final T struct;
262 
StructOrError(T struct)263     private StructOrError(T struct) {
264       this.struct = checkNotNull(struct, "struct");
265       this.errorDetail = null;
266     }
267 
StructOrError(String errorDetail)268     private StructOrError(String errorDetail) {
269       this.struct = null;
270       this.errorDetail = checkNotNull(errorDetail, "errorDetail");
271     }
272 
273     /**
274      * Returns struct if exists, otherwise null.
275      */
276     @VisibleForTesting
277     @Nullable
getStruct()278     T getStruct() {
279       return struct;
280     }
281 
282     /**
283      * Returns error detail if exists, otherwise null.
284      */
285     @VisibleForTesting
286     @Nullable
getErrorDetail()287     String getErrorDetail() {
288       return errorDetail;
289     }
290   }
291 }
292