1use kube::ResourceExt;
2use linkerd_policy_controller_k8s_api as k8s;
3use linkerd_policy_test::{
4 await_condition, await_route_status, create, find_route_condition, mk_route, update,
5 with_temp_ns,
6};
7
8#[tokio::test(flavor = "current_thread")]
9async fn inbound_accepted_parent() {
10 with_temp_ns(|client, ns| async move {
11 // Create a test 'Server'
12 let server_name = "test-accepted-server";
13 let server = k8s::policy::Server {
14 metadata: k8s::ObjectMeta {
15 namespace: Some(ns.to_string()),
16 name: Some(server_name.to_string()),
17 ..Default::default()
18 },
19 spec: k8s::policy::ServerSpec {
20 selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::from_iter(
21 Some(("app", server_name)),
22 )),
23 port: k8s::policy::server::Port::Name("http".to_string()),
24 proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
25 },
26 };
27 let server = create(&client, server).await;
28 let srv_ref = vec![k8s::policy::httproute::ParentReference {
29 group: Some("policy.linkerd.io".to_string()),
30 kind: Some("Server".to_string()),
31 namespace: server.namespace(),
32 name: server.name_unchecked(),
33 section_name: None,
34 port: None,
35 }];
36
37 // Create a route that references the Server resource.
38 let _route = create(&client, mk_route(&ns, "test-accepted-route", Some(srv_ref))).await;
39 // Wait until route is updated with a status
40 let statuses = await_route_status(&client, &ns, "test-accepted-route")
41 .await
42 .parents;
43
44 let route_status = statuses
45 .clone()
46 .into_iter()
47 .find(|route_status| route_status.parent_ref.name == server_name)
48 .expect("must have at least one parent status");
49
50 // Check status references to parent we have created
51 assert_eq!(
52 route_status.parent_ref.group.as_deref(),
53 Some("policy.linkerd.io")
54 );
55 assert_eq!(route_status.parent_ref.kind.as_deref(), Some("Server"));
56
57 // Check status is accepted with a status of 'True'
58 let cond = find_route_condition(&statuses, server_name)
59 .expect("must have at least one 'Accepted' condition for accepted server");
60 assert_eq!(cond.status, "True");
61 assert_eq!(cond.reason, "Accepted")
62 })
63 .await;
64}
65
66#[tokio::test(flavor = "current_thread")]
67async fn inbound_multiple_parents() {
68 with_temp_ns(|client, ns| async move {
69 // Exercise accepted test with a valid, and an invalid parent reference
70 let srv_refs = vec![
71 k8s::policy::httproute::ParentReference {
72 group: Some("policy.linkerd.io".to_string()),
73 kind: Some("Server".to_string()),
74 namespace: Some(ns.clone()),
75 name: "test-valid-server".to_string(),
76 section_name: None,
77 port: None,
78 },
79 k8s::policy::httproute::ParentReference {
80 group: Some("policy.linkerd.io".to_string()),
81 kind: Some("Server".to_string()),
82 namespace: Some(ns.clone()),
83 name: "test-invalid-server".to_string(),
84 section_name: None,
85 port: None,
86 },
87 ];
88
89 // Create only one of the parents
90 let server = k8s::policy::Server {
91 metadata: k8s::ObjectMeta {
92 namespace: Some(ns.to_string()),
93 name: Some("test-valid-server".to_string()),
94 ..Default::default()
95 },
96 spec: k8s::policy::ServerSpec {
97 selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::from_iter(
98 Some(("app", "test-valid-server")),
99 )),
100 port: k8s::policy::server::Port::Name("http".to_string()),
101 proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
102 },
103 };
104 let _server = create(&client, server).await;
105
106 // Create a route that references both parents.
107 let _route = create(
108 &client,
109 mk_route(&ns, "test-multiple-parents-route", Some(srv_refs)),
110 )
111 .await;
112 // Wait until route is updated with a status
113 let parent_status = await_route_status(&client, &ns, "test-multiple-parents-route")
114 .await
115 .parents;
116
117 // Find status for invalid parent and extract the condition
118 let invalid_cond = find_route_condition(&parent_status, "test-invalid-server")
119 .expect("must have at least one 'Accepted' condition set for invalid parent");
120 // Route shouldn't be accepted
121 assert_eq!(invalid_cond.status, "False");
122 assert_eq!(invalid_cond.reason, "NoMatchingParent");
123
124 // Find status for valid parent and extract the condition
125 let valid_cond = find_route_condition(&parent_status, "test-valid-server")
126 .expect("must have at least one 'Accepted' condition set for valid parent");
127 assert_eq!(valid_cond.status, "True");
128 assert_eq!(valid_cond.reason, "Accepted")
129 })
130 .await
131}
132
133#[tokio::test(flavor = "current_thread")]
134async fn inbound_no_parent_ref_patch() {
135 with_temp_ns(|client, ns| async move {
136 // Create a test 'Server'
137 let server_name = "test-accepted-server";
138 let server = k8s::policy::Server {
139 metadata: k8s::ObjectMeta {
140 namespace: Some(ns.to_string()),
141 name: Some(server_name.to_string()),
142 ..Default::default()
143 },
144 spec: k8s::policy::ServerSpec {
145 selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::from_iter(
146 Some(("app", server_name)),
147 )),
148 port: k8s::policy::server::Port::Name("http".to_string()),
149 proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
150 },
151 };
152 let server = create(&client, server).await;
153 let srv_ref = vec![k8s::policy::httproute::ParentReference {
154 group: Some("policy.linkerd.io".to_string()),
155 kind: Some("Server".to_string()),
156 namespace: server.namespace(),
157 name: server.name_unchecked(),
158 section_name: None,
159 port: None,
160 }];
161 // Create a route with a parent reference.
162 let route = create(
163 &client,
164 mk_route(&ns, "test-no-parent-refs-route", Some(srv_ref)),
165 )
166 .await;
167
168 // Status may not be set straight away. To account for that, wrap a
169 // status condition watcher in a timeout.
170 let status = await_route_status(&client, &ns, "test-no-parent-refs-route").await;
171 // If timeout has elapsed, then route did not receive a status patch
172 assert!(
173 status.parents.len() == 1,
174 "HTTPRoute Status should have 1 parent status"
175 );
176
177 // Update route to remove parent_refs
178 let _route = update(&client, mk_route(&ns, "test-no-parent-refs-route", None)).await;
179
180 // Wait for the status to be updated to contain no parent statuses.
181 await_condition::<k8s::policy::HttpRoute>(
182 &client,
183 &ns,
184 &route.name_unchecked(),
185 |obj: Option<&k8s::policy::HttpRoute>| -> bool {
186 obj.and_then(|route| route.status.as_ref())
187 .is_some_and(|status| status.inner.parents.is_empty())
188 },
189 )
190 .await
191 .expect("HTTPRoute Status should have no parent status");
192 })
193 .await
194}
195
196#[tokio::test(flavor = "current_thread")]
197// Tests that inbound routes (routes attached to a `Server`) are properly
198// reconciled when the parentReference changes. Additionally, tests that routes
199// whose parentRefs do not exist are patched with an appropriate status.
200async fn inbound_accepted_reconcile_no_parent() {
201 with_temp_ns(|client, ns| async move {
202 // Given a route with a nonexistent parentReference, we expect to have an
203 // 'Accepted' condition with 'False' as a status.
204 let server_name = "test-reconcile-inbound-server";
205 let srv_ref = vec![k8s::policy::httproute::ParentReference {
206 group: Some("policy.linkerd.io".to_string()),
207 kind: Some("Server".to_string()),
208 namespace: Some(ns.clone()),
209 name: server_name.to_string(),
210 section_name: None,
211 port: None,
212 }];
213 let _route = create(
214 &client,
215 mk_route(&ns, "test-reconcile-inbound-route", Some(srv_ref)),
216 )
217 .await;
218 let route_status = await_route_status(&client, &ns, "test-reconcile-inbound-route").await;
219 let cond = find_route_condition(&route_status.parents, server_name)
220 .expect("must have at least one 'Accepted' condition set for parent");
221 // Test when parent ref does not exist we get Accepted { False }.
222 assert_eq!(cond.status, "False");
223 assert_eq!(cond.reason, "NoMatchingParent");
224
225 // Create the 'Server' that route references and expect it to be picked up
226 // by the index. Consequently, route will have its status reconciled.
227 let server = k8s::policy::Server {
228 metadata: k8s::ObjectMeta {
229 namespace: Some(ns.to_string()),
230 name: Some(server_name.to_string()),
231 ..Default::default()
232 },
233 spec: k8s::policy::ServerSpec {
234 selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::from_iter(
235 Some(("app", server_name)),
236 )),
237 port: k8s::policy::server::Port::Name("http".to_string()),
238 proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
239 },
240 };
241 create(&client, server).await;
242
243 // HTTPRoute may not be patched instantly, await the route condition
244 // status becoming accepted.
245 let _route_status = await_condition(
246 &client,
247 &ns,
248 "test-reconcile-inbound-route",
249 |obj: Option<&k8s::policy::httproute::HttpRoute>| -> bool {
250 tracing::trace!(?obj, "got route status");
251 let status = match obj.and_then(|route| route.status.as_ref()) {
252 Some(status) => status,
253 None => return false,
254 };
255 let cond = match find_route_condition(&status.inner.parents, server_name) {
256 Some(cond) => cond,
257 None => return false,
258 };
259 cond.status == "True" && cond.reason == "Accepted"
260 },
261 )
262 .await
263 .expect("must fetch route")
264 .status
265 .expect("route must contain a status representation");
266 })
267 .await;
268}
269
270#[tokio::test(flavor = "current_thread")]
271async fn inbound_accepted_reconcile_parent_delete() {
272 with_temp_ns(|client, ns| async move {
273 // Attach a route to a Server and expect the route to be patched with an
274 // Accepted status.
275 let server_name = "test-reconcile-delete-server";
276 let server = k8s::policy::Server {
277 metadata: k8s::ObjectMeta {
278 namespace: Some(ns.to_string()),
279 name: Some(server_name.to_string()),
280 ..Default::default()
281 },
282 spec: k8s::policy::ServerSpec {
283 selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::from_iter(
284 Some(("app", server_name)),
285 )),
286 port: k8s::policy::server::Port::Name("http".to_string()),
287 proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
288 },
289 };
290 create(&client, server).await;
291
292 // Create parentReference and route
293 let srv_ref = vec![k8s::policy::httproute::ParentReference {
294 group: Some("policy.linkerd.io".to_string()),
295 kind: Some("Server".to_string()),
296 namespace: Some(ns.clone()),
297 name: server_name.to_string(),
298 section_name: None,
299 port: None,
300 }];
301 let _route = create(
302 &client,
303 mk_route(&ns, "test-reconcile-delete-route", Some(srv_ref)),
304 )
305 .await;
306 let route_status = await_route_status(&client, &ns, "test-reconcile-delete-route").await;
307 let cond = find_route_condition(&route_status.parents, server_name)
308 .expect("must have at least one 'Accepted' condition");
309 assert_eq!(cond.status, "True");
310 assert_eq!(cond.reason, "Accepted");
311
312 // Delete Server
313 let api: kube::Api<k8s::policy::Server> = kube::Api::namespaced(client.clone(), &ns);
314 api.delete(
315 "test-reconcile-delete-server",
316 &kube::api::DeleteParams::default(),
317 )
318 .await
319 .expect("API delete request failed");
320
321 // HTTPRoute may not be patched instantly, await the route condition
322 // becoming NoMatchingParent.
323 let _route_status = await_condition(
324 &client,
325 &ns,
326 "test-reconcile-delete-route",
327 |obj: Option<&k8s::policy::httproute::HttpRoute>| -> bool {
328 tracing::trace!(?obj, "got route status");
329 let status = match obj.and_then(|route| route.status.as_ref()) {
330 Some(status) => status,
331 None => return false,
332 };
333 let cond = match find_route_condition(&status.inner.parents, server_name) {
334 Some(cond) => cond,
335 None => return false,
336 };
337 cond.status == "False" && cond.reason == "NoMatchingParent"
338 },
339 )
340 .await
341 .expect("must fetch route")
342 .status
343 .expect("route must contain a status representation");
344 })
345 .await;
346}
View as plain text