1 /* 2 * Copyright 2019 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 21 import com.google.common.annotations.VisibleForTesting; 22 import com.google.common.base.MoreObjects; 23 import com.google.protobuf.ListValue; 24 import com.google.protobuf.NullValue; 25 import com.google.protobuf.Struct; 26 import com.google.protobuf.Value; 27 import java.util.ArrayList; 28 import java.util.Collections; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.Objects; 32 import javax.annotation.Nullable; 33 34 /** 35 * Defines gRPC data types for Envoy protobuf messages used in xDS protocol. Each data type has 36 * the same name as Envoy's corresponding protobuf message, but only with fields used by gRPC. 37 * 38 * <p>Each data type should define a {@code fromEnvoyProtoXXX} static method to convert an Envoy 39 * proto message to an instance of that data type. 40 * 41 * <p>For data types that need to be sent as protobuf messages, a {@code toEnvoyProtoXXX} instance 42 * method is defined to convert an instance to Envoy proto message. 43 * 44 * <p>Data conversion should follow the invariant: converted data is guaranteed to be valid for 45 * gRPC. If the protobuf message contains invalid data, the conversion should fail and no object 46 * should be instantiated. 47 */ 48 // TODO(chengyuanzhang): put data types into smaller categories. 49 final class EnvoyProtoData { 50 51 // Prevent instantiation. EnvoyProtoData()52 private EnvoyProtoData() { 53 } 54 55 /** 56 * See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.core.v3.Node}. 57 */ 58 public static final class Node { 59 60 private final String id; 61 private final String cluster; 62 @Nullable 63 private final Map<String, ?> metadata; 64 @Nullable 65 private final Locality locality; 66 private final List<Address> listeningAddresses; 67 private final String buildVersion; 68 private final String userAgentName; 69 @Nullable 70 private final String userAgentVersion; 71 private final List<String> clientFeatures; 72 Node( String id, String cluster, @Nullable Map<String, ?> metadata, @Nullable Locality locality, List<Address> listeningAddresses, String buildVersion, String userAgentName, @Nullable String userAgentVersion, List<String> clientFeatures)73 private Node( 74 String id, String cluster, @Nullable Map<String, ?> metadata, @Nullable Locality locality, 75 List<Address> listeningAddresses, String buildVersion, String userAgentName, 76 @Nullable String userAgentVersion, List<String> clientFeatures) { 77 this.id = checkNotNull(id, "id"); 78 this.cluster = checkNotNull(cluster, "cluster"); 79 this.metadata = metadata; 80 this.locality = locality; 81 this.listeningAddresses = Collections.unmodifiableList( 82 checkNotNull(listeningAddresses, "listeningAddresses")); 83 this.buildVersion = checkNotNull(buildVersion, "buildVersion"); 84 this.userAgentName = checkNotNull(userAgentName, "userAgentName"); 85 this.userAgentVersion = userAgentVersion; 86 this.clientFeatures = Collections.unmodifiableList( 87 checkNotNull(clientFeatures, "clientFeatures")); 88 } 89 90 @Override toString()91 public String toString() { 92 return MoreObjects.toStringHelper(this) 93 .add("id", id) 94 .add("cluster", cluster) 95 .add("metadata", metadata) 96 .add("locality", locality) 97 .add("listeningAddresses", listeningAddresses) 98 .add("buildVersion", buildVersion) 99 .add("userAgentName", userAgentName) 100 .add("userAgentVersion", userAgentVersion) 101 .add("clientFeatures", clientFeatures) 102 .toString(); 103 } 104 105 @Override equals(Object o)106 public boolean equals(Object o) { 107 if (this == o) { 108 return true; 109 } 110 if (o == null || getClass() != o.getClass()) { 111 return false; 112 } 113 Node node = (Node) o; 114 return Objects.equals(id, node.id) 115 && Objects.equals(cluster, node.cluster) 116 && Objects.equals(metadata, node.metadata) 117 && Objects.equals(locality, node.locality) 118 && Objects.equals(listeningAddresses, node.listeningAddresses) 119 && Objects.equals(buildVersion, node.buildVersion) 120 && Objects.equals(userAgentName, node.userAgentName) 121 && Objects.equals(userAgentVersion, node.userAgentVersion) 122 && Objects.equals(clientFeatures, node.clientFeatures); 123 } 124 125 @Override hashCode()126 public int hashCode() { 127 return Objects 128 .hash(id, cluster, metadata, locality, listeningAddresses, buildVersion, userAgentName, 129 userAgentVersion, clientFeatures); 130 } 131 132 static final class Builder { 133 private String id = ""; 134 private String cluster = ""; 135 @Nullable 136 private Map<String, ?> metadata; 137 @Nullable 138 private Locality locality; 139 // TODO(sanjaypujare): eliminate usage of listening_addresses field. 140 private final List<Address> listeningAddresses = new ArrayList<>(); 141 private String buildVersion = ""; 142 private String userAgentName = ""; 143 @Nullable 144 private String userAgentVersion; 145 private final List<String> clientFeatures = new ArrayList<>(); 146 Builder()147 private Builder() { 148 } 149 setId(String id)150 Builder setId(String id) { 151 this.id = checkNotNull(id, "id"); 152 return this; 153 } 154 setCluster(String cluster)155 Builder setCluster(String cluster) { 156 this.cluster = checkNotNull(cluster, "cluster"); 157 return this; 158 } 159 setMetadata(Map<String, ?> metadata)160 Builder setMetadata(Map<String, ?> metadata) { 161 this.metadata = checkNotNull(metadata, "metadata"); 162 return this; 163 } 164 setLocality(Locality locality)165 Builder setLocality(Locality locality) { 166 this.locality = checkNotNull(locality, "locality"); 167 return this; 168 } 169 addListeningAddresses(Address address)170 Builder addListeningAddresses(Address address) { 171 listeningAddresses.add(checkNotNull(address, "address")); 172 return this; 173 } 174 setBuildVersion(String buildVersion)175 Builder setBuildVersion(String buildVersion) { 176 this.buildVersion = checkNotNull(buildVersion, "buildVersion"); 177 return this; 178 } 179 setUserAgentName(String userAgentName)180 Builder setUserAgentName(String userAgentName) { 181 this.userAgentName = checkNotNull(userAgentName, "userAgentName"); 182 return this; 183 } 184 setUserAgentVersion(String userAgentVersion)185 Builder setUserAgentVersion(String userAgentVersion) { 186 this.userAgentVersion = checkNotNull(userAgentVersion, "userAgentVersion"); 187 return this; 188 } 189 addClientFeatures(String clientFeature)190 Builder addClientFeatures(String clientFeature) { 191 this.clientFeatures.add(checkNotNull(clientFeature, "clientFeature")); 192 return this; 193 } 194 build()195 Node build() { 196 return new Node( 197 id, cluster, metadata, locality, listeningAddresses, buildVersion, userAgentName, 198 userAgentVersion, clientFeatures); 199 } 200 } 201 newBuilder()202 static Builder newBuilder() { 203 return new Builder(); 204 } 205 toBuilder()206 Builder toBuilder() { 207 Builder builder = new Builder(); 208 builder.id = id; 209 builder.cluster = cluster; 210 builder.metadata = metadata; 211 builder.locality = locality; 212 builder.buildVersion = buildVersion; 213 builder.listeningAddresses.addAll(listeningAddresses); 214 builder.userAgentName = userAgentName; 215 builder.userAgentVersion = userAgentVersion; 216 builder.clientFeatures.addAll(clientFeatures); 217 return builder; 218 } 219 getId()220 String getId() { 221 return id; 222 } 223 getCluster()224 String getCluster() { 225 return cluster; 226 } 227 228 @Nullable getMetadata()229 Map<String, ?> getMetadata() { 230 return metadata; 231 } 232 233 @Nullable getLocality()234 Locality getLocality() { 235 return locality; 236 } 237 getListeningAddresses()238 List<Address> getListeningAddresses() { 239 return listeningAddresses; 240 } 241 242 @SuppressWarnings("deprecation") 243 @VisibleForTesting toEnvoyProtoNode()244 public io.envoyproxy.envoy.config.core.v3.Node toEnvoyProtoNode() { 245 io.envoyproxy.envoy.config.core.v3.Node.Builder builder = 246 io.envoyproxy.envoy.config.core.v3.Node.newBuilder(); 247 builder.setId(id); 248 builder.setCluster(cluster); 249 if (metadata != null) { 250 Struct.Builder structBuilder = Struct.newBuilder(); 251 for (Map.Entry<String, ?> entry : metadata.entrySet()) { 252 structBuilder.putFields(entry.getKey(), convertToValue(entry.getValue())); 253 } 254 builder.setMetadata(structBuilder); 255 } 256 if (locality != null) { 257 builder.setLocality( 258 io.envoyproxy.envoy.config.core.v3.Locality.newBuilder() 259 .setRegion(locality.region()) 260 .setZone(locality.zone()) 261 .setSubZone(locality.subZone())); 262 } 263 for (Address address : listeningAddresses) { 264 builder.addListeningAddresses(address.toEnvoyProtoAddress()); 265 } 266 builder.setUserAgentName(userAgentName); 267 if (userAgentVersion != null) { 268 builder.setUserAgentVersion(userAgentVersion); 269 } 270 builder.addAllClientFeatures(clientFeatures); 271 return builder.build(); 272 } 273 } 274 275 /** 276 * Converts Java representation of the given JSON value to protobuf's {@link 277 * com.google.protobuf.Value} representation. 278 * 279 * <p>The given {@code rawObject} must be a valid JSON value in Java representation, which is 280 * either a {@code Map<String, ?>}, {@code List<?>}, {@code String}, {@code Double}, {@code 281 * Boolean}, or {@code null}. 282 */ convertToValue(Object rawObject)283 private static Value convertToValue(Object rawObject) { 284 Value.Builder valueBuilder = Value.newBuilder(); 285 if (rawObject == null) { 286 valueBuilder.setNullValue(NullValue.NULL_VALUE); 287 } else if (rawObject instanceof Double) { 288 valueBuilder.setNumberValue((Double) rawObject); 289 } else if (rawObject instanceof String) { 290 valueBuilder.setStringValue((String) rawObject); 291 } else if (rawObject instanceof Boolean) { 292 valueBuilder.setBoolValue((Boolean) rawObject); 293 } else if (rawObject instanceof Map) { 294 Struct.Builder structBuilder = Struct.newBuilder(); 295 @SuppressWarnings("unchecked") 296 Map<String, ?> map = (Map<String, ?>) rawObject; 297 for (Map.Entry<String, ?> entry : map.entrySet()) { 298 structBuilder.putFields(entry.getKey(), convertToValue(entry.getValue())); 299 } 300 valueBuilder.setStructValue(structBuilder); 301 } else if (rawObject instanceof List) { 302 ListValue.Builder listBuilder = ListValue.newBuilder(); 303 List<?> list = (List<?>) rawObject; 304 for (Object obj : list) { 305 listBuilder.addValues(convertToValue(obj)); 306 } 307 valueBuilder.setListValue(listBuilder); 308 } 309 return valueBuilder.build(); 310 } 311 312 /** 313 * See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.core.v3.Address}. 314 */ 315 static final class Address { 316 private final String address; 317 private final int port; 318 Address(String address, int port)319 Address(String address, int port) { 320 this.address = checkNotNull(address, "address"); 321 this.port = port; 322 } 323 toEnvoyProtoAddress()324 io.envoyproxy.envoy.config.core.v3.Address toEnvoyProtoAddress() { 325 return 326 io.envoyproxy.envoy.config.core.v3.Address.newBuilder().setSocketAddress( 327 io.envoyproxy.envoy.config.core.v3.SocketAddress.newBuilder().setAddress(address) 328 .setPortValue(port)).build(); 329 } 330 toEnvoyProtoAddressV2()331 io.envoyproxy.envoy.api.v2.core.Address toEnvoyProtoAddressV2() { 332 return 333 io.envoyproxy.envoy.api.v2.core.Address.newBuilder().setSocketAddress( 334 io.envoyproxy.envoy.api.v2.core.SocketAddress.newBuilder().setAddress(address) 335 .setPortValue(port)).build(); 336 } 337 338 @Override toString()339 public String toString() { 340 return MoreObjects.toStringHelper(this) 341 .add("address", address) 342 .add("port", port) 343 .toString(); 344 } 345 346 @Override equals(Object o)347 public boolean equals(Object o) { 348 if (this == o) { 349 return true; 350 } 351 if (o == null || getClass() != o.getClass()) { 352 return false; 353 } 354 Address address1 = (Address) o; 355 return port == address1.port && Objects.equals(address, address1.address); 356 } 357 358 @Override hashCode()359 public int hashCode() { 360 return Objects.hash(address, port); 361 } 362 } 363 } 364