Skip to content

Commit 1bdd051

Browse files
committed
minimal impl
1 parent 91c38a9 commit 1bdd051

4 files changed

Lines changed: 389 additions & 11 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
* Copyright 2026 LY Corporation
3+
*
4+
* LY Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://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, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package com.linecorp.armeria.xds.it;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.io.File;
21+
import java.nio.file.Files;
22+
import java.util.Base64;
23+
import java.util.List;
24+
25+
import org.junit.jupiter.api.Order;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.extension.RegisterExtension;
28+
29+
import com.linecorp.armeria.client.ClientRequestContext;
30+
import com.linecorp.armeria.client.ClientRequestContextCaptor;
31+
import com.linecorp.armeria.client.Clients;
32+
import com.linecorp.armeria.client.WebClient;
33+
import com.linecorp.armeria.common.AggregatedHttpResponse;
34+
import com.linecorp.armeria.common.HttpMethod;
35+
import com.linecorp.armeria.common.HttpRequest;
36+
import com.linecorp.armeria.common.HttpResponse;
37+
import com.linecorp.armeria.common.HttpStatus;
38+
import com.linecorp.armeria.common.SessionProtocol;
39+
import com.linecorp.armeria.common.logging.RequestLog;
40+
import com.linecorp.armeria.common.logging.RequestLogAccess;
41+
import com.linecorp.armeria.server.ServerBuilder;
42+
import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension;
43+
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
44+
import com.linecorp.armeria.xds.XdsBootstrap;
45+
import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor;
46+
47+
class MixedTransportSocketRetryTest {
48+
49+
@RegisterExtension
50+
@Order(0)
51+
static final XdsCertificateExtension cert =
52+
new XdsCertificateExtension(new SelfSignedCertificateExtension());
53+
54+
@RegisterExtension
55+
@Order(1)
56+
static final ServerExtension server = new ServerExtension() {
57+
@Override
58+
protected void configure(ServerBuilder sb) {
59+
sb.http(0);
60+
sb.https(0);
61+
sb.tls(cert.tlsKeyPair());
62+
sb.service("/", (ctx, req) -> HttpResponse.of(HttpStatus.SERVICE_UNAVAILABLE));
63+
}
64+
};
65+
66+
// language=YAML
67+
private static final String BOOTSTRAP_TEMPLATE =
68+
"""
69+
static_resources:
70+
listeners:
71+
- name: my-listener
72+
api_listener:
73+
api_listener:
74+
"@type": type.googleapis.com/envoy.extensions.filters.network.\
75+
http_connection_manager.v3.HttpConnectionManager
76+
stat_prefix: http
77+
route_config:
78+
name: local_route
79+
virtual_hosts:
80+
- name: local_service
81+
domains: [ "*" ]
82+
routes:
83+
- match:
84+
prefix: /
85+
route:
86+
cluster: my-cluster
87+
retry_policy:
88+
retry_on: "5xx"
89+
num_retries: 3
90+
http_filters:
91+
- name: envoy.filters.http.router
92+
typed_config:
93+
"@type": type.googleapis.com/envoy.extensions.filters.\
94+
http.router.v3.Router
95+
clusters:
96+
- name: my-cluster
97+
type: STATIC
98+
lb_policy: ROUND_ROBIN
99+
load_assignment:
100+
cluster_name: my-cluster
101+
endpoints:
102+
- lb_endpoints:
103+
- endpoint:
104+
address:
105+
socket_address:
106+
address: 127.0.0.1
107+
port_value: %d
108+
metadata:
109+
filter_metadata:
110+
"envoy.transport_socket_match":
111+
mode: "tls"
112+
- endpoint:
113+
address:
114+
socket_address:
115+
address: 127.0.0.1
116+
port_value: %d
117+
metadata:
118+
filter_metadata:
119+
"envoy.transport_socket_match":
120+
mode: "plaintext"
121+
transport_socket:
122+
name: envoy.transport_sockets.tls
123+
typed_config:
124+
"@type": type.googleapis.com/envoy.extensions.transport_sockets.\
125+
tls.v3.UpstreamTlsContext
126+
common_tls_context:
127+
validation_context:
128+
trusted_ca:
129+
inline_bytes: %s
130+
transport_socket_matches:
131+
- name: tls-match
132+
match:
133+
mode: "tls"
134+
transport_socket:
135+
name: envoy.transport_sockets.tls
136+
typed_config:
137+
"@type": type.googleapis.com/envoy.extensions.transport_sockets.\
138+
tls.v3.UpstreamTlsContext
139+
common_tls_context:
140+
validation_context:
141+
trusted_ca:
142+
inline_bytes: %s
143+
- name: plaintext-match
144+
match:
145+
mode: "plaintext"
146+
transport_socket:
147+
name: envoy.transport_sockets.raw_buffer
148+
typed_config:
149+
"@type": type.googleapis.com/envoy.extensions.transport_sockets.\
150+
raw_buffer.v3.RawBuffer
151+
""";
152+
153+
@Test
154+
void retryChildContextsHaveCorrectSessionProtocol() throws Exception {
155+
final String ca = base64Cert(cert.certificateFile());
156+
final String yaml = BOOTSTRAP_TEMPLATE.formatted(
157+
server.httpsPort(), server.httpPort(), ca, ca);
158+
159+
try (XdsBootstrap xdsBootstrap = XdsBootstrap.of(XdsResourceReader.fromYaml(yaml));
160+
XdsHttpPreprocessor preprocessor =
161+
XdsHttpPreprocessor.ofListener("my-listener", xdsBootstrap)) {
162+
163+
final WebClient client = WebClient.of(preprocessor);
164+
165+
// Send 2 requests so round robin hits both endpoints across attempts.
166+
for (int i = 0; i < 2; i++) {
167+
final ClientRequestContext parentCtx;
168+
try (ClientRequestContextCaptor captor = Clients.newContextCaptor()) {
169+
final AggregatedHttpResponse res =
170+
client.blocking().execute(HttpRequest.of(HttpMethod.GET, "/"));
171+
assertThat(res.status()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
172+
parentCtx = captor.get();
173+
}
174+
175+
// 1 original + 3 retries = 4 child contexts
176+
final List<RequestLogAccess> children = parentCtx.log().children();
177+
assertThat(children).hasSize(4);
178+
179+
for (RequestLogAccess childLogAccess : children) {
180+
final RequestLog childLog = childLogAccess.whenComplete().join();
181+
final SessionProtocol protocol = childLog.sessionProtocol();
182+
final ClientRequestContext childCtx =
183+
(ClientRequestContext) childLogAccess.context();
184+
final int port = childCtx.endpoint().port();
185+
if (port == server.httpsPort()) {
186+
assertThat(protocol.isTls()).isTrue();
187+
} else {
188+
assertThat(protocol.isTls()).isFalse();
189+
}
190+
}
191+
}
192+
}
193+
}
194+
195+
private static String base64Cert(File certFile) throws Exception {
196+
return Base64.getEncoder().encodeToString(Files.readAllBytes(certFile.toPath()));
197+
}
198+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright 2026 LY Corporation
3+
*
4+
* LY Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://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, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package com.linecorp.armeria.xds.it;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.io.File;
21+
import java.nio.file.Files;
22+
import java.util.Base64;
23+
24+
import org.junit.jupiter.api.Order;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.extension.RegisterExtension;
27+
28+
import com.linecorp.armeria.client.BlockingWebClient;
29+
import com.linecorp.armeria.client.ClientRequestContext;
30+
import com.linecorp.armeria.client.ClientRequestContextCaptor;
31+
import com.linecorp.armeria.client.Clients;
32+
import com.linecorp.armeria.client.WebClient;
33+
import com.linecorp.armeria.common.AggregatedHttpResponse;
34+
import com.linecorp.armeria.common.HttpResponse;
35+
import com.linecorp.armeria.common.HttpStatus;
36+
import com.linecorp.armeria.common.SessionProtocol;
37+
import com.linecorp.armeria.common.logging.RequestLog;
38+
import com.linecorp.armeria.server.ServerBuilder;
39+
import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension;
40+
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
41+
import com.linecorp.armeria.xds.XdsBootstrap;
42+
import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor;
43+
44+
class MixedTransportSocketSessionProtocolTest {
45+
46+
@RegisterExtension
47+
@Order(0)
48+
static final XdsCertificateExtension cert =
49+
new XdsCertificateExtension(new SelfSignedCertificateExtension());
50+
51+
@RegisterExtension
52+
@Order(1)
53+
static final ServerExtension server = new ServerExtension() {
54+
@Override
55+
protected void configure(ServerBuilder sb) {
56+
sb.http(0);
57+
sb.https(0);
58+
sb.tls(cert.tlsKeyPair());
59+
sb.service("/", (ctx, req) -> HttpResponse.of(HttpStatus.OK));
60+
}
61+
};
62+
63+
// language=YAML
64+
private static final String BOOTSTRAP_TEMPLATE =
65+
"""
66+
static_resources:
67+
listeners:
68+
- name: my-listener
69+
api_listener:
70+
api_listener:
71+
"@type": type.googleapis.com/envoy.extensions.filters.network.\
72+
http_connection_manager.v3.HttpConnectionManager
73+
stat_prefix: http
74+
route_config:
75+
name: local_route
76+
virtual_hosts:
77+
- name: local_service
78+
domains: [ "*" ]
79+
routes:
80+
- match:
81+
prefix: /
82+
route:
83+
cluster: my-cluster
84+
http_filters:
85+
- name: envoy.filters.http.router
86+
typed_config:
87+
"@type": type.googleapis.com/envoy.extensions.filters.\
88+
http.router.v3.Router
89+
clusters:
90+
- name: my-cluster
91+
type: STATIC
92+
lb_policy: ROUND_ROBIN
93+
load_assignment:
94+
cluster_name: my-cluster
95+
endpoints:
96+
- lb_endpoints:
97+
- endpoint:
98+
address:
99+
socket_address:
100+
address: 127.0.0.1
101+
port_value: %d
102+
metadata:
103+
filter_metadata:
104+
"envoy.transport_socket_match":
105+
mode: "tls"
106+
- endpoint:
107+
address:
108+
socket_address:
109+
address: 127.0.0.1
110+
port_value: %d
111+
metadata:
112+
filter_metadata:
113+
"envoy.transport_socket_match":
114+
mode: "plaintext"
115+
transport_socket:
116+
name: envoy.transport_sockets.tls
117+
typed_config:
118+
"@type": type.googleapis.com/envoy.extensions.transport_sockets.\
119+
tls.v3.UpstreamTlsContext
120+
common_tls_context:
121+
validation_context:
122+
trusted_ca:
123+
inline_bytes: %s
124+
transport_socket_matches:
125+
- name: tls-match
126+
match:
127+
mode: "tls"
128+
transport_socket:
129+
name: envoy.transport_sockets.tls
130+
typed_config:
131+
"@type": type.googleapis.com/envoy.extensions.transport_sockets.\
132+
tls.v3.UpstreamTlsContext
133+
common_tls_context:
134+
validation_context:
135+
trusted_ca:
136+
inline_bytes: %s
137+
- name: plaintext-match
138+
match:
139+
mode: "plaintext"
140+
transport_socket:
141+
name: envoy.transport_sockets.raw_buffer
142+
typed_config:
143+
"@type": type.googleapis.com/envoy.extensions.transport_sockets.\
144+
raw_buffer.v3.RawBuffer
145+
""";
146+
147+
@Test
148+
void mixedEndpointsUseCorrectSessionProtocol() throws Exception {
149+
final String ca = base64Cert(cert.certificateFile());
150+
final String yaml = BOOTSTRAP_TEMPLATE.formatted(
151+
server.httpsPort(), server.httpPort(), ca, ca);
152+
153+
try (XdsBootstrap xdsBootstrap = XdsBootstrap.of(XdsResourceReader.fromYaml(yaml));
154+
XdsHttpPreprocessor preprocessor =
155+
XdsHttpPreprocessor.ofListener("my-listener", xdsBootstrap)) {
156+
157+
final BlockingWebClient client = WebClient.of(preprocessor).blocking();
158+
159+
// Send 2 requests (= number of endpoints) via round robin.
160+
// Each request hits a different endpoint.
161+
for (int i = 0; i < 2; i++) {
162+
try (ClientRequestContextCaptor captor = Clients.newContextCaptor()) {
163+
final AggregatedHttpResponse res = client.get("/");
164+
assertThat(res.status()).isEqualTo(HttpStatus.OK);
165+
final ClientRequestContext ctx = captor.get();
166+
final RequestLog log = ctx.log().whenComplete().join();
167+
final SessionProtocol protocol = log.sessionProtocol();
168+
final int port = ctx.endpoint().port();
169+
if (port == server.httpsPort()) {
170+
assertThat(protocol.isTls()).isTrue();
171+
} else {
172+
assertThat(protocol.isTls()).isFalse();
173+
}
174+
}
175+
}
176+
}
177+
}
178+
179+
private static String base64Cert(File certFile) throws Exception {
180+
return Base64.getEncoder().encodeToString(Files.readAllBytes(certFile.toPath()));
181+
}
182+
}

0 commit comments

Comments
 (0)