1use k8s::Condition;
2use k8s_gateway_api::{ParentReference, RouteParentStatus, RouteStatus};
3use k8s_openapi::chrono::Utc;
4use kube::ResourceExt;
5use linkerd_policy_controller_core::POLICY_CONTROLLER_NAME;
6use linkerd_policy_controller_k8s_api as k8s;
7use linkerd_policy_test::{
8 await_condition, await_route_status, create, find_route_condition, mk_route, with_temp_ns,
9};
10
11#[tokio::test(flavor = "current_thread")]
12async fn accepted_parent() {
13 with_temp_ns(|client, ns| async move {
14 // Create a parent Service
15 let svc_name = "test-service";
16 let svc = k8s::Service {
17 metadata: k8s::ObjectMeta {
18 namespace: Some(ns.clone()),
19 name: Some(svc_name.to_string()),
20 ..Default::default()
21 },
22 spec: Some(k8s::ServiceSpec {
23 type_: Some("ClusterIP".to_string()),
24 ports: Some(vec![k8s::ServicePort {
25 port: 80,
26 ..Default::default()
27 }]),
28 ..Default::default()
29 }),
30 ..k8s::Service::default()
31 };
32 let svc = create(&client, svc).await;
33 let svc_ref = vec![k8s::policy::httproute::ParentReference {
34 group: Some("core".to_string()),
35 kind: Some("Service".to_string()),
36 namespace: svc.namespace(),
37 name: svc.name_unchecked(),
38 section_name: None,
39 port: Some(80),
40 }];
41
42 // Create a route that references the Service resource.
43 let _route = create(&client, mk_route(&ns, "test-route", Some(svc_ref))).await;
44 // Wait until route is updated with a status
45 let statuses = await_route_status(&client, &ns, "test-route").await.parents;
46
47 let route_status = statuses
48 .clone()
49 .into_iter()
50 .find(|route_status| route_status.parent_ref.name == svc_name)
51 .expect("must have at least one parent status");
52
53 // Check status references to parent we have created
54 assert_eq!(route_status.parent_ref.group.as_deref(), Some("core"));
55 assert_eq!(route_status.parent_ref.kind.as_deref(), Some("Service"));
56
57 // Check status is accepted with a status of 'True'
58 let cond = find_route_condition(&statuses, svc_name)
59 .expect("must have at least one 'Accepted' condition for accepted servuce");
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 no_cluster_ip() {
68 with_temp_ns(|client, ns| async move {
69 // Create a parent Service
70 let svc = k8s::Service {
71 metadata: k8s::ObjectMeta {
72 namespace: Some(ns.clone()),
73 name: Some("test-service".to_string()),
74 ..Default::default()
75 },
76 spec: Some(k8s::ServiceSpec {
77 cluster_ip: Some("None".to_string()),
78 type_: Some("ClusterIP".to_string()),
79 ports: Some(vec![k8s::ServicePort {
80 port: 80,
81 ..Default::default()
82 }]),
83 ..Default::default()
84 }),
85 ..k8s::Service::default()
86 };
87 let svc = create(&client, svc).await;
88 let svc_ref = vec![k8s::policy::httproute::ParentReference {
89 group: Some("core".to_string()),
90 kind: Some("Service".to_string()),
91 namespace: svc.namespace(),
92 name: svc.name_unchecked(),
93 section_name: None,
94 port: Some(80),
95 }];
96
97 // Create a route that references the Service resource.
98 let _route = create(&client, mk_route(&ns, "test-route", Some(svc_ref))).await;
99 // Wait until route is updated with a status
100 let status = await_route_status(&client, &ns, "test-route").await;
101 let cond = find_route_condition(&status.parents, "test-service")
102 .expect("must have at least one 'Accepted' condition set for parent");
103 // Parent with no ClusterIP should not match.
104 assert_eq!(cond.status, "False");
105 assert_eq!(cond.reason, "NoMatchingParent");
106 })
107 .await;
108}
109
110#[tokio::test(flavor = "current_thread")]
111async fn external_name() {
112 with_temp_ns(|client, ns| async move {
113 // Create a parent Service
114 let svc = k8s::Service {
115 metadata: k8s::ObjectMeta {
116 namespace: Some(ns.clone()),
117 name: Some("test-service".to_string()),
118 ..Default::default()
119 },
120 spec: Some(k8s::ServiceSpec {
121 type_: Some("ExternalName".to_string()),
122 external_name: Some("linkerd.io".to_string()),
123 ports: Some(vec![k8s::ServicePort {
124 port: 80,
125 ..Default::default()
126 }]),
127 ..Default::default()
128 }),
129 ..k8s::Service::default()
130 };
131 let svc = create(&client, svc).await;
132 let svc_ref = vec![k8s::policy::httproute::ParentReference {
133 group: Some("core".to_string()),
134 kind: Some("Service".to_string()),
135 namespace: svc.namespace(),
136 name: svc.name_unchecked(),
137 section_name: None,
138 port: Some(80),
139 }];
140
141 // Create a route that references the Service resource.
142 let _route = create(&client, mk_route(&ns, "test-route", Some(svc_ref))).await;
143 // Wait until route is updated with a status
144 let status = await_route_status(&client, &ns, "test-route").await;
145 let cond = find_route_condition(&status.parents, "test-service")
146 .expect("must have at least one 'Accepted' condition set for parent");
147 // Parent with ExternalName should not match.
148 assert_eq!(cond.status, "False");
149 assert_eq!(cond.reason, "NoMatchingParent");
150 })
151 .await;
152}
153
154#[tokio::test(flavor = "current_thread")]
155async fn multiple_statuses() {
156 with_temp_ns(|client, ns| async move {
157 // Create a parent Service
158 let svc_name = "test-service";
159 let svc = k8s::Service {
160 metadata: k8s::ObjectMeta {
161 namespace: Some(ns.clone()),
162 name: Some(svc_name.to_string()),
163 ..Default::default()
164 },
165 spec: Some(k8s::ServiceSpec {
166 type_: Some("ClusterIP".to_string()),
167 ports: Some(vec![k8s::ServicePort {
168 port: 80,
169 ..Default::default()
170 }]),
171 ..Default::default()
172 }),
173 ..k8s::Service::default()
174 };
175 let svc = create(&client, svc).await;
176 let svc_ref = vec![k8s::policy::httproute::ParentReference {
177 group: Some("core".to_string()),
178 kind: Some("Service".to_string()),
179 namespace: svc.namespace(),
180 name: svc.name_unchecked(),
181 section_name: None,
182 port: Some(80),
183 }];
184
185 // Create a route that references the Service resource.
186 let _route = create(&client, mk_route(&ns, "test-route", Some(svc_ref))).await;
187
188 // Patch a status onto the HttpRoute.
189 let value = serde_json::json!({
190 "apiVersion": "policy.linkerd.io",
191 "kind": "HTTPRoute",
192 "name": "test-route",
193 "status": k8s::policy::httproute::HttpRouteStatus {
194 inner: RouteStatus {
195 parents: vec![RouteParentStatus {
196 conditions: vec![Condition {
197 last_transition_time: k8s::Time(Utc::now()),
198 message: "".to_string(),
199 observed_generation: None,
200 reason: "Accepted".to_string(),
201 status: "True".to_string(),
202 type_: "Accepted".to_string(),
203 }],
204 controller_name: "someone/else".to_string(),
205 parent_ref: ParentReference {
206 group: Some("gateway.networking.k8s.io".to_string()),
207 name: "foo".to_string(),
208 kind: Some("Gateway".to_string()),
209 namespace: Some("bar".to_string()),
210 port: None,
211 section_name: None,
212 },
213 }],
214 },
215 },
216 });
217 let patch = k8s::Patch::Merge(value);
218 let patch_params = k8s::PatchParams::apply("someone/else");
219 let api = k8s::Api::<k8s::policy::HttpRoute>::namespaced(client.clone(), &ns);
220 api.patch_status("test-route", &patch_params, &patch)
221 .await
222 .expect("failed to patch status");
223
224 await_condition(
225 &client,
226 &ns,
227 "test-route",
228 |obj: Option<&k8s::policy::HttpRoute>| -> bool {
229 obj.and_then(|route| route.status.as_ref())
230 .map(|status| {
231 let statuses = &status.inner.parents;
232
233 let other_status_found = statuses
234 .iter()
235 .any(|route_status| route_status.controller_name == "someone/else");
236
237 let linkerd_status_found = statuses.iter().any(|route_status| {
238 route_status.controller_name == POLICY_CONTROLLER_NAME
239 });
240
241 other_status_found && linkerd_status_found
242 })
243 .unwrap_or(false)
244 },
245 )
246 .await
247 .expect("must have both statuses");
248 })
249 .await;
250}
View as plain text