diff --git a/it/xds-client/src/main/java/com/linecorp/armeria/xds/it/XdsResourceReader.java b/it/xds-client/src/main/java/com/linecorp/armeria/xds/it/XdsResourceReader.java deleted file mode 100644 index 6c5ed8337e8..00000000000 --- a/it/xds-client/src/main/java/com/linecorp/armeria/xds/it/XdsResourceReader.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2024 LINE Corporation - * - * LY Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package com.linecorp.armeria.xds.it; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.google.common.escape.Escaper; -import com.google.common.escape.Escapers; -import com.google.protobuf.GeneratedMessage; -import com.google.protobuf.util.JsonFormat; -import com.google.protobuf.util.JsonFormat.Parser; -import com.google.protobuf.util.JsonFormat.TypeRegistry; - -import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap; -import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router; -import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; -import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext; - -public final class XdsResourceReader { - - private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - private static final ObjectMapper jsonMapper = new ObjectMapper(); - private static final Parser parser = - JsonFormat.parser().usingTypeRegistry(TypeRegistry.newBuilder() - .add(HttpConnectionManager.getDescriptor()) - .add(Router.getDescriptor()) - .add(UpstreamTlsContext.getDescriptor()) - .build()); - - public static Bootstrap fromYaml(String yaml) { - final Bootstrap.Builder bootstrapBuilder = Bootstrap.newBuilder(); - try { - final JsonNode jsonNode = mapper.reader().readTree(yaml); - parser.merge(jsonNode.toString(), bootstrapBuilder); - } catch (Exception e) { - throw new RuntimeException(e); - } - return bootstrapBuilder.build(); - } - - @SuppressWarnings("unchecked") - public static T fromYaml(String yaml, Class clazz) { - final GeneratedMessage.Builder builder; - try { - builder = (GeneratedMessage.Builder) clazz.getMethod("newBuilder").invoke(null); - final JsonNode jsonNode = mapper.reader().readTree(yaml); - parser.merge(jsonNode.toString(), builder); - } catch (Exception e) { - throw new RuntimeException(e); - } - return (T) builder.build(); - } - - @SuppressWarnings("unchecked") - public static T fromJson(String json, Class clazz) { - final GeneratedMessage.Builder builder; - try { - builder = (GeneratedMessage.Builder) clazz.getMethod("newBuilder").invoke(null); - final JsonNode jsonNode = jsonMapper.reader().readTree(json); - parser.merge(jsonNode.toString(), builder); - } catch (Exception e) { - throw new RuntimeException(e); - } - return (T) builder.build(); - } - - private static final Escaper multiLineEscaper = Escapers.builder() - .addEscape('\\', "\\\\") - .addEscape('"', "\\\"") - .addEscape('\n', "\\n") - .addEscape('\r', "\\r") - .build(); - - static String escapeMultiLine(String str) { - return multiLineEscaper.escape(str); - } - - private XdsResourceReader() {} -} diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/client/endpoint/TransportSocketMatchUtilTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/client/endpoint/TransportSocketMatchUtilTest.java index 507db4daa57..1b747db6a87 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/client/endpoint/TransportSocketMatchUtilTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/client/endpoint/TransportSocketMatchUtilTest.java @@ -35,7 +35,7 @@ import com.linecorp.armeria.xds.TransportSocketMatchSnapshot; import com.linecorp.armeria.xds.TransportSocketSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; -import com.linecorp.armeria.xds.it.XdsResourceReader; +import com.linecorp.armeria.xds.XdsResourceReader; import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap; import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/BootstrapSecretsTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/BootstrapSecretsTest.java index 55e1289c137..74f242f3de6 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/BootstrapSecretsTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/BootstrapSecretsTest.java @@ -31,6 +31,7 @@ import com.linecorp.armeria.xds.TlsCertificateSnapshot; import com.linecorp.armeria.xds.TransportSocketSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/BootstrapTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/BootstrapTest.java index 68bd11ce919..d63c2ed6146 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/BootstrapTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/BootstrapTest.java @@ -35,6 +35,7 @@ import com.linecorp.armeria.xds.ClusterSnapshot; import com.linecorp.armeria.xds.SnapshotWatcher; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import io.envoyproxy.controlplane.cache.v3.SimpleCache; import io.envoyproxy.controlplane.cache.v3.Snapshot; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/CertificateValidationContextTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/CertificateValidationContextTest.java index 3f88b424ee0..58004e99fb9 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/CertificateValidationContextTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/CertificateValidationContextTest.java @@ -41,6 +41,7 @@ import com.linecorp.armeria.xds.ListenerSnapshot; import com.linecorp.armeria.xds.TransportSocketSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import io.envoyproxy.controlplane.cache.v3.SimpleCache; import io.envoyproxy.controlplane.cache.v3.Snapshot; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ConfigSourceLifecycleObserverTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ConfigSourceLifecycleObserverTest.java index ff09ce9b86e..e63368a89e4 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ConfigSourceLifecycleObserverTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ConfigSourceLifecycleObserverTest.java @@ -40,6 +40,7 @@ import com.linecorp.armeria.testing.junit5.server.ServerExtension; import com.linecorp.armeria.xds.ListenerRoot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.XdsType; import io.envoyproxy.controlplane.cache.v3.SimpleCache; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ControlPlaneTlsIntegrationTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ControlPlaneTlsIntegrationTest.java index 0ee11683545..c2b0a4c73d1 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ControlPlaneTlsIntegrationTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ControlPlaneTlsIntegrationTest.java @@ -46,6 +46,7 @@ import com.linecorp.armeria.xds.ListenerRoot; import com.linecorp.armeria.xds.ListenerSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; import io.envoyproxy.controlplane.cache.v3.SimpleCache; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DataSourceTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DataSourceTest.java index ad9d7d3f302..03d5c0180b3 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DataSourceTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DataSourceTest.java @@ -42,6 +42,7 @@ import com.linecorp.armeria.xds.TlsCertificateSnapshot; import com.linecorp.armeria.xds.TransportSocketSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import io.envoyproxy.controlplane.cache.v3.SimpleCache; import io.envoyproxy.controlplane.cache.v3.Snapshot; @@ -51,6 +52,11 @@ class DataSourceTest { + private static String escapeMultiLine(String str) { + return str.replace("\\", "\\\\").replace("\"", "\\\"") + .replace("\n", "\\n").replace("\r", "\\r"); + } + private static final String GROUP = "key"; private static final SimpleCache cache = new SimpleCache<>(node -> GROUP); private static final AtomicLong version = new AtomicLong(); @@ -842,8 +848,8 @@ void tlsCertificateWithInlineString() throws Exception { inline_string: "%s" certificate_chain: inline_string: "%s" - """.formatted(XdsResourceReader.escapeMultiLine(privateKeyContent), - XdsResourceReader.escapeMultiLine(certContent)); + """.formatted(escapeMultiLine(privateKeyContent), + escapeMultiLine(certContent)); final Secret secret = XdsResourceReader.fromYaml(tlsCertYaml, Secret.class); version.incrementAndGet(); cache.setSnapshot(GROUP, Snapshot.create(ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DynamicSecretTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DynamicSecretTest.java index ff471914b66..5773deb91fe 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DynamicSecretTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DynamicSecretTest.java @@ -40,6 +40,7 @@ import com.linecorp.armeria.xds.TransportSocketMatchSnapshot; import com.linecorp.armeria.xds.TransportSocketSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import io.envoyproxy.controlplane.cache.v3.SimpleCache; import io.envoyproxy.controlplane.cache.v3.Snapshot; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ErrorHandlingTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ErrorHandlingTest.java index e04aa9372ef..473a0a25343 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ErrorHandlingTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ErrorHandlingTest.java @@ -48,6 +48,7 @@ import com.linecorp.armeria.xds.SnapshotWatcher; import com.linecorp.armeria.xds.XdsBootstrap; import com.linecorp.armeria.xds.XdsResourceException; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.XdsType; import io.envoyproxy.controlplane.cache.v3.SimpleCache; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/HealthCheckedTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/HealthCheckedTest.java index 60c3d37e775..1d6fb55cd9b 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/HealthCheckedTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/HealthCheckedTest.java @@ -39,6 +39,7 @@ import com.linecorp.armeria.xds.ListenerSnapshot; import com.linecorp.armeria.xds.SnapshotWatcher; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsLoadBalancer; import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/LoadBalancerReloadTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/LoadBalancerReloadTest.java index 8c1b3aca66d..89c19135c49 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/LoadBalancerReloadTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/LoadBalancerReloadTest.java @@ -46,6 +46,7 @@ import com.linecorp.armeria.xds.ListenerSnapshot; import com.linecorp.armeria.xds.SnapshotWatcher; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsLoadBalancer; import io.envoyproxy.controlplane.cache.v3.SimpleCache; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/PreprocessorErrorTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/PreprocessorErrorTest.java index 383d4ed9962..29787661538 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/PreprocessorErrorTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/PreprocessorErrorTest.java @@ -35,6 +35,7 @@ import com.linecorp.armeria.common.TimeoutException; import com.linecorp.armeria.internal.client.DefaultClientRequestContext; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; class PreprocessorErrorTest { diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ResourceNodeMetricTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ResourceNodeMetricTest.java index 5a8f159ec79..61b0ef9412d 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ResourceNodeMetricTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ResourceNodeMetricTest.java @@ -46,6 +46,7 @@ import com.linecorp.armeria.xds.ClusterRoot; import com.linecorp.armeria.xds.ListenerRoot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import io.envoyproxy.controlplane.cache.v3.SimpleCache; import io.envoyproxy.controlplane.cache.v3.Snapshot; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RetryTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RetryTest.java index a121b5e7535..1abf4f6ab34 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RetryTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RetryTest.java @@ -43,6 +43,7 @@ import com.linecorp.armeria.common.RequestHeaders; import com.linecorp.armeria.common.ResponseHeaders; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; class RetryTest { diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RouteMatcherTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RouteMatcherTest.java index e0175fe7804..d709f2f59f1 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RouteMatcherTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RouteMatcherTest.java @@ -32,6 +32,7 @@ import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.RequestHeaders; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; class RouteMatcherTest { diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TimeoutTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TimeoutTest.java index a96fbc18196..fea3147cf91 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TimeoutTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TimeoutTest.java @@ -34,6 +34,7 @@ import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; class TimeoutTest { diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TlsPeerVerificationIntegrationTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TlsPeerVerificationIntegrationTest.java index bca0ced645e..8a122d5e34c 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TlsPeerVerificationIntegrationTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TlsPeerVerificationIntegrationTest.java @@ -51,6 +51,7 @@ import com.linecorp.armeria.xds.ListenerSnapshot; import com.linecorp.armeria.xds.TransportSocketSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; class TlsPeerVerificationIntegrationTest { diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TlsValidationContextSdsIntegrationTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TlsValidationContextSdsIntegrationTest.java index 3989375ec2d..3e27bcb0ba2 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TlsValidationContextSdsIntegrationTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TlsValidationContextSdsIntegrationTest.java @@ -44,6 +44,7 @@ import com.linecorp.armeria.xds.ListenerRoot; import com.linecorp.armeria.xds.ListenerSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; import io.envoyproxy.controlplane.cache.v3.SimpleCache; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TransportSocketMatchesIntegrationTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TransportSocketMatchesIntegrationTest.java index cb76b28327a..dd62174f61c 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TransportSocketMatchesIntegrationTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TransportSocketMatchesIntegrationTest.java @@ -35,6 +35,7 @@ import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; import com.linecorp.armeria.testing.junit5.server.ServerExtension; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; class TransportSocketMatchesIntegrationTest { diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TransportSocketSnapshotTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TransportSocketSnapshotTest.java index 2c8894699b2..e647b44b2e7 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TransportSocketSnapshotTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/TransportSocketSnapshotTest.java @@ -16,7 +16,6 @@ package com.linecorp.armeria.xds.it; -import static com.linecorp.armeria.xds.it.XdsResourceReader.escapeMultiLine; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -48,11 +47,17 @@ import com.linecorp.armeria.xds.TlsCertificateSnapshot; import com.linecorp.armeria.xds.TransportSocketSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap; class TransportSocketSnapshotTest { + private static String escapeMultiLine(String str) { + return str.replace("\\", "\\\\").replace("\"", "\\\"") + .replace("\n", "\\n").replace("\r", "\\r"); + } + @RegisterExtension static SelfSignedCertificateExtension certificate = new SelfSignedCertificateExtension(); diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java index a3d9ce2cd41..911115a7d6e 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/VirtualHostRoutingTest.java @@ -36,6 +36,7 @@ import com.linecorp.armeria.common.RequestHeaders; import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; class VirtualHostRoutingTest { diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsEndpointGroupTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsEndpointGroupTest.java index f045ec07f63..43278b459b7 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsEndpointGroupTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsEndpointGroupTest.java @@ -38,6 +38,7 @@ import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; import com.linecorp.armeria.testing.junit5.server.ServerExtension; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsEndpointGroup; import io.envoyproxy.controlplane.cache.v3.SimpleCache; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsLoadBalancerLifecycleObserverTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsLoadBalancerLifecycleObserverTest.java index 52d8226f81b..8ee1a2aa1cd 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsLoadBalancerLifecycleObserverTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsLoadBalancerLifecycleObserverTest.java @@ -37,6 +37,7 @@ import com.linecorp.armeria.testing.junit5.server.ServerExtension; import com.linecorp.armeria.xds.ListenerRoot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import io.envoyproxy.controlplane.cache.v3.SimpleCache; import io.envoyproxy.controlplane.cache.v3.Snapshot; diff --git a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsPreprocessorTest.java b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsPreprocessorTest.java index 7236fe2fbf9..1a7d96803e7 100644 --- a/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsPreprocessorTest.java +++ b/it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsPreprocessorTest.java @@ -36,6 +36,7 @@ import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; import com.linecorp.armeria.testing.junit5.server.ServerExtension; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; import io.envoyproxy.controlplane.cache.v3.SimpleCache; @@ -147,6 +148,22 @@ void testWithListener() { } } + @Test + void whenReady() { + final Bootstrap bootstrap = + bootstrapYaml(BOOTSTRAP_CLUSTER_NAME, + server.httpSocketAddress().getHostString(), + server.httpPort(), + null); + try (XdsBootstrap xdsBootstrap = XdsBootstrap.of(bootstrap, eventLoop.get()); + XdsHttpPreprocessor xdsPreprocessor = + XdsHttpPreprocessor.ofListener(LISTENER_NAME, xdsBootstrap)) { + xdsPreprocessor.whenReady().join(); + final BlockingWebClient blockingClient = WebClient.of(xdsPreprocessor).blocking(); + assertThat(blockingClient.get("/hello").contentUtf8()).isEqualTo("world"); + } + } + @Test void testAllHttps() { final Bootstrap bootstrap = diff --git a/it/xds-no-validation/src/test/java/com/linecorp/armeria/xds/it/RegressionGuard1Test.java b/it/xds-no-validation/src/test/java/com/linecorp/armeria/xds/it/RegressionGuard1Test.java index 6e11b9383c5..d2bd0188822 100644 --- a/it/xds-no-validation/src/test/java/com/linecorp/armeria/xds/it/RegressionGuard1Test.java +++ b/it/xds-no-validation/src/test/java/com/linecorp/armeria/xds/it/RegressionGuard1Test.java @@ -39,6 +39,7 @@ import com.linecorp.armeria.xds.ListenerRoot; import com.linecorp.armeria.xds.ListenerSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import com.linecorp.armeria.xds.client.endpoint.XdsEndpointGroup; import io.envoyproxy.controlplane.cache.v3.SimpleCache; diff --git a/it/xds-no-validation/src/test/java/com/linecorp/armeria/xds/it/RegressionGuard2Test.java b/it/xds-no-validation/src/test/java/com/linecorp/armeria/xds/it/RegressionGuard2Test.java index 809ceab4616..021884c6489 100644 --- a/it/xds-no-validation/src/test/java/com/linecorp/armeria/xds/it/RegressionGuard2Test.java +++ b/it/xds-no-validation/src/test/java/com/linecorp/armeria/xds/it/RegressionGuard2Test.java @@ -33,6 +33,7 @@ import com.linecorp.armeria.xds.ListenerRoot; import com.linecorp.armeria.xds.ListenerSnapshot; import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.XdsResourceReader; import io.envoyproxy.controlplane.cache.v3.SimpleCache; import io.envoyproxy.controlplane.cache.v3.Snapshot; diff --git a/site-new/sidebars.ts b/site-new/sidebars.ts index 75b605bde12..98cc3e79a7d 100644 --- a/site-new/sidebars.ts +++ b/site-new/sidebars.ts @@ -76,6 +76,23 @@ const sidebars: SidebarsConfig = { title: 'Advanced', }, items: [ + { + type: 'category', + label: 'xDS', + link: { + type: 'generated-index', + title: 'xDS', + }, + items: [ + 'advanced/xds/introduction', + 'advanced/xds/getting-started', + 'advanced/xds/concepts', + 'advanced/xds/metrics', + 'advanced/xds/extensions', + 'advanced/xds/supported-features', + 'advanced/xds/running-with-istio', + ], + }, 'advanced/logging', 'advanced/structured-logging', 'advanced/request-context', diff --git a/site-new/src/content/docs/advanced/xds/concepts.mdx b/site-new/src/content/docs/advanced/xds/concepts.mdx new file mode 100644 index 00000000000..5251cdd2277 --- /dev/null +++ b/site-new/src/content/docs/advanced/xds/concepts.mdx @@ -0,0 +1,300 @@ +--- +sidebar_position: 2 +--- + +# Concepts + +## The service mesh landscape + +Service meshes provide powerful traffic management capabilities — +dynamic routing, load balancing, retries, timeouts, and mutual TLS — without requiring +changes to application code. However, they are tightly coupled to orchestration platforms +like Kubernetes. A typical deployment looks like this: + +1. A **control plane** watches the orchestration platform for service + changes and computes routing configuration. +2. A **sidecar proxy** (e.g. Envoy) is injected alongside every application container. +3. The control plane pushes configuration to sidecars using the **xDS protocol**. +4. All traffic flows through the sidecar, which enforces policies transparently. + +```bob-svg + +--------------------+ + | | + request | Control Plane | + | | | + v +---+------------+---+ + +---------------+----------------+ | | + | +<---+ | + | Sidecar Proxy (Inbound) | xDS | + | (mTLS, auth, rate limit) | | + | | | + +---------------+----------------+ | + | | + v | + +---------------+----------------+ | + | | | + | User Application | | + | | | + +---------------+----------------+ | + | | + v | + +---------------+----------------+ | + | +<----------------+ + | Sidecar Proxy (Outbound) | xDS + | (discovery, LB, retry, mTLS) | + | | + +---------------+----------------+ + | + v + remote service +``` + +This architecture works well when you already run on Kubernetes, but it introduces +significant overhead for teams that don't: + +- You need an orchestration platform just to get service mesh features. +- Every service gets a sidecar proxy, adding latency, memory, and operational complexity. +- Debugging becomes harder when requests pass through an additional network hop. + +## xDS without the sidecar + +Armeria implements the xDS protocol directly in the application. Instead of routing +traffic through a sidecar proxy, Armeria speaks xDS to a control plane and applies +the configuration internally — endpoint discovery, routing, retries, timeouts, and +load balancing all happen in-process. + +This means soon you can use a service mesh control plane **without** requiring +Kubernetes or sidecar proxies. You can also run a standalone xDS control plane for +environments where Kubernetes isn't available. + +```bob-svg + +--------------------+ + | | + request | Control Plane | + | | | + v +---+------------+---+ + +---------------------------------------------+ | | + | JVM Runtime | | | + | | | | + | +---------------+----------------+ +<-+ | + | | | | | + | | xDS Inbound (coming soon) | | | + | | (mTLS, auth, rate limit) | | | + | | | | | + | +---------------+----------------+ | | + | | | | + | v | | + | +---------------+----------------+ | | + | | | | | + | | User Application | | | + | | | | | + | +---------------+----------------+ | | + | | | | + | v | | + | +---------------+----------------+ +<-----+ + | | | | + | | xDS Outbound | | + | | (discovery, LB, retry, mTLS) | | + | | | | + | +---------------+----------------+ | + | | | + +---------------------------------------------+ + | + v + remote service +``` + +- **xDS Inbound** *(coming soon)* — will handle incoming requests before they reach your + application. Will control routing, authentication, rate limiting, and other server-side + policies. +- **User Application** — your business logic +- **xDS Outbound** — handles outgoing requests after they leave your application. + Controls endpoint discovery, load balancing, retries, timeouts, and mTLS. + +Both layers receive their configuration from the control plane via xDS, so policies +can be updated at runtime without redeploying the application. + +## Server (Inbound) + +Coming soon. + +## Client (Outbound) + +```bob-svg + +------------------+ +--------------------+ + | | | | + | bootstrap.yaml | | Control Plane | + | | | | + +--------+---------+ +----------+---------+ + | | + v | xDS + +--------+--------------------+ | + | +<---------------+ + | XdsBootstrap | + | | + +--------+--------------------+ + | + | + +--------+--------------------+ + | | + | XdsPreprocessor | + | | + +--------+--------------------+ + | + v + remote service +``` + +```java +Bootstrap bootstrap = XdsResourceReader.fromYamlFile("bootstrap.yaml"); +XdsBootstrap xdsBootstrap = XdsBootstrap.of(bootstrap); +XdsHttpPreprocessor preprocessor = XdsHttpPreprocessor.ofListener("my-listener", xdsBootstrap); +WebClient client = WebClient.of(preprocessor); +``` + +- **bootstrap.yaml** — an Envoy + [Bootstrap](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/bootstrap/v3/bootstrap.proto) + configuration file that tells Armeria where the control plane is and optionally declares + static resources. +- [XdsBootstrap](type:com.linecorp.armeria.xds.XdsBootstrap) — reads the bootstrap + configuration, connects to the control plane, and subscribes to xDS resources + (Listeners, Routes, Clusters, Endpoints). +- [XdsHttpPreprocessor](type:com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor) / + [XdsRpcPreprocessor](type:com.linecorp.armeria.xds.client.endpoint.XdsRpcPreprocessor) — + consumes xDS updates and applies them to each outgoing request (route matching, + endpoint selection, timeouts, retries, mTLS). Passed directly to the Armeria client, + replacing the usual scheme, host, and port. + +### How the preprocessor works + +In a traditional sidecar setup, an outgoing request is intercepted by the Envoy proxy, +which listens on a local port. The request enters the HttpConnectionManager, passes through +downstream HTTP filters, hits the router (which selects the route, endpoint, and TLS +parameters), then passes through upstream HTTP filters before reaching the remote service. + +```bob-svg + request + | + v + Envoy Port (e.g. 15001) + | + v + HttpConnectionManager + | + v + Downstream HTTP Filters + | + v + Router (route match, endpoint select, TLS) + | + v + Upstream HTTP Filters + | + v + remote service +``` + +Armeria replaces this entire flow in-process. The preprocessor acts as the +HttpConnectionManager — a request sent via `WebClient#execute` follows the same +stages without leaving the JVM. + +```java +WebClient client = WebClient.of(preprocessor); +client.execute(HttpRequest.of(HttpMethod.GET, "/hello")); +``` + +```bob-svg + WebClient#execute(HttpRequest) + | + v + Downstream HTTP Filters (preprocessors) + | + v + Router (route match, endpoint select, TLS) + | + v + Upstream HTTP Filters (decorators) + | + v + User Decorators WebClient.builder(preprocessor).decorator(...) + | + v + remote service +``` + +1. **Downstream HTTP filters** — xDS filters declared in the Listener's `http_filters` + (e.g. RBAC, rate limiting). These run as preprocessors. +2. **Router** — matches the request to a route, selects an endpoint using the cluster's + load balancing policy, and determines the protocol and TLS parameters. +3. **Upstream HTTP filters** — per-route filters declared on the router. These run as + decorators. +4. **User decorators** — any decorators added manually via `WebClient.builder(preprocessor).decorator(...)`. + +### Snapshots + +A snapshot is an immutable, point-in-time view of the xDS resource tree. When the +control plane pushes an update, Armeria rebuilds the affected part of the tree and +produces a new snapshot. Snapshots form a tree that mirrors the xDS resource hierarchy: + +```bob-svg + ListenerSnapshot + | + +---> RouteSnapshot + | + +---> ClusterSnapshot + | | + | +---> EndpointSnapshot + | + +---> ClusterSnapshot + | + +---> EndpointSnapshot +``` + +Snapshots have the following guarantees: + +- **Complete** — partial snapshots are never published. A snapshot is only produced + once all dependent resources (routes, clusters, endpoints) have been resolved. +- **Error-free** — if any resource in the tree fails to load, the snapshot is not + published. The preprocessor continues using the last successful snapshot. +- **Immutable** — once published, a snapshot never changes. The preprocessor can read + it on the client thread without synchronization. When a new snapshot is produced, + it simply replaces the previous reference. + +### Threading model + +xDS resource management and request processing run on separate threads: + +```bob-svg + +-------------------------+ + | xDS Event Loop | ++-------------------+ xDS | (xds-common-worker) | +| Control Plane +-------->| | ++-------------------+ | - resource parsing | + | - snapshot updates | + +------------+------------+ + | + | ListenerSnapshot + | + +------------+------------+ + WebClient#execute -------->| Client Thread +---------> remote service + | | + | - filter execution | + | - routing | + | - load balancing | + +-------------------------+ +``` + +- **xDS event loop** — a single dedicated thread (`xds-common-worker`) handles all + communication with the control plane, resource parsing, and snapshot updates. This + thread is created automatically and shared across all `XdsBootstrap` instances. +- **Client thread** — request processing (filter execution, routing, load balancing) + runs on whatever thread the client executes on. + +The two are connected through a thread-safe handoff — when a new `ListenerSnapshot` +arrives on the xDS event loop, it is published to the preprocessor and becomes visible +to all subsequent requests on the client thread. There is no locking on the request path. + +This means: +- xDS updates do not block request processing. +- Request processing does not block xDS updates. +- No additional threads are introduced beyond the single xDS event loop. diff --git a/site-new/src/content/docs/advanced/xds/extensions.mdx b/site-new/src/content/docs/advanced/xds/extensions.mdx new file mode 100644 index 00000000000..67d75728391 --- /dev/null +++ b/site-new/src/content/docs/advanced/xds/extensions.mdx @@ -0,0 +1,139 @@ +--- +sidebar_position: 4 +--- + +# Extensions + +:::note + +The extensions API is not yet finalized and may change in future releases. + +::: + +Envoy's architecture is built around extensions — pluggable components that handle +specific concerns like HTTP filtering, transport sockets, and config sources. Each +extension is identified by a **name** (e.g. `envoy.filters.http.router`) and/or a +**type URL** (e.g. `type.googleapis.com/envoy.extensions.filters.http.router.v3.Router`). + +Armeria follows the same model. Extensions are resolved from a registry by type URL +(primary) or name (fallback). Built-in extensions cover the core xDS functionality, +and custom extensions can be added via the Java `ServiceLoader` mechanism. + +## HTTP filter extensions + +HTTP filters are the most common extension point. They intercept requests as they +pass through the filter chain (see [Concepts — How the preprocessor works](/docs/advanced/xds/concepts#how-the-preprocessor-works)). + +To create a custom HTTP filter, implement +[HttpFilterFactory](type:com.linecorp.armeria.xds.filter.HttpFilterFactory) and +[XdsHttpFilter](type:com.linecorp.armeria.xds.filter.XdsHttpFilter). + +### HttpFilterFactory + +A factory that creates an `XdsHttpFilter` from an xDS `HttpFilter` config. Factories +are responsible for all config parsing, including unwrapping per-route overrides. + +```java +import com.linecorp.armeria.xds.filter.HttpFilterFactory; +import com.linecorp.armeria.xds.filter.XdsHttpFilter; + +public class MyFilterFactory implements HttpFilterFactory { + + @Override + public String name() { + return "my.custom.filter"; + } + + @Override + public List typeUrls() { + return List.of("type.googleapis.com/my.custom.filter.v1.Config"); + } + + @Override + @Nullable + public XdsHttpFilter create(HttpFilter httpFilter, Any config, + XdsResourceValidator validator) { // for validating and unpacking Any protos + // Parse config and return an XdsHttpFilter, or null to skip + MyConfig myConfig = validator.unpack(config, MyConfig.class); + return new MyFilter(myConfig); + } +} +``` + +### XdsHttpFilter + +The resolved filter returned by the factory. It can act as a downstream filter +(preprocessor), an upstream filter (decorator), or both: + +```java +import com.linecorp.armeria.xds.filter.XdsHttpFilter; + +public class MyFilter implements XdsHttpFilter { + + @Override + public HttpPreprocessor httpPreprocessor() { + // Runs as a downstream filter (before routing) + return (delegate, ctx, req) -> { + // custom logic + return delegate.execute(ctx, req); + }; + } + + @Override + public DecoratingHttpClientFunction httpDecorator() { + // Runs as an upstream filter (after routing) + return (delegate, ctx, req) -> { + // custom logic + return delegate.execute(ctx, req); + }; + } +} +``` + +### Registration + +Register your factory using Java's `ServiceLoader`. Create the file: + +``` +META-INF/services/com.linecorp.armeria.xds.filter.HttpFilterFactory +``` + +With the fully qualified class name of your factory: + +``` +com.example.MyFilterFactory +``` + +The factory will be discovered automatically when `XdsBootstrap` is created. + +### Usage in bootstrap YAML + +Once registered, reference your filter by name and type URL in the Listener's +`http_filters` list: + +```yaml +listeners: + - name: my-listener + api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + http_filters: + - name: my.custom.filter + typed_config: + "@type": type.googleapis.com/my.custom.filter.v1.Config + some_field: some_value + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + name: route + virtual_hosts: + - name: my-vhost + domains: ["*"] + routes: + - match: { prefix: "/" } + route: { cluster: my-cluster } +``` + +Filters are executed in the order they are declared. The `envoy.filters.http.router` +filter must always be the last filter in the chain. diff --git a/site-new/src/content/docs/advanced/xds/getting-started.mdx b/site-new/src/content/docs/advanced/xds/getting-started.mdx new file mode 100644 index 00000000000..9037abb4394 --- /dev/null +++ b/site-new/src/content/docs/advanced/xds/getting-started.mdx @@ -0,0 +1,259 @@ +--- +sidebar_position: 1 +--- + +# Getting started + +Armeria's xDS module lets clients discover endpoints, route requests, and apply policies +(retry, timeout, load balancing) dynamically via an xDS control plane such as Istio or +the Envoy control plane. See [Supported features](/docs/advanced/xds/supported-features) +for the full list of xDS resources Armeria understands, or [Concepts](/docs/advanced/xds/concepts) +for a deeper look at the architecture. + +## Dependencies + + + +## Setting up a new client + +### Creating an Envoy Bootstrap + +The xDS module is driven by an Envoy +[Bootstrap](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/bootstrap/v3/bootstrap.proto) +protobuf message. The easiest way to define one is to write a YAML file (the same format used +by Envoy) and load it with [XdsResourceReader](type:com.linecorp.armeria.xds.XdsResourceReader). + +#### Static resources + +All listeners, clusters, and endpoints are declared inline — no control plane needed. + +Create a file `bootstrap.yaml`: + +```yaml +static_resources: + listeners: + - name: my-listener + api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + route_config: + name: route + virtual_hosts: + - name: my-vhost + domains: ["*"] + routes: + - match: { prefix: "/" } + route: { cluster: my-cluster } + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + clusters: + - name: my-cluster + type: STATIC + load_assignment: + cluster_name: my-cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8080 +``` + +Then load it in Java: + +```java +import com.linecorp.armeria.xds.XdsResourceReader; +import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap; + +Bootstrap bootstrap = XdsResourceReader.fromYamlFile("bootstrap.yaml"); +``` + +#### Dynamic resources (control plane) + +Point the bootstrap at an xDS control plane using `ads_config`. Only a bootstrap cluster +is declared statically; everything else is fetched dynamically. + +```yaml +static_resources: + clusters: + - name: xds-cluster + type: STATIC + load_assignment: + cluster_name: xds-cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: istiod.istio-system.svc + port_value: 15010 +dynamic_resources: + ads_config: + api_type: DELTA_GRPC + grpc_services: + - envoy_grpc: + cluster_name: xds-cluster + lds_config: + ads: {} + cds_config: + ads: {} +``` + +```java +Bootstrap bootstrap = XdsResourceReader.fromYamlFile("bootstrap.yaml"); +``` + +### Creating the XdsBootstrap + +Wrap the Envoy `Bootstrap` in an Armeria [XdsBootstrap](type:com.linecorp.armeria.xds.XdsBootstrap): + +```java +import com.linecorp.armeria.xds.XdsBootstrap; + +// Simple creation +XdsBootstrap xdsBootstrap = XdsBootstrap.of(bootstrap); + +// Or use the builder for customization +XdsBootstrap xdsBootstrap = XdsBootstrap.builder(bootstrap) + .meterRegistry(meterRegistry) + .build(); +``` + +### Creating the preprocessor + +An [XdsHttpPreprocessor](type:com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor) +bridges xDS config into an Armeria client. It resolves endpoints, applies route-level policies, +and selects the target cluster — all transparently. For Thrift clients, use +[XdsRpcPreprocessor](type:com.linecorp.armeria.xds.client.endpoint.XdsRpcPreprocessor) instead. + +```java +import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; +import com.linecorp.armeria.xds.client.endpoint.XdsRpcPreprocessor; + +// For HTTP and gRPC clients +XdsHttpPreprocessor preprocessor = + XdsHttpPreprocessor.ofListener("my-listener", xdsBootstrap); + +// For Thrift clients +XdsRpcPreprocessor rpcPreprocessor = + XdsRpcPreprocessor.ofListener("my-listener", xdsBootstrap); +``` + +- `"my-listener"` — the name of the Listener resource to watch. If a matching listener + is declared in `static_resources` it will be used directly; otherwise it will be + fetched from the control plane via LDS. +- `xdsBootstrap` — the [XdsBootstrap](type:com.linecorp.armeria.xds.XdsBootstrap) created + in the previous step. + +The preprocessor begins fetching xDS resources immediately. You can wait for the initial +configuration to be loaded before serving traffic: + +```java +preprocessor.whenReady().join(); +``` + +### Building the client and making requests + +Pass the preprocessor directly to the client factory method. No scheme, host, or port is needed — +the preprocessor resolves everything from the xDS configuration. + +**HTTP:** + +```java +WebClient client = WebClient.of(preprocessor); +AggregatedHttpResponse res = client.get("/hello").aggregate().join(); +``` + +**gRPC:** + +```java +MyServiceBlockingStub stub = GrpcClients.newClient(preprocessor, MyServiceBlockingStub.class); +stub.myMethod(request); +``` + +**Thrift:** + +```java +HelloService.Iface iface = ThriftClients.newClient(rpcPreprocessor, HelloService.Iface.class); +iface.hello(); +``` + +### Cleanup + +Close the preprocessor and bootstrap when they are no longer needed: + +```java +preprocessor.close(); +xdsBootstrap.close(); +``` + +## Field mappings + +With xDS, settings like endpoints and timeouts are configured in YAML rather than in Java code. + +**Armeria:** + +```java +WebClient client = WebClient.builder("http://my-service:8080") + .responseTimeout(Duration.ofSeconds(5)) + .build(); +``` + +**xDS — endpoint address and port** (in a Cluster): + +```yaml +clusters: + - name: my-cluster + load_assignment: + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: my-service + port_value: 8080 +``` + +**xDS — response timeout** (in a VirtualHost route): + +```yaml +virtual_hosts: + - name: my-vhost + routes: + - match: { prefix: "/" } + route: + cluster: my-cluster + timeout: 5s +``` + +### Real-life example + +You can combine xDS with standard Armeria client options by using the builder form. +Decorators from xDS configuration wrap around decorators you add via the builder. +The xDS decorators run first (outermost), then your custom decorators, then the wire. + +For any field **not** explicitly set in the xDS configuration, the original Armeria +client defaults apply. For example, if no `timeout` is specified on a route, the +client's `responseTimeout` is used as-is. + +```java +import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.client.logging.LoggingClient; + +WebClient client = + WebClient.builder(preprocessor) + // Overridden per-request if the xDS route sets `timeout`. + // Used as the default when the route does not set `timeout`. + .responseTimeoutMillis(3000) + // Always applied regardless of xDS configuration. + .decorator(LoggingClient.newDecorator()) + .build(); +``` diff --git a/site-new/src/content/docs/advanced/xds/introduction.mdx b/site-new/src/content/docs/advanced/xds/introduction.mdx new file mode 100644 index 00000000000..aef138a37af --- /dev/null +++ b/site-new/src/content/docs/advanced/xds/introduction.mdx @@ -0,0 +1,39 @@ +--- +sidebar_position: 0 +--- + +# Introduction + +xDS is the protocol that service meshes use to push configuration — routing rules, +endpoints, load balancing policies, TLS certificates — from a control plane to the +data plane. It was originally designed for Envoy but has become a +[standard](https://github.com/cncf/xds) adopted by multiple projects. + +Armeria implements the xDS protocol directly in your application. Instead of routing +traffic through a sidecar proxy, Armeria speaks xDS to a control plane and applies +the configuration in-process — endpoint discovery, routing, load balancing, retries, +timeouts, and mTLS all happen without an extra network hop. + +## Current status + +**Outbound (client)** — available now. Armeria clients can discover endpoints, match +routes, apply retry and timeout policies, and establish mTLS connections, all driven +by xDS configuration from a control plane or static bootstrap. + +**Inbound (server)** — coming soon. Server-side xDS will handle incoming request +routing, authentication, rate limiting, and other policies. + +## Documentation guide + +- [Getting started](/docs/advanced/xds/getting-started) — dependencies, bootstrap + setup, creating a client, and making your first xDS-powered request +- [Concepts](/docs/advanced/xds/concepts) — how Armeria replaces the sidecar proxy, + the preprocessor pipeline, snapshots, and the threading model +- [Metrics](/docs/advanced/xds/metrics) — control plane, resource, and load balancer + metrics exposed by the xDS module +- [Extensions](/docs/advanced/xds/extensions) — adding custom HTTP filter extensions + via the `HttpFilterFactory` SPI +- [Supported features](/docs/advanced/xds/supported-features) — which xDS fields + Armeria recognizes and how unsupported fields are handled +- [Running with Istio](/docs/advanced/xds/running-with-istio) — connecting to Istio's control + plane using the sidecar-injected bootstrap diff --git a/site-new/src/content/docs/advanced/xds/metrics.mdx b/site-new/src/content/docs/advanced/xds/metrics.mdx new file mode 100644 index 00000000000..b27050b19f6 --- /dev/null +++ b/site-new/src/content/docs/advanced/xds/metrics.mdx @@ -0,0 +1,103 @@ +--- +sidebar_position: 3 +--- + +# Metrics + +Armeria's xDS module exposes metrics for monitoring control plane communication, +resource state, and load balancer behavior. All metrics are prefixed with `armeria.xds` +by default. + +:::note + +The metrics listed on this page are not yet finalized and may change in future releases. + +::: + +## Configuration + +Metrics are configured when creating the [XdsBootstrap](type:com.linecorp.armeria.xds.XdsBootstrap): + +```java +XdsBootstrap xdsBootstrap = XdsBootstrap.builder(bootstrap) + .meterRegistry(meterRegistry) + .meterIdPrefix(new MeterIdPrefix("my.custom.prefix")) + .build(); +``` + +## Control plane metrics + +These metrics track xDS stream lifecycle and communication with the control plane. + +All metrics in this group are tagged with: +- `type` — the config source type (e.g. `ads`, `api_config_source`) +- `name` — the gRPC cluster name +- `xdsType` — the resource type (e.g. `listeners`, `routes`, `clusters`, `endpoints`, `secrets`) + +| Metric | Type | Description | +|---|---|---| +| `configsource.stream.active` | Gauge | Whether the stream is currently active (`1` or `0`) | +| `configsource.stream.opened` | Counter | Number of times the xDS stream was opened | +| `configsource.stream.completed` | Counter | Number of normal stream closures | +| `configsource.stream.error` | Counter | Number of stream errors | +| `configsource.stream.request` | Counter | Discovery requests sent | +| `configsource.stream.response` | Counter | Discovery responses received | +| `configsource.resource.parse.success` | Counter | Resources successfully parsed | +| `configsource.resource.parse.rejected` | Counter | Resources rejected due to parse errors | + +## Resource node metrics + +These metrics track the state of individual xDS resources. They are created per resource +when a subscription is established and removed when the last subscriber unsubscribes. + +All metrics in this group are tagged with: +- `name` — the resource name (e.g. listener name, cluster name) +- `type` — the resource type (e.g. `listener`, `cluster`, `cluster_load_assignment`) + +| Metric | Type | Description | +|---|---|---| +| `resource.node.revision` | Gauge | Latest revision of the loaded resource | +| `resource.node.error` | Counter | Errors encountered when updating this resource | +| `resource.node.missing` | Counter | Times this resource was reported as missing | + +## Load balancer metrics + +These metrics track endpoint selection, health, and locality distribution. They are +created dynamically per cluster and updated as the load balancer state changes. + +### Cluster-level metrics + +Tagged with `cluster` (the cluster name). + +| Metric | Type | Description | +|---|---|---| +| `lb.resource.updated.revision` | Gauge | Latest cluster resource revision | +| `lb.resource.updated.count` | Counter | Number of cluster resource updates | +| `lb.endpoints.updated.revision` | Gauge | Latest endpoint snapshot revision | +| `lb.endpoints.updated.count` | Counter | Number of endpoint updates | +| `lb.state.updated.revision` | Gauge | Latest load balancer state revision | +| `lb.state.updated.count` | Counter | Number of load balancer state updates | +| `lb.state.rejected.revision` | Gauge | Revision of last rejected state | +| `lb.state.rejected.count` | Counter | Number of rejected load balancer states | +| `lb.state.subsets` | Gauge | Number of endpoint subsets | + +### Priority-level metrics + +Tagged with `cluster` and `priority` (integer, `0` is highest). + +| Metric | Type | Description | +|---|---|---| +| `lb.state.load.healthy` | Gauge | Healthy endpoint load at this priority | +| `lb.state.load.degraded` | Gauge | Degraded endpoint load at this priority | +| `lb.state.panic` | Gauge | Whether panic mode is active (`1` or `0`) | + +### Locality-level metrics + +Tagged with `cluster`, `priority`, `region`, `zone`, and `sub_zone`. + +| Metric | Type | Description | +|---|---|---| +| `lb.membership.total` | Gauge | Total endpoints in this locality | +| `lb.membership.healthy` | Gauge | Healthy endpoints in this locality | +| `lb.membership.degraded` | Gauge | Degraded endpoints in this locality | +| `lb.locality.weight` | Gauge | Weight assigned to this locality | diff --git a/site-new/src/content/docs/advanced/xds/running-with-istio.mdx b/site-new/src/content/docs/advanced/xds/running-with-istio.mdx new file mode 100644 index 00000000000..2d60cb05cd2 --- /dev/null +++ b/site-new/src/content/docs/advanced/xds/running-with-istio.mdx @@ -0,0 +1,84 @@ +--- +sidebar_position: 6 +title: Running with Istio +--- + +# Running with Istio + +Armeria can connect directly to Istio's control plane (Istiod) using the xDS protocol, +allowing your application to participate in an Istio service mesh without relying on a +sidecar proxy for outbound traffic. + +## Prerequisites + +This guide assumes your pod has Istio sidecar injection enabled. When sidecar mode is +active, Istio injects an Envoy bootstrap configuration at `/etc/istio/proxy/envoy-rev.json` +containing the control plane address, node identity, and TLS certificates needed to +communicate with Istiod. + +Armeria can load this bootstrap directly and use it to subscribe to xDS resources — +the same resources that would normally be consumed by the Envoy sidecar. + +## Loading the Istio bootstrap + +```java +import com.linecorp.armeria.xds.XdsResourceReader; +import com.linecorp.armeria.xds.XdsBootstrap; +import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; + +import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap; + +// Load the bootstrap injected by Istio +Bootstrap bootstrap = XdsResourceReader.fromJsonFile("/etc/istio/proxy/envoy-rev.json"); + +XdsBootstrap xdsBootstrap = XdsBootstrap.of(bootstrap); +``` + +## Making requests + +Once you know the listener name, create a preprocessor and use it like any other +Armeria client: + +```java +import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor; +import com.linecorp.armeria.client.WebClient; + +// Resolve the listener name for the outbound listener. This may be different based on your configuration. +String listenerName = serviceClusterIp + "_8080"; + +XdsHttpPreprocessor preprocessor = + XdsHttpPreprocessor.ofListener(listenerName, xdsBootstrap); + +WebClient client = WebClient.of(preprocessor); +AggregatedHttpResponse response = client.get("/hello").aggregate().join(); +``` + +Armeria fetches the full listener configuration from Istiod — including routes, +clusters, and endpoints — exactly as the Envoy sidecar would. + +## What you get + +By connecting to Istio's control plane directly, Armeria receives the same +configuration that Envoy sidecars use: + +- **Endpoint discovery** — service endpoints are resolved dynamically via EDS +- **Load balancing** — cluster-level load balancing policies are applied +- **mTLS** — certificates provisioned by Istio are used for secure communication +- **Route matching** — Istio's VirtualService and DestinationRule configurations + are reflected in the xDS routes and clusters + +## Istio-specific filters + +Istio's xDS configuration may include HTTP filters that are specific to Istio +(e.g. `envoy.filters.http.wasm`, `istio.stats`). Armeria does not ship built-in +support for these filters, so you may need to register custom +[HttpFilterFactory](/docs/advanced/xds/extensions) implementations that handle +or skip them. + +:::note + +Armeria's Istio integration is under active development. Not all Istio features +are supported yet — see [Supported features](/docs/advanced/xds/supported-features) +for the current coverage. + +::: diff --git a/site-new/src/content/docs/advanced/xds/supported-features.mdx b/site-new/src/content/docs/advanced/xds/supported-features.mdx new file mode 100644 index 00000000000..3f43af26bb9 --- /dev/null +++ b/site-new/src/content/docs/advanced/xds/supported-features.mdx @@ -0,0 +1,91 @@ +--- +sidebar_position: 5 +--- + +# Supported features + +Armeria does not implement every field in the xDS specification. To help users +identify which fields are supported, support information is embedded directly in +the protobuf definitions using a custom annotation. + +## Supported field validation + +Each supported field in the xDS proto files is annotated with +`(armeria.xds.supported) = true`. When a protobuf message is received from the +control plane, Armeria can validate it against these annotations — if a field that +is **not** annotated as supported contains a non-default value, the validator +detects it. + +The behavior when an unsupported field is detected is controlled by an +`UnsupportedFieldHandler`. The following built-in handlers are available: + +| Handler | Behavior | +|---|---| +| `UnsupportedFieldHandler.warn()` | Logs a warning (default) | +| `UnsupportedFieldHandler.reject()` | Throws an `IllegalArgumentException` | +| `UnsupportedFieldHandler.ignore()` | Silently ignores the field | + +:::note + +The list below is a summary and may become stale. The proto annotations are the +source of truth for which fields are supported. You can find them in the +`xds-api/src/main/proto/` directory of the `armeria-xds-api` module, which contains +the annotated envoy proto files and the `armeria/xds/supported.proto` annotation definition. + +::: + +## Bootstrap + +- `static_resources` — listeners, clusters, secrets +- `dynamic_resources` — LDS, CDS, ADS config +- `node` — node identification +- `cluster_manager.local_cluster_name` + +## Listeners + +- `name` +- `api_listener` — HttpConnectionManager via API listener +- HttpConnectionManager: stat_prefix, rds, route_config, http_filters + +## Routes + +- `RouteConfiguration` — name, virtual_hosts, ignore_port_in_host_matching +- `VirtualHost` — name, domains, routes, typed_per_filter_config, retry_policy +- `Route` — name, match, route, typed_per_filter_config +- `RouteMatch` — prefix, path, safe_regex, path_separated_prefix, + case_sensitive, headers, query_parameters, grpc +- `RouteAction` — cluster, metadata_match, timeout + +## Clusters + +- `name`, `type` (`STATIC`, `STRICT_DNS`, `EDS`) +- `lb_policy` (`ROUND_ROBIN`, `LEAST_REQUEST`, `RANDOM`) +- `eds_cluster_config` — EDS config source and service name +- `lb_subset_config` — fallback policy, subset selectors +- `slow_start_config` — slow start window, aggression, min weight percent +- `transport_socket_matches` — name, match, transport socket +- Health checking: http_health_check (host, path, method), thresholds + +## Endpoints + +- `ClusterLoadAssignment` — cluster_name, endpoints, policy + (overprovisioning factor, weighted priority health) +- `LocalityLbEndpoints` — locality, lb_endpoints, load_balancing_weight, + priority, metadata +- `LbEndpoint` — endpoint, health_status, metadata, load_balancing_weight +- `Endpoint` — address, health_check_config, hostname + +## TLS + +- `UpstreamTlsContext` — common_tls_context, sni +- `CommonTlsContext` — tls_certificates, tls_certificate_sds_secret_configs, + validation_context, validation_context_sds_secret_config, + combined_validation_context +- `TlsCertificate` — certificate_chain, private_key, password, watched_directory +- `CertificateValidationContext` — trusted_ca, system_root_certs, + watched_directory, match_typed_subject_alt_names +- SDS: `SdsSecretConfig` — name, sds_config + +## String matching + +- exact, prefix, suffix, safe_regex, contains, ignore_case diff --git a/site-new/src/content/docs/client/service-discovery.mdx b/site-new/src/content/docs/client/service-discovery.mdx index 87aa8286388..361049f4364 100644 --- a/site-new/src/content/docs/client/service-discovery.mdx +++ b/site-new/src/content/docs/client/service-discovery.mdx @@ -1,5 +1,11 @@ # Client-side load balancing and service discovery +:::tip + +If you are using a service mesh with xDS (e.g., Istio or Envoy), see the [xDS section](/docs/advanced/xds/getting-started) for xDS-based service discovery and load balancing. + +::: + You can configure an Armeria client to distribute its requests to more than one server autonomously, unlike traditional server-side load balancing where the requests go through a dedicated load balancer such as [L4 and L7 switches](https://en.wikipedia.org/wiki/Multilayer_switch#Layer_4%E2%80%937_switch,_web_switch,_or_content_switch). diff --git a/xds/build.gradle b/xds/build.gradle index b373688390d..f4e69543cc0 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -10,6 +10,8 @@ dependencies { api project(':xds-api') api project(':xds-validator') + implementation libs.jackson.dataformat.yaml + implementation libs.protobuf.java.util implementation libs.re2j testImplementation libs.controlplane.server diff --git a/xds/src/main/java/com/linecorp/armeria/xds/XdsResourceReader.java b/xds/src/main/java/com/linecorp/armeria/xds/XdsResourceReader.java new file mode 100644 index 00000000000..e6794a485dd --- /dev/null +++ b/xds/src/main/java/com/linecorp/armeria/xds/XdsResourceReader.java @@ -0,0 +1,200 @@ +/* + * Copyright 2024 LINE Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.xds; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.reflections.Reflections; +import org.reflections.scanners.SubTypesScanner; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.FilterBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.GeneratedMessageV3; +import com.google.protobuf.util.JsonFormat; +import com.google.protobuf.util.JsonFormat.Parser; +import com.google.protobuf.util.JsonFormat.TypeRegistry; + +import com.linecorp.armeria.common.annotation.UnstableApi; + +import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap; + +/** + * A utility for reading xDS resources from YAML or JSON. + * + *

All well-known Envoy protobuf types (under the {@code io.envoyproxy} package) are + * automatically registered so that {@code @type} fields in YAML/JSON are resolved correctly. + * + *

Example usage: + *

{@code
+ * Bootstrap bootstrap = XdsResourceReader.fromYamlFile("bootstrap.yaml");
+ * }
+ */ +@UnstableApi +public final class XdsResourceReader { + + private static final Logger logger = LoggerFactory.getLogger(XdsResourceReader.class); + + private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); + private static final ObjectMapper jsonMapper = new ObjectMapper(); + + private static final class DefaultParserHolder { + static final Parser INSTANCE = JsonFormat.parser().usingTypeRegistry(buildDefaultTypeRegistry()); + + private static TypeRegistry buildDefaultTypeRegistry() { + final TypeRegistry.Builder builder = TypeRegistry.newBuilder(); + final ConfigurationBuilder configuration = new ConfigurationBuilder() + // list of jars containing extensions + .setUrls(ClasspathHelper.forPackage("io.envoyproxy.envoy.extensions")) + .setScanners(new SubTypesScanner()) + // within each jar, only scan classes matching package name + .filterInputsBy(new FilterBuilder().include( + FilterBuilder.prefix("io.envoyproxy.envoy.extensions"))); + final Reflections reflections = new Reflections(configuration); + for (Class clazz : reflections.getSubTypesOf(GeneratedMessageV3.class)) { + try { + final Descriptor descriptor = + (Descriptor) clazz.getMethod("getDescriptor").invoke(null); + builder.add(descriptor); + } catch (Exception e) { + logger.warn("Failed to register descriptor for {}", clazz.getName(), e); + } + } + return builder.build(); + } + } + + /** + * Reads a {@link Bootstrap} from the given YAML string. + */ + public static Bootstrap fromYaml(String yaml) { + requireNonNull(yaml, "yaml"); + return XdsResourceReader.parseYaml(yaml, Bootstrap.class, + DefaultParserHolder.INSTANCE); + } + + /** + * Reads a {@link Bootstrap} from the YAML file at the given path. + */ + public static Bootstrap fromYamlFile(Path path) { + requireNonNull(path, "path"); + return fromYaml(readFile(path)); + } + + /** + * Reads a {@link Bootstrap} from the YAML file at the given path. + */ + public static Bootstrap fromYamlFile(String path) { + requireNonNull(path, "path"); + return fromYamlFile(Paths.get(path)); + } + + /** + * Reads a protobuf message of the specified type from the given YAML string. + */ + public static T fromYaml(String yaml, Class clazz) { + requireNonNull(yaml, "yaml"); + requireNonNull(clazz, "clazz"); + return parseYaml(yaml, clazz, DefaultParserHolder.INSTANCE); + } + + /** + * Reads a {@link Bootstrap} from the given JSON string. + */ + public static Bootstrap fromJson(String json) { + requireNonNull(json, "json"); + return XdsResourceReader.parseJson(json, Bootstrap.class, + DefaultParserHolder.INSTANCE); + } + + /** + * Reads a {@link Bootstrap} from the JSON file at the given path. + */ + public static Bootstrap fromJsonFile(Path path) { + requireNonNull(path, "path"); + return fromJson(readFile(path)); + } + + /** + * Reads a {@link Bootstrap} from the JSON file at the given path. + */ + public static Bootstrap fromJsonFile(String path) { + requireNonNull(path, "path"); + return fromJsonFile(Paths.get(path)); + } + + /** + * Reads a protobuf message of the specified type from the given JSON string. + */ + public static T fromJson(String json, Class clazz) { + requireNonNull(json, "json"); + requireNonNull(clazz, "clazz"); + return parseJson(json, clazz, DefaultParserHolder.INSTANCE); + } + + @SuppressWarnings("unchecked") + private static T parseYaml(String yaml, Class clazz, + Parser parser) { + final GeneratedMessageV3.Builder builder; + try { + builder = (GeneratedMessageV3.Builder) clazz.getMethod("newBuilder").invoke(null); + final JsonNode jsonNode = yamlMapper.reader().readTree(yaml); + parser.merge(jsonNode.toString(), builder); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to parse YAML as " + clazz.getSimpleName(), e); + } + return (T) builder.build(); + } + + @SuppressWarnings("unchecked") + private static T parseJson(String json, Class clazz, + Parser parser) { + final GeneratedMessageV3.Builder builder; + try { + builder = (GeneratedMessageV3.Builder) clazz.getMethod("newBuilder").invoke(null); + final JsonNode jsonNode = jsonMapper.reader().readTree(json); + parser.merge(jsonNode.toString(), builder); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to parse JSON as " + clazz.getSimpleName(), e); + } + return (T) builder.build(); + } + + private static String readFile(Path path) { + try { + final byte[] bytes = Files.readAllBytes(path); + return new String(bytes, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read file: " + path, e); + } + } + + private XdsResourceReader() {} +} diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouteConfigSelector.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouteConfigSelector.java index 0c2d758b4b6..e2df35b0e95 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouteConfigSelector.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouteConfigSelector.java @@ -16,8 +16,11 @@ package com.linecorp.armeria.xds.client.endpoint; +import java.util.concurrent.CompletableFuture; + import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.common.annotation.Nullable; +import com.linecorp.armeria.common.util.UnmodifiableFuture; import com.linecorp.armeria.internal.client.AbstractAsyncSelector; import com.linecorp.armeria.xds.ListenerRoot; import com.linecorp.armeria.xds.ListenerSnapshot; @@ -26,6 +29,8 @@ final class RouteConfigSelector extends AbstractAsyncSelector implements SnapshotWatcher { + private final CompletableFuture whenReady = new CompletableFuture<>(); + @Nullable private volatile RouteConfig routeConfig; @@ -45,6 +50,11 @@ public void onUpdate(@Nullable ListenerSnapshot snapshot, @Nullable Throwable t) return; } routeConfig = new RouteConfig(snapshot); + whenReady.complete(null); refresh(); } + + CompletableFuture whenReady() { + return UnmodifiableFuture.wrap(whenReady); + } } diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsPreprocessor.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsPreprocessor.java index bc5ce85cacf..138327e83dc 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsPreprocessor.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsPreprocessor.java @@ -84,6 +84,14 @@ private O execute0(PreClient delegate, PreClientRequestContext ctx, I req, abstract O execute1(PreClient delegate, PreClientRequestContext ctx, I req, RouteConfig routeConfig) throws Exception; + /** + * Returns a {@link CompletableFuture} that completes when the initial xDS configuration + * has been received and the preprocessor is ready to route requests. + */ + public CompletableFuture whenReady() { + return routeConfigSelector.whenReady(); + } + @Override public void close() { listenerRoot.close();